@meistrari/agent-core 0.0.0 → 0.1.0

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