diff --git a/apps/server/src/mcp/OrchestratorMcpService.test.ts b/apps/server/src/mcp/OrchestratorMcpService.test.ts index b524921f02a6..46da6b2ef426 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.test.ts @@ -2219,6 +2219,7 @@ describe("OrchestratorMcpService provider resolution", () => { .updateScheduledTask(supervisedClient, { scheduledTaskId: bound.id, prompt: "Something else", + target: { model: "gpt-5.4-mini" }, }) .pipe(Effect.flip); assert.equal(error.code, "runtime_mode_escalation_denied"); @@ -2279,6 +2280,330 @@ describe("OrchestratorMcpService provider resolution", () => { ); }); + describe("scheduled task targets", () => { + const miniModel = "gpt-5.4-mini"; + const callerSelection = { + instanceId: codexInstanceId, + model: "gpt-5.4", + options: [{ id: "reasoningEffort", value: "high" }], + }; + const stored: ScheduledTask = { + id: ScheduledTaskId.make("scheduled-task:triage"), + title: "Triage", + prompt: "Triage {{body.issue.title}}", + enabled: true, + schedule: { type: "webhook", signature: null }, + projectId, + threadId: null, + workspaceStrategy: { type: "root" }, + modelSelection: callerSelection, + runtimeMode: "full-access", + interactionMode: "default", + createdBy: "agent", + creationSource: "mcp", + createdAt: "2026-10-05T00:00:00.000Z", + updatedAt: "2026-10-05T00:00:00.000Z", + nextRunAt: null, + lastRunAt: null, + lastRunStatus: "never", + lastRunError: null, + runCount: 0, + }; + + const codex: ServerProvider = { + ...providerSnapshot({ + instanceId: codexInstanceId, + driver: ProviderDriverKind.make("codex"), + }), + models: [callerSelection.model, miniModel].map((slug) => ({ + slug, + name: slug, + isCustom: false, + capabilities: { + optionDescriptors: [ + { + id: "reasoningEffort", + label: "Reasoning effort", + type: "select", + options: [ + { id: "low", label: "Low" }, + { id: "high", label: "High" }, + ], + }, + ], + }, + })), + }; + const peer: ServerProvider = { + ...providerSnapshot({ + instanceId: antigravityInstanceId, + driver: ProviderDriverKind.make("antigravity"), + }), + models: codex.models.toReversed(), + supportedRuntimeModes: ["approval-required", "full-access"], + }; + const client: McpInvocationScope = { + ...scope, + thread: undefined, + client: { sessionId: "scheduler", label: "Test", access: "full-access" }, + }; + const createInput = { prompt: "Triage", schedule: stored.schedule, bindToCurrentThread: false }; + + const setup = Effect.fnUntraced(function* ( + runtimeMode: ScheduledTask["runtimeMode"] = "full-access", + ) { + const saved = yield* Ref.make>([]); + const providers = yield* Ref.make>([codex, peer]); + const enabled = yield* Ref.make(true); + const reads = yield* Ref.make(0); + const probes = yield* Ref.make>([]); + const initial = { ...stored, runtimeMode }; + const parent = parentProjection([], callerSelection); + const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe( + Effect.provide( + layerOrchestratorService.pipe( + Layer.provide( + Layer.mergeAll( + NodeServices.layer, + ServerSettings.layerTest(), + Layer.mock(ThreadManagementService.ThreadManagementService)({ + getThreadRecords: () => + Effect.succeed({ + ...parent, + thread: { ...parent.thread, runtimeMode }, + }), + isProviderInstanceEnabledForProject: () => Ref.get(enabled), + }), + Layer.mock(ProviderRegistry.ProviderRegistry)({ + getProviders: Ref.update(reads, (count) => count + 1).pipe( + Effect.andThen(Ref.get(providers)), + ), + refreshInstance: (id) => + Ref.update(probes, (ids) => [...ids, id]).pipe(Effect.as([codex, peer])), + }), + adapterRegistryLayer([codexInstanceId, antigravityInstanceId]), + Layer.mock(ProjectService.ProjectService)({ + getById: () => + Effect.succeedSome({ + id: projectId, + defaultModelSelection: { instanceId: codexInstanceId, model: miniModel }, + } as never), + }), + Layer.mock(SecretRequests.SecretRequests)({}), + Layer.mock(ScheduledTaskService.ScheduledTaskService)({ + list: () => + Ref.get(saved).pipe( + Effect.map((tasks) => ({ + tasks: [tasks.at(-1) ?? initial], + })), + ), + upsert: (input) => { + const task = { + ...initial, + ...input, + id: input.id ?? initial.id, + threadId: input.threadId ?? null, + createdBy: input.createdBy ?? initial.createdBy, + creationSource: input.creationSource ?? initial.creationSource, + schedule: + input.schedule.type === "webhook" + ? { ...input.schedule, signature: input.schedule.signature ?? null } + : input.schedule, + }; + return Ref.update(saved, (tasks) => [...tasks, task]).pipe(Effect.as({ task })); + }, + }), + ), + ), + ), + ), + ); + return { mcp, saved, providers, enabled, reads, probes }; + }); + + it.effect("preserves legacy selections and modes without consulting providers", () => + Effect.gen(function* () { + const { mcp, saved, providers, reads, probes } = yield* setup("auto"); + yield* Ref.set(providers, []); + yield* mcp.scheduleTask(scope, createInput); + yield* mcp.updateScheduledTask(scope, { scheduledTaskId: stored.id, title: "Renamed" }); + yield* mcp.scheduleTask(client, { ...createInput, projectId }); + yield* mcp.updateScheduledTask(client, { scheduledTaskId: stored.id, enabled: false }); + assert.deepEqual( + (yield* Ref.get(saved)).map((task) => task.modelSelection), + [ + callerSelection, + callerSelection, + { instanceId: codexInstanceId, model: miniModel }, + { instanceId: codexInstanceId, model: miniModel }, + ], + ); + assert.deepEqual( + (yield* Ref.get(saved)).map((task) => task.runtimeMode), + ["auto", "auto", "full-access", "full-access"], + ); + assert.equal(yield* Ref.get(reads), 0); + assert.deepEqual(yield* Ref.get(probes), []); + }), + ); + + it.effect( + "saves targeted selections and reports them in create, update and list summaries", + () => + Effect.gen(function* () { + const { mcp, saved } = yield* setup(); + const created = yield* mcp.scheduleTask(scope, { + ...createInput, + target: { model: miniModel }, + }); + assert.containSubset(created, { + providerInstanceId: codexInstanceId, + model: miniModel, + runtimeMode: "full-access", + interactionMode: "default", + }); + assert.isUndefined(created.options); + const options = [{ id: "reasoningEffort", value: "low" }]; + const updated = yield* mcp.updateScheduledTask(scope, { + scheduledTaskId: stored.id, + target: { options }, + }); + const summary = { + providerInstanceId: codexInstanceId, + model: miniModel, + options, + runtimeMode: "full-access", + interactionMode: "default", + }; + assert.containSubset(updated, summary); + assert.containSubset((yield* mcp.listScheduledTasks(scope, {})).tasks, [summary]); + yield* mcp.updateScheduledTask(scope, { + scheduledTaskId: stored.id, + target: { model: callerSelection.model }, + }); + yield* mcp.scheduleTask(client, { ...createInput, projectId, target: { options } }); + assert.deepEqual( + (yield* Ref.get(saved)).map((task) => task.modelSelection), + [ + { instanceId: codexInstanceId, model: miniModel }, + { instanceId: codexInstanceId, model: miniModel, options }, + { instanceId: codexInstanceId, model: callerSelection.model }, + { instanceId: codexInstanceId, model: miniModel, options }, + ], + ); + }), + ); + + it.effect( + "keeps an advertised stored model on provider-only updates and refuses incompatible providers", + () => + Effect.gen(function* () { + const { mcp, saved, providers } = yield* setup(); + const moved = yield* mcp.updateScheduledTask(scope, { + scheduledTaskId: stored.id, + target: { providerInstanceId: peer.instanceId }, + }); + assert.equal(moved.model, callerSelection.model); + assert.isUndefined(moved.options); + yield* Ref.set(providers, [{ ...codex, models: [codex.models[1]!] }, peer]); + const error = yield* mcp + .updateScheduledTask(scope, { + scheduledTaskId: stored.id, + target: { providerInstanceId: codexInstanceId }, + }) + .pipe(Effect.flip); + assert.equal(error.code, "model_unavailable"); + yield* Ref.set(providers, [{ ...codex, models: [] }, peer]); + const unadvertised = yield* mcp + .updateScheduledTask(scope, { + scheduledTaskId: stored.id, + target: { providerInstanceId: codexInstanceId }, + }) + .pipe(Effect.flip); + assert.equal(unadvertised.code, "model_unavailable"); + assert.deepEqual( + (yield* Ref.get(saved)).map((task) => task.modelSelection), + [{ instanceId: peer.instanceId, model: callerSelection.model }], + ); + }), + ); + + it.effect("validates targets before writing and re-probes an unavailable provider once", () => + Effect.gen(function* () { + const { mcp, saved, providers, enabled, probes } = yield* setup(); + const unknownModel = yield* mcp + .scheduleTask(scope, { + ...createInput, + target: { model: "unknown-model" }, + }) + .pipe(Effect.flip); + assert.equal(unknownModel.code, "model_unavailable"); + const invalidOptions = yield* mcp + .updateScheduledTask(scope, { + scheduledTaskId: stored.id, + target: { options: [{ id: "reasoningEffort", value: "invalid" }] }, + }) + .pipe(Effect.flip); + assert.equal(invalidOptions.code, "invalid_request"); + yield* Ref.set(enabled, false); + const disabled = yield* mcp + .updateScheduledTask(scope, { + scheduledTaskId: stored.id, + target: { model: miniModel }, + }) + .pipe(Effect.flip); + assert.equal(disabled.code, "invalid_request"); + yield* Ref.set(enabled, true); + const noAdapter = { ...peer, instanceId: ProviderInstanceId.make("no-adapter") }; + yield* Ref.set(providers, [codex, noAdapter]); + const unavailable = yield* mcp + .scheduleTask(scope, { + ...createInput, + target: { providerInstanceId: noAdapter.instanceId }, + }) + .pipe(Effect.flip); + assert.equal(unavailable.code, "provider_unavailable"); + assert.deepEqual(yield* Ref.get(saved), []); + assert.deepEqual(yield* Ref.get(probes), [noAdapter.instanceId]); + yield* Ref.set(providers, [{ ...peer, installed: false }]); + const recovered = yield* mcp.scheduleTask(scope, { + ...createInput, + target: { providerInstanceId: peer.instanceId }, + }); + assert.equal(recovered.model, miniModel); + assert.equal(recovered.providerInstanceId, peer.instanceId); + assert.deepEqual(yield* Ref.get(probes), [noAdapter.instanceId, peer.instanceId]); + assert.equal((yield* Ref.get(saved)).length, 1); + }), + ); + + it.effect("steps down an inherited runtime when creating a task on another provider", () => + Effect.gen(function* () { + const { mcp, saved } = yield* setup("auto"); + const result = yield* mcp.scheduleTask(scope, { + ...createInput, + target: { providerInstanceId: peer.instanceId }, + }); + assert.equal((yield* Ref.get(saved))[0]?.runtimeMode, "approval-required"); + assert.equal(result.runtimeMode, "approval-required"); + assert.equal(result.interactionMode, "default"); + }), + ); + + it.effect("steps down a stored runtime when updating a task to another provider", () => + Effect.gen(function* () { + const { mcp, saved } = yield* setup("auto"); + const result = yield* mcp.updateScheduledTask(client, { + scheduledTaskId: stored.id, + target: { providerInstanceId: peer.instanceId }, + }); + assert.equal((yield* Ref.get(saved))[0]?.runtimeMode, "approval-required"); + assert.equal(result.runtimeMode, "approval-required"); + assert.equal(result.interactionMode, "default"); + }), + ); + }); + describe("resolveRuntimeMode", () => { const muse = { modelSelection: { instanceId: ProviderInstanceId.make("muse"), model: "muse-model" }, diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts index 86b54b238a16..7141a78db0b1 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.ts @@ -123,6 +123,7 @@ const DEFAULT_THREAD_ITEM_MAX_CHARS = 20_000; interface ResolvedTarget { readonly modelSelection: ModelSelection; + readonly advertisedModels: ServerProvider["models"]; readonly supportedRuntimeModes: ReadonlyArray | undefined; } @@ -274,6 +275,11 @@ function scheduledTaskSummary(task: ScheduledTask, mayRun: boolean): Orchestrato schedule: task.schedule, nextRunAt: task.nextRunAt, lastRunStatus: task.lastRunStatus, + providerInstanceId: task.modelSelection.instanceId, + model: task.modelSelection.model, + ...(task.modelSelection.options === undefined ? {} : { options: task.modelSelection.options }), + runtimeMode: task.runtimeMode, + interactionMode: task.interactionMode, ...(task.webhook?.url == null || !mayRun ? {} : { webhookUrl: task.webhook.url }), ...(task.webhook === undefined ? {} @@ -1196,7 +1202,12 @@ const make = Effect.gen(function* () { }; const resolveTarget = (input: { - readonly parent: Pick; + readonly parent: { + readonly thread: Pick< + OrchestrationV2ThreadProjection["thread"], + "projectId" | "modelSelection" + >; + }; readonly target: OrchestratorMcpTarget | undefined; readonly providers: ReadonlyArray; /** @@ -1330,10 +1341,39 @@ const make = Effect.gen(function* () { : requestedOptions === undefined ? { instanceId, model } : { instanceId, model, options: requestedOptions }, + advertisedModels: provider.models, supportedRuntimeModes: provider.supportedRuntimeModes, }; }); + const resolveScheduledTaskTarget = Effect.fnUntraced(function* (input: { + readonly projectId: ProjectId; + readonly inherited: ModelSelection; + readonly target: OrchestratorMcpTarget; + }) { + const target = yield* resolveTargetRechecking({ + parent: { thread: { projectId: input.projectId, modelSelection: input.inherited } }, + target: input.target, + providers: yield* loadProviders, + }); + // Driver-only resolution already filters candidates by project enablement. + if ( + (input.target.providerInstanceId !== undefined || input.target.driverKind === undefined) && + !(yield* threadManagement.isProviderInstanceEnabledForProject({ + projectId: input.projectId, + instanceId: target.modelSelection.instanceId, + })) + ) { + return yield* threadManagementFailure( + new ThreadManagementService.ThreadManagementProviderInstanceDisabledError({ + projectId: input.projectId, + instanceId: target.modelSelection.instanceId, + }), + ); + } + return target; + }); + const requestKey = (clientRequestId: string | undefined): Effect.Effect => clientRequestId === undefined ? crypto.randomUUIDv4().pipe(Effect.orDie) @@ -1655,8 +1695,20 @@ const make = Effect.gen(function* () { : "bindToCurrentThread binds to this thread, which belongs to a different project.", ); } - const modelSelection = + const inherited = parent?.thread.modelSelection ?? (yield* projectDefaultModelSelection(project)); + const target = + input.target === undefined + ? undefined + : yield* resolveScheduledTaskTarget({ + projectId, + inherited, + target: input.target, + }); + const runtimeMode = + target === undefined + ? limits.runtimeMode + : yield* resolveRuntimeMode(limits.runtimeMode, undefined, target); const derivedTitle = input.prompt.split("\n")[0]?.trim() ?? ""; const title = input.title ?? (derivedTitle.length > 0 ? derivedTitle.slice(0, 80) : "Scheduled task"); @@ -1668,8 +1720,8 @@ const make = Effect.gen(function* () { projectId, threadId: bindToCurrentThread && parent !== undefined ? parent.thread.id : null, workspaceStrategy: scheduledTaskWorkspaceStrategy(bindToCurrentThread), - modelSelection, - runtimeMode: limits.runtimeMode, + modelSelection: target?.modelSelection ?? inherited, + runtimeMode, interactionMode: limits.interactionMode, createdBy: "agent", creationSource: "mcp", @@ -1734,6 +1786,32 @@ const make = Effect.gen(function* () { : input.bindToCurrentThread && parent !== undefined ? parent.thread.id : null; + const target = + input.target === undefined + ? undefined + : yield* resolveScheduledTaskTarget({ + projectId: existing.projectId, + inherited: existing.modelSelection, + target: { + ...input.target, + model: input.target.model ?? existing.modelSelection.model, + }, + }); + if ( + target !== undefined && + input.target?.model === undefined && + target.modelSelection.instanceId !== existing.modelSelection.instanceId && + !target.advertisedModels.some((model) => model.slug === existing.modelSelection.model) + ) { + return yield* failure( + "model_unavailable", + `Model ${existing.modelSelection.model} is not advertised by provider ${target.modelSelection.instanceId}; name a model in target to switch providers.`, + ); + } + const runtimeMode = + target === undefined + ? existing.runtimeMode + : yield* resolveRuntimeMode(existing.runtimeMode, undefined, target); // Rebinding changes where runs execute, so the workspace strategy must // follow: unbinding a root-strategy task would otherwise run loose // prompts in the shared project checkout. @@ -1750,8 +1828,8 @@ const make = Effect.gen(function* () { projectId: existing.projectId, threadId, workspaceStrategy, - modelSelection: existing.modelSelection, - runtimeMode: existing.runtimeMode, + modelSelection: target?.modelSelection ?? existing.modelSelection, + runtimeMode, interactionMode: existing.interactionMode, createdBy: existing.createdBy, creationSource: existing.creationSource, diff --git a/apps/server/src/mcp/toolkits/orchestrator/tools.ts b/apps/server/src/mcp/toolkits/orchestrator/tools.ts index f0dabc0260e3..56565d1560a9 100644 --- a/apps/server/src/mcp/toolkits/orchestrator/tools.ts +++ b/apps/server/src/mcp/toolkits/orchestrator/tools.ts @@ -116,7 +116,7 @@ const WorkflowStopTool = Tool.make("t3_workflow_stop", { export const ScheduleTaskTool = Tool.make("schedule_task", { description: - "Create persistent work in the app scheduler that runs even when no turn is active. Pass schedule as a STRUCTURED OBJECT, never JSON text. Timers: {type:'interval', everyMs:3600000} is hourly; {type:'fixed_time', timeOfDay:'09:00', weekdays:[1,2,3,4,5]} is weekday mornings; report the returned nextRunAt. Webhooks: {type:'webhook'} runs once per request to a generated URL. The run sees the request ONLY through prompt placeholders: {{body.path}} (e.g. {{body.action}}, {{body.release.tag_name}}), {{headers.name}}, {{query.name}}, {{body}}, or {{request}} (method, headers with credentials redacted, and body). Each value arrives framed as untrusted request data, and $skill, @file or /command mentions inside it do not act; without a signature, anyone with the URL can trigger a run. For a sender that signs requests, first call request_secret so the user enters the secret privately (never ask for it in chat or invent one), then set signature with the returned secretRef, e.g. GitHub: {type:'webhook', signature:{header:'x-hub-signature-256', encoding:'hex', prefix:'sha256=', secretRef}}. The result's webhookUrl is the public URL to give the user; if it is absent, this environment has no T3 Connect managed tunnel, so tell the user to enable T3 Connect remote access rather than sharing a path. Omit projectId for this thread's project. In this thread's project, runs post into THIS thread by default (bindToCurrentThread=true), which suits an orchestrator that sees every trigger, delegates work, and can dedupe against what is in flight; use false only when the user wants a fresh top-level thread per run. Elsewhere each run launches a fresh thread. Provider, model, and runtime settings inherit from this thread, or from the project default when there is no calling thread.", + "Create persistent work in the app scheduler that runs even when no turn is active. Pass schedule as a STRUCTURED OBJECT, never JSON text. Timers: {type:'interval', everyMs:3600000} is hourly; {type:'fixed_time', timeOfDay:'09:00', weekdays:[1,2,3,4,5]} is weekday mornings; report the returned nextRunAt. Webhooks: {type:'webhook'} runs once per request to a generated URL. The run sees the request ONLY through prompt placeholders: {{body.path}} (e.g. {{body.action}}, {{body.release.tag_name}}), {{headers.name}}, {{query.name}}, {{body}}, or {{request}} (method, headers with credentials redacted, and body). Each value arrives framed as untrusted request data, and $skill, @file or /command mentions inside it do not act; without a signature, anyone with the URL can trigger a run. For a sender that signs requests, first call request_secret so the user enters the secret privately (never ask for it in chat or invent one), then set signature with the returned secretRef, e.g. GitHub: {type:'webhook', signature:{header:'x-hub-signature-256', encoding:'hex', prefix:'sha256=', secretRef}}. The result's webhookUrl is the public URL to give the user; if it is absent, this environment has no T3 Connect managed tunnel, so tell the user to enable T3 Connect remote access rather than sharing a path. Omit projectId for this thread's project. In this thread's project, runs post into THIS thread by default (bindToCurrentThread=true), which suits an orchestrator that sees every trigger, delegates work, and can dedupe against what is in flight; use false only when the user wants a fresh top-level thread per run. Elsewhere each run launches a fresh thread. Provider, model, and runtime settings are copied once from this thread. With no calling thread, provider and model come from the project default and the runtime mode from this client's permission ceiling. Pass target (provider instance, model, and options from orchestrator_capabilities) to run the task on a different model, e.g. a cheaper one for triage; options carry over only when the provider and model stay the same. With target, the inherited runtime steps down when the provider does not support it. Target applies to fresh threads; bound runs follow their thread's current settings.", parameters: OrchestratorMcpScheduleTaskInput, success: OrchestratorMcpScheduleTaskResult, failure: OrchestratorMcpFailure, @@ -130,7 +130,7 @@ export const ScheduleTaskTool = Tool.make("schedule_task", { const ListScheduledTasksTool = Tool.make("list_scheduled_tasks", { parameters: OrchestratorMcpListScheduledTasksInput, description: - "List recurring scheduled tasks in a project (omit projectId for the calling thread's project, or every project when there is no calling thread), including their id, schedule, prompt, enabled state, bound thread, next run time, and last run status. Use the returned scheduledTaskId with update_scheduled_task or delete_scheduled_task.", + "List recurring scheduled tasks in a project (omit projectId for the calling thread's project, or every project when there is no calling thread), including their id, schedule, prompt, enabled state, bound thread, provider, model, options, modes, next run time, and last run status. Use the returned scheduledTaskId with update_scheduled_task or delete_scheduled_task.", success: OrchestratorMcpListScheduledTasksResult, failure: OrchestratorMcpFailure, failureMode: "return", @@ -143,7 +143,7 @@ const ListScheduledTasksTool = Tool.make("list_scheduled_tasks", { const UpdateScheduledTaskTool = Tool.make("update_scheduled_task", { description: - "Update an existing scheduled task by scheduledTaskId (from list_scheduled_tasks). Only the provided fields change; omit a field to leave it as-is. Use enabled=false to pause a task without deleting it. Set bindToCurrentThread to move the task between posting into this thread and launching a fresh thread per run.", + "Update an existing scheduled task by scheduledTaskId (from list_scheduled_tasks). Only the provided fields change; omit a field to leave it as-is. Use enabled=false to pause a task without deleting it. Set bindToCurrentThread to move the task between posting into this thread and launching a fresh thread per run. Set target to change the provider instance, model, or options its runs use; omitted fields inherit the task's stored selection. A provider-only change keeps the stored model if the destination advertises it, otherwise it fails with model_unavailable. Options reset when the provider or model changes. With target, the stored runtime steps down when the provider does not support it. Target applies to fresh threads; bound runs follow their thread's current settings.", parameters: OrchestratorMcpUpdateScheduledTaskInput, success: OrchestratorMcpScheduleTaskResult, failure: OrchestratorMcpFailure, diff --git a/packages/contracts/src/orchestratorMcp.ts b/packages/contracts/src/orchestratorMcp.ts index 778712ad2399..45ec032cada4 100644 --- a/packages/contracts/src/orchestratorMcp.ts +++ b/packages/contracts/src/orchestratorMcp.ts @@ -546,6 +546,12 @@ export const OrchestratorMcpScheduleTaskInput = Schema.Struct({ "True (default) posts each run into this thread; false creates a fresh top-level thread per run.", }), ), + target: Schema.optional( + OrchestratorMcpTarget.annotate({ + description: + "Provider, model, and options for runs that launch a fresh thread. Omit to inherit the calling thread's selection, or the project default when there is no calling thread. Options carry over only when the provider and model stay the same.", + }), + ), clientRequestId: Schema.optional(OrchestratorMcpClientRequestId), }); export type OrchestratorMcpScheduleTaskInput = typeof OrchestratorMcpScheduleTaskInput.Type; @@ -560,6 +566,11 @@ export const OrchestratorMcpScheduledTask = Schema.Struct({ schedule: ScheduledTaskSchedule, nextRunAt: Schema.NullOr(IsoDateTime), lastRunStatus: ScheduledTaskRunStatus, + providerInstanceId: ProviderInstanceId, + model: Schema.String, + options: Schema.optional(Schema.Array(ProviderOptionSelection)), + runtimeMode: RuntimeMode, + interactionMode: ProviderInteractionMode, webhookUrl: Schema.optional(Schema.String).annotate({ description: "Public URL to give the sender. Absent when this environment has no T3 Connect managed tunnel; the user must enable T3 Connect remote access first.", @@ -597,6 +608,12 @@ export const OrchestratorMcpUpdateScheduledTaskInput = Schema.Struct({ schedule: Schema.optional(OrchestratorMcpSchedule), enabled: Schema.optional(Schema.Boolean), bindToCurrentThread: Schema.optional(Schema.Boolean), + target: Schema.optional( + OrchestratorMcpTarget.annotate({ + description: + "Change the provider, model, or options for runs that launch a fresh thread. Omitted fields inherit the stored selection. A provider-only change keeps the stored model if the destination advertises it, otherwise it fails with model_unavailable. Options carry over only when the provider and model stay the same.", + }), + ), }); export type OrchestratorMcpUpdateScheduledTaskInput = typeof OrchestratorMcpUpdateScheduledTaskInput.Type;