@meistrari/agent-core 0.0.0 → 0.1.1

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 (85) hide show
  1. package/README.md +36 -3
  2. package/bin/supervisor.ts +116 -0
  3. package/package.json +42 -3
  4. package/scripts/build-supervisor-executable.ts +32 -0
  5. package/src/agents/agent-error-serializer.ts +23 -0
  6. package/src/agents/agent-event-stream.ts +81 -0
  7. package/src/agents/agent-id.ts +17 -0
  8. package/src/agents/agent-operation.ts +11 -0
  9. package/src/agents/agent-provider.ts +37 -0
  10. package/src/agents/agent-run.ts +32 -0
  11. package/src/agents/agent-runtime-error.ts +161 -0
  12. package/src/agents/agent-session-events.ts +32 -0
  13. package/src/agents/agent-tool-runner.ts +83 -0
  14. package/src/agents/agent-tool.ts +111 -0
  15. package/src/agents/author-context.ts +36 -0
  16. package/src/agents/claude/claude-command-mapper.ts +234 -0
  17. package/src/agents/claude/claude-event-mapper.ts +748 -0
  18. package/src/agents/claude/claude-provider.ts +191 -0
  19. package/src/agents/claude/claude-run.ts +468 -0
  20. package/src/agents/claude/claude-tool-mapper.ts +186 -0
  21. package/src/agents/claude/index.ts +1 -0
  22. package/src/agents/codex/codex-auth.ts +34 -0
  23. package/src/agents/codex/codex-command-mapper.ts +78 -0
  24. package/src/agents/codex/codex-event-mapper.ts +721 -0
  25. package/src/agents/codex/codex-json-rpc-client.ts +326 -0
  26. package/src/agents/codex/codex-protocol.ts +36 -0
  27. package/src/agents/codex/codex-provider.ts +1050 -0
  28. package/src/agents/codex/codex-run.ts +404 -0
  29. package/src/agents/codex/codex-skill-catalog.ts +158 -0
  30. package/src/agents/codex/codex-skill-roots.ts +19 -0
  31. package/src/agents/codex/codex-tool-mapper.ts +55 -0
  32. package/src/agents/codex/codex.errors.ts +68 -0
  33. package/src/agents/codex/generated/meta.gen.ts +606 -0
  34. package/src/agents/codex/generated/namespaces.gen.ts +311 -0
  35. package/src/agents/codex/generated/schema.gen.ts +34883 -0
  36. package/src/agents/codex/index.ts +2 -0
  37. package/src/agents/index.ts +14 -0
  38. package/src/agents/input-attachment-preparation.ts +86 -0
  39. package/src/agents/input-attachment.errors.ts +16 -0
  40. package/src/agents/instructions.ts +8 -0
  41. package/src/agents/materialized-input-attachment.ts +26 -0
  42. package/src/agents/message-id.ts +23 -0
  43. package/src/agents/normalize.ts +8 -0
  44. package/src/agents/sandbox-environment.ts +1 -0
  45. package/src/agents/tools/ping.tool.ts +13 -0
  46. package/src/agents/user-input-request.ts +470 -0
  47. package/src/protocol/agent-event.ts +2 -1
  48. package/src/protocol/agent-usage.ts +14 -0
  49. package/src/provenance.gen.ts +3 -3
  50. package/src/supervisor/agent-provider-factory.ts +189 -0
  51. package/src/supervisor/bootstrap-binder.ts +133 -0
  52. package/src/supervisor/config.ts +49 -0
  53. package/src/supervisor/control-authority-verifier.ts +135 -0
  54. package/src/supervisor/create-supervisor-runtime.ts +25 -0
  55. package/src/supervisor/errors.ts +24 -0
  56. package/src/supervisor/index.ts +34 -0
  57. package/src/supervisor/persistence/json.ts +21 -0
  58. package/src/supervisor/persistence/state-discovery.ts +56 -0
  59. package/src/supervisor/persistence/supervisor-store.ts +364 -0
  60. package/src/supervisor/ports/index.ts +109 -0
  61. package/src/supervisor/provider-factory.ts +37 -0
  62. package/src/supervisor/resident.ts +143 -0
  63. package/src/supervisor/rpc-client.ts +120 -0
  64. package/src/supervisor/runtime-handler.ts +309 -0
  65. package/src/supervisor/websocket-server.ts +435 -0
  66. package/src/supervisor-protocol/bootstrap.ts +1 -1
  67. package/src/template-onboarding.ts +47 -0
  68. package/src/testing/es256-test-keys.ts +73 -0
  69. package/src/testing/in-memory-runtime-control-plane.ts +206 -0
  70. package/src/testing/index.ts +6 -0
  71. package/src/testing/loopback-supervisor-connection.ts +71 -0
  72. package/src/testing/scripted-provider.ts +64 -0
  73. package/src/worker-runtime-client/command-pump.ts +127 -0
  74. package/src/worker-runtime-client/connection-attempt.ts +349 -0
  75. package/src/worker-runtime-client/control-authority-signer.ts +100 -0
  76. package/src/worker-runtime-client/e2b-supervisor-connection.ts +102 -0
  77. package/src/worker-runtime-client/frame-processor.ts +178 -0
  78. package/src/worker-runtime-client/index.ts +27 -0
  79. package/src/worker-runtime-client/lease-reconciler.ts +14 -0
  80. package/src/worker-runtime-client/ports.ts +139 -0
  81. package/src/worker-runtime-client/postgres-notification-listener.ts +91 -0
  82. package/src/worker-runtime-client/rpc-dispatcher.ts +27 -0
  83. package/src/worker-runtime-client/rpc-request-manager.ts +141 -0
  84. package/src/worker-runtime-client/sandbox-connection-runtime.ts +301 -0
  85. package/src/worker-runtime-client/token-crypto.ts +46 -0
@@ -0,0 +1,721 @@
1
+ import type { AgentError, AgentEventActor, AgentMessageRole, AgentSubagentStatus, AgentToolCallStatus, AgentToolContent, AgentToolResult, AgentTurnEndStatus, AgentUsage, JsonObject, JsonValue } from '../../protocol'
2
+ import type { AgentWorkStatus } from '../../protocol/agent-work'
3
+ import type { AgentEventDraft } from '../agent-event-stream'
4
+ import type { CodexJsonRpcMessage, CodexServerNotification } from './codex-json-rpc-client'
5
+ import type { ServerNotificationParamsByMethod } from './codex-protocol'
6
+ import { createHash } from 'node:crypto'
7
+ import { jsonObjectSchema, jsonValueSchema } from '../../protocol'
8
+ import { CANONICAL_TOOL } from '../../protocol/agent-tool-name'
9
+ import { createRuntimeWorkItemId } from '../agent-id'
10
+ import { AgentProviderProtocolError } from '../agent-runtime-error'
11
+ import { stripAuthorContext } from '../author-context'
12
+ import { optionalNonEmpty, removeUndefined } from '../normalize'
13
+ import { isCodexServerNotification } from './codex-json-rpc-client'
14
+
15
+ export type CodexNotification = CodexServerNotification
16
+
17
+ type CodexTokenUsage = ServerNotificationParamsByMethod['thread/tokenUsage/updated']['tokenUsage']
18
+ type CodexTokenCounts = CodexTokenUsage['total']
19
+ type CodexPlanStepStatus = ServerNotificationParamsByMethod['turn/plan/updated']['plan'][number]['status']
20
+ type CodexStartedItem = ServerNotificationParamsByMethod['item/started']['item']
21
+ type CodexCompletedItem = ServerNotificationParamsByMethod['item/completed']['item']
22
+ type CodexWebSearchItem = Extract<CodexCompletedItem, { type: 'webSearch' }>
23
+ type CodexFileChangeItem = Extract<CodexCompletedItem, { type: 'fileChange' }>
24
+ type CodexImageGenerationItem = Extract<CodexCompletedItem, { type: 'imageGeneration' }>
25
+ type CodexUserMessageItem = Extract<CodexCompletedItem, { type: 'userMessage' }>
26
+ type CodexHookPromptItem = Extract<CodexCompletedItem, { type: 'hookPrompt' }>
27
+ type CodexDynamicToolCompletedItem = Extract<CodexCompletedItem, { type: 'dynamicToolCall' }>
28
+ type CodexDynamicToolContentItem = NonNullable<CodexDynamicToolCompletedItem['contentItems']>[number]
29
+ type CodexCollabAgentToolCompletedItem = Extract<CodexCompletedItem, { type: 'collabAgentToolCall' }>
30
+ type CodexCollabAgentToolStartedItem = Extract<CodexStartedItem, { type: 'collabAgentToolCall' }>
31
+ type CodexSubAgentActivityCompletedItem = Extract<CodexCompletedItem, { type: 'subAgentActivity' }>
32
+ type CodexCollabAgentState = CodexCollabAgentToolCompletedItem['agentsStates'][string]
33
+ type CodexThread = ServerNotificationParamsByMethod['thread/started']['thread']
34
+ type CodexThreadMetadata = Pick<CodexThread, 'id' | 'agentNickname' | 'agentRole'>
35
+ type CodexTurn = ServerNotificationParamsByMethod['turn/completed']['turn']
36
+ type CodexTurnError = NonNullable<CodexTurn['error']>
37
+ type CodexSubagentRef = Extract<AgentEventActor, { type: 'subagent' }>
38
+ interface CodexPromptCommandRef { commandId: string, origin: 'user' | 'supervisor' }
39
+ type CodexTurnEventDraft = Extract<AgentEventDraft, { turnId: string }>
40
+ type CodexUnownedTurnEventDraft = CodexTurnEventDraft extends infer Draft
41
+ ? Draft extends CodexTurnEventDraft
42
+ ? Omit<Draft, 'actor'>
43
+ : never
44
+ : never
45
+
46
+ const mainActor = { type: 'main', actorId: 'main' } satisfies AgentEventActor
47
+
48
+ export interface CodexEventMapperState {
49
+ readonly endedSubagentIds: Set<string>
50
+ readonly subagentByThreadId: Map<string, CodexSubagentRef>
51
+ readonly subagentById: Map<string, CodexSubagentRef>
52
+ readonly subagentNameByThreadId: Map<string, string>
53
+ readonly pendingDraftsByThreadId: Map<string, CodexUnownedTurnEventDraft[]>
54
+ readonly turnErrorsById: Map<string, AgentError>
55
+ readonly startedTurnIds: Set<string>
56
+ readonly startedReasoningIds: Set<string>
57
+ readonly reasoningSummaryById: Map<string, Map<number, string>>
58
+ readonly promptCommandByClientUserMessageId: Map<string, CodexPromptCommandRef>
59
+ mainThreadId?: string
60
+ }
61
+
62
+ 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() }
64
+ }
65
+
66
+ export function codexNotificationFromMessage(message: CodexJsonRpcMessage): CodexNotification | null {
67
+ return isCodexServerNotification(message) ? message : null
68
+ }
69
+
70
+ export function cacheCodexThreadMetadata(thread: CodexThreadMetadata, state: CodexEventMapperState): void {
71
+ cacheThreadMetadata(thread, state)
72
+ }
73
+
74
+ export function mapCodexNotification(notification: CodexNotification, state: CodexEventMapperState): AgentEventDraft[] {
75
+ // Codex has many app-server notifications; the runtime maps the subset that has a provider-neutral AgentEvent.
76
+
77
+ switch (notification.method) {
78
+ case 'thread/started':
79
+ cacheThreadMetadata(notification.params.thread, state)
80
+ return []
81
+ case 'turn/started':
82
+ state.mainThreadId ??= notification.params.threadId
83
+ state.startedTurnIds.add(notification.params.turn.id)
84
+ return emitOrBufferByThread({ draft: { type: 'turn.started', turnId: notification.params.turn.id, payload: {} }, threadId: notification.params.threadId, state })
85
+ case 'turn/completed': {
86
+ if (!state.startedTurnIds.has(notification.params.turn.id))
87
+ return []
88
+ const payload = turnEndedPayload(notification.params.turn, state)
89
+ return [
90
+ ...mapChildTurnCompleted({ threadId: notification.params.threadId, turnId: notification.params.turn.id, turnPayload: payload, state }),
91
+ ...emitOrBufferByThread({ draft: { type: 'turn.ended', turnId: notification.params.turn.id, payload }, threadId: notification.params.threadId, state }),
92
+ ]
93
+ }
94
+ case 'thread/tokenUsage/updated': {
95
+ if (!state.startedTurnIds.has(notification.params.turnId))
96
+ return []
97
+ 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 })
113
+ }
114
+ case 'turn/plan/updated':
115
+ if (!state.startedTurnIds.has(notification.params.turnId) || notification.params.threadId !== state.mainThreadId)
116
+ return []
117
+ return [{
118
+ type: 'work.observed',
119
+ turnId: notification.params.turnId,
120
+ actor: mainActor,
121
+ payload: {
122
+ observations: [{
123
+ kind: 'snapshot',
124
+ items: notification.params.plan.map(item => ({
125
+ id: createRuntimeWorkItemId(),
126
+ title: item.step,
127
+ status: workStatusFromCodexPlan(item.status),
128
+ })),
129
+ }],
130
+ },
131
+ }]
132
+ case 'error':
133
+ if (!state.startedTurnIds.has(notification.params.turnId))
134
+ return []
135
+ if (!notification.params.willRetry)
136
+ state.turnErrorsById.set(notification.params.turnId, agentErrorFromCodexTurnError(notification.params.error))
137
+ return []
138
+ case 'item/agentMessage/delta':
139
+ if (!state.startedTurnIds.has(notification.params.turnId))
140
+ return []
141
+ return emitOrBufferByThread({ draft: { type: 'message.delta', turnId: notification.params.turnId, payload: { messageId: notification.params.itemId, role: 'assistant', delta: notification.params.delta } }, threadId: notification.params.threadId, state })
142
+ case 'item/reasoning/summaryTextDelta': {
143
+ if (!state.startedTurnIds.has(notification.params.turnId) || !notification.params.delta)
144
+ return []
145
+ appendReasoningSummaryDelta({ reasoningId: notification.params.itemId, summaryIndex: notification.params.summaryIndex, delta: notification.params.delta, state })
146
+ if (!state.startedReasoningIds.has(notification.params.itemId)) {
147
+ const summary = accumulatedReasoningSummary(notification.params.itemId, state)
148
+ if (!summary)
149
+ return []
150
+ const started = ensureReasoningStarted({ reasoningId: notification.params.itemId, turnId: notification.params.turnId, threadId: notification.params.threadId, state })
151
+ return [...started, ...emitOrBufferByThread({ draft: { type: 'reasoning.summary.delta', turnId: notification.params.turnId, payload: { reasoningId: notification.params.itemId, text: summary } }, threadId: notification.params.threadId, state })]
152
+ }
153
+ return emitOrBufferByThread({ draft: { type: 'reasoning.summary.delta', turnId: notification.params.turnId, payload: { reasoningId: notification.params.itemId, text: notification.params.delta } }, threadId: notification.params.threadId, state })
154
+ }
155
+ case 'item/commandExecution/outputDelta':
156
+ case 'item/fileChange/outputDelta':
157
+ if (!state.startedTurnIds.has(notification.params.turnId))
158
+ return []
159
+ 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 })
160
+ case 'item/started':
161
+ if (!state.startedTurnIds.has(notification.params.turnId))
162
+ return []
163
+ return mapItemStarted({ turnId: notification.params.turnId, threadId: notification.params.threadId, item: notification.params.item, state })
164
+ case 'item/completed':
165
+ if (!state.startedTurnIds.has(notification.params.turnId))
166
+ return []
167
+ return mapItemCompleted({ turnId: notification.params.turnId, threadId: notification.params.threadId, item: notification.params.item, state })
168
+ default:
169
+ return []
170
+ }
171
+ }
172
+
173
+ function workStatusFromCodexPlan(status: CodexPlanStepStatus): AgentWorkStatus {
174
+ if (status === 'inProgress')
175
+ return 'in_progress'
176
+ return status
177
+ }
178
+
179
+ function turnEndedPayload(turn: CodexTurn, state: CodexEventMapperState): Extract<AgentEventDraft, { type: 'turn.ended' }>['payload'] {
180
+ const status = turnEndStatusFromCodex(turn.status)
181
+ const providerError = turn.error ? agentErrorFromCodexTurnError(turn.error) : state.turnErrorsById.get(turn.id)
182
+ if (providerError)
183
+ state.turnErrorsById.delete(turn.id)
184
+
185
+ return removeUndefined({
186
+ status,
187
+ reason: turn.status,
188
+ error: providerError,
189
+ duration: turn.durationMs ?? undefined,
190
+ })
191
+ }
192
+
193
+ function turnEndStatusFromCodex(status: CodexTurn['status']): AgentTurnEndStatus {
194
+ if (status === 'completed')
195
+ return 'completed'
196
+ if (status === 'failed')
197
+ return 'failed'
198
+ if (status === 'interrupted')
199
+ return 'interrupted'
200
+
201
+ throw new AgentProviderProtocolError({ message: `Codex emitted turn/completed with non-terminal status: ${status}` })
202
+ }
203
+
204
+ function mapItemStarted({ turnId, threadId, item, state }: { turnId: string, threadId: string, item: CodexStartedItem, state: CodexEventMapperState }): AgentEventDraft[] {
205
+ const message = messageForItem(item, state)
206
+ if (message)
207
+ return emitOrBufferByThread({ draft: { type: 'message.started', turnId, payload: removeUndefined({ messageId: item.id, ...message }) }, threadId, state })
208
+
209
+ const toolCall = toolCallStart(item)
210
+ if (toolCall)
211
+ return emitOrBufferByThread({ draft: { type: 'tool.call.started', turnId, payload: { toolCallId: item.id, toolName: toolCall.toolName, input: toolCall.input } }, threadId, state })
212
+
213
+ if (item.type === 'contextCompaction')
214
+ return emitOrBufferByThread({ draft: { type: 'context.compaction.started', turnId, payload: { compactionId: item.id, trigger: 'unknown' } }, threadId, state })
215
+
216
+ return []
217
+ }
218
+
219
+ function toolCallStart(item: CodexStartedItem): { toolName: string, input: JsonValue } | undefined {
220
+ 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 }) }
222
+ if (item.type === 'webSearch')
223
+ return { toolName: CANONICAL_TOOL.webSearch, input: toJsonObject({ query: optionalNonEmpty(item.query) }) }
224
+ 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 })) }) }
226
+ if (item.type === 'imageView')
227
+ return { toolName: CANONICAL_TOOL.viewImage, input: toJsonObject({ path: item.path }) }
228
+ if (item.type === 'imageGeneration')
229
+ return { toolName: CANONICAL_TOOL.imageGeneration, input: toJsonObject({ revisedPrompt: optionalNonEmpty(item.revisedPrompt) }) }
230
+ if (item.type === 'dynamicToolCall')
231
+ return { toolName: item.tool, input: jsonInput(item.arguments) }
232
+ if (item.type === 'collabAgentToolCall')
233
+ return { toolName: item.tool, input: collabToolInput(item) }
234
+ return undefined
235
+ }
236
+
237
+ function appendReasoningSummaryDelta({ reasoningId, summaryIndex, delta, state }: { reasoningId: string, summaryIndex: number, delta: string, state: CodexEventMapperState }): void {
238
+ const sections = state.reasoningSummaryById.get(reasoningId) ?? new Map<number, string>()
239
+ sections.set(summaryIndex, (sections.get(summaryIndex) ?? '') + delta)
240
+ state.reasoningSummaryById.set(reasoningId, sections)
241
+ }
242
+
243
+ function ensureReasoningStarted({ reasoningId, turnId, threadId, state }: { reasoningId: string, turnId: string, threadId: string, state: CodexEventMapperState }): AgentEventDraft[] {
244
+ if (state.startedReasoningIds.has(reasoningId))
245
+ return []
246
+ state.startedReasoningIds.add(reasoningId)
247
+ return emitOrBufferByThread({ draft: { type: 'reasoning.started', turnId, payload: { reasoningId } }, threadId, state })
248
+ }
249
+
250
+ function mapItemCompleted({ turnId, threadId, item, state }: { turnId: string, threadId: string, item: CodexCompletedItem, state: CodexEventMapperState }): AgentEventDraft[] {
251
+ const message = messageForItem(item, state)
252
+ if (message)
253
+ return emitOrBufferByThread({ draft: { type: 'message.ended', turnId, payload: removeUndefined({ messageId: item.id, ...message, text: messageEndedText(item) }) }, threadId, state })
254
+
255
+ const completion = toolCallCompletion(item)
256
+ if (completion)
257
+ return emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: removeUndefined({ toolCallId: item.id, ...completion }) }, threadId, state })
258
+
259
+ if (item.type === 'subAgentActivity')
260
+ return mapSubAgentActivityCompleted({ turnId, threadId, item, state })
261
+
262
+ if (item.type === 'contextCompaction')
263
+ return emitOrBufferByThread({ draft: { type: 'context.compaction.completed', turnId, payload: { compactionId: item.id, trigger: 'unknown', status: 'completed' } }, threadId, state })
264
+
265
+ if (item.type === 'reasoning')
266
+ return mapReasoningCompleted({ turnId, threadId, item, state })
267
+
268
+ if (item.type === 'collabAgentToolCall')
269
+ return mapCollabAgentToolCompleted({ turnId, threadId, item, mapperState: state })
270
+
271
+ return []
272
+ }
273
+
274
+ function mapReasoningCompleted({ turnId, threadId, item, state }: { turnId: string, threadId: string, item: Extract<CodexCompletedItem, { type: 'reasoning' }>, state: CodexEventMapperState }): AgentEventDraft[] {
275
+ const accumulatedSummary = accumulatedReasoningSummary(item.id, state)
276
+ state.reasoningSummaryById.delete(item.id)
277
+
278
+ const authoritativeSummary = summaryIfDisplayable(item.summary?.join('\n'))
279
+ const summary = authoritativeSummary ?? accumulatedSummary
280
+ if (!summary)
281
+ return []
282
+
283
+ return [
284
+ ...ensureReasoningStarted({ reasoningId: item.id, turnId, threadId, state }),
285
+ ...emitOrBufferByThread({ draft: { type: 'reasoning.ended', turnId, payload: removeUndefined({ reasoningId: item.id, summary }) }, threadId, state }),
286
+ ]
287
+ }
288
+
289
+ function accumulatedReasoningSummary(reasoningId: string, state: CodexEventMapperState): string | undefined {
290
+ return summaryIfDisplayable([...state.reasoningSummaryById.get(reasoningId)?.entries() ?? []]
291
+ .sort(([left], [right]) => left - right)
292
+ .map(([, section]) => section)
293
+ .join('\n'))
294
+ }
295
+
296
+ function summaryIfDisplayable(value: string | undefined): string | undefined {
297
+ return optionalNonEmpty(value) ? value : undefined
298
+ }
299
+
300
+ function messageForItem(item: CodexStartedItem | CodexCompletedItem, state: CodexEventMapperState): { role: AgentMessageRole, commandId?: string } | undefined {
301
+ if (item.type === 'agentMessage')
302
+ return { role: 'assistant' }
303
+ if (item.type === 'hookPrompt')
304
+ return { role: 'system' }
305
+ if (item.type !== 'userMessage')
306
+ return undefined
307
+
308
+ const promptCommand = item.clientId ? state.promptCommandByClientUserMessageId.get(item.clientId) : undefined
309
+ const role: AgentMessageRole = promptCommand?.origin === 'supervisor' ? 'system' : 'user'
310
+ return removeUndefined({ role, commandId: promptCommand?.commandId })
311
+ }
312
+
313
+ function toolCallCompletion(item: CodexCompletedItem): { status: AgentToolCallStatus, output?: AgentToolResult, error?: AgentError } | undefined {
314
+ if (item.type === 'webSearch')
315
+ return { status: 'completed', output: webSearchResult(item) }
316
+ if (item.type === 'fileChange') {
317
+ const status = fileChangeStatus(item.status)
318
+ return { status, output: fileChangeResult(item, status), error: status === 'failed' ? { message: 'Codex failed to apply the patch.' } : undefined }
319
+ }
320
+ if (item.type === 'imageView')
321
+ return { status: 'completed', output: { success: true, content: [{ type: 'text', text: item.path }] } }
322
+ if (item.type === 'imageGeneration')
323
+ return { status: item.status === 'failed' ? 'failed' : 'completed', output: imageGenerationResult(item) }
324
+ if (item.type === 'commandExecution') {
325
+ const status = item.exitCode === undefined || item.exitCode === null || item.exitCode === 0 ? 'completed' : 'failed'
326
+ return { status, output: commandExecutionResult(item, status), error: 'error' in item && typeof item.error === 'string' ? { message: item.error } : undefined }
327
+ }
328
+ if (item.type === 'dynamicToolCall')
329
+ return { status: item.success === false ? 'failed' : 'completed', output: dynamicToolResult(item) }
330
+ return undefined
331
+ }
332
+
333
+ function webSearchResult(item: CodexWebSearchItem): AgentToolResult {
334
+ const lines: string[] = []
335
+ const query = optionalNonEmpty(item.query)
336
+ if (query)
337
+ lines.push(`Query: ${query}`)
338
+ const url = webSearchActionUrl(item.action)
339
+ if (url)
340
+ lines.push(url)
341
+ return { success: true, content: [{ type: 'text', text: lines.join('\n') || 'Web search completed.' }] }
342
+ }
343
+
344
+ function webSearchActionUrl(action: CodexWebSearchItem['action']): string | undefined {
345
+ if (action && (action.type === 'openPage' || action.type === 'findInPage'))
346
+ return optionalNonEmpty(action.url)
347
+ return undefined
348
+ }
349
+
350
+ function fileChangeStatus(status: CodexFileChangeItem['status']): AgentToolCallStatus {
351
+ if (status === 'failed')
352
+ return 'failed'
353
+ if (status === 'declined')
354
+ return 'cancelled'
355
+ return 'completed'
356
+ }
357
+
358
+ function fileChangeResult(item: CodexFileChangeItem, status: AgentToolCallStatus): AgentToolResult | undefined {
359
+ const text = item.changes.map(change => change.diff).filter(diff => diff && diff.trim()).join('\n')
360
+ if (!text)
361
+ return undefined
362
+ return { success: status === 'completed', content: [{ type: 'text', text }] }
363
+ }
364
+
365
+ function imageGenerationResult(item: CodexImageGenerationItem): AgentToolResult | undefined {
366
+ const lines: string[] = []
367
+ const savedPath = optionalNonEmpty(item.savedPath)
368
+ if (savedPath)
369
+ lines.push(`Saved to ${savedPath}`)
370
+ const revised = optionalNonEmpty(item.revisedPrompt)
371
+ if (revised)
372
+ lines.push(`Revised prompt: ${revised}`)
373
+ if (!lines.length)
374
+ return undefined
375
+ return { success: item.status !== 'failed', content: [{ type: 'text', text: lines.join('\n') }] }
376
+ }
377
+
378
+ function messageEndedText(item: CodexCompletedItem): string | undefined {
379
+ if (item.type === 'userMessage')
380
+ return userMessageText(item)
381
+ if (item.type === 'hookPrompt')
382
+ return hookPromptText(item)
383
+ if (item.type === 'agentMessage')
384
+ return item.text || undefined
385
+ return undefined
386
+ }
387
+
388
+ function userMessageText(item: CodexUserMessageItem): string | undefined {
389
+ const parts: string[] = []
390
+ for (const entry of item.content) {
391
+ if (entry.type === 'text')
392
+ parts.push(entry.text)
393
+ }
394
+ return optionalNonEmpty(stripAuthorContext(parts.join('\n')))
395
+ }
396
+
397
+ function hookPromptText(item: CodexHookPromptItem): string | undefined {
398
+ return optionalNonEmpty(item.fragments.map(fragment => fragment.text).join('\n'))
399
+ }
400
+
401
+ function commandExecutionResult(item: Extract<CodexCompletedItem, { type: 'commandExecution' }>, status: 'completed' | 'failed'): AgentToolResult | undefined {
402
+ const text = optionalNonEmpty(item.aggregatedOutput ?? undefined)
403
+ if (!text)
404
+ return undefined
405
+
406
+ return { success: status === 'completed', content: [{ type: 'text', text }] }
407
+ }
408
+
409
+ function dynamicToolResult(item: CodexDynamicToolCompletedItem): AgentToolResult | undefined {
410
+ if (!item.contentItems)
411
+ return undefined
412
+
413
+ return {
414
+ success: item.success ?? item.status !== 'failed',
415
+ content: item.contentItems.map(dynamicToolContent),
416
+ }
417
+ }
418
+
419
+ function dynamicToolContent(item: CodexDynamicToolContentItem): AgentToolContent {
420
+ if (item.type === 'inputText')
421
+ return { type: 'text', text: item.text }
422
+
423
+ if (item.type === 'inputAudio')
424
+ return { type: 'text', text: item.audioUrl }
425
+
426
+ const dataUrl = /^data:([^;,]+);base64,(.*)$/s.exec(item.imageUrl)
427
+ if (dataUrl)
428
+ return { type: 'image', mimeType: dataUrl[1] ?? 'application/octet-stream', data: dataUrl[2] ?? '' }
429
+
430
+ return { type: 'text', text: item.imageUrl }
431
+ }
432
+
433
+ function mapCollabAgentToolCompleted({ turnId, threadId, item, mapperState }: { turnId: string, threadId: string, item: CodexCollabAgentToolCompletedItem, mapperState: CodexEventMapperState }): AgentEventDraft[] {
434
+ const drafts = emitOrBufferByThread({
435
+ draft: {
436
+ type: 'tool.call.completed',
437
+ turnId,
438
+ payload: removeUndefined({
439
+ toolCallId: item.id,
440
+ status: item.status === 'failed' ? 'failed' as const : 'completed' as const,
441
+ error: item.status === 'failed' ? { message: `Codex ${item.tool} failed.` } : undefined,
442
+ }),
443
+ },
444
+ threadId,
445
+ state: mapperState,
446
+ })
447
+
448
+ if (item.tool === 'spawnAgent') {
449
+ const subagentId = item.receiverThreadIds.at(0)
450
+ if (!subagentId || item.status === 'failed' || mapperState.subagentByThreadId.has(subagentId))
451
+ return drafts
452
+
453
+ const actor = subagentActorFromCollabItem({ subagentId, parentToolCallId: item.id, name: mapperState.subagentNameByThreadId.get(subagentId) })
454
+ mapperState.subagentByThreadId.set(subagentId, actor)
455
+ mapperState.subagentById.set(subagentId, actor)
456
+ drafts.push({
457
+ type: 'subagent.started',
458
+ turnId,
459
+ actor,
460
+ payload: removeUndefined({
461
+ ...subagentPayloadRef(actor),
462
+ description: optionalNonEmpty(item.prompt),
463
+ }),
464
+ })
465
+ drafts.push(...flushPendingDrafts(subagentId, mapperState))
466
+ return drafts
467
+ }
468
+
469
+ if (item.tool !== 'wait' && item.tool !== 'closeAgent')
470
+ return drafts
471
+
472
+ drafts.push(...Object.entries(item.agentsStates).flatMap(([subagentId, state]) => {
473
+ const status = subagentStatusFromAgentState(state)
474
+ const actor = mapperState.subagentById.get(subagentId)
475
+ if (!status || !actor || mapperState.endedSubagentIds.has(subagentId))
476
+ return []
477
+
478
+ mapperState.endedSubagentIds.add(subagentId)
479
+ return [{
480
+ type: 'subagent.ended',
481
+ turnId,
482
+ actor,
483
+ payload: removeUndefined({
484
+ ...subagentPayloadRef(actor),
485
+ status,
486
+ summary: optionalNonEmpty(state.message),
487
+ error: status === 'failed' ? { message: optionalNonEmpty(state.message) ?? `Subagent ${subagentId} failed.` } : undefined,
488
+ }),
489
+ } satisfies AgentEventDraft]
490
+ }))
491
+ return drafts
492
+ }
493
+
494
+ function mapSubAgentActivityCompleted(input: { turnId: string, threadId: string, item: CodexSubAgentActivityCompletedItem, state: CodexEventMapperState }): AgentEventDraft[] {
495
+ const { turnId, threadId, item, state } = input
496
+ if (item.kind === 'started') {
497
+ if (state.subagentByThreadId.has(item.agentThreadId))
498
+ return []
499
+
500
+ const actor = subagentActorFromCollabItem({
501
+ subagentId: item.agentThreadId,
502
+ parentToolCallId: item.id,
503
+ name: item.agentPath,
504
+ })
505
+ state.subagentByThreadId.set(item.agentThreadId, actor)
506
+ state.subagentById.set(item.agentThreadId, actor)
507
+ return [
508
+ ...emitOrBufferByThread({ draft: { type: 'tool.call.started', turnId, payload: { toolCallId: item.id, toolName: 'spawnAgent' } }, threadId, state }),
509
+ ...emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: { toolCallId: item.id, status: 'completed' } }, threadId, state }),
510
+ {
511
+ type: 'subagent.started',
512
+ turnId,
513
+ actor,
514
+ payload: subagentPayloadRef(actor),
515
+ },
516
+ ...flushPendingDrafts(item.agentThreadId, state),
517
+ ]
518
+ }
519
+
520
+ if (item.kind === 'interacted') {
521
+ return [
522
+ ...emitOrBufferByThread({ draft: { type: 'tool.call.started', turnId, payload: { toolCallId: item.id, toolName: 'sendInput' } }, threadId, state }),
523
+ ...emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: { toolCallId: item.id, status: 'completed' } }, threadId, state }),
524
+ ]
525
+ }
526
+
527
+ if (item.kind !== 'interrupted')
528
+ return []
529
+
530
+ const actor = state.subagentByThreadId.get(item.agentThreadId)
531
+ if (!actor || state.endedSubagentIds.has(item.agentThreadId))
532
+ return []
533
+
534
+ state.endedSubagentIds.add(item.agentThreadId)
535
+ return [{
536
+ type: 'subagent.ended',
537
+ turnId,
538
+ actor,
539
+ payload: {
540
+ ...subagentPayloadRef(actor),
541
+ status: 'cancelled',
542
+ },
543
+ }]
544
+ }
545
+
546
+ function mapChildTurnCompleted(input: {
547
+ threadId: string
548
+ turnId: string
549
+ turnPayload: Extract<AgentEventDraft, { type: 'turn.ended' }>['payload']
550
+ state: CodexEventMapperState
551
+ }): AgentEventDraft[] {
552
+ const actor = input.state.subagentByThreadId.get(input.threadId)
553
+ if (!actor || input.state.endedSubagentIds.has(actor.subagentId))
554
+ return []
555
+
556
+ input.state.endedSubagentIds.add(actor.subagentId)
557
+ return [subagentEndedFromTurn({ actor, turnId: input.turnId, turnPayload: input.turnPayload })]
558
+ }
559
+
560
+ function subagentEndedFromTurn(input: {
561
+ actor: CodexSubagentRef
562
+ turnId: string
563
+ turnPayload: Extract<AgentEventDraft, { type: 'turn.ended' }>['payload']
564
+ }): AgentEventDraft {
565
+ return {
566
+ type: 'subagent.ended',
567
+ turnId: input.turnId,
568
+ actor: input.actor,
569
+ payload: removeUndefined({
570
+ ...subagentPayloadRef(input.actor),
571
+ status: input.turnPayload.status === 'interrupted' ? 'cancelled' : input.turnPayload.status,
572
+ error: input.turnPayload.error,
573
+ }),
574
+ }
575
+ }
576
+
577
+ function subagentStatusFromAgentState(state: CodexCollabAgentState): AgentSubagentStatus | undefined {
578
+ if (state.status === 'completed')
579
+ return 'completed'
580
+ if (state.status === 'errored' || state.status === 'notFound')
581
+ return 'failed'
582
+ if (state.status === 'interrupted' || state.status === 'shutdown')
583
+ return 'cancelled'
584
+
585
+ return undefined
586
+ }
587
+
588
+ function agentErrorFromCodexTurnError(error: CodexTurnError): AgentError {
589
+ return removeUndefined({
590
+ message: error.message,
591
+ code: codexErrorCode(error.codexErrorInfo),
592
+ })
593
+ }
594
+
595
+ function codexErrorCode(info: CodexTurnError['codexErrorInfo']): string | undefined {
596
+ if (!info)
597
+ return undefined
598
+ if (typeof info === 'string')
599
+ return info
600
+ const key = Object.keys(info).at(0)
601
+ return key || undefined
602
+ }
603
+
604
+ function emitOrBufferByThread({ draft, threadId, state }: { draft: CodexUnownedTurnEventDraft, threadId: string, state: CodexEventMapperState }): AgentEventDraft[] {
605
+ const actor = actorForThread(threadId, state)
606
+ if (actor)
607
+ return [{ ...draft, actor }]
608
+
609
+ const pending = state.pendingDraftsByThreadId.get(threadId) ?? []
610
+ pending.push(draft)
611
+ state.pendingDraftsByThreadId.set(threadId, pending)
612
+ return []
613
+ }
614
+
615
+ function actorForThread(threadId: string, state: CodexEventMapperState): AgentEventActor | undefined {
616
+ const subagentActor = state.subagentByThreadId.get(threadId)
617
+ if (subagentActor)
618
+ return subagentActor
619
+ return state.mainThreadId === undefined || threadId === state.mainThreadId ? mainActor : undefined
620
+ }
621
+
622
+ function flushPendingDrafts(threadId: string, state: CodexEventMapperState): AgentEventDraft[] {
623
+ const actor = state.subagentByThreadId.get(threadId)
624
+ if (!actor)
625
+ return []
626
+ const pending = state.pendingDraftsByThreadId.get(threadId) ?? []
627
+ state.pendingDraftsByThreadId.delete(threadId)
628
+
629
+ const drafts: AgentEventDraft[] = []
630
+ for (const draft of pending) {
631
+ if (draft.type === 'turn.ended' && !state.endedSubagentIds.has(actor.subagentId)) {
632
+ state.endedSubagentIds.add(actor.subagentId)
633
+ drafts.push(subagentEndedFromTurn({ actor, turnId: draft.turnId, turnPayload: draft.payload }))
634
+ }
635
+ drafts.push({ ...draft, actor })
636
+ }
637
+ return drafts
638
+ }
639
+
640
+ function cacheThreadMetadata(thread: CodexThreadMetadata, state: CodexEventMapperState): void {
641
+ const name = codexSubagentDisplayName(thread)
642
+ if (name)
643
+ cacheSubagentName({ subagentId: thread.id, name }, state)
644
+ }
645
+
646
+ function cacheSubagentName(input: { subagentId: string, name: string }, state: CodexEventMapperState): void {
647
+ state.subagentNameByThreadId.set(input.subagentId, input.name)
648
+ const existing = state.subagentByThreadId.get(input.subagentId)
649
+ if (!existing || existing.name === input.name)
650
+ return
651
+
652
+ const renamed = { ...existing, name: input.name }
653
+ state.subagentByThreadId.set(input.subagentId, renamed)
654
+ state.subagentById.set(existing.subagentId, renamed)
655
+ }
656
+
657
+ function codexSubagentDisplayName(thread: CodexThreadMetadata): string | undefined {
658
+ const nickname = optionalNonEmpty(thread.agentNickname)
659
+ const role = optionalNonEmpty(thread.agentRole)
660
+ if (nickname && role)
661
+ return `${nickname} [${role}]`
662
+ return nickname ?? role
663
+ }
664
+
665
+ function subagentActorFromCollabItem(input: { subagentId: string, parentToolCallId: string, name?: string }): CodexSubagentRef {
666
+ return removeUndefined({
667
+ type: 'subagent' as const,
668
+ actorId: input.subagentId,
669
+ subagentId: input.subagentId,
670
+ parentActorId: 'main',
671
+ origin: { type: 'tool_call' as const, toolCallId: input.parentToolCallId },
672
+ parentToolCallId: input.parentToolCallId,
673
+ name: input.name,
674
+ })
675
+ }
676
+
677
+ function subagentPayloadRef(actor: CodexSubagentRef): Pick<CodexSubagentRef, 'actorId' | 'subagentId' | 'parentActorId' | 'origin' | 'parentToolCallId' | 'name'> {
678
+ return removeUndefined({
679
+ actorId: actor.actorId,
680
+ subagentId: actor.subagentId,
681
+ parentActorId: actor.parentActorId,
682
+ origin: actor.origin,
683
+ parentToolCallId: actor.parentToolCallId,
684
+ name: actor.name,
685
+ })
686
+ }
687
+
688
+ function collabToolInput(item: CodexCollabAgentToolStartedItem): JsonObject {
689
+ return toJsonObject({
690
+ senderThreadId: item.senderThreadId,
691
+ receiverThreadIds: item.receiverThreadIds.join(','),
692
+ prompt: item.prompt,
693
+ model: item.model,
694
+ reasoningEffort: typeof item.reasoningEffort === 'string' ? item.reasoningEffort : undefined,
695
+ })
696
+ }
697
+
698
+ function usageFromTokenUsage(value: CodexTokenUsage | undefined): AgentUsage | undefined {
699
+ return value ? usageFromCounts(value.last) : undefined
700
+ }
701
+
702
+ function usageFromCounts(value: CodexTokenCounts): AgentUsage {
703
+ return removeUndefined({
704
+ inputTokens: value.inputTokens,
705
+ outputTokens: value.outputTokens,
706
+ cacheReadTokens: value.cachedInputTokens,
707
+ reasoningOutputTokens: value.reasoningOutputTokens,
708
+ totalTokens: value.totalTokens,
709
+ })
710
+ }
711
+
712
+ function jsonInput(value: unknown): JsonValue {
713
+ const parsed = jsonValueSchema.safeParse(value)
714
+ if (parsed.success)
715
+ return parsed.data
716
+ return {}
717
+ }
718
+
719
+ function toJsonObject(value: { readonly [K in string]?: string | number | boolean | null | undefined }): JsonObject {
720
+ return jsonObjectSchema.parse(Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)))
721
+ }