@tanstack/ai-claude-code 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/adapters/claude-run-source.d.ts +14 -0
- package/dist/esm/adapters/claude-run-source.js +70 -0
- package/dist/esm/adapters/claude-run-source.js.map +1 -0
- package/dist/esm/adapters/projection.js +2 -1
- package/dist/esm/adapters/projection.js.map +1 -1
- package/dist/esm/adapters/text.d.ts +9 -1
- package/dist/esm/adapters/text.js +69 -17
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/provider-options.d.ts +6 -0
- package/dist/esm/stream/sdk-types.d.ts +4 -0
- package/dist/esm/stream/translate.d.ts +8 -0
- package/dist/esm/stream/translate.js +77 -2
- package/dist/esm/stream/translate.js.map +1 -1
- package/package.json +6 -6
- package/src/adapters/claude-run-source.ts +66 -0
- package/src/adapters/text.ts +120 -26
- package/src/provider-options.ts +6 -0
- package/src/stream/sdk-types.ts +12 -2
- package/src/stream/translate.ts +127 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventType, buildBaseUsage } from "@tanstack/ai";
|
|
2
|
+
import { parseJsonFromAssistantText, structuredOutputCompleteChunk, structuredOutputStartChunk } from "@tanstack/ai/adapter-internals";
|
|
2
3
|
//#region src/stream/translate.ts
|
|
3
4
|
/** Name of the CUSTOM event carrying the Claude Code session id. */
|
|
4
5
|
var SESSION_ID_EVENT = "claude-code.session-id";
|
|
@@ -6,6 +7,12 @@ var SESSION_ID_EVENT = "claude-code.session-id";
|
|
|
6
7
|
var BRIDGED_MCP_SERVER_NAME = "tanstack";
|
|
7
8
|
var BRIDGED_MCP_PREFIX = `mcp__${BRIDGED_MCP_SERVER_NAME}__`;
|
|
8
9
|
/**
|
|
10
|
+
* Claude Code `--json-schema` injects a fake tool named StructuredOutput.
|
|
11
|
+
* The model "calls" it with the schema JSON. The result message may omit
|
|
12
|
+
* `structured_output`; harvest the tool input in that case.
|
|
13
|
+
*/
|
|
14
|
+
var SYNTHETIC_STRUCTURED_OUTPUT_TOOL = "StructuredOutput";
|
|
15
|
+
/**
|
|
9
16
|
* Strip the bridged MCP server prefix so tool-call events match the TanStack
|
|
10
17
|
* tool names the application registered. Built-in harness tools (Bash, Read,
|
|
11
18
|
* Edit, ...) and foreign MCP tools pass through verbatim.
|
|
@@ -13,6 +20,12 @@ var BRIDGED_MCP_PREFIX = `mcp__${BRIDGED_MCP_SERVER_NAME}__`;
|
|
|
13
20
|
function stripMcpPrefix(name) {
|
|
14
21
|
return name.startsWith(BRIDGED_MCP_PREFIX) ? name.slice(BRIDGED_MCP_PREFIX.length) : name;
|
|
15
22
|
}
|
|
23
|
+
function isUsefulStructuredObject(value) {
|
|
24
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
25
|
+
}
|
|
26
|
+
function rememberStructuredOutput(current, next) {
|
|
27
|
+
return isUsefulStructuredObject(next) ? next : current;
|
|
28
|
+
}
|
|
16
29
|
function stringifyToolResultContent(content) {
|
|
17
30
|
if (content === void 0) return "";
|
|
18
31
|
if (typeof content === "string") return content;
|
|
@@ -58,6 +71,11 @@ async function* translateSdkStream(sdkMessages, ctx) {
|
|
|
58
71
|
let runStarted = false;
|
|
59
72
|
/** Tool calls started but with no result yet. */
|
|
60
73
|
const unresolvedToolCalls = /* @__PURE__ */ new Set();
|
|
74
|
+
const syntheticOutputToolIds = /* @__PURE__ */ new Set();
|
|
75
|
+
let capturedStructuredOutput;
|
|
76
|
+
let assistantTextForHarvest = "";
|
|
77
|
+
let partialStructuredJson = "";
|
|
78
|
+
let partialIsStructuredOutput = false;
|
|
61
79
|
/** Anthropic message ids whose text/thinking already streamed via partials. */
|
|
62
80
|
const streamedMessageIds = /* @__PURE__ */ new Set();
|
|
63
81
|
let partialMessageId = null;
|
|
@@ -117,8 +135,31 @@ async function* translateSdkStream(sdkMessages, ctx) {
|
|
|
117
135
|
}
|
|
118
136
|
partialReasoningId = null;
|
|
119
137
|
}
|
|
138
|
+
function* emitStructuredOutput(object) {
|
|
139
|
+
const raw = JSON.stringify(object);
|
|
140
|
+
const messageId = genId();
|
|
141
|
+
yield structuredOutputStartChunk({
|
|
142
|
+
messageId,
|
|
143
|
+
model,
|
|
144
|
+
threadId,
|
|
145
|
+
runId
|
|
146
|
+
});
|
|
147
|
+
yield structuredOutputCompleteChunk({
|
|
148
|
+
messageId,
|
|
149
|
+
model,
|
|
150
|
+
threadId,
|
|
151
|
+
runId,
|
|
152
|
+
object,
|
|
153
|
+
raw
|
|
154
|
+
});
|
|
155
|
+
}
|
|
120
156
|
function* emitToolUse(block) {
|
|
121
157
|
const toolCallName = stripMcpPrefix(block.name);
|
|
158
|
+
if (ctx.expectStructuredOutput === true && toolCallName === "StructuredOutput") {
|
|
159
|
+
capturedStructuredOutput = rememberStructuredOutput(capturedStructuredOutput, block.input);
|
|
160
|
+
syntheticOutputToolIds.add(block.id);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
122
163
|
const args = JSON.stringify(block.input ?? {});
|
|
123
164
|
yield {
|
|
124
165
|
type: EventType.TOOL_CALL_START,
|
|
@@ -153,6 +194,7 @@ async function* translateSdkStream(sdkMessages, ctx) {
|
|
|
153
194
|
if (alreadyStreamed) continue;
|
|
154
195
|
const messageId = message.message.id ?? genId();
|
|
155
196
|
const text = block.text;
|
|
197
|
+
if (!alreadyStreamed) assistantTextForHarvest += text;
|
|
156
198
|
yield {
|
|
157
199
|
type: EventType.TEXT_MESSAGE_START,
|
|
158
200
|
messageId,
|
|
@@ -218,6 +260,10 @@ async function* translateSdkStream(sdkMessages, ctx) {
|
|
|
218
260
|
for (const block of content) {
|
|
219
261
|
if (block.type !== "tool_result") continue;
|
|
220
262
|
const toolResult = block;
|
|
263
|
+
if (syntheticOutputToolIds.has(toolResult.tool_use_id)) {
|
|
264
|
+
syntheticOutputToolIds.delete(toolResult.tool_use_id);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
221
267
|
unresolvedToolCalls.delete(toolResult.tool_use_id);
|
|
222
268
|
yield {
|
|
223
269
|
type: EventType.TOOL_CALL_RESULT,
|
|
@@ -235,6 +281,21 @@ async function* translateSdkStream(sdkMessages, ctx) {
|
|
|
235
281
|
yield* closePartialReasoning();
|
|
236
282
|
yield* synthesizeUnresolvedResults();
|
|
237
283
|
const usage = buildUsage(message.usage, message.total_cost_usd);
|
|
284
|
+
if (ctx.expectStructuredOutput === true) {
|
|
285
|
+
const fromResult = isUsefulStructuredObject(message.structured_output) ? message.structured_output : void 0;
|
|
286
|
+
const fromTool = isUsefulStructuredObject(capturedStructuredOutput) ? capturedStructuredOutput : void 0;
|
|
287
|
+
let fromText;
|
|
288
|
+
if (fromResult === void 0 && fromTool === void 0) {
|
|
289
|
+
const raw = assistantTextForHarvest || message.result || "";
|
|
290
|
+
if (raw.trim() !== "") try {
|
|
291
|
+
fromText = parseJsonFromAssistantText(raw);
|
|
292
|
+
} catch {
|
|
293
|
+
fromText = void 0;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const object = fromResult ?? fromTool ?? fromText;
|
|
297
|
+
if (isUsefulStructuredObject(object)) yield* emitStructuredOutput(object);
|
|
298
|
+
}
|
|
238
299
|
if (message.subtype === "success") yield {
|
|
239
300
|
type: EventType.RUN_FINISHED,
|
|
240
301
|
runId,
|
|
@@ -275,6 +336,13 @@ async function* translateSdkStream(sdkMessages, ctx) {
|
|
|
275
336
|
streamedMessageIds.add(partialMessageId);
|
|
276
337
|
} else if (event.type === "content_block_start") {
|
|
277
338
|
partialBlockType = event.content_block.type;
|
|
339
|
+
const startedBlock = event.content_block;
|
|
340
|
+
partialIsStructuredOutput = ctx.expectStructuredOutput === true && startedBlock.type === "tool_use" && "name" in startedBlock && startedBlock.name === "StructuredOutput";
|
|
341
|
+
if (partialIsStructuredOutput) {
|
|
342
|
+
partialStructuredJson = "";
|
|
343
|
+
if ("id" in startedBlock && typeof startedBlock.id === "string") syntheticOutputToolIds.add(startedBlock.id);
|
|
344
|
+
if ("input" in startedBlock) capturedStructuredOutput = rememberStructuredOutput(capturedStructuredOutput, startedBlock.input);
|
|
345
|
+
}
|
|
278
346
|
if (partialBlockType === "text") {
|
|
279
347
|
partialTextMessageId = partialMessageId ?? genId();
|
|
280
348
|
partialTextContent = "";
|
|
@@ -307,6 +375,7 @@ async function* translateSdkStream(sdkMessages, ctx) {
|
|
|
307
375
|
} else if (event.type === "content_block_delta") {
|
|
308
376
|
if (event.delta.type === "text_delta" && partialTextStarted && partialTextMessageId && typeof event.delta.text === "string") {
|
|
309
377
|
partialTextContent += event.delta.text;
|
|
378
|
+
assistantTextForHarvest += event.delta.text;
|
|
310
379
|
yield {
|
|
311
380
|
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
312
381
|
messageId: partialTextMessageId,
|
|
@@ -315,7 +384,8 @@ async function* translateSdkStream(sdkMessages, ctx) {
|
|
|
315
384
|
delta: event.delta.text,
|
|
316
385
|
content: partialTextContent
|
|
317
386
|
};
|
|
318
|
-
} else if (event.delta.type === "
|
|
387
|
+
} else if (event.delta.type === "input_json_delta" && partialIsStructuredOutput && typeof event.delta.partial_json === "string") partialStructuredJson += event.delta.partial_json;
|
|
388
|
+
else if (event.delta.type === "thinking_delta" && partialReasoningId && typeof event.delta.thinking === "string") yield {
|
|
319
389
|
type: EventType.REASONING_MESSAGE_CONTENT,
|
|
320
390
|
messageId: partialReasoningId,
|
|
321
391
|
delta: event.delta.thinking,
|
|
@@ -323,9 +393,14 @@ async function* translateSdkStream(sdkMessages, ctx) {
|
|
|
323
393
|
timestamp: now()
|
|
324
394
|
};
|
|
325
395
|
} else if (event.type === "content_block_stop") {
|
|
396
|
+
if (partialIsStructuredOutput && partialStructuredJson !== "") try {
|
|
397
|
+
capturedStructuredOutput = rememberStructuredOutput(capturedStructuredOutput, JSON.parse(partialStructuredJson));
|
|
398
|
+
} catch {}
|
|
326
399
|
if (partialBlockType === "text") yield* closePartialText();
|
|
327
400
|
else if (partialBlockType === "thinking") yield* closePartialReasoning();
|
|
328
401
|
partialBlockType = null;
|
|
402
|
+
partialIsStructuredOutput = false;
|
|
403
|
+
partialStructuredJson = "";
|
|
329
404
|
}
|
|
330
405
|
}
|
|
331
406
|
try {
|
|
@@ -365,6 +440,6 @@ async function* translateSdkStream(sdkMessages, ctx) {
|
|
|
365
440
|
}
|
|
366
441
|
}
|
|
367
442
|
//#endregion
|
|
368
|
-
export { BRIDGED_MCP_SERVER_NAME, SESSION_ID_EVENT, stripMcpPrefix, translateSdkStream };
|
|
443
|
+
export { BRIDGED_MCP_SERVER_NAME, SESSION_ID_EVENT, SYNTHETIC_STRUCTURED_OUTPUT_TOOL, stripMcpPrefix, translateSdkStream };
|
|
369
444
|
|
|
370
445
|
//# sourceMappingURL=translate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate.js","names":[],"sources":["../../../src/stream/translate.ts"],"sourcesContent":["import { EventType, buildBaseUsage } from '@tanstack/ai'\nimport type { StreamChunk, TokenUsage } from '@tanstack/ai'\nimport type {\n AgentSdkMessage,\n SdkAssistantMessage,\n SdkPartialAssistantMessage,\n SdkResultMessage,\n SdkToolResultContent,\n SdkUsage,\n SdkUserMessage,\n} from './sdk-types'\n\n/** Name of the CUSTOM event carrying the Claude Code session id. */\nexport const SESSION_ID_EVENT = 'claude-code.session-id'\n\n/** Server name used for bridged TanStack tools (model sees `mcp__tanstack__<name>`). */\nexport const BRIDGED_MCP_SERVER_NAME = 'tanstack'\n\nconst BRIDGED_MCP_PREFIX = `mcp__${BRIDGED_MCP_SERVER_NAME}__`\n\n/** Claude Code-specific usage details attached to RUN_FINISHED usage. */\nexport type ClaudeCodeProviderUsageDetails = {\n /** Total cost of the harness run in USD, as reported by Claude Code. */\n totalCostUsd?: number\n}\n\nexport interface TranslateContext {\n model: string\n runId: string\n threadId: string\n parentRunId?: string\n genId: () => string\n /** Called as soon as the harness reports its session id. */\n onSessionId?: (sessionId: string) => void\n /** Called for each raw SDK message, for logging. */\n onSdkMessage?: (message: AgentSdkMessage) => void\n}\n\n/**\n * Strip the bridged MCP server prefix so tool-call events match the TanStack\n * tool names the application registered. Built-in harness tools (Bash, Read,\n * Edit, ...) and foreign MCP tools pass through verbatim.\n */\nexport function stripMcpPrefix(name: string): string {\n return name.startsWith(BRIDGED_MCP_PREFIX)\n ? name.slice(BRIDGED_MCP_PREFIX.length)\n : name\n}\n\nfunction stringifyToolResultContent(\n content: SdkToolResultContent | undefined,\n): string {\n if (content === undefined) return ''\n if (typeof content === 'string') return content\n return content\n .map((block) => (typeof block.text === 'string' ? block.text : ''))\n .join('')\n}\n\nfunction buildUsage(\n usage: SdkUsage | undefined,\n totalCostUsd: number | undefined,\n): TokenUsage<ClaudeCodeProviderUsageDetails> | undefined {\n if (!usage) return undefined\n const promptTokens = usage.input_tokens ?? 0\n const completionTokens = usage.output_tokens ?? 0\n const result = buildBaseUsage<ClaudeCodeProviderUsageDetails>({\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n })\n const cacheWrite = usage.cache_creation_input_tokens\n const cacheRead = usage.cache_read_input_tokens\n const promptTokensDetails = {\n ...(cacheWrite ? { cacheWriteTokens: cacheWrite } : {}),\n ...(cacheRead ? { cachedTokens: cacheRead } : {}),\n }\n if (Object.keys(promptTokensDetails).length > 0) {\n result.promptTokensDetails = promptTokensDetails\n }\n if (totalCostUsd !== undefined) {\n result.providerUsageDetails = { totalCostUsd }\n }\n return result\n}\n\n/**\n * Translate a Claude Code Agent SDK message stream into AG-UI StreamChunk\n * events.\n *\n * The harness runs its own agent loop and executes its own tools, so the\n * translation always ends with `finishReason: 'stop'` (or `'length'` /\n * RUN_ERROR) — never `'tool_calls'`. Harness tool activity is emitted as\n * already-resolved TOOL_CALL_START/ARGS/END + TOOL_CALL_RESULT sequences so\n * UIs can render it, while the TanStack engine never tries to execute them.\n *\n * Invariant: every TOOL_CALL_START is eventually paired with a\n * TOOL_CALL_RESULT (synthesized as `{\"status\":\"interrupted\"}` when the run\n * ends or aborts before the harness reported one) so the engine's\n * pending-tool-call scan on the next request never force-executes them.\n */\nexport async function* translateSdkStream(\n sdkMessages: AsyncIterable<AgentSdkMessage>,\n ctx: TranslateContext,\n): AsyncIterable<StreamChunk> {\n const { model, runId, threadId, genId } = ctx\n const now = () => Date.now()\n\n let runStarted = false\n /** Tool calls started but with no result yet. */\n const unresolvedToolCalls = new Set<string>()\n /** Anthropic message ids whose text/thinking already streamed via partials. */\n const streamedMessageIds = new Set<string>()\n\n // Partial-stream state\n let partialMessageId: string | null = null\n let partialBlockType: string | null = null\n let partialTextMessageId: string | null = null\n let partialTextContent = ''\n let partialTextStarted = false\n let partialReasoningId: string | null = null\n\n function* startRun(): Generator<StreamChunk> {\n if (runStarted) return\n runStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: now(),\n ...(ctx.parentRunId !== undefined && { parentRunId: ctx.parentRunId }),\n }\n }\n\n function* synthesizeUnresolvedResults(): Generator<StreamChunk> {\n for (const toolCallId of unresolvedToolCalls) {\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId,\n messageId: genId(),\n model,\n timestamp: now(),\n content: JSON.stringify({ status: 'interrupted' }),\n }\n }\n unresolvedToolCalls.clear()\n }\n\n function* closePartialText(): Generator<StreamChunk> {\n if (partialTextStarted && partialTextMessageId) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: partialTextMessageId,\n model,\n timestamp: now(),\n }\n }\n partialTextStarted = false\n partialTextMessageId = null\n partialTextContent = ''\n }\n\n function* closePartialReasoning(): Generator<StreamChunk> {\n if (partialReasoningId) {\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: partialReasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: partialReasoningId,\n model,\n timestamp: now(),\n }\n }\n partialReasoningId = null\n }\n\n function* emitToolUse(block: {\n id: string\n name: string\n input: unknown\n }): Generator<StreamChunk> {\n const toolCallName = stripMcpPrefix(block.name)\n const args = JSON.stringify(block.input ?? {})\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId: block.id,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: block.id,\n model,\n timestamp: now(),\n delta: args,\n args,\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: block.id,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n input: block.input ?? {},\n }\n unresolvedToolCalls.add(block.id)\n }\n\n function* handleAssistant(\n message: SdkAssistantMessage,\n ): Generator<StreamChunk> {\n const alreadyStreamed =\n message.message.id !== undefined &&\n streamedMessageIds.has(message.message.id)\n\n for (const block of message.message.content) {\n if (block.type === 'text') {\n if (alreadyStreamed) continue\n const messageId = message.message.id ?? genId()\n const text = (block as { text: string }).text\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: now(),\n delta: text,\n content: text,\n }\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId,\n model,\n timestamp: now(),\n }\n } else if (block.type === 'thinking') {\n if (alreadyStreamed) continue\n const reasoningId = genId()\n const thinking = (block as { thinking: string }).thinking\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningId,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningId,\n delta: thinking,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n } else if (block.type === 'tool_use') {\n yield* emitToolUse(\n block as { id: string; name: string; input: unknown },\n )\n }\n }\n }\n\n function* handleUser(message: SdkUserMessage): Generator<StreamChunk> {\n const content = message.message.content\n if (typeof content === 'string') return\n for (const block of content) {\n if (block.type !== 'tool_result') continue\n const toolResult = block as {\n tool_use_id: string\n content?: SdkToolResultContent\n is_error?: boolean\n }\n unresolvedToolCalls.delete(toolResult.tool_use_id)\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId: toolResult.tool_use_id,\n messageId: genId(),\n model,\n timestamp: now(),\n content: stringifyToolResultContent(toolResult.content),\n ...(toolResult.is_error === true && { state: 'output-error' as const }),\n }\n }\n }\n\n function* handleResult(message: SdkResultMessage): Generator<StreamChunk> {\n yield* closePartialText()\n yield* closePartialReasoning()\n yield* synthesizeUnresolvedResults()\n\n const usage = buildUsage(message.usage, message.total_cost_usd)\n if (message.subtype === 'success') {\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason: 'stop',\n ...(usage !== undefined && { usage }),\n }\n } else if (message.subtype === 'error_max_turns') {\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason: 'length',\n ...(usage !== undefined && { usage }),\n }\n } else {\n const errorMessage =\n message.errors && message.errors.length > 0\n ? message.errors.join('; ')\n : `Claude Code run failed: ${message.subtype}`\n yield {\n type: EventType.RUN_ERROR,\n model,\n timestamp: now(),\n message: errorMessage,\n code: message.subtype,\n error: { message: errorMessage, code: message.subtype },\n }\n }\n }\n\n function* handleStreamEvent(\n message: SdkPartialAssistantMessage,\n ): Generator<StreamChunk> {\n const event = message.event\n if (event.type === 'message_start') {\n partialMessageId = event.message.id ?? genId()\n streamedMessageIds.add(partialMessageId)\n } else if (event.type === 'content_block_start') {\n partialBlockType = event.content_block.type\n if (partialBlockType === 'text') {\n partialTextMessageId = partialMessageId ?? genId()\n partialTextContent = ''\n if (!partialTextStarted) {\n partialTextStarted = true\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: partialTextMessageId,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n }\n } else if (partialBlockType === 'thinking') {\n partialReasoningId = genId()\n yield {\n type: EventType.REASONING_START,\n messageId: partialReasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: partialReasoningId,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n }\n } else if (event.type === 'content_block_delta') {\n if (\n event.delta.type === 'text_delta' &&\n partialTextStarted &&\n partialTextMessageId &&\n typeof event.delta.text === 'string'\n ) {\n partialTextContent += event.delta.text\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: partialTextMessageId,\n model,\n timestamp: now(),\n delta: event.delta.text,\n content: partialTextContent,\n }\n } else if (\n event.delta.type === 'thinking_delta' &&\n partialReasoningId &&\n typeof event.delta.thinking === 'string'\n ) {\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: partialReasoningId,\n delta: event.delta.thinking,\n model,\n timestamp: now(),\n }\n }\n } else if (event.type === 'content_block_stop') {\n if (partialBlockType === 'text') {\n yield* closePartialText()\n } else if (partialBlockType === 'thinking') {\n yield* closePartialReasoning()\n }\n partialBlockType = null\n }\n }\n\n try {\n for await (const sdkMessage of sdkMessages) {\n ctx.onSdkMessage?.(sdkMessage)\n\n if (sdkMessage.type === 'system' && sdkMessage.subtype === 'init') {\n yield* startRun()\n ctx.onSessionId?.(sdkMessage.session_id)\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: SESSION_ID_EVENT,\n value: {\n sessionId: sdkMessage.session_id,\n model: sdkMessage.model,\n tools: sdkMessage.tools,\n },\n }\n continue\n }\n\n // Anything before init still needs RUN_STARTED first.\n yield* startRun()\n\n if (sdkMessage.type === 'stream_event') {\n if (sdkMessage.parent_tool_use_id !== null) continue\n yield* handleStreamEvent(sdkMessage)\n } else if (sdkMessage.type === 'assistant') {\n if (sdkMessage.parent_tool_use_id !== null) continue\n yield* handleAssistant(sdkMessage)\n } else if (sdkMessage.type === 'user') {\n if (sdkMessage.parent_tool_use_id !== null) continue\n yield* handleUser(sdkMessage)\n } else if (sdkMessage.type === 'result') {\n yield* handleResult(sdkMessage)\n }\n // All other SDK message types (status, hooks, notifications, ...) are\n // harness-internal and intentionally ignored.\n }\n } catch (error) {\n // The run is dying (abort or SDK failure). Pair any started tool calls\n // with a synthetic result first so the next request's pending-tool-call\n // scan doesn't try to execute them, then let the adapter surface the\n // error as RUN_ERROR.\n yield* synthesizeUnresolvedResults()\n throw error\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"translate.js","names":[],"sources":["../../../src/stream/translate.ts"],"sourcesContent":["import { EventType, buildBaseUsage } from '@tanstack/ai'\nimport {\n parseJsonFromAssistantText,\n structuredOutputCompleteChunk,\n structuredOutputStartChunk,\n} from '@tanstack/ai/adapter-internals'\nimport type { StreamChunk, TokenUsage } from '@tanstack/ai'\nimport type {\n AgentSdkMessage,\n SdkAssistantMessage,\n SdkPartialAssistantMessage,\n SdkResultMessage,\n SdkToolResultContent,\n SdkUsage,\n SdkUserMessage,\n} from './sdk-types'\n\n/** Name of the CUSTOM event carrying the Claude Code session id. */\nexport const SESSION_ID_EVENT = 'claude-code.session-id'\n\n/** Server name used for bridged TanStack tools (model sees `mcp__tanstack__<name>`). */\nexport const BRIDGED_MCP_SERVER_NAME = 'tanstack'\n\nconst BRIDGED_MCP_PREFIX = `mcp__${BRIDGED_MCP_SERVER_NAME}__`\n\n/**\n * Claude Code `--json-schema` injects a fake tool named StructuredOutput.\n * The model \"calls\" it with the schema JSON. The result message may omit\n * `structured_output`; harvest the tool input in that case.\n */\nexport const SYNTHETIC_STRUCTURED_OUTPUT_TOOL = 'StructuredOutput'\n\n/** Claude Code-specific usage details attached to RUN_FINISHED usage. */\nexport type ClaudeCodeProviderUsageDetails = {\n /** Total cost of the harness run in USD, as reported by Claude Code. */\n totalCostUsd?: number\n}\n\nexport interface TranslateContext {\n model: string\n runId: string\n threadId: string\n parentRunId?: string\n genId: () => string\n /** Called as soon as the harness reports its session id. */\n onSessionId?: (sessionId: string) => void\n /** Called for each raw SDK message, for logging. */\n onSdkMessage?: (message: AgentSdkMessage) => void\n /** Emit structured-output events from `result.structured_output`. */\n expectStructuredOutput?: boolean\n}\n\n/**\n * Strip the bridged MCP server prefix so tool-call events match the TanStack\n * tool names the application registered. Built-in harness tools (Bash, Read,\n * Edit, ...) and foreign MCP tools pass through verbatim.\n */\nexport function stripMcpPrefix(name: string): string {\n return name.startsWith(BRIDGED_MCP_PREFIX)\n ? name.slice(BRIDGED_MCP_PREFIX.length)\n : name\n}\n\nfunction isUsefulStructuredObject(value: unknown): boolean {\n return (\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n Object.keys(value).length > 0\n )\n}\n\nfunction rememberStructuredOutput(current: unknown, next: unknown): unknown {\n return isUsefulStructuredObject(next) ? next : current\n}\n\nfunction stringifyToolResultContent(\n content: SdkToolResultContent | undefined,\n): string {\n if (content === undefined) return ''\n if (typeof content === 'string') return content\n return content\n .map((block) => (typeof block.text === 'string' ? block.text : ''))\n .join('')\n}\n\nfunction buildUsage(\n usage: SdkUsage | undefined,\n totalCostUsd: number | undefined,\n): TokenUsage<ClaudeCodeProviderUsageDetails> | undefined {\n if (!usage) return undefined\n const promptTokens = usage.input_tokens ?? 0\n const completionTokens = usage.output_tokens ?? 0\n const result = buildBaseUsage<ClaudeCodeProviderUsageDetails>({\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n })\n const cacheWrite = usage.cache_creation_input_tokens\n const cacheRead = usage.cache_read_input_tokens\n const promptTokensDetails = {\n ...(cacheWrite ? { cacheWriteTokens: cacheWrite } : {}),\n ...(cacheRead ? { cachedTokens: cacheRead } : {}),\n }\n if (Object.keys(promptTokensDetails).length > 0) {\n result.promptTokensDetails = promptTokensDetails\n }\n if (totalCostUsd !== undefined) {\n result.providerUsageDetails = { totalCostUsd }\n }\n return result\n}\n\n/**\n * Translate a Claude Code Agent SDK message stream into AG-UI StreamChunk\n * events.\n *\n * The harness runs its own agent loop and executes its own tools, so the\n * translation always ends with `finishReason: 'stop'` (or `'length'` /\n * RUN_ERROR) — never `'tool_calls'`. Harness tool activity is emitted as\n * already-resolved TOOL_CALL_START/ARGS/END + TOOL_CALL_RESULT sequences so\n * UIs can render it, while the TanStack engine never tries to execute them.\n *\n * Invariant: every TOOL_CALL_START is eventually paired with a\n * TOOL_CALL_RESULT (synthesized as `{\"status\":\"interrupted\"}` when the run\n * ends or aborts before the harness reported one) so the engine's\n * pending-tool-call scan on the next request never force-executes them.\n */\nexport async function* translateSdkStream(\n sdkMessages: AsyncIterable<AgentSdkMessage>,\n ctx: TranslateContext,\n): AsyncIterable<StreamChunk> {\n const { model, runId, threadId, genId } = ctx\n const now = () => Date.now()\n\n let runStarted = false\n /** Tool calls started but with no result yet. */\n const unresolvedToolCalls = new Set<string>()\n const syntheticOutputToolIds = new Set<string>()\n let capturedStructuredOutput: unknown\n let assistantTextForHarvest = ''\n let partialStructuredJson = ''\n let partialIsStructuredOutput = false\n /** Anthropic message ids whose text/thinking already streamed via partials. */\n const streamedMessageIds = new Set<string>()\n\n // Partial-stream state\n let partialMessageId: string | null = null\n let partialBlockType: string | null = null\n let partialTextMessageId: string | null = null\n let partialTextContent = ''\n let partialTextStarted = false\n let partialReasoningId: string | null = null\n\n function* startRun(): Generator<StreamChunk> {\n if (runStarted) return\n runStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: now(),\n ...(ctx.parentRunId !== undefined && { parentRunId: ctx.parentRunId }),\n }\n }\n\n function* synthesizeUnresolvedResults(): Generator<StreamChunk> {\n for (const toolCallId of unresolvedToolCalls) {\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId,\n messageId: genId(),\n model,\n timestamp: now(),\n content: JSON.stringify({ status: 'interrupted' }),\n }\n }\n unresolvedToolCalls.clear()\n }\n\n function* closePartialText(): Generator<StreamChunk> {\n if (partialTextStarted && partialTextMessageId) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: partialTextMessageId,\n model,\n timestamp: now(),\n }\n }\n partialTextStarted = false\n partialTextMessageId = null\n partialTextContent = ''\n }\n\n function* closePartialReasoning(): Generator<StreamChunk> {\n if (partialReasoningId) {\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: partialReasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: partialReasoningId,\n model,\n timestamp: now(),\n }\n }\n partialReasoningId = null\n }\n\n function* emitStructuredOutput(object: unknown): Generator<StreamChunk> {\n const raw = JSON.stringify(object)\n const messageId = genId()\n yield structuredOutputStartChunk({\n messageId,\n model,\n threadId,\n runId,\n })\n yield structuredOutputCompleteChunk({\n messageId,\n model,\n threadId,\n runId,\n object,\n raw,\n })\n }\n\n function* emitToolUse(block: {\n id: string\n name: string\n input: unknown\n }): Generator<StreamChunk> {\n const toolCallName = stripMcpPrefix(block.name)\n if (\n ctx.expectStructuredOutput === true &&\n toolCallName === SYNTHETIC_STRUCTURED_OUTPUT_TOOL\n ) {\n capturedStructuredOutput = rememberStructuredOutput(\n capturedStructuredOutput,\n block.input,\n )\n syntheticOutputToolIds.add(block.id)\n return\n }\n const args = JSON.stringify(block.input ?? {})\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId: block.id,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: block.id,\n model,\n timestamp: now(),\n delta: args,\n args,\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: block.id,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n input: block.input ?? {},\n }\n unresolvedToolCalls.add(block.id)\n }\n\n function* handleAssistant(\n message: SdkAssistantMessage,\n ): Generator<StreamChunk> {\n const alreadyStreamed =\n message.message.id !== undefined &&\n streamedMessageIds.has(message.message.id)\n\n for (const block of message.message.content) {\n if (block.type === 'text') {\n if (alreadyStreamed) continue\n const messageId = message.message.id ?? genId()\n const text = (block as { text: string }).text\n if (!alreadyStreamed) assistantTextForHarvest += text\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: now(),\n delta: text,\n content: text,\n }\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId,\n model,\n timestamp: now(),\n }\n } else if (block.type === 'thinking') {\n if (alreadyStreamed) continue\n const reasoningId = genId()\n const thinking = (block as { thinking: string }).thinking\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningId,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningId,\n delta: thinking,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n } else if (block.type === 'tool_use') {\n yield* emitToolUse(\n block as { id: string; name: string; input: unknown },\n )\n }\n }\n }\n\n function* handleUser(message: SdkUserMessage): Generator<StreamChunk> {\n const content = message.message.content\n if (typeof content === 'string') return\n for (const block of content) {\n if (block.type !== 'tool_result') continue\n const toolResult = block as {\n tool_use_id: string\n content?: SdkToolResultContent\n is_error?: boolean\n }\n if (syntheticOutputToolIds.has(toolResult.tool_use_id)) {\n syntheticOutputToolIds.delete(toolResult.tool_use_id)\n continue\n }\n unresolvedToolCalls.delete(toolResult.tool_use_id)\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId: toolResult.tool_use_id,\n messageId: genId(),\n model,\n timestamp: now(),\n content: stringifyToolResultContent(toolResult.content),\n ...(toolResult.is_error === true && { state: 'output-error' as const }),\n }\n }\n }\n\n function* handleResult(message: SdkResultMessage): Generator<StreamChunk> {\n yield* closePartialText()\n yield* closePartialReasoning()\n yield* synthesizeUnresolvedResults()\n\n const usage = buildUsage(message.usage, message.total_cost_usd)\n if (ctx.expectStructuredOutput === true) {\n const fromResult = isUsefulStructuredObject(message.structured_output)\n ? message.structured_output\n : undefined\n const fromTool = isUsefulStructuredObject(capturedStructuredOutput)\n ? capturedStructuredOutput\n : undefined\n let fromText: unknown\n if (fromResult === undefined && fromTool === undefined) {\n const raw = assistantTextForHarvest || message.result || ''\n if (raw.trim() !== '') {\n try {\n fromText = parseJsonFromAssistantText(raw)\n } catch {\n fromText = undefined\n }\n }\n }\n const object = fromResult ?? fromTool ?? fromText\n if (isUsefulStructuredObject(object)) {\n yield* emitStructuredOutput(object)\n }\n }\n if (message.subtype === 'success') {\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason: 'stop',\n ...(usage !== undefined && { usage }),\n }\n } else if (message.subtype === 'error_max_turns') {\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason: 'length',\n ...(usage !== undefined && { usage }),\n }\n } else {\n const errorMessage =\n message.errors && message.errors.length > 0\n ? message.errors.join('; ')\n : `Claude Code run failed: ${message.subtype}`\n yield {\n type: EventType.RUN_ERROR,\n model,\n timestamp: now(),\n message: errorMessage,\n code: message.subtype,\n error: { message: errorMessage, code: message.subtype },\n }\n }\n }\n\n function* handleStreamEvent(\n message: SdkPartialAssistantMessage,\n ): Generator<StreamChunk> {\n const event = message.event\n if (event.type === 'message_start') {\n partialMessageId = event.message.id ?? genId()\n streamedMessageIds.add(partialMessageId)\n } else if (event.type === 'content_block_start') {\n partialBlockType = event.content_block.type\n const startedBlock = event.content_block\n partialIsStructuredOutput =\n ctx.expectStructuredOutput === true &&\n startedBlock.type === 'tool_use' &&\n 'name' in startedBlock &&\n startedBlock.name === SYNTHETIC_STRUCTURED_OUTPUT_TOOL\n if (partialIsStructuredOutput) {\n partialStructuredJson = ''\n if ('id' in startedBlock && typeof startedBlock.id === 'string') {\n syntheticOutputToolIds.add(startedBlock.id)\n }\n if ('input' in startedBlock) {\n capturedStructuredOutput = rememberStructuredOutput(\n capturedStructuredOutput,\n startedBlock.input,\n )\n }\n }\n if (partialBlockType === 'text') {\n partialTextMessageId = partialMessageId ?? genId()\n partialTextContent = ''\n if (!partialTextStarted) {\n partialTextStarted = true\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: partialTextMessageId,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n }\n } else if (partialBlockType === 'thinking') {\n partialReasoningId = genId()\n yield {\n type: EventType.REASONING_START,\n messageId: partialReasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: partialReasoningId,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n }\n } else if (event.type === 'content_block_delta') {\n if (\n event.delta.type === 'text_delta' &&\n partialTextStarted &&\n partialTextMessageId &&\n typeof event.delta.text === 'string'\n ) {\n partialTextContent += event.delta.text\n assistantTextForHarvest += event.delta.text\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: partialTextMessageId,\n model,\n timestamp: now(),\n delta: event.delta.text,\n content: partialTextContent,\n }\n } else if (\n event.delta.type === 'input_json_delta' &&\n partialIsStructuredOutput &&\n typeof event.delta.partial_json === 'string'\n ) {\n partialStructuredJson += event.delta.partial_json\n } else if (\n event.delta.type === 'thinking_delta' &&\n partialReasoningId &&\n typeof event.delta.thinking === 'string'\n ) {\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: partialReasoningId,\n delta: event.delta.thinking,\n model,\n timestamp: now(),\n }\n }\n } else if (event.type === 'content_block_stop') {\n if (partialIsStructuredOutput && partialStructuredJson !== '') {\n try {\n capturedStructuredOutput = rememberStructuredOutput(\n capturedStructuredOutput,\n JSON.parse(partialStructuredJson),\n )\n } catch {\n // Incomplete JSON; the complete assistant tool_use may still arrive.\n }\n }\n if (partialBlockType === 'text') {\n yield* closePartialText()\n } else if (partialBlockType === 'thinking') {\n yield* closePartialReasoning()\n }\n partialBlockType = null\n partialIsStructuredOutput = false\n partialStructuredJson = ''\n }\n }\n\n try {\n for await (const sdkMessage of sdkMessages) {\n ctx.onSdkMessage?.(sdkMessage)\n\n if (sdkMessage.type === 'system' && sdkMessage.subtype === 'init') {\n yield* startRun()\n ctx.onSessionId?.(sdkMessage.session_id)\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: SESSION_ID_EVENT,\n value: {\n sessionId: sdkMessage.session_id,\n model: sdkMessage.model,\n tools: sdkMessage.tools,\n },\n }\n continue\n }\n\n // Anything before init still needs RUN_STARTED first.\n yield* startRun()\n\n if (sdkMessage.type === 'stream_event') {\n if (sdkMessage.parent_tool_use_id !== null) continue\n yield* handleStreamEvent(sdkMessage)\n } else if (sdkMessage.type === 'assistant') {\n if (sdkMessage.parent_tool_use_id !== null) continue\n yield* handleAssistant(sdkMessage)\n } else if (sdkMessage.type === 'user') {\n if (sdkMessage.parent_tool_use_id !== null) continue\n yield* handleUser(sdkMessage)\n } else if (sdkMessage.type === 'result') {\n yield* handleResult(sdkMessage)\n }\n // All other SDK message types (status, hooks, notifications, ...) are\n // harness-internal and intentionally ignored.\n }\n } catch (error) {\n // The run is dying (abort or SDK failure). Pair any started tool calls\n // with a synthetic result first so the next request's pending-tool-call\n // scan doesn't try to execute them, then let the adapter surface the\n // error as RUN_ERROR.\n yield* synthesizeUnresolvedResults()\n throw error\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-claude-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Claude Code harness adapter for TanStack AI — run Claude Code as a chat backend with local tool execution and stateful sessions.",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"src"
|
|
38
38
|
],
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@tanstack/ai": "^0.
|
|
41
|
-
"@tanstack/ai-sandbox": "^0.3.
|
|
40
|
+
"@tanstack/ai": "^0.45.0",
|
|
41
|
+
"@tanstack/ai-sandbox": "^0.3.3"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@vitest/coverage-v8": "4.
|
|
45
|
-
"@tanstack/ai": "0.
|
|
46
|
-
"@tanstack/ai-sandbox": "0.3.
|
|
44
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
45
|
+
"@tanstack/ai": "0.45.0",
|
|
46
|
+
"@tanstack/ai-sandbox": "0.3.3",
|
|
47
47
|
"@tanstack/ai-sandbox-local-process": "0.2.1"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Placeholder swapped for the schema JSON after the runner reads the files. */
|
|
2
|
+
export const CLAUDE_JSON_SCHEMA_PLACEHOLDER = '__TANSTACK_SCHEMA__'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Written into the sandbox and run with `node`.
|
|
6
|
+
*
|
|
7
|
+
* The shell only sees two filenames. The runner reads the argv array and the
|
|
8
|
+
* schema JSON from those files, then spawn()s claude so `--json-schema` is a
|
|
9
|
+
* real argv value (the CLI rejects a file path).
|
|
10
|
+
*
|
|
11
|
+
* On Windows, `claude` is often a `.cmd` shim. `spawn(cmd, args)` without a
|
|
12
|
+
* shell cannot find that shim, so we go through `sh` with `"$0"` / `"$@"`
|
|
13
|
+
* (git-bash is already on PATH in the local-process sandbox).
|
|
14
|
+
*/
|
|
15
|
+
export const CLAUDE_RUNNER_SOURCE = `import { spawn } from 'node:child_process'
|
|
16
|
+
import { readFileSync } from 'node:fs'
|
|
17
|
+
|
|
18
|
+
const argvFile = process.argv[2]
|
|
19
|
+
if (!argvFile) {
|
|
20
|
+
console.error('tanstack-claude-run: missing argv file')
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const argv = JSON.parse(readFileSync(argvFile, 'utf8'))
|
|
25
|
+
if (!Array.isArray(argv)) {
|
|
26
|
+
console.error('tanstack-claude-run: argv file must be a JSON array')
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const schemaFile = process.argv[3]
|
|
31
|
+
if (schemaFile) {
|
|
32
|
+
const schema = readFileSync(schemaFile, 'utf8')
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
if (argv[i] === ${JSON.stringify(CLAUDE_JSON_SCHEMA_PLACEHOLDER)}) argv[i] = schema
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [cmd, ...args] = argv
|
|
39
|
+
if (typeof cmd !== 'string' || cmd === '') {
|
|
40
|
+
console.error('tanstack-claude-run: missing command')
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const opts = {
|
|
45
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
46
|
+
env: process.env,
|
|
47
|
+
windowsHide: true,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const child =
|
|
51
|
+
process.platform === 'win32'
|
|
52
|
+
? spawn('sh', ['-c', 'exec "$0" "$@"', cmd, ...args], opts)
|
|
53
|
+
: spawn(cmd, args, opts)
|
|
54
|
+
|
|
55
|
+
process.stdin.pipe(child.stdin)
|
|
56
|
+
child.stdout.pipe(process.stdout)
|
|
57
|
+
child.stderr.pipe(process.stderr)
|
|
58
|
+
child.on('error', (error) => {
|
|
59
|
+
console.error(error)
|
|
60
|
+
process.exit(1)
|
|
61
|
+
})
|
|
62
|
+
child.on('exit', (code, signal) => {
|
|
63
|
+
if (signal) process.exit(1)
|
|
64
|
+
process.exit(code ?? 1)
|
|
65
|
+
})
|
|
66
|
+
`
|
package/src/adapters/text.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { EventType, normalizeSystemPrompts } from '@tanstack/ai'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
appendOutputSchemaInstruction,
|
|
4
|
+
toRunErrorRawEvent,
|
|
5
|
+
} from '@tanstack/ai/adapter-internals'
|
|
3
6
|
import { BaseTextAdapter } from '@tanstack/ai/adapters'
|
|
4
7
|
import {
|
|
5
8
|
SandboxCapability,
|
|
@@ -26,6 +29,11 @@ import { buildPrompt } from '../messages/prompt'
|
|
|
26
29
|
import { translateSdkStream } from '../stream/translate'
|
|
27
30
|
import { mapPolicyToClaudeFlags } from './policy-map'
|
|
28
31
|
import { projectClaudeWorkspace } from './projection'
|
|
32
|
+
import {
|
|
33
|
+
CLAUDE_JSON_SCHEMA_PLACEHOLDER,
|
|
34
|
+
CLAUDE_RUNNER_SOURCE,
|
|
35
|
+
} from './claude-run-source'
|
|
36
|
+
|
|
29
37
|
import type { ClaudePolicyFlags } from './policy-map'
|
|
30
38
|
import type {
|
|
31
39
|
BridgeEventChannel,
|
|
@@ -89,6 +97,12 @@ export interface ClaudeCodeTextConfig {
|
|
|
89
97
|
streamPartials?: boolean
|
|
90
98
|
/** Extra environment variables for the claude process inside the sandbox. */
|
|
91
99
|
env?: Record<string, string>
|
|
100
|
+
/**
|
|
101
|
+
* `'api-key'` (default) injects `ANTHROPIC_API_KEY`.
|
|
102
|
+
* `'host'` uses `claude login` and does not inject the key.
|
|
103
|
+
* Not inferred from the sandbox.
|
|
104
|
+
*/
|
|
105
|
+
authMode?: 'host' | 'api-key'
|
|
92
106
|
/** Emit a `file.changed` CUSTOM event with the git diff after the run (default true). */
|
|
93
107
|
emitDiff?: boolean
|
|
94
108
|
}
|
|
@@ -98,6 +112,27 @@ function q(value: string): string {
|
|
|
98
112
|
return `'${value.replace(/'/g, `'\\''`)}'`
|
|
99
113
|
}
|
|
100
114
|
|
|
115
|
+
/** Copy host Anthropic auth into the sandbox process. Docker `exec` Env replaces the container env, so a key set only at create time can vanish. */
|
|
116
|
+
function hostClaudeAuthEnv(): Record<string, string> {
|
|
117
|
+
const env: Record<string, string> = {}
|
|
118
|
+
const apiKey = process.env.ANTHROPIC_API_KEY
|
|
119
|
+
if (apiKey) env.ANTHROPIC_API_KEY = apiKey
|
|
120
|
+
const authToken = process.env.ANTHROPIC_AUTH_TOKEN
|
|
121
|
+
if (authToken) env.ANTHROPIC_AUTH_TOKEN = authToken
|
|
122
|
+
return env
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Windows Node often has USERPROFILE but no HOME. Claude then cannot find
|
|
127
|
+
* `~/.claude.json` (the `claude login` file) and prints "Not logged in".
|
|
128
|
+
*/
|
|
129
|
+
function localProcessHomeEnv(provider: string): Record<string, string> {
|
|
130
|
+
if (provider !== 'local-process') return {}
|
|
131
|
+
if (process.env.HOME) return {}
|
|
132
|
+
const home = process.env.USERPROFILE
|
|
133
|
+
return home ? { HOME: home } : {}
|
|
134
|
+
}
|
|
135
|
+
|
|
101
136
|
/** Format a host tool-bridge as claude's `--mcp-config` JSON. */
|
|
102
137
|
function bridgeToMcpConfig(bridge: HostToolBridge): string {
|
|
103
138
|
return JSON.stringify({
|
|
@@ -153,56 +188,71 @@ export class ClaudeCodeTextAdapter<
|
|
|
153
188
|
)
|
|
154
189
|
}
|
|
155
190
|
|
|
191
|
+
supportsCombinedToolsAndSchema(): boolean {
|
|
192
|
+
return true
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
combinedStructuredOutputSource(): 'event' {
|
|
196
|
+
return 'event'
|
|
197
|
+
}
|
|
198
|
+
|
|
156
199
|
/** Build the `claude` command line (prompt goes via stdin, not argv). */
|
|
157
|
-
private
|
|
200
|
+
private buildArgv(
|
|
158
201
|
options: TextOptions<ClaudeCodeTextProviderOptions>,
|
|
159
202
|
resume: string | undefined,
|
|
160
203
|
policyFlags: ClaudePolicyFlags,
|
|
161
204
|
mcpConfigPath: string | undefined,
|
|
162
205
|
permissionPromptTool: string | undefined,
|
|
163
|
-
|
|
206
|
+
hasJsonSchema: boolean,
|
|
207
|
+
): Array<string> {
|
|
164
208
|
const config = this.adapterConfig
|
|
165
209
|
const modelOptions = options.modelOptions
|
|
166
|
-
const
|
|
210
|
+
const exeParts = (config.claudeExecutable ?? 'claude').split(' ')
|
|
167
211
|
|
|
212
|
+
// `--setting-sources user` before `-p`. Do not pass `--bare`: that flag
|
|
213
|
+
// skips stored `claude login` credentials and prints
|
|
214
|
+
// "Not logged in · Please run /login" (claude-code#51047).
|
|
215
|
+
// `-p` can take the next token as the prompt, so these flags stay first.
|
|
168
216
|
const args: Array<string> = [
|
|
217
|
+
...exeParts,
|
|
218
|
+
'--setting-sources',
|
|
219
|
+
'user',
|
|
169
220
|
'-p',
|
|
170
221
|
'--output-format',
|
|
171
222
|
'stream-json',
|
|
172
223
|
'--verbose',
|
|
173
224
|
'--model',
|
|
174
|
-
|
|
225
|
+
this.model,
|
|
175
226
|
]
|
|
176
227
|
|
|
177
228
|
if (config.streamPartials !== false) args.push('--include-partial-messages')
|
|
178
|
-
if (resume !== undefined) args.push('--resume',
|
|
229
|
+
if (resume !== undefined) args.push('--resume', resume)
|
|
179
230
|
|
|
180
|
-
// Precedence: per-call modelOptions > adapter config > policy > sandbox default.
|
|
181
231
|
const permissionMode =
|
|
182
232
|
modelOptions?.permissionMode ??
|
|
183
233
|
config.permissionMode ??
|
|
184
234
|
policyFlags.permissionMode ??
|
|
185
235
|
'bypassPermissions'
|
|
186
|
-
args.push('--permission-mode',
|
|
236
|
+
args.push('--permission-mode', permissionMode)
|
|
187
237
|
|
|
188
238
|
const maxTurns = modelOptions?.maxTurns ?? config.maxTurns
|
|
189
239
|
if (maxTurns !== undefined) args.push('--max-turns', String(maxTurns))
|
|
190
240
|
|
|
191
|
-
for (const dir of config.addDirs ?? []) args.push('--add-dir',
|
|
241
|
+
for (const dir of config.addDirs ?? []) args.push('--add-dir', dir)
|
|
192
242
|
|
|
193
243
|
const allowedTools = [
|
|
194
244
|
...(modelOptions?.allowedTools ?? config.allowedTools ?? []),
|
|
195
245
|
...policyFlags.allowedTools,
|
|
196
246
|
]
|
|
197
247
|
if (allowedTools.length > 0) {
|
|
198
|
-
args.push('--allowedTools',
|
|
248
|
+
args.push('--allowedTools', [...new Set(allowedTools)].join(','))
|
|
199
249
|
}
|
|
200
250
|
const disallowedTools = [
|
|
201
251
|
...(modelOptions?.disallowedTools ?? config.disallowedTools ?? []),
|
|
202
252
|
...policyFlags.disallowedTools,
|
|
203
253
|
]
|
|
204
254
|
if (disallowedTools.length > 0) {
|
|
205
|
-
args.push('--disallowedTools',
|
|
255
|
+
args.push('--disallowedTools', [...new Set(disallowedTools)].join(','))
|
|
206
256
|
}
|
|
207
257
|
|
|
208
258
|
const systemPrompts = normalizeSystemPrompts(options.systemPrompts)
|
|
@@ -214,15 +264,18 @@ export class ClaudeCodeTextAdapter<
|
|
|
214
264
|
config.systemPromptMode === 'replace'
|
|
215
265
|
? '--system-prompt'
|
|
216
266
|
: '--append-system-prompt'
|
|
217
|
-
args.push(flag,
|
|
267
|
+
args.push(flag, joined)
|
|
218
268
|
}
|
|
219
269
|
|
|
220
|
-
if (mcpConfigPath !== undefined) args.push('--mcp-config',
|
|
270
|
+
if (mcpConfigPath !== undefined) args.push('--mcp-config', mcpConfigPath)
|
|
271
|
+
if (hasJsonSchema) {
|
|
272
|
+
args.push('--json-schema', CLAUDE_JSON_SCHEMA_PLACEHOLDER)
|
|
273
|
+
}
|
|
221
274
|
if (permissionPromptTool !== undefined) {
|
|
222
|
-
args.push('--permission-prompt-tool',
|
|
275
|
+
args.push('--permission-prompt-tool', permissionPromptTool)
|
|
223
276
|
}
|
|
224
277
|
|
|
225
|
-
return
|
|
278
|
+
return args
|
|
226
279
|
}
|
|
227
280
|
|
|
228
281
|
/**
|
|
@@ -308,6 +361,7 @@ export class ClaudeCodeTextAdapter<
|
|
|
308
361
|
const sandbox = this.sandboxFrom(options)
|
|
309
362
|
cleanupSandbox = sandbox
|
|
310
363
|
const cwd = this.workdir(options)
|
|
364
|
+
|
|
311
365
|
// Durability comes from `withSandbox(sandbox, { runs, durability })`, read
|
|
312
366
|
// back off the capability bus. Absent it, everything below resolves to
|
|
313
367
|
// exactly today's behavior (no journal option, no alignment, and a
|
|
@@ -390,10 +444,15 @@ export class ClaudeCodeTextAdapter<
|
|
|
390
444
|
})
|
|
391
445
|
}
|
|
392
446
|
|
|
393
|
-
const
|
|
447
|
+
const built = buildPrompt(
|
|
394
448
|
options.messages,
|
|
395
449
|
options.modelOptions?.sessionId,
|
|
396
450
|
)
|
|
451
|
+
const resume = built.resume
|
|
452
|
+
const prompt =
|
|
453
|
+
options.outputSchema !== undefined
|
|
454
|
+
? appendOutputSchemaInstruction(built.prompt, options.outputSchema)
|
|
455
|
+
: built.prompt
|
|
397
456
|
// Both files below name themselves after `runId`, and durability makes
|
|
398
457
|
// `runId` CALLER-chosen. Raw, a `/` in it would silently turn each basename
|
|
399
458
|
// into a nested path (writing outside the intended directory, or failing on
|
|
@@ -421,7 +480,17 @@ export class ClaudeCodeTextAdapter<
|
|
|
421
480
|
tempFiles.push(mcpConfigPath)
|
|
422
481
|
mcpConfigArg = mcpConfigFile
|
|
423
482
|
}
|
|
424
|
-
|
|
483
|
+
let jsonSchemaFile: string | undefined
|
|
484
|
+
if (options.outputSchema !== undefined) {
|
|
485
|
+
jsonSchemaFile = `tanstack-output-schema-${runIdSegment}.json`
|
|
486
|
+
const schemaPath = `${cwd}/${jsonSchemaFile}`
|
|
487
|
+
await sandbox.fs.write(schemaPath, JSON.stringify(options.outputSchema))
|
|
488
|
+
tempFiles.push(schemaPath)
|
|
489
|
+
}
|
|
490
|
+
const runnerFile = `tanstack-claude-run-${runIdSegment}.mjs`
|
|
491
|
+
await sandbox.fs.write(`${cwd}/${runnerFile}`, CLAUDE_RUNNER_SOURCE)
|
|
492
|
+
tempFiles.push(`${cwd}/${runnerFile}`)
|
|
493
|
+
const argv = this.buildArgv(
|
|
425
494
|
options,
|
|
426
495
|
resume,
|
|
427
496
|
mapPolicyToClaudeFlags(policy),
|
|
@@ -429,7 +498,17 @@ export class ClaudeCodeTextAdapter<
|
|
|
429
498
|
bridge && permission
|
|
430
499
|
? `mcp__${bridge.name}__${permission.toolName}`
|
|
431
500
|
: undefined,
|
|
501
|
+
jsonSchemaFile !== undefined,
|
|
432
502
|
)
|
|
503
|
+
// Filenames only on the shell line. JSON (schema, system prompt, MCP
|
|
504
|
+
// config path) lives in the argv file so git-bash cannot retokenize it.
|
|
505
|
+
const argvFile = `tanstack-claude-argv-${runIdSegment}.json`
|
|
506
|
+
await sandbox.fs.write(`${cwd}/${argvFile}`, JSON.stringify(argv))
|
|
507
|
+
tempFiles.push(`${cwd}/${argvFile}`)
|
|
508
|
+
const command =
|
|
509
|
+
jsonSchemaFile === undefined
|
|
510
|
+
? `node ${q(runnerFile)} ${q(argvFile)}`
|
|
511
|
+
: `node ${q(runnerFile)} ${q(argvFile)} ${q(jsonSchemaFile)}`
|
|
433
512
|
|
|
434
513
|
// Deliver the prompt. The default feeds it over stdin (keeps it out of
|
|
435
514
|
// argv). Providers without a writable host→process stdin (e.g. Cloudflare)
|
|
@@ -450,17 +529,31 @@ export class ClaudeCodeTextAdapter<
|
|
|
450
529
|
{ provider: 'claude-code', model: this.model },
|
|
451
530
|
)
|
|
452
531
|
|
|
532
|
+
const authMode =
|
|
533
|
+
options.modelOptions?.authMode ??
|
|
534
|
+
this.adapterConfig.authMode ??
|
|
535
|
+
'api-key'
|
|
536
|
+
const injectApiKey = authMode === 'api-key'
|
|
537
|
+
|
|
453
538
|
const journalOptions = journalOptionsFor(durability, runId)
|
|
454
539
|
const rawEvents = spawnNdjson(sandbox, runCommand, {
|
|
455
540
|
cwd,
|
|
456
541
|
...(stdinInput !== undefined ? { input: stdinInput } : {}),
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
542
|
+
// Isolated sandboxes often run as root. Claude refuses
|
|
543
|
+
// `--dangerously-skip-permissions` as root unless IS_SANDBOX=1.
|
|
544
|
+
// CLAUDE_CODE_SANDBOXED marks a real isolation boundary. Do not set
|
|
545
|
+
// either on local-process: that provider runs on the host.
|
|
546
|
+
env: {
|
|
547
|
+
...(sandbox.provider === 'local-process'
|
|
548
|
+
? {}
|
|
549
|
+
: {
|
|
550
|
+
IS_SANDBOX: '1',
|
|
551
|
+
CLAUDE_CODE_SANDBOXED: '1',
|
|
552
|
+
}),
|
|
553
|
+
...(injectApiKey ? hostClaudeAuthEnv() : {}),
|
|
554
|
+
...localProcessHomeEnv(sandbox.provider),
|
|
555
|
+
...this.adapterConfig.env,
|
|
556
|
+
},
|
|
464
557
|
...(options.abortController?.signal
|
|
465
558
|
? { signal: options.abortController.signal }
|
|
466
559
|
: options.request?.signal
|
|
@@ -509,6 +602,7 @@ export class ClaudeCodeTextAdapter<
|
|
|
509
602
|
// which mixes in `Date.now()` / `Math.random()`). See
|
|
510
603
|
// `createRunScopedIdGen` in `@tanstack/ai-sandbox`.
|
|
511
604
|
genId: createRunScopedIdGen(runId),
|
|
605
|
+
...(options.outputSchema ? { expectStructuredOutput: true } : {}),
|
|
512
606
|
onSdkMessage: (message) =>
|
|
513
607
|
logger.provider(`provider=claude-code type=${message.type}`, {
|
|
514
608
|
chunk: message,
|
|
@@ -585,8 +679,8 @@ export class ClaudeCodeTextAdapter<
|
|
|
585
679
|
): Promise<StructuredOutputResult<unknown>> {
|
|
586
680
|
return Promise.reject(
|
|
587
681
|
new Error(
|
|
588
|
-
'
|
|
589
|
-
'
|
|
682
|
+
'This harness honors outputSchema on chat() in the same turn. ' +
|
|
683
|
+
'Pass outputSchema to chat(), or use a model adapter for a one-shot extract.',
|
|
590
684
|
),
|
|
591
685
|
)
|
|
592
686
|
}
|