@meistrari/agent-core 0.2.0-experimental.1 → 0.2.0-experimental.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +864 -135
  19. package/src/agents/claude/claude-file-change.ts +128 -0
  20. package/src/agents/claude/claude-provider.ts +344 -29
  21. package/src/agents/claude/claude-run.ts +893 -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 +375 -64
  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,48 @@ 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
400
  const status = item.exitCode === undefined || item.exitCode === null || item.exitCode === 0 ? 'completed' : 'failed'
326
401
  const error = 'error' in item && typeof item.error === 'string'
327
402
  ? { message: item.error }
328
403
  : status === 'failed' ? { message: `Command exited with code ${item.exitCode}.` } : undefined
329
- return { status, output: commandExecutionResult(item, status), error }
404
+ return { toolName, status, output: commandExecutionResult(item, status), error }
330
405
  }
331
406
  if (item.type === 'dynamicToolCall')
332
- return { status: item.success === false ? 'failed' : 'completed', output: dynamicToolResult(item) }
407
+ return { toolName, status: item.success === false ? 'failed' : 'completed', output: dynamicToolResult(item) }
408
+ if (item.type === 'mcpToolCall') {
409
+ if (item.status === 'inProgress') {
410
+ return {
411
+ toolName,
412
+ status: 'failed',
413
+ output: mcpToolResult(item, 'failed'),
414
+ error: { message: 'MCP tool result could not be determined because completion status was inconsistent.' },
415
+ }
416
+ }
417
+
418
+ const status = item.status
419
+ return {
420
+ toolName,
421
+ status,
422
+ output: mcpToolResult(item, status),
423
+ error: status === 'failed' ? mcpToolError(item.error?.message) : undefined,
424
+ }
425
+ }
333
426
  return undefined
334
427
  }
335
428
 
@@ -373,9 +466,18 @@ function imageGenerationResult(item: CodexImageGenerationItem): AgentToolResult
373
466
  const revised = optionalNonEmpty(item.revisedPrompt)
374
467
  if (revised)
375
468
  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') }] }
469
+ const content: AgentToolContent[] = lines.length > 0 ? [{ type: 'text', text: lines.join('\n') }] : []
470
+ if (item.result !== '')
471
+ content.push({ type: 'image', data: item.result, mimeType: 'image/png' })
472
+ const failure = ownJsonSlot(item, 'failure')
473
+ if (item.result === '' || failure !== undefined || item.transparentBackground !== undefined) {
474
+ content.push(jsonToolContent({
475
+ ...(item.result === '' ? { result: '' } : {}),
476
+ ...(failure !== undefined ? { failure } : {}),
477
+ ...(item.transparentBackground !== undefined ? { transparentBackground: item.transparentBackground } : {}),
478
+ }))
479
+ }
480
+ return { success: item.status !== 'failed', content }
379
481
  }
380
482
 
381
483
  function messageEndedText(item: CodexCompletedItem): string | undefined {
@@ -391,10 +493,10 @@ function messageEndedText(item: CodexCompletedItem): string | undefined {
391
493
  function userMessageText(item: CodexUserMessageItem): string | undefined {
392
494
  const parts: string[] = []
393
495
  for (const entry of item.content) {
394
- if (entry.type === 'text')
496
+ if (entry.type === 'text' && typeof entry.text === 'string')
395
497
  parts.push(entry.text)
396
498
  }
397
- return optionalNonEmpty(stripAuthorContext(parts.join('\n')))
499
+ return optionalNonEmpty(stripSessionContext(stripAuthorContext(parts.join('\n'))))
398
500
  }
399
501
 
400
502
  function hookPromptText(item: CodexHookPromptItem): string | undefined {
@@ -414,6 +516,33 @@ function commandExecutionResult(item: Extract<CodexCompletedItem, { type: 'comma
414
516
  return { success: status === 'completed', content: [{ type: 'text', text }] }
415
517
  }
416
518
 
519
+ function mcpToolResult(item: CodexMcpToolCompletedItem, status: 'completed' | 'failed'): AgentToolResult | undefined {
520
+ if (item.result === null || item.result === undefined)
521
+ return undefined
522
+
523
+ if (typeof item.result !== 'object' || Array.isArray(item.result)) {
524
+ const parsed = jsonValueSchema.safeParse(item.result)
525
+ return parsed.success
526
+ ? { success: status === 'completed', content: [jsonToolContent(parsed.data)] }
527
+ : undefined
528
+ }
529
+
530
+ const content: AgentToolContent[] = []
531
+ const resultContent = ownJsonSlot(item.result, 'content')
532
+ if (resultContent !== undefined) {
533
+ if (Array.isArray(resultContent))
534
+ content.push(...resultContent.map(mcpToolContent))
535
+ else
536
+ content.push(mcpToolContent(resultContent))
537
+ }
538
+ const structuredContent = ownJsonSlot(item.result, 'structuredContent')
539
+ return removeUndefined({ success: status === 'completed', content, structuredContent })
540
+ }
541
+
542
+ function mcpToolError(value: unknown): AgentError {
543
+ return { message: typeof value === 'string' ? value : 'MCP tool call failed.' }
544
+ }
545
+
417
546
  function dynamicToolResult(item: CodexDynamicToolCompletedItem): AgentToolResult | undefined {
418
547
  if (!item.contentItems)
419
548
  return undefined
@@ -428,14 +557,18 @@ function dynamicToolContent(item: CodexDynamicToolContentItem): AgentToolContent
428
557
  if (item.type === 'inputText')
429
558
  return { type: 'text', text: item.text }
430
559
 
431
- if (item.type === 'inputAudio')
432
- return { type: 'text', text: item.audioUrl }
560
+ if (item.type === 'inputAudio') {
561
+ const dataUrl = /^data:([^;,]+);base64,(.+)$/s.exec(item.audioUrl)
562
+ return dataUrl
563
+ ? { type: 'audio', mimeType: dataUrl[1]!, data: dataUrl[2]! }
564
+ : jsonToolContent({ type: 'audio', uri: item.audioUrl })
565
+ }
433
566
 
434
567
  const dataUrl = /^data:([^;,]+);base64,(.*)$/s.exec(item.imageUrl)
435
568
  if (dataUrl)
436
569
  return { type: 'image', mimeType: dataUrl[1] ?? 'application/octet-stream', data: dataUrl[2] ?? '' }
437
570
 
438
- return { type: 'text', text: item.imageUrl }
571
+ return { type: 'image', uri: item.imageUrl }
439
572
  }
440
573
 
441
574
  function mapCollabAgentToolCompleted({ turnId, threadId, item, mapperState }: { turnId: string, threadId: string, item: CodexCollabAgentToolCompletedItem, mapperState: CodexEventMapperState }): AgentEventDraft[] {
@@ -445,6 +578,7 @@ function mapCollabAgentToolCompleted({ turnId, threadId, item, mapperState }: {
445
578
  turnId,
446
579
  payload: removeUndefined({
447
580
  toolCallId: item.id,
581
+ toolName: item.tool,
448
582
  status: item.status === 'failed' ? 'failed' as const : 'completed' as const,
449
583
  error: item.status === 'failed' ? { message: `Codex ${item.tool} failed.` } : undefined,
450
584
  }),
@@ -454,23 +588,33 @@ function mapCollabAgentToolCompleted({ turnId, threadId, item, mapperState }: {
454
588
  })
455
589
 
456
590
  if (item.tool === 'spawnAgent') {
457
- const subagentId = item.receiverThreadIds.at(0)
458
- if (!subagentId || item.status === 'failed' || mapperState.subagentByThreadId.has(subagentId))
591
+ if (item.status === 'failed')
459
592
  return drafts
460
593
 
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))
594
+ for (const subagentId of new Set(item.receiverThreadIds)) {
595
+ if (!subagentId || mapperState.subagentByThreadId.has(subagentId))
596
+ continue
597
+
598
+ const actor = subagentActorFromCollabItem({
599
+ subagentId,
600
+ parentThreadId: item.senderThreadId,
601
+ parentToolCallId: item.id,
602
+ name: mapperState.subagentNameByThreadId.get(subagentId),
603
+ state: mapperState,
604
+ })
605
+ mapperState.subagentByThreadId.set(subagentId, actor)
606
+ mapperState.subagentById.set(subagentId, actor)
607
+ drafts.push({
608
+ type: 'subagent.started',
609
+ turnId,
610
+ actor,
611
+ payload: removeUndefined({
612
+ ...subagentPayloadRef(actor),
613
+ description: optionalNonEmpty(item.prompt),
614
+ }),
615
+ })
616
+ drafts.push(...flushPendingDrafts(subagentId, mapperState))
617
+ }
474
618
  return drafts
475
619
  }
476
620
 
@@ -507,14 +651,16 @@ function mapSubAgentActivityCompleted(input: { turnId: string, threadId: string,
507
651
 
508
652
  const actor = subagentActorFromCollabItem({
509
653
  subagentId: item.agentThreadId,
654
+ parentThreadId: threadId,
510
655
  parentToolCallId: item.id,
511
656
  name: item.agentPath,
657
+ state,
512
658
  })
513
659
  state.subagentByThreadId.set(item.agentThreadId, actor)
514
660
  state.subagentById.set(item.agentThreadId, actor)
515
661
  return [
516
662
  ...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 }),
663
+ ...emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: { toolCallId: item.id, toolName: 'spawnAgent', status: 'completed' } }, threadId, state }),
518
664
  {
519
665
  type: 'subagent.started',
520
666
  turnId,
@@ -528,7 +674,7 @@ function mapSubAgentActivityCompleted(input: { turnId: string, threadId: string,
528
674
  if (item.kind === 'interacted') {
529
675
  return [
530
676
  ...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 }),
677
+ ...emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: { toolCallId: item.id, toolName: 'sendInput', status: 'completed' } }, threadId, state }),
532
678
  ]
533
679
  }
534
680
 
@@ -624,7 +770,7 @@ function actorForThread(threadId: string, state: CodexEventMapperState): AgentEv
624
770
  const subagentActor = state.subagentByThreadId.get(threadId)
625
771
  if (subagentActor)
626
772
  return subagentActor
627
- return state.mainThreadId === undefined || threadId === state.mainThreadId ? mainActor : undefined
773
+ return threadId === state.mainThreadId ? mainActor : undefined
628
774
  }
629
775
 
630
776
  function flushPendingDrafts(threadId: string, state: CodexEventMapperState): AgentEventDraft[] {
@@ -670,12 +816,18 @@ function codexSubagentDisplayName(thread: CodexThreadMetadata): string | undefin
670
816
  return nickname ?? role
671
817
  }
672
818
 
673
- function subagentActorFromCollabItem(input: { subagentId: string, parentToolCallId: string, name?: string }): CodexSubagentRef {
819
+ function subagentActorFromCollabItem(input: {
820
+ subagentId: string
821
+ parentThreadId: string
822
+ parentToolCallId: string
823
+ name?: string
824
+ state: CodexEventMapperState
825
+ }): CodexSubagentRef {
674
826
  return removeUndefined({
675
827
  type: 'subagent' as const,
676
828
  actorId: input.subagentId,
677
829
  subagentId: input.subagentId,
678
- parentActorId: 'main',
830
+ parentActorId: input.parentThreadId === input.state.mainThreadId ? 'main' : input.parentThreadId,
679
831
  origin: { type: 'tool_call' as const, toolCallId: input.parentToolCallId },
680
832
  parentToolCallId: input.parentToolCallId,
681
833
  name: input.name,
@@ -703,10 +855,169 @@ function collabToolInput(item: CodexCollabAgentToolStartedItem): JsonObject {
703
855
  })
704
856
  }
705
857
 
858
+ function observeMainTurnStarted({ threadId, turnId, state }: { threadId: string, turnId: string, state: CodexEventMapperState }): void {
859
+ if (threadId !== state.mainThreadId || state.seenMainTurnIds.has(turnId))
860
+ return
861
+
862
+ state.seenMainTurnIds.add(turnId)
863
+ state.currentMainTurn = { status: 'open', turnId }
864
+ state.openContextCompactionIds.clear()
865
+ state.closedContextCompactionIds.clear()
866
+ invalidateContextSnapshotBaseline(state)
867
+ }
868
+
869
+ function observeMainTurnCompleted({ threadId, turnId, state }: { threadId: string, turnId: string, state: CodexEventMapperState }): void {
870
+ if (!isCurrentOpenMainTurn({ threadId, turnId, state }))
871
+ return
872
+
873
+ state.currentMainTurn = { status: 'closed' }
874
+ state.openContextCompactionIds.clear()
875
+ state.closedContextCompactionIds.clear()
876
+ }
877
+
878
+ function observeContextCompactionStarted({ threadId, turnId, compactionId, state }: { threadId: string, turnId: string, compactionId: string, state: CodexEventMapperState }): void {
879
+ if (!isCurrentOpenMainTurn({ threadId, turnId, state })
880
+ || state.openContextCompactionIds.has(compactionId)
881
+ || state.closedContextCompactionIds.has(compactionId)) {
882
+ return
883
+ }
884
+
885
+ state.openContextCompactionIds.add(compactionId)
886
+ invalidateContextSnapshotBaseline(state)
887
+ }
888
+
889
+ function observeContextCompactionCompleted({ threadId, turnId, compactionId, state }: { threadId: string, turnId: string, compactionId: string, state: CodexEventMapperState }): void {
890
+ if (!isCurrentOpenMainTurn({ threadId, turnId, state }) || state.closedContextCompactionIds.has(compactionId))
891
+ return
892
+
893
+ state.openContextCompactionIds.delete(compactionId)
894
+ state.closedContextCompactionIds.add(compactionId)
895
+ invalidateContextSnapshotBaseline(state)
896
+ }
897
+
898
+ function mapContextSnapshotFromTokenUsage({ threadId, turnId, tokenUsage, state }: { threadId: string, turnId: string, tokenUsage: CodexTokenUsage, state: CodexEventMapperState }): AgentEventDraft[] {
899
+ if (state.openContextCompactionIds.size > 0)
900
+ return []
901
+
902
+ return reportContextSnapshot({
903
+ threadId,
904
+ turnId,
905
+ snapshot: contextSnapshotFromTokenUsage(tokenUsage),
906
+ state,
907
+ })
908
+ }
909
+
910
+ function contextSnapshotFromTokenUsage(tokenUsage: CodexTokenUsage): AgentContextSnapshot {
911
+ const { last } = tokenUsage
912
+ if (!Number.isSafeInteger(last.totalTokens) || last.totalTokens < 0
913
+ || (last.inputTokens === 0 && last.outputTokens === 0)) {
914
+ return { status: 'unknown' }
915
+ }
916
+
917
+ const capacityTokens = tokenUsage.modelContextWindow
918
+ if (!Number.isSafeInteger(capacityTokens) || capacityTokens === null || capacityTokens === undefined || capacityTokens <= 0)
919
+ return { status: 'observed', usedTokens: last.totalTokens, window: { status: 'unknown' } }
920
+
921
+ return {
922
+ status: 'observed',
923
+ usedTokens: last.totalTokens,
924
+ window: {
925
+ status: 'known',
926
+ capacityTokens,
927
+ remainingPercent: remainingContextPercent({ usedTokens: last.totalTokens, capacityTokens }),
928
+ },
929
+ }
930
+ }
931
+
932
+ function remainingContextPercent({ usedTokens, capacityTokens }: { usedTokens: number, capacityTokens: number }): number {
933
+ if (capacityTokens <= CODEX_CONTEXT_BASELINE_TOKENS)
934
+ return 0
935
+
936
+ const usableTokens = capacityTokens - CODEX_CONTEXT_BASELINE_TOKENS
937
+ const usedUsableTokens = Math.max(usedTokens - CODEX_CONTEXT_BASELINE_TOKENS, 0)
938
+ const remainingTokens = Math.max(usableTokens - usedUsableTokens, 0)
939
+ return Math.round(Math.min(Math.max(100 * remainingTokens / usableTokens, 0), 100))
940
+ }
941
+
942
+ function reportContextSnapshot({ threadId, turnId, snapshot, state }: { threadId: string, turnId: string, snapshot: AgentContextSnapshot, state: CodexEventMapperState }): AgentEventDraft[] {
943
+ if (!isCurrentOpenMainTurn({ threadId, turnId, state })
944
+ || !state.startedTurnIds.has(turnId)
945
+ || state.openContextCompactionIds.size > 0
946
+ || contextSnapshotMatchesBaseline(snapshot, state.contextSnapshotBaseline)) {
947
+ return []
948
+ }
949
+
950
+ state.contextSnapshotBaseline = { status: 'reported', snapshot }
951
+ return emitOrBufferByThread({
952
+ draft: { type: 'context.updated', turnId, payload: { snapshot } },
953
+ threadId,
954
+ state,
955
+ })
956
+ }
957
+
958
+ function isCurrentOpenMainTurn({ threadId, turnId, state }: { threadId: string, turnId: string, state: CodexEventMapperState }): boolean {
959
+ return threadId === state.mainThreadId
960
+ && state.currentMainTurn.status === 'open'
961
+ && state.currentMainTurn.turnId === turnId
962
+ }
963
+
964
+ function contextSnapshotMatchesBaseline(snapshot: AgentContextSnapshot, baseline: CodexContextSnapshotBaseline): boolean {
965
+ if (baseline.status === 'empty')
966
+ return snapshot.status === 'unknown'
967
+ if (snapshot.status !== baseline.snapshot.status)
968
+ return false
969
+ if (snapshot.status === 'unknown' || baseline.snapshot.status === 'unknown')
970
+ return true
971
+ if (snapshot.usedTokens !== baseline.snapshot.usedTokens || snapshot.window.status !== baseline.snapshot.window.status)
972
+ return false
973
+ if (snapshot.window.status === 'unknown' || baseline.snapshot.window.status === 'unknown')
974
+ return true
975
+ return snapshot.window.capacityTokens === baseline.snapshot.window.capacityTokens
976
+ && snapshot.window.remainingPercent === baseline.snapshot.window.remainingPercent
977
+ }
978
+
979
+ function invalidateContextSnapshotBaseline(state: CodexEventMapperState): void {
980
+ state.contextSnapshotBaseline = { status: 'empty' }
981
+ }
982
+
706
983
  function usageFromTokenUsage(value: CodexTokenUsage | undefined): AgentUsage | undefined {
707
984
  return value ? usageFromCounts(value.last) : undefined
708
985
  }
709
986
 
987
+ // Codex reports `total` for the thread loaded in one app-server, and whether a resumed thread
988
+ // restores it depends on the release: 0.153.4 restarts it at zero, 0.155.0 restores it. Accounting
989
+ // therefore sums each model call's `last` usage, once per distinct `total`, in a scope owned by this
990
+ // run. A resumed run opens a new scope, so a checkpoint never regresses or counts a call twice.
991
+ function codexUsageAccounting(threadId: string, tokenUsage: CodexTokenUsage, state: CodexEventMapperState): AgentUsageAccounting {
992
+ const scopeId = `${threadId}:${state.usageScopeInstanceId}`
993
+ const total = JSON.stringify(tokenUsage.total)
994
+ const previous = state.usageAccountingByThreadId.get(threadId)
995
+ let usage = previous?.usage ?? {}
996
+ if (previous?.total !== total) {
997
+ usage = addUsage(usage, {
998
+ ...usageFromCounts(tokenUsage.last),
999
+ inputTokens: Math.max(0, tokenUsage.last.inputTokens - tokenUsage.last.cachedInputTokens),
1000
+ })
1001
+ state.usageAccountingByThreadId.set(threadId, { total, usage })
1002
+ }
1003
+ return {
1004
+ observationId: createHash('sha256').update(scopeId).update(JSON.stringify(usage)).digest('hex'),
1005
+ mode: 'cumulative',
1006
+ scopeId,
1007
+ includesChildren: false,
1008
+ usage,
1009
+ }
1010
+ }
1011
+
1012
+ function addUsage(accumulated: AgentUsage, call: AgentUsage): AgentUsage {
1013
+ const sum: AgentUsage = { ...accumulated }
1014
+ for (const [key, value] of Object.entries(call) as [keyof AgentUsage, number | undefined][]) {
1015
+ if (value !== undefined)
1016
+ sum[key] = (sum[key] ?? 0) + value
1017
+ }
1018
+ return sum
1019
+ }
1020
+
710
1021
  function usageFromCounts(value: CodexTokenCounts): AgentUsage {
711
1022
  return removeUndefined({
712
1023
  inputTokens: value.inputTokens,