@agentex/agent 0.0.23 → 0.0.26

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 (177) hide show
  1. package/CHANGELOG.md +338 -0
  2. package/LICENSE +21 -0
  3. package/README.md +110 -0
  4. package/dist/derived.d.ts +5 -3
  5. package/dist/derived.d.ts.map +1 -1
  6. package/dist/derived.js +11 -7
  7. package/dist/derived.js.map +1 -1
  8. package/dist/index.d.ts +7 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +4 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/providers/acp/index.d.ts +1 -1
  13. package/dist/providers/acp/index.d.ts.map +1 -1
  14. package/dist/providers/acp/index.js +5 -97
  15. package/dist/providers/acp/index.js.map +1 -1
  16. package/dist/providers/acp/session.d.ts +8 -1
  17. package/dist/providers/acp/session.d.ts.map +1 -1
  18. package/dist/providers/acp/session.js +94 -0
  19. package/dist/providers/acp/session.js.map +1 -1
  20. package/dist/providers/claude/attach.d.ts +8 -0
  21. package/dist/providers/claude/attach.d.ts.map +1 -0
  22. package/dist/providers/claude/attach.js +113 -0
  23. package/dist/providers/claude/attach.js.map +1 -0
  24. package/dist/providers/claude/execute.d.ts.map +1 -1
  25. package/dist/providers/claude/execute.js +17 -2
  26. package/dist/providers/claude/execute.js.map +1 -1
  27. package/dist/providers/claude/goal-capability.d.ts +15 -0
  28. package/dist/providers/claude/goal-capability.d.ts.map +1 -0
  29. package/dist/providers/claude/goal-capability.js +20 -0
  30. package/dist/providers/claude/goal-capability.js.map +1 -0
  31. package/dist/providers/claude/index.d.ts.map +1 -1
  32. package/dist/providers/claude/index.js +8 -4
  33. package/dist/providers/claude/index.js.map +1 -1
  34. package/dist/providers/claude/session.d.ts +11 -9
  35. package/dist/providers/claude/session.d.ts.map +1 -1
  36. package/dist/providers/claude/session.js +36 -14
  37. package/dist/providers/claude/session.js.map +1 -1
  38. package/dist/providers/codex/attach.d.ts +9 -0
  39. package/dist/providers/codex/attach.d.ts.map +1 -0
  40. package/dist/providers/codex/attach.js +93 -0
  41. package/dist/providers/codex/attach.js.map +1 -0
  42. package/dist/providers/codex/execute.d.ts.map +1 -1
  43. package/dist/providers/codex/execute.js +17 -3
  44. package/dist/providers/codex/execute.js.map +1 -1
  45. package/dist/providers/codex/goal-capability.d.ts +13 -0
  46. package/dist/providers/codex/goal-capability.d.ts.map +1 -0
  47. package/dist/providers/codex/goal-capability.js +18 -0
  48. package/dist/providers/codex/goal-capability.js.map +1 -0
  49. package/dist/providers/codex/index.d.ts +1 -0
  50. package/dist/providers/codex/index.d.ts.map +1 -1
  51. package/dist/providers/codex/index.js +9 -6
  52. package/dist/providers/codex/index.js.map +1 -1
  53. package/dist/providers/codex/session.d.ts +11 -7
  54. package/dist/providers/codex/session.d.ts.map +1 -1
  55. package/dist/providers/codex/session.js +37 -12
  56. package/dist/providers/codex/session.js.map +1 -1
  57. package/dist/providers/codex/transcript-normalize.d.ts +28 -0
  58. package/dist/providers/codex/transcript-normalize.d.ts.map +1 -0
  59. package/dist/providers/codex/transcript-normalize.js +191 -0
  60. package/dist/providers/codex/transcript-normalize.js.map +1 -0
  61. package/dist/providers/cursor/index.d.ts.map +1 -1
  62. package/dist/providers/cursor/index.js +2 -2
  63. package/dist/providers/cursor/index.js.map +1 -1
  64. package/dist/providers/openclaw/index.d.ts.map +1 -1
  65. package/dist/providers/openclaw/index.js +2 -2
  66. package/dist/providers/openclaw/index.js.map +1 -1
  67. package/dist/providers/opencode/index.d.ts.map +1 -1
  68. package/dist/providers/opencode/index.js +3 -5
  69. package/dist/providers/opencode/index.js.map +1 -1
  70. package/dist/providers/pi/index.d.ts.map +1 -1
  71. package/dist/providers/pi/index.js +3 -5
  72. package/dist/providers/pi/index.js.map +1 -1
  73. package/dist/providers/process/index.d.ts.map +1 -1
  74. package/dist/providers/process/index.js +2 -2
  75. package/dist/providers/process/index.js.map +1 -1
  76. package/dist/registry.d.ts +0 -1
  77. package/dist/registry.d.ts.map +1 -1
  78. package/dist/registry.js +0 -4
  79. package/dist/registry.js.map +1 -1
  80. package/dist/sessions/index.d.ts +3 -0
  81. package/dist/sessions/index.d.ts.map +1 -0
  82. package/dist/sessions/index.js +2 -0
  83. package/dist/sessions/index.js.map +1 -0
  84. package/dist/sessions/record.d.ts +43 -0
  85. package/dist/sessions/record.d.ts.map +1 -0
  86. package/dist/sessions/record.js +85 -0
  87. package/dist/sessions/record.js.map +1 -0
  88. package/dist/types.d.ts +176 -0
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js.map +1 -1
  91. package/dist/utils/endpoint.d.ts +38 -0
  92. package/dist/utils/endpoint.d.ts.map +1 -0
  93. package/dist/utils/endpoint.js +151 -0
  94. package/dist/utils/endpoint.js.map +1 -0
  95. package/dist/utils/env.d.ts.map +1 -1
  96. package/dist/utils/env.js +5 -1
  97. package/dist/utils/env.js.map +1 -1
  98. package/dist/utils/uuid.d.ts +7 -1
  99. package/dist/utils/uuid.d.ts.map +1 -1
  100. package/dist/utils/uuid.js +21 -1
  101. package/dist/utils/uuid.js.map +1 -1
  102. package/package.json +64 -7
  103. package/src/derived.ts +311 -0
  104. package/src/goals/controller.ts +442 -0
  105. package/src/goals/index.ts +21 -0
  106. package/src/goals/normalize.ts +173 -0
  107. package/src/goals/sentinel.ts +90 -0
  108. package/src/index.ts +270 -0
  109. package/src/providers/_shared/http-agent.ts +304 -0
  110. package/src/providers/acp/index.ts +103 -0
  111. package/src/providers/acp/parse.ts +131 -0
  112. package/src/providers/acp/session.ts +744 -0
  113. package/src/providers/claude/attach.ts +147 -0
  114. package/src/providers/claude/codec.ts +43 -0
  115. package/src/providers/claude/execute.ts +300 -0
  116. package/src/providers/claude/goal-capability.ts +21 -0
  117. package/src/providers/claude/index.ts +72 -0
  118. package/src/providers/claude/mcp.ts +82 -0
  119. package/src/providers/claude/parse.ts +824 -0
  120. package/src/providers/claude/session.ts +1192 -0
  121. package/src/providers/claude/transcript.ts +555 -0
  122. package/src/providers/codex/attach.ts +123 -0
  123. package/src/providers/codex/codec.ts +50 -0
  124. package/src/providers/codex/execute.ts +337 -0
  125. package/src/providers/codex/goal-capability.ts +19 -0
  126. package/src/providers/codex/index.ts +57 -0
  127. package/src/providers/codex/modes.ts +159 -0
  128. package/src/providers/codex/parse.ts +691 -0
  129. package/src/providers/codex/plan-mode.ts +49 -0
  130. package/src/providers/codex/session.ts +1287 -0
  131. package/src/providers/codex/transcript-normalize.ts +197 -0
  132. package/src/providers/codex/transcript.ts +487 -0
  133. package/src/providers/codex/usage-scanner.ts +178 -0
  134. package/src/providers/copilot/index.ts +19 -0
  135. package/src/providers/cursor/codec.ts +44 -0
  136. package/src/providers/cursor/execute.ts +271 -0
  137. package/src/providers/cursor/index.ts +25 -0
  138. package/src/providers/cursor/parse.ts +288 -0
  139. package/src/providers/gemini/index.ts +21 -0
  140. package/src/providers/openclaw/codec.ts +40 -0
  141. package/src/providers/openclaw/execute.ts +19 -0
  142. package/src/providers/openclaw/index.ts +29 -0
  143. package/src/providers/opencode/codec.ts +50 -0
  144. package/src/providers/opencode/event-parse.ts +141 -0
  145. package/src/providers/opencode/execute.ts +251 -0
  146. package/src/providers/opencode/http-session.ts +427 -0
  147. package/src/providers/opencode/index.ts +30 -0
  148. package/src/providers/opencode/parse.ts +203 -0
  149. package/src/providers/opencode/server.ts +0 -0
  150. package/src/providers/pi/codec.ts +44 -0
  151. package/src/providers/pi/execute.ts +297 -0
  152. package/src/providers/pi/index.ts +30 -0
  153. package/src/providers/pi/parse.ts +231 -0
  154. package/src/providers/pi/session.ts +381 -0
  155. package/src/providers/process/execute.ts +148 -0
  156. package/src/providers/process/index.ts +52 -0
  157. package/src/registry.ts +40 -0
  158. package/src/sessions/index.ts +8 -0
  159. package/src/sessions/record.ts +108 -0
  160. package/src/types.ts +1638 -0
  161. package/src/utils/ask-user-question.ts +57 -0
  162. package/src/utils/auth.ts +661 -0
  163. package/src/utils/binary.ts +179 -0
  164. package/src/utils/endpoint.ts +172 -0
  165. package/src/utils/env.ts +63 -0
  166. package/src/utils/execute-all.ts +68 -0
  167. package/src/utils/exit-plan-mode.ts +40 -0
  168. package/src/utils/instructions.ts +427 -0
  169. package/src/utils/process.ts +223 -0
  170. package/src/utils/runtime-config.ts +100 -0
  171. package/src/utils/runtime-homes.ts +49 -0
  172. package/src/utils/skill-commands.ts +493 -0
  173. package/src/utils/skills.ts +500 -0
  174. package/src/utils/template.ts +16 -0
  175. package/src/utils/tool-names.ts +51 -0
  176. package/src/utils/uuid.ts +21 -0
  177. package/src/utils/workspace.ts +156 -0
@@ -0,0 +1,691 @@
1
+ import type {
2
+ BaseStreamEventFields,
3
+ GoalSource,
4
+ StreamEvent,
5
+ TokenUsage,
6
+ } from "../../types.js";
7
+ import { normalizeCodexGoalRecord } from "../../goals/normalize.js";
8
+
9
+ const PROVIDER_TYPE = "codex";
10
+
11
+ export interface CodexParsedResult {
12
+ sessionId: string | null;
13
+ /**
14
+ * Codex doesn't emit `model` in its NDJSON output — always null from
15
+ * stdout. Executors should fall back to the requested model.
16
+ */
17
+ model: string | null;
18
+ usage: TokenUsage | null;
19
+ costUsd: number | null;
20
+ summary: string | null;
21
+ isError: boolean;
22
+ errorCode: string | null;
23
+ errorMessage: string | null;
24
+ /** Final `turn.completed` / `turn.failed` / `error` event verbatim. */
25
+ finalEvent: Record<string, unknown> | null;
26
+ }
27
+
28
+ function parseJson(line: string): Record<string, unknown> | null {
29
+ try {
30
+ const parsed = JSON.parse(line);
31
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
32
+ return parsed as Record<string, unknown>;
33
+ }
34
+ } catch {
35
+ // Skip malformed lines
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function asString(value: unknown, fallback: string): string {
41
+ return typeof value === "string" ? value : fallback;
42
+ }
43
+
44
+ function asNullableString(value: unknown): string | null {
45
+ return typeof value === "string" && value.length > 0 ? value : null;
46
+ }
47
+
48
+ function asNumber(value: unknown, fallback: number): number {
49
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
50
+ }
51
+
52
+ function asNullableNumber(value: unknown): number | null {
53
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
54
+ }
55
+
56
+ function parseObject(value: unknown): Record<string, unknown> {
57
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
58
+ return value as Record<string, unknown>;
59
+ }
60
+ return {};
61
+ }
62
+
63
+ /**
64
+ * Build base fields. For Codex, `eventId` is always null (neither wire
65
+ * format emits a per-line UUID). `sessionId` and `turnId` come from the
66
+ * caller — the v2 parser extracts them from the event's `params`; the
67
+ * NDJSON parser tracks sessionId across lines and leaves turnId null.
68
+ */
69
+ function baseFields(
70
+ event: Record<string, unknown>,
71
+ sessionId: string | null,
72
+ messageId: string | null,
73
+ turnId: string | null,
74
+ ): BaseStreamEventFields {
75
+ return {
76
+ timestamp: new Date().toISOString(),
77
+ providerType: PROVIDER_TYPE,
78
+ sessionId,
79
+ messageId,
80
+ eventId: null,
81
+ turnId,
82
+ parentToolCallId: null,
83
+ raw: event,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Build a normalized `goal_status` event from a Codex goal record. Returns null
89
+ * when the record is unusable. Shared by the v2 notification path and the
90
+ * NDJSON `event_msg`/`thread_goal_updated` path.
91
+ */
92
+ function buildCodexGoalEvent(
93
+ goal: Record<string, unknown>,
94
+ base: BaseStreamEventFields,
95
+ source: GoalSource = "model",
96
+ ): StreamEvent | null {
97
+ const fields = normalizeCodexGoalRecord(goal, source);
98
+ if (!fields) return null;
99
+ const ev: Extract<StreamEvent, { type: "goal_status" }> = {
100
+ type: "goal_status",
101
+ objective: fields.objective,
102
+ status: fields.status,
103
+ met: fields.met,
104
+ enforced: fields.enforced,
105
+ source: fields.source,
106
+ ...base,
107
+ };
108
+ if (fields.blockedReason !== undefined) ev.blockedReason = fields.blockedReason;
109
+ if (fields.tokensUsed !== undefined) ev.tokensUsed = fields.tokensUsed;
110
+ if (fields.timeUsedSeconds !== undefined) ev.timeUsedSeconds = fields.timeUsedSeconds;
111
+ if (fields.tokenBudget !== undefined) ev.tokenBudget = fields.tokenBudget;
112
+ return ev;
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Run-level parser used by executeCodexProvider to summarize a full stdout.
117
+ // Operates on NDJSON only (executor uses `codex exec --json`).
118
+ // ---------------------------------------------------------------------------
119
+
120
+ export function parseCodexJsonl(stdout: string): CodexParsedResult {
121
+ let sessionId: string | null = null;
122
+ let totalInputTokens = 0;
123
+ let totalOutputTokens = 0;
124
+ let totalCachedInputTokens = 0;
125
+ let hasUsage = false;
126
+ let summary: string | null = null;
127
+ let isError = false;
128
+ let errorMessage: string | null = null;
129
+ let finalEvent: Record<string, unknown> | null = null;
130
+
131
+ for (const rawLine of stdout.split(/\r?\n/)) {
132
+ const line = rawLine.trim();
133
+ if (!line) continue;
134
+ const event = parseJson(line);
135
+ if (!event) continue;
136
+
137
+ const type = asString(event["type"], "");
138
+
139
+ if (type === "thread.started") {
140
+ sessionId = asNullableString(event["thread_id"]) ?? sessionId;
141
+ continue;
142
+ }
143
+
144
+ if (type === "item.completed") {
145
+ const item = parseObject(event["item"]);
146
+ if (asString(item["type"], "") === "agent_message") {
147
+ const directText = asString(item["text"], "");
148
+ if (directText) {
149
+ summary = directText;
150
+ } else {
151
+ const content = Array.isArray(item["content"]) ? item["content"] : [];
152
+ for (const entry of content) {
153
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
154
+ const block = entry as Record<string, unknown>;
155
+ if (asString(block["type"], "") === "output_text") {
156
+ const text = asString(block["text"], "");
157
+ if (text) summary = text;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ continue;
163
+ }
164
+
165
+ if (type === "turn.completed") {
166
+ const usage = parseObject(event["usage"]);
167
+ const inputTokens = asNumber(usage["input_tokens"], 0);
168
+ const outputTokens = asNumber(usage["output_tokens"], 0);
169
+ const cachedInputTokens = asNumber(usage["cached_input_tokens"], 0);
170
+ if (inputTokens > 0 || outputTokens > 0 || cachedInputTokens > 0) {
171
+ totalInputTokens += inputTokens;
172
+ totalOutputTokens += outputTokens;
173
+ totalCachedInputTokens += cachedInputTokens;
174
+ hasUsage = true;
175
+ }
176
+ finalEvent = event;
177
+ continue;
178
+ }
179
+
180
+ if (type === "turn.failed") {
181
+ isError = true;
182
+ errorMessage = asNullableString(event["message"]) ?? asNullableString(event["error"]);
183
+ finalEvent = event;
184
+ continue;
185
+ }
186
+
187
+ if (type === "error") {
188
+ isError = true;
189
+ errorMessage = asNullableString(event["message"]);
190
+ finalEvent = event;
191
+ continue;
192
+ }
193
+ }
194
+
195
+ const usage: TokenUsage | null = hasUsage ? {
196
+ inputTokens: totalInputTokens,
197
+ outputTokens: totalOutputTokens,
198
+ ...(totalCachedInputTokens > 0 ? { cachedInputTokens: totalCachedInputTokens } : {}),
199
+ } : null;
200
+
201
+ return {
202
+ sessionId,
203
+ model: null,
204
+ usage,
205
+ costUsd: null, // Codex doesn't report cost
206
+ summary,
207
+ isError,
208
+ errorCode: null,
209
+ errorMessage,
210
+ finalEvent,
211
+ };
212
+ }
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Stream-line parser. Auto-detects wire format:
216
+ // - `codex exec --json` NDJSON: `{"type":"...","...": ...}`
217
+ // - `codex --json` v2 JSON-RPC: `{"jsonrpc":"2.0","method":"...","params":{...}}`
218
+ // Both shapes produce the same StreamEvent variants so downstream
219
+ // consumers don't branch on format.
220
+ // ---------------------------------------------------------------------------
221
+
222
+ /**
223
+ * Parse a single Codex line into a StreamEvent.
224
+ *
225
+ * @param line Raw JSON text.
226
+ * @param sessionId Caller-tracked thread id (NDJSON emits it once on
227
+ * `thread.started` and the executor threads it through).
228
+ * v2 parses it from `params.threadId` directly and
229
+ * ignores this arg.
230
+ */
231
+ export function parseCodexStreamLine(
232
+ line: string,
233
+ sessionId: string | null = null,
234
+ ): StreamEvent | null {
235
+ const event = parseJson(line);
236
+ if (!event) return null;
237
+
238
+ // v2 JSON-RPC notification: has `method` + `params`.
239
+ if (typeof event["method"] === "string") {
240
+ return parseV2Notification(event);
241
+ }
242
+
243
+ // NDJSON legacy format: has `type`.
244
+ return parseNdjsonEvent(event, sessionId);
245
+ }
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // v2 JSON-RPC notifications (codex --json app-server mode)
249
+ // ---------------------------------------------------------------------------
250
+
251
+ function parseV2Notification(event: Record<string, unknown>): StreamEvent | null {
252
+ const method = asString(event["method"], "");
253
+ const params = parseObject(event["params"]);
254
+
255
+ // Extract thread + turn scope. Most notifications carry threadId at the
256
+ // top of params; thread/started nests it under thread.id.
257
+ const thread = parseObject(params["thread"]);
258
+ const threadId =
259
+ asNullableString(params["threadId"]) ??
260
+ asNullableString(thread["id"]);
261
+
262
+ // turn/started + turn/completed nest the turn object; everything else
263
+ // puts turnId at the top of params.
264
+ const turn = parseObject(params["turn"]);
265
+ const turnId =
266
+ asNullableString(params["turnId"]) ??
267
+ asNullableString(turn["id"]);
268
+
269
+ const makeBase = (messageId: string | null) =>
270
+ baseFields(event, threadId, messageId, turnId);
271
+
272
+ // ---- Thread lifecycle ----
273
+
274
+ if (method === "thread/started") {
275
+ return {
276
+ type: "system",
277
+ subtype: "init",
278
+ model: null,
279
+ cwd: asNullableString(thread["cwd"]),
280
+ tools: null,
281
+ permissionMode: null,
282
+ ...makeBase(null),
283
+ };
284
+ }
285
+
286
+ // ---- Turn lifecycle ----
287
+
288
+ if (method === "turn/started") {
289
+ // Lifecycle marker; items will follow. Skip to reduce noise.
290
+ return null;
291
+ }
292
+
293
+ if (method === "turn/completed") {
294
+ // codex 0.130 reports turn failures via `turn/completed` with
295
+ // `turn.status: "failed"` (+ `turn.error.message`), NOT only `turn/failed`.
296
+ // Surface that as an errored result instead of a clean completion.
297
+ const status = asNullableString(turn["status"]);
298
+ const failed = status === "failed" || status === "cancelled";
299
+ const turnError = parseObject(turn["error"]);
300
+ return {
301
+ type: "result",
302
+ text: failed ? asString(turnError["message"], "") : "",
303
+ costUsd: null,
304
+ isError: failed,
305
+ stopReason: null,
306
+ terminalReason: status,
307
+ numTurns: null,
308
+ durationMs: asNullableNumber(turn["durationMs"]),
309
+ ...makeBase(null),
310
+ };
311
+ }
312
+
313
+ if (method === "turn/failed") {
314
+ return {
315
+ type: "result",
316
+ text: asString(params["message"], ""),
317
+ costUsd: null,
318
+ isError: true,
319
+ stopReason: null,
320
+ terminalReason: asNullableString(turn["status"]),
321
+ numTurns: null,
322
+ durationMs: asNullableNumber(turn["durationMs"]),
323
+ ...makeBase(null),
324
+ };
325
+ }
326
+
327
+ // ---- Item lifecycle ----
328
+
329
+ if (method === "item/started" || method === "item/completed") {
330
+ const item = parseObject(params["item"]);
331
+ const itemType = asString(item["type"], "");
332
+ const itemId =
333
+ asNullableString(item["id"]) ??
334
+ asNullableString(item["call_id"]);
335
+ const base = makeBase(itemId);
336
+
337
+ // Tool starts — emit tool_call on item/started only.
338
+ if (method === "item/started") {
339
+ if (itemType === "command_execution") {
340
+ return {
341
+ type: "tool_call",
342
+ toolCallId: itemId,
343
+ name: "command_execution",
344
+ input: asString(item["command"], ""),
345
+ ...base,
346
+ };
347
+ }
348
+ if (itemType === "function_call") {
349
+ return {
350
+ type: "tool_call",
351
+ toolCallId: itemId,
352
+ name: asString(item["name"], "function_call"),
353
+ input: item["arguments"] ?? item["input"] ?? "",
354
+ ...base,
355
+ };
356
+ }
357
+ // reasoning, agentMessage, userMessage — wait for item/completed.
358
+ return null;
359
+ }
360
+
361
+ // item/completed — emit the terminal event for each item type.
362
+ if (itemType === "command_execution") {
363
+ const exitCode = asNullableNumber(item["exit_code"]);
364
+ return {
365
+ type: "tool_result",
366
+ toolCallId: itemId,
367
+ toolName: "command_execution",
368
+ content: asString(item["aggregated_output"], ""),
369
+ isError: exitCode !== null && exitCode !== 0,
370
+ exitCode,
371
+ ...base,
372
+ };
373
+ }
374
+ if (itemType === "function_call") {
375
+ const output = item["output"] ?? item["result"] ?? "";
376
+ return {
377
+ type: "tool_result",
378
+ toolCallId: itemId,
379
+ toolName: asString(item["name"], "function_call"),
380
+ content: typeof output === "string" ? output : JSON.stringify(output),
381
+ isError: item["status"] === "failed",
382
+ exitCode: null,
383
+ ...base,
384
+ };
385
+ }
386
+ if (itemType === "agentMessage") {
387
+ const directText = asString(item["text"], "");
388
+ if (directText || directText === "") {
389
+ return {
390
+ type: "assistant",
391
+ text: directText,
392
+ ...base,
393
+ };
394
+ }
395
+ }
396
+ if (itemType === "reasoning") {
397
+ // Reasoning content may be empty / encrypted out-of-band — consumers
398
+ // that need the raw payload read it from `raw`.
399
+ const text =
400
+ asString(item["text"], "") ||
401
+ extractReasoningText(item);
402
+ return {
403
+ type: "thinking",
404
+ text,
405
+ ...base,
406
+ };
407
+ }
408
+ if (itemType === "userMessage") {
409
+ // Consumer persists user input on the write path before calling send().
410
+ // We don't re-emit it here to keep a single source of truth.
411
+ return null;
412
+ }
413
+ // Unknown item type — surface as unknown.
414
+ return {
415
+ type: "unknown",
416
+ subtype: `item/completed:${itemType}`,
417
+ ...base,
418
+ };
419
+ }
420
+
421
+ // ---- Streaming deltas: block-level only for v1; skip token deltas. ----
422
+
423
+ if (method === "item/agentMessage/delta" || method === "item/reasoning/delta") {
424
+ return null;
425
+ }
426
+
427
+ // ---- Rate limits ----
428
+
429
+ if (method === "account/rateLimits/updated") {
430
+ const rateLimits = parseObject(params["rateLimits"]);
431
+ const primary = parseObject(rateLimits["primary"]);
432
+ const usedPercent = asNullableNumber(primary["usedPercent"]);
433
+ return {
434
+ type: "rate_limit",
435
+ status: usedPercent !== null && usedPercent >= 100 ? "rejected" : "allowed",
436
+ limitType: asNullableString(rateLimits["limitId"]),
437
+ resetAt: null,
438
+ overageStatus: null,
439
+ isUsingOverage: null,
440
+ ...makeBase(null),
441
+ };
442
+ }
443
+
444
+ // ---- Goal lifecycle (experimental; wire shape unofficial — read loosely) ----
445
+
446
+ if (method === "thread/goal/updated" || method === "thread/goal/set") {
447
+ const ev = buildCodexGoalEvent(parseObject(params["goal"]), makeBase(null));
448
+ if (ev) return ev;
449
+ }
450
+
451
+ if (method === "thread/goal/cleared") {
452
+ const goal = parseObject(params["goal"]);
453
+ return {
454
+ type: "goal_status",
455
+ objective: asString(goal["objective"], ""),
456
+ status: "cleared",
457
+ met: false,
458
+ enforced: false,
459
+ source: "host",
460
+ ...makeBase(null),
461
+ };
462
+ }
463
+
464
+ // ---- Pure telemetry / status ----
465
+
466
+ if (
467
+ method === "thread/tokenUsage/updated" ||
468
+ method === "thread/status/changed" ||
469
+ method === "mcpServer/startupStatus/updated"
470
+ ) {
471
+ return null;
472
+ }
473
+
474
+ // ---- Forward-compat: unknown method ----
475
+
476
+ return {
477
+ type: "unknown",
478
+ subtype: method,
479
+ ...makeBase(null),
480
+ };
481
+ }
482
+
483
+ function extractReasoningText(item: Record<string, unknown>): string {
484
+ // Codex reasoning items carry summary and content arrays. Concatenate
485
+ // any visible text fragments for display; fall back to "" if all empty
486
+ // or encrypted.
487
+ const parts: string[] = [];
488
+ for (const key of ["summary", "content"]) {
489
+ const arr = Array.isArray(item[key]) ? (item[key] as unknown[]) : [];
490
+ for (const entry of arr) {
491
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
492
+ const block = entry as Record<string, unknown>;
493
+ const text = asString(block["text"], "");
494
+ if (text) parts.push(text);
495
+ }
496
+ }
497
+ return parts.join("\n").trim();
498
+ }
499
+
500
+ // ---------------------------------------------------------------------------
501
+ // NDJSON format (codex exec --json)
502
+ // ---------------------------------------------------------------------------
503
+
504
+ function parseNdjsonEvent(
505
+ event: Record<string, unknown>,
506
+ sessionId: string | null,
507
+ ): StreamEvent | null {
508
+ const type = asString(event["type"], "");
509
+
510
+ if (type === "thread.started") {
511
+ const threadId = asNullableString(event["thread_id"]);
512
+ return {
513
+ type: "system",
514
+ subtype: "init",
515
+ model: null,
516
+ cwd: null,
517
+ tools: null,
518
+ permissionMode: null,
519
+ ...baseFields(event, threadId, null, null),
520
+ };
521
+ }
522
+
523
+ if (type === "item.started") {
524
+ const item = parseObject(event["item"]);
525
+ const itemType = asString(item["type"], "");
526
+ const itemId = asNullableString(item["id"]) ?? asNullableString(item["call_id"]);
527
+ const base = baseFields(event, sessionId, itemId, null);
528
+ if (itemType === "command_execution") {
529
+ return {
530
+ type: "tool_call",
531
+ toolCallId: itemId,
532
+ name: "command_execution",
533
+ input: asString(item["command"], ""),
534
+ ...base,
535
+ };
536
+ }
537
+ if (itemType === "function_call") {
538
+ return {
539
+ type: "tool_call",
540
+ toolCallId: itemId,
541
+ name: asString(item["name"], "function_call"),
542
+ input: item["arguments"] ?? item["input"] ?? "",
543
+ ...base,
544
+ };
545
+ }
546
+ }
547
+
548
+ if (type === "item.completed") {
549
+ const item = parseObject(event["item"]);
550
+ const itemType = asString(item["type"], "");
551
+ const itemId = asNullableString(item["id"]) ?? asNullableString(item["call_id"]);
552
+ const base = baseFields(event, sessionId, itemId, null);
553
+ if (itemType === "command_execution") {
554
+ const exitCode = asNullableNumber(item["exit_code"]);
555
+ return {
556
+ type: "tool_result",
557
+ toolCallId: itemId,
558
+ toolName: "command_execution",
559
+ content: asString(item["aggregated_output"], ""),
560
+ isError: exitCode !== null && exitCode !== 0,
561
+ exitCode,
562
+ ...base,
563
+ };
564
+ }
565
+ if (itemType === "function_call") {
566
+ const output = item["output"] ?? item["result"] ?? "";
567
+ return {
568
+ type: "tool_result",
569
+ toolCallId: itemId,
570
+ toolName: asString(item["name"], "function_call"),
571
+ content: typeof output === "string" ? output : JSON.stringify(output),
572
+ isError: item["status"] === "failed",
573
+ exitCode: null,
574
+ ...base,
575
+ };
576
+ }
577
+ if (itemType === "agent_message") {
578
+ const directText = asString(item["text"], "");
579
+ if (directText) {
580
+ return {
581
+ type: "assistant",
582
+ text: directText,
583
+ ...base,
584
+ };
585
+ }
586
+ const content = Array.isArray(item["content"]) ? item["content"] : [];
587
+ for (const entry of content) {
588
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
589
+ const block = entry as Record<string, unknown>;
590
+ if (asString(block["type"], "") === "output_text") {
591
+ return {
592
+ type: "assistant",
593
+ text: asString(block["text"], ""),
594
+ ...base,
595
+ };
596
+ }
597
+ }
598
+ }
599
+ if (itemType === "reasoning") {
600
+ const text = asString(item["text"], "") || extractReasoningText(item);
601
+ return {
602
+ type: "thinking",
603
+ text,
604
+ ...base,
605
+ };
606
+ }
607
+ }
608
+
609
+ if (type === "turn.completed") {
610
+ return {
611
+ type: "result",
612
+ text: "",
613
+ costUsd: null,
614
+ isError: false,
615
+ stopReason: null,
616
+ terminalReason: null,
617
+ numTurns: null,
618
+ durationMs: null,
619
+ ...baseFields(event, sessionId, null, null),
620
+ };
621
+ }
622
+
623
+ if (type === "error" || type === "turn.failed") {
624
+ return {
625
+ type: "result",
626
+ text: asString(event["message"], ""),
627
+ costUsd: null,
628
+ isError: true,
629
+ stopReason: null,
630
+ terminalReason: null,
631
+ numTurns: null,
632
+ durationMs: null,
633
+ ...baseFields(event, sessionId, null, null),
634
+ };
635
+ }
636
+
637
+ // ---- Goal lifecycle (experimental; wire shape unofficial — read loosely) ----
638
+ // Observed on this machine as an `event_msg` wrapper; also tolerate the bare
639
+ // `thread_goal_updated` / `thread.goal.updated` spellings.
640
+ if (type === "event_msg") {
641
+ const payload = parseObject(event["payload"]);
642
+ if (asString(payload["type"], "") === "thread_goal_updated") {
643
+ const tid = asNullableString(payload["threadId"]) ?? sessionId;
644
+ const ev = buildCodexGoalEvent(parseObject(payload["goal"]), baseFields(event, tid, null, null));
645
+ if (ev) return ev;
646
+ }
647
+ }
648
+
649
+ if (type === "thread_goal_updated" || type === "thread.goal.updated") {
650
+ const tid = asNullableString(event["threadId"]) ?? asNullableString(event["thread_id"]) ?? sessionId;
651
+ const ev = buildCodexGoalEvent(parseObject(event["goal"]), baseFields(event, tid, null, null));
652
+ if (ev) return ev;
653
+ }
654
+
655
+ // Forward-compat: surface unknown event types.
656
+ return {
657
+ type: "unknown",
658
+ subtype: type,
659
+ ...baseFields(event, sessionId, null, null),
660
+ };
661
+ }
662
+
663
+ // ---------------------------------------------------------------------------
664
+ // Error detection utilities (unchanged)
665
+ // ---------------------------------------------------------------------------
666
+
667
+ const CODEX_ROLLOUT_NOISE_RE =
668
+ /^\d{4}-\d{2}-\d{2}T\S+\s+ERROR\s+codex_core::rollout::list:/i;
669
+
670
+ export function stripCodexRolloutNoise(text: string): string {
671
+ return text
672
+ .split(/\r?\n/)
673
+ .filter((line) => {
674
+ const trimmed = line.trim();
675
+ if (!trimmed) return true;
676
+ return !CODEX_ROLLOUT_NOISE_RE.test(trimmed);
677
+ })
678
+ .join("\n");
679
+ }
680
+
681
+ const CODEX_AUTH_RE = /OPENAI_API_KEY\s+is\s+not\s+set|unauthorized|authentication.*required|invalid.*api.*key/i;
682
+
683
+ export function isCodexAuthRequired(stdout: string, stderr: string): boolean {
684
+ return CODEX_AUTH_RE.test(stdout) || CODEX_AUTH_RE.test(stderr);
685
+ }
686
+
687
+ const CODEX_UNKNOWN_SESSION_RE = /unknown.*session|session.*not.*found|thread.*not.*found/i;
688
+
689
+ export function isCodexUnknownSessionError(stdout: string, stderr: string): boolean {
690
+ return CODEX_UNKNOWN_SESSION_RE.test(stdout) || CODEX_UNKNOWN_SESSION_RE.test(stderr);
691
+ }