Skip to content
117 changes: 117 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,56 @@ describe("ProviderCommandReactor", () => {
});
});

it("forwards claude effort options through session start and turn send", async () => {
const harness = await createHarness();
const now = new Date().toISOString();

await Effect.runPromise(
harness.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.makeUnsafe("cmd-turn-start-claude-effort"),
threadId: ThreadId.makeUnsafe("thread-1"),
message: {
messageId: asMessageId("user-message-claude-effort"),
role: "user",
text: "hello with effort",
attachments: [],
},
provider: "claudeAgent",
model: "claude-sonnet-4-6",
modelOptions: {
claudeAgent: {
effort: "max",
},
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
createdAt: now,
}),
);

await waitFor(() => harness.startSession.mock.calls.length === 1);
await waitFor(() => harness.sendTurn.mock.calls.length === 1);
expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({
provider: "claudeAgent",
model: "claude-sonnet-4-6",
modelOptions: {
claudeAgent: {
effort: "max",
},
},
});
expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({
threadId: ThreadId.makeUnsafe("thread-1"),
model: "claude-sonnet-4-6",
modelOptions: {
claudeAgent: {
effort: "max",
},
},
});
});

it("forwards plan interaction mode to the provider turn request", async () => {
const harness = await createHarness();
const now = new Date().toISOString();
Expand Down Expand Up @@ -531,6 +581,73 @@ describe("ProviderCommandReactor", () => {
expect(harness.stopSession.mock.calls.length).toBe(0);
});

it("restarts claude sessions when claude effort changes", async () => {
const harness = await createHarness();
const now = new Date().toISOString();

await Effect.runPromise(
harness.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.makeUnsafe("cmd-turn-start-claude-effort-1"),
threadId: ThreadId.makeUnsafe("thread-1"),
message: {
messageId: asMessageId("user-message-claude-effort-1"),
role: "user",
text: "first claude turn",
attachments: [],
},
provider: "claudeAgent",
model: "claude-sonnet-4-6",
modelOptions: {
claudeAgent: {
effort: "medium",
},
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
createdAt: now,
}),
);

await waitFor(() => harness.startSession.mock.calls.length === 1);
await waitFor(() => harness.sendTurn.mock.calls.length === 1);

await Effect.runPromise(
harness.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.makeUnsafe("cmd-turn-start-claude-effort-2"),
threadId: ThreadId.makeUnsafe("thread-1"),
message: {
messageId: asMessageId("user-message-claude-effort-2"),
role: "user",
text: "second claude turn",
attachments: [],
},
provider: "claudeAgent",
model: "claude-sonnet-4-6",
modelOptions: {
claudeAgent: {
effort: "max",
},
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
createdAt: now,
}),
);

await waitFor(() => harness.startSession.mock.calls.length === 2);
await waitFor(() => harness.sendTurn.mock.calls.length === 2);
expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({
provider: "claudeAgent",
modelOptions: {
claudeAgent: {
effort: "max",
},
},
});
});

it("restarts the provider session when runtime mode is updated on the thread", async () => {
const harness = await createHarness();
const now = new Date().toISOString();
Expand Down
42 changes: 31 additions & 11 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";
const WORKTREE_BRANCH_PREFIX = "t3code";
const TEMP_WORKTREE_BRANCH_PATTERN = new RegExp(`^${WORKTREE_BRANCH_PREFIX}\\/[0-9a-f]{8}$`);

const sameModelOptions = (
left: ProviderModelOptions | undefined,
right: ProviderModelOptions | undefined,
): boolean => JSON.stringify(left ?? null) === JSON.stringify(right ?? null);

function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderServiceError>): boolean {
const error = Cause.squash(cause);
if (Schema.is(ProviderAdapterRequestError)(error)) {
Expand Down Expand Up @@ -137,6 +142,7 @@ const make = Effect.gen(function* () {
);

const threadProviderOptions = new Map<string, ProviderStartOptions>();
const threadModelOptions = new Map<string, ProviderModelOptions>();

const appendProviderFailureActivity = (input: {
readonly threadId: ThreadId;
Expand Down Expand Up @@ -289,13 +295,23 @@ const make = Effect.gen(function* () {
: (yield* providerService.getCapabilities(currentProvider)).sessionModelSwitch;
const modelChanged = options?.model !== undefined && options.model !== activeSession?.model;
const shouldRestartForModelChange = modelChanged && sessionModelSwitch === "restart-session";
const previousModelOptions = threadModelOptions.get(threadId);
const shouldRestartForModelOptionsChange =
currentProvider === "claudeAgent" &&
options?.modelOptions !== undefined &&
!sameModelOptions(previousModelOptions, options.modelOptions);

if (!runtimeModeChanged && !providerChanged && !shouldRestartForModelChange) {
if (
!runtimeModeChanged &&
!providerChanged &&
!shouldRestartForModelChange &&
!shouldRestartForModelOptionsChange
) {
return existingSessionThreadId;
}

const resumeCursor =
providerChanged || shouldRestartForModelChange
providerChanged || shouldRestartForModelChange || shouldRestartForModelOptionsChange
? undefined
: (activeSession?.resumeCursor ?? undefined);
yield* Effect.logInfo("provider command reactor restarting provider session", {
Expand All @@ -309,6 +325,7 @@ const make = Effect.gen(function* () {
providerChanged,
modelChanged,
shouldRestartForModelChange,
shouldRestartForModelOptionsChange,
hasResumeCursor: resumeCursor !== undefined,
});
const restartedSession = yield* startProviderSession({
Expand Down Expand Up @@ -348,15 +365,18 @@ const make = Effect.gen(function* () {
if (!thread) {
return;
}
if (input.providerOptions !== undefined) {
threadProviderOptions.set(input.threadId, input.providerOptions);
}
yield* ensureSessionForThread(input.threadId, input.createdAt, {
...(input.provider !== undefined ? { provider: input.provider } : {}),
...(input.model !== undefined ? { model: input.model } : {}),
...(input.modelOptions !== undefined ? { modelOptions: input.modelOptions } : {}),
...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}),
});
if (input.providerOptions !== undefined) {
threadProviderOptions.set(input.threadId, input.providerOptions);
}
if (input.modelOptions !== undefined) {
threadModelOptions.set(input.threadId, input.modelOptions);
}
const normalizedInput = toNonEmptyProviderInput(input.messageText);
const normalizedAttachments = input.attachments ?? [];
const activeSession = yield* providerService
Expand Down Expand Up @@ -657,13 +677,13 @@ const make = Effect.gen(function* () {
return;
}
const cachedProviderOptions = threadProviderOptions.get(event.payload.threadId);
yield* ensureSessionForThread(
event.payload.threadId,
event.occurredAt,
cachedProviderOptions !== undefined
const cachedModelOptions = threadModelOptions.get(event.payload.threadId);
yield* ensureSessionForThread(event.payload.threadId, event.occurredAt, {
...(cachedProviderOptions !== undefined
? { providerOptions: cachedProviderOptions }
: undefined,
);
: {}),
...(cachedModelOptions !== undefined ? { modelOptions: cachedModelOptions } : {}),
});
return;
}
case "thread.turn-start-requested":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,7 @@ describe("ProviderRuntimeIngestion", () => {
payload: {
taskId: "turn-task-1",
description: "Comparing the desktop rollout chunks to the app-server stream.",
summary: "Code reviewer is validating the desktop rollout chunks.",
},
});

Expand Down Expand Up @@ -1474,8 +1475,9 @@ describe("ProviderRuntimeIngestion", () => {
expect(started?.kind).toBe("task.started");
expect(started?.summary).toBe("Plan task started");
expect(progress?.kind).toBe("task.progress");
expect(progressPayload?.detail).toBe(
"Comparing the desktop rollout chunks to the app-server stream.",
expect(progressPayload?.detail).toBe("Code reviewer is validating the desktop rollout chunks.");
expect(progressPayload?.summary).toBe(
"Code reviewer is validating the desktop rollout chunks.",
);
expect(completed?.kind).toBe("task.completed");
expect(completedPayload?.detail).toBe("<proposed_plan>\n# Plan title\n</proposed_plan>");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,8 @@ function runtimeEventToActivities(
summary: "Reasoning update",
payload: {
taskId: event.payload.taskId,
detail: truncateDetail(event.payload.description),
detail: truncateDetail(event.payload.summary ?? event.payload.description),
...(event.payload.summary ? { summary: truncateDetail(event.payload.summary) } : {}),
...(event.payload.lastToolName ? { lastToolName: event.payload.lastToolName } : {}),
...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),
},
Expand Down
Loading