@meistrari/agent-core 0.2.0-experimental.1 → 0.2.0-experimental.2
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 +50 -0
- package/docs/consumer-capability-matrix.md +374 -0
- package/package.json +17 -10
- package/scripts/generate-codex-protocol.ts +711 -0
- package/src/agents/agent-operation.ts +8 -0
- package/src/agents/agent-provider.ts +3 -0
- package/src/agents/agent-run.ts +14 -2
- package/src/agents/agent-runtime-error.ts +28 -0
- package/src/agents/agent-session-events.ts +33 -0
- package/src/agents/agent-tool-runner.ts +12 -11
- package/src/agents/agent-tool.ts +12 -4
- package/src/agents/author-context.ts +1 -1
- package/src/agents/claude/claude-auth.ts +40 -0
- package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
- package/src/agents/claude/claude-code-process.ts +98 -0
- package/src/agents/claude/claude-command-mapper.ts +63 -3
- package/src/agents/claude/claude-desktop.ts +55 -0
- package/src/agents/claude/claude-event-mapper.ts +864 -135
- package/src/agents/claude/claude-file-change.ts +128 -0
- package/src/agents/claude/claude-provider.ts +344 -29
- package/src/agents/claude/claude-run.ts +893 -130
- package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
- package/src/agents/claude/claude-skill-catalog.ts +75 -0
- package/src/agents/claude/claude-steered-notifications.ts +180 -0
- package/src/agents/claude/claude-tool-mapper.ts +81 -2
- package/src/agents/claude/claude-workspace-skills.ts +87 -0
- package/src/agents/claude/index.ts +2 -0
- package/src/agents/codex/codex-auth-mapper.ts +27 -0
- package/src/agents/codex/codex-auth.ts +19 -0
- package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
- package/src/agents/codex/codex-browser-auth-status.ts +55 -0
- package/src/agents/codex/codex-command-mapper.ts +38 -3
- package/src/agents/codex/codex-event-mapper.ts +375 -64
- package/src/agents/codex/codex-json-rpc-client.ts +66 -21
- package/src/agents/codex/codex-provider.ts +118 -236
- package/src/agents/codex/codex-run-auth.ts +113 -0
- package/src/agents/codex/codex-run.ts +423 -53
- package/src/agents/codex/codex-tool-mapper.ts +2 -2
- package/src/agents/codex/codex.errors.ts +8 -0
- package/src/agents/codex/generated/meta.gen.ts +144 -6
- package/src/agents/codex/generated/namespaces.gen.ts +92 -3
- package/src/agents/codex/generated/schema.gen.ts +24698 -15638
- package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/input-attachment-preparation.ts +13 -0
- package/src/agents/mcp-tool-content.ts +82 -0
- package/src/agents/model-rebinding-run.ts +14 -3
- package/src/agents/selected-repository-instructions.ts +202 -0
- package/src/agents/session-context.ts +41 -0
- package/src/agents/subscription-token-refresh.ts +73 -0
- package/src/agents/user-input-request.ts +19 -20
- package/src/errors/index.ts +1 -1
- package/src/errors/validation-error.ts +6 -0
- package/src/protocol/agent-catalog.ts +159 -0
- package/src/protocol/agent-command.ts +52 -7
- package/src/protocol/agent-content.ts +36 -16
- package/src/protocol/agent-event.ts +210 -73
- package/src/protocol/agent-identity.ts +29 -0
- package/src/protocol/agent-json.ts +122 -8
- package/src/protocol/agent-metadata.ts +2 -2
- package/src/protocol/agent-model.ts +13 -1
- package/src/protocol/agent-notification.ts +110 -0
- package/src/protocol/agent-presentation.ts +71 -20
- package/src/protocol/agent-tool.ts +66 -10
- package/src/protocol/agent-usage.ts +3 -1
- package/src/protocol/agent-user-input.ts +4 -4
- package/src/protocol/agent-work.ts +12 -10
- package/src/protocol/index.ts +2 -0
- package/src/protocol/rejected-keys.ts +11 -0
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
|
@@ -2,7 +2,7 @@ import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk'
|
|
|
2
2
|
import type { TaskCreateInput, TaskCreateOutput, TaskGetInput, TaskGetOutput, TaskListInput, TaskListOutput, TaskUpdateInput, TaskUpdateOutput } from '@anthropic-ai/claude-agent-sdk/sdk-tools'
|
|
3
3
|
import type { MessageParam } from '@anthropic-ai/sdk/resources'
|
|
4
4
|
import type { BetaMessage, BetaRawMessageStreamEvent, BetaUsage } from '@anthropic-ai/sdk/resources/beta/messages/messages'
|
|
5
|
-
import type { AgentError, AgentEventActor, AgentUsage, JsonValue } from '../../protocol'
|
|
5
|
+
import type { AgentContextSnapshot, AgentError, AgentEventActor, AgentToolContent, AgentUsage, JsonValue } from '../../protocol'
|
|
6
6
|
import type { AgentWorkObservation } from '../../protocol/agent-work'
|
|
7
7
|
import type { AgentEventDraft } from '../agent-event-stream'
|
|
8
8
|
import { randomUUID } from 'node:crypto'
|
|
@@ -12,7 +12,9 @@ import { canonicalToolName } from '../../protocol/agent-tool-name'
|
|
|
12
12
|
import { agentWorkStatusSchema } from '../../protocol/agent-work'
|
|
13
13
|
import { createRuntimeTurnId } from '../agent-id'
|
|
14
14
|
import { AgentProviderProtocolError } from '../agent-runtime-error'
|
|
15
|
+
import { isJsonObject, jsonToolContent, mcpToolContent, ownJsonSlot } from '../mcp-tool-content'
|
|
15
16
|
import { optionalNonEmpty, removeUndefined } from '../normalize'
|
|
17
|
+
import { claudeFileChangeText } from './claude-file-change'
|
|
16
18
|
import { CLAUDE_AGENT_TOOL_SERVER } from './claude-tool-mapper'
|
|
17
19
|
|
|
18
20
|
type UserContentBlock = Exclude<MessageParam['content'], string>[number]
|
|
@@ -23,6 +25,18 @@ type ClaudeTaskStartedMessage = Extract<SDKMessage, { type: 'system', subtype: '
|
|
|
23
25
|
type ClaudeTaskProgressMessage = Extract<SDKMessage, { type: 'system', subtype: 'task_progress' }>
|
|
24
26
|
type ClaudeTaskNotificationMessage = Extract<SDKMessage, { type: 'system', subtype: 'task_notification' }>
|
|
25
27
|
type ClaudeUserMessage = Extract<SDKMessage, { type: 'user' }>
|
|
28
|
+
type ClaudeRateLimitInfo = Extract<SDKMessage, { type: 'rate_limit_event' }>['rate_limit_info']
|
|
29
|
+
type ClaudeResultMessage = Extract<SDKMessage, { type: 'result' }>
|
|
30
|
+
/** The main agent's latest context observation in the current turn: the input side of its last model response. */
|
|
31
|
+
type ClaudeContextObservation = { status: 'none' } | { status: 'observed', model: string, usedTokens: number }
|
|
32
|
+
/**
|
|
33
|
+
* Latest subscription usage-limit fact from `rate_limit_event`; a new event overwrites it. A rejection
|
|
34
|
+
* with a reset time (epoch seconds) stays in force until then, because the CLI throttles repeats of an
|
|
35
|
+
* unchanged rejection; one without a reset time applies only to the turn it preceded.
|
|
36
|
+
*/
|
|
37
|
+
type ClaudeUsageLimit
|
|
38
|
+
= | { status: 'available' }
|
|
39
|
+
| { status: 'exhausted', resetsAt?: number, rateLimitType?: ClaudeRateLimitInfo['rateLimitType'] }
|
|
26
40
|
type ClaudeSubagentActor = Extract<AgentEventActor, { type: 'subagent' }>
|
|
27
41
|
type TaskCreateWorkInput = Pick<TaskCreateInput, 'subject' | 'description'>
|
|
28
42
|
type TaskGetWorkInput = Pick<TaskGetInput, 'taskId'>
|
|
@@ -43,6 +57,11 @@ type ClaudeUnownedTurnEventDraft = ClaudeTurnEventDraft extends infer Draft
|
|
|
43
57
|
? Omit<Draft, 'actor'>
|
|
44
58
|
: never
|
|
45
59
|
: never
|
|
60
|
+
interface ClaudeActorContext { actor: AgentEventActor, turnId: string }
|
|
61
|
+
type ClaudeTaskActorResolution
|
|
62
|
+
= | { status: 'resolved', context: { actor: ClaudeSubagentActor, turnId: string } }
|
|
63
|
+
| { status: 'pending' }
|
|
64
|
+
| { status: 'conflict' }
|
|
46
65
|
|
|
47
66
|
const mainActor = { type: 'main', actorId: 'main' } satisfies AgentEventActor
|
|
48
67
|
|
|
@@ -99,6 +118,14 @@ export interface ClaudeEventMapperState {
|
|
|
99
118
|
readonly usageScopeId: string
|
|
100
119
|
readonly toolNameById: Map<string, string>
|
|
101
120
|
readonly pendingWorkCallById: Map<string, PendingClaudeWorkCall>
|
|
121
|
+
readonly hostMcpToolCallIds: Set<string>
|
|
122
|
+
readonly mcpToolCallIdByTaskId: Map<string, string | undefined>
|
|
123
|
+
readonly endedMcpTaskIds: Set<string>
|
|
124
|
+
readonly conflictingMcpTaskIds: Set<string>
|
|
125
|
+
/** Tasks that are neither MCP tasks nor visible subagents (for example backgrounded Bash); their lifecycle has no event. */
|
|
126
|
+
readonly nonSubagentTaskIds: Set<string>
|
|
127
|
+
/** Initiating tool calls of those tasks; frames forwarded under them have no visible owner. */
|
|
128
|
+
readonly nonSubagentToolCallIds: Set<string>
|
|
102
129
|
readonly startedMessageIds: Set<string>
|
|
103
130
|
readonly endedMessageIds: Set<string>
|
|
104
131
|
readonly streamedTextByMessageId: Map<string, string>
|
|
@@ -106,12 +133,39 @@ export interface ClaudeEventMapperState {
|
|
|
106
133
|
readonly streamedReasoningByMessageId: Map<string, string>
|
|
107
134
|
readonly contentBlockTypeByIndex: Map<number, string>
|
|
108
135
|
readonly subagentByToolCallId: Map<string, ClaudeSubagentActor>
|
|
136
|
+
readonly subagentByTaskId: Map<string, ClaudeSubagentActor>
|
|
137
|
+
readonly toolOwnerById: Map<string, ClaudeActorContext>
|
|
138
|
+
readonly turnIdBySubagentId: Map<string, string>
|
|
139
|
+
/** Initiating tool call of each subagent's current run: its Agent call, then each SendMessage resume. */
|
|
140
|
+
readonly runToolCallIdBySubagentId: Map<string, string>
|
|
141
|
+
readonly pendingMessages: SDKMessage[]
|
|
142
|
+
readonly startedSubagentIds: Set<string>
|
|
143
|
+
readonly endedSubagentIds: Set<string>
|
|
109
144
|
readonly actorByMessageId: Map<string, AgentEventActor>
|
|
145
|
+
readonly currentMessageIdByActorId: Map<string, string>
|
|
146
|
+
readonly contentBlockTypeByActorId: Map<string, Map<number, string>>
|
|
110
147
|
readonly startedReasoningIds: Set<string>
|
|
111
148
|
readonly endedReasoningIds: Set<string>
|
|
112
149
|
turnStarted: boolean
|
|
113
150
|
interrupt: { status: 'none' } | { status: 'requesting' | 'accepted', turnId: string }
|
|
151
|
+
contextObservation: ClaudeContextObservation
|
|
152
|
+
/** The last context snapshot reported in the current turn, so repeated frames do not re-report it. */
|
|
153
|
+
reportedContext?: AgentContextSnapshot
|
|
154
|
+
/** Context windows the CLI reported per model key in result `modelUsage`, reused for mid-turn snapshots. */
|
|
155
|
+
readonly contextWindowByModel: Map<string, number>
|
|
156
|
+
usageLimit: ClaudeUsageLimit
|
|
157
|
+
mainTurnRateLimited: boolean
|
|
158
|
+
sessionCwd?: string
|
|
159
|
+
sessionModel?: string
|
|
114
160
|
sessionState?: 'running' | 'idle'
|
|
161
|
+
/** The CLI reports its own session state (CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS), so its `idle` ends the session's work. */
|
|
162
|
+
cliReportsSessionState?: true
|
|
163
|
+
/** Steering (`now`) inputs the CLI queued and has not started or dropped; they supersede the running ask. */
|
|
164
|
+
readonly queuedSteerIds: Set<string>
|
|
165
|
+
/** Submitted input identity until the main response confirms consumption. */
|
|
166
|
+
readonly promptCommandByInputId: Map<string, string>
|
|
167
|
+
/** The end of a turn whose ask a queued steer superseded; the turn continues if the steer starts. */
|
|
168
|
+
heldTurnEnd?: Extract<AgentEventDraft, { type: 'turn.ended' }>
|
|
115
169
|
currentTurnId?: string
|
|
116
170
|
currentCommandId?: string
|
|
117
171
|
currentMessageId?: string
|
|
@@ -123,6 +177,12 @@ export function createClaudeEventMapperState(): ClaudeEventMapperState {
|
|
|
123
177
|
usageScopeId: `claude-query:${randomUUID()}`,
|
|
124
178
|
toolNameById: new Map(),
|
|
125
179
|
pendingWorkCallById: new Map(),
|
|
180
|
+
hostMcpToolCallIds: new Set(),
|
|
181
|
+
mcpToolCallIdByTaskId: new Map(),
|
|
182
|
+
endedMcpTaskIds: new Set(),
|
|
183
|
+
conflictingMcpTaskIds: new Set(),
|
|
184
|
+
nonSubagentTaskIds: new Set(),
|
|
185
|
+
nonSubagentToolCallIds: new Set(),
|
|
126
186
|
startedMessageIds: new Set(),
|
|
127
187
|
endedMessageIds: new Set(),
|
|
128
188
|
streamedTextByMessageId: new Map(),
|
|
@@ -130,72 +190,158 @@ export function createClaudeEventMapperState(): ClaudeEventMapperState {
|
|
|
130
190
|
streamedReasoningByMessageId: new Map(),
|
|
131
191
|
contentBlockTypeByIndex: new Map(),
|
|
132
192
|
subagentByToolCallId: new Map(),
|
|
193
|
+
subagentByTaskId: new Map(),
|
|
194
|
+
toolOwnerById: new Map(),
|
|
195
|
+
turnIdBySubagentId: new Map(),
|
|
196
|
+
runToolCallIdBySubagentId: new Map(),
|
|
197
|
+
pendingMessages: [],
|
|
198
|
+
startedSubagentIds: new Set(),
|
|
199
|
+
endedSubagentIds: new Set(),
|
|
133
200
|
actorByMessageId: new Map(),
|
|
201
|
+
currentMessageIdByActorId: new Map(),
|
|
202
|
+
contentBlockTypeByActorId: new Map(),
|
|
134
203
|
startedReasoningIds: new Set(),
|
|
135
204
|
endedReasoningIds: new Set(),
|
|
136
205
|
turnStarted: false,
|
|
137
206
|
interrupt: { status: 'none' },
|
|
207
|
+
contextObservation: { status: 'none' },
|
|
208
|
+
contextWindowByModel: new Map(),
|
|
209
|
+
queuedSteerIds: new Set(),
|
|
210
|
+
promptCommandByInputId: new Map(),
|
|
211
|
+
usageLimit: { status: 'available' },
|
|
212
|
+
mainTurnRateLimited: false,
|
|
138
213
|
}
|
|
139
214
|
}
|
|
140
215
|
|
|
141
|
-
export function mapClaudeMessage(message: SDKMessage, state: ClaudeEventMapperState
|
|
216
|
+
export function mapClaudeMessage(message: SDKMessage, state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
217
|
+
const drafts = mapClaudeMessageWithKnownActor(message, state)
|
|
218
|
+
if (drafts === undefined) {
|
|
219
|
+
state.pendingMessages.push(message)
|
|
220
|
+
return []
|
|
221
|
+
}
|
|
222
|
+
return [...drafts, ...flushPendingMessages(state)]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function mapClaudeMessageWithKnownActor(message: SDKMessage, state: ClaudeEventMapperState): AgentEventDraft[] | undefined {
|
|
142
226
|
const drafts: AgentEventDraft[] = []
|
|
143
227
|
|
|
228
|
+
if ('parent_tool_use_id' in message && message.parent_tool_use_id !== null && state.nonSubagentToolCallIds.has(message.parent_tool_use_id))
|
|
229
|
+
return drafts
|
|
230
|
+
|
|
231
|
+
// allowed/allowed_warning snapshots, system/api_retry, and auth_status have no provider-neutral
|
|
232
|
+
// event; Codex likewise drops its rate-limit snapshots and will-retry errors. Only a rejected
|
|
233
|
+
// limit is retained, to classify the turn failure it causes.
|
|
234
|
+
if (message.type === 'rate_limit_event') {
|
|
235
|
+
state.usageLimit = usageLimitFromRateLimitInfo(message.rate_limit_info)
|
|
236
|
+
return drafts
|
|
237
|
+
}
|
|
238
|
+
|
|
144
239
|
if (message.type === 'system') {
|
|
240
|
+
if (message.subtype === 'init') {
|
|
241
|
+
state.sessionCwd = message.cwd
|
|
242
|
+
state.sessionModel = message.model
|
|
243
|
+
}
|
|
244
|
+
if (message.subtype === 'session_state_changed')
|
|
245
|
+
state.cliReportsSessionState = true
|
|
145
246
|
if (message.subtype === 'session_state_changed' && message.state === 'running')
|
|
146
247
|
ensureTurnStarted(drafts, state)
|
|
147
|
-
if (message.subtype === 'session_state_changed' && message.state === 'idle')
|
|
248
|
+
if (message.subtype === 'session_state_changed' && message.state === 'idle') {
|
|
249
|
+
drafts.push(...flushHeldTurnEnd(state))
|
|
148
250
|
ensureSessionIdle(drafts, state)
|
|
251
|
+
}
|
|
149
252
|
if (message.subtype === 'status')
|
|
150
253
|
drafts.push(...mapStatusMessage(message, state))
|
|
151
254
|
if (message.subtype === 'compact_boundary')
|
|
152
255
|
drafts.push(...mapCompactBoundaryMessage(message, state))
|
|
153
|
-
if (message.subtype === 'task_started')
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
256
|
+
if (message.subtype === 'task_started') {
|
|
257
|
+
const taskDrafts = mapTaskStartedMessage(message, state)
|
|
258
|
+
if (!taskDrafts)
|
|
259
|
+
return undefined
|
|
260
|
+
drafts.push(...taskDrafts)
|
|
261
|
+
}
|
|
262
|
+
if (message.subtype === 'task_progress') {
|
|
263
|
+
const taskDrafts = mapTaskProgressMessage(message, state)
|
|
264
|
+
if (!taskDrafts)
|
|
265
|
+
return undefined
|
|
266
|
+
drafts.push(...taskDrafts)
|
|
267
|
+
}
|
|
268
|
+
if (message.subtype === 'task_notification') {
|
|
269
|
+
const taskDrafts = mapTaskNotificationMessage(message, state)
|
|
270
|
+
if (!taskDrafts)
|
|
271
|
+
return undefined
|
|
272
|
+
drafts.push(...taskDrafts)
|
|
273
|
+
}
|
|
159
274
|
return drafts
|
|
160
275
|
}
|
|
161
276
|
|
|
162
277
|
if (message.type === 'assistant') {
|
|
163
|
-
const
|
|
164
|
-
|
|
278
|
+
const context = actorContextFromParentToolUseId(message.parent_tool_use_id, drafts, state)
|
|
279
|
+
if (!context)
|
|
280
|
+
return undefined
|
|
281
|
+
const { actor, turnId } = context
|
|
282
|
+
if (actor.type === 'main') {
|
|
283
|
+
correlateConsumedPrompts({ message, turnId, drafts, state })
|
|
284
|
+
observeMainAssistantMessage(message, state)
|
|
285
|
+
}
|
|
165
286
|
ensureAssistantMessageStarted(drafts, state, message.message, turnId, actor)
|
|
166
|
-
|
|
287
|
+
if (actor.type === 'main')
|
|
288
|
+
reportContextSnapshot({ drafts, state, turnId })
|
|
289
|
+
drafts.push(...mapAssistantContent(message.message, state, turnId, actor, actor.type === 'main'))
|
|
167
290
|
if (message.message.stop_reason !== null || shouldEndAssistantSnapshot(message.message, state, actor))
|
|
168
291
|
endAssistantMessage(drafts, state, message.message, turnId, actor)
|
|
169
292
|
return drafts
|
|
170
293
|
}
|
|
171
294
|
|
|
172
295
|
if (message.type === 'stream_event') {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
296
|
+
const context = actorContextFromParentToolUseId(message.parent_tool_use_id, drafts, state)
|
|
297
|
+
if (!context)
|
|
298
|
+
return undefined
|
|
299
|
+
if (context.actor.type === 'main')
|
|
300
|
+
correlateConsumedPrompts({ message, turnId: context.turnId, drafts, state })
|
|
301
|
+
drafts.push(...mapStreamEvent(message.event, state, context.turnId, context.actor))
|
|
176
302
|
return drafts
|
|
177
303
|
}
|
|
178
304
|
|
|
179
305
|
if (message.type === 'user') {
|
|
180
|
-
const
|
|
181
|
-
|
|
306
|
+
const context = actorContextFromParentToolUseId(message.parent_tool_use_id, drafts, state)
|
|
307
|
+
if (!context)
|
|
308
|
+
return undefined
|
|
309
|
+
const { actor, turnId } = context
|
|
182
310
|
const content = message.message.content
|
|
183
311
|
const messageId = message.uuid ?? `user-${turnId}`
|
|
184
312
|
if (typeof content === 'string')
|
|
185
|
-
return actor ? subagentUserMessageEvents({ messageId, text: content, turnId, actor }) : drafts
|
|
186
|
-
if (actor)
|
|
313
|
+
return actor.type === 'subagent' ? subagentUserMessageEvents({ messageId, text: content, turnId, actor }) : drafts
|
|
314
|
+
if (actor.type === 'subagent')
|
|
187
315
|
drafts.push(...subagentUserContentEvents({ messageId, content, turnId, actor }))
|
|
188
|
-
|
|
316
|
+
const hostTrailerToolCallId = correlatedHostTrailerToolCallId({ content, message, state })
|
|
317
|
+
const structuredResultToolCallId = soleToolResultToolCallId(content)
|
|
318
|
+
drafts.push(...content.flatMap(block => mapUserContentBlock({
|
|
319
|
+
block,
|
|
320
|
+
message,
|
|
321
|
+
state,
|
|
322
|
+
turnId,
|
|
323
|
+
actor,
|
|
324
|
+
isMain: actor.type === 'main',
|
|
325
|
+
hostTrailer: block.type === 'tool_result' && block.tool_use_id === hostTrailerToolCallId ? message.tool_use_result : undefined,
|
|
326
|
+
ownsStructuredResult: block.type === 'tool_result' && block.tool_use_id === structuredResultToolCallId,
|
|
327
|
+
})))
|
|
189
328
|
return drafts
|
|
190
329
|
}
|
|
191
330
|
|
|
192
331
|
if (message.type === 'result') {
|
|
193
332
|
const turnId = ensureTurnStarted(drafts, state)
|
|
333
|
+
correlateConsumedPrompts({ message, turnId, drafts, state })
|
|
194
334
|
const usage = usageFromObject(message.usage)
|
|
195
|
-
// SDK usage is per
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
335
|
+
// Claude Agent SDK result usage is per completed ask/turn for the stream-json path we use,
|
|
336
|
+
// not a monotonic counter across the long-lived query. Emit it directly so summing
|
|
337
|
+
// normalized usage events gives the run total; do not diff against prior results.
|
|
338
|
+
// modelUsage is cumulative within this query process and resets when a resumed CLI starts;
|
|
339
|
+
// accounting keeps that distinction so the control plane can deduplicate observations.
|
|
340
|
+
// Accounting needs the result's own identity; a result without one still reports usage.
|
|
341
|
+
if (usage && !message.uuid) {
|
|
342
|
+
drafts.push({ type: 'usage', turnId, actor: mainActor, payload: { usage } })
|
|
343
|
+
}
|
|
344
|
+
else if (usage) {
|
|
199
345
|
drafts.push({ type: 'usage', turnId, actor: mainActor, payload: { usage, accounting: {
|
|
200
346
|
observationId: message.uuid,
|
|
201
347
|
mode: Object.keys(message.modelUsage ?? {}).length ? 'cumulative' : 'increment',
|
|
@@ -210,36 +356,64 @@ export function mapClaudeMessage(message: SDKMessage, state: ClaudeEventMapperSt
|
|
|
210
356
|
}])),
|
|
211
357
|
} } })
|
|
212
358
|
}
|
|
213
|
-
|
|
214
|
-
// Usage belongs to this segment and must still be emitted exactly once. The segment
|
|
215
|
-
// may have been aborted mid-generation (error_during_execution): close whatever it
|
|
216
|
-
// streamed so the steer's user message follows a closed assistant step.
|
|
217
|
-
if (options.continueTurn) {
|
|
218
|
-
endOpenReasoning(drafts, state, turnId)
|
|
219
|
-
endOpenAssistantMessage(drafts, state, turnId)
|
|
220
|
-
return drafts
|
|
221
|
-
}
|
|
359
|
+
reportContextSnapshot({ drafts, state, turnId, result: message })
|
|
222
360
|
endOpenReasoning(drafts, state, turnId)
|
|
223
361
|
const interruptAccepted = state.interrupt.status === 'accepted' && state.interrupt.turnId === turnId
|
|
224
|
-
|
|
225
|
-
|
|
362
|
+
const usageLimit = state.mainTurnRateLimited ? usageLimitInForce(state.usageLimit, Date.now()) : undefined
|
|
363
|
+
const ended: Extract<AgentEventDraft, { type: 'turn.ended' }> = { type: 'turn.ended', turnId, actor: mainActor, payload: turnEndedPayload({ message, interruptAccepted, usageLimit }) }
|
|
226
364
|
state.pendingWorkCallById.clear()
|
|
227
|
-
state.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
365
|
+
state.contextObservation = { status: 'none' }
|
|
366
|
+
const heldBySteer = !interruptAccepted && state.queuedSteerIds.size > 0
|
|
367
|
+
// A superseded ask may end mid-response: close the streamed message before the steer's
|
|
368
|
+
// input can enter the continued turn.
|
|
369
|
+
if (heldBySteer)
|
|
370
|
+
endOpenAssistantMessage(drafts, state, turnId)
|
|
231
371
|
state.currentMessageId = undefined
|
|
372
|
+
state.contentBlockTypeByIndex.clear()
|
|
232
373
|
state.currentCompactionId = undefined
|
|
374
|
+
// Claude Code steers by superseding: a `now` input cancels the running ask, which ends with
|
|
375
|
+
// this result (`error_during_execution` before any model output), and the CLI then starts the
|
|
376
|
+
// steer on the same transcript. The turn stays open until the steer starts (it continues) or is
|
|
377
|
+
// dropped, or the CLI goes idle (the held end is final). An accepted interrupt always ends it.
|
|
378
|
+
if (heldBySteer) {
|
|
379
|
+
state.heldTurnEnd = ended
|
|
380
|
+
return drafts
|
|
381
|
+
}
|
|
382
|
+
drafts.push(ended)
|
|
383
|
+
endTurn(drafts, state)
|
|
233
384
|
return drafts
|
|
234
385
|
}
|
|
235
386
|
|
|
236
387
|
return drafts
|
|
237
388
|
}
|
|
238
389
|
|
|
390
|
+
function correlateConsumedPrompts({ message, turnId, drafts, state }: {
|
|
391
|
+
message: Extract<SDKMessage, { type: 'assistant' | 'stream_event' | 'result' }>
|
|
392
|
+
turnId: string
|
|
393
|
+
drafts: AgentEventDraft[]
|
|
394
|
+
state: ClaudeEventMapperState
|
|
395
|
+
}): void {
|
|
396
|
+
// Pair with the turn assigned to this very frame, after ensureTurnStarted and before result
|
|
397
|
+
// clears it. Lifecycle receipts and the turn active at submission do not prove consumption.
|
|
398
|
+
// SDK 0.3.257 declares only the singular field; 0.3.280 adds merged-input UUIDs.
|
|
399
|
+
const inputIds = 'user_message_uuids' in message && Array.isArray(message.user_message_uuids)
|
|
400
|
+
? message.user_message_uuids.filter((id): id is string => typeof id === 'string')
|
|
401
|
+
: message.user_message_uuid ? [message.user_message_uuid] : []
|
|
402
|
+
for (const inputId of inputIds) {
|
|
403
|
+
const commandId = state.promptCommandByInputId.get(inputId)
|
|
404
|
+
if (!commandId)
|
|
405
|
+
continue
|
|
406
|
+
drafts.push({ type: 'prompt.turn-correlated', payload: { commandId, turnId } })
|
|
407
|
+
state.promptCommandByInputId.delete(inputId)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
239
411
|
function mapStatusMessage(message: ClaudeStatusMessage, state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
240
412
|
const drafts: AgentEventDraft[] = []
|
|
241
413
|
if (message.status === 'compacting') {
|
|
242
414
|
const turnId = ensureTurnStarted(drafts, state)
|
|
415
|
+
state.contextObservation = { status: 'none' }
|
|
416
|
+
state.reportedContext = undefined
|
|
243
417
|
if (state.currentCompactionId)
|
|
244
418
|
return drafts
|
|
245
419
|
state.currentCompactionId = message.uuid
|
|
@@ -258,7 +432,7 @@ function mapStatusMessage(message: ClaudeStatusMessage, state: ClaudeEventMapper
|
|
|
258
432
|
compactionId,
|
|
259
433
|
trigger: 'unknown',
|
|
260
434
|
status: 'failed',
|
|
261
|
-
error:
|
|
435
|
+
error: compactionError(message),
|
|
262
436
|
},
|
|
263
437
|
})
|
|
264
438
|
state.currentCompactionId = undefined
|
|
@@ -267,13 +441,44 @@ function mapStatusMessage(message: ClaudeStatusMessage, state: ClaudeEventMapper
|
|
|
267
441
|
return drafts
|
|
268
442
|
}
|
|
269
443
|
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
444
|
+
function isMcpTaskMessage(message: { task_id: string, tool_use_id?: string }, state: ClaudeEventMapperState): boolean {
|
|
445
|
+
if (state.mcpToolCallIdByTaskId.has(message.task_id))
|
|
446
|
+
return true
|
|
447
|
+
const name = message.tool_use_id ? state.toolNameById.get(message.tool_use_id) : undefined
|
|
448
|
+
return name !== undefined && isHostMcpToolName(name)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Only an Agent-tool child (`local_agent`) is a subagent; the CLI also reports backgrounded Bash,
|
|
453
|
+
* workflows, monitors, and other tasks through the same lifecycle. The pinned CLI always sets the
|
|
454
|
+
* task type; an untyped task keeps the subagent mapping its ancestry evidence supports. The SDK
|
|
455
|
+
* marks housekeeping tasks `skip_transcript` for hosts to hide.
|
|
456
|
+
*/
|
|
457
|
+
function isVisibleSubagentTask(message: ClaudeTaskStartedMessage): boolean {
|
|
458
|
+
return (message.task_type === undefined || message.task_type === 'local_agent') && message.skip_transcript !== true
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* SendMessage resumes a finished subagent as a new run: the CLI repeats its task_id with the
|
|
463
|
+
* SendMessage call as tool_use_id. The run keeps the subagent's identity and turn, so it reopens the
|
|
464
|
+
* same subagent. Returns null when the message is not a resume of a known subagent.
|
|
465
|
+
*/
|
|
466
|
+
function mapResumedSubagentRun(message: ClaudeTaskStartedMessage, state: ClaudeEventMapperState): AgentEventDraft[] | undefined | null {
|
|
467
|
+
const actor = state.subagentByTaskId.get(message.task_id)
|
|
468
|
+
const toolCallId = message.tool_use_id
|
|
469
|
+
if (!actor || !toolCallId || toolCallId === actor.parentToolCallId || state.subagentByToolCallId.has(toolCallId))
|
|
470
|
+
return null
|
|
471
|
+
const owner = state.toolOwnerById.get(toolCallId)
|
|
472
|
+
if (!owner)
|
|
473
|
+
return undefined
|
|
474
|
+
const turnId = state.turnIdBySubagentId.get(message.task_id)
|
|
475
|
+
if (owner.actor.actorId !== actor.parentActorId || !turnId)
|
|
476
|
+
return []
|
|
477
|
+
|
|
478
|
+
state.subagentByToolCallId.set(toolCallId, actor)
|
|
479
|
+
state.runToolCallIdBySubagentId.set(message.task_id, toolCallId)
|
|
480
|
+
state.endedSubagentIds.delete(message.task_id)
|
|
481
|
+
return [{
|
|
277
482
|
type: 'subagent.started',
|
|
278
483
|
turnId,
|
|
279
484
|
actor,
|
|
@@ -281,51 +486,134 @@ function mapTaskStartedMessage(message: ClaudeTaskStartedMessage, state: ClaudeE
|
|
|
281
486
|
...subagentPayloadRef(actor),
|
|
282
487
|
description: message.description,
|
|
283
488
|
}),
|
|
284
|
-
}
|
|
285
|
-
return drafts
|
|
489
|
+
}]
|
|
286
490
|
}
|
|
287
491
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
492
|
+
/** A resumed subagent must not take progress or a terminal from a run that SendMessage superseded. */
|
|
493
|
+
function isEarlierSubagentRun(message: { task_id: string, tool_use_id?: string }, state: ClaudeEventMapperState): boolean {
|
|
494
|
+
const runToolCallId = state.runToolCallIdBySubagentId.get(message.task_id)
|
|
495
|
+
return message.tool_use_id !== undefined && runToolCallId !== undefined && message.tool_use_id !== runToolCallId
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function mapTaskStartedMessage(message: ClaudeTaskStartedMessage, state: ClaudeEventMapperState): AgentEventDraft[] | undefined {
|
|
499
|
+
if (message.task_type === 'mcp_task' || isMcpTaskMessage(message, state)) {
|
|
500
|
+
const previous = state.mcpToolCallIdByTaskId.get(message.task_id)
|
|
501
|
+
if (previous && message.tool_use_id && previous !== message.tool_use_id)
|
|
502
|
+
state.conflictingMcpTaskIds.add(message.task_id)
|
|
503
|
+
else
|
|
504
|
+
state.mcpToolCallIdByTaskId.set(message.task_id, previous ?? message.tool_use_id)
|
|
505
|
+
return []
|
|
506
|
+
}
|
|
507
|
+
if (!isVisibleSubagentTask(message)) {
|
|
508
|
+
state.nonSubagentTaskIds.add(message.task_id)
|
|
509
|
+
if (message.tool_use_id)
|
|
510
|
+
state.nonSubagentToolCallIds.add(message.tool_use_id)
|
|
511
|
+
return []
|
|
512
|
+
}
|
|
513
|
+
const resumed = mapResumedSubagentRun(message, state)
|
|
514
|
+
if (resumed !== null)
|
|
515
|
+
return resumed
|
|
516
|
+
const resolution = subagentActorForTaskMessage({ subagentId: message.task_id, parentToolCallId: message.tool_use_id, name: message.subagent_type }, state)
|
|
517
|
+
if (resolution.status === 'pending')
|
|
518
|
+
return undefined
|
|
519
|
+
if (resolution.status === 'conflict' || state.startedSubagentIds.has(message.task_id))
|
|
520
|
+
return []
|
|
521
|
+
|
|
522
|
+
state.startedSubagentIds.add(message.task_id)
|
|
523
|
+
const { actor, turnId } = resolution.context
|
|
524
|
+
return [{
|
|
525
|
+
type: 'subagent.started',
|
|
294
526
|
turnId,
|
|
295
527
|
actor,
|
|
296
528
|
payload: removeUndefined({
|
|
297
529
|
...subagentPayloadRef(actor),
|
|
298
|
-
|
|
299
|
-
lastToolName: optionalNonEmpty(message.last_tool_name),
|
|
300
|
-
usage: usageFromTaskUsage(message.usage),
|
|
530
|
+
description: message.description,
|
|
301
531
|
}),
|
|
302
|
-
}
|
|
303
|
-
return drafts
|
|
532
|
+
}]
|
|
304
533
|
}
|
|
305
534
|
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
535
|
+
function mapTaskProgressMessage(message: ClaudeTaskProgressMessage, state: ClaudeEventMapperState): AgentEventDraft[] | undefined {
|
|
536
|
+
if (isMcpTaskMessage(message, state)) {
|
|
537
|
+
if (!state.mcpToolCallIdByTaskId.has(message.task_id))
|
|
538
|
+
state.mcpToolCallIdByTaskId.set(message.task_id, message.tool_use_id)
|
|
539
|
+
return []
|
|
540
|
+
}
|
|
541
|
+
if (state.nonSubagentTaskIds.has(message.task_id))
|
|
542
|
+
return []
|
|
543
|
+
const resolution = subagentActorForTaskMessage({ subagentId: message.task_id, parentToolCallId: message.tool_use_id, name: message.subagent_type }, state)
|
|
544
|
+
if (resolution.status === 'pending')
|
|
545
|
+
return undefined
|
|
546
|
+
if (resolution.status === 'conflict' || isEarlierSubagentRun(message, state))
|
|
547
|
+
return []
|
|
548
|
+
|
|
549
|
+
const { actor, turnId } = resolution.context
|
|
550
|
+
return [{
|
|
551
|
+
type: 'subagent.progress',
|
|
312
552
|
turnId,
|
|
313
553
|
actor,
|
|
314
554
|
payload: removeUndefined({
|
|
315
555
|
...subagentPayloadRef(actor),
|
|
316
|
-
status: taskNotificationStatus(message.status),
|
|
317
556
|
summary: optionalNonEmpty(message.summary),
|
|
318
|
-
|
|
319
|
-
|
|
557
|
+
lastToolName: optionalNonEmpty(message.last_tool_name),
|
|
558
|
+
usage: usageFromTaskUsage(message.usage),
|
|
320
559
|
}),
|
|
321
|
-
}
|
|
322
|
-
|
|
560
|
+
}]
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function mapTaskNotificationMessage(message: ClaudeTaskNotificationMessage, state: ClaudeEventMapperState): AgentEventDraft[] | undefined {
|
|
564
|
+
if (isMcpTaskMessage(message, state) || message.resource_links !== undefined) {
|
|
565
|
+
const rememberedToolCallId = state.mcpToolCallIdByTaskId.get(message.task_id)
|
|
566
|
+
if (rememberedToolCallId && message.tool_use_id && rememberedToolCallId !== message.tool_use_id)
|
|
567
|
+
state.conflictingMcpTaskIds.add(message.task_id)
|
|
568
|
+
if (state.endedMcpTaskIds.has(message.task_id) || state.conflictingMcpTaskIds.has(message.task_id))
|
|
569
|
+
return []
|
|
570
|
+
const toolCallId = message.tool_use_id ?? rememberedToolCallId
|
|
571
|
+
const owner = toolCallId ? state.toolOwnerById.get(toolCallId) : undefined
|
|
572
|
+
if (!toolCallId || !owner)
|
|
573
|
+
return undefined
|
|
574
|
+
state.endedMcpTaskIds.add(message.task_id)
|
|
575
|
+
const content: AgentToolContent[] = [{ type: 'text', text: message.summary }]
|
|
576
|
+
content.push(...(message.resource_links ?? []).map(link => resourceLinkContent(jsonValueSchema.parse(link))))
|
|
577
|
+
if (message.output_file !== '')
|
|
578
|
+
content.push(jsonToolContent({ outputFile: message.output_file }))
|
|
579
|
+
return [{
|
|
580
|
+
type: 'tool.output.updated',
|
|
581
|
+
...owner,
|
|
582
|
+
payload: { toolCallId, output: { success: message.status === 'completed', content } },
|
|
583
|
+
}]
|
|
584
|
+
}
|
|
585
|
+
if (state.nonSubagentTaskIds.has(message.task_id))
|
|
586
|
+
return []
|
|
587
|
+
const resolution = subagentActorForTaskMessage({ subagentId: message.task_id, parentToolCallId: message.tool_use_id }, state)
|
|
588
|
+
if (resolution.status === 'pending')
|
|
589
|
+
return undefined
|
|
590
|
+
if (resolution.status === 'conflict' || isEarlierSubagentRun(message, state) || state.endedSubagentIds.has(message.task_id))
|
|
591
|
+
return []
|
|
592
|
+
|
|
593
|
+
state.endedSubagentIds.add(message.task_id)
|
|
594
|
+
const { actor, turnId } = resolution.context
|
|
595
|
+
return [
|
|
596
|
+
{
|
|
597
|
+
type: 'subagent.ended',
|
|
598
|
+
turnId,
|
|
599
|
+
actor,
|
|
600
|
+
payload: removeUndefined({
|
|
601
|
+
...subagentPayloadRef(actor),
|
|
602
|
+
status: taskNotificationStatus(message.status),
|
|
603
|
+
summary: optionalNonEmpty(message.summary),
|
|
604
|
+
usage: message.usage ? usageFromTaskUsage(message.usage) : undefined,
|
|
605
|
+
error: message.status === 'failed' ? { message: optionalNonEmpty(message.summary) ?? `Subagent ${message.task_id} failed.` } : undefined,
|
|
606
|
+
}),
|
|
607
|
+
},
|
|
608
|
+
]
|
|
323
609
|
}
|
|
324
610
|
|
|
325
611
|
function mapCompactBoundaryMessage(message: ClaudeCompactBoundaryMessage, state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
326
612
|
const drafts: AgentEventDraft[] = []
|
|
327
613
|
const turnId = ensureTurnStarted(drafts, state)
|
|
328
614
|
const compactionId = state.currentCompactionId ?? message.uuid
|
|
615
|
+
state.contextObservation = { status: 'none' }
|
|
616
|
+
state.reportedContext = undefined
|
|
329
617
|
drafts.push({
|
|
330
618
|
type: 'context.compaction.completed',
|
|
331
619
|
turnId,
|
|
@@ -343,7 +631,7 @@ function mapCompactBoundaryMessage(message: ClaudeCompactBoundaryMessage, state:
|
|
|
343
631
|
return drafts
|
|
344
632
|
}
|
|
345
633
|
|
|
346
|
-
function mapAssistantContent(message: BetaMessage, state: ClaudeEventMapperState, turnId: string, actor: AgentEventActor
|
|
634
|
+
function mapAssistantContent(message: BetaMessage, state: ClaudeEventMapperState, turnId: string, actor: AgentEventActor, isMain: boolean): AgentEventDraft[] {
|
|
347
635
|
const drafts: AgentEventDraft[] = []
|
|
348
636
|
const streamedText = state.streamedTextByMessageId.has(message.id)
|
|
349
637
|
const streamedReasoning = state.streamedReasoningMessageIds.has(message.id)
|
|
@@ -360,6 +648,12 @@ function mapAssistantContent(message: BetaMessage, state: ClaudeEventMapperState
|
|
|
360
648
|
if (block.type === 'tool_use') {
|
|
361
649
|
const toolName = normalizeToolName(block.name)
|
|
362
650
|
state.toolNameById.set(block.id, toolName)
|
|
651
|
+
if (isHostMcpToolName(block.name))
|
|
652
|
+
state.hostMcpToolCallIds.add(block.id)
|
|
653
|
+
// SDK task_id identifies the child, while task_started.tool_use_id identifies this
|
|
654
|
+
// initiating call; the actor that emitted the tool_use is the only ancestry evidence.
|
|
655
|
+
if (!state.toolOwnerById.has(block.id))
|
|
656
|
+
state.toolOwnerById.set(block.id, { actor, turnId })
|
|
363
657
|
if (isMain) {
|
|
364
658
|
const pendingWorkCall = pendingClaudeWorkCall(block.name, block.input)
|
|
365
659
|
if (pendingWorkCall)
|
|
@@ -371,27 +665,53 @@ function mapAssistantContent(message: BetaMessage, state: ClaudeEventMapperState
|
|
|
371
665
|
return drafts
|
|
372
666
|
}
|
|
373
667
|
|
|
374
|
-
function mapUserContentBlock(
|
|
668
|
+
function mapUserContentBlock(input: {
|
|
669
|
+
block: UserContentBlock
|
|
670
|
+
message: ClaudeUserMessage
|
|
671
|
+
state: ClaudeEventMapperState
|
|
672
|
+
turnId: string
|
|
673
|
+
actor: AgentEventActor
|
|
674
|
+
isMain: boolean
|
|
675
|
+
hostTrailer?: unknown
|
|
676
|
+
ownsStructuredResult: boolean
|
|
677
|
+
}): AgentEventDraft[] {
|
|
678
|
+
const { block, message, state, turnId, actor, isMain, hostTrailer, ownsStructuredResult } = input
|
|
375
679
|
if (block.type !== 'tool_result')
|
|
376
680
|
return []
|
|
377
681
|
const toolCallId = block.tool_use_id
|
|
378
682
|
const pendingWorkCall = state.pendingWorkCallById.get(toolCallId)
|
|
379
683
|
if (pendingWorkCall)
|
|
380
684
|
state.pendingWorkCallById.delete(toolCallId)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
685
|
+
state.hostMcpToolCallIds.delete(toolCallId)
|
|
686
|
+
|
|
687
|
+
const fileChangeText = ownsStructuredResult && block.is_error !== true
|
|
688
|
+
? claudeFileChangeText({ toolName: state.toolNameById.get(toolCallId), toolUseResult: message.tool_use_result, cwd: state.sessionCwd })
|
|
689
|
+
: undefined
|
|
690
|
+
const mapped = fileChangeText === undefined
|
|
691
|
+
? mappedModelToolResultContent({ content: block.content, isError: block.is_error === true })
|
|
692
|
+
: { content: [{ type: 'text' as const, text: fileChangeText }], text: fileChangeText }
|
|
693
|
+
const content = hostTrailer === undefined ? mapped.content : mappedHostMcpContent({ trailer: hostTrailer, modelBlocks: mapped.content })
|
|
694
|
+
const structuredContent = ownJsonSlot(hostTrailer, 'structuredContent')
|
|
695
|
+
const reportedError = ownJsonSlot(hostTrailer, 'isError')
|
|
696
|
+
const output = removeUndefined({
|
|
697
|
+
success: block.is_error !== true,
|
|
698
|
+
content,
|
|
699
|
+
structuredContent,
|
|
700
|
+
reportedSuccess: typeof reportedError === 'boolean' ? !reportedError : undefined,
|
|
701
|
+
})
|
|
384
702
|
const drafts: AgentEventDraft[] = []
|
|
385
|
-
if (text)
|
|
386
|
-
drafts.push(withActor({ type: 'tool.output.delta', turnId, payload: { toolCallId, delta: text } }, actor))
|
|
703
|
+
if (mapped.text !== undefined)
|
|
704
|
+
drafts.push(withActor({ type: 'tool.output.delta', turnId, payload: { toolCallId, delta: mapped.text } }, actor))
|
|
387
705
|
drafts.push(withActor({
|
|
388
706
|
type: 'tool.call.completed',
|
|
389
707
|
turnId,
|
|
390
708
|
payload: removeUndefined({
|
|
391
709
|
toolCallId,
|
|
710
|
+
toolName: state.toolNameById.get(toolCallId),
|
|
392
711
|
status: block.is_error === true ? 'failed' : 'completed',
|
|
393
712
|
output,
|
|
394
|
-
|
|
713
|
+
// The failure detail already rides in output; like Codex, the error is a short summary.
|
|
714
|
+
error: block.is_error === true ? { message: `Tool ${state.toolNameById.get(toolCallId) ?? toolCallId} failed.` } : undefined,
|
|
395
715
|
}),
|
|
396
716
|
}, actor))
|
|
397
717
|
|
|
@@ -468,20 +788,19 @@ function parseClaudeTaskResult<Output>({ value, toolName, schema }: { value: unk
|
|
|
468
788
|
})
|
|
469
789
|
}
|
|
470
790
|
|
|
471
|
-
function mapStreamEvent(event: BetaRawMessageStreamEvent, state: ClaudeEventMapperState, turnId: string, actor: AgentEventActor
|
|
791
|
+
function mapStreamEvent(event: BetaRawMessageStreamEvent, state: ClaudeEventMapperState, turnId: string, actor: AgentEventActor): AgentEventDraft[] {
|
|
472
792
|
const drafts: AgentEventDraft[] = []
|
|
473
793
|
if (event.type === 'message_start') {
|
|
474
|
-
state
|
|
475
|
-
|
|
476
|
-
state.actorByMessageId.set(event.message.id, actor)
|
|
794
|
+
setCurrentMessageId(state, actor, event.message.id)
|
|
795
|
+
state.actorByMessageId.set(event.message.id, actor)
|
|
477
796
|
ensureAssistantMessageStarted(drafts, state, event.message, turnId, actor)
|
|
478
797
|
return drafts
|
|
479
798
|
}
|
|
480
799
|
|
|
481
800
|
if (event.type === 'content_block_start') {
|
|
482
|
-
const messageId = currentMessageId(state)
|
|
483
|
-
const messageActor =
|
|
484
|
-
state.
|
|
801
|
+
const messageId = currentMessageId(state, actor)
|
|
802
|
+
const messageActor = state.actorByMessageId.get(messageId) ?? actor
|
|
803
|
+
contentBlockTypes(state, actor).set(event.index, event.content_block.type)
|
|
485
804
|
if (event.content_block.type === 'thinking') {
|
|
486
805
|
state.streamedReasoningMessageIds.add(messageId)
|
|
487
806
|
ensureReasoningStarted(drafts, state, turnId, messageId, messageActor)
|
|
@@ -490,30 +809,30 @@ function mapStreamEvent(event: BetaRawMessageStreamEvent, state: ClaudeEventMapp
|
|
|
490
809
|
}
|
|
491
810
|
|
|
492
811
|
if (event.type === 'content_block_stop') {
|
|
493
|
-
const messageId = currentMessageId(state)
|
|
494
|
-
const messageActor =
|
|
495
|
-
const
|
|
812
|
+
const messageId = currentMessageId(state, actor)
|
|
813
|
+
const messageActor = state.actorByMessageId.get(messageId) ?? actor
|
|
814
|
+
const blockTypes = contentBlockTypes(state, actor)
|
|
815
|
+
const blockType = blockTypes.get(event.index)
|
|
496
816
|
if (blockType === 'thinking')
|
|
497
817
|
endReasoning(drafts, state, turnId, messageId, state.streamedReasoningByMessageId.get(messageId), messageActor)
|
|
498
|
-
|
|
818
|
+
blockTypes.delete(event.index)
|
|
499
819
|
return drafts
|
|
500
820
|
}
|
|
501
821
|
|
|
502
822
|
if (event.type === 'message_stop') {
|
|
503
|
-
const messageId = currentMessageId(state)
|
|
504
|
-
const messageActor =
|
|
823
|
+
const messageId = currentMessageId(state, actor)
|
|
824
|
+
const messageActor = state.actorByMessageId.get(messageId) ?? actor
|
|
505
825
|
endAssistantMessageById(drafts, state, turnId, messageId, state.streamedTextByMessageId.get(messageId), messageActor)
|
|
506
826
|
state.actorByMessageId.delete(messageId)
|
|
507
|
-
state
|
|
508
|
-
state.contentBlockTypeByIndex.clear()
|
|
827
|
+
clearCurrentMessage(state, actor)
|
|
509
828
|
return drafts
|
|
510
829
|
}
|
|
511
830
|
|
|
512
831
|
if (event.type !== 'content_block_delta')
|
|
513
832
|
return drafts
|
|
514
833
|
|
|
515
|
-
const messageId = currentMessageId(state)
|
|
516
|
-
const messageActor =
|
|
834
|
+
const messageId = currentMessageId(state, actor)
|
|
835
|
+
const messageActor = state.actorByMessageId.get(messageId) ?? actor
|
|
517
836
|
const delta = event.delta
|
|
518
837
|
if (delta.type === 'thinking_delta') {
|
|
519
838
|
state.streamedReasoningMessageIds.add(messageId)
|
|
@@ -530,18 +849,64 @@ function mapStreamEvent(event: BetaRawMessageStreamEvent, state: ClaudeEventMapp
|
|
|
530
849
|
}
|
|
531
850
|
|
|
532
851
|
function ensureTurnStarted(drafts: AgentEventDraft[], state: ClaudeEventMapperState): string {
|
|
533
|
-
if (state.turnStarted
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
852
|
+
if (!state.turnStarted || !state.currentTurnId) {
|
|
853
|
+
state.turnStarted = true
|
|
854
|
+
state.interrupt = { status: 'none' }
|
|
855
|
+
state.currentTurnId = createRuntimeTurnId()
|
|
856
|
+
state.currentMessageId = undefined
|
|
857
|
+
state.contentBlockTypeByIndex.clear()
|
|
858
|
+
drafts.push({ type: 'turn.started', turnId: state.currentTurnId, actor: mainActor, payload: removeUndefined({ commandId: state.currentCommandId }) })
|
|
859
|
+
}
|
|
541
860
|
ensureSessionRunning(drafts, state)
|
|
542
861
|
return state.currentTurnId
|
|
543
862
|
}
|
|
544
863
|
|
|
864
|
+
// Turn-scoped state after the main turn's `turn.ended`. With CLI state events the CLI's own `idle`
|
|
865
|
+
// ends the session's work; without them the turn end is the only turn-over signal.
|
|
866
|
+
function endTurn(drafts: AgentEventDraft[], state: ClaudeEventMapperState): void {
|
|
867
|
+
if (!state.cliReportsSessionState)
|
|
868
|
+
ensureSessionIdle(drafts, state)
|
|
869
|
+
state.reportedContext = undefined
|
|
870
|
+
state.mainTurnRateLimited = false
|
|
871
|
+
state.usageLimit = usageLimitAfterTurn(state.usageLimit)
|
|
872
|
+
state.turnStarted = false
|
|
873
|
+
state.interrupt = { status: 'none' }
|
|
874
|
+
state.currentTurnId = undefined
|
|
875
|
+
state.currentCommandId = undefined
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** Emits a superseded turn's held end as final: its steer will not continue it. */
|
|
879
|
+
export function flushHeldTurnEnd(state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
880
|
+
const held = state.heldTurnEnd
|
|
881
|
+
if (!held)
|
|
882
|
+
return []
|
|
883
|
+
state.heldTurnEnd = undefined
|
|
884
|
+
const drafts: AgentEventDraft[] = [held]
|
|
885
|
+
endTurn(drafts, state)
|
|
886
|
+
return drafts
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Applies the CLI's `command_lifecycle` receipt for a steering input. `queued` marks a pending
|
|
891
|
+
* supersession; `started` continues the held turn (or starts one, so an interrupt has a target);
|
|
892
|
+
* any other state drops the steer, and the held end is final once no steer can continue it.
|
|
893
|
+
*/
|
|
894
|
+
export function mapSteerLifecycle(input: { steerId: string, state: 'queued' | 'started' | 'completed' | 'cancelled' | 'discarded' | 'refused' }, state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
895
|
+
if (input.state === 'queued') {
|
|
896
|
+
state.queuedSteerIds.add(input.steerId)
|
|
897
|
+
return []
|
|
898
|
+
}
|
|
899
|
+
if (!state.queuedSteerIds.delete(input.steerId) && input.state !== 'started')
|
|
900
|
+
return []
|
|
901
|
+
if (input.state === 'started') {
|
|
902
|
+
state.heldTurnEnd = undefined
|
|
903
|
+
const drafts: AgentEventDraft[] = []
|
|
904
|
+
ensureTurnStarted(drafts, state)
|
|
905
|
+
return drafts
|
|
906
|
+
}
|
|
907
|
+
return state.queuedSteerIds.size === 0 ? flushHeldTurnEnd(state) : []
|
|
908
|
+
}
|
|
909
|
+
|
|
545
910
|
function ensureSessionRunning(drafts: AgentEventDraft[], state: ClaudeEventMapperState): void {
|
|
546
911
|
if (state.sessionState === 'running')
|
|
547
912
|
return
|
|
@@ -556,22 +921,49 @@ function ensureSessionIdle(drafts: AgentEventDraft[], state: ClaudeEventMapperSt
|
|
|
556
921
|
drafts.push({ type: 'session.state.changed', payload: { state: 'idle' } })
|
|
557
922
|
}
|
|
558
923
|
|
|
559
|
-
function ensureAssistantMessageStarted(drafts: AgentEventDraft[], state: ClaudeEventMapperState, message: BetaMessage, turnId: string, actor: AgentEventActor
|
|
560
|
-
state
|
|
561
|
-
|
|
562
|
-
state.actorByMessageId.set(message.id, actor)
|
|
924
|
+
function ensureAssistantMessageStarted(drafts: AgentEventDraft[], state: ClaudeEventMapperState, message: BetaMessage, turnId: string, actor: AgentEventActor): void {
|
|
925
|
+
setCurrentMessageId(state, actor, message.id)
|
|
926
|
+
state.actorByMessageId.set(message.id, actor)
|
|
563
927
|
if (state.startedMessageIds.has(message.id))
|
|
564
928
|
return
|
|
565
929
|
state.startedMessageIds.add(message.id)
|
|
566
930
|
drafts.push(withActor({ type: 'message.started', turnId, payload: { messageId: message.id, role: 'assistant' } }, actor))
|
|
567
931
|
}
|
|
568
932
|
|
|
569
|
-
function
|
|
570
|
-
if (
|
|
571
|
-
|
|
933
|
+
function setCurrentMessageId(state: ClaudeEventMapperState, actor: AgentEventActor, messageId: string): void {
|
|
934
|
+
if (actor.type === 'main')
|
|
935
|
+
state.currentMessageId = messageId
|
|
936
|
+
else state.currentMessageIdByActorId.set(actor.actorId, messageId)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function currentMessageId(state: ClaudeEventMapperState, actor: AgentEventActor): string {
|
|
940
|
+
const messageId = actor.type === 'main' ? state.currentMessageId : state.currentMessageIdByActorId.get(actor.actorId)
|
|
941
|
+
if (messageId)
|
|
942
|
+
return messageId
|
|
572
943
|
throw new AgentProviderProtocolError({ message: 'Claude stream emitted content_block_delta before message_start.' })
|
|
573
944
|
}
|
|
574
945
|
|
|
946
|
+
function contentBlockTypes(state: ClaudeEventMapperState, actor: AgentEventActor): Map<number, string> {
|
|
947
|
+
if (actor.type === 'main')
|
|
948
|
+
return state.contentBlockTypeByIndex
|
|
949
|
+
const existing = state.contentBlockTypeByActorId.get(actor.actorId)
|
|
950
|
+
if (existing)
|
|
951
|
+
return existing
|
|
952
|
+
const created = new Map<number, string>()
|
|
953
|
+
state.contentBlockTypeByActorId.set(actor.actorId, created)
|
|
954
|
+
return created
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function clearCurrentMessage(state: ClaudeEventMapperState, actor: AgentEventActor): void {
|
|
958
|
+
if (actor.type === 'main') {
|
|
959
|
+
state.currentMessageId = undefined
|
|
960
|
+
state.contentBlockTypeByIndex.clear()
|
|
961
|
+
return
|
|
962
|
+
}
|
|
963
|
+
state.currentMessageIdByActorId.delete(actor.actorId)
|
|
964
|
+
state.contentBlockTypeByActorId.delete(actor.actorId)
|
|
965
|
+
}
|
|
966
|
+
|
|
575
967
|
function endAssistantMessage(drafts: AgentEventDraft[], state: ClaudeEventMapperState, message: BetaMessage, turnId: string, actor: AgentEventActor | undefined): void {
|
|
576
968
|
if (state.endedMessageIds.has(message.id))
|
|
577
969
|
return
|
|
@@ -605,7 +997,9 @@ function endReasoning(drafts: AgentEventDraft[], state: ClaudeEventMapperState,
|
|
|
605
997
|
|
|
606
998
|
function endOpenReasoning(drafts: AgentEventDraft[], state: ClaudeEventMapperState, turnId: string): void {
|
|
607
999
|
for (const reasoningId of state.startedReasoningIds) {
|
|
608
|
-
|
|
1000
|
+
const messageId = reasoningId.slice('reasoning-'.length)
|
|
1001
|
+
const actor = state.actorByMessageId.get(messageId)
|
|
1002
|
+
if (actor?.type === 'main' && !state.endedReasoningIds.has(reasoningId)) {
|
|
609
1003
|
state.endedReasoningIds.add(reasoningId)
|
|
610
1004
|
drafts.push({ type: 'reasoning.ended', turnId, actor: mainActor, payload: { reasoningId } })
|
|
611
1005
|
}
|
|
@@ -626,7 +1020,11 @@ function reasoningIdForMessage(messageId: string): string {
|
|
|
626
1020
|
return `reasoning-${messageId}`
|
|
627
1021
|
}
|
|
628
1022
|
|
|
629
|
-
function turnEndedPayload(message
|
|
1023
|
+
function turnEndedPayload({ message, interruptAccepted, usageLimit }: {
|
|
1024
|
+
message: ClaudeResultMessage
|
|
1025
|
+
interruptAccepted: boolean
|
|
1026
|
+
usageLimit: Extract<ClaudeUsageLimit, { status: 'exhausted' }> | undefined
|
|
1027
|
+
}): Extract<AgentEventDraft, { type: 'turn.ended' }>['payload'] {
|
|
630
1028
|
if (interruptAccepted && message.subtype === 'error_during_execution') {
|
|
631
1029
|
return removeUndefined({
|
|
632
1030
|
status: 'interrupted' as const,
|
|
@@ -647,45 +1045,227 @@ function turnEndedPayload(message: Extract<SDKMessage, { type: 'result' }>, inte
|
|
|
647
1045
|
status: 'failed' as const,
|
|
648
1046
|
reason: message.stop_reason ?? message.subtype,
|
|
649
1047
|
duration: message.duration_ms,
|
|
650
|
-
error: resultError(message),
|
|
1048
|
+
error: usageLimit ? usageLimitError({ error: resultError(message), usageLimit }) : resultError(message),
|
|
651
1049
|
})
|
|
652
1050
|
}
|
|
653
1051
|
|
|
654
|
-
|
|
655
|
-
|
|
1052
|
+
// Codex reports a plan usage limit as codexErrorInfo `usageLimitExceeded`; reuse that code so
|
|
1053
|
+
// consumers classify both providers alike. The reset time rides on `cause` because AgentError
|
|
1054
|
+
// has no structured slot for it.
|
|
1055
|
+
function usageLimitError({ error, usageLimit }: { error: AgentError, usageLimit: Extract<ClaudeUsageLimit, { status: 'exhausted' }> }): AgentError {
|
|
1056
|
+
const resetsAt = usageLimit.resetsAt === undefined ? undefined : isoFromEpochSeconds(usageLimit.resetsAt)
|
|
1057
|
+
return removeUndefined({
|
|
1058
|
+
message: error.message,
|
|
1059
|
+
code: 'usageLimitExceeded',
|
|
1060
|
+
cause: resetsAt ? removeUndefined({ message: `Claude usage limit resets at ${resetsAt}.`, code: usageLimit.rateLimitType }) : undefined,
|
|
1061
|
+
})
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function usageLimitFromRateLimitInfo(info: ClaudeRateLimitInfo): ClaudeUsageLimit {
|
|
1065
|
+
if (info.status !== 'rejected')
|
|
1066
|
+
return { status: 'available' }
|
|
1067
|
+
const resetsAt = info.resetsAt !== undefined && Number.isFinite(info.resetsAt) && info.resetsAt > 0 ? info.resetsAt : undefined
|
|
1068
|
+
return removeUndefined({ status: 'exhausted' as const, resetsAt, rateLimitType: info.rateLimitType })
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Mirrors the CLI's own expiry check (`resetsAt * 1000 <= Date.now()` on CLI 2.1.280).
|
|
1072
|
+
function usageLimitInForce(limit: ClaudeUsageLimit, nowMs: number): Extract<ClaudeUsageLimit, { status: 'exhausted' }> | undefined {
|
|
1073
|
+
if (limit.status !== 'exhausted')
|
|
1074
|
+
return undefined
|
|
1075
|
+
return limit.resetsAt === undefined || nowMs < limit.resetsAt * 1000 ? limit : undefined
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function usageLimitAfterTurn(limit: ClaudeUsageLimit): ClaudeUsageLimit {
|
|
1079
|
+
return limit.status === 'exhausted' && limit.resetsAt !== undefined ? limit : { status: 'available' }
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function isoFromEpochSeconds(seconds: number): string | undefined {
|
|
1083
|
+
const date = new Date(seconds * 1000)
|
|
1084
|
+
return Number.isFinite(seconds) && seconds > 0 && Number.isFinite(date.getTime()) ? date.toISOString() : undefined
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function observeMainAssistantMessage(message: Extract<SDKMessage, { type: 'assistant' }>, state: ClaudeEventMapperState): void {
|
|
1088
|
+
if (message.error === 'rate_limit')
|
|
1089
|
+
state.mainTurnRateLimited = true
|
|
1090
|
+
// Claude Code's own context report counts the last response's input side (verified against
|
|
1091
|
+
// getContextUsage on CLI 2.1.280); output is partial on streamed frames and previous thinking
|
|
1092
|
+
// is dropped from later requests. A zero input side is a synthetic frame, not an observation.
|
|
1093
|
+
const { usage } = message.message
|
|
1094
|
+
const usedTokens = usage.input_tokens + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0)
|
|
1095
|
+
if (usedTokens > 0 && Number.isSafeInteger(usedTokens))
|
|
1096
|
+
state.contextObservation = { status: 'observed', model: message.message.model, usedTokens }
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Codex reports context on every token-usage update; Claude's equivalent is each main response,
|
|
1100
|
+
// so report whenever the snapshot changes and again at turn end once the result names the window.
|
|
1101
|
+
function reportContextSnapshot({ drafts, state, turnId, result }: {
|
|
1102
|
+
drafts: AgentEventDraft[]
|
|
1103
|
+
state: ClaudeEventMapperState
|
|
1104
|
+
turnId: string
|
|
1105
|
+
result?: ClaudeResultMessage
|
|
1106
|
+
}): void {
|
|
1107
|
+
if (state.contextObservation.status !== 'observed' || state.currentCompactionId !== undefined)
|
|
1108
|
+
return
|
|
1109
|
+
if (result)
|
|
1110
|
+
rememberContextWindows(result, state)
|
|
1111
|
+
const snapshot = contextSnapshot({ observation: state.contextObservation, state })
|
|
1112
|
+
if (state.reportedContext !== undefined && contextSnapshotsEqual(snapshot, state.reportedContext))
|
|
1113
|
+
return
|
|
1114
|
+
state.reportedContext = snapshot
|
|
1115
|
+
drafts.push({ type: 'context.updated', turnId, actor: mainActor, payload: { snapshot } })
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function rememberContextWindows(result: ClaudeResultMessage, state: ClaudeEventMapperState): void {
|
|
1119
|
+
for (const [model, usage] of Object.entries(result.modelUsage)) {
|
|
1120
|
+
if (Number.isSafeInteger(usage.contextWindow) && usage.contextWindow > 0)
|
|
1121
|
+
state.contextWindowByModel.set(model, usage.contextWindow)
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function contextSnapshot({ observation, state }: {
|
|
1126
|
+
observation: Extract<ClaudeContextObservation, { status: 'observed' }>
|
|
1127
|
+
state: ClaudeEventMapperState
|
|
1128
|
+
}): AgentContextSnapshot {
|
|
1129
|
+
// modelUsage is keyed by the requested model string, which is the init model for an explicit id
|
|
1130
|
+
// and the resolved id (also the assistant model) for an alias.
|
|
1131
|
+
const capacityTokens = state.contextWindowByModel.get(observation.model)
|
|
1132
|
+
?? (state.sessionModel === undefined ? undefined : state.contextWindowByModel.get(state.sessionModel))
|
|
1133
|
+
if (capacityTokens === undefined)
|
|
1134
|
+
return { status: 'observed', usedTokens: observation.usedTokens, window: { status: 'unknown' } }
|
|
1135
|
+
// Claude Code reports context against the raw window without a reserved baseline.
|
|
1136
|
+
const remainingPercent = Math.round(Math.min(Math.max(100 * (capacityTokens - observation.usedTokens) / capacityTokens, 0), 100))
|
|
1137
|
+
return { status: 'observed', usedTokens: observation.usedTokens, window: { status: 'known', capacityTokens, remainingPercent } }
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function contextSnapshotsEqual(left: AgentContextSnapshot, right: AgentContextSnapshot): boolean {
|
|
1141
|
+
return JSON.stringify(left) === JSON.stringify(right)
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
export function compactionError(message: ClaudeStatusMessage): AgentError {
|
|
1145
|
+
return { message: message.compact_error || 'Claude compaction failed.' }
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
export function resultError(message: Extract<SDKMessage, { type: 'result' }>): AgentError {
|
|
1149
|
+
// SDK success/is_error carries API failure text in result rather than the errors array.
|
|
1150
|
+
const detail = message.subtype === 'success'
|
|
1151
|
+
? message.result
|
|
1152
|
+
: message.errors.length > 0 ? message.errors.join('\n') : undefined
|
|
656
1153
|
return {
|
|
657
|
-
message:
|
|
1154
|
+
message: detail || `Claude result ended with ${message.subtype}.`,
|
|
658
1155
|
code: message.subtype,
|
|
659
1156
|
}
|
|
660
1157
|
}
|
|
661
1158
|
|
|
662
|
-
function shouldEndAssistantSnapshot(message: BetaMessage, state: ClaudeEventMapperState, actor:
|
|
663
|
-
return actor
|
|
1159
|
+
function shouldEndAssistantSnapshot(message: BetaMessage, state: ClaudeEventMapperState, actor: AgentEventActor): boolean {
|
|
1160
|
+
return actor.type === 'subagent'
|
|
664
1161
|
&& !state.streamedTextByMessageId.has(message.id)
|
|
665
1162
|
&& !state.streamedReasoningMessageIds.has(message.id)
|
|
666
1163
|
}
|
|
667
1164
|
|
|
668
|
-
function
|
|
669
|
-
|
|
1165
|
+
function actorContextFromParentToolUseId(parentToolUseId: string | null, drafts: AgentEventDraft[], state: ClaudeEventMapperState): ClaudeActorContext | undefined {
|
|
1166
|
+
if (parentToolUseId === null)
|
|
1167
|
+
return { actor: mainActor, turnId: ensureTurnStarted(drafts, state) }
|
|
1168
|
+
|
|
1169
|
+
const actor = state.subagentByToolCallId.get(parentToolUseId)
|
|
1170
|
+
const turnId = actor ? state.turnIdBySubagentId.get(actor.subagentId) : undefined
|
|
1171
|
+
return actor && turnId ? { actor, turnId } : undefined
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function subagentActorForTaskMessage(input: { subagentId: string, parentToolCallId?: string, name?: string }, state: ClaudeEventMapperState): ClaudeTaskActorResolution {
|
|
1175
|
+
const existingByTask = state.subagentByTaskId.get(input.subagentId)
|
|
1176
|
+
if (existingByTask) {
|
|
1177
|
+
if (input.parentToolCallId && existingByTask.parentToolCallId !== input.parentToolCallId && state.subagentByToolCallId.get(input.parentToolCallId)?.subagentId !== input.subagentId)
|
|
1178
|
+
return { status: 'conflict' }
|
|
1179
|
+
const turnId = state.turnIdBySubagentId.get(input.subagentId)
|
|
1180
|
+
if (!turnId)
|
|
1181
|
+
return { status: 'conflict' }
|
|
1182
|
+
const actor = actorWithObservedName(existingByTask, input.name, state)
|
|
1183
|
+
return { status: 'resolved', context: { actor, turnId } }
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
if (!input.parentToolCallId)
|
|
1187
|
+
return { status: 'pending' }
|
|
1188
|
+
const existingByTool = state.subagentByToolCallId.get(input.parentToolCallId)
|
|
1189
|
+
if (existingByTool)
|
|
1190
|
+
return { status: existingByTool.subagentId === input.subagentId ? 'pending' : 'conflict' }
|
|
1191
|
+
|
|
1192
|
+
const owner = state.toolOwnerById.get(input.parentToolCallId)
|
|
1193
|
+
if (!owner)
|
|
1194
|
+
return { status: 'pending' }
|
|
1195
|
+
if (input.subagentId === mainActor.actorId || owner.actor.actorId === input.subagentId || wouldCreateActorCycle(input.subagentId, owner.actor, state))
|
|
1196
|
+
return { status: 'conflict' }
|
|
1197
|
+
|
|
1198
|
+
const actor = subagentActorFromTask({
|
|
1199
|
+
subagentId: input.subagentId,
|
|
1200
|
+
parentActorId: owner.actor.actorId,
|
|
1201
|
+
parentToolCallId: input.parentToolCallId,
|
|
1202
|
+
name: input.name,
|
|
1203
|
+
})
|
|
1204
|
+
state.subagentByTaskId.set(input.subagentId, actor)
|
|
1205
|
+
state.subagentByToolCallId.set(input.parentToolCallId, actor)
|
|
1206
|
+
state.turnIdBySubagentId.set(input.subagentId, owner.turnId)
|
|
1207
|
+
state.runToolCallIdBySubagentId.set(input.subagentId, input.parentToolCallId)
|
|
1208
|
+
return { status: 'resolved', context: { actor, turnId: owner.turnId } }
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function actorWithObservedName(actor: ClaudeSubagentActor, name: string | undefined, state: ClaudeEventMapperState): ClaudeSubagentActor {
|
|
1212
|
+
if (!name || actor.name === name)
|
|
1213
|
+
return actor
|
|
1214
|
+
if (actor.name)
|
|
1215
|
+
return actor
|
|
1216
|
+
|
|
1217
|
+
const named = { ...actor, name }
|
|
1218
|
+
state.subagentByTaskId.set(actor.subagentId, named)
|
|
1219
|
+
if (actor.parentToolCallId)
|
|
1220
|
+
state.subagentByToolCallId.set(actor.parentToolCallId, named)
|
|
1221
|
+
return named
|
|
670
1222
|
}
|
|
671
1223
|
|
|
672
|
-
function
|
|
673
|
-
|
|
674
|
-
|
|
1224
|
+
function wouldCreateActorCycle(subagentId: string, parent: AgentEventActor, state: ClaudeEventMapperState): boolean {
|
|
1225
|
+
let actor = parent
|
|
1226
|
+
const visited = new Set<string>()
|
|
1227
|
+
while (actor.type === 'subagent') {
|
|
1228
|
+
if (actor.actorId === subagentId || visited.has(actor.actorId))
|
|
1229
|
+
return true
|
|
1230
|
+
visited.add(actor.actorId)
|
|
1231
|
+
const next = state.subagentByTaskId.get(actor.parentActorId)
|
|
1232
|
+
if (!next)
|
|
1233
|
+
return false
|
|
1234
|
+
actor = next
|
|
1235
|
+
}
|
|
1236
|
+
return false
|
|
675
1237
|
}
|
|
676
1238
|
|
|
677
|
-
function subagentActorFromTask(input: { subagentId: string, parentToolCallId
|
|
1239
|
+
function subagentActorFromTask(input: { subagentId: string, parentActorId: string, parentToolCallId: string, name?: string }): ClaudeSubagentActor {
|
|
678
1240
|
return removeUndefined({
|
|
679
1241
|
type: 'subagent' as const,
|
|
680
1242
|
actorId: input.subagentId,
|
|
681
1243
|
subagentId: input.subagentId,
|
|
682
|
-
parentActorId:
|
|
683
|
-
origin:
|
|
1244
|
+
parentActorId: input.parentActorId,
|
|
1245
|
+
origin: { type: 'tool_call' as const, toolCallId: input.parentToolCallId },
|
|
684
1246
|
parentToolCallId: input.parentToolCallId,
|
|
685
1247
|
name: input.name,
|
|
686
1248
|
})
|
|
687
1249
|
}
|
|
688
1250
|
|
|
1251
|
+
function flushPendingMessages(state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
1252
|
+
const drafts: AgentEventDraft[] = []
|
|
1253
|
+
let resolved = true
|
|
1254
|
+
while (resolved) {
|
|
1255
|
+
resolved = false
|
|
1256
|
+
for (let index = 0; index < state.pendingMessages.length; index += 1) {
|
|
1257
|
+
const pendingDrafts = mapClaudeMessageWithKnownActor(state.pendingMessages[index]!, state)
|
|
1258
|
+
if (pendingDrafts === undefined)
|
|
1259
|
+
continue
|
|
1260
|
+
state.pendingMessages.splice(index, 1)
|
|
1261
|
+
drafts.push(...pendingDrafts)
|
|
1262
|
+
resolved = true
|
|
1263
|
+
break
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return drafts
|
|
1267
|
+
}
|
|
1268
|
+
|
|
689
1269
|
function subagentPayloadRef(actor: ClaudeSubagentActor): Pick<ClaudeSubagentActor, 'actorId' | 'subagentId' | 'parentActorId' | 'origin' | 'parentToolCallId' | 'name'> {
|
|
690
1270
|
return removeUndefined({
|
|
691
1271
|
actorId: actor.actorId,
|
|
@@ -737,12 +1317,15 @@ function normalizeToolName(name: string): string {
|
|
|
737
1317
|
function usageFromObject(value: BetaUsage | undefined): AgentUsage | undefined {
|
|
738
1318
|
if (!value)
|
|
739
1319
|
return undefined
|
|
1320
|
+
// Anthropic input_tokens excludes cache reads and writes; the provider-neutral inputTokens, like
|
|
1321
|
+
// Codex's, counts every input token, with the cached subsets reported alongside it.
|
|
1322
|
+
const inputTokens = value.input_tokens + (value.cache_read_input_tokens ?? 0) + (value.cache_creation_input_tokens ?? 0)
|
|
740
1323
|
return removeUndefined({
|
|
741
|
-
inputTokens
|
|
1324
|
+
inputTokens,
|
|
742
1325
|
outputTokens: value.output_tokens,
|
|
743
1326
|
cacheReadTokens: value.cache_read_input_tokens ?? undefined,
|
|
744
1327
|
cacheWriteTokens: value.cache_creation_input_tokens ?? undefined,
|
|
745
|
-
totalTokens:
|
|
1328
|
+
totalTokens: inputTokens + value.output_tokens,
|
|
746
1329
|
})
|
|
747
1330
|
}
|
|
748
1331
|
|
|
@@ -756,13 +1339,159 @@ function thinkingFromAssistantContent(content: BetaMessage['content']): string |
|
|
|
756
1339
|
return text || undefined
|
|
757
1340
|
}
|
|
758
1341
|
|
|
759
|
-
function
|
|
760
|
-
if (
|
|
1342
|
+
function correlatedHostTrailerToolCallId(input: { content: UserContentBlock[], message: ClaudeUserMessage, state: ClaudeEventMapperState }): string | undefined {
|
|
1343
|
+
if (input.message.tool_use_result === undefined)
|
|
761
1344
|
return undefined
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1345
|
+
const toolCallId = soleToolResultToolCallId(input.content)
|
|
1346
|
+
return toolCallId && input.state.hostMcpToolCallIds.has(toolCallId) ? toolCallId : undefined
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// The SDK attaches one message-level tool_use_result, so it is attributable only to a sole tool_result.
|
|
1350
|
+
function soleToolResultToolCallId(content: UserContentBlock[]): string | undefined {
|
|
1351
|
+
const toolResults = content.filter((block): block is UserToolResultBlock => block.type === 'tool_result')
|
|
1352
|
+
return toolResults.length === 1 ? toolResults[0]!.tool_use_id : undefined
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function mappedModelToolResultContent({ content, isError }: { content: UserToolResultBlock['content'], isError: boolean }): { content: AgentToolContent[], text?: string } {
|
|
1356
|
+
if (typeof content === 'string') {
|
|
1357
|
+
const text = isError ? withoutToolUseErrorWrapper(content) : content
|
|
1358
|
+
return { content: [{ type: 'text', text }], text }
|
|
1359
|
+
}
|
|
1360
|
+
if (content === undefined)
|
|
1361
|
+
return { content: [] }
|
|
1362
|
+
if (!Array.isArray(content)) {
|
|
1363
|
+
const parsed = jsonValueSchema.safeParse(content)
|
|
1364
|
+
return parsed.success ? { content: [jsonToolContent(parsed.data)] } : { content: [] }
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const mapped: AgentToolContent[] = []
|
|
1368
|
+
let text = ''
|
|
1369
|
+
let hasText = false
|
|
1370
|
+
for (const block of content) {
|
|
1371
|
+
const parsed = jsonValueSchema.safeParse(block)
|
|
1372
|
+
if (!parsed.success)
|
|
1373
|
+
continue
|
|
1374
|
+
const value = isError ? withoutToolUseErrorBlockWrapper(parsed.data) : parsed.data
|
|
1375
|
+
if (isJsonObject(value) && value.type === 'text' && typeof value.text === 'string') {
|
|
1376
|
+
hasText = true
|
|
1377
|
+
text += value.text
|
|
1378
|
+
}
|
|
1379
|
+
mapped.push(modelToolContent(value))
|
|
1380
|
+
}
|
|
1381
|
+
return removeUndefined({ content: mapped, text: hasText ? text : undefined })
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Claude Code wraps its own tool failures as `<tool_use_error>…</tool_use_error>` for the model and
|
|
1385
|
+
// strips the same wrapper before display (CLI 2.1.280), so the wrapper is transport, not output.
|
|
1386
|
+
const TOOL_USE_ERROR_WRAPPER = /^<tool_use_error>([\s\S]*)<\/tool_use_error>$/
|
|
1387
|
+
|
|
1388
|
+
function withoutToolUseErrorWrapper(text: string): string {
|
|
1389
|
+
return TOOL_USE_ERROR_WRAPPER.exec(text)?.[1] ?? text
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function withoutToolUseErrorBlockWrapper(value: JsonValue): JsonValue {
|
|
1393
|
+
if (!isJsonObject(value) || value.type !== 'text' || typeof value.text !== 'string')
|
|
1394
|
+
return value
|
|
1395
|
+
return { ...value, text: withoutToolUseErrorWrapper(value.text) }
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function modelToolContent(value: JsonValue): AgentToolContent {
|
|
1399
|
+
if (!isJsonObject(value))
|
|
1400
|
+
return jsonToolContent(value)
|
|
1401
|
+
if (value.type === 'image' && isJsonObject(value.source)) {
|
|
1402
|
+
const source = value.source
|
|
1403
|
+
const extras = Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'type' && key !== 'source'))
|
|
1404
|
+
const sourceExtras = Object.fromEntries(Object.entries(source).filter(([key]) => !['type', 'media_type', 'data', 'url'].includes(key)))
|
|
1405
|
+
if (Object.keys(sourceExtras).length > 0)
|
|
1406
|
+
extras.source = sourceExtras
|
|
1407
|
+
const metadata = Object.keys(extras).length > 0 ? { extras } : {}
|
|
1408
|
+
if (source.type === 'base64' && typeof source.media_type === 'string' && source.media_type.length > 0
|
|
1409
|
+
&& typeof source.data === 'string' && source.data.length > 0) {
|
|
1410
|
+
return { type: 'image', mimeType: source.media_type, data: source.data, ...metadata }
|
|
1411
|
+
}
|
|
1412
|
+
if (source.type === 'url' && typeof source.url === 'string' && source.url.length > 0)
|
|
1413
|
+
return { type: 'image', uri: source.url, ...metadata }
|
|
1414
|
+
}
|
|
1415
|
+
return mcpToolContent(value)
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function mappedHostMcpContent(input: { trailer: unknown, modelBlocks: AgentToolContent[] }): AgentToolContent[] {
|
|
1419
|
+
const content: AgentToolContent[] = [...input.modelBlocks]
|
|
1420
|
+
const hostContent = ownJsonSlot(input.trailer, 'content')
|
|
1421
|
+
if (hostContent !== undefined) {
|
|
1422
|
+
const hostBlocks = Array.isArray(hostContent) ? hostContent.map(mcpToolContent) : [mcpToolContent(hostContent)]
|
|
1423
|
+
mergeToolContentOccurrences(content, hostBlocks)
|
|
1424
|
+
}
|
|
1425
|
+
const resourceLinks = ownJsonSlot(input.trailer, 'resourceLinks')
|
|
1426
|
+
if (resourceLinks !== undefined) {
|
|
1427
|
+
if (Array.isArray(resourceLinks))
|
|
1428
|
+
mergeToolContentOccurrences(content, resourceLinks.map(resourceLinkContent))
|
|
1429
|
+
else
|
|
1430
|
+
content.push(jsonToolContent(resourceLinks))
|
|
1431
|
+
}
|
|
1432
|
+
return content
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function mergeToolContentOccurrences(target: AgentToolContent[], incoming: AgentToolContent[]): void {
|
|
1436
|
+
let cursor = 0
|
|
1437
|
+
for (let incomingIndex = 0; incomingIndex < incoming.length; incomingIndex += 1) {
|
|
1438
|
+
const block = incoming[incomingIndex]!
|
|
1439
|
+
const index = target.findIndex((existing, position) => position >= cursor
|
|
1440
|
+
&& jsonValuesEqual(contentIdentity(existing), contentIdentity(block)))
|
|
1441
|
+
if (index === -1) {
|
|
1442
|
+
const nextMatch = target.findIndex((existing, position) => position >= cursor
|
|
1443
|
+
&& incoming.slice(incomingIndex + 1).some(next => jsonValuesEqual(contentIdentity(existing), contentIdentity(next))))
|
|
1444
|
+
const insertion = nextMatch === -1 ? target.length : nextMatch
|
|
1445
|
+
target.splice(insertion, 0, block)
|
|
1446
|
+
cursor = insertion + 1
|
|
1447
|
+
}
|
|
1448
|
+
else {
|
|
1449
|
+
cursor = index + 1
|
|
1450
|
+
const existing = target[index]!
|
|
1451
|
+
if (block.type !== 'json' && existing.type !== 'json') {
|
|
1452
|
+
target[index] = removeUndefined({
|
|
1453
|
+
...block,
|
|
1454
|
+
annotations: block.annotations ?? existing.annotations,
|
|
1455
|
+
extras: existing.extras || block.extras ? { ...existing.extras, ...block.extras } : undefined,
|
|
1456
|
+
})
|
|
1457
|
+
}
|
|
1458
|
+
else {
|
|
1459
|
+
target[index] = block
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function contentIdentity(content: AgentToolContent): JsonValue {
|
|
1466
|
+
return Object.fromEntries(Object.entries(content).filter(([key]) => key !== 'extras' && key !== 'annotations'))
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function resourceLinkContent(value: JsonValue): AgentToolContent {
|
|
1470
|
+
if (isJsonObject(value) && typeof value.name === 'string' && typeof value.uri === 'string')
|
|
1471
|
+
return mcpToolContent({ ...value, type: 'resource_link' })
|
|
1472
|
+
return jsonToolContent(value)
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function jsonValuesEqual(left: JsonValue | undefined, right: JsonValue | undefined): boolean {
|
|
1476
|
+
if (left === right)
|
|
1477
|
+
return true
|
|
1478
|
+
if (left === undefined || right === undefined || typeof left !== typeof right || left === null || right === null)
|
|
1479
|
+
return false
|
|
1480
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
1481
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
|
|
1482
|
+
return false
|
|
1483
|
+
return left.every((entry, index) => jsonValuesEqual(entry, right[index]))
|
|
1484
|
+
}
|
|
1485
|
+
if (!isJsonObject(left) || !isJsonObject(right))
|
|
1486
|
+
return false
|
|
1487
|
+
const leftKeys = Object.keys(left)
|
|
1488
|
+
const rightKeys = Object.keys(right)
|
|
1489
|
+
return leftKeys.length === rightKeys.length
|
|
1490
|
+
&& leftKeys.every(key => Object.hasOwn(right, key) && jsonValuesEqual(left[key], right[key]))
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function isHostMcpToolName(name: string): boolean {
|
|
1494
|
+
return name.startsWith('mcp__') && !name.startsWith(CLAUDE_RUNTIME_TOOL_PREFIX)
|
|
766
1495
|
}
|
|
767
1496
|
|
|
768
1497
|
function jsonInput(value: unknown): JsonValue {
|