@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.
- package/CHANGELOG.md +338 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/derived.d.ts +5 -3
- package/dist/derived.d.ts.map +1 -1
- package/dist/derived.js +11 -7
- package/dist/derived.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/acp/index.d.ts +1 -1
- package/dist/providers/acp/index.d.ts.map +1 -1
- package/dist/providers/acp/index.js +5 -97
- package/dist/providers/acp/index.js.map +1 -1
- package/dist/providers/acp/session.d.ts +8 -1
- package/dist/providers/acp/session.d.ts.map +1 -1
- package/dist/providers/acp/session.js +94 -0
- package/dist/providers/acp/session.js.map +1 -1
- package/dist/providers/claude/attach.d.ts +8 -0
- package/dist/providers/claude/attach.d.ts.map +1 -0
- package/dist/providers/claude/attach.js +113 -0
- package/dist/providers/claude/attach.js.map +1 -0
- package/dist/providers/claude/execute.d.ts.map +1 -1
- package/dist/providers/claude/execute.js +17 -2
- package/dist/providers/claude/execute.js.map +1 -1
- package/dist/providers/claude/goal-capability.d.ts +15 -0
- package/dist/providers/claude/goal-capability.d.ts.map +1 -0
- package/dist/providers/claude/goal-capability.js +20 -0
- package/dist/providers/claude/goal-capability.js.map +1 -0
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +8 -4
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/session.d.ts +11 -9
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +36 -14
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/attach.d.ts +9 -0
- package/dist/providers/codex/attach.d.ts.map +1 -0
- package/dist/providers/codex/attach.js +93 -0
- package/dist/providers/codex/attach.js.map +1 -0
- package/dist/providers/codex/execute.d.ts.map +1 -1
- package/dist/providers/codex/execute.js +17 -3
- package/dist/providers/codex/execute.js.map +1 -1
- package/dist/providers/codex/goal-capability.d.ts +13 -0
- package/dist/providers/codex/goal-capability.d.ts.map +1 -0
- package/dist/providers/codex/goal-capability.js +18 -0
- package/dist/providers/codex/goal-capability.js.map +1 -0
- package/dist/providers/codex/index.d.ts +1 -0
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +9 -6
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/session.d.ts +11 -7
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +37 -12
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/codex/transcript-normalize.d.ts +28 -0
- package/dist/providers/codex/transcript-normalize.d.ts.map +1 -0
- package/dist/providers/codex/transcript-normalize.js +191 -0
- package/dist/providers/codex/transcript-normalize.js.map +1 -0
- package/dist/providers/cursor/index.d.ts.map +1 -1
- package/dist/providers/cursor/index.js +2 -2
- package/dist/providers/cursor/index.js.map +1 -1
- package/dist/providers/openclaw/index.d.ts.map +1 -1
- package/dist/providers/openclaw/index.js +2 -2
- package/dist/providers/openclaw/index.js.map +1 -1
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +3 -5
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/pi/index.d.ts.map +1 -1
- package/dist/providers/pi/index.js +3 -5
- package/dist/providers/pi/index.js.map +1 -1
- package/dist/providers/process/index.d.ts.map +1 -1
- package/dist/providers/process/index.js +2 -2
- package/dist/providers/process/index.js.map +1 -1
- package/dist/registry.d.ts +0 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +0 -4
- package/dist/registry.js.map +1 -1
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/record.d.ts +43 -0
- package/dist/sessions/record.d.ts.map +1 -0
- package/dist/sessions/record.js +85 -0
- package/dist/sessions/record.js.map +1 -0
- package/dist/types.d.ts +176 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/endpoint.d.ts +38 -0
- package/dist/utils/endpoint.d.ts.map +1 -0
- package/dist/utils/endpoint.js +151 -0
- package/dist/utils/endpoint.js.map +1 -0
- package/dist/utils/env.d.ts.map +1 -1
- package/dist/utils/env.js +5 -1
- package/dist/utils/env.js.map +1 -1
- package/dist/utils/uuid.d.ts +7 -1
- package/dist/utils/uuid.d.ts.map +1 -1
- package/dist/utils/uuid.js +21 -1
- package/dist/utils/uuid.js.map +1 -1
- package/package.json +64 -7
- package/src/derived.ts +311 -0
- package/src/goals/controller.ts +442 -0
- package/src/goals/index.ts +21 -0
- package/src/goals/normalize.ts +173 -0
- package/src/goals/sentinel.ts +90 -0
- package/src/index.ts +270 -0
- package/src/providers/_shared/http-agent.ts +304 -0
- package/src/providers/acp/index.ts +103 -0
- package/src/providers/acp/parse.ts +131 -0
- package/src/providers/acp/session.ts +744 -0
- package/src/providers/claude/attach.ts +147 -0
- package/src/providers/claude/codec.ts +43 -0
- package/src/providers/claude/execute.ts +300 -0
- package/src/providers/claude/goal-capability.ts +21 -0
- package/src/providers/claude/index.ts +72 -0
- package/src/providers/claude/mcp.ts +82 -0
- package/src/providers/claude/parse.ts +824 -0
- package/src/providers/claude/session.ts +1192 -0
- package/src/providers/claude/transcript.ts +555 -0
- package/src/providers/codex/attach.ts +123 -0
- package/src/providers/codex/codec.ts +50 -0
- package/src/providers/codex/execute.ts +337 -0
- package/src/providers/codex/goal-capability.ts +19 -0
- package/src/providers/codex/index.ts +57 -0
- package/src/providers/codex/modes.ts +159 -0
- package/src/providers/codex/parse.ts +691 -0
- package/src/providers/codex/plan-mode.ts +49 -0
- package/src/providers/codex/session.ts +1287 -0
- package/src/providers/codex/transcript-normalize.ts +197 -0
- package/src/providers/codex/transcript.ts +487 -0
- package/src/providers/codex/usage-scanner.ts +178 -0
- package/src/providers/copilot/index.ts +19 -0
- package/src/providers/cursor/codec.ts +44 -0
- package/src/providers/cursor/execute.ts +271 -0
- package/src/providers/cursor/index.ts +25 -0
- package/src/providers/cursor/parse.ts +288 -0
- package/src/providers/gemini/index.ts +21 -0
- package/src/providers/openclaw/codec.ts +40 -0
- package/src/providers/openclaw/execute.ts +19 -0
- package/src/providers/openclaw/index.ts +29 -0
- package/src/providers/opencode/codec.ts +50 -0
- package/src/providers/opencode/event-parse.ts +141 -0
- package/src/providers/opencode/execute.ts +251 -0
- package/src/providers/opencode/http-session.ts +427 -0
- package/src/providers/opencode/index.ts +30 -0
- package/src/providers/opencode/parse.ts +203 -0
- package/src/providers/opencode/server.ts +0 -0
- package/src/providers/pi/codec.ts +44 -0
- package/src/providers/pi/execute.ts +297 -0
- package/src/providers/pi/index.ts +30 -0
- package/src/providers/pi/parse.ts +231 -0
- package/src/providers/pi/session.ts +381 -0
- package/src/providers/process/execute.ts +148 -0
- package/src/providers/process/index.ts +52 -0
- package/src/registry.ts +40 -0
- package/src/sessions/index.ts +8 -0
- package/src/sessions/record.ts +108 -0
- package/src/types.ts +1638 -0
- package/src/utils/ask-user-question.ts +57 -0
- package/src/utils/auth.ts +661 -0
- package/src/utils/binary.ts +179 -0
- package/src/utils/endpoint.ts +172 -0
- package/src/utils/env.ts +63 -0
- package/src/utils/execute-all.ts +68 -0
- package/src/utils/exit-plan-mode.ts +40 -0
- package/src/utils/instructions.ts +427 -0
- package/src/utils/process.ts +223 -0
- package/src/utils/runtime-config.ts +100 -0
- package/src/utils/runtime-homes.ts +49 -0
- package/src/utils/skill-commands.ts +493 -0
- package/src/utils/skills.ts +500 -0
- package/src/utils/template.ts +16 -0
- package/src/utils/tool-names.ts +51 -0
- package/src/utils/uuid.ts +21 -0
- 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
|
+
}
|