@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.
Files changed (71) hide show
  1. package/README.md +50 -0
  2. package/docs/consumer-capability-matrix.md +374 -0
  3. package/package.json +17 -10
  4. package/scripts/generate-codex-protocol.ts +711 -0
  5. package/src/agents/agent-operation.ts +8 -0
  6. package/src/agents/agent-provider.ts +3 -0
  7. package/src/agents/agent-run.ts +14 -2
  8. package/src/agents/agent-runtime-error.ts +28 -0
  9. package/src/agents/agent-session-events.ts +33 -0
  10. package/src/agents/agent-tool-runner.ts +12 -11
  11. package/src/agents/agent-tool.ts +12 -4
  12. package/src/agents/author-context.ts +1 -1
  13. package/src/agents/claude/claude-auth.ts +40 -0
  14. package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
  15. package/src/agents/claude/claude-code-process.ts +98 -0
  16. package/src/agents/claude/claude-command-mapper.ts +63 -3
  17. package/src/agents/claude/claude-desktop.ts +55 -0
  18. package/src/agents/claude/claude-event-mapper.ts +868 -135
  19. package/src/agents/claude/claude-file-change.ts +128 -0
  20. package/src/agents/claude/claude-provider.ts +356 -30
  21. package/src/agents/claude/claude-run.ts +898 -130
  22. package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
  23. package/src/agents/claude/claude-skill-catalog.ts +75 -0
  24. package/src/agents/claude/claude-steered-notifications.ts +180 -0
  25. package/src/agents/claude/claude-tool-mapper.ts +81 -2
  26. package/src/agents/claude/claude-workspace-skills.ts +87 -0
  27. package/src/agents/claude/index.ts +2 -0
  28. package/src/agents/codex/codex-auth-mapper.ts +27 -0
  29. package/src/agents/codex/codex-auth.ts +19 -0
  30. package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
  31. package/src/agents/codex/codex-browser-auth-status.ts +55 -0
  32. package/src/agents/codex/codex-command-mapper.ts +38 -3
  33. package/src/agents/codex/codex-event-mapper.ts +384 -67
  34. package/src/agents/codex/codex-json-rpc-client.ts +66 -21
  35. package/src/agents/codex/codex-provider.ts +118 -236
  36. package/src/agents/codex/codex-run-auth.ts +113 -0
  37. package/src/agents/codex/codex-run.ts +423 -53
  38. package/src/agents/codex/codex-tool-mapper.ts +2 -2
  39. package/src/agents/codex/codex.errors.ts +8 -0
  40. package/src/agents/codex/generated/meta.gen.ts +144 -6
  41. package/src/agents/codex/generated/namespaces.gen.ts +92 -3
  42. package/src/agents/codex/generated/schema.gen.ts +24698 -15638
  43. package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
  44. package/src/agents/index.ts +5 -0
  45. package/src/agents/input-attachment-preparation.ts +13 -0
  46. package/src/agents/mcp-tool-content.ts +82 -0
  47. package/src/agents/model-rebinding-run.ts +14 -3
  48. package/src/agents/selected-repository-instructions.ts +202 -0
  49. package/src/agents/session-context.ts +41 -0
  50. package/src/agents/subscription-token-refresh.ts +73 -0
  51. package/src/agents/user-input-request.ts +19 -20
  52. package/src/errors/index.ts +1 -1
  53. package/src/errors/validation-error.ts +6 -0
  54. package/src/protocol/agent-catalog.ts +159 -0
  55. package/src/protocol/agent-command.ts +52 -7
  56. package/src/protocol/agent-content.ts +36 -16
  57. package/src/protocol/agent-event.ts +210 -73
  58. package/src/protocol/agent-identity.ts +29 -0
  59. package/src/protocol/agent-json.ts +122 -8
  60. package/src/protocol/agent-metadata.ts +2 -2
  61. package/src/protocol/agent-model.ts +13 -1
  62. package/src/protocol/agent-notification.ts +110 -0
  63. package/src/protocol/agent-presentation.ts +71 -20
  64. package/src/protocol/agent-tool.ts +66 -10
  65. package/src/protocol/agent-usage.ts +3 -1
  66. package/src/protocol/agent-user-input.ts +4 -4
  67. package/src/protocol/agent-work.ts +12 -10
  68. package/src/protocol/index.ts +2 -0
  69. package/src/protocol/rejected-keys.ts +11 -0
  70. package/src/provenance.gen.ts +3 -3
  71. package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
@@ -1,15 +1,17 @@
1
- import type { AgentError, AgentEventActor, AgentMessageRole, AgentSubagentStatus, AgentToolCallStatus, AgentToolContent, AgentToolResult, AgentTurnEndStatus, AgentUsage, JsonObject, JsonValue } from '../../protocol'
1
+ import type { AgentContextSnapshot, AgentError, AgentEventActor, AgentMessageRole, AgentSubagentStatus, AgentToolCallStatus, AgentToolContent, AgentToolResult, AgentTurnEndStatus, AgentUsage, AgentUsageAccounting, JsonObject, JsonValue } from '../../protocol'
2
2
  import type { AgentWorkStatus } from '../../protocol/agent-work'
3
3
  import type { AgentEventDraft } from '../agent-event-stream'
4
4
  import type { CodexJsonRpcMessage, CodexServerNotification } from './codex-json-rpc-client'
5
5
  import type { ServerNotificationParamsByMethod } from './codex-protocol'
6
- import { createHash } from 'node:crypto'
6
+ import { createHash, randomUUID } from 'node:crypto'
7
7
  import { jsonObjectSchema, jsonValueSchema } from '../../protocol'
8
8
  import { CANONICAL_TOOL } from '../../protocol/agent-tool-name'
9
9
  import { createRuntimeWorkItemId } from '../agent-id'
10
10
  import { AgentProviderProtocolError } from '../agent-runtime-error'
11
11
  import { stripAuthorContext } from '../author-context'
12
+ import { jsonToolContent, mcpToolContent, ownJsonSlot } from '../mcp-tool-content'
12
13
  import { optionalNonEmpty, removeUndefined } from '../normalize'
14
+ import { stripSessionContext } from '../session-context'
13
15
  import { isCodexServerNotification } from './codex-json-rpc-client'
14
16
 
15
17
  export type CodexNotification = CodexServerNotification
@@ -26,6 +28,7 @@ type CodexUserMessageItem = Extract<CodexCompletedItem, { type: 'userMessage' }>
26
28
  type CodexHookPromptItem = Extract<CodexCompletedItem, { type: 'hookPrompt' }>
27
29
  type CodexDynamicToolCompletedItem = Extract<CodexCompletedItem, { type: 'dynamicToolCall' }>
28
30
  type CodexDynamicToolContentItem = NonNullable<CodexDynamicToolCompletedItem['contentItems']>[number]
31
+ type CodexMcpToolCompletedItem = Extract<CodexCompletedItem, { type: 'mcpToolCall' }>
29
32
  type CodexCollabAgentToolCompletedItem = Extract<CodexCompletedItem, { type: 'collabAgentToolCall' }>
30
33
  type CodexCollabAgentToolStartedItem = Extract<CodexStartedItem, { type: 'collabAgentToolCall' }>
31
34
  type CodexSubAgentActivityCompletedItem = Extract<CodexCompletedItem, { type: 'subAgentActivity' }>
@@ -36,6 +39,8 @@ type CodexTurn = ServerNotificationParamsByMethod['turn/completed']['turn']
36
39
  type CodexTurnError = NonNullable<CodexTurn['error']>
37
40
  type CodexSubagentRef = Extract<AgentEventActor, { type: 'subagent' }>
38
41
  interface CodexPromptCommandRef { commandId: string, origin: 'user' | 'supervisor' }
42
+ type CodexMainTurnState = { status: 'closed' } | { status: 'open', turnId: string }
43
+ type CodexContextSnapshotBaseline = { status: 'empty' } | { status: 'reported', snapshot: AgentContextSnapshot }
39
44
  type CodexTurnEventDraft = Extract<AgentEventDraft, { turnId: string }>
40
45
  type CodexUnownedTurnEventDraft = CodexTurnEventDraft extends infer Draft
41
46
  ? Draft extends CodexTurnEventDraft
@@ -44,6 +49,9 @@ type CodexUnownedTurnEventDraft = CodexTurnEventDraft extends infer Draft
44
49
  : never
45
50
 
46
51
  const mainActor = { type: 'main', actorId: 'main' } satisfies AgentEventActor
52
+ // Codex's own remaining-context formula (codex-rs `protocol.rs` / `tui/token_usage.rs`, 0.153.4)
53
+ // subtracts a fixed 12,000-token baseline from both the used count and the capacity.
54
+ const CODEX_CONTEXT_BASELINE_TOKENS = 12_000
47
55
 
48
56
  export interface CodexEventMapperState {
49
57
  readonly endedSubagentIds: Set<string>
@@ -56,11 +64,37 @@ export interface CodexEventMapperState {
56
64
  readonly startedReasoningIds: Set<string>
57
65
  readonly reasoningSummaryById: Map<string, Map<number, string>>
58
66
  readonly promptCommandByClientUserMessageId: Map<string, CodexPromptCommandRef>
67
+ readonly seenMainTurnIds: Set<string>
68
+ readonly openContextCompactionIds: Set<string>
69
+ readonly closedContextCompactionIds: Set<string>
70
+ currentMainTurn: CodexMainTurnState
71
+ contextSnapshotBaseline: CodexContextSnapshotBaseline
59
72
  mainThreadId?: string
73
+ // Usage accounting scope owned by this run; see codexUsageAccounting.
74
+ readonly usageScopeInstanceId: string
75
+ readonly usageAccountingByThreadId: Map<string, { total: string, usage: AgentUsage }>
60
76
  }
61
77
 
62
78
  export function createCodexEventMapperState(): CodexEventMapperState {
63
- return { endedSubagentIds: new Set(), subagentByThreadId: new Map(), subagentById: new Map(), subagentNameByThreadId: new Map(), pendingDraftsByThreadId: new Map(), turnErrorsById: new Map(), startedTurnIds: new Set(), startedReasoningIds: new Set(), reasoningSummaryById: new Map(), promptCommandByClientUserMessageId: new Map() }
79
+ return {
80
+ endedSubagentIds: new Set(),
81
+ subagentByThreadId: new Map(),
82
+ subagentById: new Map(),
83
+ subagentNameByThreadId: new Map(),
84
+ pendingDraftsByThreadId: new Map(),
85
+ turnErrorsById: new Map(),
86
+ startedTurnIds: new Set(),
87
+ startedReasoningIds: new Set(),
88
+ reasoningSummaryById: new Map(),
89
+ promptCommandByClientUserMessageId: new Map(),
90
+ seenMainTurnIds: new Set(),
91
+ openContextCompactionIds: new Set(),
92
+ closedContextCompactionIds: new Set(),
93
+ currentMainTurn: { status: 'closed' },
94
+ contextSnapshotBaseline: { status: 'empty' },
95
+ usageScopeInstanceId: randomUUID(),
96
+ usageAccountingByThreadId: new Map(),
97
+ }
64
98
  }
65
99
 
66
100
  export function codexNotificationFromMessage(message: CodexJsonRpcMessage): CodexNotification | null {
@@ -79,13 +113,14 @@ export function mapCodexNotification(notification: CodexNotification, state: Cod
79
113
  cacheThreadMetadata(notification.params.thread, state)
80
114
  return []
81
115
  case 'turn/started':
82
- state.mainThreadId ??= notification.params.threadId
83
116
  state.startedTurnIds.add(notification.params.turn.id)
117
+ observeMainTurnStarted({ threadId: notification.params.threadId, turnId: notification.params.turn.id, state })
84
118
  return emitOrBufferByThread({ draft: { type: 'turn.started', turnId: notification.params.turn.id, payload: {} }, threadId: notification.params.threadId, state })
85
119
  case 'turn/completed': {
86
120
  if (!state.startedTurnIds.has(notification.params.turn.id))
87
121
  return []
88
122
  const payload = turnEndedPayload(notification.params.turn, state)
123
+ observeMainTurnCompleted({ threadId: notification.params.threadId, turnId: notification.params.turn.id, state })
89
124
  return [
90
125
  ...mapChildTurnCompleted({ threadId: notification.params.threadId, turnId: notification.params.turn.id, turnPayload: payload, state }),
91
126
  ...emitOrBufferByThread({ draft: { type: 'turn.ended', turnId: notification.params.turn.id, payload }, threadId: notification.params.threadId, state }),
@@ -95,21 +130,21 @@ export function mapCodexNotification(notification: CodexNotification, state: Cod
95
130
  if (!state.startedTurnIds.has(notification.params.turnId))
96
131
  return []
97
132
  const usage = usageFromTokenUsage(notification.params.tokenUsage)
98
- if (!usage)
99
- return []
100
- return emitOrBufferByThread({ draft: { type: 'usage', turnId: notification.params.turnId, payload: {
101
- usage,
102
- accounting: {
103
- observationId: createHash('sha256').update(notification.params.threadId).update(JSON.stringify(notification.params.tokenUsage.total)).digest('hex'),
104
- mode: 'cumulative',
105
- scopeId: notification.params.threadId,
106
- includesChildren: false,
107
- usage: {
108
- ...usageFromCounts(notification.params.tokenUsage.total),
109
- inputTokens: Math.max(0, notification.params.tokenUsage.total.inputTokens - notification.params.tokenUsage.total.cachedInputTokens),
110
- },
111
- },
112
- } }, threadId: notification.params.threadId, state })
133
+ const usageDrafts = usage
134
+ ? emitOrBufferByThread({ draft: { type: 'usage', turnId: notification.params.turnId, payload: {
135
+ usage,
136
+ accounting: codexUsageAccounting(notification.params.threadId, notification.params.tokenUsage, state),
137
+ } }, threadId: notification.params.threadId, state })
138
+ : []
139
+ return [
140
+ ...usageDrafts,
141
+ ...mapContextSnapshotFromTokenUsage({
142
+ threadId: notification.params.threadId,
143
+ turnId: notification.params.turnId,
144
+ tokenUsage: notification.params.tokenUsage,
145
+ state,
146
+ }),
147
+ ]
113
148
  }
114
149
  case 'turn/plan/updated':
115
150
  if (!state.startedTurnIds.has(notification.params.turnId) || notification.params.threadId !== state.mainThreadId)
@@ -134,7 +169,9 @@ export function mapCodexNotification(notification: CodexNotification, state: Cod
134
169
  return []
135
170
  if (!notification.params.willRetry)
136
171
  state.turnErrorsById.set(notification.params.turnId, agentErrorFromCodexTurnError(notification.params.error))
137
- return []
172
+ return codexErrorCode(notification.params.error.codexErrorInfo) === 'contextWindowExceeded'
173
+ ? reportContextSnapshot({ threadId: notification.params.threadId, turnId: notification.params.turnId, snapshot: { status: 'unknown' }, state })
174
+ : []
138
175
  case 'item/agentMessage/delta':
139
176
  if (!state.startedTurnIds.has(notification.params.turnId))
140
177
  return []
@@ -157,6 +194,10 @@ export function mapCodexNotification(notification: CodexNotification, state: Cod
157
194
  if (!state.startedTurnIds.has(notification.params.turnId))
158
195
  return []
159
196
  return emitOrBufferByThread({ draft: { type: 'tool.output.delta', turnId: notification.params.turnId, payload: { toolCallId: notification.params.itemId, delta: notification.params.delta } }, threadId: notification.params.threadId, state })
197
+ case 'item/mcpToolCall/progress':
198
+ if (!state.startedTurnIds.has(notification.params.turnId))
199
+ return []
200
+ return emitOrBufferByThread({ draft: { type: 'tool.output.delta', turnId: notification.params.turnId, payload: { toolCallId: notification.params.itemId, delta: notification.params.message } }, threadId: notification.params.threadId, state })
160
201
  case 'item/started':
161
202
  if (!state.startedTurnIds.has(notification.params.turnId))
162
203
  return []
@@ -210,27 +251,55 @@ function mapItemStarted({ turnId, threadId, item, state }: { turnId: string, thr
210
251
  if (toolCall)
211
252
  return emitOrBufferByThread({ draft: { type: 'tool.call.started', turnId, payload: { toolCallId: item.id, toolName: toolCall.toolName, input: toolCall.input } }, threadId, state })
212
253
 
213
- if (item.type === 'contextCompaction')
254
+ if (item.type === 'contextCompaction') {
255
+ observeContextCompactionStarted({ threadId, turnId, compactionId: item.id, state })
214
256
  return emitOrBufferByThread({ draft: { type: 'context.compaction.started', turnId, payload: { compactionId: item.id, trigger: 'unknown' } }, threadId, state })
257
+ }
215
258
 
216
259
  return []
217
260
  }
218
261
 
219
262
  function toolCallStart(item: CodexStartedItem): { toolName: string, input: JsonValue } | undefined {
263
+ const toolName = toolNameForItem(item)
264
+ if (!toolName)
265
+ return undefined
266
+
267
+ if (item.type === 'commandExecution')
268
+ return { toolName, input: toJsonObject({ command: Array.isArray(item.command) ? item.command.join(' ') : String(item.command ?? ''), cwd: item.cwd }) }
269
+ if (item.type === 'webSearch')
270
+ return { toolName, input: toJsonObject({ query: optionalNonEmpty(item.query) }) }
271
+ if (item.type === 'fileChange')
272
+ return { toolName, input: jsonInput({ files: item.changes.map(change => removeUndefined({ path: change.path, kind: change.kind?.type })) }) }
273
+ if (item.type === 'imageView')
274
+ return { toolName, input: toJsonObject({ path: item.path }) }
275
+ if (item.type === 'imageGeneration')
276
+ return { toolName, input: toJsonObject({ revisedPrompt: optionalNonEmpty(item.revisedPrompt) }) }
277
+ if (item.type === 'dynamicToolCall')
278
+ return { toolName, input: jsonInput(item.arguments) }
279
+ if (item.type === 'mcpToolCall')
280
+ return { toolName, input: jsonInput(item.arguments) }
281
+ if (item.type === 'collabAgentToolCall')
282
+ return { toolName, input: collabToolInput(item) }
283
+ return undefined
284
+ }
285
+
286
+ function toolNameForItem(item: CodexStartedItem | CodexCompletedItem): string | undefined {
220
287
  if (item.type === 'commandExecution')
221
- return { toolName: CANONICAL_TOOL.bash, input: toJsonObject({ command: Array.isArray(item.command) ? item.command.join(' ') : String(item.command ?? ''), cwd: item.cwd }) }
288
+ return CANONICAL_TOOL.bash
222
289
  if (item.type === 'webSearch')
223
- return { toolName: CANONICAL_TOOL.webSearch, input: toJsonObject({ query: optionalNonEmpty(item.query) }) }
290
+ return CANONICAL_TOOL.webSearch
224
291
  if (item.type === 'fileChange')
225
- return { toolName: CANONICAL_TOOL.edit, input: jsonInput({ files: item.changes.map(change => removeUndefined({ path: change.path, kind: change.kind?.type })) }) }
292
+ return CANONICAL_TOOL.edit
226
293
  if (item.type === 'imageView')
227
- return { toolName: CANONICAL_TOOL.viewImage, input: toJsonObject({ path: item.path }) }
294
+ return CANONICAL_TOOL.viewImage
228
295
  if (item.type === 'imageGeneration')
229
- return { toolName: CANONICAL_TOOL.imageGeneration, input: toJsonObject({ revisedPrompt: optionalNonEmpty(item.revisedPrompt) }) }
296
+ return CANONICAL_TOOL.imageGeneration
230
297
  if (item.type === 'dynamicToolCall')
231
- return { toolName: item.tool, input: jsonInput(item.arguments) }
298
+ return item.tool
299
+ if (item.type === 'mcpToolCall')
300
+ return `mcp:${item.server}/${item.tool}`
232
301
  if (item.type === 'collabAgentToolCall')
233
- return { toolName: item.tool, input: collabToolInput(item) }
302
+ return item.tool
234
303
  return undefined
235
304
  }
236
305
 
@@ -259,8 +328,10 @@ function mapItemCompleted({ turnId, threadId, item, state }: { turnId: string, t
259
328
  if (item.type === 'subAgentActivity')
260
329
  return mapSubAgentActivityCompleted({ turnId, threadId, item, state })
261
330
 
262
- if (item.type === 'contextCompaction')
331
+ if (item.type === 'contextCompaction') {
332
+ observeContextCompactionCompleted({ threadId, turnId, compactionId: item.id, state })
263
333
  return emitOrBufferByThread({ draft: { type: 'context.compaction.completed', turnId, payload: { compactionId: item.id, trigger: 'unknown', status: 'completed' } }, threadId, state })
334
+ }
264
335
 
265
336
  if (item.type === 'reasoning')
266
337
  return mapReasoningCompleted({ turnId, threadId, item, state })
@@ -310,26 +381,54 @@ function messageForItem(item: CodexStartedItem | CodexCompletedItem, state: Code
310
381
  return removeUndefined({ role, commandId: promptCommand?.commandId })
311
382
  }
312
383
 
313
- function toolCallCompletion(item: CodexCompletedItem): { status: AgentToolCallStatus, output?: AgentToolResult, error?: AgentError } | undefined {
384
+ function toolCallCompletion(item: CodexCompletedItem): { toolName: string, status: AgentToolCallStatus, output?: AgentToolResult, error?: AgentError } | undefined {
385
+ const toolName = toolNameForItem(item)
386
+ if (!toolName)
387
+ return undefined
388
+
314
389
  if (item.type === 'webSearch')
315
- return { status: 'completed', output: webSearchResult(item) }
390
+ return { toolName, status: 'completed', output: webSearchResult(item) }
316
391
  if (item.type === 'fileChange') {
317
392
  const status = fileChangeStatus(item.status)
318
- return { status, output: fileChangeResult(item, status), error: status === 'failed' ? { message: 'Codex failed to apply the patch.' } : undefined }
393
+ return { toolName, status, output: fileChangeResult(item, status), error: status === 'failed' ? { message: 'Codex failed to apply the patch.' } : undefined }
319
394
  }
320
395
  if (item.type === 'imageView')
321
- return { status: 'completed', output: { success: true, content: [{ type: 'text', text: item.path }] } }
396
+ return { toolName, status: 'completed', output: { success: true, content: [{ type: 'text', text: item.path }] } }
322
397
  if (item.type === 'imageGeneration')
323
- return { status: item.status === 'failed' ? 'failed' : 'completed', output: imageGenerationResult(item) }
398
+ return { toolName, status: item.status === 'failed' ? 'failed' : 'completed', output: imageGenerationResult(item) }
324
399
  if (item.type === 'commandExecution') {
325
- const status = item.exitCode === undefined || item.exitCode === null || item.exitCode === 0 ? 'completed' : 'failed'
400
+ const status = item.status === 'declined'
401
+ ? 'cancelled'
402
+ : item.status !== 'completed' || (item.exitCode != null && item.exitCode !== 0) ? 'failed' : 'completed'
326
403
  const error = 'error' in item && typeof item.error === 'string'
327
404
  ? { message: item.error }
328
- : status === 'failed' ? { message: `Command exited with code ${item.exitCode}.` } : undefined
329
- return { status, output: commandExecutionResult(item, status), error }
405
+ : status === 'failed'
406
+ ? { message: item.exitCode != null && item.exitCode !== 0
407
+ ? `Command exited with code ${item.exitCode}.`
408
+ : item.status === 'inProgress' ? 'Command completion status was inconsistent.' : 'Command execution failed.' }
409
+ : undefined
410
+ return { toolName, status, output: commandExecutionResult(item, status), error }
330
411
  }
331
412
  if (item.type === 'dynamicToolCall')
332
- return { status: item.success === false ? 'failed' : 'completed', output: dynamicToolResult(item) }
413
+ return { toolName, status: item.success === false ? 'failed' : 'completed', output: dynamicToolResult(item) }
414
+ if (item.type === 'mcpToolCall') {
415
+ if (item.status === 'inProgress') {
416
+ return {
417
+ toolName,
418
+ status: 'failed',
419
+ output: mcpToolResult(item, 'failed'),
420
+ error: { message: 'MCP tool result could not be determined because completion status was inconsistent.' },
421
+ }
422
+ }
423
+
424
+ const status = item.status
425
+ return {
426
+ toolName,
427
+ status,
428
+ output: mcpToolResult(item, status),
429
+ error: status === 'failed' ? mcpToolError(item.error?.message) : undefined,
430
+ }
431
+ }
333
432
  return undefined
334
433
  }
335
434
 
@@ -373,9 +472,18 @@ function imageGenerationResult(item: CodexImageGenerationItem): AgentToolResult
373
472
  const revised = optionalNonEmpty(item.revisedPrompt)
374
473
  if (revised)
375
474
  lines.push(`Revised prompt: ${revised}`)
376
- if (!lines.length)
377
- return undefined
378
- return { success: item.status !== 'failed', content: [{ type: 'text', text: lines.join('\n') }] }
475
+ const content: AgentToolContent[] = lines.length > 0 ? [{ type: 'text', text: lines.join('\n') }] : []
476
+ if (item.result !== '')
477
+ content.push({ type: 'image', data: item.result, mimeType: 'image/png' })
478
+ const failure = ownJsonSlot(item, 'failure')
479
+ if (item.result === '' || failure !== undefined || item.transparentBackground !== undefined) {
480
+ content.push(jsonToolContent({
481
+ ...(item.result === '' ? { result: '' } : {}),
482
+ ...(failure !== undefined ? { failure } : {}),
483
+ ...(item.transparentBackground !== undefined ? { transparentBackground: item.transparentBackground } : {}),
484
+ }))
485
+ }
486
+ return { success: item.status !== 'failed', content }
379
487
  }
380
488
 
381
489
  function messageEndedText(item: CodexCompletedItem): string | undefined {
@@ -391,17 +499,17 @@ function messageEndedText(item: CodexCompletedItem): string | undefined {
391
499
  function userMessageText(item: CodexUserMessageItem): string | undefined {
392
500
  const parts: string[] = []
393
501
  for (const entry of item.content) {
394
- if (entry.type === 'text')
502
+ if (entry.type === 'text' && typeof entry.text === 'string')
395
503
  parts.push(entry.text)
396
504
  }
397
- return optionalNonEmpty(stripAuthorContext(parts.join('\n')))
505
+ return optionalNonEmpty(stripSessionContext(stripAuthorContext(parts.join('\n'))))
398
506
  }
399
507
 
400
508
  function hookPromptText(item: CodexHookPromptItem): string | undefined {
401
509
  return optionalNonEmpty(item.fragments.map(fragment => fragment.text).join('\n'))
402
510
  }
403
511
 
404
- function commandExecutionResult(item: Extract<CodexCompletedItem, { type: 'commandExecution' }>, status: 'completed' | 'failed'): AgentToolResult | undefined {
512
+ function commandExecutionResult(item: Extract<CodexCompletedItem, { type: 'commandExecution' }>, status: AgentToolCallStatus): AgentToolResult | undefined {
405
513
  // Stdout is content, not a label: whitespace and an explicitly empty
406
514
  // successful output must survive normalization into the complete step.
407
515
  // Native build_command_execution_end_item encodes empty captured output as
@@ -414,6 +522,33 @@ function commandExecutionResult(item: Extract<CodexCompletedItem, { type: 'comma
414
522
  return { success: status === 'completed', content: [{ type: 'text', text }] }
415
523
  }
416
524
 
525
+ function mcpToolResult(item: CodexMcpToolCompletedItem, status: 'completed' | 'failed'): AgentToolResult | undefined {
526
+ if (item.result === null || item.result === undefined)
527
+ return undefined
528
+
529
+ if (typeof item.result !== 'object' || Array.isArray(item.result)) {
530
+ const parsed = jsonValueSchema.safeParse(item.result)
531
+ return parsed.success
532
+ ? { success: status === 'completed', content: [jsonToolContent(parsed.data)] }
533
+ : undefined
534
+ }
535
+
536
+ const content: AgentToolContent[] = []
537
+ const resultContent = ownJsonSlot(item.result, 'content')
538
+ if (resultContent !== undefined) {
539
+ if (Array.isArray(resultContent))
540
+ content.push(...resultContent.map(mcpToolContent))
541
+ else
542
+ content.push(mcpToolContent(resultContent))
543
+ }
544
+ const structuredContent = ownJsonSlot(item.result, 'structuredContent')
545
+ return removeUndefined({ success: status === 'completed', content, structuredContent })
546
+ }
547
+
548
+ function mcpToolError(value: unknown): AgentError {
549
+ return { message: typeof value === 'string' ? value : 'MCP tool call failed.' }
550
+ }
551
+
417
552
  function dynamicToolResult(item: CodexDynamicToolCompletedItem): AgentToolResult | undefined {
418
553
  if (!item.contentItems)
419
554
  return undefined
@@ -428,14 +563,18 @@ function dynamicToolContent(item: CodexDynamicToolContentItem): AgentToolContent
428
563
  if (item.type === 'inputText')
429
564
  return { type: 'text', text: item.text }
430
565
 
431
- if (item.type === 'inputAudio')
432
- return { type: 'text', text: item.audioUrl }
566
+ if (item.type === 'inputAudio') {
567
+ const dataUrl = /^data:([^;,]+);base64,(.+)$/s.exec(item.audioUrl)
568
+ return dataUrl
569
+ ? { type: 'audio', mimeType: dataUrl[1]!, data: dataUrl[2]! }
570
+ : jsonToolContent({ type: 'audio', uri: item.audioUrl })
571
+ }
433
572
 
434
573
  const dataUrl = /^data:([^;,]+);base64,(.*)$/s.exec(item.imageUrl)
435
574
  if (dataUrl)
436
575
  return { type: 'image', mimeType: dataUrl[1] ?? 'application/octet-stream', data: dataUrl[2] ?? '' }
437
576
 
438
- return { type: 'text', text: item.imageUrl }
577
+ return { type: 'image', uri: item.imageUrl }
439
578
  }
440
579
 
441
580
  function mapCollabAgentToolCompleted({ turnId, threadId, item, mapperState }: { turnId: string, threadId: string, item: CodexCollabAgentToolCompletedItem, mapperState: CodexEventMapperState }): AgentEventDraft[] {
@@ -445,6 +584,7 @@ function mapCollabAgentToolCompleted({ turnId, threadId, item, mapperState }: {
445
584
  turnId,
446
585
  payload: removeUndefined({
447
586
  toolCallId: item.id,
587
+ toolName: item.tool,
448
588
  status: item.status === 'failed' ? 'failed' as const : 'completed' as const,
449
589
  error: item.status === 'failed' ? { message: `Codex ${item.tool} failed.` } : undefined,
450
590
  }),
@@ -454,23 +594,33 @@ function mapCollabAgentToolCompleted({ turnId, threadId, item, mapperState }: {
454
594
  })
455
595
 
456
596
  if (item.tool === 'spawnAgent') {
457
- const subagentId = item.receiverThreadIds.at(0)
458
- if (!subagentId || item.status === 'failed' || mapperState.subagentByThreadId.has(subagentId))
597
+ if (item.status === 'failed')
459
598
  return drafts
460
599
 
461
- const actor = subagentActorFromCollabItem({ subagentId, parentToolCallId: item.id, name: mapperState.subagentNameByThreadId.get(subagentId) })
462
- mapperState.subagentByThreadId.set(subagentId, actor)
463
- mapperState.subagentById.set(subagentId, actor)
464
- drafts.push({
465
- type: 'subagent.started',
466
- turnId,
467
- actor,
468
- payload: removeUndefined({
469
- ...subagentPayloadRef(actor),
470
- description: optionalNonEmpty(item.prompt),
471
- }),
472
- })
473
- drafts.push(...flushPendingDrafts(subagentId, mapperState))
600
+ for (const subagentId of new Set(item.receiverThreadIds)) {
601
+ if (!subagentId || mapperState.subagentByThreadId.has(subagentId))
602
+ continue
603
+
604
+ const actor = subagentActorFromCollabItem({
605
+ subagentId,
606
+ parentThreadId: item.senderThreadId,
607
+ parentToolCallId: item.id,
608
+ name: mapperState.subagentNameByThreadId.get(subagentId),
609
+ state: mapperState,
610
+ })
611
+ mapperState.subagentByThreadId.set(subagentId, actor)
612
+ mapperState.subagentById.set(subagentId, actor)
613
+ drafts.push({
614
+ type: 'subagent.started',
615
+ turnId,
616
+ actor,
617
+ payload: removeUndefined({
618
+ ...subagentPayloadRef(actor),
619
+ description: optionalNonEmpty(item.prompt),
620
+ }),
621
+ })
622
+ drafts.push(...flushPendingDrafts(subagentId, mapperState))
623
+ }
474
624
  return drafts
475
625
  }
476
626
 
@@ -507,14 +657,16 @@ function mapSubAgentActivityCompleted(input: { turnId: string, threadId: string,
507
657
 
508
658
  const actor = subagentActorFromCollabItem({
509
659
  subagentId: item.agentThreadId,
660
+ parentThreadId: threadId,
510
661
  parentToolCallId: item.id,
511
662
  name: item.agentPath,
663
+ state,
512
664
  })
513
665
  state.subagentByThreadId.set(item.agentThreadId, actor)
514
666
  state.subagentById.set(item.agentThreadId, actor)
515
667
  return [
516
668
  ...emitOrBufferByThread({ draft: { type: 'tool.call.started', turnId, payload: { toolCallId: item.id, toolName: 'spawnAgent' } }, threadId, state }),
517
- ...emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: { toolCallId: item.id, status: 'completed' } }, threadId, state }),
669
+ ...emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: { toolCallId: item.id, toolName: 'spawnAgent', status: 'completed' } }, threadId, state }),
518
670
  {
519
671
  type: 'subagent.started',
520
672
  turnId,
@@ -528,7 +680,7 @@ function mapSubAgentActivityCompleted(input: { turnId: string, threadId: string,
528
680
  if (item.kind === 'interacted') {
529
681
  return [
530
682
  ...emitOrBufferByThread({ draft: { type: 'tool.call.started', turnId, payload: { toolCallId: item.id, toolName: 'sendInput' } }, threadId, state }),
531
- ...emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: { toolCallId: item.id, status: 'completed' } }, threadId, state }),
683
+ ...emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: { toolCallId: item.id, toolName: 'sendInput', status: 'completed' } }, threadId, state }),
532
684
  ]
533
685
  }
534
686
 
@@ -624,7 +776,7 @@ function actorForThread(threadId: string, state: CodexEventMapperState): AgentEv
624
776
  const subagentActor = state.subagentByThreadId.get(threadId)
625
777
  if (subagentActor)
626
778
  return subagentActor
627
- return state.mainThreadId === undefined || threadId === state.mainThreadId ? mainActor : undefined
779
+ return threadId === state.mainThreadId ? mainActor : undefined
628
780
  }
629
781
 
630
782
  function flushPendingDrafts(threadId: string, state: CodexEventMapperState): AgentEventDraft[] {
@@ -670,12 +822,18 @@ function codexSubagentDisplayName(thread: CodexThreadMetadata): string | undefin
670
822
  return nickname ?? role
671
823
  }
672
824
 
673
- function subagentActorFromCollabItem(input: { subagentId: string, parentToolCallId: string, name?: string }): CodexSubagentRef {
825
+ function subagentActorFromCollabItem(input: {
826
+ subagentId: string
827
+ parentThreadId: string
828
+ parentToolCallId: string
829
+ name?: string
830
+ state: CodexEventMapperState
831
+ }): CodexSubagentRef {
674
832
  return removeUndefined({
675
833
  type: 'subagent' as const,
676
834
  actorId: input.subagentId,
677
835
  subagentId: input.subagentId,
678
- parentActorId: 'main',
836
+ parentActorId: input.parentThreadId === input.state.mainThreadId ? 'main' : input.parentThreadId,
679
837
  origin: { type: 'tool_call' as const, toolCallId: input.parentToolCallId },
680
838
  parentToolCallId: input.parentToolCallId,
681
839
  name: input.name,
@@ -703,10 +861,169 @@ function collabToolInput(item: CodexCollabAgentToolStartedItem): JsonObject {
703
861
  })
704
862
  }
705
863
 
864
+ function observeMainTurnStarted({ threadId, turnId, state }: { threadId: string, turnId: string, state: CodexEventMapperState }): void {
865
+ if (threadId !== state.mainThreadId || state.seenMainTurnIds.has(turnId))
866
+ return
867
+
868
+ state.seenMainTurnIds.add(turnId)
869
+ state.currentMainTurn = { status: 'open', turnId }
870
+ state.openContextCompactionIds.clear()
871
+ state.closedContextCompactionIds.clear()
872
+ invalidateContextSnapshotBaseline(state)
873
+ }
874
+
875
+ function observeMainTurnCompleted({ threadId, turnId, state }: { threadId: string, turnId: string, state: CodexEventMapperState }): void {
876
+ if (!isCurrentOpenMainTurn({ threadId, turnId, state }))
877
+ return
878
+
879
+ state.currentMainTurn = { status: 'closed' }
880
+ state.openContextCompactionIds.clear()
881
+ state.closedContextCompactionIds.clear()
882
+ }
883
+
884
+ function observeContextCompactionStarted({ threadId, turnId, compactionId, state }: { threadId: string, turnId: string, compactionId: string, state: CodexEventMapperState }): void {
885
+ if (!isCurrentOpenMainTurn({ threadId, turnId, state })
886
+ || state.openContextCompactionIds.has(compactionId)
887
+ || state.closedContextCompactionIds.has(compactionId)) {
888
+ return
889
+ }
890
+
891
+ state.openContextCompactionIds.add(compactionId)
892
+ invalidateContextSnapshotBaseline(state)
893
+ }
894
+
895
+ function observeContextCompactionCompleted({ threadId, turnId, compactionId, state }: { threadId: string, turnId: string, compactionId: string, state: CodexEventMapperState }): void {
896
+ if (!isCurrentOpenMainTurn({ threadId, turnId, state }) || state.closedContextCompactionIds.has(compactionId))
897
+ return
898
+
899
+ state.openContextCompactionIds.delete(compactionId)
900
+ state.closedContextCompactionIds.add(compactionId)
901
+ invalidateContextSnapshotBaseline(state)
902
+ }
903
+
904
+ function mapContextSnapshotFromTokenUsage({ threadId, turnId, tokenUsage, state }: { threadId: string, turnId: string, tokenUsage: CodexTokenUsage, state: CodexEventMapperState }): AgentEventDraft[] {
905
+ if (state.openContextCompactionIds.size > 0)
906
+ return []
907
+
908
+ return reportContextSnapshot({
909
+ threadId,
910
+ turnId,
911
+ snapshot: contextSnapshotFromTokenUsage(tokenUsage),
912
+ state,
913
+ })
914
+ }
915
+
916
+ function contextSnapshotFromTokenUsage(tokenUsage: CodexTokenUsage): AgentContextSnapshot {
917
+ const { last } = tokenUsage
918
+ if (!Number.isSafeInteger(last.totalTokens) || last.totalTokens < 0
919
+ || (last.inputTokens === 0 && last.outputTokens === 0)) {
920
+ return { status: 'unknown' }
921
+ }
922
+
923
+ const capacityTokens = tokenUsage.modelContextWindow
924
+ if (!Number.isSafeInteger(capacityTokens) || capacityTokens === null || capacityTokens === undefined || capacityTokens <= 0)
925
+ return { status: 'observed', usedTokens: last.totalTokens, window: { status: 'unknown' } }
926
+
927
+ return {
928
+ status: 'observed',
929
+ usedTokens: last.totalTokens,
930
+ window: {
931
+ status: 'known',
932
+ capacityTokens,
933
+ remainingPercent: remainingContextPercent({ usedTokens: last.totalTokens, capacityTokens }),
934
+ },
935
+ }
936
+ }
937
+
938
+ function remainingContextPercent({ usedTokens, capacityTokens }: { usedTokens: number, capacityTokens: number }): number {
939
+ if (capacityTokens <= CODEX_CONTEXT_BASELINE_TOKENS)
940
+ return 0
941
+
942
+ const usableTokens = capacityTokens - CODEX_CONTEXT_BASELINE_TOKENS
943
+ const usedUsableTokens = Math.max(usedTokens - CODEX_CONTEXT_BASELINE_TOKENS, 0)
944
+ const remainingTokens = Math.max(usableTokens - usedUsableTokens, 0)
945
+ return Math.round(Math.min(Math.max(100 * remainingTokens / usableTokens, 0), 100))
946
+ }
947
+
948
+ function reportContextSnapshot({ threadId, turnId, snapshot, state }: { threadId: string, turnId: string, snapshot: AgentContextSnapshot, state: CodexEventMapperState }): AgentEventDraft[] {
949
+ if (!isCurrentOpenMainTurn({ threadId, turnId, state })
950
+ || !state.startedTurnIds.has(turnId)
951
+ || state.openContextCompactionIds.size > 0
952
+ || contextSnapshotMatchesBaseline(snapshot, state.contextSnapshotBaseline)) {
953
+ return []
954
+ }
955
+
956
+ state.contextSnapshotBaseline = { status: 'reported', snapshot }
957
+ return emitOrBufferByThread({
958
+ draft: { type: 'context.updated', turnId, payload: { snapshot } },
959
+ threadId,
960
+ state,
961
+ })
962
+ }
963
+
964
+ function isCurrentOpenMainTurn({ threadId, turnId, state }: { threadId: string, turnId: string, state: CodexEventMapperState }): boolean {
965
+ return threadId === state.mainThreadId
966
+ && state.currentMainTurn.status === 'open'
967
+ && state.currentMainTurn.turnId === turnId
968
+ }
969
+
970
+ function contextSnapshotMatchesBaseline(snapshot: AgentContextSnapshot, baseline: CodexContextSnapshotBaseline): boolean {
971
+ if (baseline.status === 'empty')
972
+ return snapshot.status === 'unknown'
973
+ if (snapshot.status !== baseline.snapshot.status)
974
+ return false
975
+ if (snapshot.status === 'unknown' || baseline.snapshot.status === 'unknown')
976
+ return true
977
+ if (snapshot.usedTokens !== baseline.snapshot.usedTokens || snapshot.window.status !== baseline.snapshot.window.status)
978
+ return false
979
+ if (snapshot.window.status === 'unknown' || baseline.snapshot.window.status === 'unknown')
980
+ return true
981
+ return snapshot.window.capacityTokens === baseline.snapshot.window.capacityTokens
982
+ && snapshot.window.remainingPercent === baseline.snapshot.window.remainingPercent
983
+ }
984
+
985
+ function invalidateContextSnapshotBaseline(state: CodexEventMapperState): void {
986
+ state.contextSnapshotBaseline = { status: 'empty' }
987
+ }
988
+
706
989
  function usageFromTokenUsage(value: CodexTokenUsage | undefined): AgentUsage | undefined {
707
990
  return value ? usageFromCounts(value.last) : undefined
708
991
  }
709
992
 
993
+ // Codex reports `total` for the thread loaded in one app-server, and whether a resumed thread
994
+ // restores it depends on the release: 0.153.4 restarts it at zero, 0.155.0 restores it. Accounting
995
+ // therefore sums each model call's `last` usage, once per distinct `total`, in a scope owned by this
996
+ // run. A resumed run opens a new scope, so a checkpoint never regresses or counts a call twice.
997
+ function codexUsageAccounting(threadId: string, tokenUsage: CodexTokenUsage, state: CodexEventMapperState): AgentUsageAccounting {
998
+ const scopeId = `${threadId}:${state.usageScopeInstanceId}`
999
+ const total = JSON.stringify(tokenUsage.total)
1000
+ const previous = state.usageAccountingByThreadId.get(threadId)
1001
+ let usage = previous?.usage ?? {}
1002
+ if (previous?.total !== total) {
1003
+ usage = addUsage(usage, {
1004
+ ...usageFromCounts(tokenUsage.last),
1005
+ inputTokens: Math.max(0, tokenUsage.last.inputTokens - tokenUsage.last.cachedInputTokens),
1006
+ })
1007
+ state.usageAccountingByThreadId.set(threadId, { total, usage })
1008
+ }
1009
+ return {
1010
+ observationId: createHash('sha256').update(scopeId).update(JSON.stringify(usage)).digest('hex'),
1011
+ mode: 'cumulative',
1012
+ scopeId,
1013
+ includesChildren: false,
1014
+ usage,
1015
+ }
1016
+ }
1017
+
1018
+ function addUsage(accumulated: AgentUsage, call: AgentUsage): AgentUsage {
1019
+ const sum: AgentUsage = { ...accumulated }
1020
+ for (const [key, value] of Object.entries(call) as [keyof AgentUsage, number | undefined][]) {
1021
+ if (value !== undefined)
1022
+ sum[key] = (sum[key] ?? 0) + value
1023
+ }
1024
+ return sum
1025
+ }
1026
+
710
1027
  function usageFromCounts(value: CodexTokenCounts): AgentUsage {
711
1028
  return removeUndefined({
712
1029
  inputTokens: value.inputTokens,