@meistrari/agent-core 0.2.0-experimental.0 → 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 +82 -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 +876 -124
- 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 +922 -91
- 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 +428 -48
- 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 +54 -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 +4 -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/steps/contract.ts +4 -1
- package/src/steps/projector.ts +17 -1
- package/src/supervisor/agent-provider-factory.ts +60 -5
- package/src/supervisor/bootstrap-binder.ts +3 -2
- package/src/supervisor/index.ts +1 -1
- package/src/supervisor/persistence/supervisor-store.ts +22 -8
- package/src/supervisor/runtime-handler.ts +30 -2
- package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
- package/src/supervisor-protocol/bootstrap.ts +4 -1
- package/src/supervisor-protocol/command-body.ts +4 -1
- package/src/supervisor-protocol/envelopes/supervisor-to-control-plane.ts +1 -0
- package/src/testing/in-memory-runtime-control-plane.ts +118 -12
- package/src/testing/index.ts +1 -1
- package/src/worker-runtime-client/command-pump.ts +85 -10
- package/src/worker-runtime-client/connection-attempt.ts +95 -20
- package/src/worker-runtime-client/frame-processor.ts +242 -58
- package/src/worker-runtime-client/index.ts +8 -2
- package/src/worker-runtime-client/ports.ts +34 -2
- package/src/worker-runtime-client/rpc-request-manager.ts +152 -107
- package/src/worker-runtime-client/sandbox-connection-runtime.ts +72 -35
|
@@ -2,16 +2,19 @@ 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
|
+
import { randomUUID } from 'node:crypto'
|
|
8
9
|
import z from 'zod'
|
|
9
10
|
import { jsonValueSchema } from '../../protocol'
|
|
10
11
|
import { canonicalToolName } from '../../protocol/agent-tool-name'
|
|
11
12
|
import { agentWorkStatusSchema } from '../../protocol/agent-work'
|
|
12
13
|
import { createRuntimeTurnId } from '../agent-id'
|
|
13
14
|
import { AgentProviderProtocolError } from '../agent-runtime-error'
|
|
15
|
+
import { isJsonObject, jsonToolContent, mcpToolContent, ownJsonSlot } from '../mcp-tool-content'
|
|
14
16
|
import { optionalNonEmpty, removeUndefined } from '../normalize'
|
|
17
|
+
import { claudeFileChangeText } from './claude-file-change'
|
|
15
18
|
import { CLAUDE_AGENT_TOOL_SERVER } from './claude-tool-mapper'
|
|
16
19
|
|
|
17
20
|
type UserContentBlock = Exclude<MessageParam['content'], string>[number]
|
|
@@ -22,6 +25,18 @@ type ClaudeTaskStartedMessage = Extract<SDKMessage, { type: 'system', subtype: '
|
|
|
22
25
|
type ClaudeTaskProgressMessage = Extract<SDKMessage, { type: 'system', subtype: 'task_progress' }>
|
|
23
26
|
type ClaudeTaskNotificationMessage = Extract<SDKMessage, { type: 'system', subtype: 'task_notification' }>
|
|
24
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'] }
|
|
25
40
|
type ClaudeSubagentActor = Extract<AgentEventActor, { type: 'subagent' }>
|
|
26
41
|
type TaskCreateWorkInput = Pick<TaskCreateInput, 'subject' | 'description'>
|
|
27
42
|
type TaskGetWorkInput = Pick<TaskGetInput, 'taskId'>
|
|
@@ -42,6 +57,11 @@ type ClaudeUnownedTurnEventDraft = ClaudeTurnEventDraft extends infer Draft
|
|
|
42
57
|
? Omit<Draft, 'actor'>
|
|
43
58
|
: never
|
|
44
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' }
|
|
45
65
|
|
|
46
66
|
const mainActor = { type: 'main', actorId: 'main' } satisfies AgentEventActor
|
|
47
67
|
|
|
@@ -95,8 +115,17 @@ const taskListWorkOutputSchema = z.object({
|
|
|
95
115
|
}).passthrough() satisfies z.ZodType<TaskListWorkOutput>
|
|
96
116
|
|
|
97
117
|
export interface ClaudeEventMapperState {
|
|
118
|
+
readonly usageScopeId: string
|
|
98
119
|
readonly toolNameById: Map<string, string>
|
|
99
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>
|
|
100
129
|
readonly startedMessageIds: Set<string>
|
|
101
130
|
readonly endedMessageIds: Set<string>
|
|
102
131
|
readonly streamedTextByMessageId: Map<string, string>
|
|
@@ -104,12 +133,39 @@ export interface ClaudeEventMapperState {
|
|
|
104
133
|
readonly streamedReasoningByMessageId: Map<string, string>
|
|
105
134
|
readonly contentBlockTypeByIndex: Map<number, string>
|
|
106
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>
|
|
107
144
|
readonly actorByMessageId: Map<string, AgentEventActor>
|
|
145
|
+
readonly currentMessageIdByActorId: Map<string, string>
|
|
146
|
+
readonly contentBlockTypeByActorId: Map<string, Map<number, string>>
|
|
108
147
|
readonly startedReasoningIds: Set<string>
|
|
109
148
|
readonly endedReasoningIds: Set<string>
|
|
110
149
|
turnStarted: boolean
|
|
111
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
|
|
112
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' }>
|
|
113
169
|
currentTurnId?: string
|
|
114
170
|
currentCommandId?: string
|
|
115
171
|
currentMessageId?: string
|
|
@@ -118,8 +174,15 @@ export interface ClaudeEventMapperState {
|
|
|
118
174
|
|
|
119
175
|
export function createClaudeEventMapperState(): ClaudeEventMapperState {
|
|
120
176
|
return {
|
|
177
|
+
usageScopeId: `claude-query:${randomUUID()}`,
|
|
121
178
|
toolNameById: new Map(),
|
|
122
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(),
|
|
123
186
|
startedMessageIds: new Set(),
|
|
124
187
|
endedMessageIds: new Set(),
|
|
125
188
|
streamedTextByMessageId: new Map(),
|
|
@@ -127,106 +190,230 @@ export function createClaudeEventMapperState(): ClaudeEventMapperState {
|
|
|
127
190
|
streamedReasoningByMessageId: new Map(),
|
|
128
191
|
contentBlockTypeByIndex: new Map(),
|
|
129
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(),
|
|
130
200
|
actorByMessageId: new Map(),
|
|
201
|
+
currentMessageIdByActorId: new Map(),
|
|
202
|
+
contentBlockTypeByActorId: new Map(),
|
|
131
203
|
startedReasoningIds: new Set(),
|
|
132
204
|
endedReasoningIds: new Set(),
|
|
133
205
|
turnStarted: false,
|
|
134
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,
|
|
135
213
|
}
|
|
136
214
|
}
|
|
137
215
|
|
|
138
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 {
|
|
139
226
|
const drafts: AgentEventDraft[] = []
|
|
140
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
|
+
|
|
141
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
|
|
142
246
|
if (message.subtype === 'session_state_changed' && message.state === 'running')
|
|
143
247
|
ensureTurnStarted(drafts, state)
|
|
144
|
-
if (message.subtype === 'session_state_changed' && message.state === 'idle')
|
|
248
|
+
if (message.subtype === 'session_state_changed' && message.state === 'idle') {
|
|
249
|
+
drafts.push(...flushHeldTurnEnd(state))
|
|
145
250
|
ensureSessionIdle(drafts, state)
|
|
251
|
+
}
|
|
146
252
|
if (message.subtype === 'status')
|
|
147
253
|
drafts.push(...mapStatusMessage(message, state))
|
|
148
254
|
if (message.subtype === 'compact_boundary')
|
|
149
255
|
drafts.push(...mapCompactBoundaryMessage(message, state))
|
|
150
|
-
if (message.subtype === 'task_started')
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
}
|
|
156
274
|
return drafts
|
|
157
275
|
}
|
|
158
276
|
|
|
159
277
|
if (message.type === 'assistant') {
|
|
160
|
-
const
|
|
161
|
-
|
|
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
|
+
}
|
|
162
286
|
ensureAssistantMessageStarted(drafts, state, message.message, turnId, actor)
|
|
163
|
-
|
|
287
|
+
if (actor.type === 'main')
|
|
288
|
+
reportContextSnapshot({ drafts, state, turnId })
|
|
289
|
+
drafts.push(...mapAssistantContent(message.message, state, turnId, actor, actor.type === 'main'))
|
|
164
290
|
if (message.message.stop_reason !== null || shouldEndAssistantSnapshot(message.message, state, actor))
|
|
165
291
|
endAssistantMessage(drafts, state, message.message, turnId, actor)
|
|
166
292
|
return drafts
|
|
167
293
|
}
|
|
168
294
|
|
|
169
295
|
if (message.type === 'stream_event') {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
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))
|
|
173
302
|
return drafts
|
|
174
303
|
}
|
|
175
304
|
|
|
176
305
|
if (message.type === 'user') {
|
|
177
|
-
const
|
|
178
|
-
|
|
306
|
+
const context = actorContextFromParentToolUseId(message.parent_tool_use_id, drafts, state)
|
|
307
|
+
if (!context)
|
|
308
|
+
return undefined
|
|
309
|
+
const { actor, turnId } = context
|
|
179
310
|
const content = message.message.content
|
|
180
311
|
const messageId = message.uuid ?? `user-${turnId}`
|
|
181
312
|
if (typeof content === 'string')
|
|
182
|
-
return actor ? subagentUserMessageEvents({ messageId, text: content, turnId, actor }) : drafts
|
|
183
|
-
if (actor)
|
|
313
|
+
return actor.type === 'subagent' ? subagentUserMessageEvents({ messageId, text: content, turnId, actor }) : drafts
|
|
314
|
+
if (actor.type === 'subagent')
|
|
184
315
|
drafts.push(...subagentUserContentEvents({ messageId, content, turnId, actor }))
|
|
185
|
-
|
|
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
|
+
})))
|
|
186
328
|
return drafts
|
|
187
329
|
}
|
|
188
330
|
|
|
189
331
|
if (message.type === 'result') {
|
|
190
332
|
const turnId = ensureTurnStarted(drafts, state)
|
|
333
|
+
correlateConsumedPrompts({ message, turnId, drafts, state })
|
|
191
334
|
const usage = usageFromObject(message.usage)
|
|
192
335
|
// Claude Agent SDK result usage is per completed ask/turn for the stream-json path we use,
|
|
193
336
|
// not a monotonic counter across the long-lived query. Emit it directly so summing
|
|
194
337
|
// normalized usage events gives the run total; do not diff against prior results.
|
|
195
|
-
|
|
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) {
|
|
196
345
|
drafts.push({ type: 'usage', turnId, actor: mainActor, payload: { usage, accounting: {
|
|
197
346
|
observationId: message.uuid,
|
|
198
|
-
mode: 'increment',
|
|
199
|
-
scopeId: turnId,
|
|
347
|
+
mode: Object.keys(message.modelUsage ?? {}).length ? 'cumulative' : 'increment',
|
|
348
|
+
scopeId: Object.keys(message.modelUsage ?? {}).length ? state.usageScopeId : turnId,
|
|
200
349
|
includesChildren: true,
|
|
201
350
|
models: Object.fromEntries(Object.entries(message.modelUsage ?? {}).map(([model, counts]) => [model, {
|
|
202
351
|
inputTokens: counts.inputTokens,
|
|
203
352
|
outputTokens: counts.outputTokens,
|
|
204
353
|
cacheReadTokens: counts.cacheReadInputTokens,
|
|
205
354
|
cacheWriteTokens: counts.cacheCreationInputTokens,
|
|
355
|
+
webSearchRequests: counts.webSearchRequests,
|
|
206
356
|
}])),
|
|
207
357
|
} } })
|
|
208
358
|
}
|
|
359
|
+
reportContextSnapshot({ drafts, state, turnId, result: message })
|
|
209
360
|
endOpenReasoning(drafts, state, turnId)
|
|
210
361
|
const interruptAccepted = state.interrupt.status === 'accepted' && state.interrupt.turnId === turnId
|
|
211
|
-
|
|
212
|
-
|
|
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 }) }
|
|
213
364
|
state.pendingWorkCallById.clear()
|
|
214
|
-
state.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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)
|
|
218
371
|
state.currentMessageId = undefined
|
|
372
|
+
state.contentBlockTypeByIndex.clear()
|
|
219
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)
|
|
220
384
|
return drafts
|
|
221
385
|
}
|
|
222
386
|
|
|
223
387
|
return drafts
|
|
224
388
|
}
|
|
225
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
|
+
|
|
226
411
|
function mapStatusMessage(message: ClaudeStatusMessage, state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
227
412
|
const drafts: AgentEventDraft[] = []
|
|
228
413
|
if (message.status === 'compacting') {
|
|
229
414
|
const turnId = ensureTurnStarted(drafts, state)
|
|
415
|
+
state.contextObservation = { status: 'none' }
|
|
416
|
+
state.reportedContext = undefined
|
|
230
417
|
if (state.currentCompactionId)
|
|
231
418
|
return drafts
|
|
232
419
|
state.currentCompactionId = message.uuid
|
|
@@ -245,7 +432,7 @@ function mapStatusMessage(message: ClaudeStatusMessage, state: ClaudeEventMapper
|
|
|
245
432
|
compactionId,
|
|
246
433
|
trigger: 'unknown',
|
|
247
434
|
status: 'failed',
|
|
248
|
-
error:
|
|
435
|
+
error: compactionError(message),
|
|
249
436
|
},
|
|
250
437
|
})
|
|
251
438
|
state.currentCompactionId = undefined
|
|
@@ -254,13 +441,44 @@ function mapStatusMessage(message: ClaudeStatusMessage, state: ClaudeEventMapper
|
|
|
254
441
|
return drafts
|
|
255
442
|
}
|
|
256
443
|
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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 [{
|
|
264
482
|
type: 'subagent.started',
|
|
265
483
|
turnId,
|
|
266
484
|
actor,
|
|
@@ -268,51 +486,134 @@ function mapTaskStartedMessage(message: ClaudeTaskStartedMessage, state: ClaudeE
|
|
|
268
486
|
...subagentPayloadRef(actor),
|
|
269
487
|
description: message.description,
|
|
270
488
|
}),
|
|
271
|
-
}
|
|
272
|
-
return drafts
|
|
489
|
+
}]
|
|
273
490
|
}
|
|
274
491
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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',
|
|
281
526
|
turnId,
|
|
282
527
|
actor,
|
|
283
528
|
payload: removeUndefined({
|
|
284
529
|
...subagentPayloadRef(actor),
|
|
285
|
-
|
|
286
|
-
lastToolName: optionalNonEmpty(message.last_tool_name),
|
|
287
|
-
usage: usageFromTaskUsage(message.usage),
|
|
530
|
+
description: message.description,
|
|
288
531
|
}),
|
|
289
|
-
}
|
|
290
|
-
return drafts
|
|
532
|
+
}]
|
|
291
533
|
}
|
|
292
534
|
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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',
|
|
299
552
|
turnId,
|
|
300
553
|
actor,
|
|
301
554
|
payload: removeUndefined({
|
|
302
555
|
...subagentPayloadRef(actor),
|
|
303
|
-
status: taskNotificationStatus(message.status),
|
|
304
556
|
summary: optionalNonEmpty(message.summary),
|
|
305
|
-
|
|
306
|
-
|
|
557
|
+
lastToolName: optionalNonEmpty(message.last_tool_name),
|
|
558
|
+
usage: usageFromTaskUsage(message.usage),
|
|
307
559
|
}),
|
|
308
|
-
}
|
|
309
|
-
|
|
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
|
+
]
|
|
310
609
|
}
|
|
311
610
|
|
|
312
611
|
function mapCompactBoundaryMessage(message: ClaudeCompactBoundaryMessage, state: ClaudeEventMapperState): AgentEventDraft[] {
|
|
313
612
|
const drafts: AgentEventDraft[] = []
|
|
314
613
|
const turnId = ensureTurnStarted(drafts, state)
|
|
315
614
|
const compactionId = state.currentCompactionId ?? message.uuid
|
|
615
|
+
state.contextObservation = { status: 'none' }
|
|
616
|
+
state.reportedContext = undefined
|
|
316
617
|
drafts.push({
|
|
317
618
|
type: 'context.compaction.completed',
|
|
318
619
|
turnId,
|
|
@@ -330,7 +631,7 @@ function mapCompactBoundaryMessage(message: ClaudeCompactBoundaryMessage, state:
|
|
|
330
631
|
return drafts
|
|
331
632
|
}
|
|
332
633
|
|
|
333
|
-
function mapAssistantContent(message: BetaMessage, state: ClaudeEventMapperState, turnId: string, actor: AgentEventActor
|
|
634
|
+
function mapAssistantContent(message: BetaMessage, state: ClaudeEventMapperState, turnId: string, actor: AgentEventActor, isMain: boolean): AgentEventDraft[] {
|
|
334
635
|
const drafts: AgentEventDraft[] = []
|
|
335
636
|
const streamedText = state.streamedTextByMessageId.has(message.id)
|
|
336
637
|
const streamedReasoning = state.streamedReasoningMessageIds.has(message.id)
|
|
@@ -347,6 +648,12 @@ function mapAssistantContent(message: BetaMessage, state: ClaudeEventMapperState
|
|
|
347
648
|
if (block.type === 'tool_use') {
|
|
348
649
|
const toolName = normalizeToolName(block.name)
|
|
349
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 })
|
|
350
657
|
if (isMain) {
|
|
351
658
|
const pendingWorkCall = pendingClaudeWorkCall(block.name, block.input)
|
|
352
659
|
if (pendingWorkCall)
|
|
@@ -358,27 +665,53 @@ function mapAssistantContent(message: BetaMessage, state: ClaudeEventMapperState
|
|
|
358
665
|
return drafts
|
|
359
666
|
}
|
|
360
667
|
|
|
361
|
-
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
|
|
362
679
|
if (block.type !== 'tool_result')
|
|
363
680
|
return []
|
|
364
681
|
const toolCallId = block.tool_use_id
|
|
365
682
|
const pendingWorkCall = state.pendingWorkCallById.get(toolCallId)
|
|
366
683
|
if (pendingWorkCall)
|
|
367
684
|
state.pendingWorkCallById.delete(toolCallId)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
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
|
+
})
|
|
371
702
|
const drafts: AgentEventDraft[] = []
|
|
372
|
-
if (text)
|
|
373
|
-
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))
|
|
374
705
|
drafts.push(withActor({
|
|
375
706
|
type: 'tool.call.completed',
|
|
376
707
|
turnId,
|
|
377
708
|
payload: removeUndefined({
|
|
378
709
|
toolCallId,
|
|
710
|
+
toolName: state.toolNameById.get(toolCallId),
|
|
379
711
|
status: block.is_error === true ? 'failed' : 'completed',
|
|
380
712
|
output,
|
|
381
|
-
|
|
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,
|
|
382
715
|
}),
|
|
383
716
|
}, actor))
|
|
384
717
|
|
|
@@ -455,20 +788,19 @@ function parseClaudeTaskResult<Output>({ value, toolName, schema }: { value: unk
|
|
|
455
788
|
})
|
|
456
789
|
}
|
|
457
790
|
|
|
458
|
-
function mapStreamEvent(event: BetaRawMessageStreamEvent, state: ClaudeEventMapperState, turnId: string, actor: AgentEventActor
|
|
791
|
+
function mapStreamEvent(event: BetaRawMessageStreamEvent, state: ClaudeEventMapperState, turnId: string, actor: AgentEventActor): AgentEventDraft[] {
|
|
459
792
|
const drafts: AgentEventDraft[] = []
|
|
460
793
|
if (event.type === 'message_start') {
|
|
461
|
-
state
|
|
462
|
-
|
|
463
|
-
state.actorByMessageId.set(event.message.id, actor)
|
|
794
|
+
setCurrentMessageId(state, actor, event.message.id)
|
|
795
|
+
state.actorByMessageId.set(event.message.id, actor)
|
|
464
796
|
ensureAssistantMessageStarted(drafts, state, event.message, turnId, actor)
|
|
465
797
|
return drafts
|
|
466
798
|
}
|
|
467
799
|
|
|
468
800
|
if (event.type === 'content_block_start') {
|
|
469
|
-
const messageId = currentMessageId(state)
|
|
470
|
-
const messageActor =
|
|
471
|
-
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)
|
|
472
804
|
if (event.content_block.type === 'thinking') {
|
|
473
805
|
state.streamedReasoningMessageIds.add(messageId)
|
|
474
806
|
ensureReasoningStarted(drafts, state, turnId, messageId, messageActor)
|
|
@@ -477,30 +809,30 @@ function mapStreamEvent(event: BetaRawMessageStreamEvent, state: ClaudeEventMapp
|
|
|
477
809
|
}
|
|
478
810
|
|
|
479
811
|
if (event.type === 'content_block_stop') {
|
|
480
|
-
const messageId = currentMessageId(state)
|
|
481
|
-
const messageActor =
|
|
482
|
-
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)
|
|
483
816
|
if (blockType === 'thinking')
|
|
484
817
|
endReasoning(drafts, state, turnId, messageId, state.streamedReasoningByMessageId.get(messageId), messageActor)
|
|
485
|
-
|
|
818
|
+
blockTypes.delete(event.index)
|
|
486
819
|
return drafts
|
|
487
820
|
}
|
|
488
821
|
|
|
489
822
|
if (event.type === 'message_stop') {
|
|
490
|
-
const messageId = currentMessageId(state)
|
|
491
|
-
const messageActor =
|
|
823
|
+
const messageId = currentMessageId(state, actor)
|
|
824
|
+
const messageActor = state.actorByMessageId.get(messageId) ?? actor
|
|
492
825
|
endAssistantMessageById(drafts, state, turnId, messageId, state.streamedTextByMessageId.get(messageId), messageActor)
|
|
493
826
|
state.actorByMessageId.delete(messageId)
|
|
494
|
-
state
|
|
495
|
-
state.contentBlockTypeByIndex.clear()
|
|
827
|
+
clearCurrentMessage(state, actor)
|
|
496
828
|
return drafts
|
|
497
829
|
}
|
|
498
830
|
|
|
499
831
|
if (event.type !== 'content_block_delta')
|
|
500
832
|
return drafts
|
|
501
833
|
|
|
502
|
-
const messageId = currentMessageId(state)
|
|
503
|
-
const messageActor =
|
|
834
|
+
const messageId = currentMessageId(state, actor)
|
|
835
|
+
const messageActor = state.actorByMessageId.get(messageId) ?? actor
|
|
504
836
|
const delta = event.delta
|
|
505
837
|
if (delta.type === 'thinking_delta') {
|
|
506
838
|
state.streamedReasoningMessageIds.add(messageId)
|
|
@@ -517,18 +849,64 @@ function mapStreamEvent(event: BetaRawMessageStreamEvent, state: ClaudeEventMapp
|
|
|
517
849
|
}
|
|
518
850
|
|
|
519
851
|
function ensureTurnStarted(drafts: AgentEventDraft[], state: ClaudeEventMapperState): string {
|
|
520
|
-
if (state.turnStarted
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
+
}
|
|
528
860
|
ensureSessionRunning(drafts, state)
|
|
529
861
|
return state.currentTurnId
|
|
530
862
|
}
|
|
531
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
|
+
|
|
532
910
|
function ensureSessionRunning(drafts: AgentEventDraft[], state: ClaudeEventMapperState): void {
|
|
533
911
|
if (state.sessionState === 'running')
|
|
534
912
|
return
|
|
@@ -543,22 +921,49 @@ function ensureSessionIdle(drafts: AgentEventDraft[], state: ClaudeEventMapperSt
|
|
|
543
921
|
drafts.push({ type: 'session.state.changed', payload: { state: 'idle' } })
|
|
544
922
|
}
|
|
545
923
|
|
|
546
|
-
function ensureAssistantMessageStarted(drafts: AgentEventDraft[], state: ClaudeEventMapperState, message: BetaMessage, turnId: string, actor: AgentEventActor
|
|
547
|
-
state
|
|
548
|
-
|
|
549
|
-
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)
|
|
550
927
|
if (state.startedMessageIds.has(message.id))
|
|
551
928
|
return
|
|
552
929
|
state.startedMessageIds.add(message.id)
|
|
553
930
|
drafts.push(withActor({ type: 'message.started', turnId, payload: { messageId: message.id, role: 'assistant' } }, actor))
|
|
554
931
|
}
|
|
555
932
|
|
|
556
|
-
function
|
|
557
|
-
if (
|
|
558
|
-
|
|
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
|
|
559
943
|
throw new AgentProviderProtocolError({ message: 'Claude stream emitted content_block_delta before message_start.' })
|
|
560
944
|
}
|
|
561
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
|
+
|
|
562
967
|
function endAssistantMessage(drafts: AgentEventDraft[], state: ClaudeEventMapperState, message: BetaMessage, turnId: string, actor: AgentEventActor | undefined): void {
|
|
563
968
|
if (state.endedMessageIds.has(message.id))
|
|
564
969
|
return
|
|
@@ -592,18 +997,34 @@ function endReasoning(drafts: AgentEventDraft[], state: ClaudeEventMapperState,
|
|
|
592
997
|
|
|
593
998
|
function endOpenReasoning(drafts: AgentEventDraft[], state: ClaudeEventMapperState, turnId: string): void {
|
|
594
999
|
for (const reasoningId of state.startedReasoningIds) {
|
|
595
|
-
|
|
1000
|
+
const messageId = reasoningId.slice('reasoning-'.length)
|
|
1001
|
+
const actor = state.actorByMessageId.get(messageId)
|
|
1002
|
+
if (actor?.type === 'main' && !state.endedReasoningIds.has(reasoningId)) {
|
|
596
1003
|
state.endedReasoningIds.add(reasoningId)
|
|
597
1004
|
drafts.push({ type: 'reasoning.ended', turnId, actor: mainActor, payload: { reasoningId } })
|
|
598
1005
|
}
|
|
599
1006
|
}
|
|
600
1007
|
}
|
|
601
1008
|
|
|
1009
|
+
function endOpenAssistantMessage(drafts: AgentEventDraft[], state: ClaudeEventMapperState, turnId: string): void {
|
|
1010
|
+
const messageId = state.currentMessageId
|
|
1011
|
+
if (!messageId)
|
|
1012
|
+
return
|
|
1013
|
+
endAssistantMessageById(drafts, state, turnId, messageId, state.streamedTextByMessageId.get(messageId), state.actorByMessageId.get(messageId))
|
|
1014
|
+
state.actorByMessageId.delete(messageId)
|
|
1015
|
+
state.currentMessageId = undefined
|
|
1016
|
+
state.contentBlockTypeByIndex.clear()
|
|
1017
|
+
}
|
|
1018
|
+
|
|
602
1019
|
function reasoningIdForMessage(messageId: string): string {
|
|
603
1020
|
return `reasoning-${messageId}`
|
|
604
1021
|
}
|
|
605
1022
|
|
|
606
|
-
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'] {
|
|
607
1028
|
if (interruptAccepted && message.subtype === 'error_during_execution') {
|
|
608
1029
|
return removeUndefined({
|
|
609
1030
|
status: 'interrupted' as const,
|
|
@@ -624,45 +1045,227 @@ function turnEndedPayload(message: Extract<SDKMessage, { type: 'result' }>, inte
|
|
|
624
1045
|
status: 'failed' as const,
|
|
625
1046
|
reason: message.stop_reason ?? message.subtype,
|
|
626
1047
|
duration: message.duration_ms,
|
|
627
|
-
error: resultError(message),
|
|
1048
|
+
error: usageLimit ? usageLimitError({ error: resultError(message), usageLimit }) : resultError(message),
|
|
628
1049
|
})
|
|
629
1050
|
}
|
|
630
1051
|
|
|
631
|
-
|
|
632
|
-
|
|
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
|
|
633
1153
|
return {
|
|
634
|
-
message:
|
|
1154
|
+
message: detail || `Claude result ended with ${message.subtype}.`,
|
|
635
1155
|
code: message.subtype,
|
|
636
1156
|
}
|
|
637
1157
|
}
|
|
638
1158
|
|
|
639
|
-
function shouldEndAssistantSnapshot(message: BetaMessage, state: ClaudeEventMapperState, actor:
|
|
640
|
-
return actor
|
|
1159
|
+
function shouldEndAssistantSnapshot(message: BetaMessage, state: ClaudeEventMapperState, actor: AgentEventActor): boolean {
|
|
1160
|
+
return actor.type === 'subagent'
|
|
641
1161
|
&& !state.streamedTextByMessageId.has(message.id)
|
|
642
1162
|
&& !state.streamedReasoningMessageIds.has(message.id)
|
|
643
1163
|
}
|
|
644
1164
|
|
|
645
|
-
function
|
|
646
|
-
|
|
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
|
|
647
1172
|
}
|
|
648
1173
|
|
|
649
|
-
function subagentActorForTaskMessage(input: { subagentId: string, parentToolCallId?: string, name?: string }, state: ClaudeEventMapperState):
|
|
650
|
-
const
|
|
651
|
-
|
|
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
|
|
1222
|
+
}
|
|
1223
|
+
|
|
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
|
|
652
1237
|
}
|
|
653
1238
|
|
|
654
|
-
function subagentActorFromTask(input: { subagentId: string, parentToolCallId
|
|
1239
|
+
function subagentActorFromTask(input: { subagentId: string, parentActorId: string, parentToolCallId: string, name?: string }): ClaudeSubagentActor {
|
|
655
1240
|
return removeUndefined({
|
|
656
1241
|
type: 'subagent' as const,
|
|
657
1242
|
actorId: input.subagentId,
|
|
658
1243
|
subagentId: input.subagentId,
|
|
659
|
-
parentActorId:
|
|
660
|
-
origin:
|
|
1244
|
+
parentActorId: input.parentActorId,
|
|
1245
|
+
origin: { type: 'tool_call' as const, toolCallId: input.parentToolCallId },
|
|
661
1246
|
parentToolCallId: input.parentToolCallId,
|
|
662
1247
|
name: input.name,
|
|
663
1248
|
})
|
|
664
1249
|
}
|
|
665
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
|
+
|
|
666
1269
|
function subagentPayloadRef(actor: ClaudeSubagentActor): Pick<ClaudeSubagentActor, 'actorId' | 'subagentId' | 'parentActorId' | 'origin' | 'parentToolCallId' | 'name'> {
|
|
667
1270
|
return removeUndefined({
|
|
668
1271
|
actorId: actor.actorId,
|
|
@@ -714,12 +1317,15 @@ function normalizeToolName(name: string): string {
|
|
|
714
1317
|
function usageFromObject(value: BetaUsage | undefined): AgentUsage | undefined {
|
|
715
1318
|
if (!value)
|
|
716
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)
|
|
717
1323
|
return removeUndefined({
|
|
718
|
-
inputTokens
|
|
1324
|
+
inputTokens,
|
|
719
1325
|
outputTokens: value.output_tokens,
|
|
720
1326
|
cacheReadTokens: value.cache_read_input_tokens ?? undefined,
|
|
721
1327
|
cacheWriteTokens: value.cache_creation_input_tokens ?? undefined,
|
|
722
|
-
totalTokens:
|
|
1328
|
+
totalTokens: inputTokens + value.output_tokens,
|
|
723
1329
|
})
|
|
724
1330
|
}
|
|
725
1331
|
|
|
@@ -733,13 +1339,159 @@ function thinkingFromAssistantContent(content: BetaMessage['content']): string |
|
|
|
733
1339
|
return text || undefined
|
|
734
1340
|
}
|
|
735
1341
|
|
|
736
|
-
function
|
|
737
|
-
if (
|
|
1342
|
+
function correlatedHostTrailerToolCallId(input: { content: UserContentBlock[], message: ClaudeUserMessage, state: ClaudeEventMapperState }): string | undefined {
|
|
1343
|
+
if (input.message.tool_use_result === undefined)
|
|
738
1344
|
return undefined
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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)
|
|
743
1495
|
}
|
|
744
1496
|
|
|
745
1497
|
function jsonInput(value: unknown): JsonValue {
|