@tanstack/ai-claude-code 0.3.2 → 0.4.1
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 +7 -7
- 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/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
|
}
|
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
|
|