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
164 changes: 164 additions & 0 deletions apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1691,6 +1691,170 @@ describe("ClaudeAdapterLive", () => {
);
});

for (const ending of ["results", "turn-completed", "stream-failed"] as const) {
it.effect(`isolates parent and sibling tool streams through ${ending}`, () => {
const harness = makeHarness();
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
const eventsFiber = yield* adapter.streamEvents.pipe(
Stream.takeUntil((event) => event.type === "turn.completed"),
Stream.runCollect,
Effect.forkChild,
);
yield* adapter.startSession({
threadId: THREAD_ID,
provider: ProviderDriverKind.make("claudeAgent"),
runtimeMode: "full-access",
});
yield* adapter.sendTurn({ threadId: THREAD_ID, input: "Delegate work", attachments: [] });
const stream = (owner: string | null, event: unknown) =>
harness.query.emit({
type: "stream_event",
session_id: "sdk-isolated",
uuid: "stream-isolated",
parent_tool_use_id: owner,
event,
} as unknown as SDKMessage);
const owners = [null, "task-a", "task-b"] as const;
for (const owner of ["task-a", "task-b"]) {
harness.query.emit({
type: "system",
subtype: "task_started",
task_id: `agent-${owner}`,
tool_use_id: owner,
description: owner,
task_type: "local_agent",
session_id: "sdk-isolated",
uuid: `started-${owner}`,
} as unknown as SDKMessage);
}
stream(null, {
type: "content_block_start",
index: 1,
content_block: { type: "text", text: "" },
});
stream(null, {
type: "content_block_delta",
index: 1,
delta: { type: "text_delta", text: "Parent " },
});
for (const owner of owners) {
stream(owner, {
type: "content_block_start",
index: 0,
content_block: {
type: "tool_use",
id: `tool-${owner ?? "parent"}`,
name: "Read",
input: {},
},
});
}
for (const owner of owners) {
stream(owner, {
type: "content_block_delta",
index: 0,
delta: { type: "input_json_delta", partial_json: '{"file_path":"' },
});
}
for (const owner of owners.toReversed()) {
stream(owner, {
type: "content_block_delta",
index: 0,
delta: { type: "input_json_delta", partial_json: `${owner ?? "parent"}.ts"}` },
});
stream(owner, { type: "content_block_stop", index: 0 });
}
// A suppressed child text block must not close the parent's block at the same index.
stream("task-a", {
type: "content_block_start",
index: 1,
content_block: { type: "text", text: "" },
});
stream("task-a", { type: "content_block_stop", index: 1 });
stream(null, {
type: "content_block_delta",
index: 1,
delta: { type: "text_delta", text: "answer" },
});
stream(null, { type: "content_block_stop", index: 1 });
if (ending === "results") {
for (const owner of owners.toReversed()) {
harness.query.emit({
type: "user",
session_id: "sdk-isolated",
uuid: "result-isolated",
parent_tool_use_id: owner,
message: {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: `tool-${owner ?? "parent"}`,
content: `result-${owner ?? "parent"}`,
is_error: owner === "task-b",
},
],
},
} as unknown as SDKMessage);
}
}
if (ending === "stream-failed") harness.query.fail(new Error("SDK stream failed"));
else
harness.query.emit({
type: "result",
subtype: "success",
is_error: false,
errors: [],
session_id: "sdk-isolated",
uuid: "result-isolated",
} as unknown as SDKMessage);
const events = Array.from(yield* Fiber.join(eventsFiber));
const textDeltas = events.filter(
(event) =>
event.type === "content.delta" && event.payload.streamKind === "assistant_text",
);
assert.equal(textDeltas.length, 2);
assert.equal(textDeltas[0]?.itemId, textDeltas[1]?.itemId);
for (const owner of owners) {
const completed = events.filter(
(event) =>
event.type === "item.completed" && event.itemId === `tool-${owner ?? "parent"}`,
);
assert.equal(completed.length, 1);
const event = completed[0];
assert.equal(event?.type, "item.completed");
if (event?.type !== "item.completed") return;
assert.equal(event.payload.parentToolUseId, owner ?? undefined);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
assert.equal(event.payload.agentId, owner === null ? undefined : `agent-${owner}`);
assert.equal(
event.payload.status,
ending === "stream-failed" || (ending === "results" && owner === "task-b")
? "failed"
: "completed",
);
assert.deepEqual(event.payload.data, {
toolName: "Read",
input: { file_path: `${owner ?? "parent"}.ts` },
...(ending === "results"
? {
result: {
type: "tool_result",
tool_use_id: `tool-${owner ?? "parent"}`,
content: `result-${owner ?? "parent"}`,
is_error: owner === "task-b",
},
}
: {}),
});
}
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
);
});
}

it.effect("maps Claude reasoning deltas, streamed tool inputs, and tool results", () => {
const harness = makeHarness();
return Effect.gen(function* () {
Expand Down
21 changes: 11 additions & 10 deletions apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ interface ClaudeSessionContext {
* turnStartMessageIds, and a long-lived session would otherwise hold every
* message it ever produced. */
readonly turns: Array<{ readonly id: TurnId }>;
readonly inFlightTools: Map<number, ToolInFlight>;
readonly inFlightTools: Map<string, ToolInFlight>;
readonly claudeTasks: Map<string, ClaudeTaskState>;
readonly taskAgents: Map<string, ClaudeTaskAgentState>;
/**
Expand Down Expand Up @@ -2805,6 +2805,8 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
status: status === "completed" ? "completed" : "failed",
title: tool.title,
...(tool.detail ? { detail: tool.detail } : {}),
...(tool.agentId ? { agentId: tool.agentId } : {}),
...(tool.parentToolUseId ? { parentToolUseId: tool.parentToolUseId } : {}),
data: {
toolName: tool.toolName,
input: tool.input,
Expand Down Expand Up @@ -2989,7 +2991,9 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
}

if (event.delta.type === "input_json_delta") {
const tool = context.inFlightTools.get(event.index);
// Every parent and subagent message numbers its own content blocks from zero.
const toolKey = `${streamParentToolUseId ?? ""}:${event.index}`;
const tool = context.inFlightTools.get(toolKey);
if (!tool || typeof event.delta.partial_json !== "string") {
return;
}
Expand All @@ -3013,7 +3017,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
parsedInput && Object.keys(parsedInput).length > 0
? toolInputFingerprint(parsedInput)
: undefined;
context.inFlightTools.set(event.index, nextTool);
context.inFlightTools.set(toolKey, nextTool);

if (
!parsedInput ||
Expand All @@ -3027,7 +3031,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
...nextTool,
lastEmittedInputFingerprint: nextFingerprint,
};
context.inFlightTools.set(event.index, nextTool);
context.inFlightTools.set(toolKey, nextTool);

const stamp = yield* makeEventStamp();
yield* offerRuntimeEvent({
Expand Down Expand Up @@ -3138,7 +3142,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
...(owningAgentId ? { agentId: owningAgentId } : {}),
...(parentToolUseId ? { parentToolUseId } : {}),
};
context.inFlightTools.set(index, tool);
context.inFlightTools.set(`${parentToolUseId ?? ""}:${index}`, tool);

const stamp = yield* makeEventStamp();
yield* offerRuntimeEvent({
Expand Down Expand Up @@ -3174,6 +3178,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
}

if (event.type === "content_block_stop") {
if (streamParentToolUseId !== null && streamParentToolUseId !== undefined) return;
const { index } = event;
const assistantBlock = context.turnState?.assistantTextBlocks.get(index);
if (assistantBlock) {
Expand All @@ -3184,10 +3189,6 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
});
return;
}
const tool = context.inFlightTools.get(index);
if (!tool) {
return;
}
}
});

Expand Down Expand Up @@ -4467,7 +4468,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (

const pendingApprovals = new Map<ApprovalRequestId, PendingApproval>();
const pendingUserInputs = new Map<ApprovalRequestId, PendingUserInput>();
const inFlightTools = new Map<number, ToolInFlight>();
const inFlightTools = new Map<string, ToolInFlight>();
const claudeTasks = new Map<string, ClaudeTaskState>();
const taskAgents = new Map<string, ClaudeTaskAgentState>();
const pendingTaskModels = new Map<string, string>();
Expand Down
Loading