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
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 @@ -472,6 +522,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 @@ -74,6 +74,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 @@ -136,6 +141,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 @@ -270,13 +276,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 @@ -290,6 +306,7 @@ const make = Effect.gen(function* () {
providerChanged,
modelChanged,
shouldRestartForModelChange,
shouldRestartForModelOptionsChange,
hasResumeCursor: resumeCursor !== undefined,
});
const restartedSession = yield* startProviderSession({
Expand Down Expand Up @@ -329,15 +346,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 @@ -627,13 +647,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
Loading