@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.
@@ -27,4 +27,10 @@ export interface ClaudeCodeTextProviderOptions {
27
27
  disallowedTools?: Array<string>
28
28
  /** Per-call override of the harness working directory. */
29
29
  cwd?: string
30
+ /**
31
+ * `'api-key'` (default) injects `ANTHROPIC_API_KEY`.
32
+ * `'host'` uses `claude login`.
33
+ * Not inferred from the sandbox.
34
+ */
35
+ authMode?: 'host' | 'api-key'
30
36
  }
@@ -60,12 +60,22 @@ export type SdkRawStreamEvent =
60
60
  | {
61
61
  type: 'content_block_start'
62
62
  index: number
63
- content_block: { type: string }
63
+ content_block: {
64
+ type: string
65
+ id?: string
66
+ name?: string
67
+ input?: unknown
68
+ }
64
69
  }
65
70
  | {
66
71
  type: 'content_block_delta'
67
72
  index: number
68
- delta: { type: string; text?: string; thinking?: string }
73
+ delta: {
74
+ type: string
75
+ text?: string
76
+ thinking?: string
77
+ partial_json?: string
78
+ }
69
79
  }
70
80
  | { type: 'content_block_stop'; index: number }
71
81
  | { type: 'message_delta' }
@@ -1,4 +1,9 @@
1
1
  import { EventType, buildBaseUsage } from '@tanstack/ai'
2
+ import {
3
+ parseJsonFromAssistantText,
4
+ structuredOutputCompleteChunk,
5
+ structuredOutputStartChunk,
6
+ } from '@tanstack/ai/adapter-internals'
2
7
  import type { StreamChunk, TokenUsage } from '@tanstack/ai'
3
8
  import type {
4
9
  AgentSdkMessage,
@@ -18,6 +23,13 @@ export const BRIDGED_MCP_SERVER_NAME = 'tanstack'
18
23
 
19
24
  const BRIDGED_MCP_PREFIX = `mcp__${BRIDGED_MCP_SERVER_NAME}__`
20
25
 
26
+ /**
27
+ * Claude Code `--json-schema` injects a fake tool named StructuredOutput.
28
+ * The model "calls" it with the schema JSON. The result message may omit
29
+ * `structured_output`; harvest the tool input in that case.
30
+ */
31
+ export const SYNTHETIC_STRUCTURED_OUTPUT_TOOL = 'StructuredOutput'
32
+
21
33
  /** Claude Code-specific usage details attached to RUN_FINISHED usage. */
22
34
  export type ClaudeCodeProviderUsageDetails = {
23
35
  /** Total cost of the harness run in USD, as reported by Claude Code. */
@@ -34,6 +46,8 @@ export interface TranslateContext {
34
46
  onSessionId?: (sessionId: string) => void
35
47
  /** Called for each raw SDK message, for logging. */
36
48
  onSdkMessage?: (message: AgentSdkMessage) => void
49
+ /** Emit structured-output events from `result.structured_output`. */
50
+ expectStructuredOutput?: boolean
37
51
  }
38
52
 
39
53
  /**
@@ -47,6 +61,19 @@ export function stripMcpPrefix(name: string): string {
47
61
  : name
48
62
  }
49
63
 
64
+ function isUsefulStructuredObject(value: unknown): boolean {
65
+ return (
66
+ value !== null &&
67
+ typeof value === 'object' &&
68
+ !Array.isArray(value) &&
69
+ Object.keys(value).length > 0
70
+ )
71
+ }
72
+
73
+ function rememberStructuredOutput(current: unknown, next: unknown): unknown {
74
+ return isUsefulStructuredObject(next) ? next : current
75
+ }
76
+
50
77
  function stringifyToolResultContent(
51
78
  content: SdkToolResultContent | undefined,
52
79
  ): string {
@@ -109,6 +136,11 @@ export async function* translateSdkStream(
109
136
  let runStarted = false
110
137
  /** Tool calls started but with no result yet. */
111
138
  const unresolvedToolCalls = new Set<string>()
139
+ const syntheticOutputToolIds = new Set<string>()
140
+ let capturedStructuredOutput: unknown
141
+ let assistantTextForHarvest = ''
142
+ let partialStructuredJson = ''
143
+ let partialIsStructuredOutput = false
112
144
  /** Anthropic message ids whose text/thinking already streamed via partials. */
113
145
  const streamedMessageIds = new Set<string>()
114
146
 
@@ -179,12 +211,42 @@ export async function* translateSdkStream(
179
211
  partialReasoningId = null
180
212
  }
181
213
 
214
+ function* emitStructuredOutput(object: unknown): Generator<StreamChunk> {
215
+ const raw = JSON.stringify(object)
216
+ const messageId = genId()
217
+ yield structuredOutputStartChunk({
218
+ messageId,
219
+ model,
220
+ threadId,
221
+ runId,
222
+ })
223
+ yield structuredOutputCompleteChunk({
224
+ messageId,
225
+ model,
226
+ threadId,
227
+ runId,
228
+ object,
229
+ raw,
230
+ })
231
+ }
232
+
182
233
  function* emitToolUse(block: {
183
234
  id: string
184
235
  name: string
185
236
  input: unknown
186
237
  }): Generator<StreamChunk> {
187
238
  const toolCallName = stripMcpPrefix(block.name)
239
+ if (
240
+ ctx.expectStructuredOutput === true &&
241
+ toolCallName === SYNTHETIC_STRUCTURED_OUTPUT_TOOL
242
+ ) {
243
+ capturedStructuredOutput = rememberStructuredOutput(
244
+ capturedStructuredOutput,
245
+ block.input,
246
+ )
247
+ syntheticOutputToolIds.add(block.id)
248
+ return
249
+ }
188
250
  const args = JSON.stringify(block.input ?? {})
189
251
  yield {
190
252
  type: EventType.TOOL_CALL_START,
@@ -226,6 +288,7 @@ export async function* translateSdkStream(
226
288
  if (alreadyStreamed) continue
227
289
  const messageId = message.message.id ?? genId()
228
290
  const text = (block as { text: string }).text
291
+ if (!alreadyStreamed) assistantTextForHarvest += text
229
292
  yield {
230
293
  type: EventType.TEXT_MESSAGE_START,
231
294
  messageId,
@@ -301,6 +364,10 @@ export async function* translateSdkStream(
301
364
  content?: SdkToolResultContent
302
365
  is_error?: boolean
303
366
  }
367
+ if (syntheticOutputToolIds.has(toolResult.tool_use_id)) {
368
+ syntheticOutputToolIds.delete(toolResult.tool_use_id)
369
+ continue
370
+ }
304
371
  unresolvedToolCalls.delete(toolResult.tool_use_id)
305
372
  yield {
306
373
  type: EventType.TOOL_CALL_RESULT,
@@ -320,6 +387,29 @@ export async function* translateSdkStream(
320
387
  yield* synthesizeUnresolvedResults()
321
388
 
322
389
  const usage = buildUsage(message.usage, message.total_cost_usd)
390
+ if (ctx.expectStructuredOutput === true) {
391
+ const fromResult = isUsefulStructuredObject(message.structured_output)
392
+ ? message.structured_output
393
+ : undefined
394
+ const fromTool = isUsefulStructuredObject(capturedStructuredOutput)
395
+ ? capturedStructuredOutput
396
+ : undefined
397
+ let fromText: unknown
398
+ if (fromResult === undefined && fromTool === undefined) {
399
+ const raw = assistantTextForHarvest || message.result || ''
400
+ if (raw.trim() !== '') {
401
+ try {
402
+ fromText = parseJsonFromAssistantText(raw)
403
+ } catch {
404
+ fromText = undefined
405
+ }
406
+ }
407
+ }
408
+ const object = fromResult ?? fromTool ?? fromText
409
+ if (isUsefulStructuredObject(object)) {
410
+ yield* emitStructuredOutput(object)
411
+ }
412
+ }
323
413
  if (message.subtype === 'success') {
324
414
  yield {
325
415
  type: EventType.RUN_FINISHED,
@@ -365,6 +455,24 @@ export async function* translateSdkStream(
365
455
  streamedMessageIds.add(partialMessageId)
366
456
  } else if (event.type === 'content_block_start') {
367
457
  partialBlockType = event.content_block.type
458
+ const startedBlock = event.content_block
459
+ partialIsStructuredOutput =
460
+ ctx.expectStructuredOutput === true &&
461
+ startedBlock.type === 'tool_use' &&
462
+ 'name' in startedBlock &&
463
+ startedBlock.name === SYNTHETIC_STRUCTURED_OUTPUT_TOOL
464
+ if (partialIsStructuredOutput) {
465
+ partialStructuredJson = ''
466
+ if ('id' in startedBlock && typeof startedBlock.id === 'string') {
467
+ syntheticOutputToolIds.add(startedBlock.id)
468
+ }
469
+ if ('input' in startedBlock) {
470
+ capturedStructuredOutput = rememberStructuredOutput(
471
+ capturedStructuredOutput,
472
+ startedBlock.input,
473
+ )
474
+ }
475
+ }
368
476
  if (partialBlockType === 'text') {
369
477
  partialTextMessageId = partialMessageId ?? genId()
370
478
  partialTextContent = ''
@@ -402,6 +510,7 @@ export async function* translateSdkStream(
402
510
  typeof event.delta.text === 'string'
403
511
  ) {
404
512
  partialTextContent += event.delta.text
513
+ assistantTextForHarvest += event.delta.text
405
514
  yield {
406
515
  type: EventType.TEXT_MESSAGE_CONTENT,
407
516
  messageId: partialTextMessageId,
@@ -410,6 +519,12 @@ export async function* translateSdkStream(
410
519
  delta: event.delta.text,
411
520
  content: partialTextContent,
412
521
  }
522
+ } else if (
523
+ event.delta.type === 'input_json_delta' &&
524
+ partialIsStructuredOutput &&
525
+ typeof event.delta.partial_json === 'string'
526
+ ) {
527
+ partialStructuredJson += event.delta.partial_json
413
528
  } else if (
414
529
  event.delta.type === 'thinking_delta' &&
415
530
  partialReasoningId &&
@@ -424,12 +539,24 @@ export async function* translateSdkStream(
424
539
  }
425
540
  }
426
541
  } else if (event.type === 'content_block_stop') {
542
+ if (partialIsStructuredOutput && partialStructuredJson !== '') {
543
+ try {
544
+ capturedStructuredOutput = rememberStructuredOutput(
545
+ capturedStructuredOutput,
546
+ JSON.parse(partialStructuredJson),
547
+ )
548
+ } catch {
549
+ // Incomplete JSON; the complete assistant tool_use may still arrive.
550
+ }
551
+ }
427
552
  if (partialBlockType === 'text') {
428
553
  yield* closePartialText()
429
554
  } else if (partialBlockType === 'thinking') {
430
555
  yield* closePartialReasoning()
431
556
  }
432
557
  partialBlockType = null
558
+ partialIsStructuredOutput = false
559
+ partialStructuredJson = ''
433
560
  }
434
561
  }
435
562