@tanstack/ai-claude-code 0.1.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/README.md +18 -0
- package/dist/esm/adapters/policy-map.d.ts +8 -0
- package/dist/esm/adapters/policy-map.js +51 -0
- package/dist/esm/adapters/policy-map.js.map +1 -0
- package/dist/esm/adapters/projection.d.ts +14 -0
- package/dist/esm/adapters/projection.js +101 -0
- package/dist/esm/adapters/projection.js.map +1 -0
- package/dist/esm/adapters/text.d.ts +75 -0
- package/dist/esm/adapters/text.js +306 -0
- package/dist/esm/adapters/text.js.map +1 -0
- package/dist/esm/index.d.ts +10 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/messages/prompt.d.ts +17 -0
- package/dist/esm/messages/prompt.js +37 -0
- package/dist/esm/messages/prompt.js.map +1 -0
- package/dist/esm/model-meta.d.ts +10 -0
- package/dist/esm/model-meta.js +14 -0
- package/dist/esm/model-meta.js.map +1 -0
- package/dist/esm/provider-options.d.ts +30 -0
- package/dist/esm/stream/sdk-types.d.ts +124 -0
- package/dist/esm/stream/translate.d.ts +44 -0
- package/dist/esm/stream/translate.js +368 -0
- package/dist/esm/stream/translate.js.map +1 -0
- package/package.json +62 -0
- package/src/adapters/policy-map.ts +83 -0
- package/src/adapters/projection.ts +226 -0
- package/src/adapters/text.ts +539 -0
- package/src/index.ts +21 -0
- package/src/messages/prompt.ts +68 -0
- package/src/model-meta.ts +21 -0
- package/src/provider-options.ts +30 -0
- package/src/stream/sdk-types.ts +135 -0
- package/src/stream/translate.ts +483 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural subset of the `@anthropic-ai/claude-agent-sdk` message types that
|
|
3
|
+
* the stream translator consumes.
|
|
4
|
+
*
|
|
5
|
+
* These are intentionally defined structurally (rather than imported from the
|
|
6
|
+
* agent SDK) so the translator stays a pure, fixture-testable state machine
|
|
7
|
+
* and the package's public types don't depend on the agent SDK's bundled
|
|
8
|
+
* `@anthropic-ai/sdk` type imports.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface SdkInitMessage {
|
|
12
|
+
type: 'system'
|
|
13
|
+
subtype: 'init'
|
|
14
|
+
session_id: string
|
|
15
|
+
model: string
|
|
16
|
+
tools: Array<string>
|
|
17
|
+
cwd?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SdkAssistantContentBlock =
|
|
21
|
+
| { type: 'text'; text: string }
|
|
22
|
+
| { type: 'thinking'; thinking: string }
|
|
23
|
+
| { type: 'tool_use'; id: string; name: string; input: unknown }
|
|
24
|
+
| { type: string; [key: string]: unknown }
|
|
25
|
+
|
|
26
|
+
export interface SdkAssistantMessage {
|
|
27
|
+
type: 'assistant'
|
|
28
|
+
message: {
|
|
29
|
+
id?: string
|
|
30
|
+
content: Array<SdkAssistantContentBlock>
|
|
31
|
+
}
|
|
32
|
+
parent_tool_use_id: string | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type SdkToolResultContent =
|
|
36
|
+
| string
|
|
37
|
+
| Array<{ type: string; text?: string; [key: string]: unknown }>
|
|
38
|
+
|
|
39
|
+
export type SdkUserContentBlock =
|
|
40
|
+
| {
|
|
41
|
+
type: 'tool_result'
|
|
42
|
+
tool_use_id: string
|
|
43
|
+
content?: SdkToolResultContent
|
|
44
|
+
is_error?: boolean
|
|
45
|
+
}
|
|
46
|
+
| { type: string; [key: string]: unknown }
|
|
47
|
+
|
|
48
|
+
export interface SdkUserMessage {
|
|
49
|
+
type: 'user'
|
|
50
|
+
message: {
|
|
51
|
+
role: 'user'
|
|
52
|
+
content: string | Array<SdkUserContentBlock>
|
|
53
|
+
}
|
|
54
|
+
parent_tool_use_id: string | null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Raw Anthropic streaming events forwarded when `includePartialMessages` is set. */
|
|
58
|
+
export type SdkRawStreamEvent =
|
|
59
|
+
| { type: 'message_start'; message: { id?: string } }
|
|
60
|
+
| {
|
|
61
|
+
type: 'content_block_start'
|
|
62
|
+
index: number
|
|
63
|
+
content_block: { type: string }
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
type: 'content_block_delta'
|
|
67
|
+
index: number
|
|
68
|
+
delta: { type: string; text?: string; thinking?: string }
|
|
69
|
+
}
|
|
70
|
+
| { type: 'content_block_stop'; index: number }
|
|
71
|
+
| { type: 'message_delta' }
|
|
72
|
+
| { type: 'message_stop' }
|
|
73
|
+
|
|
74
|
+
export interface SdkPartialAssistantMessage {
|
|
75
|
+
type: 'stream_event'
|
|
76
|
+
event: SdkRawStreamEvent
|
|
77
|
+
parent_tool_use_id: string | null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SdkUsage {
|
|
81
|
+
input_tokens?: number
|
|
82
|
+
output_tokens?: number
|
|
83
|
+
cache_read_input_tokens?: number
|
|
84
|
+
cache_creation_input_tokens?: number
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface SdkResultMessage {
|
|
88
|
+
type: 'result'
|
|
89
|
+
subtype:
|
|
90
|
+
| 'success'
|
|
91
|
+
| 'error_max_turns'
|
|
92
|
+
| 'error_during_execution'
|
|
93
|
+
| 'error_max_budget_usd'
|
|
94
|
+
| 'error_max_structured_output_retries'
|
|
95
|
+
result?: string
|
|
96
|
+
errors?: Array<string>
|
|
97
|
+
usage?: SdkUsage
|
|
98
|
+
total_cost_usd?: number
|
|
99
|
+
structured_output?: unknown
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Harness-internal system messages the translator deliberately ignores.
|
|
104
|
+
* (The real SDK union has many more members; unknown runtime types simply
|
|
105
|
+
* fall through every branch.)
|
|
106
|
+
*/
|
|
107
|
+
export interface SdkNoiseSystemMessage {
|
|
108
|
+
type: 'system'
|
|
109
|
+
subtype:
|
|
110
|
+
| 'status'
|
|
111
|
+
| 'permission_denied'
|
|
112
|
+
| 'plugin_install'
|
|
113
|
+
| 'session_state_changed'
|
|
114
|
+
| 'task_notification'
|
|
115
|
+
| 'task_progress'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Other harness-internal top-level message types the translator ignores. */
|
|
119
|
+
export interface SdkNoiseMessage {
|
|
120
|
+
type:
|
|
121
|
+
| 'tool_progress'
|
|
122
|
+
| 'auth_status'
|
|
123
|
+
| 'rate_limit_event'
|
|
124
|
+
| 'prompt_suggestion'
|
|
125
|
+
| 'compact_boundary'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type AgentSdkMessage =
|
|
129
|
+
| SdkInitMessage
|
|
130
|
+
| SdkAssistantMessage
|
|
131
|
+
| SdkUserMessage
|
|
132
|
+
| SdkPartialAssistantMessage
|
|
133
|
+
| SdkResultMessage
|
|
134
|
+
| SdkNoiseSystemMessage
|
|
135
|
+
| SdkNoiseMessage
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { EventType, buildBaseUsage } from '@tanstack/ai'
|
|
2
|
+
import type { StreamChunk, TokenUsage } from '@tanstack/ai'
|
|
3
|
+
import type {
|
|
4
|
+
AgentSdkMessage,
|
|
5
|
+
SdkAssistantMessage,
|
|
6
|
+
SdkPartialAssistantMessage,
|
|
7
|
+
SdkResultMessage,
|
|
8
|
+
SdkToolResultContent,
|
|
9
|
+
SdkUsage,
|
|
10
|
+
SdkUserMessage,
|
|
11
|
+
} from './sdk-types'
|
|
12
|
+
|
|
13
|
+
/** Name of the CUSTOM event carrying the Claude Code session id. */
|
|
14
|
+
export const SESSION_ID_EVENT = 'claude-code.session-id'
|
|
15
|
+
|
|
16
|
+
/** Server name used for bridged TanStack tools (model sees `mcp__tanstack__<name>`). */
|
|
17
|
+
export const BRIDGED_MCP_SERVER_NAME = 'tanstack'
|
|
18
|
+
|
|
19
|
+
const BRIDGED_MCP_PREFIX = `mcp__${BRIDGED_MCP_SERVER_NAME}__`
|
|
20
|
+
|
|
21
|
+
/** Claude Code-specific usage details attached to RUN_FINISHED usage. */
|
|
22
|
+
export type ClaudeCodeProviderUsageDetails = {
|
|
23
|
+
/** Total cost of the harness run in USD, as reported by Claude Code. */
|
|
24
|
+
totalCostUsd?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TranslateContext {
|
|
28
|
+
model: string
|
|
29
|
+
runId: string
|
|
30
|
+
threadId: string
|
|
31
|
+
parentRunId?: string
|
|
32
|
+
genId: () => string
|
|
33
|
+
/** Called as soon as the harness reports its session id. */
|
|
34
|
+
onSessionId?: (sessionId: string) => void
|
|
35
|
+
/** Called for each raw SDK message, for logging. */
|
|
36
|
+
onSdkMessage?: (message: AgentSdkMessage) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Strip the bridged MCP server prefix so tool-call events match the TanStack
|
|
41
|
+
* tool names the application registered. Built-in harness tools (Bash, Read,
|
|
42
|
+
* Edit, ...) and foreign MCP tools pass through verbatim.
|
|
43
|
+
*/
|
|
44
|
+
export function stripMcpPrefix(name: string): string {
|
|
45
|
+
return name.startsWith(BRIDGED_MCP_PREFIX)
|
|
46
|
+
? name.slice(BRIDGED_MCP_PREFIX.length)
|
|
47
|
+
: name
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stringifyToolResultContent(
|
|
51
|
+
content: SdkToolResultContent | undefined,
|
|
52
|
+
): string {
|
|
53
|
+
if (content === undefined) return ''
|
|
54
|
+
if (typeof content === 'string') return content
|
|
55
|
+
return content
|
|
56
|
+
.map((block) => (typeof block.text === 'string' ? block.text : ''))
|
|
57
|
+
.join('')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildUsage(
|
|
61
|
+
usage: SdkUsage | undefined,
|
|
62
|
+
totalCostUsd: number | undefined,
|
|
63
|
+
): TokenUsage<ClaudeCodeProviderUsageDetails> | undefined {
|
|
64
|
+
if (!usage) return undefined
|
|
65
|
+
const promptTokens = usage.input_tokens ?? 0
|
|
66
|
+
const completionTokens = usage.output_tokens ?? 0
|
|
67
|
+
const result = buildBaseUsage<ClaudeCodeProviderUsageDetails>({
|
|
68
|
+
promptTokens,
|
|
69
|
+
completionTokens,
|
|
70
|
+
totalTokens: promptTokens + completionTokens,
|
|
71
|
+
})
|
|
72
|
+
const cacheWrite = usage.cache_creation_input_tokens
|
|
73
|
+
const cacheRead = usage.cache_read_input_tokens
|
|
74
|
+
const promptTokensDetails = {
|
|
75
|
+
...(cacheWrite ? { cacheWriteTokens: cacheWrite } : {}),
|
|
76
|
+
...(cacheRead ? { cachedTokens: cacheRead } : {}),
|
|
77
|
+
}
|
|
78
|
+
if (Object.keys(promptTokensDetails).length > 0) {
|
|
79
|
+
result.promptTokensDetails = promptTokensDetails
|
|
80
|
+
}
|
|
81
|
+
if (totalCostUsd !== undefined) {
|
|
82
|
+
result.providerUsageDetails = { totalCostUsd }
|
|
83
|
+
}
|
|
84
|
+
return result
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Translate a Claude Code Agent SDK message stream into AG-UI StreamChunk
|
|
89
|
+
* events.
|
|
90
|
+
*
|
|
91
|
+
* The harness runs its own agent loop and executes its own tools, so the
|
|
92
|
+
* translation always ends with `finishReason: 'stop'` (or `'length'` /
|
|
93
|
+
* RUN_ERROR) — never `'tool_calls'`. Harness tool activity is emitted as
|
|
94
|
+
* already-resolved TOOL_CALL_START/ARGS/END + TOOL_CALL_RESULT sequences so
|
|
95
|
+
* UIs can render it, while the TanStack engine never tries to execute them.
|
|
96
|
+
*
|
|
97
|
+
* Invariant: every TOOL_CALL_START is eventually paired with a
|
|
98
|
+
* TOOL_CALL_RESULT (synthesized as `{"status":"interrupted"}` when the run
|
|
99
|
+
* ends or aborts before the harness reported one) so the engine's
|
|
100
|
+
* pending-tool-call scan on the next request never force-executes them.
|
|
101
|
+
*/
|
|
102
|
+
export async function* translateSdkStream(
|
|
103
|
+
sdkMessages: AsyncIterable<AgentSdkMessage>,
|
|
104
|
+
ctx: TranslateContext,
|
|
105
|
+
): AsyncIterable<StreamChunk> {
|
|
106
|
+
const { model, runId, threadId, genId } = ctx
|
|
107
|
+
const now = () => Date.now()
|
|
108
|
+
|
|
109
|
+
let runStarted = false
|
|
110
|
+
/** Tool calls started but with no result yet. */
|
|
111
|
+
const unresolvedToolCalls = new Set<string>()
|
|
112
|
+
/** Anthropic message ids whose text/thinking already streamed via partials. */
|
|
113
|
+
const streamedMessageIds = new Set<string>()
|
|
114
|
+
|
|
115
|
+
// Partial-stream state
|
|
116
|
+
let partialMessageId: string | null = null
|
|
117
|
+
let partialBlockType: string | null = null
|
|
118
|
+
let partialTextMessageId: string | null = null
|
|
119
|
+
let partialTextContent = ''
|
|
120
|
+
let partialTextStarted = false
|
|
121
|
+
let partialReasoningId: string | null = null
|
|
122
|
+
|
|
123
|
+
function* startRun(): Generator<StreamChunk> {
|
|
124
|
+
if (runStarted) return
|
|
125
|
+
runStarted = true
|
|
126
|
+
yield {
|
|
127
|
+
type: EventType.RUN_STARTED,
|
|
128
|
+
runId,
|
|
129
|
+
threadId,
|
|
130
|
+
model,
|
|
131
|
+
timestamp: now(),
|
|
132
|
+
...(ctx.parentRunId !== undefined && { parentRunId: ctx.parentRunId }),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function* synthesizeUnresolvedResults(): Generator<StreamChunk> {
|
|
137
|
+
for (const toolCallId of unresolvedToolCalls) {
|
|
138
|
+
yield {
|
|
139
|
+
type: EventType.TOOL_CALL_RESULT,
|
|
140
|
+
toolCallId,
|
|
141
|
+
messageId: genId(),
|
|
142
|
+
model,
|
|
143
|
+
timestamp: now(),
|
|
144
|
+
content: JSON.stringify({ status: 'interrupted' }),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
unresolvedToolCalls.clear()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function* closePartialText(): Generator<StreamChunk> {
|
|
151
|
+
if (partialTextStarted && partialTextMessageId) {
|
|
152
|
+
yield {
|
|
153
|
+
type: EventType.TEXT_MESSAGE_END,
|
|
154
|
+
messageId: partialTextMessageId,
|
|
155
|
+
model,
|
|
156
|
+
timestamp: now(),
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
partialTextStarted = false
|
|
160
|
+
partialTextMessageId = null
|
|
161
|
+
partialTextContent = ''
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function* closePartialReasoning(): Generator<StreamChunk> {
|
|
165
|
+
if (partialReasoningId) {
|
|
166
|
+
yield {
|
|
167
|
+
type: EventType.REASONING_MESSAGE_END,
|
|
168
|
+
messageId: partialReasoningId,
|
|
169
|
+
model,
|
|
170
|
+
timestamp: now(),
|
|
171
|
+
}
|
|
172
|
+
yield {
|
|
173
|
+
type: EventType.REASONING_END,
|
|
174
|
+
messageId: partialReasoningId,
|
|
175
|
+
model,
|
|
176
|
+
timestamp: now(),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
partialReasoningId = null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function* emitToolUse(block: {
|
|
183
|
+
id: string
|
|
184
|
+
name: string
|
|
185
|
+
input: unknown
|
|
186
|
+
}): Generator<StreamChunk> {
|
|
187
|
+
const toolCallName = stripMcpPrefix(block.name)
|
|
188
|
+
const args = JSON.stringify(block.input ?? {})
|
|
189
|
+
yield {
|
|
190
|
+
type: EventType.TOOL_CALL_START,
|
|
191
|
+
toolCallId: block.id,
|
|
192
|
+
toolCallName,
|
|
193
|
+
toolName: toolCallName,
|
|
194
|
+
model,
|
|
195
|
+
timestamp: now(),
|
|
196
|
+
}
|
|
197
|
+
yield {
|
|
198
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
199
|
+
toolCallId: block.id,
|
|
200
|
+
model,
|
|
201
|
+
timestamp: now(),
|
|
202
|
+
delta: args,
|
|
203
|
+
args,
|
|
204
|
+
}
|
|
205
|
+
yield {
|
|
206
|
+
type: EventType.TOOL_CALL_END,
|
|
207
|
+
toolCallId: block.id,
|
|
208
|
+
toolCallName,
|
|
209
|
+
toolName: toolCallName,
|
|
210
|
+
model,
|
|
211
|
+
timestamp: now(),
|
|
212
|
+
input: block.input ?? {},
|
|
213
|
+
}
|
|
214
|
+
unresolvedToolCalls.add(block.id)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function* handleAssistant(
|
|
218
|
+
message: SdkAssistantMessage,
|
|
219
|
+
): Generator<StreamChunk> {
|
|
220
|
+
const alreadyStreamed =
|
|
221
|
+
message.message.id !== undefined &&
|
|
222
|
+
streamedMessageIds.has(message.message.id)
|
|
223
|
+
|
|
224
|
+
for (const block of message.message.content) {
|
|
225
|
+
if (block.type === 'text') {
|
|
226
|
+
if (alreadyStreamed) continue
|
|
227
|
+
const messageId = message.message.id ?? genId()
|
|
228
|
+
const text = (block as { text: string }).text
|
|
229
|
+
yield {
|
|
230
|
+
type: EventType.TEXT_MESSAGE_START,
|
|
231
|
+
messageId,
|
|
232
|
+
model,
|
|
233
|
+
timestamp: now(),
|
|
234
|
+
role: 'assistant',
|
|
235
|
+
}
|
|
236
|
+
yield {
|
|
237
|
+
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
238
|
+
messageId,
|
|
239
|
+
model,
|
|
240
|
+
timestamp: now(),
|
|
241
|
+
delta: text,
|
|
242
|
+
content: text,
|
|
243
|
+
}
|
|
244
|
+
yield {
|
|
245
|
+
type: EventType.TEXT_MESSAGE_END,
|
|
246
|
+
messageId,
|
|
247
|
+
model,
|
|
248
|
+
timestamp: now(),
|
|
249
|
+
}
|
|
250
|
+
} else if (block.type === 'thinking') {
|
|
251
|
+
if (alreadyStreamed) continue
|
|
252
|
+
const reasoningId = genId()
|
|
253
|
+
const thinking = (block as { thinking: string }).thinking
|
|
254
|
+
yield {
|
|
255
|
+
type: EventType.REASONING_START,
|
|
256
|
+
messageId: reasoningId,
|
|
257
|
+
model,
|
|
258
|
+
timestamp: now(),
|
|
259
|
+
}
|
|
260
|
+
yield {
|
|
261
|
+
type: EventType.REASONING_MESSAGE_START,
|
|
262
|
+
messageId: reasoningId,
|
|
263
|
+
role: 'reasoning' as const,
|
|
264
|
+
model,
|
|
265
|
+
timestamp: now(),
|
|
266
|
+
}
|
|
267
|
+
yield {
|
|
268
|
+
type: EventType.REASONING_MESSAGE_CONTENT,
|
|
269
|
+
messageId: reasoningId,
|
|
270
|
+
delta: thinking,
|
|
271
|
+
model,
|
|
272
|
+
timestamp: now(),
|
|
273
|
+
}
|
|
274
|
+
yield {
|
|
275
|
+
type: EventType.REASONING_MESSAGE_END,
|
|
276
|
+
messageId: reasoningId,
|
|
277
|
+
model,
|
|
278
|
+
timestamp: now(),
|
|
279
|
+
}
|
|
280
|
+
yield {
|
|
281
|
+
type: EventType.REASONING_END,
|
|
282
|
+
messageId: reasoningId,
|
|
283
|
+
model,
|
|
284
|
+
timestamp: now(),
|
|
285
|
+
}
|
|
286
|
+
} else if (block.type === 'tool_use') {
|
|
287
|
+
yield* emitToolUse(
|
|
288
|
+
block as { id: string; name: string; input: unknown },
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function* handleUser(message: SdkUserMessage): Generator<StreamChunk> {
|
|
295
|
+
const content = message.message.content
|
|
296
|
+
if (typeof content === 'string') return
|
|
297
|
+
for (const block of content) {
|
|
298
|
+
if (block.type !== 'tool_result') continue
|
|
299
|
+
const toolResult = block as {
|
|
300
|
+
tool_use_id: string
|
|
301
|
+
content?: SdkToolResultContent
|
|
302
|
+
is_error?: boolean
|
|
303
|
+
}
|
|
304
|
+
unresolvedToolCalls.delete(toolResult.tool_use_id)
|
|
305
|
+
yield {
|
|
306
|
+
type: EventType.TOOL_CALL_RESULT,
|
|
307
|
+
toolCallId: toolResult.tool_use_id,
|
|
308
|
+
messageId: genId(),
|
|
309
|
+
model,
|
|
310
|
+
timestamp: now(),
|
|
311
|
+
content: stringifyToolResultContent(toolResult.content),
|
|
312
|
+
...(toolResult.is_error === true && { state: 'output-error' as const }),
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function* handleResult(message: SdkResultMessage): Generator<StreamChunk> {
|
|
318
|
+
yield* closePartialText()
|
|
319
|
+
yield* closePartialReasoning()
|
|
320
|
+
yield* synthesizeUnresolvedResults()
|
|
321
|
+
|
|
322
|
+
const usage = buildUsage(message.usage, message.total_cost_usd)
|
|
323
|
+
if (message.subtype === 'success') {
|
|
324
|
+
yield {
|
|
325
|
+
type: EventType.RUN_FINISHED,
|
|
326
|
+
runId,
|
|
327
|
+
threadId,
|
|
328
|
+
model,
|
|
329
|
+
timestamp: now(),
|
|
330
|
+
finishReason: 'stop',
|
|
331
|
+
...(usage !== undefined && { usage }),
|
|
332
|
+
}
|
|
333
|
+
} else if (message.subtype === 'error_max_turns') {
|
|
334
|
+
yield {
|
|
335
|
+
type: EventType.RUN_FINISHED,
|
|
336
|
+
runId,
|
|
337
|
+
threadId,
|
|
338
|
+
model,
|
|
339
|
+
timestamp: now(),
|
|
340
|
+
finishReason: 'length',
|
|
341
|
+
...(usage !== undefined && { usage }),
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
const errorMessage =
|
|
345
|
+
message.errors && message.errors.length > 0
|
|
346
|
+
? message.errors.join('; ')
|
|
347
|
+
: `Claude Code run failed: ${message.subtype}`
|
|
348
|
+
yield {
|
|
349
|
+
type: EventType.RUN_ERROR,
|
|
350
|
+
model,
|
|
351
|
+
timestamp: now(),
|
|
352
|
+
message: errorMessage,
|
|
353
|
+
code: message.subtype,
|
|
354
|
+
error: { message: errorMessage, code: message.subtype },
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function* handleStreamEvent(
|
|
360
|
+
message: SdkPartialAssistantMessage,
|
|
361
|
+
): Generator<StreamChunk> {
|
|
362
|
+
const event = message.event
|
|
363
|
+
if (event.type === 'message_start') {
|
|
364
|
+
partialMessageId = event.message.id ?? genId()
|
|
365
|
+
streamedMessageIds.add(partialMessageId)
|
|
366
|
+
} else if (event.type === 'content_block_start') {
|
|
367
|
+
partialBlockType = event.content_block.type
|
|
368
|
+
if (partialBlockType === 'text') {
|
|
369
|
+
partialTextMessageId = partialMessageId ?? genId()
|
|
370
|
+
partialTextContent = ''
|
|
371
|
+
if (!partialTextStarted) {
|
|
372
|
+
partialTextStarted = true
|
|
373
|
+
yield {
|
|
374
|
+
type: EventType.TEXT_MESSAGE_START,
|
|
375
|
+
messageId: partialTextMessageId,
|
|
376
|
+
model,
|
|
377
|
+
timestamp: now(),
|
|
378
|
+
role: 'assistant',
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} else if (partialBlockType === 'thinking') {
|
|
382
|
+
partialReasoningId = genId()
|
|
383
|
+
yield {
|
|
384
|
+
type: EventType.REASONING_START,
|
|
385
|
+
messageId: partialReasoningId,
|
|
386
|
+
model,
|
|
387
|
+
timestamp: now(),
|
|
388
|
+
}
|
|
389
|
+
yield {
|
|
390
|
+
type: EventType.REASONING_MESSAGE_START,
|
|
391
|
+
messageId: partialReasoningId,
|
|
392
|
+
role: 'reasoning' as const,
|
|
393
|
+
model,
|
|
394
|
+
timestamp: now(),
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} else if (event.type === 'content_block_delta') {
|
|
398
|
+
if (
|
|
399
|
+
event.delta.type === 'text_delta' &&
|
|
400
|
+
partialTextStarted &&
|
|
401
|
+
partialTextMessageId &&
|
|
402
|
+
typeof event.delta.text === 'string'
|
|
403
|
+
) {
|
|
404
|
+
partialTextContent += event.delta.text
|
|
405
|
+
yield {
|
|
406
|
+
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
407
|
+
messageId: partialTextMessageId,
|
|
408
|
+
model,
|
|
409
|
+
timestamp: now(),
|
|
410
|
+
delta: event.delta.text,
|
|
411
|
+
content: partialTextContent,
|
|
412
|
+
}
|
|
413
|
+
} else if (
|
|
414
|
+
event.delta.type === 'thinking_delta' &&
|
|
415
|
+
partialReasoningId &&
|
|
416
|
+
typeof event.delta.thinking === 'string'
|
|
417
|
+
) {
|
|
418
|
+
yield {
|
|
419
|
+
type: EventType.REASONING_MESSAGE_CONTENT,
|
|
420
|
+
messageId: partialReasoningId,
|
|
421
|
+
delta: event.delta.thinking,
|
|
422
|
+
model,
|
|
423
|
+
timestamp: now(),
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} else if (event.type === 'content_block_stop') {
|
|
427
|
+
if (partialBlockType === 'text') {
|
|
428
|
+
yield* closePartialText()
|
|
429
|
+
} else if (partialBlockType === 'thinking') {
|
|
430
|
+
yield* closePartialReasoning()
|
|
431
|
+
}
|
|
432
|
+
partialBlockType = null
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
for await (const sdkMessage of sdkMessages) {
|
|
438
|
+
ctx.onSdkMessage?.(sdkMessage)
|
|
439
|
+
|
|
440
|
+
if (sdkMessage.type === 'system' && sdkMessage.subtype === 'init') {
|
|
441
|
+
yield* startRun()
|
|
442
|
+
ctx.onSessionId?.(sdkMessage.session_id)
|
|
443
|
+
yield {
|
|
444
|
+
type: EventType.CUSTOM,
|
|
445
|
+
model,
|
|
446
|
+
timestamp: now(),
|
|
447
|
+
name: SESSION_ID_EVENT,
|
|
448
|
+
value: {
|
|
449
|
+
sessionId: sdkMessage.session_id,
|
|
450
|
+
model: sdkMessage.model,
|
|
451
|
+
tools: sdkMessage.tools,
|
|
452
|
+
},
|
|
453
|
+
}
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Anything before init still needs RUN_STARTED first.
|
|
458
|
+
yield* startRun()
|
|
459
|
+
|
|
460
|
+
if (sdkMessage.type === 'stream_event') {
|
|
461
|
+
if (sdkMessage.parent_tool_use_id !== null) continue
|
|
462
|
+
yield* handleStreamEvent(sdkMessage)
|
|
463
|
+
} else if (sdkMessage.type === 'assistant') {
|
|
464
|
+
if (sdkMessage.parent_tool_use_id !== null) continue
|
|
465
|
+
yield* handleAssistant(sdkMessage)
|
|
466
|
+
} else if (sdkMessage.type === 'user') {
|
|
467
|
+
if (sdkMessage.parent_tool_use_id !== null) continue
|
|
468
|
+
yield* handleUser(sdkMessage)
|
|
469
|
+
} else if (sdkMessage.type === 'result') {
|
|
470
|
+
yield* handleResult(sdkMessage)
|
|
471
|
+
}
|
|
472
|
+
// All other SDK message types (status, hooks, notifications, ...) are
|
|
473
|
+
// harness-internal and intentionally ignored.
|
|
474
|
+
}
|
|
475
|
+
} catch (error) {
|
|
476
|
+
// The run is dying (abort or SDK failure). Pair any started tool calls
|
|
477
|
+
// with a synthetic result first so the next request's pending-tool-call
|
|
478
|
+
// scan doesn't try to execute them, then let the adapter surface the
|
|
479
|
+
// error as RUN_ERROR.
|
|
480
|
+
yield* synthesizeUnresolvedResults()
|
|
481
|
+
throw error
|
|
482
|
+
}
|
|
483
|
+
}
|