@meistrari/agent-core 0.2.0-experimental.1 → 0.2.0-experimental.3
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 +868 -135
- package/src/agents/claude/claude-file-change.ts +128 -0
- package/src/agents/claude/claude-provider.ts +356 -30
- package/src/agents/claude/claude-run.ts +898 -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 +384 -67
- 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,68 @@ 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
|
+
// Main-response UUIDs prove consumption even if the started receipt was omitted. This
|
|
404
|
+
// also applies to supervisor inputs, which intentionally have no human-prompt correlation.
|
|
405
|
+
if (state.queuedSteerIds.delete(inputId))
|
|
406
|
+
state.heldTurnEnd = undefined
|
|
407
|
+
const commandId = state.promptCommandByInputId.get(inputId)
|
|
408
|
+
if (!commandId)
|
|
409
|
+
continue
|
|
410
|
+
drafts.push({ type: 'prompt.turn-correlated', payload: { commandId, turnId } })
|
|
411
|
+
state.promptCommandByInputId.delete(inputId)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
239
415
|
function mapStatusMessage(message: ClaudeStatusMessage, state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
240
416
|
const drafts: AgentEventDraft[] = []
|
|
241
417
|
if (message.status === 'compacting') {
|
|
242
418
|
const turnId = ensureTurnStarted(drafts, state)
|
|
419
|
+
state.contextObservation = { status: 'none' }
|
|
420
|
+
state.reportedContext = undefined
|
|
243
421
|
if (state.currentCompactionId)
|
|
244
422
|
return drafts
|
|
245
423
|
state.currentCompactionId = message.uuid
|
|
@@ -258,7 +436,7 @@ function mapStatusMessage(message: ClaudeStatusMessage, state: ClaudeEventMapper
|
|
|
258
436
|
compactionId,
|
|
259
437
|
trigger: 'unknown',
|
|
260
438
|
status: 'failed',
|
|
261
|
-
error:
|
|
439
|
+
error: compactionError(message),
|
|
262
440
|
},
|
|
263
441
|
})
|
|
264
442
|
state.currentCompactionId = undefined
|
|
@@ -267,13 +445,44 @@ function mapStatusMessage(message: ClaudeStatusMessage, state: ClaudeEventMapper
|
|
|
267
445
|
return drafts
|
|
268
446
|
}
|
|
269
447
|
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
448
|
+
function isMcpTaskMessage(message: { task_id: string, tool_use_id?: string }, state: ClaudeEventMapperState): boolean {
|
|
449
|
+
if (state.mcpToolCallIdByTaskId.has(message.task_id))
|
|
450
|
+
return true
|
|
451
|
+
const name = message.tool_use_id ? state.toolNameById.get(message.tool_use_id) : undefined
|
|
452
|
+
return name !== undefined && isHostMcpToolName(name)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Only an Agent-tool child (`local_agent`) is a subagent; the CLI also reports backgrounded Bash,
|
|
457
|
+
* workflows, monitors, and other tasks through the same lifecycle. The pinned CLI always sets the
|
|
458
|
+
* task type; an untyped task keeps the subagent mapping its ancestry evidence supports. The SDK
|
|
459
|
+
* marks housekeeping tasks `skip_transcript` for hosts to hide.
|
|
460
|
+
*/
|
|
461
|
+
function isVisibleSubagentTask(message: ClaudeTaskStartedMessage): boolean {
|
|
462
|
+
return (message.task_type === undefined || message.task_type === 'local_agent') && message.skip_transcript !== true
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* SendMessage resumes a finished subagent as a new run: the CLI repeats its task_id with the
|
|
467
|
+
* SendMessage call as tool_use_id. The run keeps the subagent's identity and turn, so it reopens the
|
|
468
|
+
* same subagent. Returns null when the message is not a resume of a known subagent.
|
|
469
|
+
*/
|
|
470
|
+
function mapResumedSubagentRun(message: ClaudeTaskStartedMessage, state: ClaudeEventMapperState): AgentEventDraft[] | undefined | null {
|
|
471
|
+
const actor = state.subagentByTaskId.get(message.task_id)
|
|
472
|
+
const toolCallId = message.tool_use_id
|
|
473
|
+
if (!actor || !toolCallId || toolCallId === actor.parentToolCallId || state.subagentByToolCallId.has(toolCallId))
|
|
474
|
+
return null
|
|
475
|
+
const owner = state.toolOwnerById.get(toolCallId)
|
|
476
|
+
if (!owner)
|
|
477
|
+
return undefined
|
|
478
|
+
const turnId = state.turnIdBySubagentId.get(message.task_id)
|
|
479
|
+
if (owner.actor.actorId !== actor.parentActorId || !turnId)
|
|
480
|
+
return []
|
|
481
|
+
|
|
482
|
+
state.subagentByToolCallId.set(toolCallId, actor)
|
|
483
|
+
state.runToolCallIdBySubagentId.set(message.task_id, toolCallId)
|
|
484
|
+
state.endedSubagentIds.delete(message.task_id)
|
|
485
|
+
return [{
|
|
277
486
|
type: 'subagent.started',
|
|
278
487
|
turnId,
|
|
279
488
|
actor,
|
|
@@ -281,51 +490,134 @@ function mapTaskStartedMessage(message: ClaudeTaskStartedMessage, state: ClaudeE
|
|
|
281
490
|
...subagentPayloadRef(actor),
|
|
282
491
|
description: message.description,
|
|
283
492
|
}),
|
|
284
|
-
}
|
|
285
|
-
return drafts
|
|
493
|
+
}]
|
|
286
494
|
}
|
|
287
495
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
496
|
+
/** A resumed subagent must not take progress or a terminal from a run that SendMessage superseded. */
|
|
497
|
+
function isEarlierSubagentRun(message: { task_id: string, tool_use_id?: string }, state: ClaudeEventMapperState): boolean {
|
|
498
|
+
const runToolCallId = state.runToolCallIdBySubagentId.get(message.task_id)
|
|
499
|
+
return message.tool_use_id !== undefined && runToolCallId !== undefined && message.tool_use_id !== runToolCallId
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function mapTaskStartedMessage(message: ClaudeTaskStartedMessage, state: ClaudeEventMapperState): AgentEventDraft[] | undefined {
|
|
503
|
+
if (message.task_type === 'mcp_task' || isMcpTaskMessage(message, state)) {
|
|
504
|
+
const previous = state.mcpToolCallIdByTaskId.get(message.task_id)
|
|
505
|
+
if (previous && message.tool_use_id && previous !== message.tool_use_id)
|
|
506
|
+
state.conflictingMcpTaskIds.add(message.task_id)
|
|
507
|
+
else
|
|
508
|
+
state.mcpToolCallIdByTaskId.set(message.task_id, previous ?? message.tool_use_id)
|
|
509
|
+
return []
|
|
510
|
+
}
|
|
511
|
+
if (!isVisibleSubagentTask(message)) {
|
|
512
|
+
state.nonSubagentTaskIds.add(message.task_id)
|
|
513
|
+
if (message.tool_use_id)
|
|
514
|
+
state.nonSubagentToolCallIds.add(message.tool_use_id)
|
|
515
|
+
return []
|
|
516
|
+
}
|
|
517
|
+
const resumed = mapResumedSubagentRun(message, state)
|
|
518
|
+
if (resumed !== null)
|
|
519
|
+
return resumed
|
|
520
|
+
const resolution = subagentActorForTaskMessage({ subagentId: message.task_id, parentToolCallId: message.tool_use_id, name: message.subagent_type }, state)
|
|
521
|
+
if (resolution.status === 'pending')
|
|
522
|
+
return undefined
|
|
523
|
+
if (resolution.status === 'conflict' || state.startedSubagentIds.has(message.task_id))
|
|
524
|
+
return []
|
|
525
|
+
|
|
526
|
+
state.startedSubagentIds.add(message.task_id)
|
|
527
|
+
const { actor, turnId } = resolution.context
|
|
528
|
+
return [{
|
|
529
|
+
type: 'subagent.started',
|
|
294
530
|
turnId,
|
|
295
531
|
actor,
|
|
296
532
|
payload: removeUndefined({
|
|
297
533
|
...subagentPayloadRef(actor),
|
|
298
|
-
|
|
299
|
-
lastToolName: optionalNonEmpty(message.last_tool_name),
|
|
300
|
-
usage: usageFromTaskUsage(message.usage),
|
|
534
|
+
description: message.description,
|
|
301
535
|
}),
|
|
302
|
-
}
|
|
303
|
-
return drafts
|
|
536
|
+
}]
|
|
304
537
|
}
|
|
305
538
|
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
539
|
+
function mapTaskProgressMessage(message: ClaudeTaskProgressMessage, state: ClaudeEventMapperState): AgentEventDraft[] | undefined {
|
|
540
|
+
if (isMcpTaskMessage(message, state)) {
|
|
541
|
+
if (!state.mcpToolCallIdByTaskId.has(message.task_id))
|
|
542
|
+
state.mcpToolCallIdByTaskId.set(message.task_id, message.tool_use_id)
|
|
543
|
+
return []
|
|
544
|
+
}
|
|
545
|
+
if (state.nonSubagentTaskIds.has(message.task_id))
|
|
546
|
+
return []
|
|
547
|
+
const resolution = subagentActorForTaskMessage({ subagentId: message.task_id, parentToolCallId: message.tool_use_id, name: message.subagent_type }, state)
|
|
548
|
+
if (resolution.status === 'pending')
|
|
549
|
+
return undefined
|
|
550
|
+
if (resolution.status === 'conflict' || isEarlierSubagentRun(message, state))
|
|
551
|
+
return []
|
|
552
|
+
|
|
553
|
+
const { actor, turnId } = resolution.context
|
|
554
|
+
return [{
|
|
555
|
+
type: 'subagent.progress',
|
|
312
556
|
turnId,
|
|
313
557
|
actor,
|
|
314
558
|
payload: removeUndefined({
|
|
315
559
|
...subagentPayloadRef(actor),
|
|
316
|
-
status: taskNotificationStatus(message.status),
|
|
317
560
|
summary: optionalNonEmpty(message.summary),
|
|
318
|
-
|
|
319
|
-
|
|
561
|
+
lastToolName: optionalNonEmpty(message.last_tool_name),
|
|
562
|
+
usage: usageFromTaskUsage(message.usage),
|
|
320
563
|
}),
|
|
321
|
-
}
|
|
322
|
-
|
|
564
|
+
}]
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function mapTaskNotificationMessage(message: ClaudeTaskNotificationMessage, state: ClaudeEventMapperState): AgentEventDraft[] | undefined {
|
|
568
|
+
if (isMcpTaskMessage(message, state) || message.resource_links !== undefined) {
|
|
569
|
+
const rememberedToolCallId = state.mcpToolCallIdByTaskId.get(message.task_id)
|
|
570
|
+
if (rememberedToolCallId && message.tool_use_id && rememberedToolCallId !== message.tool_use_id)
|
|
571
|
+
state.conflictingMcpTaskIds.add(message.task_id)
|
|
572
|
+
if (state.endedMcpTaskIds.has(message.task_id) || state.conflictingMcpTaskIds.has(message.task_id))
|
|
573
|
+
return []
|
|
574
|
+
const toolCallId = message.tool_use_id ?? rememberedToolCallId
|
|
575
|
+
const owner = toolCallId ? state.toolOwnerById.get(toolCallId) : undefined
|
|
576
|
+
if (!toolCallId || !owner)
|
|
577
|
+
return undefined
|
|
578
|
+
state.endedMcpTaskIds.add(message.task_id)
|
|
579
|
+
const content: AgentToolContent[] = [{ type: 'text', text: message.summary }]
|
|
580
|
+
content.push(...(message.resource_links ?? []).map(link => resourceLinkContent(jsonValueSchema.parse(link))))
|
|
581
|
+
if (message.output_file !== '')
|
|
582
|
+
content.push(jsonToolContent({ outputFile: message.output_file }))
|
|
583
|
+
return [{
|
|
584
|
+
type: 'tool.output.updated',
|
|
585
|
+
...owner,
|
|
586
|
+
payload: { toolCallId, output: { success: message.status === 'completed', content } },
|
|
587
|
+
}]
|
|
588
|
+
}
|
|
589
|
+
if (state.nonSubagentTaskIds.has(message.task_id))
|
|
590
|
+
return []
|
|
591
|
+
const resolution = subagentActorForTaskMessage({ subagentId: message.task_id, parentToolCallId: message.tool_use_id }, state)
|
|
592
|
+
if (resolution.status === 'pending')
|
|
593
|
+
return undefined
|
|
594
|
+
if (resolution.status === 'conflict' || isEarlierSubagentRun(message, state) || state.endedSubagentIds.has(message.task_id))
|
|
595
|
+
return []
|
|
596
|
+
|
|
597
|
+
state.endedSubagentIds.add(message.task_id)
|
|
598
|
+
const { actor, turnId } = resolution.context
|
|
599
|
+
return [
|
|
600
|
+
{
|
|
601
|
+
type: 'subagent.ended',
|
|
602
|
+
turnId,
|
|
603
|
+
actor,
|
|
604
|
+
payload: removeUndefined({
|
|
605
|
+
...subagentPayloadRef(actor),
|
|
606
|
+
status: taskNotificationStatus(message.status),
|
|
607
|
+
summary: optionalNonEmpty(message.summary),
|
|
608
|
+
usage: message.usage ? usageFromTaskUsage(message.usage) : undefined,
|
|
609
|
+
error: message.status === 'failed' ? { message: optionalNonEmpty(message.summary) ?? `Subagent ${message.task_id} failed.` } : undefined,
|
|
610
|
+
}),
|
|
611
|
+
},
|
|
612
|
+
]
|
|
323
613
|
}
|
|
324
614
|
|
|
325
615
|
function mapCompactBoundaryMessage(message: ClaudeCompactBoundaryMessage, state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
326
616
|
const drafts: AgentEventDraft[] = []
|
|
327
617
|
const turnId = ensureTurnStarted(drafts, state)
|
|
328
618
|
const compactionId = state.currentCompactionId ?? message.uuid
|
|
619
|
+
state.contextObservation = { status: 'none' }
|
|
620
|
+
state.reportedContext = undefined
|
|
329
621
|
drafts.push({
|
|
330
622
|
type: 'context.compaction.completed',
|
|
331
623
|
turnId,
|
|
@@ -343,7 +635,7 @@ function mapCompactBoundaryMessage(message: ClaudeCompactBoundaryMessage, state:
|
|
|
343
635
|
return drafts
|
|
344
636
|
}
|
|
345
637
|
|
|
346
|
-
function mapAssistantContent(message: BetaMessage, state: ClaudeEventMapperState, turnId: string, actor: AgentEventActor
|
|
638
|
+
function mapAssistantContent(message: BetaMessage, state: ClaudeEventMapperState, turnId: string, actor: AgentEventActor, isMain: boolean): AgentEventDraft[] {
|
|
347
639
|
const drafts: AgentEventDraft[] = []
|
|
348
640
|
const streamedText = state.streamedTextByMessageId.has(message.id)
|
|
349
641
|
const streamedReasoning = state.streamedReasoningMessageIds.has(message.id)
|
|
@@ -360,6 +652,12 @@ function mapAssistantContent(message: BetaMessage, state: ClaudeEventMapperState
|
|
|
360
652
|
if (block.type === 'tool_use') {
|
|
361
653
|
const toolName = normalizeToolName(block.name)
|
|
362
654
|
state.toolNameById.set(block.id, toolName)
|
|
655
|
+
if (isHostMcpToolName(block.name))
|
|
656
|
+
state.hostMcpToolCallIds.add(block.id)
|
|
657
|
+
// SDK task_id identifies the child, while task_started.tool_use_id identifies this
|
|
658
|
+
// initiating call; the actor that emitted the tool_use is the only ancestry evidence.
|
|
659
|
+
if (!state.toolOwnerById.has(block.id))
|
|
660
|
+
state.toolOwnerById.set(block.id, { actor, turnId })
|
|
363
661
|
if (isMain) {
|
|
364
662
|
const pendingWorkCall = pendingClaudeWorkCall(block.name, block.input)
|
|
365
663
|
if (pendingWorkCall)
|
|
@@ -371,27 +669,53 @@ function mapAssistantContent(message: BetaMessage, state: ClaudeEventMapperState
|
|
|
371
669
|
return drafts
|
|
372
670
|
}
|
|
373
671
|
|
|
374
|
-
function mapUserContentBlock(
|
|
672
|
+
function mapUserContentBlock(input: {
|
|
673
|
+
block: UserContentBlock
|
|
674
|
+
message: ClaudeUserMessage
|
|
675
|
+
state: ClaudeEventMapperState
|
|
676
|
+
turnId: string
|
|
677
|
+
actor: AgentEventActor
|
|
678
|
+
isMain: boolean
|
|
679
|
+
hostTrailer?: unknown
|
|
680
|
+
ownsStructuredResult: boolean
|
|
681
|
+
}): AgentEventDraft[] {
|
|
682
|
+
const { block, message, state, turnId, actor, isMain, hostTrailer, ownsStructuredResult } = input
|
|
375
683
|
if (block.type !== 'tool_result')
|
|
376
684
|
return []
|
|
377
685
|
const toolCallId = block.tool_use_id
|
|
378
686
|
const pendingWorkCall = state.pendingWorkCallById.get(toolCallId)
|
|
379
687
|
if (pendingWorkCall)
|
|
380
688
|
state.pendingWorkCallById.delete(toolCallId)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
689
|
+
state.hostMcpToolCallIds.delete(toolCallId)
|
|
690
|
+
|
|
691
|
+
const fileChangeText = ownsStructuredResult && block.is_error !== true
|
|
692
|
+
? claudeFileChangeText({ toolName: state.toolNameById.get(toolCallId), toolUseResult: message.tool_use_result, cwd: state.sessionCwd })
|
|
693
|
+
: undefined
|
|
694
|
+
const mapped = fileChangeText === undefined
|
|
695
|
+
? mappedModelToolResultContent({ content: block.content, isError: block.is_error === true })
|
|
696
|
+
: { content: [{ type: 'text' as const, text: fileChangeText }], text: fileChangeText }
|
|
697
|
+
const content = hostTrailer === undefined ? mapped.content : mappedHostMcpContent({ trailer: hostTrailer, modelBlocks: mapped.content })
|
|
698
|
+
const structuredContent = ownJsonSlot(hostTrailer, 'structuredContent')
|
|
699
|
+
const reportedError = ownJsonSlot(hostTrailer, 'isError')
|
|
700
|
+
const output = removeUndefined({
|
|
701
|
+
success: block.is_error !== true,
|
|
702
|
+
content,
|
|
703
|
+
structuredContent,
|
|
704
|
+
reportedSuccess: typeof reportedError === 'boolean' ? !reportedError : undefined,
|
|
705
|
+
})
|
|
384
706
|
const drafts: AgentEventDraft[] = []
|
|
385
|
-
if (text)
|
|
386
|
-
drafts.push(withActor({ type: 'tool.output.delta', turnId, payload: { toolCallId, delta: text } }, actor))
|
|
707
|
+
if (mapped.text !== undefined)
|
|
708
|
+
drafts.push(withActor({ type: 'tool.output.delta', turnId, payload: { toolCallId, delta: mapped.text } }, actor))
|
|
387
709
|
drafts.push(withActor({
|
|
388
710
|
type: 'tool.call.completed',
|
|
389
711
|
turnId,
|
|
390
712
|
payload: removeUndefined({
|
|
391
713
|
toolCallId,
|
|
714
|
+
toolName: state.toolNameById.get(toolCallId),
|
|
392
715
|
status: block.is_error === true ? 'failed' : 'completed',
|
|
393
716
|
output,
|
|
394
|
-
|
|
717
|
+
// The failure detail already rides in output; like Codex, the error is a short summary.
|
|
718
|
+
error: block.is_error === true ? { message: `Tool ${state.toolNameById.get(toolCallId) ?? toolCallId} failed.` } : undefined,
|
|
395
719
|
}),
|
|
396
720
|
}, actor))
|
|
397
721
|
|
|
@@ -468,20 +792,19 @@ function parseClaudeTaskResult<Output>({ value, toolName, schema }: { value: unk
|
|
|
468
792
|
})
|
|
469
793
|
}
|
|
470
794
|
|
|
471
|
-
function mapStreamEvent(event: BetaRawMessageStreamEvent, state: ClaudeEventMapperState, turnId: string, actor: AgentEventActor
|
|
795
|
+
function mapStreamEvent(event: BetaRawMessageStreamEvent, state: ClaudeEventMapperState, turnId: string, actor: AgentEventActor): AgentEventDraft[] {
|
|
472
796
|
const drafts: AgentEventDraft[] = []
|
|
473
797
|
if (event.type === 'message_start') {
|
|
474
|
-
state
|
|
475
|
-
|
|
476
|
-
state.actorByMessageId.set(event.message.id, actor)
|
|
798
|
+
setCurrentMessageId(state, actor, event.message.id)
|
|
799
|
+
state.actorByMessageId.set(event.message.id, actor)
|
|
477
800
|
ensureAssistantMessageStarted(drafts, state, event.message, turnId, actor)
|
|
478
801
|
return drafts
|
|
479
802
|
}
|
|
480
803
|
|
|
481
804
|
if (event.type === 'content_block_start') {
|
|
482
|
-
const messageId = currentMessageId(state)
|
|
483
|
-
const messageActor =
|
|
484
|
-
state.
|
|
805
|
+
const messageId = currentMessageId(state, actor)
|
|
806
|
+
const messageActor = state.actorByMessageId.get(messageId) ?? actor
|
|
807
|
+
contentBlockTypes(state, actor).set(event.index, event.content_block.type)
|
|
485
808
|
if (event.content_block.type === 'thinking') {
|
|
486
809
|
state.streamedReasoningMessageIds.add(messageId)
|
|
487
810
|
ensureReasoningStarted(drafts, state, turnId, messageId, messageActor)
|
|
@@ -490,30 +813,30 @@ function mapStreamEvent(event: BetaRawMessageStreamEvent, state: ClaudeEventMapp
|
|
|
490
813
|
}
|
|
491
814
|
|
|
492
815
|
if (event.type === 'content_block_stop') {
|
|
493
|
-
const messageId = currentMessageId(state)
|
|
494
|
-
const messageActor =
|
|
495
|
-
const
|
|
816
|
+
const messageId = currentMessageId(state, actor)
|
|
817
|
+
const messageActor = state.actorByMessageId.get(messageId) ?? actor
|
|
818
|
+
const blockTypes = contentBlockTypes(state, actor)
|
|
819
|
+
const blockType = blockTypes.get(event.index)
|
|
496
820
|
if (blockType === 'thinking')
|
|
497
821
|
endReasoning(drafts, state, turnId, messageId, state.streamedReasoningByMessageId.get(messageId), messageActor)
|
|
498
|
-
|
|
822
|
+
blockTypes.delete(event.index)
|
|
499
823
|
return drafts
|
|
500
824
|
}
|
|
501
825
|
|
|
502
826
|
if (event.type === 'message_stop') {
|
|
503
|
-
const messageId = currentMessageId(state)
|
|
504
|
-
const messageActor =
|
|
827
|
+
const messageId = currentMessageId(state, actor)
|
|
828
|
+
const messageActor = state.actorByMessageId.get(messageId) ?? actor
|
|
505
829
|
endAssistantMessageById(drafts, state, turnId, messageId, state.streamedTextByMessageId.get(messageId), messageActor)
|
|
506
830
|
state.actorByMessageId.delete(messageId)
|
|
507
|
-
state
|
|
508
|
-
state.contentBlockTypeByIndex.clear()
|
|
831
|
+
clearCurrentMessage(state, actor)
|
|
509
832
|
return drafts
|
|
510
833
|
}
|
|
511
834
|
|
|
512
835
|
if (event.type !== 'content_block_delta')
|
|
513
836
|
return drafts
|
|
514
837
|
|
|
515
|
-
const messageId = currentMessageId(state)
|
|
516
|
-
const messageActor =
|
|
838
|
+
const messageId = currentMessageId(state, actor)
|
|
839
|
+
const messageActor = state.actorByMessageId.get(messageId) ?? actor
|
|
517
840
|
const delta = event.delta
|
|
518
841
|
if (delta.type === 'thinking_delta') {
|
|
519
842
|
state.streamedReasoningMessageIds.add(messageId)
|
|
@@ -530,18 +853,64 @@ function mapStreamEvent(event: BetaRawMessageStreamEvent, state: ClaudeEventMapp
|
|
|
530
853
|
}
|
|
531
854
|
|
|
532
855
|
function ensureTurnStarted(drafts: AgentEventDraft[], state: ClaudeEventMapperState): string {
|
|
533
|
-
if (state.turnStarted
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
856
|
+
if (!state.turnStarted || !state.currentTurnId) {
|
|
857
|
+
state.turnStarted = true
|
|
858
|
+
state.interrupt = { status: 'none' }
|
|
859
|
+
state.currentTurnId = createRuntimeTurnId()
|
|
860
|
+
state.currentMessageId = undefined
|
|
861
|
+
state.contentBlockTypeByIndex.clear()
|
|
862
|
+
drafts.push({ type: 'turn.started', turnId: state.currentTurnId, actor: mainActor, payload: removeUndefined({ commandId: state.currentCommandId }) })
|
|
863
|
+
}
|
|
541
864
|
ensureSessionRunning(drafts, state)
|
|
542
865
|
return state.currentTurnId
|
|
543
866
|
}
|
|
544
867
|
|
|
868
|
+
// Turn-scoped state after the main turn's `turn.ended`. With CLI state events the CLI's own `idle`
|
|
869
|
+
// ends the session's work; without them the turn end is the only turn-over signal.
|
|
870
|
+
function endTurn(drafts: AgentEventDraft[], state: ClaudeEventMapperState): void {
|
|
871
|
+
if (!state.cliReportsSessionState)
|
|
872
|
+
ensureSessionIdle(drafts, state)
|
|
873
|
+
state.reportedContext = undefined
|
|
874
|
+
state.mainTurnRateLimited = false
|
|
875
|
+
state.usageLimit = usageLimitAfterTurn(state.usageLimit)
|
|
876
|
+
state.turnStarted = false
|
|
877
|
+
state.interrupt = { status: 'none' }
|
|
878
|
+
state.currentTurnId = undefined
|
|
879
|
+
state.currentCommandId = undefined
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Emits a superseded turn's held end as final: its steer will not continue it. */
|
|
883
|
+
export function flushHeldTurnEnd(state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
884
|
+
const held = state.heldTurnEnd
|
|
885
|
+
if (!held)
|
|
886
|
+
return []
|
|
887
|
+
state.heldTurnEnd = undefined
|
|
888
|
+
const drafts: AgentEventDraft[] = [held]
|
|
889
|
+
endTurn(drafts, state)
|
|
890
|
+
return drafts
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Applies the CLI's `command_lifecycle` receipt for a steering input. `queued` marks a pending
|
|
895
|
+
* supersession; `started` continues the held turn (or starts one, so an interrupt has a target);
|
|
896
|
+
* any other state drops the steer, and the held end is final once no steer can continue it.
|
|
897
|
+
*/
|
|
898
|
+
export function mapSteerLifecycle(input: { steerId: string, state: 'queued' | 'started' | 'completed' | 'cancelled' | 'discarded' | 'refused' }, state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
899
|
+
if (input.state === 'queued') {
|
|
900
|
+
state.queuedSteerIds.add(input.steerId)
|
|
901
|
+
return []
|
|
902
|
+
}
|
|
903
|
+
if (!state.queuedSteerIds.delete(input.steerId) && input.state !== 'started')
|
|
904
|
+
return []
|
|
905
|
+
if (input.state === 'started') {
|
|
906
|
+
state.heldTurnEnd = undefined
|
|
907
|
+
const drafts: AgentEventDraft[] = []
|
|
908
|
+
ensureTurnStarted(drafts, state)
|
|
909
|
+
return drafts
|
|
910
|
+
}
|
|
911
|
+
return state.queuedSteerIds.size === 0 ? flushHeldTurnEnd(state) : []
|
|
912
|
+
}
|
|
913
|
+
|
|
545
914
|
function ensureSessionRunning(drafts: AgentEventDraft[], state: ClaudeEventMapperState): void {
|
|
546
915
|
if (state.sessionState === 'running')
|
|
547
916
|
return
|
|
@@ -556,22 +925,49 @@ function ensureSessionIdle(drafts: AgentEventDraft[], state: ClaudeEventMapperSt
|
|
|
556
925
|
drafts.push({ type: 'session.state.changed', payload: { state: 'idle' } })
|
|
557
926
|
}
|
|
558
927
|
|
|
559
|
-
function ensureAssistantMessageStarted(drafts: AgentEventDraft[], state: ClaudeEventMapperState, message: BetaMessage, turnId: string, actor: AgentEventActor
|
|
560
|
-
state
|
|
561
|
-
|
|
562
|
-
state.actorByMessageId.set(message.id, actor)
|
|
928
|
+
function ensureAssistantMessageStarted(drafts: AgentEventDraft[], state: ClaudeEventMapperState, message: BetaMessage, turnId: string, actor: AgentEventActor): void {
|
|
929
|
+
setCurrentMessageId(state, actor, message.id)
|
|
930
|
+
state.actorByMessageId.set(message.id, actor)
|
|
563
931
|
if (state.startedMessageIds.has(message.id))
|
|
564
932
|
return
|
|
565
933
|
state.startedMessageIds.add(message.id)
|
|
566
934
|
drafts.push(withActor({ type: 'message.started', turnId, payload: { messageId: message.id, role: 'assistant' } }, actor))
|
|
567
935
|
}
|
|
568
936
|
|
|
569
|
-
function
|
|
570
|
-
if (
|
|
571
|
-
|
|
937
|
+
function setCurrentMessageId(state: ClaudeEventMapperState, actor: AgentEventActor, messageId: string): void {
|
|
938
|
+
if (actor.type === 'main')
|
|
939
|
+
state.currentMessageId = messageId
|
|
940
|
+
else state.currentMessageIdByActorId.set(actor.actorId, messageId)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function currentMessageId(state: ClaudeEventMapperState, actor: AgentEventActor): string {
|
|
944
|
+
const messageId = actor.type === 'main' ? state.currentMessageId : state.currentMessageIdByActorId.get(actor.actorId)
|
|
945
|
+
if (messageId)
|
|
946
|
+
return messageId
|
|
572
947
|
throw new AgentProviderProtocolError({ message: 'Claude stream emitted content_block_delta before message_start.' })
|
|
573
948
|
}
|
|
574
949
|
|
|
950
|
+
function contentBlockTypes(state: ClaudeEventMapperState, actor: AgentEventActor): Map<number, string> {
|
|
951
|
+
if (actor.type === 'main')
|
|
952
|
+
return state.contentBlockTypeByIndex
|
|
953
|
+
const existing = state.contentBlockTypeByActorId.get(actor.actorId)
|
|
954
|
+
if (existing)
|
|
955
|
+
return existing
|
|
956
|
+
const created = new Map<number, string>()
|
|
957
|
+
state.contentBlockTypeByActorId.set(actor.actorId, created)
|
|
958
|
+
return created
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function clearCurrentMessage(state: ClaudeEventMapperState, actor: AgentEventActor): void {
|
|
962
|
+
if (actor.type === 'main') {
|
|
963
|
+
state.currentMessageId = undefined
|
|
964
|
+
state.contentBlockTypeByIndex.clear()
|
|
965
|
+
return
|
|
966
|
+
}
|
|
967
|
+
state.currentMessageIdByActorId.delete(actor.actorId)
|
|
968
|
+
state.contentBlockTypeByActorId.delete(actor.actorId)
|
|
969
|
+
}
|
|
970
|
+
|
|
575
971
|
function endAssistantMessage(drafts: AgentEventDraft[], state: ClaudeEventMapperState, message: BetaMessage, turnId: string, actor: AgentEventActor | undefined): void {
|
|
576
972
|
if (state.endedMessageIds.has(message.id))
|
|
577
973
|
return
|
|
@@ -605,7 +1001,9 @@ function endReasoning(drafts: AgentEventDraft[], state: ClaudeEventMapperState,
|
|
|
605
1001
|
|
|
606
1002
|
function endOpenReasoning(drafts: AgentEventDraft[], state: ClaudeEventMapperState, turnId: string): void {
|
|
607
1003
|
for (const reasoningId of state.startedReasoningIds) {
|
|
608
|
-
|
|
1004
|
+
const messageId = reasoningId.slice('reasoning-'.length)
|
|
1005
|
+
const actor = state.actorByMessageId.get(messageId)
|
|
1006
|
+
if (actor?.type === 'main' && !state.endedReasoningIds.has(reasoningId)) {
|
|
609
1007
|
state.endedReasoningIds.add(reasoningId)
|
|
610
1008
|
drafts.push({ type: 'reasoning.ended', turnId, actor: mainActor, payload: { reasoningId } })
|
|
611
1009
|
}
|
|
@@ -626,7 +1024,11 @@ function reasoningIdForMessage(messageId: string): string {
|
|
|
626
1024
|
return `reasoning-${messageId}`
|
|
627
1025
|
}
|
|
628
1026
|
|
|
629
|
-
function turnEndedPayload(message
|
|
1027
|
+
function turnEndedPayload({ message, interruptAccepted, usageLimit }: {
|
|
1028
|
+
message: ClaudeResultMessage
|
|
1029
|
+
interruptAccepted: boolean
|
|
1030
|
+
usageLimit: Extract<ClaudeUsageLimit, { status: 'exhausted' }> | undefined
|
|
1031
|
+
}): Extract<AgentEventDraft, { type: 'turn.ended' }>['payload'] {
|
|
630
1032
|
if (interruptAccepted && message.subtype === 'error_during_execution') {
|
|
631
1033
|
return removeUndefined({
|
|
632
1034
|
status: 'interrupted' as const,
|
|
@@ -647,45 +1049,227 @@ function turnEndedPayload(message: Extract<SDKMessage, { type: 'result' }>, inte
|
|
|
647
1049
|
status: 'failed' as const,
|
|
648
1050
|
reason: message.stop_reason ?? message.subtype,
|
|
649
1051
|
duration: message.duration_ms,
|
|
650
|
-
error: resultError(message),
|
|
1052
|
+
error: usageLimit ? usageLimitError({ error: resultError(message), usageLimit }) : resultError(message),
|
|
651
1053
|
})
|
|
652
1054
|
}
|
|
653
1055
|
|
|
654
|
-
|
|
655
|
-
|
|
1056
|
+
// Codex reports a plan usage limit as codexErrorInfo `usageLimitExceeded`; reuse that code so
|
|
1057
|
+
// consumers classify both providers alike. The reset time rides on `cause` because AgentError
|
|
1058
|
+
// has no structured slot for it.
|
|
1059
|
+
function usageLimitError({ error, usageLimit }: { error: AgentError, usageLimit: Extract<ClaudeUsageLimit, { status: 'exhausted' }> }): AgentError {
|
|
1060
|
+
const resetsAt = usageLimit.resetsAt === undefined ? undefined : isoFromEpochSeconds(usageLimit.resetsAt)
|
|
1061
|
+
return removeUndefined({
|
|
1062
|
+
message: error.message,
|
|
1063
|
+
code: 'usageLimitExceeded',
|
|
1064
|
+
cause: resetsAt ? removeUndefined({ message: `Claude usage limit resets at ${resetsAt}.`, code: usageLimit.rateLimitType }) : undefined,
|
|
1065
|
+
})
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function usageLimitFromRateLimitInfo(info: ClaudeRateLimitInfo): ClaudeUsageLimit {
|
|
1069
|
+
if (info.status !== 'rejected')
|
|
1070
|
+
return { status: 'available' }
|
|
1071
|
+
const resetsAt = info.resetsAt !== undefined && Number.isFinite(info.resetsAt) && info.resetsAt > 0 ? info.resetsAt : undefined
|
|
1072
|
+
return removeUndefined({ status: 'exhausted' as const, resetsAt, rateLimitType: info.rateLimitType })
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Mirrors the CLI's own expiry check (`resetsAt * 1000 <= Date.now()` on CLI 2.1.280).
|
|
1076
|
+
function usageLimitInForce(limit: ClaudeUsageLimit, nowMs: number): Extract<ClaudeUsageLimit, { status: 'exhausted' }> | undefined {
|
|
1077
|
+
if (limit.status !== 'exhausted')
|
|
1078
|
+
return undefined
|
|
1079
|
+
return limit.resetsAt === undefined || nowMs < limit.resetsAt * 1000 ? limit : undefined
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function usageLimitAfterTurn(limit: ClaudeUsageLimit): ClaudeUsageLimit {
|
|
1083
|
+
return limit.status === 'exhausted' && limit.resetsAt !== undefined ? limit : { status: 'available' }
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function isoFromEpochSeconds(seconds: number): string | undefined {
|
|
1087
|
+
const date = new Date(seconds * 1000)
|
|
1088
|
+
return Number.isFinite(seconds) && seconds > 0 && Number.isFinite(date.getTime()) ? date.toISOString() : undefined
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function observeMainAssistantMessage(message: Extract<SDKMessage, { type: 'assistant' }>, state: ClaudeEventMapperState): void {
|
|
1092
|
+
if (message.error === 'rate_limit')
|
|
1093
|
+
state.mainTurnRateLimited = true
|
|
1094
|
+
// Claude Code's own context report counts the last response's input side (verified against
|
|
1095
|
+
// getContextUsage on CLI 2.1.280); output is partial on streamed frames and previous thinking
|
|
1096
|
+
// is dropped from later requests. A zero input side is a synthetic frame, not an observation.
|
|
1097
|
+
const { usage } = message.message
|
|
1098
|
+
const usedTokens = usage.input_tokens + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0)
|
|
1099
|
+
if (usedTokens > 0 && Number.isSafeInteger(usedTokens))
|
|
1100
|
+
state.contextObservation = { status: 'observed', model: message.message.model, usedTokens }
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Codex reports context on every token-usage update; Claude's equivalent is each main response,
|
|
1104
|
+
// so report whenever the snapshot changes and again at turn end once the result names the window.
|
|
1105
|
+
function reportContextSnapshot({ drafts, state, turnId, result }: {
|
|
1106
|
+
drafts: AgentEventDraft[]
|
|
1107
|
+
state: ClaudeEventMapperState
|
|
1108
|
+
turnId: string
|
|
1109
|
+
result?: ClaudeResultMessage
|
|
1110
|
+
}): void {
|
|
1111
|
+
if (state.contextObservation.status !== 'observed' || state.currentCompactionId !== undefined)
|
|
1112
|
+
return
|
|
1113
|
+
if (result)
|
|
1114
|
+
rememberContextWindows(result, state)
|
|
1115
|
+
const snapshot = contextSnapshot({ observation: state.contextObservation, state })
|
|
1116
|
+
if (state.reportedContext !== undefined && contextSnapshotsEqual(snapshot, state.reportedContext))
|
|
1117
|
+
return
|
|
1118
|
+
state.reportedContext = snapshot
|
|
1119
|
+
drafts.push({ type: 'context.updated', turnId, actor: mainActor, payload: { snapshot } })
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function rememberContextWindows(result: ClaudeResultMessage, state: ClaudeEventMapperState): void {
|
|
1123
|
+
for (const [model, usage] of Object.entries(result.modelUsage)) {
|
|
1124
|
+
if (Number.isSafeInteger(usage.contextWindow) && usage.contextWindow > 0)
|
|
1125
|
+
state.contextWindowByModel.set(model, usage.contextWindow)
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function contextSnapshot({ observation, state }: {
|
|
1130
|
+
observation: Extract<ClaudeContextObservation, { status: 'observed' }>
|
|
1131
|
+
state: ClaudeEventMapperState
|
|
1132
|
+
}): AgentContextSnapshot {
|
|
1133
|
+
// modelUsage is keyed by the requested model string, which is the init model for an explicit id
|
|
1134
|
+
// and the resolved id (also the assistant model) for an alias.
|
|
1135
|
+
const capacityTokens = state.contextWindowByModel.get(observation.model)
|
|
1136
|
+
?? (state.sessionModel === undefined ? undefined : state.contextWindowByModel.get(state.sessionModel))
|
|
1137
|
+
if (capacityTokens === undefined)
|
|
1138
|
+
return { status: 'observed', usedTokens: observation.usedTokens, window: { status: 'unknown' } }
|
|
1139
|
+
// Claude Code reports context against the raw window without a reserved baseline.
|
|
1140
|
+
const remainingPercent = Math.round(Math.min(Math.max(100 * (capacityTokens - observation.usedTokens) / capacityTokens, 0), 100))
|
|
1141
|
+
return { status: 'observed', usedTokens: observation.usedTokens, window: { status: 'known', capacityTokens, remainingPercent } }
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function contextSnapshotsEqual(left: AgentContextSnapshot, right: AgentContextSnapshot): boolean {
|
|
1145
|
+
return JSON.stringify(left) === JSON.stringify(right)
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
export function compactionError(message: ClaudeStatusMessage): AgentError {
|
|
1149
|
+
return { message: message.compact_error || 'Claude compaction failed.' }
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
export function resultError(message: Extract<SDKMessage, { type: 'result' }>): AgentError {
|
|
1153
|
+
// SDK success/is_error carries API failure text in result rather than the errors array.
|
|
1154
|
+
const detail = message.subtype === 'success'
|
|
1155
|
+
? message.result
|
|
1156
|
+
: message.errors.length > 0 ? message.errors.join('\n') : undefined
|
|
656
1157
|
return {
|
|
657
|
-
message:
|
|
1158
|
+
message: detail || `Claude result ended with ${message.subtype}.`,
|
|
658
1159
|
code: message.subtype,
|
|
659
1160
|
}
|
|
660
1161
|
}
|
|
661
1162
|
|
|
662
|
-
function shouldEndAssistantSnapshot(message: BetaMessage, state: ClaudeEventMapperState, actor:
|
|
663
|
-
return actor
|
|
1163
|
+
function shouldEndAssistantSnapshot(message: BetaMessage, state: ClaudeEventMapperState, actor: AgentEventActor): boolean {
|
|
1164
|
+
return actor.type === 'subagent'
|
|
664
1165
|
&& !state.streamedTextByMessageId.has(message.id)
|
|
665
1166
|
&& !state.streamedReasoningMessageIds.has(message.id)
|
|
666
1167
|
}
|
|
667
1168
|
|
|
668
|
-
function
|
|
669
|
-
|
|
1169
|
+
function actorContextFromParentToolUseId(parentToolUseId: string | null, drafts: AgentEventDraft[], state: ClaudeEventMapperState): ClaudeActorContext | undefined {
|
|
1170
|
+
if (parentToolUseId === null)
|
|
1171
|
+
return { actor: mainActor, turnId: ensureTurnStarted(drafts, state) }
|
|
1172
|
+
|
|
1173
|
+
const actor = state.subagentByToolCallId.get(parentToolUseId)
|
|
1174
|
+
const turnId = actor ? state.turnIdBySubagentId.get(actor.subagentId) : undefined
|
|
1175
|
+
return actor && turnId ? { actor, turnId } : undefined
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function subagentActorForTaskMessage(input: { subagentId: string, parentToolCallId?: string, name?: string }, state: ClaudeEventMapperState): ClaudeTaskActorResolution {
|
|
1179
|
+
const existingByTask = state.subagentByTaskId.get(input.subagentId)
|
|
1180
|
+
if (existingByTask) {
|
|
1181
|
+
if (input.parentToolCallId && existingByTask.parentToolCallId !== input.parentToolCallId && state.subagentByToolCallId.get(input.parentToolCallId)?.subagentId !== input.subagentId)
|
|
1182
|
+
return { status: 'conflict' }
|
|
1183
|
+
const turnId = state.turnIdBySubagentId.get(input.subagentId)
|
|
1184
|
+
if (!turnId)
|
|
1185
|
+
return { status: 'conflict' }
|
|
1186
|
+
const actor = actorWithObservedName(existingByTask, input.name, state)
|
|
1187
|
+
return { status: 'resolved', context: { actor, turnId } }
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (!input.parentToolCallId)
|
|
1191
|
+
return { status: 'pending' }
|
|
1192
|
+
const existingByTool = state.subagentByToolCallId.get(input.parentToolCallId)
|
|
1193
|
+
if (existingByTool)
|
|
1194
|
+
return { status: existingByTool.subagentId === input.subagentId ? 'pending' : 'conflict' }
|
|
1195
|
+
|
|
1196
|
+
const owner = state.toolOwnerById.get(input.parentToolCallId)
|
|
1197
|
+
if (!owner)
|
|
1198
|
+
return { status: 'pending' }
|
|
1199
|
+
if (input.subagentId === mainActor.actorId || owner.actor.actorId === input.subagentId || wouldCreateActorCycle(input.subagentId, owner.actor, state))
|
|
1200
|
+
return { status: 'conflict' }
|
|
1201
|
+
|
|
1202
|
+
const actor = subagentActorFromTask({
|
|
1203
|
+
subagentId: input.subagentId,
|
|
1204
|
+
parentActorId: owner.actor.actorId,
|
|
1205
|
+
parentToolCallId: input.parentToolCallId,
|
|
1206
|
+
name: input.name,
|
|
1207
|
+
})
|
|
1208
|
+
state.subagentByTaskId.set(input.subagentId, actor)
|
|
1209
|
+
state.subagentByToolCallId.set(input.parentToolCallId, actor)
|
|
1210
|
+
state.turnIdBySubagentId.set(input.subagentId, owner.turnId)
|
|
1211
|
+
state.runToolCallIdBySubagentId.set(input.subagentId, input.parentToolCallId)
|
|
1212
|
+
return { status: 'resolved', context: { actor, turnId: owner.turnId } }
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function actorWithObservedName(actor: ClaudeSubagentActor, name: string | undefined, state: ClaudeEventMapperState): ClaudeSubagentActor {
|
|
1216
|
+
if (!name || actor.name === name)
|
|
1217
|
+
return actor
|
|
1218
|
+
if (actor.name)
|
|
1219
|
+
return actor
|
|
1220
|
+
|
|
1221
|
+
const named = { ...actor, name }
|
|
1222
|
+
state.subagentByTaskId.set(actor.subagentId, named)
|
|
1223
|
+
if (actor.parentToolCallId)
|
|
1224
|
+
state.subagentByToolCallId.set(actor.parentToolCallId, named)
|
|
1225
|
+
return named
|
|
670
1226
|
}
|
|
671
1227
|
|
|
672
|
-
function
|
|
673
|
-
|
|
674
|
-
|
|
1228
|
+
function wouldCreateActorCycle(subagentId: string, parent: AgentEventActor, state: ClaudeEventMapperState): boolean {
|
|
1229
|
+
let actor = parent
|
|
1230
|
+
const visited = new Set<string>()
|
|
1231
|
+
while (actor.type === 'subagent') {
|
|
1232
|
+
if (actor.actorId === subagentId || visited.has(actor.actorId))
|
|
1233
|
+
return true
|
|
1234
|
+
visited.add(actor.actorId)
|
|
1235
|
+
const next = state.subagentByTaskId.get(actor.parentActorId)
|
|
1236
|
+
if (!next)
|
|
1237
|
+
return false
|
|
1238
|
+
actor = next
|
|
1239
|
+
}
|
|
1240
|
+
return false
|
|
675
1241
|
}
|
|
676
1242
|
|
|
677
|
-
function subagentActorFromTask(input: { subagentId: string, parentToolCallId
|
|
1243
|
+
function subagentActorFromTask(input: { subagentId: string, parentActorId: string, parentToolCallId: string, name?: string }): ClaudeSubagentActor {
|
|
678
1244
|
return removeUndefined({
|
|
679
1245
|
type: 'subagent' as const,
|
|
680
1246
|
actorId: input.subagentId,
|
|
681
1247
|
subagentId: input.subagentId,
|
|
682
|
-
parentActorId:
|
|
683
|
-
origin:
|
|
1248
|
+
parentActorId: input.parentActorId,
|
|
1249
|
+
origin: { type: 'tool_call' as const, toolCallId: input.parentToolCallId },
|
|
684
1250
|
parentToolCallId: input.parentToolCallId,
|
|
685
1251
|
name: input.name,
|
|
686
1252
|
})
|
|
687
1253
|
}
|
|
688
1254
|
|
|
1255
|
+
function flushPendingMessages(state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
1256
|
+
const drafts: AgentEventDraft[] = []
|
|
1257
|
+
let resolved = true
|
|
1258
|
+
while (resolved) {
|
|
1259
|
+
resolved = false
|
|
1260
|
+
for (let index = 0; index < state.pendingMessages.length; index += 1) {
|
|
1261
|
+
const pendingDrafts = mapClaudeMessageWithKnownActor(state.pendingMessages[index]!, state)
|
|
1262
|
+
if (pendingDrafts === undefined)
|
|
1263
|
+
continue
|
|
1264
|
+
state.pendingMessages.splice(index, 1)
|
|
1265
|
+
drafts.push(...pendingDrafts)
|
|
1266
|
+
resolved = true
|
|
1267
|
+
break
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return drafts
|
|
1271
|
+
}
|
|
1272
|
+
|
|
689
1273
|
function subagentPayloadRef(actor: ClaudeSubagentActor): Pick<ClaudeSubagentActor, 'actorId' | 'subagentId' | 'parentActorId' | 'origin' | 'parentToolCallId' | 'name'> {
|
|
690
1274
|
return removeUndefined({
|
|
691
1275
|
actorId: actor.actorId,
|
|
@@ -737,12 +1321,15 @@ function normalizeToolName(name: string): string {
|
|
|
737
1321
|
function usageFromObject(value: BetaUsage | undefined): AgentUsage | undefined {
|
|
738
1322
|
if (!value)
|
|
739
1323
|
return undefined
|
|
1324
|
+
// Anthropic input_tokens excludes cache reads and writes; the provider-neutral inputTokens, like
|
|
1325
|
+
// Codex's, counts every input token, with the cached subsets reported alongside it.
|
|
1326
|
+
const inputTokens = value.input_tokens + (value.cache_read_input_tokens ?? 0) + (value.cache_creation_input_tokens ?? 0)
|
|
740
1327
|
return removeUndefined({
|
|
741
|
-
inputTokens
|
|
1328
|
+
inputTokens,
|
|
742
1329
|
outputTokens: value.output_tokens,
|
|
743
1330
|
cacheReadTokens: value.cache_read_input_tokens ?? undefined,
|
|
744
1331
|
cacheWriteTokens: value.cache_creation_input_tokens ?? undefined,
|
|
745
|
-
totalTokens:
|
|
1332
|
+
totalTokens: inputTokens + value.output_tokens,
|
|
746
1333
|
})
|
|
747
1334
|
}
|
|
748
1335
|
|
|
@@ -756,13 +1343,159 @@ function thinkingFromAssistantContent(content: BetaMessage['content']): string |
|
|
|
756
1343
|
return text || undefined
|
|
757
1344
|
}
|
|
758
1345
|
|
|
759
|
-
function
|
|
760
|
-
if (
|
|
1346
|
+
function correlatedHostTrailerToolCallId(input: { content: UserContentBlock[], message: ClaudeUserMessage, state: ClaudeEventMapperState }): string | undefined {
|
|
1347
|
+
if (input.message.tool_use_result === undefined)
|
|
761
1348
|
return undefined
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1349
|
+
const toolCallId = soleToolResultToolCallId(input.content)
|
|
1350
|
+
return toolCallId && input.state.hostMcpToolCallIds.has(toolCallId) ? toolCallId : undefined
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// The SDK attaches one message-level tool_use_result, so it is attributable only to a sole tool_result.
|
|
1354
|
+
function soleToolResultToolCallId(content: UserContentBlock[]): string | undefined {
|
|
1355
|
+
const toolResults = content.filter((block): block is UserToolResultBlock => block.type === 'tool_result')
|
|
1356
|
+
return toolResults.length === 1 ? toolResults[0]!.tool_use_id : undefined
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function mappedModelToolResultContent({ content, isError }: { content: UserToolResultBlock['content'], isError: boolean }): { content: AgentToolContent[], text?: string } {
|
|
1360
|
+
if (typeof content === 'string') {
|
|
1361
|
+
const text = isError ? withoutToolUseErrorWrapper(content) : content
|
|
1362
|
+
return { content: [{ type: 'text', text }], text }
|
|
1363
|
+
}
|
|
1364
|
+
if (content === undefined)
|
|
1365
|
+
return { content: [] }
|
|
1366
|
+
if (!Array.isArray(content)) {
|
|
1367
|
+
const parsed = jsonValueSchema.safeParse(content)
|
|
1368
|
+
return parsed.success ? { content: [jsonToolContent(parsed.data)] } : { content: [] }
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const mapped: AgentToolContent[] = []
|
|
1372
|
+
let text = ''
|
|
1373
|
+
let hasText = false
|
|
1374
|
+
for (const block of content) {
|
|
1375
|
+
const parsed = jsonValueSchema.safeParse(block)
|
|
1376
|
+
if (!parsed.success)
|
|
1377
|
+
continue
|
|
1378
|
+
const value = isError ? withoutToolUseErrorBlockWrapper(parsed.data) : parsed.data
|
|
1379
|
+
if (isJsonObject(value) && value.type === 'text' && typeof value.text === 'string') {
|
|
1380
|
+
hasText = true
|
|
1381
|
+
text += value.text
|
|
1382
|
+
}
|
|
1383
|
+
mapped.push(modelToolContent(value))
|
|
1384
|
+
}
|
|
1385
|
+
return removeUndefined({ content: mapped, text: hasText ? text : undefined })
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Claude Code wraps its own tool failures as `<tool_use_error>…</tool_use_error>` for the model and
|
|
1389
|
+
// strips the same wrapper before display (CLI 2.1.280), so the wrapper is transport, not output.
|
|
1390
|
+
const TOOL_USE_ERROR_WRAPPER = /^<tool_use_error>([\s\S]*)<\/tool_use_error>$/
|
|
1391
|
+
|
|
1392
|
+
function withoutToolUseErrorWrapper(text: string): string {
|
|
1393
|
+
return TOOL_USE_ERROR_WRAPPER.exec(text)?.[1] ?? text
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function withoutToolUseErrorBlockWrapper(value: JsonValue): JsonValue {
|
|
1397
|
+
if (!isJsonObject(value) || value.type !== 'text' || typeof value.text !== 'string')
|
|
1398
|
+
return value
|
|
1399
|
+
return { ...value, text: withoutToolUseErrorWrapper(value.text) }
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function modelToolContent(value: JsonValue): AgentToolContent {
|
|
1403
|
+
if (!isJsonObject(value))
|
|
1404
|
+
return jsonToolContent(value)
|
|
1405
|
+
if (value.type === 'image' && isJsonObject(value.source)) {
|
|
1406
|
+
const source = value.source
|
|
1407
|
+
const extras = Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'type' && key !== 'source'))
|
|
1408
|
+
const sourceExtras = Object.fromEntries(Object.entries(source).filter(([key]) => !['type', 'media_type', 'data', 'url'].includes(key)))
|
|
1409
|
+
if (Object.keys(sourceExtras).length > 0)
|
|
1410
|
+
extras.source = sourceExtras
|
|
1411
|
+
const metadata = Object.keys(extras).length > 0 ? { extras } : {}
|
|
1412
|
+
if (source.type === 'base64' && typeof source.media_type === 'string' && source.media_type.length > 0
|
|
1413
|
+
&& typeof source.data === 'string' && source.data.length > 0) {
|
|
1414
|
+
return { type: 'image', mimeType: source.media_type, data: source.data, ...metadata }
|
|
1415
|
+
}
|
|
1416
|
+
if (source.type === 'url' && typeof source.url === 'string' && source.url.length > 0)
|
|
1417
|
+
return { type: 'image', uri: source.url, ...metadata }
|
|
1418
|
+
}
|
|
1419
|
+
return mcpToolContent(value)
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function mappedHostMcpContent(input: { trailer: unknown, modelBlocks: AgentToolContent[] }): AgentToolContent[] {
|
|
1423
|
+
const content: AgentToolContent[] = [...input.modelBlocks]
|
|
1424
|
+
const hostContent = ownJsonSlot(input.trailer, 'content')
|
|
1425
|
+
if (hostContent !== undefined) {
|
|
1426
|
+
const hostBlocks = Array.isArray(hostContent) ? hostContent.map(mcpToolContent) : [mcpToolContent(hostContent)]
|
|
1427
|
+
mergeToolContentOccurrences(content, hostBlocks)
|
|
1428
|
+
}
|
|
1429
|
+
const resourceLinks = ownJsonSlot(input.trailer, 'resourceLinks')
|
|
1430
|
+
if (resourceLinks !== undefined) {
|
|
1431
|
+
if (Array.isArray(resourceLinks))
|
|
1432
|
+
mergeToolContentOccurrences(content, resourceLinks.map(resourceLinkContent))
|
|
1433
|
+
else
|
|
1434
|
+
content.push(jsonToolContent(resourceLinks))
|
|
1435
|
+
}
|
|
1436
|
+
return content
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function mergeToolContentOccurrences(target: AgentToolContent[], incoming: AgentToolContent[]): void {
|
|
1440
|
+
let cursor = 0
|
|
1441
|
+
for (let incomingIndex = 0; incomingIndex < incoming.length; incomingIndex += 1) {
|
|
1442
|
+
const block = incoming[incomingIndex]!
|
|
1443
|
+
const index = target.findIndex((existing, position) => position >= cursor
|
|
1444
|
+
&& jsonValuesEqual(contentIdentity(existing), contentIdentity(block)))
|
|
1445
|
+
if (index === -1) {
|
|
1446
|
+
const nextMatch = target.findIndex((existing, position) => position >= cursor
|
|
1447
|
+
&& incoming.slice(incomingIndex + 1).some(next => jsonValuesEqual(contentIdentity(existing), contentIdentity(next))))
|
|
1448
|
+
const insertion = nextMatch === -1 ? target.length : nextMatch
|
|
1449
|
+
target.splice(insertion, 0, block)
|
|
1450
|
+
cursor = insertion + 1
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
cursor = index + 1
|
|
1454
|
+
const existing = target[index]!
|
|
1455
|
+
if (block.type !== 'json' && existing.type !== 'json') {
|
|
1456
|
+
target[index] = removeUndefined({
|
|
1457
|
+
...block,
|
|
1458
|
+
annotations: block.annotations ?? existing.annotations,
|
|
1459
|
+
extras: existing.extras || block.extras ? { ...existing.extras, ...block.extras } : undefined,
|
|
1460
|
+
})
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
target[index] = block
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function contentIdentity(content: AgentToolContent): JsonValue {
|
|
1470
|
+
return Object.fromEntries(Object.entries(content).filter(([key]) => key !== 'extras' && key !== 'annotations'))
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function resourceLinkContent(value: JsonValue): AgentToolContent {
|
|
1474
|
+
if (isJsonObject(value) && typeof value.name === 'string' && typeof value.uri === 'string')
|
|
1475
|
+
return mcpToolContent({ ...value, type: 'resource_link' })
|
|
1476
|
+
return jsonToolContent(value)
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function jsonValuesEqual(left: JsonValue | undefined, right: JsonValue | undefined): boolean {
|
|
1480
|
+
if (left === right)
|
|
1481
|
+
return true
|
|
1482
|
+
if (left === undefined || right === undefined || typeof left !== typeof right || left === null || right === null)
|
|
1483
|
+
return false
|
|
1484
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
1485
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
|
|
1486
|
+
return false
|
|
1487
|
+
return left.every((entry, index) => jsonValuesEqual(entry, right[index]))
|
|
1488
|
+
}
|
|
1489
|
+
if (!isJsonObject(left) || !isJsonObject(right))
|
|
1490
|
+
return false
|
|
1491
|
+
const leftKeys = Object.keys(left)
|
|
1492
|
+
const rightKeys = Object.keys(right)
|
|
1493
|
+
return leftKeys.length === rightKeys.length
|
|
1494
|
+
&& leftKeys.every(key => Object.hasOwn(right, key) && jsonValuesEqual(left[key], right[key]))
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function isHostMcpToolName(name: string): boolean {
|
|
1498
|
+
return name.startsWith('mcp__') && !name.startsWith(CLAUDE_RUNTIME_TOOL_PREFIX)
|
|
766
1499
|
}
|
|
767
1500
|
|
|
768
1501
|
function jsonInput(value: unknown): JsonValue {
|