Skip to content
Merged
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
56 changes: 56 additions & 0 deletions apps/server/src/mcp/toolkits/project/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,62 @@ it.effect(
}),
);

it.effect("a launch steps an inherited mode down to the provider's, and refuses one it lacks", () =>
Effect.gen(function* () {
// Pi offers no Auto; a mode a provider lacks would run as Supervised.
const pi = ProviderInstanceId.make("pi");
const launchPi = Effect.fn("test.launchPi")(function* (runtimeMode?: RuntimeMode) {
const launched: Array<ThreadLaunch.ThreadLaunchInput> = [];
const { projectId, dependencies } = clientLaunchHarness({
runtimeModeCeiling: "auto",
launched,
providers: [
{
instanceId: pi,
driver: ProviderDriverKind.make("pi"),
enabled: true,
installed: true,
status: "ready",
version: null,
auth: { status: "authenticated" },
checkedAt: "2026-01-01T00:00:00Z",
models: [],
slashCommands: [],
skills: [],
supportedRuntimeModes: ["approval-required", "auto-accept-edits", "full-access"],
},
],
});
const toolkit = yield* ProjectToolkit.pipe(
Effect.provide(
McpToolAccess.HandlersLayer.layer(ProjectHandlers.layer).pipe(
Layer.provide(dependencies),
),
),
);
const result = yield* toolkit
.handle("t3_thread_launch", {
projectId,
title: "Fix",
modelSelection: { instanceId: pi, model: "pi-model" },
...(runtimeMode === undefined ? {} : { runtimeMode }),
})
.pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies));
return { result: result.at(-1)?.result, launched };
});

const inherited = yield* launchPi();
expect(inherited.launched[0]?.runtimeMode).toBe("auto-accept-edits");

const requested = yield* launchPi("auto");
expect(requested.result).toMatchObject({ code: "invalid_request" });
expect(requested.launched).toHaveLength(0);

const offered = yield* launchPi("approval-required");
expect(offered.launched[0]?.runtimeMode).toBe("approval-required");
}),
);

it.effect("a launch retried with the same clientRequestId replays the first one", () =>
Effect.gen(function* () {
const launched: Array<ThreadLaunch.ThreadLaunchInput> = [];
Expand Down
11 changes: 10 additions & 1 deletion apps/server/src/mcp/toolkits/project/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,21 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, {
message:
"Pass modelSelection: the project has no default model. orchestrator_capabilities lists providers and models.",
});
const startedRuntimeMode =
let startedRuntimeMode =
projectDefaults !== undefined && input.runtimeMode === undefined
? yield* resolveRuntimeMode(runtimeMode, projectDefaults.defaultRuntimeMode).pipe(
Effect.orElseSucceed(() => runtimeMode),
)
: runtimeMode;
if (!replaying) {
const providers = yield* (yield* ProviderRegistry.ProviderRegistry).getProviders;
startedRuntimeMode = yield* resolveRuntimeMode(startedRuntimeMode, input.runtimeMode, {
modelSelection,
supportedRuntimeModes: providers.find(
(provider) => provider.instanceId === modelSelection.instanceId,
)?.supportedRuntimeModes,
});
}
const result = yield* ThreadMessageIntake.launchThread({
commandId,
threadId,
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/peer/PeerForwarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,9 @@ const serveBox = (seen: Ref.Ref<Seen>, linkSession: Ref.Ref<string>) =>
),
),
Layer.provide(NodeServices.layer),
Layer.provide(Layer.mock(ProviderRegistry.ProviderRegistry)({})),
Layer.provide(
Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
),
Layer.provide(Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({})),
Layer.provide(Layer.mock(ScheduledTaskService.ScheduledTaskService)({})),
Layer.provide(Layer.mock(SecretRequests.SecretRequests)({})),
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/peer/RemoteDelegation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,9 @@ const boxToolkitLayer = (thread: BoxThread) => {
Layer.provide(NodeServices.layer),
),
),
Layer.provide(Layer.mock(ProviderRegistry.ProviderRegistry)({})),
Layer.provide(
Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
),
Layer.provide(Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({})),
Layer.provide(Layer.mock(ScheduledTaskService.ScheduledTaskService)({})),
Layer.provide(
Expand Down
4 changes: 3 additions & 1 deletion docs/orchestration-v2/orchestrator-mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,9 @@ Pass the task in `message`. Project, model, and modes inherit when omitted;
workspace does not. `scratch: true` launches without a project, in a folder of
its own under the environment's Scratch project. For stacked PRs, use the parent branch as `baseRef` with
`startFromOrigin: false`. The new thread may not run with broader runtime or
interaction modes than the caller. Launch has no retry key, so inspect existing threads after a failed or lost response before
interaction modes than the caller. As for `delegate_task`, a runtime mode the
provider does not offer is refused, and an inherited or default one steps down
to the broadest mode it offers. Launch has no retry key, so inspect existing threads after a failed or lost response before
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
launching again. `create_threads` remains the batch option for a shared checkout.

### `t3_thread_list`
Expand Down
Loading