Skip to content
Closed
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
163 changes: 163 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7903,6 +7903,169 @@ describe("ClaudeAdapterV2 background wake turns", () => {
),
);

it.effect("keeps a running turn's catalog through a refresh and a steer", () =>
Effect.gen(function* () {
let catalog: ClaudeModelCatalog = BUNDLED_CLAUDE_MODEL_CATALOG;
const harness = yield* makeWakeHarnessWithOptions({
modelCatalog: Effect.sync(() => catalog),
});
const input = makeClaudeTestTurnInput({
threadId: harness.threadId,
providerThread: harness.providerThread,
now: yield* DateTime.now,
attemptId: RunAttemptId.make("attempt-catalog-refresh"),
text: "Hello",
attachments: [],
});
yield* harness.runtime.startTurn(input);
const startWindow = harness.runtime.getModelContextWindow?.(CLAUDE_TEST_MODEL_SELECTION);
assert.isNumber(startWindow);
catalog = { models: [] };
const idAllocator = yield* IdAllocator.IdAllocatorV2;
yield* harness.runtime.steerTurn({
threadId: harness.threadId,
runId: input.runId,
providerThread: harness.providerThread,
providerTurnId: idAllocator.derive.providerTurn({
driver: ClaudeAdapterV2.CLAUDE_PROVIDER,
nativeTurnId: `turn:${input.attemptId}`,
}),
message: {
createdBy: "user",
creationSource: "web",
messageId: MessageId.make("catalog-refresh-steer"),
text: "Keep going",
attachments: [],
},
});
const usage = { ...zeroUsage, input_tokens: 1_000, output_tokens: 2 };
const frames = [
usageStreamFrame({ type: "message_start", message: { id: "msg_refresh", usage } }),
claudeSdkFrame({
type: "assistant",
parent_tool_use_id: null,
message: {
model: CLAUDE_TEST_MODEL_SELECTION.model,
id: "msg_refresh",
type: "message",
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu-catalog-refresh",
name: "Agent",
input: { description: "Check", subagent_type: "general-purpose", prompt: "Check." },
},
],
},
uuid: "00000000-0000-4000-8000-000000000690",
session_id: WAKE_NATIVE_SESSION,
}),
claudeSdkFrame({
type: "system",
subtype: "task_started",
task_id: "task-catalog-refresh",
tool_use_id: "toolu-catalog-refresh",
description: "Check",
task_type: "local_agent",
uuid: "00000000-0000-4000-8000-000000000691",
session_id: WAKE_NATIVE_SESSION,
}),
makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000692", result: "OK" }),
];
for (const frame of frames) yield* Queue.offer(harness.sdkMessages, frame);
yield* Queue.take(harness.terminalReceipts);

assert.deepEqual(
harness.offeredMessages.map((message) => message.message.content),
["Ultrathink:\nHello", "Ultrathink:\nKeep going"],
);
const windows = harness.events.flatMap((event) =>
event.type === "provider_turn.updated" && event.providerTurn.tokenUsage
? [event.providerTurn.tokenUsage.maxTokens]
: [],
);
assert.deepEqual(windows, [startWindow]);
const subagent = harness.events.find((event) => event.type === "subagent.updated");
assert.equal(
subagent?.type === "subagent.updated"
? getModelSelectionStringOptionValue(subagent.subagent.modelSelection, "effort")
: undefined,
"high",
);
assert.isUndefined(harness.runtime.getModelContextWindow?.(CLAUDE_TEST_MODEL_SELECTION));
}).pipe(
Effect.provide(
Layer.mergeAll(
IdAllocator.layer,
McpProviderSessions.layer,
NodeServices.layer,
NodeCrypto.layer,
),
),
),
);

it.effect("opens a turn's query with its catalog after a refresh during startup", () =>
Effect.gen(function* () {
let catalog: ClaudeModelCatalog = BUNDLED_CLAUDE_MODEL_CATALOG;
let beforeMcpRead: Effect.Effect<void> = Effect.void;
const harness = yield* makeWakeHarnessWithOptions({
modelCatalog: Effect.sync(() => catalog),
}).pipe(
Effect.updateService(McpProviderSessions.McpProviderSessions, (sessions) => ({
...sessions,
read: (threadId) => Effect.andThen(beforeMcpRead, sessions.read(threadId)),
})),
);
const reached = yield* Deferred.make<void>();
const release = yield* Deferred.make<void>();
beforeMcpRead = Deferred.succeed(reached, undefined).pipe(
Effect.andThen(Deferred.await(release)),
);
const input = makeClaudeTestTurnInput({
threadId: harness.threadId,
providerThread: harness.providerThread,
now: yield* DateTime.now,
attemptId: RunAttemptId.make("attempt-catalog-startup"),
text: "Hello",
attachments: [],
});
const starting = yield* harness.runtime.startTurn(input).pipe(Effect.forkChild);
yield* Deferred.await(reached);
catalog = { models: [] };
// Any steer reloads the adapter-wide catalog, even one that then finds no live query.
yield* harness.runtime
.steerTurn({
threadId: harness.threadId,
runId: input.runId,
providerThread: harness.providerThread,
providerTurnId: ProviderTurnId.make("provider-turn-catalog-startup"),
message: {
createdBy: "user",
creationSource: "web",
messageId: MessageId.make("catalog-startup-steer"),
text: "Keep going",
attachments: [],
},
})
.pipe(Effect.flip);
assert.isUndefined(harness.runtime.getModelContextWindow?.(CLAUDE_TEST_MODEL_SELECTION));
yield* Deferred.succeed(release, undefined);
yield* Fiber.join(starting);
assert.equal(harness.getOpenedOptions()?.effort, "high");
}).pipe(
Effect.provide(
Layer.mergeAll(
IdAllocator.layer,
McpProviderSessions.layer,
NodeServices.layer,
NodeCrypto.layer,
),
),
),
);

it.effect.each(["root", "mcp", "subagent", "remote"] as const)(
"asks Claude for suggestions only on threads a person drives (%s)",
(lineage) =>
Expand Down
53 changes: 32 additions & 21 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4331,6 +4331,8 @@ function formatClaudeUsageLimitWait(waitMs: number): string {
interface ActiveClaudeTurnContext {
readonly queryPolicy: ClaudeRuntimeQueryPolicy;
readonly input: ProviderAdapter.ProviderAdapterV2TurnInput;
// Snapshot taken at turn start; a later refresh applies from the next turn.
readonly modelCatalog: ClaudeModelCatalog;
readonly autoCompactWindow: string;
readonly nativeThreadId: string;
readonly nativeTurnId: string;
Expand Down Expand Up @@ -4882,11 +4884,12 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
let modelCatalog = yield* loadModelCatalog;
// Results report the window Claude Code compacts against; outside the catalog it is the only source.
const reportedContextWindows = new Map<string, number>();
const claudeContextWindow = (modelSelection: ModelSelection): number | null =>
resolveClaudeCatalogContextWindowTokens(modelCatalog, modelSelection) ??
reportedContextWindows.get(
compileClaudeModelSelection(modelSelection, modelCatalog).apiModelId,
) ??
const claudeContextWindow = (
catalog: ClaudeModelCatalog,
modelSelection: ModelSelection,
): number | null =>
resolveClaudeCatalogContextWindowTokens(catalog, modelSelection) ??
reportedContextWindows.get(compileClaudeModelSelection(modelSelection, catalog).apiModelId) ??
null;
const continuationRequests = adapterOptions.continuationRequests ?? {
offer: () => Effect.void,
Expand Down Expand Up @@ -5464,7 +5467,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
// Only message_start marks the request start; an assistant snapshot
// arrives after generation, so it leaves the cache expiry unknown.
if (requestStarted && context.streamUsage !== null) context.streamUsage.requestedAt = now;
const maxTokens = claudeContextWindow(context.input.modelSelection);
const maxTokens = claudeContextWindow(context.modelCatalog, context.input.modelSelection);
const tokenUsage =
typeof usage === "number"
? {
Expand Down Expand Up @@ -6867,8 +6870,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
existingSubagent === undefined
? (input.effort ??
input.owner?.effort ??
compileClaudeModelSelection(input.context.input.modelSelection, modelCatalog)
.effort)
compileClaudeModelSelection(
input.context.input.modelSelection,
input.context.modelCatalog,
).effort)
: existingSubagent.effort;
const model = input.model ?? priorTask?.model ?? null;
const task = {
Expand Down Expand Up @@ -6906,7 +6911,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
...(existingSubagent === undefined || existingSubagent.task.modelSelection !== undefined
? {
modelSelection: claudeSubagentModelSelection(
modelCatalog,
input.context.modelCatalog,
input.context.input.modelSelection.instanceId,
model || input.context.input.modelSelection.model,
effort,
Expand Down Expand Up @@ -9166,7 +9171,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
if (message.message.model && message.message.model !== "<synthetic>") {
context.rootModels.add(message.message.model);
context.executedModel = resolveClaudeReportedModelSlug(
modelCatalog,
context.modelCatalog,
message.message.model,
);
}
Expand Down Expand Up @@ -9450,7 +9455,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
const model =
snapshotModel.length === 0 || snapshotModel === "<synthetic>"
? undefined
: canonicalClaudeSubagentModel(modelCatalog, snapshotModel);
: canonicalClaudeSubagentModel(context.modelCatalog, snapshotModel);
if (parentToolUseId !== null && model !== undefined) {
const subagent = yield* resolveSubagentByToolUseId(context, parentToolUseId);
if (subagent === undefined) {
Expand Down Expand Up @@ -9756,7 +9761,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
const nativeToolInput = claudeNativeToolInputFromUnknown(toolUse.input);
if (toolUse.name === "Agent") {
rememberClaudeSubagentLaunch(
modelCatalog,
context.modelCatalog,
context,
pendingSubagentLaunchesByToolUseId,
toolUse.id,
Expand Down Expand Up @@ -10356,7 +10361,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
const startToolCall = toolName !== "Agent" && options?.startToolCall !== false;
if (toolName === "Agent") {
rememberClaudeSubagentLaunch(
modelCatalog,
context.modelCatalog,
context,
pendingSubagentLaunchesByToolUseId,
nativeRequestId,
Expand Down Expand Up @@ -10857,6 +10862,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
const openQuery = Effect.fnUntraced(function* (
turnInput: ProviderAdapter.ProviderAdapterV2TurnInput,
nativeThreadId: string,
turnCatalog: ClaudeModelCatalog,
) {
const queryPolicy = yield* queryPolicyForTurn(turnInput);
const mcpSession = yield* mcpSessions.read(turnInput.threadId);
Expand All @@ -10871,7 +10877,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
const queryPolicyKey = claudeEffectiveQueryPolicyKey(queryPolicy, mcpOverrides);
const compiledSelection = compileClaudeModelSelection(
turnInput.modelSelection,
modelCatalog,
turnCatalog,
);
const resumeSessionAt = yield* getNativeConversationHeadId(turnInput.providerThread);
const existing = yield* Ref.get(queryContext);
Expand Down Expand Up @@ -10987,7 +10993,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
);
const queryOptions = makeClaudeQueryOptions({
modelSelection: turnInput.modelSelection,
modelCatalog,
modelCatalog: turnCatalog,
nativeThreadId,
title: turnInput.appThread.title,
resume: shouldResume,
Expand Down Expand Up @@ -11182,7 +11188,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (

const startTurn = Effect.fn("ClaudeAdapterV2.startTurn")(
function* (turnInput: ProviderAdapter.ProviderAdapterV2TurnInput) {
modelCatalog = yield* loadModelCatalog;
const turnCatalog = yield* loadModelCatalog;
modelCatalog = turnCatalog;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const startedAt = yield* DateTime.now;
const nativeThreadId = yield* getNativeThreadId(turnInput.providerThread);
const nativeTurnId = `turn:${turnInput.attemptId}`;
Expand Down Expand Up @@ -11241,7 +11248,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
: yield* makeClaudeUserMessageWithAttachments({
text: applyClaudePromptEffortPrefix(
turnInput.message.text,
compileClaudeModelSelection(turnInput.modelSelection, modelCatalog)
compileClaudeModelSelection(turnInput.modelSelection, turnCatalog)
.promptEffort,
),
attachments: turnInput.message.attachments,
Expand All @@ -11254,14 +11261,15 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(Path.Path, path),
);
const querySession = yield* openQuery(turnInput, nativeThreadId);
const querySession = yield* openQuery(turnInput, nativeThreadId, turnCatalog);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const autoCompactWindow = yield* resolveAutoCompactWindow(
querySession.settingsCwd,
turnInput.modelSelection,
querySession.launchArgSettings,
);
const context: ActiveClaudeTurnContext = {
input: turnInput,
modelCatalog: turnCatalog,
autoCompactWindow,
queryPolicy: querySession.queryPolicy,
nativeThreadId,
Expand Down Expand Up @@ -11699,8 +11707,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
const userMessage = yield* makeClaudeUserMessageWithAttachments({
text: applyClaudePromptEffortPrefix(
turnInput.message.text,
compileClaudeModelSelection(currentTurn.input.modelSelection, modelCatalog)
.promptEffort,
compileClaudeModelSelection(
currentTurn.input.modelSelection,
currentTurn.modelCatalog,
).promptEffort,
),
attachments: turnInput.message.attachments,
priority: "now",
Expand Down Expand Up @@ -11855,7 +11865,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
driver: CLAUDE_PROVIDER,
providerSessionId: input.providerSessionId,
providerSession: session,
getModelContextWindow: (selection) => claudeContextWindow(selection) ?? undefined,
getModelContextWindow: (selection) =>
claudeContextWindow(modelCatalog, selection) ?? undefined,
events: Stream.fromEffectRepeat(Queue.take(events)),
backgroundTasksEndWithProcess: true,
hasPendingBackgroundWork: Effect.gen(function* () {
Expand Down
Loading