@kodelyth/codex 2026.5.42 → 2026.6.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 (138) hide show
  1. package/package.json +17 -2
  2. package/doctor-contract-api.test.ts +0 -44
  3. package/doctor-contract-api.ts +0 -68
  4. package/harness.ts +0 -72
  5. package/index.test.ts +0 -230
  6. package/index.ts +0 -66
  7. package/media-understanding-provider.test.ts +0 -486
  8. package/media-understanding-provider.ts +0 -521
  9. package/prompt-overlay-runtime-contract.test.ts +0 -48
  10. package/prompt-overlay.ts +0 -21
  11. package/provider-catalog.ts +0 -83
  12. package/provider-discovery.ts +0 -45
  13. package/provider.test.ts +0 -384
  14. package/provider.ts +0 -243
  15. package/src/app-server/app-inventory-cache.test.ts +0 -176
  16. package/src/app-server/app-inventory-cache.ts +0 -324
  17. package/src/app-server/approval-bridge.test.ts +0 -1471
  18. package/src/app-server/approval-bridge.ts +0 -1211
  19. package/src/app-server/auth-bridge.test.ts +0 -1449
  20. package/src/app-server/auth-bridge.ts +0 -614
  21. package/src/app-server/auth-profile-runtime-contract.test.ts +0 -239
  22. package/src/app-server/capabilities.ts +0 -27
  23. package/src/app-server/client-factory.ts +0 -24
  24. package/src/app-server/client.test.ts +0 -563
  25. package/src/app-server/client.ts +0 -715
  26. package/src/app-server/compact.test.ts +0 -710
  27. package/src/app-server/compact.ts +0 -500
  28. package/src/app-server/computer-use.test.ts +0 -788
  29. package/src/app-server/computer-use.ts +0 -683
  30. package/src/app-server/config.test.ts +0 -879
  31. package/src/app-server/config.ts +0 -1038
  32. package/src/app-server/context-engine-projection.test.ts +0 -252
  33. package/src/app-server/context-engine-projection.ts +0 -403
  34. package/src/app-server/delivery-no-reply-runtime-contract.test.ts +0 -80
  35. package/src/app-server/dynamic-tool-diagnostics.ts +0 -73
  36. package/src/app-server/dynamic-tool-profile.ts +0 -69
  37. package/src/app-server/dynamic-tools.test.ts +0 -1302
  38. package/src/app-server/dynamic-tools.ts +0 -623
  39. package/src/app-server/elicitation-bridge.test.ts +0 -1056
  40. package/src/app-server/elicitation-bridge.ts +0 -783
  41. package/src/app-server/event-projector.test.ts +0 -2668
  42. package/src/app-server/event-projector.ts +0 -2057
  43. package/src/app-server/image-payload-sanitizer.test.ts +0 -49
  44. package/src/app-server/image-payload-sanitizer.ts +0 -167
  45. package/src/app-server/klaw-owned-tool-runtime-contract.test.ts +0 -456
  46. package/src/app-server/local-runtime-attribution.ts +0 -39
  47. package/src/app-server/managed-binary.test.ts +0 -139
  48. package/src/app-server/managed-binary.ts +0 -193
  49. package/src/app-server/models.test.ts +0 -246
  50. package/src/app-server/models.ts +0 -172
  51. package/src/app-server/native-hook-relay.test.ts +0 -271
  52. package/src/app-server/native-hook-relay.ts +0 -150
  53. package/src/app-server/native-subagent-task-mirror.test.ts +0 -573
  54. package/src/app-server/native-subagent-task-mirror.ts +0 -497
  55. package/src/app-server/outcome-fallback-runtime-contract.test.ts +0 -404
  56. package/src/app-server/plugin-activation.test.ts +0 -336
  57. package/src/app-server/plugin-activation.ts +0 -283
  58. package/src/app-server/plugin-app-cache-key.ts +0 -74
  59. package/src/app-server/plugin-approval-roundtrip.ts +0 -122
  60. package/src/app-server/plugin-inventory.test.ts +0 -355
  61. package/src/app-server/plugin-inventory.ts +0 -357
  62. package/src/app-server/plugin-thread-config.test.ts +0 -865
  63. package/src/app-server/plugin-thread-config.ts +0 -455
  64. package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +0 -33
  65. package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +0 -199
  66. package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +0 -102
  67. package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +0 -227
  68. package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +0 -2630
  69. package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +0 -2630
  70. package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +0 -1659
  71. package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +0 -1655
  72. package/src/app-server/protocol-validators.test.ts +0 -75
  73. package/src/app-server/protocol-validators.ts +0 -203
  74. package/src/app-server/protocol.ts +0 -520
  75. package/src/app-server/rate-limit-cache.ts +0 -48
  76. package/src/app-server/rate-limits.test.ts +0 -202
  77. package/src/app-server/rate-limits.ts +0 -583
  78. package/src/app-server/request.ts +0 -73
  79. package/src/app-server/run-attempt.context-engine.test.ts +0 -1004
  80. package/src/app-server/run-attempt.test.ts +0 -9477
  81. package/src/app-server/run-attempt.ts +0 -4683
  82. package/src/app-server/run-attempt.vision-tools.test.ts +0 -35
  83. package/src/app-server/schema-normalization-runtime-contract.test.ts +0 -206
  84. package/src/app-server/session-binding.test.ts +0 -303
  85. package/src/app-server/session-binding.ts +0 -398
  86. package/src/app-server/session-history.ts +0 -44
  87. package/src/app-server/shared-client.test.ts +0 -589
  88. package/src/app-server/shared-client.ts +0 -289
  89. package/src/app-server/side-question.test.ts +0 -1175
  90. package/src/app-server/side-question.ts +0 -1007
  91. package/src/app-server/test-support.ts +0 -48
  92. package/src/app-server/thread-lifecycle.test.ts +0 -447
  93. package/src/app-server/thread-lifecycle.ts +0 -939
  94. package/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +0 -442
  95. package/src/app-server/timeout.ts +0 -9
  96. package/src/app-server/tool-progress-normalization.ts +0 -77
  97. package/src/app-server/trajectory.test.ts +0 -205
  98. package/src/app-server/trajectory.ts +0 -365
  99. package/src/app-server/transcript-mirror.test.ts +0 -524
  100. package/src/app-server/transcript-mirror.ts +0 -208
  101. package/src/app-server/transcript-repair-runtime-contract.test.ts +0 -44
  102. package/src/app-server/transport-stdio.test.ts +0 -171
  103. package/src/app-server/transport-stdio.ts +0 -107
  104. package/src/app-server/transport-websocket.test.ts +0 -69
  105. package/src/app-server/transport-websocket.ts +0 -90
  106. package/src/app-server/transport.ts +0 -117
  107. package/src/app-server/user-input-bridge.test.ts +0 -249
  108. package/src/app-server/user-input-bridge.ts +0 -316
  109. package/src/app-server/version.ts +0 -4
  110. package/src/app-server/vision-tools.ts +0 -12
  111. package/src/command-account.ts +0 -544
  112. package/src/command-formatters.ts +0 -425
  113. package/src/command-handlers.ts +0 -2004
  114. package/src/command-rpc.test.ts +0 -16
  115. package/src/command-rpc.ts +0 -142
  116. package/src/commands.test.ts +0 -3312
  117. package/src/commands.ts +0 -65
  118. package/src/conversation-binding-data.ts +0 -124
  119. package/src/conversation-binding.test.ts +0 -599
  120. package/src/conversation-binding.ts +0 -561
  121. package/src/conversation-control.test.ts +0 -126
  122. package/src/conversation-control.ts +0 -303
  123. package/src/conversation-turn-collector.test.ts +0 -191
  124. package/src/conversation-turn-collector.ts +0 -186
  125. package/src/conversation-turn-input.test.ts +0 -141
  126. package/src/conversation-turn-input.ts +0 -106
  127. package/src/manifest.test.ts +0 -20
  128. package/src/migration/apply.ts +0 -501
  129. package/src/migration/helpers.ts +0 -55
  130. package/src/migration/plan.ts +0 -461
  131. package/src/migration/provider.test.ts +0 -1741
  132. package/src/migration/provider.ts +0 -41
  133. package/src/migration/source.ts +0 -643
  134. package/src/migration/targets.ts +0 -25
  135. package/src/node-cli-sessions.test.ts +0 -180
  136. package/src/node-cli-sessions.ts +0 -711
  137. package/test-api.ts +0 -82
  138. package/tsconfig.json +0 -16
@@ -1,2057 +0,0 @@
1
- import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
2
- import {
3
- classifyAgentHarnessTerminalOutcome,
4
- embeddedAgentLog,
5
- emitAgentEvent as emitGlobalAgentEvent,
6
- formatErrorMessage,
7
- formatToolAggregate,
8
- formatToolProgressOutput,
9
- inferToolMetaFromArgs,
10
- normalizeUsage,
11
- runAgentHarnessAfterCompactionHook,
12
- runAgentHarnessAfterToolCallHook,
13
- runAgentHarnessBeforeCompactionHook,
14
- TOOL_PROGRESS_OUTPUT_MAX_CHARS,
15
- type AgentMessage,
16
- type EmbeddedRunAttemptParams,
17
- type EmbeddedRunAttemptResult,
18
- type HeartbeatToolResponse,
19
- type MessagingToolSend,
20
- type MessagingToolSourceReplyPayload,
21
- type ToolProgressDetailMode,
22
- } from "klaw/plugin-sdk/agent-harness-runtime";
23
- import { emitTrustedDiagnosticEvent } from "klaw/plugin-sdk/diagnostic-runtime";
24
- import { resolveCodexLocalRuntimeAttribution } from "./local-runtime-attribution.js";
25
- import { CodexNativeSubagentTaskMirror } from "./native-subagent-task-mirror.js";
26
- import { readCodexTurn } from "./protocol-validators.js";
27
- import {
28
- isJsonObject,
29
- type CodexDynamicToolCallOutputContentItem,
30
- type CodexServerNotification,
31
- type CodexThreadItem,
32
- type CodexTurn,
33
- type JsonObject,
34
- type JsonValue,
35
- } from "./protocol.js";
36
- import { readRecentCodexRateLimits, rememberCodexRateLimits } from "./rate-limit-cache.js";
37
- import { formatCodexUsageLimitErrorMessage } from "./rate-limits.js";
38
- import { readCodexMirroredSessionHistoryMessages } from "./session-history.js";
39
- import {
40
- resolveCodexToolProgressDetailMode,
41
- sanitizeCodexAgentEventRecord,
42
- sanitizeCodexToolArguments,
43
- } from "./tool-progress-normalization.js";
44
- import type { CodexTrajectoryRecorder } from "./trajectory.js";
45
- import { attachCodexMirrorIdentity, buildCodexUserPromptMessage } from "./transcript-mirror.js";
46
-
47
- export type CodexAppServerToolTelemetry = {
48
- didSendViaMessagingTool: boolean;
49
- messagingToolSentTexts: string[];
50
- messagingToolSentMediaUrls: string[];
51
- messagingToolSentTargets: MessagingToolSend[];
52
- messagingToolSourceReplyPayloads?: MessagingToolSourceReplyPayload[];
53
- heartbeatToolResponse?: HeartbeatToolResponse;
54
- toolMediaUrls?: string[];
55
- toolAudioAsVoice?: boolean;
56
- successfulCronAdds?: number;
57
- };
58
-
59
- export type CodexAppServerEventProjectorOptions = {
60
- nativePostToolUseRelayEnabled?: boolean;
61
- trajectoryRecorder?: CodexTrajectoryRecorder | null;
62
- };
63
-
64
- const ZERO_USAGE: Usage = {
65
- input: 0,
66
- output: 0,
67
- cacheRead: 0,
68
- cacheWrite: 0,
69
- totalTokens: 0,
70
- cost: {
71
- input: 0,
72
- output: 0,
73
- cacheRead: 0,
74
- cacheWrite: 0,
75
- total: 0,
76
- },
77
- };
78
-
79
- const CURRENT_TOKEN_USAGE_KEYS = [
80
- "last",
81
- "current",
82
- "lastCall",
83
- "lastCallUsage",
84
- "lastTokenUsage",
85
- "last_token_usage",
86
- ] as const;
87
-
88
- const CODEX_PROMPT_TOTAL_INPUT_KEYS = [
89
- "inputTokens",
90
- "input_tokens",
91
- "promptTokens",
92
- "prompt_tokens",
93
- ] as const;
94
-
95
- const MAX_TOOL_OUTPUT_DELTA_MESSAGES_PER_ITEM = 20;
96
- const TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS = 12_000;
97
- const TRANSCRIPT_PROGRESS_SUPPRESSED_TOOL_NAMES = new Set([
98
- "message",
99
- "messages",
100
- "reply",
101
- "send",
102
- "reaction",
103
- "react",
104
- "typing",
105
- ]);
106
-
107
- export function shouldEmitTranscriptToolProgress(toolName: unknown, args?: unknown): boolean {
108
- const normalized = typeof toolName === "string" ? toolName.trim().toLowerCase() : "";
109
- return Boolean(
110
- normalized &&
111
- !TRANSCRIPT_PROGRESS_SUPPRESSED_TOOL_NAMES.has(normalized) &&
112
- !isActivityLogCommandProgress(normalized, args),
113
- );
114
- }
115
-
116
- type ToolTranscriptCallInput = {
117
- id: string;
118
- name: string;
119
- arguments?: unknown;
120
- };
121
-
122
- type ToolTranscriptResultInput = {
123
- id: string;
124
- name: string;
125
- text?: string;
126
- isError: boolean;
127
- };
128
-
129
- export class CodexAppServerEventProjector {
130
- private readonly assistantTextByItem = new Map<string, string>();
131
- private readonly assistantItemOrder: string[] = [];
132
- private readonly assistantPhaseByItem = new Map<string, string>();
133
- private readonly lastCommentaryProgressTextByItem = new Map<string, string>();
134
- private readonly reasoningTextByItem = new Map<string, string>();
135
- private readonly planTextByItem = new Map<string, string>();
136
- private readonly activeItemIds = new Set<string>();
137
- private readonly completedItemIds = new Set<string>();
138
- private readonly activeCompactionItemIds = new Set<string>();
139
- private readonly toolProgressTexts = new Set<string>();
140
- private readonly toolResultSummaryItemIds = new Set<string>();
141
- private readonly toolResultOutputItemIds = new Set<string>();
142
- private readonly toolResultOutputStreamedItemIds = new Set<string>();
143
- private readonly transcriptToolProgressSuppressedIds = new Set<string>();
144
- private readonly toolTranscriptArgumentsById = new Map<string, unknown>();
145
- private readonly toolResultOutputDeltaState = new Map<
146
- string,
147
- { chars: number; messages: number; truncated: boolean }
148
- >();
149
- private readonly toolResultOutputTextByItem = new Map<string, string>();
150
- private readonly toolMetas = new Map<string, { toolName: string; meta?: string }>();
151
- private readonly toolTranscriptMessages: AgentMessage[] = [];
152
- private readonly toolTranscriptCallIds = new Set<string>();
153
- private readonly toolTranscriptResultIds = new Set<string>();
154
- private readonly transcriptToolProgressCallIds = new Set<string>();
155
- private lastNativeToolError: EmbeddedRunAttemptResult["lastToolError"];
156
- private readonly nativeGeneratedMediaUrls = new Set<string>();
157
- private readonly diagnosticToolStartedAtByItem = new Map<string, number>();
158
- private readonly afterToolCallObservedItemIds = new Set<string>();
159
- private assistantStarted = false;
160
- private reasoningStarted = false;
161
- private reasoningEnded = false;
162
- private completedTurn: CodexTurn | undefined;
163
- private promptError: unknown;
164
- private promptErrorSource: EmbeddedRunAttemptResult["promptErrorSource"] = null;
165
- private aborted = false;
166
- private tokenUsage: ReturnType<typeof normalizeUsage>;
167
- private guardianReviewCount = 0;
168
- private completedCompactionCount = 0;
169
- private latestRateLimits: JsonValue | undefined;
170
- private readonly nativeSubagentTaskMirror: CodexNativeSubagentTaskMirror;
171
-
172
- constructor(
173
- private readonly params: EmbeddedRunAttemptParams,
174
- private readonly threadId: string,
175
- private readonly turnId: string,
176
- private readonly options: CodexAppServerEventProjectorOptions = {},
177
- ) {
178
- this.nativeSubagentTaskMirror = new CodexNativeSubagentTaskMirror({
179
- parentThreadId: threadId,
180
- requesterSessionKey: params.sessionKey,
181
- agentId: params.agentId,
182
- });
183
- }
184
-
185
- async handleNotification(notification: CodexServerNotification): Promise<void> {
186
- const params = isJsonObject(notification.params) ? notification.params : undefined;
187
- if (!params) {
188
- return;
189
- }
190
- try {
191
- this.nativeSubagentTaskMirror.handleNotification(notification);
192
- } catch (error) {
193
- embeddedAgentLog.warn("Failed to mirror Codex native subagent lifecycle event", {
194
- method: notification.method,
195
- error: formatErrorMessage(error),
196
- });
197
- }
198
- if (notification.method === "account/rateLimits/updated") {
199
- this.latestRateLimits = params;
200
- rememberCodexRateLimits(params);
201
- return;
202
- }
203
- if (isHookNotificationMethod(notification.method)) {
204
- if (!this.isHookNotificationForCurrentThread(params)) {
205
- return;
206
- }
207
- } else if (!this.isNotificationForTurn(params)) {
208
- return;
209
- }
210
-
211
- switch (notification.method) {
212
- case "item/agentMessage/delta":
213
- await this.handleAssistantDelta(params);
214
- break;
215
- case "item/reasoning/summaryTextDelta":
216
- case "item/reasoning/textDelta":
217
- await this.handleReasoningDelta(params);
218
- break;
219
- case "item/plan/delta":
220
- this.handlePlanDelta(params);
221
- break;
222
- case "turn/plan/updated":
223
- this.handleTurnPlanUpdated(params);
224
- break;
225
- case "item/started":
226
- await this.handleItemStarted(params);
227
- break;
228
- case "item/completed":
229
- await this.handleItemCompleted(params);
230
- break;
231
- case "item/commandExecution/outputDelta":
232
- this.handleOutputDelta(params, "bash");
233
- break;
234
- case "item/fileChange/outputDelta":
235
- this.handleOutputDelta(params, "apply_patch");
236
- break;
237
- case "item/autoApprovalReview/started":
238
- case "item/autoApprovalReview/completed":
239
- this.handleGuardianReviewNotification(notification.method, params);
240
- break;
241
- case "hook/started":
242
- case "hook/completed":
243
- this.handleHookNotification(notification.method, params);
244
- break;
245
- case "thread/tokenUsage/updated":
246
- this.handleTokenUsage(params);
247
- break;
248
- case "turn/completed":
249
- await this.handleTurnCompleted(params);
250
- break;
251
- case "rawResponseItem/completed":
252
- this.handleRawResponseItemCompleted(params);
253
- break;
254
- case "error":
255
- if (readBooleanAlias(params, ["willRetry", "will_retry"]) === true) {
256
- break;
257
- }
258
- this.promptError = this.formatCodexErrorMessage(params) ?? "codex app-server error";
259
- this.promptErrorSource = "prompt";
260
- break;
261
- default:
262
- break;
263
- }
264
- }
265
-
266
- buildResult(
267
- toolTelemetry: CodexAppServerToolTelemetry,
268
- options?: { yieldDetected?: boolean },
269
- ): EmbeddedRunAttemptResult {
270
- const assistantTexts = this.collectAssistantTexts();
271
- const reasoningText = collectTextValues(this.reasoningTextByItem).join("\n\n");
272
- const planText = collectTextValues(this.planTextByItem).join("\n\n");
273
- const lastAssistant =
274
- assistantTexts.length > 0
275
- ? this.createAssistantMessage(assistantTexts.join("\n\n"))
276
- : undefined;
277
- // Each snapshot entry is tagged with a stable mirror identity of the
278
- // shape `${turnId}:${kind}`. The mirror's idempotency key is derived
279
- // from this identity rather than from snapshot position or content
280
- // hash, so:
281
- // - Re-mirror of the same turn (retry) → same identity → no-op.
282
- // - Re-emit of a prior turn's entry into a later turn's snapshot
283
- // (the cross-turn drift mode named in #77012) → original identity
284
- // is preserved → on-disk key still matches → also a no-op.
285
- // - Two distinct turns where the user repeats verbatim content →
286
- // distinct turnIds → distinct identities → both kept.
287
- const turnId = this.turnId;
288
- const messagesSnapshot: AgentMessage[] = [
289
- attachCodexMirrorIdentity(buildCodexUserPromptMessage(this.params), `${turnId}:prompt`),
290
- ];
291
- // Codex owns the canonical thread. These mirror records keep enough local
292
- // context for Klaw history, search, and future harness switching.
293
- if (reasoningText) {
294
- messagesSnapshot.push(
295
- attachCodexMirrorIdentity(
296
- this.createAssistantMirrorMessage("Codex reasoning", reasoningText),
297
- `${turnId}:reasoning`,
298
- ),
299
- );
300
- }
301
- if (planText) {
302
- messagesSnapshot.push(
303
- attachCodexMirrorIdentity(
304
- this.createAssistantMirrorMessage("Codex plan", planText),
305
- `${turnId}:plan`,
306
- ),
307
- );
308
- }
309
- messagesSnapshot.push(...this.toolTranscriptMessages);
310
- if (lastAssistant) {
311
- messagesSnapshot.push(attachCodexMirrorIdentity(lastAssistant, `${turnId}:assistant`));
312
- }
313
- const turnFailed = this.completedTurn?.status === "failed";
314
- const turnInterrupted = this.completedTurn?.status === "interrupted";
315
- const promptError =
316
- this.promptError ??
317
- (turnFailed ? (this.completedTurn?.error?.message ?? "codex app-server turn failed") : null);
318
- const agentHarnessResultClassification = classifyAgentHarnessTerminalOutcome({
319
- assistantTexts,
320
- reasoningText,
321
- planText,
322
- promptError,
323
- turnCompleted: Boolean(this.completedTurn),
324
- });
325
- return {
326
- aborted: this.aborted || turnInterrupted,
327
- externalAbort: false,
328
- timedOut: false,
329
- idleTimedOut: false,
330
- timedOutDuringCompaction: false,
331
- timedOutDuringToolExecution: false,
332
- promptError,
333
- promptErrorSource: promptError ? this.promptErrorSource || "prompt" : null,
334
- sessionIdUsed: this.params.sessionId,
335
- ...(agentHarnessResultClassification ? { agentHarnessResultClassification } : {}),
336
- bootstrapPromptWarningSignaturesSeen: this.params.bootstrapPromptWarningSignaturesSeen,
337
- bootstrapPromptWarningSignature: this.params.bootstrapPromptWarningSignature,
338
- messagesSnapshot,
339
- assistantTexts,
340
- toolMetas: [...this.toolMetas.values()],
341
- lastAssistant,
342
- ...(this.lastNativeToolError ? { lastToolError: this.lastNativeToolError } : {}),
343
- didSendViaMessagingTool: toolTelemetry.didSendViaMessagingTool,
344
- messagingToolSentTexts: toolTelemetry.messagingToolSentTexts,
345
- messagingToolSentMediaUrls: toolTelemetry.messagingToolSentMediaUrls,
346
- messagingToolSentTargets: toolTelemetry.messagingToolSentTargets,
347
- messagingToolSourceReplyPayloads: toolTelemetry.messagingToolSourceReplyPayloads ?? [],
348
- heartbeatToolResponse: toolTelemetry.heartbeatToolResponse,
349
- toolMediaUrls: this.buildToolMediaUrls(toolTelemetry),
350
- toolAudioAsVoice: toolTelemetry.toolAudioAsVoice,
351
- successfulCronAdds: toolTelemetry.successfulCronAdds,
352
- cloudCodeAssistFormatError: false,
353
- attemptUsage: this.tokenUsage,
354
- replayMetadata: {
355
- hadPotentialSideEffects: toolTelemetry.didSendViaMessagingTool,
356
- replaySafe: !toolTelemetry.didSendViaMessagingTool,
357
- },
358
- itemLifecycle: {
359
- startedCount: this.activeItemIds.size + this.completedItemIds.size,
360
- completedCount: this.completedItemIds.size,
361
- activeCount: this.activeItemIds.size,
362
- ...(this.completedCompactionCount > 0
363
- ? { compactionCount: this.completedCompactionCount }
364
- : {}),
365
- },
366
- yieldDetected: options?.yieldDetected || false,
367
- didSendDeterministicApprovalPrompt: this.guardianReviewCount > 0 ? false : undefined,
368
- };
369
- }
370
-
371
- recordDynamicToolCall(params: { callId: string; tool: string; arguments?: JsonValue }): void {
372
- this.recordToolTranscriptCall({
373
- id: params.callId,
374
- name: params.tool,
375
- arguments: sanitizeCodexToolArguments(params.arguments),
376
- });
377
- }
378
-
379
- recordDynamicToolResult(params: {
380
- callId: string;
381
- tool: string;
382
- success: boolean;
383
- contentItems: CodexDynamicToolCallOutputContentItem[];
384
- }): void {
385
- this.recordToolTranscriptResult({
386
- id: params.callId,
387
- name: params.tool,
388
- text: collectDynamicToolContentText(params.contentItems),
389
- isError: !params.success,
390
- });
391
- }
392
-
393
- markTimedOut(): void {
394
- this.aborted = true;
395
- this.promptError = "codex app-server attempt timed out";
396
- this.promptErrorSource = "prompt";
397
- }
398
-
399
- markAborted(): void {
400
- this.aborted = true;
401
- }
402
-
403
- isCompacting(): boolean {
404
- return this.activeCompactionItemIds.size > 0;
405
- }
406
-
407
- private async handleAssistantDelta(params: JsonObject): Promise<void> {
408
- const itemId = readString(params, "itemId") ?? readString(params, "id") ?? "assistant";
409
- const delta = readString(params, "delta") ?? "";
410
- if (!delta) {
411
- return;
412
- }
413
- if (!this.assistantStarted) {
414
- this.assistantStarted = true;
415
- await this.params.onAssistantMessageStart?.();
416
- }
417
- this.rememberAssistantItem(itemId);
418
- const text = `${this.assistantTextByItem.get(itemId) ?? ""}${delta}`;
419
- this.assistantTextByItem.set(itemId, text);
420
- if (this.isCommentaryAssistantItem(itemId)) {
421
- this.emitCommentaryProgress({ itemId, text });
422
- }
423
- // Codex app-server can emit multiple agentMessage items per turn, including
424
- // intermediate coordination/progress prose. Keep those deltas internal until
425
- // turn completion chooses the last assistant item as the user-visible reply.
426
- }
427
-
428
- private async handleReasoningDelta(params: JsonObject): Promise<void> {
429
- const itemId = readString(params, "itemId") ?? readString(params, "id") ?? "reasoning";
430
- const delta = readString(params, "delta") ?? "";
431
- if (!delta) {
432
- return;
433
- }
434
- this.reasoningStarted = true;
435
- this.reasoningTextByItem.set(itemId, `${this.reasoningTextByItem.get(itemId) ?? ""}${delta}`);
436
- await this.params.onReasoningStream?.({ text: delta });
437
- }
438
-
439
- private handlePlanDelta(params: JsonObject): void {
440
- const itemId = readString(params, "itemId") ?? readString(params, "id") ?? "plan";
441
- const delta = readString(params, "delta") ?? "";
442
- if (!delta) {
443
- return;
444
- }
445
- const text = `${this.planTextByItem.get(itemId) ?? ""}${delta}`;
446
- this.planTextByItem.set(itemId, text);
447
- this.emitPlanUpdate({ explanation: undefined, steps: splitPlanText(text) });
448
- }
449
-
450
- private handleTurnPlanUpdated(params: JsonObject): void {
451
- const plan = Array.isArray(params.plan)
452
- ? params.plan.flatMap((entry) => {
453
- if (!isJsonObject(entry)) {
454
- return [];
455
- }
456
- const step = readString(entry, "step");
457
- const status = readString(entry, "status");
458
- if (!step) {
459
- return [];
460
- }
461
- return status ? [`${step} (${status})`] : [step];
462
- })
463
- : undefined;
464
- this.emitPlanUpdate({
465
- explanation: readNullableString(params, "explanation"),
466
- steps: plan,
467
- });
468
- }
469
-
470
- private async handleItemStarted(params: JsonObject): Promise<void> {
471
- const item = readItem(params.item);
472
- const itemId = item?.id ?? readString(params, "itemId") ?? readString(params, "id");
473
- this.rememberAssistantPhase(item);
474
- if (itemId) {
475
- this.activeItemIds.add(itemId);
476
- }
477
- if (item?.type === "contextCompaction" && itemId) {
478
- this.activeCompactionItemIds.add(itemId);
479
- await runAgentHarnessBeforeCompactionHook({
480
- sessionFile: this.params.sessionFile,
481
- messages: await this.readMirroredSessionMessages(),
482
- ctx: {
483
- runId: this.params.runId,
484
- agentId: this.params.agentId,
485
- sessionKey: this.params.sessionKey,
486
- sessionId: this.params.sessionId,
487
- workspaceDir: this.params.workspaceDir,
488
- messageProvider: this.params.messageProvider ?? undefined,
489
- trigger: this.params.trigger,
490
- channelId: this.params.messageChannel ?? this.params.messageProvider ?? undefined,
491
- },
492
- });
493
- this.emitAgentEvent({
494
- stream: "compaction",
495
- data: {
496
- phase: "start",
497
- backend: "codex-app-server",
498
- threadId: this.threadId,
499
- turnId: this.turnId,
500
- itemId,
501
- },
502
- });
503
- }
504
- this.emitStandardItemEvent({ phase: "start", item });
505
- this.emitNormalizedToolItemEvent({ phase: "start", item });
506
- this.recordNativeToolTranscriptCall(item);
507
- this.emitToolResultSummary(item);
508
- this.emitAgentEvent({
509
- stream: "codex_app_server.item",
510
- data: { phase: "started", itemId, type: item?.type },
511
- });
512
- }
513
-
514
- private async handleItemCompleted(params: JsonObject): Promise<void> {
515
- const item = readItem(params.item);
516
- const itemId = item?.id ?? readString(params, "itemId") ?? readString(params, "id");
517
- if (itemId) {
518
- this.activeItemIds.delete(itemId);
519
- this.completedItemIds.add(itemId);
520
- }
521
- this.rememberAssistantPhase(item);
522
- if (item?.type === "agentMessage" && typeof item.text === "string" && item.text) {
523
- this.rememberAssistantItem(item.id);
524
- this.assistantTextByItem.set(item.id, item.text);
525
- if (this.isCommentaryAssistantItem(item.id)) {
526
- this.emitCommentaryProgress({ itemId: item.id, text: item.text });
527
- }
528
- }
529
- this.recordNativeGeneratedMedia(item);
530
- if (item?.type === "plan" && typeof item.text === "string" && item.text) {
531
- this.planTextByItem.set(item.id, item.text);
532
- this.emitPlanUpdate({ explanation: undefined, steps: splitPlanText(item.text) });
533
- }
534
- if (item?.type === "contextCompaction" && itemId) {
535
- this.activeCompactionItemIds.delete(itemId);
536
- this.completedCompactionCount += 1;
537
- await runAgentHarnessAfterCompactionHook({
538
- sessionFile: this.params.sessionFile,
539
- messages: await this.readMirroredSessionMessages(),
540
- compactedCount: -1,
541
- ctx: {
542
- runId: this.params.runId,
543
- agentId: this.params.agentId,
544
- sessionKey: this.params.sessionKey,
545
- sessionId: this.params.sessionId,
546
- workspaceDir: this.params.workspaceDir,
547
- messageProvider: this.params.messageProvider ?? undefined,
548
- trigger: this.params.trigger,
549
- channelId: this.params.messageChannel ?? this.params.messageProvider ?? undefined,
550
- },
551
- });
552
- this.emitAgentEvent({
553
- stream: "compaction",
554
- data: {
555
- phase: "end",
556
- backend: "codex-app-server",
557
- completed: true,
558
- threadId: this.threadId,
559
- turnId: this.turnId,
560
- itemId,
561
- },
562
- });
563
- }
564
- this.recordToolMeta(item);
565
- this.emitStandardItemEvent({ phase: "end", item });
566
- this.emitNormalizedToolItemEvent({ phase: "result", item });
567
- this.recordNativeToolTranscriptCall(item);
568
- this.recordNativeToolTranscriptResult(item);
569
- this.emitToolResultSummary(item);
570
- this.emitToolResultOutput(item);
571
- this.emitAgentEvent({
572
- stream: "codex_app_server.item",
573
- data: { phase: "completed", itemId, type: item?.type },
574
- });
575
- }
576
-
577
- private handleTokenUsage(params: JsonObject): void {
578
- const tokenUsage = isJsonObject(params.tokenUsage) ? params.tokenUsage : undefined;
579
- const current =
580
- (tokenUsage ? readFirstJsonObject(tokenUsage, CURRENT_TOKEN_USAGE_KEYS) : undefined) ??
581
- readFirstJsonObject(params, CURRENT_TOKEN_USAGE_KEYS);
582
- if (!current) {
583
- return;
584
- }
585
- const usage = normalizeCodexTokenUsage(current);
586
- if (usage) {
587
- this.tokenUsage = usage;
588
- }
589
- }
590
-
591
- private handleGuardianReviewNotification(method: string, params: JsonObject): void {
592
- this.guardianReviewCount += 1;
593
- const review = isJsonObject(params.review) ? params.review : undefined;
594
- const action = isJsonObject(params.action) ? params.action : undefined;
595
- this.emitAgentEvent({
596
- stream: "codex_app_server.guardian",
597
- data: {
598
- method,
599
- phase: method.endsWith("/started") ? "started" : "completed",
600
- reviewId: readString(params, "reviewId"),
601
- targetItemId: readNullableString(params, "targetItemId"),
602
- decisionSource: readString(params, "decisionSource"),
603
- status: review ? readString(review, "status") : undefined,
604
- riskLevel: review ? readString(review, "riskLevel") : undefined,
605
- userAuthorization: review ? readString(review, "userAuthorization") : undefined,
606
- rationale: review ? readNullableString(review, "rationale") : undefined,
607
- actionType: action ? readString(action, "type") : undefined,
608
- },
609
- });
610
- }
611
-
612
- private handleHookNotification(method: string, params: JsonObject): void {
613
- const run = isJsonObject(params.run) ? params.run : undefined;
614
- if (!run) {
615
- return;
616
- }
617
- const durationMs = readNumber(run, "durationMs");
618
- const entries = readHookOutputEntries(run.entries);
619
- const hookTurnId = readNullableString(params, "turnId");
620
- this.emitAgentEvent({
621
- stream: "codex_app_server.hook",
622
- data: {
623
- phase: method === "hook/started" ? "started" : "completed",
624
- threadId: this.threadId,
625
- turnId: hookTurnId === undefined ? this.turnId : hookTurnId,
626
- hookRunId: readString(run, "id"),
627
- eventName: readString(run, "eventName"),
628
- handlerType: readString(run, "handlerType"),
629
- executionMode: readString(run, "executionMode"),
630
- scope: readString(run, "scope"),
631
- source: readString(run, "source"),
632
- sourcePath: readString(run, "sourcePath"),
633
- status: readString(run, "status"),
634
- statusMessage: readNullableString(run, "statusMessage"),
635
- ...(durationMs !== undefined ? { durationMs } : {}),
636
- ...(entries.length > 0 ? { entries } : {}),
637
- },
638
- });
639
- }
640
-
641
- private async handleTurnCompleted(params: JsonObject): Promise<void> {
642
- const turn = readTurn(params.turn);
643
- if (!turn || turn.id !== this.turnId) {
644
- return;
645
- }
646
- this.completedTurn = turn;
647
- if (turn.status === "interrupted") {
648
- this.aborted = true;
649
- }
650
- if (turn.status === "failed") {
651
- this.promptError =
652
- formatCodexUsageLimitErrorMessage({
653
- message: turn.error?.message,
654
- codexErrorInfo: turn.error?.codexErrorInfo as JsonValue | null | undefined,
655
- rateLimits: this.latestRateLimits ?? readRecentCodexRateLimits(),
656
- }) ??
657
- turn.error?.message ??
658
- "codex app-server turn failed";
659
- this.promptErrorSource = "prompt";
660
- }
661
- for (const item of turn.items ?? []) {
662
- this.rememberAssistantPhase(item);
663
- if (item.type === "agentMessage" && typeof item.text === "string" && item.text) {
664
- this.rememberAssistantItem(item.id);
665
- this.assistantTextByItem.set(item.id, item.text);
666
- }
667
- this.recordNativeGeneratedMedia(item);
668
- if (item.type === "plan" && typeof item.text === "string" && item.text) {
669
- this.planTextByItem.set(item.id, item.text);
670
- this.emitPlanUpdate({ explanation: undefined, steps: splitPlanText(item.text) });
671
- }
672
- this.recordToolMeta(item);
673
- this.emitSnapshotOnlyNativeToolProgress(item);
674
- this.recordNativeToolTranscriptCall(item);
675
- this.recordNativeToolTranscriptResult(item);
676
- this.emitAfterToolCallObservation(item);
677
- this.emitToolResultSummary(item);
678
- this.emitToolResultOutput(item);
679
- }
680
- this.activeCompactionItemIds.clear();
681
- await this.maybeEndReasoning();
682
- }
683
-
684
- private emitSnapshotOnlyNativeToolProgress(item: CodexThreadItem): void {
685
- if (
686
- !shouldSynthesizeToolProgressForItem(item) ||
687
- !this.isCurrentTurnSnapshotItem(item) ||
688
- this.completedItemIds.has(item.id) ||
689
- itemStatus(item) === "running"
690
- ) {
691
- return;
692
- }
693
- const wasStarted = this.activeItemIds.has(item.id);
694
- if (!wasStarted) {
695
- this.emitStandardItemEvent({ phase: "start", item });
696
- this.emitNormalizedToolItemEvent({ phase: "start", item });
697
- }
698
- this.activeItemIds.delete(item.id);
699
- this.emitStandardItemEvent({ phase: "end", item });
700
- this.emitNormalizedToolItemEvent({ phase: "result", item });
701
- this.completedItemIds.add(item.id);
702
- }
703
-
704
- private isCurrentTurnSnapshotItem(item: CodexThreadItem): boolean {
705
- const itemTurnId = readItemString(item, "turnId") ?? readItemString(item, "turn_id");
706
- return itemTurnId === undefined || itemTurnId === this.turnId;
707
- }
708
-
709
- private handleOutputDelta(params: JsonObject, toolName: string): void {
710
- const itemId = readString(params, "itemId");
711
- const delta = readString(params, "delta");
712
- if (!itemId || !delta) {
713
- return;
714
- }
715
- appendToolOutputDeltaText(this.toolResultOutputTextByItem, itemId, delta);
716
- if (!this.shouldEmitToolOutput()) {
717
- return;
718
- }
719
- if (
720
- this.transcriptToolProgressSuppressedIds.has(itemId) ||
721
- !shouldEmitTranscriptToolProgress(toolName, this.toolTranscriptArgumentsById.get(itemId))
722
- ) {
723
- return;
724
- }
725
- const state = this.toolResultOutputDeltaState.get(itemId) ?? {
726
- chars: 0,
727
- messages: 0,
728
- truncated: false,
729
- };
730
- if (state.truncated) {
731
- return;
732
- }
733
- const remainingChars = Math.max(0, TOOL_PROGRESS_OUTPUT_MAX_CHARS - state.chars);
734
- const remainingMessages = Math.max(0, MAX_TOOL_OUTPUT_DELTA_MESSAGES_PER_ITEM - state.messages);
735
- if (remainingChars === 0 || remainingMessages === 0) {
736
- state.truncated = true;
737
- this.toolResultOutputDeltaState.set(itemId, state);
738
- this.emitToolResultMessage({
739
- itemId,
740
- text: formatToolOutput(toolName, undefined, "(output truncated)"),
741
- });
742
- return;
743
- }
744
- const chunk = delta.length > remainingChars ? delta.slice(0, remainingChars) : delta;
745
- state.chars += chunk.length;
746
- state.messages += 1;
747
- const reachedLimit =
748
- delta.length > remainingChars ||
749
- state.chars >= TOOL_PROGRESS_OUTPUT_MAX_CHARS ||
750
- state.messages >= MAX_TOOL_OUTPUT_DELTA_MESSAGES_PER_ITEM;
751
- if (reachedLimit) {
752
- state.truncated = true;
753
- }
754
- this.toolResultOutputDeltaState.set(itemId, state);
755
- this.toolResultOutputStreamedItemIds.add(itemId);
756
- this.emitToolResultMessage({
757
- itemId,
758
- text: formatToolOutput(
759
- toolName,
760
- undefined,
761
- reachedLimit ? `${chunk}\n...(truncated)...` : chunk,
762
- ),
763
- });
764
- }
765
-
766
- private handleRawResponseItemCompleted(params: JsonObject): void {
767
- const item = isJsonObject(params.item) ? params.item : undefined;
768
- if (!item || readString(item, "role") !== "assistant") {
769
- return;
770
- }
771
- const text = extractRawAssistantText(item);
772
- if (!text) {
773
- return;
774
- }
775
- const itemId = readString(item, "id") ?? `raw-assistant-${this.assistantItemOrder.length + 1}`;
776
- const phase = readString(item, "phase");
777
- if (phase) {
778
- this.assistantPhaseByItem.set(itemId, phase);
779
- }
780
- this.rememberAssistantItem(itemId);
781
- this.assistantTextByItem.set(itemId, text);
782
- if (phase === "commentary") {
783
- this.emitCommentaryProgress({ itemId, text });
784
- }
785
- }
786
-
787
- private recordNativeGeneratedMedia(item: CodexThreadItem | undefined): void {
788
- if (item?.type !== "imageGeneration") {
789
- return;
790
- }
791
- const savedPath = readItemString(item, "savedPath")?.trim();
792
- if (savedPath) {
793
- this.nativeGeneratedMediaUrls.add(savedPath);
794
- }
795
- }
796
-
797
- private buildToolMediaUrls(toolTelemetry: CodexAppServerToolTelemetry): string[] | undefined {
798
- const mediaUrls = new Set(
799
- toolTelemetry.toolMediaUrls?.map((url) => url.trim()).filter(Boolean) ?? [],
800
- );
801
- if ((toolTelemetry.messagingToolSentMediaUrls?.length ?? 0) === 0) {
802
- for (const mediaUrl of this.nativeGeneratedMediaUrls) {
803
- mediaUrls.add(mediaUrl);
804
- }
805
- }
806
- return mediaUrls.size > 0 ? [...mediaUrls] : toolTelemetry.toolMediaUrls;
807
- }
808
-
809
- private async maybeEndReasoning(): Promise<void> {
810
- if (!this.reasoningStarted || this.reasoningEnded) {
811
- return;
812
- }
813
- this.reasoningEnded = true;
814
- await this.params.onReasoningEnd?.();
815
- }
816
-
817
- private emitPlanUpdate(params: { explanation?: string | null; steps?: string[] }): void {
818
- if (!params.explanation && (!params.steps || params.steps.length === 0)) {
819
- return;
820
- }
821
- this.emitAgentEvent({
822
- stream: "plan",
823
- data: {
824
- phase: "update",
825
- title: "Plan updated",
826
- source: "codex-app-server",
827
- ...(params.explanation ? { explanation: params.explanation } : {}),
828
- ...(params.steps && params.steps.length > 0 ? { steps: params.steps } : {}),
829
- },
830
- });
831
- }
832
-
833
- private rememberAssistantPhase(item: CodexThreadItem | undefined): void {
834
- if (item?.type !== "agentMessage") {
835
- return;
836
- }
837
- const phase = readItemString(item, "phase");
838
- if (phase) {
839
- this.assistantPhaseByItem.set(item.id, phase);
840
- }
841
- }
842
-
843
- private isCommentaryAssistantItem(itemId: string): boolean {
844
- return this.assistantPhaseByItem.get(itemId) === "commentary";
845
- }
846
-
847
- private emitCommentaryProgress(params: { itemId: string; text: string }): void {
848
- const progressText = params.text.replace(/\s+/g, " ").trim();
849
- if (
850
- !progressText ||
851
- this.lastCommentaryProgressTextByItem.get(params.itemId) === progressText
852
- ) {
853
- return;
854
- }
855
- this.lastCommentaryProgressTextByItem.set(params.itemId, progressText);
856
- this.emitAgentEvent({
857
- stream: "item",
858
- data: {
859
- itemId: params.itemId,
860
- kind: "preamble",
861
- title: "Preamble",
862
- phase: "update",
863
- progressText,
864
- source: "codex-app-server",
865
- },
866
- });
867
- }
868
-
869
- private emitStandardItemEvent(params: {
870
- phase: "start" | "end";
871
- item: CodexThreadItem | undefined;
872
- }): void {
873
- const { item } = params;
874
- if (!item) {
875
- return;
876
- }
877
- const kind = itemKind(item);
878
- if (!kind) {
879
- return;
880
- }
881
- const meta = itemMeta(item, this.toolProgressDetailMode());
882
- const suppressChannelProgress = shouldSuppressChannelProgressForItem(item);
883
- this.emitAgentEvent({
884
- stream: "item",
885
- data: {
886
- itemId: item.id,
887
- phase: params.phase,
888
- kind,
889
- title: itemTitle(item),
890
- status: params.phase === "start" ? "running" : itemStatus(item),
891
- ...(itemName(item) ? { name: itemName(item) } : {}),
892
- ...(meta ? { meta } : {}),
893
- ...(suppressChannelProgress ? { suppressChannelProgress: true } : {}),
894
- },
895
- });
896
- }
897
-
898
- private emitNormalizedToolItemEvent(params: {
899
- phase: "start" | "result";
900
- item: CodexThreadItem | undefined;
901
- }): void {
902
- const { item } = params;
903
- if (!item || !shouldSynthesizeToolProgressForItem(item)) {
904
- return;
905
- }
906
- const name = itemName(item);
907
- if (!name) {
908
- return;
909
- }
910
- const status = params.phase === "result" ? itemStatus(item) : "running";
911
- const args = itemToolArgs(item);
912
- const meta = itemMeta(item, this.toolProgressDetailMode());
913
- this.recordToolTrajectoryEvent({ phase: params.phase, item, name, args, status });
914
- this.emitDiagnosticToolExecutionEvent({ phase: params.phase, item, name, status });
915
- if (params.phase === "result") {
916
- this.recordNativeToolError({ item, name, meta, status });
917
- }
918
- if (!shouldEmitTranscriptToolProgress(name, args)) {
919
- if (params.phase === "result") {
920
- this.emitAfterToolCallObservation(item);
921
- }
922
- return;
923
- }
924
- this.emitAgentEvent({
925
- stream: "tool",
926
- data: {
927
- phase: params.phase,
928
- name,
929
- itemId: item.id,
930
- toolCallId: item.id,
931
- ...(meta ? { meta } : {}),
932
- ...(params.phase === "start" && args ? { args } : {}),
933
- ...(params.phase === "result"
934
- ? {
935
- status,
936
- isError: isNonSuccessItemStatus(status),
937
- ...itemToolResult(item),
938
- }
939
- : {}),
940
- },
941
- });
942
- if (params.phase === "result") {
943
- this.emitAfterToolCallObservation(item);
944
- }
945
- }
946
-
947
- private recordNativeToolError(params: {
948
- item: CodexThreadItem;
949
- name: string;
950
- meta?: string;
951
- status: ReturnType<typeof itemStatus>;
952
- }): void {
953
- if (!isNonSuccessItemStatus(params.status)) {
954
- if (!this.lastNativeToolError) {
955
- return;
956
- }
957
- if (!this.lastNativeToolError.mutatingAction) {
958
- this.lastNativeToolError = undefined;
959
- return;
960
- }
961
- const actionFingerprint = nativeToolActionFingerprint(params.item);
962
- if (
963
- this.lastNativeToolError.actionFingerprint &&
964
- actionFingerprint &&
965
- this.lastNativeToolError.actionFingerprint === actionFingerprint
966
- ) {
967
- this.lastNativeToolError = undefined;
968
- }
969
- return;
970
- }
971
- const error = itemToolError(params.item, params.status, this.toolResultOutputTextByItem);
972
- const actionFingerprint = nativeToolActionFingerprint(params.item);
973
- this.lastNativeToolError = {
974
- toolName: params.name,
975
- ...(params.meta ? { meta: params.meta } : {}),
976
- ...(error ? { error } : {}),
977
- ...(isMutatingNativeToolItem(params.item) ? { mutatingAction: true } : {}),
978
- ...(actionFingerprint ? { actionFingerprint } : {}),
979
- };
980
- }
981
-
982
- private recordToolTrajectoryEvent(params: {
983
- phase: "start" | "result";
984
- item: CodexThreadItem;
985
- name: string;
986
- args?: Record<string, unknown>;
987
- status: ReturnType<typeof itemStatus>;
988
- }): void {
989
- if (params.phase === "start") {
990
- this.options.trajectoryRecorder?.recordEvent("tool.call", {
991
- threadId: this.threadId,
992
- turnId: this.turnId,
993
- itemId: params.item.id,
994
- toolCallId: params.item.id,
995
- name: params.name,
996
- arguments: params.args,
997
- });
998
- return;
999
- }
1000
- const toolResult = itemToolResult(params.item).result;
1001
- const output = itemOutputText(params.item, this.toolResultOutputTextByItem);
1002
- this.options.trajectoryRecorder?.recordEvent("tool.result", {
1003
- threadId: this.threadId,
1004
- turnId: this.turnId,
1005
- itemId: params.item.id,
1006
- toolCallId: params.item.id,
1007
- name: params.name,
1008
- status: params.status,
1009
- isError: isNonSuccessItemStatus(params.status),
1010
- ...(toolResult ? { result: toolResult } : {}),
1011
- ...(output ? { output } : {}),
1012
- });
1013
- }
1014
-
1015
- private emitDiagnosticToolExecutionEvent(params: {
1016
- phase: "start" | "result";
1017
- item: CodexThreadItem;
1018
- name: string;
1019
- status: ReturnType<typeof itemStatus>;
1020
- }): void {
1021
- const base = {
1022
- runId: this.params.runId,
1023
- sessionId: this.params.sessionId,
1024
- sessionKey: this.params.sessionKey,
1025
- toolName: params.name,
1026
- toolCallId: params.item.id,
1027
- };
1028
- if (params.phase === "start") {
1029
- this.diagnosticToolStartedAtByItem.set(params.item.id, Date.now());
1030
- emitTrustedDiagnosticEvent({
1031
- type: "tool.execution.started",
1032
- ...base,
1033
- });
1034
- return;
1035
- }
1036
-
1037
- const startedAt = this.diagnosticToolStartedAtByItem.get(params.item.id);
1038
- this.diagnosticToolStartedAtByItem.delete(params.item.id);
1039
- const itemDurationMs =
1040
- typeof params.item.durationMs === "number" ? params.item.durationMs : undefined;
1041
- const durationMs =
1042
- itemDurationMs ?? (startedAt === undefined ? 0 : Math.max(0, Date.now() - startedAt));
1043
- const terminalEvent =
1044
- params.status === "blocked"
1045
- ? {
1046
- type: "tool.execution.blocked" as const,
1047
- reason: "codex_native_tool_blocked",
1048
- deniedReason: "codex_native_tool_blocked",
1049
- }
1050
- : params.status === "failed"
1051
- ? {
1052
- type: "tool.execution.error" as const,
1053
- durationMs,
1054
- errorCategory: "codex_native_tool_error",
1055
- }
1056
- : {
1057
- type: "tool.execution.completed" as const,
1058
- durationMs,
1059
- };
1060
- emitTrustedDiagnosticEvent({ ...base, ...terminalEvent });
1061
- }
1062
-
1063
- private emitAfterToolCallObservation(item: CodexThreadItem): void {
1064
- if (!this.shouldEmitAfterToolCallObservation(item)) {
1065
- return;
1066
- }
1067
- const name = itemName(item);
1068
- if (!name) {
1069
- return;
1070
- }
1071
- const status = itemStatus(item);
1072
- if (status === "running") {
1073
- return;
1074
- }
1075
- this.afterToolCallObservedItemIds.add(item.id);
1076
- const result = itemToolResult(item).result;
1077
- const error = itemToolError(item, status, this.toolResultOutputTextByItem);
1078
- const startedAt =
1079
- typeof item.durationMs === "number" ? Date.now() - Math.max(0, item.durationMs) : undefined;
1080
- const hookParams = {
1081
- toolName: name,
1082
- toolCallId: item.id,
1083
- runId: this.params.runId,
1084
- agentId: this.params.agentId,
1085
- sessionId: this.params.sessionId,
1086
- sessionKey: this.params.sessionKey,
1087
- startArgs: itemToolArgs(item) ?? {},
1088
- ...(result !== undefined ? { result } : {}),
1089
- ...(error ? { error } : {}),
1090
- ...(startedAt !== undefined ? { startedAt } : {}),
1091
- };
1092
- setImmediate(() => {
1093
- void runAgentHarnessAfterToolCallHook(hookParams);
1094
- });
1095
- }
1096
-
1097
- private shouldEmitAfterToolCallObservation(item: CodexThreadItem): boolean {
1098
- if (
1099
- !shouldSynthesizeToolProgressForItem(item) ||
1100
- this.afterToolCallObservedItemIds.has(item.id)
1101
- ) {
1102
- return false;
1103
- }
1104
- if (this.options.nativePostToolUseRelayEnabled && isNativePostToolUseRelayItem(item)) {
1105
- return false;
1106
- }
1107
- return true;
1108
- }
1109
-
1110
- private emitToolResultSummary(item: CodexThreadItem | undefined): void {
1111
- if (!item || !this.params.onToolResult || !this.shouldEmitToolResult()) {
1112
- return;
1113
- }
1114
- const itemId = item.id;
1115
- if (this.toolResultSummaryItemIds.has(itemId)) {
1116
- return;
1117
- }
1118
- const toolName = itemName(item);
1119
- if (!toolName) {
1120
- return;
1121
- }
1122
- if (!shouldEmitTranscriptToolProgress(toolName, itemToolArgs(item))) {
1123
- return;
1124
- }
1125
- this.toolResultSummaryItemIds.add(itemId);
1126
- const meta = itemMeta(item, this.toolProgressDetailMode());
1127
- this.emitToolResultMessage({
1128
- itemId,
1129
- text: formatToolSummary(toolName, meta),
1130
- });
1131
- }
1132
-
1133
- private emitToolResultOutput(item: CodexThreadItem | undefined): void {
1134
- if (!item || !this.params.onToolResult || !this.shouldEmitToolOutput()) {
1135
- return;
1136
- }
1137
- const itemId = item.id;
1138
- if (this.toolResultOutputItemIds.has(itemId)) {
1139
- return;
1140
- }
1141
- if (this.toolResultOutputStreamedItemIds.has(itemId)) {
1142
- return;
1143
- }
1144
- const toolName = itemName(item);
1145
- const output = itemOutputText(item, this.toolResultOutputTextByItem);
1146
- if (!toolName || !output) {
1147
- return;
1148
- }
1149
- if (!shouldEmitTranscriptToolProgress(toolName, itemToolArgs(item))) {
1150
- return;
1151
- }
1152
- this.emitToolResultMessage({
1153
- itemId,
1154
- text: formatToolOutput(toolName, itemMeta(item, this.toolProgressDetailMode()), output),
1155
- finalOutput: true,
1156
- });
1157
- }
1158
-
1159
- private emitToolResultMessage(params: {
1160
- itemId: string;
1161
- text: string;
1162
- finalOutput?: boolean;
1163
- }): void {
1164
- const text = params.text.trim();
1165
- if (!text) {
1166
- return;
1167
- }
1168
- this.toolProgressTexts.add(text);
1169
- if (params.finalOutput) {
1170
- this.toolResultOutputItemIds.add(params.itemId);
1171
- }
1172
- try {
1173
- void Promise.resolve(this.params.onToolResult?.({ text })).catch(() => {
1174
- // Tool progress delivery is best-effort and should not affect the turn.
1175
- });
1176
- } catch {
1177
- // Tool progress delivery is best-effort and should not affect the turn.
1178
- }
1179
- }
1180
-
1181
- private shouldEmitToolResult(): boolean {
1182
- return typeof this.params.shouldEmitToolResult === "function"
1183
- ? this.params.shouldEmitToolResult()
1184
- : this.params.verboseLevel === "on" || this.params.verboseLevel === "full";
1185
- }
1186
-
1187
- private shouldEmitToolOutput(): boolean {
1188
- return typeof this.params.shouldEmitToolOutput === "function"
1189
- ? this.params.shouldEmitToolOutput()
1190
- : this.params.verboseLevel === "full";
1191
- }
1192
-
1193
- private toolProgressDetailMode(): ToolProgressDetailMode {
1194
- return resolveCodexToolProgressDetailMode(this.params.toolProgressDetail);
1195
- }
1196
-
1197
- private recordToolMeta(item: CodexThreadItem | undefined): void {
1198
- if (!item) {
1199
- return;
1200
- }
1201
- const toolName = itemName(item);
1202
- if (!toolName) {
1203
- return;
1204
- }
1205
- const meta = itemMeta(item, this.toolProgressDetailMode());
1206
- this.toolMetas.set(item.id, {
1207
- toolName,
1208
- ...(meta ? { meta } : {}),
1209
- });
1210
- }
1211
-
1212
- private recordNativeToolTranscriptCall(item: CodexThreadItem | undefined): void {
1213
- if (!item || !shouldSynthesizeToolProgressForItem(item)) {
1214
- return;
1215
- }
1216
- const name = itemName(item);
1217
- if (!name) {
1218
- return;
1219
- }
1220
- this.recordToolTranscriptCall({
1221
- id: item.id,
1222
- name,
1223
- arguments: itemToolArgs(item),
1224
- });
1225
- }
1226
-
1227
- private recordNativeToolTranscriptResult(item: CodexThreadItem | undefined): void {
1228
- if (!item || !shouldSynthesizeToolProgressForItem(item)) {
1229
- return;
1230
- }
1231
- const name = itemName(item);
1232
- if (!name) {
1233
- return;
1234
- }
1235
- this.recordToolTranscriptResult({
1236
- id: item.id,
1237
- name,
1238
- text: itemTranscriptResultText(item, this.toolResultOutputTextByItem),
1239
- isError: isNonSuccessItemStatus(itemStatus(item)),
1240
- });
1241
- }
1242
-
1243
- private recordToolTranscriptCall(params: ToolTranscriptCallInput): void {
1244
- if (!params.id || !params.name || this.toolTranscriptCallIds.has(params.id)) {
1245
- return;
1246
- }
1247
- this.toolTranscriptCallIds.add(params.id);
1248
- this.toolTranscriptArgumentsById.set(params.id, params.arguments);
1249
- if (!shouldEmitTranscriptToolProgress(params.name, params.arguments)) {
1250
- this.transcriptToolProgressSuppressedIds.add(params.id);
1251
- } else {
1252
- this.transcriptToolProgressSuppressedIds.delete(params.id);
1253
- }
1254
- this.emitTranscriptToolCallProgress(params);
1255
- this.toolTranscriptMessages.push(
1256
- attachCodexMirrorIdentity(
1257
- this.createToolCallMessage(params),
1258
- `${this.turnId}:tool:${params.id}:call`,
1259
- ),
1260
- );
1261
- }
1262
-
1263
- private recordToolTranscriptResult(params: ToolTranscriptResultInput): void {
1264
- if (!params.id || !params.name || this.toolTranscriptResultIds.has(params.id)) {
1265
- return;
1266
- }
1267
- this.toolTranscriptResultIds.add(params.id);
1268
- this.emitTranscriptToolResultProgress(params);
1269
- this.toolTranscriptMessages.push(
1270
- attachCodexMirrorIdentity(
1271
- this.createToolResultMessage(params),
1272
- `${this.turnId}:tool:${params.id}:result`,
1273
- ),
1274
- );
1275
- }
1276
-
1277
- private emitTranscriptToolCallProgress(params: ToolTranscriptCallInput): void {
1278
- if (!shouldEmitTranscriptToolProgress(params.name, params.arguments)) {
1279
- return;
1280
- }
1281
- this.transcriptToolProgressCallIds.add(params.id);
1282
- const args = normalizeToolTranscriptArguments(params.arguments);
1283
- const meta = inferToolMetaFromArgs(params.name, args, {
1284
- detailMode: this.toolProgressDetailMode(),
1285
- });
1286
- if (
1287
- !this.params.onToolResult ||
1288
- !this.shouldEmitToolResult() ||
1289
- this.toolResultSummaryItemIds.has(params.id) ||
1290
- this.toolResultOutputStreamedItemIds.has(params.id)
1291
- ) {
1292
- return;
1293
- }
1294
- this.toolResultSummaryItemIds.add(params.id);
1295
- this.emitToolResultMessage({
1296
- itemId: params.id,
1297
- text: formatToolSummary(params.name, meta),
1298
- });
1299
- }
1300
-
1301
- private emitTranscriptToolResultProgress(params: ToolTranscriptResultInput): void {
1302
- if (
1303
- this.transcriptToolProgressSuppressedIds.has(params.id) ||
1304
- !shouldEmitTranscriptToolProgress(
1305
- params.name,
1306
- this.toolTranscriptArgumentsById.get(params.id),
1307
- )
1308
- ) {
1309
- return;
1310
- }
1311
- if (!this.transcriptToolProgressCallIds.has(params.id)) {
1312
- this.emitTranscriptToolCallProgress({
1313
- id: params.id,
1314
- name: params.name,
1315
- arguments: {},
1316
- });
1317
- }
1318
- if (
1319
- !this.params.onToolResult ||
1320
- !this.shouldEmitToolOutput() ||
1321
- this.toolResultOutputItemIds.has(params.id) ||
1322
- this.toolResultOutputStreamedItemIds.has(params.id)
1323
- ) {
1324
- return;
1325
- }
1326
- const text = params.text?.trim();
1327
- if (!text) {
1328
- return;
1329
- }
1330
- this.emitToolResultMessage({
1331
- itemId: params.id,
1332
- text: formatToolOutput(params.name, undefined, text),
1333
- finalOutput: true,
1334
- });
1335
- }
1336
-
1337
- private formatCodexErrorMessage(params: JsonObject): string | undefined {
1338
- const error = isJsonObject(params.error) ? params.error : undefined;
1339
- return (
1340
- formatCodexUsageLimitErrorMessage({
1341
- message: error ? readString(error, "message") : undefined,
1342
- codexErrorInfo: error?.codexErrorInfo,
1343
- rateLimits: this.latestRateLimits ?? readRecentCodexRateLimits(),
1344
- }) ?? readCodexErrorNotificationMessage(params)
1345
- );
1346
- }
1347
-
1348
- private emitAgentEvent(
1349
- event: Parameters<NonNullable<EmbeddedRunAttemptParams["onAgentEvent"]>>[0],
1350
- ): void {
1351
- try {
1352
- emitGlobalAgentEvent({
1353
- runId: this.params.runId,
1354
- stream: event.stream,
1355
- data: event.data,
1356
- ...(this.params.sessionKey ? { sessionKey: this.params.sessionKey } : {}),
1357
- });
1358
- } catch (error) {
1359
- embeddedAgentLog.debug("codex app-server global agent event emit failed", { error });
1360
- }
1361
- try {
1362
- const maybePromise = this.params.onAgentEvent?.(event);
1363
- void Promise.resolve(maybePromise).catch((error: unknown) => {
1364
- embeddedAgentLog.debug("codex app-server agent event handler rejected", { error });
1365
- });
1366
- } catch (error) {
1367
- // Downstream event consumers must not corrupt the canonical Codex turn projection.
1368
- embeddedAgentLog.debug("codex app-server agent event handler threw", { error });
1369
- }
1370
- }
1371
-
1372
- private collectAssistantTexts(): string[] {
1373
- const finalText = this.resolveFinalAssistantText();
1374
- return finalText ? [finalText] : [];
1375
- }
1376
-
1377
- private resolveFinalAssistantText(): string | undefined {
1378
- for (let i = this.assistantItemOrder.length - 1; i >= 0; i -= 1) {
1379
- const itemId = this.assistantItemOrder[i];
1380
- if (!itemId) {
1381
- continue;
1382
- }
1383
- const text = this.assistantTextByItem.get(itemId)?.trim();
1384
- if (this.assistantPhaseByItem.get(itemId) === "commentary") {
1385
- continue;
1386
- }
1387
- if (text && !this.toolProgressTexts.has(text)) {
1388
- return text;
1389
- }
1390
- }
1391
- return undefined;
1392
- }
1393
-
1394
- private rememberAssistantItem(itemId: string): void {
1395
- if (!itemId || this.assistantItemOrder.includes(itemId)) {
1396
- return;
1397
- }
1398
- this.assistantItemOrder.push(itemId);
1399
- }
1400
-
1401
- private async readMirroredSessionMessages(): Promise<AgentMessage[]> {
1402
- return (await readCodexMirroredSessionHistoryMessages(this.params.sessionFile)) ?? [];
1403
- }
1404
-
1405
- private createAssistantMessage(text: string): AssistantMessage {
1406
- const attribution = resolveCodexLocalRuntimeAttribution(this.params);
1407
- const usage: Usage = this.tokenUsage
1408
- ? {
1409
- input: this.tokenUsage.input ?? 0,
1410
- output: this.tokenUsage.output ?? 0,
1411
- cacheRead: this.tokenUsage.cacheRead ?? 0,
1412
- cacheWrite: this.tokenUsage.cacheWrite ?? 0,
1413
- totalTokens:
1414
- this.tokenUsage.total ??
1415
- (this.tokenUsage.input ?? 0) +
1416
- (this.tokenUsage.output ?? 0) +
1417
- (this.tokenUsage.cacheRead ?? 0) +
1418
- (this.tokenUsage.cacheWrite ?? 0),
1419
- cost: ZERO_USAGE.cost,
1420
- }
1421
- : ZERO_USAGE;
1422
- return {
1423
- role: "assistant",
1424
- content: [{ type: "text", text }],
1425
- api: attribution.api ?? "openai-codex-responses",
1426
- provider: attribution.provider,
1427
- model: this.params.modelId,
1428
- usage,
1429
- stopReason: this.aborted ? "aborted" : this.promptError ? "error" : "stop",
1430
- errorMessage: this.promptError ? formatErrorMessage(this.promptError) : undefined,
1431
- timestamp: Date.now(),
1432
- };
1433
- }
1434
-
1435
- private createAssistantMirrorMessage(title: string, text: string): AssistantMessage {
1436
- const attribution = resolveCodexLocalRuntimeAttribution(this.params);
1437
- return {
1438
- role: "assistant",
1439
- content: [{ type: "text", text: `${title}:\n${text}` }],
1440
- api: attribution.api ?? "openai-codex-responses",
1441
- provider: attribution.provider,
1442
- model: this.params.modelId,
1443
- usage: ZERO_USAGE,
1444
- stopReason: "stop",
1445
- timestamp: Date.now(),
1446
- };
1447
- }
1448
-
1449
- private createToolCallMessage(params: ToolTranscriptCallInput): AgentMessage {
1450
- const args = normalizeToolTranscriptArguments(params.arguments);
1451
- const attribution = resolveCodexLocalRuntimeAttribution(this.params);
1452
- return {
1453
- role: "assistant",
1454
- content: [
1455
- {
1456
- type: "toolCall",
1457
- id: params.id,
1458
- name: params.name,
1459
- arguments: args,
1460
- input: args,
1461
- },
1462
- ],
1463
- api: attribution.api ?? "openai-codex-responses",
1464
- provider: attribution.provider,
1465
- model: this.params.modelId,
1466
- usage: ZERO_USAGE,
1467
- stopReason: "toolUse",
1468
- timestamp: Date.now(),
1469
- } as unknown as AgentMessage;
1470
- }
1471
-
1472
- private createToolResultMessage(params: ToolTranscriptResultInput): AgentMessage {
1473
- const text = truncateToolTranscriptText(params.text?.trim() || toolResultStatusText(params));
1474
- return {
1475
- role: "toolResult",
1476
- toolCallId: params.id,
1477
- toolName: params.name,
1478
- isError: params.isError,
1479
- content: [
1480
- {
1481
- type: "toolResult",
1482
- id: params.id,
1483
- name: params.name,
1484
- toolName: params.name,
1485
- toolCallId: params.id,
1486
- toolUseId: params.id,
1487
- tool_use_id: params.id,
1488
- content: text,
1489
- text,
1490
- },
1491
- ],
1492
- timestamp: Date.now(),
1493
- } as unknown as AgentMessage;
1494
- }
1495
-
1496
- private isNotificationForTurn(params: JsonObject): boolean {
1497
- const threadId = readString(params, "threadId");
1498
- const turnId = readNotificationTurnId(params);
1499
- return threadId === this.threadId && turnId === this.turnId;
1500
- }
1501
-
1502
- private isHookNotificationForCurrentThread(params: JsonObject): boolean {
1503
- const threadId = readString(params, "threadId");
1504
- const turnId = params.turnId;
1505
- return threadId === this.threadId && (turnId === this.turnId || turnId === null);
1506
- }
1507
- }
1508
-
1509
- function isHookNotificationMethod(method: string): method is "hook/started" | "hook/completed" {
1510
- return method === "hook/started" || method === "hook/completed";
1511
- }
1512
-
1513
- function readNotificationTurnId(record: JsonObject): string | undefined {
1514
- return readString(record, "turnId") ?? readNestedTurnId(record);
1515
- }
1516
-
1517
- function readNestedTurnId(record: JsonObject): string | undefined {
1518
- const turn = record.turn;
1519
- return isJsonObject(turn) ? readString(turn, "id") : undefined;
1520
- }
1521
-
1522
- function readString(record: JsonObject, key: string): string | undefined {
1523
- const value = record[key];
1524
- return typeof value === "string" ? value : undefined;
1525
- }
1526
-
1527
- function readNullableString(record: JsonObject, key: string): string | null | undefined {
1528
- const value = record[key];
1529
- if (value === null) {
1530
- return null;
1531
- }
1532
- return typeof value === "string" ? value : undefined;
1533
- }
1534
-
1535
- function readNumber(record: JsonObject, key: string): number | undefined {
1536
- const value = record[key];
1537
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
1538
- }
1539
-
1540
- function readBoolean(record: JsonObject, key: string): boolean | undefined {
1541
- const value = record[key];
1542
- return typeof value === "boolean" ? value : undefined;
1543
- }
1544
-
1545
- function readBooleanAlias(record: JsonObject, keys: readonly string[]): boolean | undefined {
1546
- for (const key of keys) {
1547
- const value = readBoolean(record, key);
1548
- if (value !== undefined) {
1549
- return value;
1550
- }
1551
- }
1552
- return undefined;
1553
- }
1554
-
1555
- function readCodexErrorNotificationMessage(record: JsonObject): string | undefined {
1556
- const error = record.error;
1557
- if (isJsonObject(error)) {
1558
- return readString(error, "message") ?? readString(error, "error");
1559
- }
1560
- return readString(record, "message");
1561
- }
1562
-
1563
- function readHookOutputEntries(
1564
- value: JsonValue | undefined,
1565
- ): Array<{ kind?: string; text: string }> {
1566
- if (!Array.isArray(value)) {
1567
- return [];
1568
- }
1569
- return value.flatMap((entry) => {
1570
- if (!isJsonObject(entry)) {
1571
- return [];
1572
- }
1573
- const text = readString(entry, "text");
1574
- if (!text) {
1575
- return [];
1576
- }
1577
- const kind = readString(entry, "kind");
1578
- return [{ ...(kind ? { kind } : {}), text }];
1579
- });
1580
- }
1581
-
1582
- function readFirstJsonObject(record: JsonObject, keys: readonly string[]): JsonObject | undefined {
1583
- for (const key of keys) {
1584
- const value = record[key];
1585
- if (isJsonObject(value)) {
1586
- return value;
1587
- }
1588
- }
1589
- return undefined;
1590
- }
1591
-
1592
- function readNumberAlias(record: JsonObject, keys: readonly string[]): number | undefined {
1593
- for (const key of keys) {
1594
- const value = readNumber(record, key);
1595
- if (value !== undefined) {
1596
- return value;
1597
- }
1598
- }
1599
- return undefined;
1600
- }
1601
-
1602
- function normalizeCodexTokenUsage(record: JsonObject): ReturnType<typeof normalizeUsage> {
1603
- const promptTotalInput = readNumberAlias(record, CODEX_PROMPT_TOTAL_INPUT_KEYS);
1604
- const cacheRead = readNumberAlias(record, [
1605
- "cachedInputTokens",
1606
- "cached_input_tokens",
1607
- "cacheRead",
1608
- "cache_read",
1609
- "cache_read_input_tokens",
1610
- "cached_tokens",
1611
- ]);
1612
- const input =
1613
- promptTotalInput !== undefined && cacheRead !== undefined
1614
- ? Math.max(0, promptTotalInput - cacheRead)
1615
- : (promptTotalInput ?? readNumber(record, "input"));
1616
-
1617
- return normalizeUsage({
1618
- input,
1619
- output: readNumberAlias(record, ["outputTokens", "output_tokens", "output"]),
1620
- cacheRead,
1621
- cacheWrite: readNumberAlias(record, [
1622
- "cacheWrite",
1623
- "cache_write",
1624
- "cacheCreationInputTokens",
1625
- "cache_creation_input_tokens",
1626
- ]),
1627
- total: readNumberAlias(record, ["totalTokens", "total_tokens", "total"]),
1628
- });
1629
- }
1630
-
1631
- function splitPlanText(text: string): string[] {
1632
- return text
1633
- .split(/\r?\n/)
1634
- .map((line) => line.trim().replace(/^[-*]\s+/, ""))
1635
- .filter((line) => line.length > 0);
1636
- }
1637
-
1638
- function collectTextValues(map: Map<string, string>): string[] {
1639
- return [...map.values()].filter((text) => text.trim().length > 0);
1640
- }
1641
-
1642
- function extractRawAssistantText(item: JsonObject): string | undefined {
1643
- const content = Array.isArray(item.content) ? item.content : [];
1644
- const text = content
1645
- .flatMap((entry) => {
1646
- if (!isJsonObject(entry)) {
1647
- return [];
1648
- }
1649
- const type = readString(entry, "type");
1650
- if (type !== "output_text" && type !== "text") {
1651
- return [];
1652
- }
1653
- const value = readString(entry, "text");
1654
- return value ? [value] : [];
1655
- })
1656
- .join("");
1657
- return text.trim() || undefined;
1658
- }
1659
-
1660
- function itemKind(
1661
- item: CodexThreadItem,
1662
- ): "tool" | "command" | "patch" | "search" | "analysis" | undefined {
1663
- switch (item.type) {
1664
- case "dynamicToolCall":
1665
- case "mcpToolCall":
1666
- return "tool";
1667
- case "commandExecution":
1668
- return "command";
1669
- case "fileChange":
1670
- return "patch";
1671
- case "webSearch":
1672
- return "search";
1673
- case "reasoning":
1674
- case "contextCompaction":
1675
- return "analysis";
1676
- default:
1677
- return undefined;
1678
- }
1679
- }
1680
-
1681
- function itemTitle(item: CodexThreadItem): string {
1682
- switch (item.type) {
1683
- case "commandExecution":
1684
- return "Command";
1685
- case "fileChange":
1686
- return "File change";
1687
- case "mcpToolCall":
1688
- return "MCP tool";
1689
- case "dynamicToolCall":
1690
- return "Tool";
1691
- case "webSearch":
1692
- return "Web search";
1693
- case "contextCompaction":
1694
- return "Context compaction";
1695
- case "reasoning":
1696
- return "Reasoning";
1697
- default:
1698
- return item.type;
1699
- }
1700
- }
1701
-
1702
- function itemStatus(item: CodexThreadItem): "completed" | "failed" | "running" | "blocked" {
1703
- const status = readItemString(item, "status");
1704
- if (status === "failed") {
1705
- return "failed";
1706
- }
1707
- if (status === "declined") {
1708
- return "blocked";
1709
- }
1710
- if (status === "inProgress" || status === "running") {
1711
- return "running";
1712
- }
1713
- return "completed";
1714
- }
1715
-
1716
- function isNonSuccessItemStatus(status: ReturnType<typeof itemStatus>): boolean {
1717
- return status === "failed" || status === "blocked";
1718
- }
1719
-
1720
- function itemName(item: CodexThreadItem): string | undefined {
1721
- if (item.type === "dynamicToolCall" && typeof item.tool === "string") {
1722
- return item.tool;
1723
- }
1724
- if (item.type === "mcpToolCall" && typeof item.tool === "string") {
1725
- const server = typeof item.server === "string" ? item.server : undefined;
1726
- return server ? `${server}.${item.tool}` : item.tool;
1727
- }
1728
- if (item.type === "commandExecution") {
1729
- return "bash";
1730
- }
1731
- if (item.type === "fileChange") {
1732
- return "apply_patch";
1733
- }
1734
- if (item.type === "webSearch") {
1735
- return "web_search";
1736
- }
1737
- return undefined;
1738
- }
1739
-
1740
- function shouldSynthesizeToolProgressForItem(item: CodexThreadItem): boolean {
1741
- switch (item.type) {
1742
- case "commandExecution":
1743
- case "fileChange":
1744
- case "webSearch":
1745
- case "mcpToolCall":
1746
- return true;
1747
- default:
1748
- return false;
1749
- }
1750
- }
1751
-
1752
- function isMutatingNativeToolItem(item: CodexThreadItem): boolean {
1753
- return item.type === "commandExecution" || item.type === "fileChange";
1754
- }
1755
-
1756
- function nativeToolActionFingerprint(item: CodexThreadItem): string | undefined {
1757
- if (item.type === "commandExecution" && typeof item.command === "string") {
1758
- return JSON.stringify({
1759
- type: item.type,
1760
- command: item.command,
1761
- cwd: typeof item.cwd === "string" ? item.cwd : "",
1762
- });
1763
- }
1764
- if (item.type === "fileChange") {
1765
- return JSON.stringify({
1766
- type: item.type,
1767
- changes: itemFileChanges(item),
1768
- });
1769
- }
1770
- return undefined;
1771
- }
1772
-
1773
- function isNativePostToolUseRelayItem(item: CodexThreadItem): boolean {
1774
- switch (item.type) {
1775
- case "commandExecution":
1776
- case "fileChange":
1777
- case "mcpToolCall":
1778
- return true;
1779
- default:
1780
- return false;
1781
- }
1782
- }
1783
-
1784
- function shouldSuppressChannelProgressForItem(item: CodexThreadItem): boolean {
1785
- if (shouldSynthesizeToolProgressForItem(item)) {
1786
- return true;
1787
- }
1788
- // Dynamic Klaw tool requests are emitted at the item/tool/call request
1789
- // boundary in run-attempt.ts. Re-emitting item notifications to channels can
1790
- // duplicate start/result progress when the app-server sends both signals.
1791
- return item.type === "dynamicToolCall";
1792
- }
1793
-
1794
- function itemToolArgs(item: CodexThreadItem): Record<string, unknown> | undefined {
1795
- if (item.type === "commandExecution") {
1796
- return sanitizeCodexAgentEventRecord({
1797
- command: item.command,
1798
- ...(typeof item.cwd === "string" ? { cwd: item.cwd } : {}),
1799
- });
1800
- }
1801
- if (item.type === "fileChange") {
1802
- return sanitizeCodexAgentEventRecord({
1803
- changes: itemFileChanges(item),
1804
- });
1805
- }
1806
- if (item.type === "webSearch" && typeof item.query === "string") {
1807
- return sanitizeCodexAgentEventRecord({ query: item.query });
1808
- }
1809
- if (item.type === "mcpToolCall") {
1810
- return sanitizeCodexToolArguments(item.arguments);
1811
- }
1812
- return undefined;
1813
- }
1814
-
1815
- function itemToolResult(item: CodexThreadItem): { result?: Record<string, unknown> } {
1816
- if (item.type === "commandExecution") {
1817
- return {
1818
- result: sanitizeCodexAgentEventRecord({
1819
- status: item.status,
1820
- exitCode: item.exitCode,
1821
- durationMs: item.durationMs,
1822
- }),
1823
- };
1824
- }
1825
- if (item.type === "fileChange") {
1826
- return {
1827
- result: sanitizeCodexAgentEventRecord({
1828
- status: item.status,
1829
- changes: itemFileChanges(item),
1830
- }),
1831
- };
1832
- }
1833
- if (item.type === "mcpToolCall") {
1834
- return {
1835
- result: sanitizeCodexAgentEventRecord({
1836
- status: item.status,
1837
- durationMs: item.durationMs,
1838
- ...(item.error ? { error: item.error } : {}),
1839
- ...(item.result ? { result: item.result } : {}),
1840
- }),
1841
- };
1842
- }
1843
- if (item.type === "webSearch") {
1844
- return { result: sanitizeCodexAgentEventRecord({ status: "completed" }) };
1845
- }
1846
- return {};
1847
- }
1848
-
1849
- function itemFileChanges(item: CodexThreadItem): Array<{ path: string; kind: string }> {
1850
- return Array.isArray(item.changes)
1851
- ? item.changes.map((change) => ({ path: change.path, kind: change.kind }))
1852
- : [];
1853
- }
1854
-
1855
- function itemToolError(
1856
- item: CodexThreadItem,
1857
- status: ReturnType<typeof itemStatus>,
1858
- outputTextByItem?: ReadonlyMap<string, string>,
1859
- ): string | undefined {
1860
- if (status === "blocked") {
1861
- return "codex native tool blocked";
1862
- }
1863
- if (status !== "failed") {
1864
- return undefined;
1865
- }
1866
- return itemOutputText(item, outputTextByItem) ?? "codex native tool failed";
1867
- }
1868
-
1869
- function itemMeta(
1870
- item: CodexThreadItem,
1871
- detailMode: ToolProgressDetailMode = "explain",
1872
- ): string | undefined {
1873
- if (item.type === "commandExecution" && typeof item.command === "string") {
1874
- return inferToolMetaFromArgs(
1875
- "exec",
1876
- {
1877
- command: item.command,
1878
- cwd: typeof item.cwd === "string" ? item.cwd : undefined,
1879
- },
1880
- { detailMode },
1881
- );
1882
- }
1883
- if (item.type === "webSearch" && typeof item.query === "string") {
1884
- return item.query;
1885
- }
1886
- const toolName = itemName(item);
1887
- if ((item.type === "dynamicToolCall" || item.type === "mcpToolCall") && toolName) {
1888
- return inferToolMetaFromArgs(toolName, item.arguments, { detailMode });
1889
- }
1890
- return undefined;
1891
- }
1892
-
1893
- function itemOutputText(
1894
- item: CodexThreadItem,
1895
- outputTextByItem?: ReadonlyMap<string, string>,
1896
- ): string | undefined {
1897
- if (item.type === "commandExecution") {
1898
- return item.aggregatedOutput?.trim() || outputTextByItem?.get(item.id)?.trim() || undefined;
1899
- }
1900
- if (item.type === "dynamicToolCall") {
1901
- return collectDynamicToolContentText(item.contentItems).trim() || undefined;
1902
- }
1903
- if (item.type === "mcpToolCall") {
1904
- if (item.error) {
1905
- return stringifyJsonValue(item.error);
1906
- }
1907
- return item.result ? stringifyJsonValue(item.result) : undefined;
1908
- }
1909
- return undefined;
1910
- }
1911
-
1912
- function itemTranscriptResultText(
1913
- item: CodexThreadItem,
1914
- outputTextByItem?: ReadonlyMap<string, string>,
1915
- ): string | undefined {
1916
- const output = itemOutputText(item, outputTextByItem);
1917
- if (output) {
1918
- return output;
1919
- }
1920
- const result = itemToolResult(item).result;
1921
- return result ? stringifyJsonValue(result) : itemStatus(item);
1922
- }
1923
-
1924
- function appendToolOutputDeltaText(
1925
- outputTextByItem: Map<string, string>,
1926
- itemId: string,
1927
- delta: string,
1928
- ): void {
1929
- const current = outputTextByItem.get(itemId) ?? "";
1930
- if (current.length >= TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS) {
1931
- return;
1932
- }
1933
- const remaining = TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS - current.length;
1934
- const next = current + (delta.length > remaining ? delta.slice(0, remaining) : delta);
1935
- outputTextByItem.set(itemId, next);
1936
- }
1937
-
1938
- function normalizeToolTranscriptArguments(value: unknown): Record<string, unknown> {
1939
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1940
- return {};
1941
- }
1942
- return value as Record<string, unknown>;
1943
- }
1944
-
1945
- function isActivityLogCommandProgress(toolName: string, args: unknown): boolean {
1946
- if (toolName !== "bash" && toolName !== "exec" && toolName !== "shell") {
1947
- return false;
1948
- }
1949
- const command = readToolCommandText(args);
1950
- return Boolean(command && command.includes("log_activity.sh"));
1951
- }
1952
-
1953
- function readToolCommandText(value: unknown): string | undefined {
1954
- if (typeof value === "string") {
1955
- return value;
1956
- }
1957
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1958
- return undefined;
1959
- }
1960
- const record = value as Record<string, unknown>;
1961
- for (const key of ["command", "cmd", "shellCommand", "script"]) {
1962
- const text = record[key];
1963
- if (typeof text === "string" && text) {
1964
- return text;
1965
- }
1966
- }
1967
- return undefined;
1968
- }
1969
-
1970
- function collectDynamicToolContentText(contentItems: CodexThreadItem["contentItems"]): string {
1971
- if (!Array.isArray(contentItems)) {
1972
- return "";
1973
- }
1974
- return contentItems
1975
- .flatMap((entry) => {
1976
- if (!isJsonObject(entry)) {
1977
- return [];
1978
- }
1979
- const text = readString(entry, "text");
1980
- return text ? [text] : [];
1981
- })
1982
- .join("\n");
1983
- }
1984
-
1985
- function truncateToolTranscriptText(text: string): string {
1986
- if (text.length <= TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS) {
1987
- return text;
1988
- }
1989
- return `${text.slice(0, TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS)}\n...(truncated)...`;
1990
- }
1991
-
1992
- function toolResultStatusText(params: ToolTranscriptResultInput): string {
1993
- return params.isError ? `${params.name} failed` : `${params.name} completed`;
1994
- }
1995
-
1996
- function stringifyJsonValue(value: unknown): string | undefined {
1997
- try {
1998
- return JSON.stringify(value, null, 2);
1999
- } catch {
2000
- return undefined;
2001
- }
2002
- }
2003
-
2004
- function formatToolSummary(toolName: string, meta?: string): string {
2005
- const trimmedMeta = meta?.trim();
2006
- return formatToolAggregate(toolName, trimmedMeta ? [trimmedMeta] : undefined, {
2007
- markdown: true,
2008
- });
2009
- }
2010
-
2011
- function formatToolOutput(toolName: string, meta: string | undefined, output: string): string {
2012
- const formattedOutput = formatToolProgressOutput(output);
2013
- if (!formattedOutput) {
2014
- return formatToolSummary(toolName, meta);
2015
- }
2016
- const fence = markdownFenceForText(formattedOutput);
2017
- return `${formatToolSummary(toolName, meta)}\n${fence}txt\n${formattedOutput}\n${fence}`;
2018
- }
2019
-
2020
- function markdownFenceForText(text: string): string {
2021
- return "`".repeat(Math.max(3, longestBacktickRun(text) + 1));
2022
- }
2023
-
2024
- function longestBacktickRun(value: string): number {
2025
- let longest = 0;
2026
- let current = 0;
2027
- for (const char of value) {
2028
- if (char === "`") {
2029
- current += 1;
2030
- longest = Math.max(longest, current);
2031
- continue;
2032
- }
2033
- current = 0;
2034
- }
2035
- return longest;
2036
- }
2037
-
2038
- function readItemString(item: CodexThreadItem, key: string): string | undefined {
2039
- const value = (item as Record<string, unknown>)[key];
2040
- return typeof value === "string" ? value : undefined;
2041
- }
2042
-
2043
- function readItem(value: JsonValue | undefined): CodexThreadItem | undefined {
2044
- if (!isJsonObject(value)) {
2045
- return undefined;
2046
- }
2047
- const type = typeof value.type === "string" ? value.type : undefined;
2048
- const id = typeof value.id === "string" ? value.id : undefined;
2049
- if (!type || !id) {
2050
- return undefined;
2051
- }
2052
- return value as CodexThreadItem;
2053
- }
2054
-
2055
- function readTurn(value: JsonValue | undefined): CodexTurn | undefined {
2056
- return readCodexTurn(value);
2057
- }