Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 186 additions & 3 deletions apps/server/src/mcp/OrchestratorMcpService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,15 +396,22 @@ describe("OrchestratorMcpService provider resolution", () => {
}),
);

const parentProjection = (subagents: ReadonlyArray<unknown>): OrchestrationV2ThreadProjection =>
const parentProjection = (
subagents: ReadonlyArray<unknown>,
modelSelection: {
readonly instanceId: ProviderInstanceId;
readonly model: string;
readonly options?: ReadonlyArray<{ readonly id: string; readonly value: unknown }>;
} = { instanceId: codexInstanceId, model: "gpt-5.4" },
): OrchestrationV2ThreadProjection =>
({
thread: {
id: parentThreadId,
projectId,
title: "MCP parent",
createdBy: "user",
creationSource: "web",
modelSelection: { instanceId: codexInstanceId, model: "gpt-5.4" },
modelSelection,
runtimeMode: "full-access",
interactionMode: "default",
},
Expand All @@ -415,7 +422,7 @@ describe("OrchestratorMcpService provider resolution", () => {
status: "running",
rootNodeId: parentNodeId,
providerInstanceId: codexInstanceId,
modelSelection: { instanceId: codexInstanceId, model: "gpt-5.4" },
modelSelection,
},
],
contextTransfers: [],
Expand Down Expand Up @@ -784,4 +791,180 @@ describe("OrchestratorMcpService provider resolution", () => {
}).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));
}),
);

it.effect(
"inherits an available parent instance for driver-only targets and otherwise selects a healthy peer",
() =>
Effect.gen(function* () {
const codexAltInstanceId = ProviderInstanceId.make("codex-alt");
const driver = ProviderDriverKind.make("codex");
const parentModelSelection = {
instanceId: codexInstanceId,
model: "gpt-5.4",
options: [{ id: "reasoningEffort", value: "high" }],
} as const;
const task = {
id: taskId,
threadId: parentThreadId,
runId: parentRunId,
parentNodeId,
origin: "app_owned",
createdBy: "agent",
driver,
providerInstanceId: codexInstanceId,
providerThreadId: null,
childThreadId,
nativeTaskRef: null,
prompt: "Summarize the diff.",
title: null,
model: "gpt-5.4",
status: "running",
result: null,
startedAt: null,
completedAt: null,
};
const cases = [
{
name: "healthy-inherited",
inheritedEnabled: true,
peerEnabled: true,
explicit: false,
selectedInstanceId: codexInstanceId,
},
{
name: "unavailable-inherited-falls-back-to-healthy-peer",
inheritedEnabled: false,
peerEnabled: true,
explicit: false,
selectedInstanceId: codexAltInstanceId,
},
{
name: "no-available-peer",
inheritedEnabled: false,
peerEnabled: false,
explicit: false,
selectedInstanceId: null,
},
{
name: "explicit-unavailable",
inheritedEnabled: false,
peerEnabled: true,
explicit: true,
selectedInstanceId: null,
},
{
name: "explicit-healthy",
inheritedEnabled: true,
peerEnabled: true,
explicit: true,
selectedInstanceId: codexAltInstanceId,
},
] as const;

for (const testCase of cases) {
const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
let delegated = false;
const dependencies = Layer.mergeAll(
NodeServices.layer,
Layer.mock(ThreadManagementService)({
getThreadProjection: (threadId) =>
Effect.succeed(
threadId === parentThreadId
? parentProjection(delegated ? [task] : [], parentModelSelection)
: childProjection,
),
dispatch: (command) =>
Ref.update(dispatched, (commands) => [...commands, command]).pipe(
Effect.andThen(
Effect.sync(() => {
delegated = true;
}),
),
Effect.as({
sequence: 1,
storedEvents: [
{
sequence: 1,
commandId: null,
event: { type: "subagent.updated", payload: task },
},
],
} as never),
),
}),
Layer.mock(ProviderRegistry)({
getProviders: Effect.succeed([
providerSnapshot({
instanceId: codexInstanceId,
driver,
model: "gpt-5.4",
enabled: testCase.inheritedEnabled,
}),
providerSnapshot({
instanceId: codexAltInstanceId,
driver,
model: "codex-alt-model",
enabled: testCase.peerEnabled,
}),
]),
}),
adapterRegistryLayer([codexInstanceId, codexAltInstanceId]),
Layer.mock(ScheduledTaskService)({}),
);

yield* Effect.gen(function* () {
const service = yield* OrchestratorMcpService.OrchestratorMcpService;
const target = testCase.explicit
? ({
providerInstanceId:
testCase.selectedInstanceId === null
? codexInstanceId
: testCase.selectedInstanceId,
} as const)
: ({ driverKind: driver } as const);
if (testCase.selectedInstanceId === null) {
const error = yield* service
.delegateTask(scope, {
task: "Summarize the diff.",
target,
mode: "async",
clientRequestId: `delegate-select-${testCase.name}`,
})
.pipe(Effect.flip);
assert.equal(error.code, "provider_unavailable", testCase.name);
assert.deepEqual(yield* Ref.get(dispatched), [], testCase.name);
return;
}
const result = yield* service.delegateTask(scope, {
task: "Summarize the diff.",
target,
mode: "async",
clientRequestId: `delegate-select-${testCase.name}`,
});
assert.equal(result.status, "running", testCase.name);
const commands = yield* Ref.get(dispatched);
assert.equal(commands.length, 1, testCase.name);
const request = commands[0] as {
type: string;
modelSelection: {
instanceId: string;
model: string;
options?: ReadonlyArray<{ id: string; value: unknown }>;
};
};
assert.equal(request.type, "delegated_task.request", testCase.name);
assert.equal(
request.modelSelection.instanceId,
testCase.selectedInstanceId,
testCase.name,
);
if (testCase.name === "healthy-inherited") {
assert.deepEqual(request.modelSelection, parentModelSelection, testCase.name);
} else {
assert.equal(request.modelSelection.model, "codex-alt-model", testCase.name);
}
}).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies))));
}
}),
);
});
7 changes: 6 additions & 1 deletion apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -815,8 +815,13 @@ const make = Effect.gen(function* () {
`No V2 provider adapter is registered for driver ${requestedDriver}.`,
);
}
// Inherit the parent's instance only when it can actually serve the
// child; an unavailable parent yields to a healthy instance of the
// requested driver rather than failing the delegation.
const inheritedCandidate = candidates.find(
(candidate) => candidate.instanceId === input.parent.thread.modelSelection.instanceId,
(candidate) =>
candidate.instanceId === input.parent.thread.modelSelection.instanceId &&
providerConstraints(candidate, true).length === 0,
);
const availableCandidate = candidates.find(
(candidate) => providerConstraints(candidate, true).length === 0,
Expand Down
5 changes: 4 additions & 1 deletion docs/orchestration-v2/orchestrator-mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,10 @@ type DelegateTaskInput = {
```

Provider, model, runtime mode, and interaction mode inherit from the parent
when omitted. Selecting a different provider without a model uses that
when omitted. A driver-only target inherits the parent's provider instance
when it can run child tasks, and otherwise selects an available instance of
that driver; an explicit `providerInstanceId` is honored exactly and fails
when unavailable. Selecting a different provider without a model uses that
provider's first advertised model.

Delegation requires an active parent run owned by the MCP credential's
Expand Down
Loading