@agentex/agent 0.0.24 → 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 (164) hide show
  1. package/CHANGELOG.md +338 -0
  2. package/LICENSE +21 -0
  3. package/README.md +52 -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 +4 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -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/goal-capability.d.ts +15 -0
  25. package/dist/providers/claude/goal-capability.d.ts.map +1 -0
  26. package/dist/providers/claude/goal-capability.js +20 -0
  27. package/dist/providers/claude/goal-capability.js.map +1 -0
  28. package/dist/providers/claude/index.d.ts.map +1 -1
  29. package/dist/providers/claude/index.js +8 -4
  30. package/dist/providers/claude/index.js.map +1 -1
  31. package/dist/providers/claude/session.d.ts +11 -9
  32. package/dist/providers/claude/session.d.ts.map +1 -1
  33. package/dist/providers/claude/session.js +29 -14
  34. package/dist/providers/claude/session.js.map +1 -1
  35. package/dist/providers/codex/attach.d.ts +9 -0
  36. package/dist/providers/codex/attach.d.ts.map +1 -0
  37. package/dist/providers/codex/attach.js +93 -0
  38. package/dist/providers/codex/attach.js.map +1 -0
  39. package/dist/providers/codex/goal-capability.d.ts +13 -0
  40. package/dist/providers/codex/goal-capability.d.ts.map +1 -0
  41. package/dist/providers/codex/goal-capability.js +18 -0
  42. package/dist/providers/codex/goal-capability.js.map +1 -0
  43. package/dist/providers/codex/index.d.ts +1 -0
  44. package/dist/providers/codex/index.d.ts.map +1 -1
  45. package/dist/providers/codex/index.js +9 -6
  46. package/dist/providers/codex/index.js.map +1 -1
  47. package/dist/providers/codex/session.d.ts +11 -7
  48. package/dist/providers/codex/session.d.ts.map +1 -1
  49. package/dist/providers/codex/session.js +24 -12
  50. package/dist/providers/codex/session.js.map +1 -1
  51. package/dist/providers/codex/transcript-normalize.d.ts +28 -0
  52. package/dist/providers/codex/transcript-normalize.d.ts.map +1 -0
  53. package/dist/providers/codex/transcript-normalize.js +191 -0
  54. package/dist/providers/codex/transcript-normalize.js.map +1 -0
  55. package/dist/providers/cursor/index.d.ts.map +1 -1
  56. package/dist/providers/cursor/index.js +2 -2
  57. package/dist/providers/cursor/index.js.map +1 -1
  58. package/dist/providers/openclaw/index.d.ts.map +1 -1
  59. package/dist/providers/openclaw/index.js +2 -2
  60. package/dist/providers/openclaw/index.js.map +1 -1
  61. package/dist/providers/opencode/index.d.ts.map +1 -1
  62. package/dist/providers/opencode/index.js +3 -5
  63. package/dist/providers/opencode/index.js.map +1 -1
  64. package/dist/providers/pi/index.d.ts.map +1 -1
  65. package/dist/providers/pi/index.js +3 -5
  66. package/dist/providers/pi/index.js.map +1 -1
  67. package/dist/providers/process/index.d.ts.map +1 -1
  68. package/dist/providers/process/index.js +2 -2
  69. package/dist/providers/process/index.js.map +1 -1
  70. package/dist/registry.d.ts +0 -1
  71. package/dist/registry.d.ts.map +1 -1
  72. package/dist/registry.js +0 -4
  73. package/dist/registry.js.map +1 -1
  74. package/dist/sessions/index.d.ts +3 -0
  75. package/dist/sessions/index.d.ts.map +1 -0
  76. package/dist/sessions/index.js +2 -0
  77. package/dist/sessions/index.js.map +1 -0
  78. package/dist/sessions/record.d.ts +43 -0
  79. package/dist/sessions/record.d.ts.map +1 -0
  80. package/dist/sessions/record.js +85 -0
  81. package/dist/sessions/record.js.map +1 -0
  82. package/dist/types.d.ts +119 -0
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js.map +1 -1
  85. package/dist/utils/uuid.d.ts +7 -1
  86. package/dist/utils/uuid.d.ts.map +1 -1
  87. package/dist/utils/uuid.js +21 -1
  88. package/dist/utils/uuid.js.map +1 -1
  89. package/package.json +64 -7
  90. package/src/derived.ts +311 -0
  91. package/src/goals/controller.ts +442 -0
  92. package/src/goals/index.ts +21 -0
  93. package/src/goals/normalize.ts +173 -0
  94. package/src/goals/sentinel.ts +90 -0
  95. package/src/index.ts +270 -0
  96. package/src/providers/_shared/http-agent.ts +304 -0
  97. package/src/providers/acp/index.ts +103 -0
  98. package/src/providers/acp/parse.ts +131 -0
  99. package/src/providers/acp/session.ts +744 -0
  100. package/src/providers/claude/attach.ts +147 -0
  101. package/src/providers/claude/codec.ts +43 -0
  102. package/src/providers/claude/execute.ts +300 -0
  103. package/src/providers/claude/goal-capability.ts +21 -0
  104. package/src/providers/claude/index.ts +72 -0
  105. package/src/providers/claude/mcp.ts +82 -0
  106. package/src/providers/claude/parse.ts +824 -0
  107. package/src/providers/claude/session.ts +1192 -0
  108. package/src/providers/claude/transcript.ts +555 -0
  109. package/src/providers/codex/attach.ts +123 -0
  110. package/src/providers/codex/codec.ts +50 -0
  111. package/src/providers/codex/execute.ts +337 -0
  112. package/src/providers/codex/goal-capability.ts +19 -0
  113. package/src/providers/codex/index.ts +57 -0
  114. package/src/providers/codex/modes.ts +159 -0
  115. package/src/providers/codex/parse.ts +691 -0
  116. package/src/providers/codex/plan-mode.ts +49 -0
  117. package/src/providers/codex/session.ts +1287 -0
  118. package/src/providers/codex/transcript-normalize.ts +197 -0
  119. package/src/providers/codex/transcript.ts +487 -0
  120. package/src/providers/codex/usage-scanner.ts +178 -0
  121. package/src/providers/copilot/index.ts +19 -0
  122. package/src/providers/cursor/codec.ts +44 -0
  123. package/src/providers/cursor/execute.ts +271 -0
  124. package/src/providers/cursor/index.ts +25 -0
  125. package/src/providers/cursor/parse.ts +288 -0
  126. package/src/providers/gemini/index.ts +21 -0
  127. package/src/providers/openclaw/codec.ts +40 -0
  128. package/src/providers/openclaw/execute.ts +19 -0
  129. package/src/providers/openclaw/index.ts +29 -0
  130. package/src/providers/opencode/codec.ts +50 -0
  131. package/src/providers/opencode/event-parse.ts +141 -0
  132. package/src/providers/opencode/execute.ts +251 -0
  133. package/src/providers/opencode/http-session.ts +427 -0
  134. package/src/providers/opencode/index.ts +30 -0
  135. package/src/providers/opencode/parse.ts +203 -0
  136. package/src/providers/opencode/server.ts +0 -0
  137. package/src/providers/pi/codec.ts +44 -0
  138. package/src/providers/pi/execute.ts +297 -0
  139. package/src/providers/pi/index.ts +30 -0
  140. package/src/providers/pi/parse.ts +231 -0
  141. package/src/providers/pi/session.ts +381 -0
  142. package/src/providers/process/execute.ts +148 -0
  143. package/src/providers/process/index.ts +52 -0
  144. package/src/registry.ts +40 -0
  145. package/src/sessions/index.ts +8 -0
  146. package/src/sessions/record.ts +108 -0
  147. package/src/types.ts +1638 -0
  148. package/src/utils/ask-user-question.ts +57 -0
  149. package/src/utils/auth.ts +661 -0
  150. package/src/utils/binary.ts +179 -0
  151. package/src/utils/endpoint.ts +172 -0
  152. package/src/utils/env.ts +63 -0
  153. package/src/utils/execute-all.ts +68 -0
  154. package/src/utils/exit-plan-mode.ts +40 -0
  155. package/src/utils/instructions.ts +427 -0
  156. package/src/utils/process.ts +223 -0
  157. package/src/utils/runtime-config.ts +100 -0
  158. package/src/utils/runtime-homes.ts +49 -0
  159. package/src/utils/skill-commands.ts +493 -0
  160. package/src/utils/skills.ts +500 -0
  161. package/src/utils/template.ts +16 -0
  162. package/src/utils/tool-names.ts +51 -0
  163. package/src/utils/uuid.ts +21 -0
  164. package/src/utils/workspace.ts +156 -0
@@ -0,0 +1,50 @@
1
+ import type { SessionCodec } from "../../types.js";
2
+
3
+ function readNonEmptyString(value: unknown): string | null {
4
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
5
+ }
6
+
7
+ function parseAsObject(raw: unknown): Record<string, unknown> | null {
8
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
9
+ return raw as Record<string, unknown>;
10
+ }
11
+
12
+ export const codexSessionCodec: SessionCodec = {
13
+ deserialize(raw: unknown): Record<string, unknown> | null {
14
+ const obj = parseAsObject(raw);
15
+ if (!obj) return null;
16
+ const sessionId =
17
+ readNonEmptyString(obj["sessionId"]) ??
18
+ readNonEmptyString(obj["session_id"]) ??
19
+ readNonEmptyString(obj["thread_id"]);
20
+ if (!sessionId) return null;
21
+ const cwd = readNonEmptyString(obj["cwd"]);
22
+ return {
23
+ sessionId,
24
+ ...(cwd ? { cwd } : {}),
25
+ };
26
+ },
27
+
28
+ serialize(params: Record<string, unknown> | null): Record<string, unknown> | null {
29
+ if (!params) return null;
30
+ const sessionId =
31
+ readNonEmptyString(params["sessionId"]) ??
32
+ readNonEmptyString(params["session_id"]) ??
33
+ readNonEmptyString(params["thread_id"]);
34
+ if (!sessionId) return null;
35
+ const cwd = readNonEmptyString(params["cwd"]);
36
+ return {
37
+ sessionId,
38
+ ...(cwd ? { cwd } : {}),
39
+ };
40
+ },
41
+
42
+ getDisplayId(params: Record<string, unknown> | null): string | null {
43
+ if (!params) return null;
44
+ return (
45
+ readNonEmptyString(params["sessionId"]) ??
46
+ readNonEmptyString(params["session_id"]) ??
47
+ readNonEmptyString(params["thread_id"])
48
+ );
49
+ },
50
+ };
@@ -0,0 +1,337 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import type { ExecutionContext, ExecutionResult } from "../../types.js";
5
+ import { findBinary } from "../../utils/binary.js";
6
+ import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
7
+ import { translateEndpoint } from "../../utils/endpoint.js";
8
+ import { runChildProcess, deriveErrorCode } from "../../utils/process.js";
9
+ import { detectAuth } from "../../utils/auth.js";
10
+ import { injectWorkspaceSkills } from "../../utils/skills.js";
11
+ import { resolveInstructions } from "../../utils/instructions.js";
12
+ import { createToolNameTracker } from "../../utils/tool-names.js";
13
+ import { prepareWorkspace } from "../../utils/workspace.js";
14
+ import type { PreparedWorkspace } from "../../utils/workspace.js";
15
+ import { uuidv7 } from "../../utils/uuid.js";
16
+ import {
17
+ parseCodexJsonl,
18
+ parseCodexStreamLine,
19
+ stripCodexRolloutNoise,
20
+ isCodexAuthRequired,
21
+ isCodexUnknownSessionError,
22
+ } from "./parse.js";
23
+ import { withPlanModePreamble } from "./plan-mode.js";
24
+ import { scanCodexSessionUsage } from "./usage-scanner.js";
25
+
26
+ export async function executeCodexProvider(ctx: ExecutionContext): Promise<ExecutionResult> {
27
+ const runId = ctx.runId ?? uuidv7();
28
+ let cwd = ctx.cwd ?? process.cwd();
29
+ const model = ctx.model ?? ctx.config?.model;
30
+ const config = ctx.config ?? {};
31
+ const startedAt = new Date().toISOString();
32
+
33
+ // 1. Resolve binary
34
+ ctx.onLifecycle?.({ phase: "preparing", step: "binary" });
35
+ let resolvedBinary;
36
+ try {
37
+ resolvedBinary = await findBinary("codex", config.command);
38
+ } catch (err) {
39
+ const errorMessage = err instanceof Error ? err.message : "Binary not found";
40
+ ctx.onLifecycle?.({ phase: "error", message: errorMessage });
41
+ return {
42
+ runId,
43
+ exitCode: null,
44
+ signal: null,
45
+ status: "failed" as const,
46
+ startedAt,
47
+ completedAt: new Date().toISOString(),
48
+ durationMs: Date.now() - new Date(startedAt).getTime(),
49
+ errorMessage,
50
+ errorCode: "binary_not_found",
51
+ costUsd: null,
52
+ model: null,
53
+ summary: null,
54
+ sessionParams: null,
55
+ sessionDisplayId: null,
56
+ clearSession: false,
57
+ billingType: null,
58
+ };
59
+ }
60
+
61
+ // 2. Workspace isolation
62
+ let workspace: PreparedWorkspace | undefined;
63
+ if (config.workspace) {
64
+ ctx.onLifecycle?.({ phase: "preparing", step: "workspace" });
65
+ workspace = await prepareWorkspace(cwd, config.workspace);
66
+ cwd = workspace.cwd;
67
+ }
68
+
69
+ // 3. Build env & resolve instructions
70
+ ctx.onLifecycle?.({ phase: "preparing", step: "instructions" });
71
+ const env = buildEnv(ctx.env);
72
+ ensurePathInEnv(env);
73
+ // Custom endpoint (BYOK / gateway / alt model) — codex needs both a
74
+ // synthesized model_providers block (`-c` args, added in buildArgs) and the
75
+ // key in env. `unset` is empty for codex (the ambient key never routes here).
76
+ const endpointTx = translateEndpoint("codex", config.endpoint);
77
+ Object.assign(env, endpointTx.env);
78
+ for (const key of endpointTx.unset) delete env[key];
79
+
80
+ // 3. Resolve instructions. In plan mode, prepend a preamble that tells the
81
+ // agent to investigate-and-propose rather than attempt-and-fail — Codex
82
+ // has no native plan-mode UX, so the system prompt is what makes plan
83
+ // mode actually work as a planning flow.
84
+ const baseInstructions = await resolveInstructions(config.instructionsFile);
85
+ const instructions = config.planMode
86
+ ? withPlanModePreamble(baseInstructions)
87
+ : baseInstructions;
88
+ const fullPrompt = instructions ? `${instructions}\n\n${ctx.prompt}` : ctx.prompt;
89
+
90
+ // 4. Inject skills into workspace
91
+ ctx.onLifecycle?.({ phase: "preparing", step: "skills" });
92
+ if (config.skillDirs && config.skillDirs.length > 0) {
93
+ try {
94
+ await injectWorkspaceSkills(config.skillDirs, cwd);
95
+ } catch {
96
+ // Non-fatal
97
+ }
98
+ }
99
+
100
+ // Detect auth/billing. Codex prefers its stored subscription (`codex login`)
101
+ // over OPENAI_API_KEY when both exist, so env-only detection would wrongly
102
+ // predict "api" whenever OPENAI_API_KEY is set. Check the auth.json stat:
103
+ // if the subscription file is present, billing is "subscription" regardless.
104
+ ctx.onLifecycle?.({ phase: "preparing", step: "auth" });
105
+ const codexAuthPath = path.join(
106
+ process.env["CODEX_HOME"] || path.join(os.homedir(), ".codex"),
107
+ "auth.json",
108
+ );
109
+ const hasCodexSubscription = await fs.stat(codexAuthPath).then(
110
+ (s) => s.isFile(),
111
+ () => false,
112
+ );
113
+ // A custom base URL is external/BYOK billing, not the codex subscription.
114
+ const billingType = config.endpoint?.baseUrl
115
+ ? "api"
116
+ : hasCodexSubscription
117
+ ? "subscription"
118
+ : detectAuth("codex", env).billingType;
119
+
120
+ // 4. Determine session resume
121
+ const sessionParams = ctx.sessionParams ?? null;
122
+ const sessionId = (() => {
123
+ if (!sessionParams) return null;
124
+ const id =
125
+ (sessionParams["sessionId"] as string | undefined) ??
126
+ (sessionParams["session_id"] as string | undefined) ??
127
+ (sessionParams["thread_id"] as string | undefined);
128
+ if (!id || typeof id !== "string") return null;
129
+ const sessionCwd = sessionParams["cwd"] as string | undefined;
130
+ if (sessionCwd && path.resolve(sessionCwd) !== path.resolve(cwd)) return null;
131
+ return id;
132
+ })();
133
+
134
+ // 5. Build args
135
+ const buildArgs = (resumeSessionId: string | null): string[] => {
136
+ const args = [...resolvedBinary.prefixArgs];
137
+ if (config.search) args.push("--search");
138
+ args.push("exec", "--json");
139
+ // planMode and skipPermissions are mutually exclusive — planMode wins.
140
+ if (config.planMode) {
141
+ args.push("--sandbox", "read-only");
142
+ } else if (config.skipPermissions) {
143
+ args.push("--dangerously-bypass-approvals-and-sandbox");
144
+ }
145
+ if (model) args.push("--model", model);
146
+ if (config.effort) args.push("-c", `model_reasoning_effort=${JSON.stringify(config.effort)}`);
147
+ // Custom endpoint model_providers overrides — before extraArgs so hosts can override.
148
+ if (endpointTx.args.length > 0) args.push(...endpointTx.args);
149
+ if (config.extraArgs) args.push(...config.extraArgs);
150
+ if (resumeSessionId) {
151
+ args.push("resume", resumeSessionId, "-");
152
+ } else {
153
+ args.push("-");
154
+ }
155
+ return args;
156
+ };
157
+
158
+ // 6. Run attempt
159
+ const runAttempt = async (resumeSessionId: string | null) => {
160
+ const args = buildArgs(resumeSessionId);
161
+ let lineBuffer = "";
162
+ // Track thread_id across lines — Codex only emits it once (thread.started)
163
+ // but downstream events need it attached for DB correlation.
164
+ let streamThreadId: string | null = resumeSessionId;
165
+ // Correlates tool_call → tool_result so emitted tool_result events carry
166
+ // toolName. One tracker per attempt (a retry restarts the stream).
167
+ const trackToolName = createToolNameTracker();
168
+
169
+ const handleLine = async (trimmed: string) => {
170
+ if (!trimmed) return;
171
+ if (streamThreadId === null) {
172
+ try {
173
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>;
174
+ if (parsed && parsed["type"] === "thread.started" && typeof parsed["thread_id"] === "string") {
175
+ streamThreadId = parsed["thread_id"];
176
+ }
177
+ } catch { /* ignore */ }
178
+ }
179
+ if (!ctx.onEvent) return;
180
+ const event = parseCodexStreamLine(trimmed, streamThreadId);
181
+ if (event) {
182
+ try { await ctx.onEvent(trackToolName(event)); } catch { /* swallow */ }
183
+ }
184
+ };
185
+
186
+ ctx.onLifecycle?.({ phase: "spawning" });
187
+ const proc = await runChildProcess({
188
+ runId,
189
+ command: resolvedBinary.bin,
190
+ args,
191
+ cwd,
192
+ env,
193
+ stdin: fullPrompt,
194
+ timeoutSec: config.timeoutSec,
195
+ graceSec: config.graceSec,
196
+ onStart: (pid) => {
197
+ ctx.onLifecycle?.({ phase: "running", pid });
198
+ ctx.onStart?.(pid);
199
+ },
200
+ signal: ctx.signal,
201
+ onOutput: async (stream, chunk) => {
202
+ if (stream === "stderr") {
203
+ // Filter rollout noise before forwarding
204
+ const cleaned = stripCodexRolloutNoise(chunk);
205
+ if (cleaned.trim() && ctx.onOutput) {
206
+ try { await ctx.onOutput(stream, cleaned); } catch { /* swallow */ }
207
+ }
208
+ } else {
209
+ if (ctx.onOutput) {
210
+ try { await ctx.onOutput(stream, chunk); } catch { /* swallow */ }
211
+ }
212
+
213
+ // Parse stdout lines for stream events
214
+ lineBuffer += chunk;
215
+ const lines = lineBuffer.split("\n");
216
+ lineBuffer = lines.pop() ?? "";
217
+ for (const line of lines) {
218
+ await handleLine(line.trim());
219
+ }
220
+ }
221
+ },
222
+ });
223
+
224
+ if (lineBuffer.trim()) {
225
+ await handleLine(lineBuffer.trim());
226
+ }
227
+
228
+ return proc;
229
+ };
230
+
231
+ // 7. Initial attempt
232
+ let proc = await runAttempt(sessionId);
233
+ let clearSession = false;
234
+
235
+ // Check for unknown session — retry once
236
+ if (
237
+ sessionId &&
238
+ !proc.timedOut &&
239
+ (proc.exitCode ?? 0) !== 0 &&
240
+ isCodexUnknownSessionError(proc.stdout, proc.stderr)
241
+ ) {
242
+ proc = await runAttempt(null);
243
+ clearSession = true;
244
+ }
245
+
246
+ const parsed = parseCodexJsonl(proc.stdout);
247
+ const processErrorCode = deriveErrorCode(proc);
248
+
249
+ let errorCode = processErrorCode;
250
+ if (!errorCode && isCodexAuthRequired(proc.stdout, proc.stderr)) {
251
+ errorCode = "auth_required";
252
+ // Codex emits `type: "error"` / `type: "turn.failed"` events but those
253
+ // are generic — the auth-vs-rate-limit distinction lives in the text.
254
+ // Surface an auth_required stream event so consumers wired to onEvent
255
+ // get the same uniform signal as Claude.
256
+ if (ctx.onEvent) {
257
+ try {
258
+ await ctx.onEvent({
259
+ type: "auth_required",
260
+ httpStatus: null,
261
+ reason: parsed.errorMessage && /invalid.*api.*key/i.test(parsed.errorMessage) ? "invalid" : "missing",
262
+ loginCommand: "codex login",
263
+ message: parsed.errorMessage,
264
+ timestamp: new Date().toISOString(),
265
+ providerType: "codex",
266
+ sessionId: parsed.sessionId,
267
+ messageId: null,
268
+ eventId: null,
269
+ turnId: null,
270
+ parentToolCallId: null,
271
+ raw: parsed.finalEvent ?? {},
272
+ });
273
+ } catch { /* swallow */ }
274
+ }
275
+ }
276
+
277
+ const errorMessage = (() => {
278
+ if (proc.timedOut) return `Timed out after ${config.timeoutSec ?? 0}s`;
279
+ if (errorCode === "auth_required") return "Codex requires OPENAI_API_KEY.";
280
+ if ((proc.exitCode ?? 0) !== 0) {
281
+ return parsed.errorMessage ?? stripCodexRolloutNoise(proc.stderr).split(/\r?\n/).map((l) => l.trim()).find(Boolean) ?? `Codex exited with code ${proc.exitCode ?? -1}`;
282
+ }
283
+ if (parsed.isError) return parsed.errorMessage;
284
+ return null;
285
+ })();
286
+
287
+ const resolvedSessionId = parsed.sessionId;
288
+ const resultSessionParams = resolvedSessionId
289
+ ? { sessionId: resolvedSessionId, cwd }
290
+ : null;
291
+
292
+ const completedAt = new Date().toISOString();
293
+ const resolvedModel = parsed.model ?? model ?? null;
294
+ const status = proc.aborted ? "aborted" as const
295
+ : proc.timedOut ? "timeout" as const
296
+ : (errorCode || errorMessage) ? "failed" as const
297
+ : "completed" as const;
298
+
299
+ if (status === "completed") {
300
+ ctx.onLifecycle?.({ phase: "completed" });
301
+ } else if (status === "aborted") {
302
+ ctx.onLifecycle?.({ phase: "cancelled" });
303
+ } else {
304
+ ctx.onLifecycle?.({ phase: "error", message: errorMessage ?? "Unknown error" });
305
+ }
306
+
307
+ return {
308
+ runId,
309
+ exitCode: proc.exitCode,
310
+ signal: proc.signal ?? null,
311
+ status,
312
+ startedAt,
313
+ completedAt,
314
+ durationMs: new Date(completedAt).getTime() - new Date(startedAt).getTime(),
315
+ errorMessage,
316
+ errorCode,
317
+ usage: parsed.usage && resolvedModel
318
+ ? { [resolvedModel]: {
319
+ inputTokens: parsed.usage.inputTokens,
320
+ outputTokens: parsed.usage.outputTokens,
321
+ ...(parsed.usage.cachedInputTokens !== undefined ? { cachedInputTokens: parsed.usage.cachedInputTokens } : {}),
322
+ } }
323
+ : await scanCodexSessionUsage({
324
+ startedAfter: new Date(startedAt),
325
+ threadId: resolvedSessionId ?? undefined,
326
+ }),
327
+ costUsd: null,
328
+ model: resolvedModel,
329
+ summary: parsed.summary,
330
+ sessionParams: resultSessionParams,
331
+ sessionDisplayId: resolvedSessionId,
332
+ clearSession,
333
+ billingType,
334
+ raw: parsed.finalEvent,
335
+ workspace,
336
+ };
337
+ }
@@ -0,0 +1,19 @@
1
+ import type { GoalCapability } from "../../types.js";
2
+
3
+ /**
4
+ * Codex carries goals as durable thread state mutated by model tools
5
+ * (create_goal/update_goal/get_goal). Advisory — the model self-reports; no
6
+ * turn-end gate. Goal mode is experimental and feature-gated upstream, so the
7
+ * native arm is best-effort and falls back to emulation (see spec §7.2).
8
+ *
9
+ * Lives in its own leaf module (not `session.ts`) so `index.ts` can read the
10
+ * static `capabilities.goals` field without pulling the heavy session machinery
11
+ * into the module graph (spec §5.1).
12
+ */
13
+ export const codexGoalCapability: GoalCapability = {
14
+ mechanism: "model-tools",
15
+ enforced: false,
16
+ statuses: ["active", "paused", "met", "blocked", "cleared"],
17
+ clears: "manual",
18
+ telemetry: true,
19
+ };
@@ -0,0 +1,57 @@
1
+ import type { ProviderModule, SessionContext, AgentSession } from "../../types.js";
2
+ import { codexSessionCodec } from "./codec.js";
3
+ import { resolveAuthForProvider } from "../../utils/auth.js";
4
+ import { codexTranscriptOps } from "./transcript.js";
5
+ import { codexGoalCapability } from "./goal-capability.js";
6
+
7
+ export const codexProvider: ProviderModule = {
8
+ type: "codex",
9
+ capabilities: {
10
+ sessions: true,
11
+ modelDiscovery: false,
12
+ quotaProbing: false,
13
+ mcp: false,
14
+ skills: true,
15
+ skillInventory: "local-discovery",
16
+ skillInvocation: "expanded-prompt",
17
+ instructions: true,
18
+ workspace: true,
19
+ planMode: true,
20
+ concurrentSend: true,
21
+ cancelQueuedMessage: false,
22
+ stopTask: false,
23
+ modes: true,
24
+ goals: codexGoalCapability,
25
+ durableSessions: true,
26
+ },
27
+ // Heavy machinery (execute.ts, session.ts, modes.ts, attach.ts) loads lazily
28
+ // on first use — every ProviderModule method is already async.
29
+ execute: async (ctx) => (await import("./execute.js")).executeCodexProvider(ctx),
30
+ createSession: async (ctx: SessionContext): Promise<AgentSession> =>
31
+ (await import("./session.js")).createCodexSession(ctx),
32
+ resolveAuth: (ctx) => resolveAuthForProvider("codex", ctx),
33
+ sessionCodec: codexSessionCodec,
34
+ transcript: codexTranscriptOps,
35
+ listModes: async (opts) => (await import("./modes.js")).listCodexModes(opts),
36
+ attachSession: async (record, opts) =>
37
+ (await import("./attach.js")).attachCodexSession(record, opts),
38
+ };
39
+
40
+ export {
41
+ getCodexTranscriptPath,
42
+ readCodexTranscript,
43
+ peekCodexTranscript,
44
+ readCodexCwd,
45
+ codexTranscriptOps,
46
+ parseCodexLine,
47
+ resolveCodexHome,
48
+ } from "./transcript.js";
49
+ export { codexLineToStreamEvents } from "./transcript-normalize.js";
50
+ export type {
51
+ GetCodexTranscriptPathOptions,
52
+ CodexTranscriptLocation,
53
+ ReadCodexTranscriptOptions,
54
+ CodexTranscriptYield,
55
+ CodexTranscriptLine,
56
+ CodexPeekResult,
57
+ } from "./transcript.js";
@@ -0,0 +1,159 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { AgentMode, ListModesOptions } from "../../types.js";
3
+ import { findBinary } from "../../utils/binary.js";
4
+ import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
5
+
6
+ /**
7
+ * A raw Codex "collaboration mode" entry as returned by the app-server's
8
+ * `collaborationMode/list` RPC. Codex's collaboration modes (e.g. Auto, Plan,
9
+ * Read Only) are the cross-provider equivalent of agentex `AgentMode`s.
10
+ */
11
+ export interface CodexCollaborationMode {
12
+ name: string;
13
+ mode: string | null;
14
+ model: string | null;
15
+ reasoning_effort: string | null;
16
+ developer_instructions: string | null;
17
+ }
18
+
19
+ /** Parse a `collaborationMode/list` response into raw entries (tolerant of
20
+ * missing fields; drops entries without a name). */
21
+ export function parseCollaborationModes(response: unknown): CodexCollaborationMode[] {
22
+ const data =
23
+ response && typeof response === "object" && Array.isArray((response as Record<string, unknown>)["data"])
24
+ ? ((response as Record<string, unknown>)["data"] as unknown[])
25
+ : [];
26
+ const out: CodexCollaborationMode[] = [];
27
+ for (const entry of data) {
28
+ if (typeof entry !== "object" || entry === null) continue;
29
+ const r = entry as Record<string, unknown>;
30
+ const name = typeof r["name"] === "string" ? r["name"] : "";
31
+ if (!name) continue;
32
+ out.push({
33
+ name,
34
+ mode: typeof r["mode"] === "string" ? r["mode"] : null,
35
+ model: typeof r["model"] === "string" ? r["model"] : null,
36
+ reasoning_effort: typeof r["reasoning_effort"] === "string" ? r["reasoning_effort"] : null,
37
+ developer_instructions:
38
+ typeof r["developer_instructions"] === "string" ? r["developer_instructions"] : null,
39
+ });
40
+ }
41
+ return out;
42
+ }
43
+
44
+ /** Convert raw collaboration modes into the cross-provider `AgentMode` shape.
45
+ * `id` is the mode string (the value passed back via `config.modeId`); falls
46
+ * back to the name when codex doesn't supply a distinct mode value. */
47
+ export function toAgentModes(modes: CodexCollaborationMode[]): AgentMode[] {
48
+ return modes.map((m) => ({
49
+ id: m.mode ?? m.name,
50
+ name: m.name,
51
+ ...(m.developer_instructions ? { description: m.developer_instructions } : {}),
52
+ }));
53
+ }
54
+
55
+ /** Build the `thread/start` / `thread/resume` `collaborationMode` parameter for
56
+ * a chosen mode id, mirroring the shape codex's app-server expects. Returns
57
+ * null when the id doesn't match any known mode. */
58
+ export function resolveCollaborationModeParam(
59
+ modes: CodexCollaborationMode[],
60
+ modeId: string,
61
+ ): { mode: string; settings: Record<string, unknown> } | null {
62
+ const match = modes.find((m) => m.mode === modeId) ?? modes.find((m) => m.name === modeId);
63
+ if (!match) return null;
64
+ const settings: Record<string, unknown> = {};
65
+ if (match.model) settings["model"] = match.model;
66
+ if (match.reasoning_effort) settings["reasoning_effort"] = match.reasoning_effort;
67
+ if (match.developer_instructions) settings["developer_instructions"] = match.developer_instructions;
68
+ return { mode: match.mode ?? match.name, settings };
69
+ }
70
+
71
+ /**
72
+ * Minimal one-shot JSON-RPC over a child's stdio: send `initialize`, then the
73
+ * `target` method, resolve with the target's result, and tear the process down.
74
+ * Used by `listCodexModes` so callers can discover modes without opening a full
75
+ * session.
76
+ */
77
+ function oneShotRpc(
78
+ proc: ReturnType<typeof spawn>,
79
+ target: string,
80
+ timeoutMs: number,
81
+ ): Promise<unknown> {
82
+ return new Promise((resolve, reject) => {
83
+ if (!proc.stdin || !proc.stdout) {
84
+ reject(new Error("codex app-server stdio unavailable"));
85
+ return;
86
+ }
87
+ let buffer = "";
88
+ let settled = false;
89
+ const initId = 1;
90
+ const targetId = 2;
91
+
92
+ const timer = setTimeout(() => finish(new Error(`codex ${target} timed out`)), timeoutMs);
93
+
94
+ function finish(err: Error | null, value?: unknown): void {
95
+ if (settled) return;
96
+ settled = true;
97
+ clearTimeout(timer);
98
+ try { proc.kill(); } catch { /* ignore */ }
99
+ if (err) reject(err);
100
+ else resolve(value);
101
+ }
102
+
103
+ proc.stdout.setEncoding("utf-8");
104
+ proc.stdout.on("data", (chunk: string) => {
105
+ buffer += chunk;
106
+ const lines = buffer.split("\n");
107
+ buffer = lines.pop() ?? "";
108
+ for (const line of lines) {
109
+ const trimmed = line.trim();
110
+ if (!trimmed) continue;
111
+ let msg: Record<string, unknown>;
112
+ try { msg = JSON.parse(trimmed) as Record<string, unknown>; } catch { continue; }
113
+ if (msg["id"] === initId && !("method" in msg)) {
114
+ // initialize acknowledged → request the target.
115
+ proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", id: targetId, method: target, params: {} }) + "\n");
116
+ } else if (msg["id"] === targetId && !("method" in msg)) {
117
+ if (msg["error"]) {
118
+ const e = msg["error"] as { message?: string };
119
+ finish(new Error(`codex ${target} error: ${e.message ?? "unknown"}`));
120
+ } else {
121
+ finish(null, msg["result"] ?? {});
122
+ }
123
+ }
124
+ }
125
+ });
126
+ proc.on("error", (err) => finish(err instanceof Error ? err : new Error(String(err))));
127
+ proc.on("exit", (code) => {
128
+ if (!settled) finish(new Error(`codex app-server exited (code=${code}) before responding`));
129
+ });
130
+
131
+ proc.stdin.write(
132
+ JSON.stringify({ jsonrpc: "2.0", id: initId, method: "initialize", params: { clientInfo: { name: "agentex", version: "1.0.0" }, capabilities: {} } }) + "\n",
133
+ );
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Discover Codex's collaboration modes by spawning a throwaway `codex
139
+ * app-server`, running the initialize handshake, requesting
140
+ * `collaborationMode/list`, and closing. Returns [] on any failure (modes are
141
+ * advisory — a discovery failure shouldn't surface as a hard error).
142
+ */
143
+ export async function listCodexModes(options?: ListModesOptions): Promise<AgentMode[]> {
144
+ const config = options?.config ?? {};
145
+ const resolved = await findBinary("codex", config.command);
146
+ const env = buildEnv(options?.env);
147
+ ensurePathInEnv(env);
148
+ const proc = spawn(resolved.bin, [...resolved.prefixArgs, "app-server"], {
149
+ cwd: options?.cwd ?? process.cwd(),
150
+ env,
151
+ stdio: ["pipe", "pipe", "pipe"],
152
+ });
153
+ try {
154
+ const response = await oneShotRpc(proc, "collaborationMode/list", config.timeoutSec ? config.timeoutSec * 1000 : 15_000);
155
+ return toAgentModes(parseCollaborationModes(response));
156
+ } catch {
157
+ return [];
158
+ }
159
+ }