@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
package/src/provider-options.ts
CHANGED
|
@@ -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
|
}
|
package/src/stream/sdk-types.ts
CHANGED
|
@@ -60,12 +60,22 @@ export type SdkRawStreamEvent =
|
|
|
60
60
|
| {
|
|
61
61
|
type: 'content_block_start'
|
|
62
62
|
index: number
|
|
63
|
-
content_block: {
|
|
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: {
|
|
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' }
|
package/src/stream/translate.ts
CHANGED
|
@@ -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
|
|