@agentex/agent 0.0.16 → 0.0.19
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/README.md +158 -18
- package/dist/derived.d.ts +69 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +218 -0
- package/dist/derived.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/_shared/http-agent.d.ts +39 -0
- package/dist/providers/_shared/http-agent.d.ts.map +1 -0
- package/dist/providers/_shared/http-agent.js +265 -0
- package/dist/providers/_shared/http-agent.js.map +1 -0
- package/dist/providers/acp/index.d.ts +29 -0
- package/dist/providers/acp/index.d.ts.map +1 -0
- package/dist/providers/acp/index.js +153 -0
- package/dist/providers/acp/index.js.map +1 -0
- package/dist/providers/acp/parse.d.ts +22 -0
- package/dist/providers/acp/parse.d.ts.map +1 -0
- package/dist/providers/acp/parse.js +122 -0
- package/dist/providers/acp/parse.js.map +1 -0
- package/dist/providers/acp/session.d.ts +36 -0
- package/dist/providers/acp/session.d.ts.map +1 -0
- package/dist/providers/acp/session.js +487 -0
- package/dist/providers/acp/session.js.map +1 -0
- package/dist/providers/claude/execute.d.ts.map +1 -1
- package/dist/providers/claude/execute.js +6 -2
- package/dist/providers/claude/execute.js.map +1 -1
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +3 -0
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/parse.d.ts.map +1 -1
- package/dist/providers/claude/parse.js +8 -0
- package/dist/providers/claude/parse.js.map +1 -1
- package/dist/providers/claude/session.d.ts +43 -4
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +215 -30
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/execute.d.ts.map +1 -1
- package/dist/providers/codex/execute.js +5 -1
- package/dist/providers/codex/execute.js.map +1 -1
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +5 -0
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/modes.d.ts +35 -0
- package/dist/providers/codex/modes.d.ts.map +1 -0
- package/dist/providers/codex/modes.js +148 -0
- package/dist/providers/codex/modes.js.map +1 -0
- package/dist/providers/codex/parse.d.ts.map +1 -1
- package/dist/providers/codex/parse.js +4 -0
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/session.d.ts +45 -4
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +408 -66
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/copilot/index.d.ts +15 -0
- package/dist/providers/copilot/index.d.ts.map +1 -0
- package/dist/providers/copilot/index.js +19 -0
- package/dist/providers/copilot/index.js.map +1 -0
- package/dist/providers/cursor/index.d.ts.map +1 -1
- package/dist/providers/cursor/index.js +3 -0
- package/dist/providers/cursor/index.js.map +1 -1
- package/dist/providers/cursor/parse.d.ts.map +1 -1
- package/dist/providers/cursor/parse.js +1 -0
- package/dist/providers/cursor/parse.js.map +1 -1
- package/dist/providers/gemini/index.d.ts.map +1 -1
- package/dist/providers/gemini/index.js +16 -18
- package/dist/providers/gemini/index.js.map +1 -1
- package/dist/providers/openclaw/execute.d.ts +5 -0
- package/dist/providers/openclaw/execute.d.ts.map +1 -1
- package/dist/providers/openclaw/execute.js +13 -173
- package/dist/providers/openclaw/execute.js.map +1 -1
- package/dist/providers/openclaw/index.d.ts.map +1 -1
- package/dist/providers/openclaw/index.js +3 -0
- package/dist/providers/openclaw/index.js.map +1 -1
- package/dist/providers/opencode/event-parse.d.ts +23 -0
- package/dist/providers/opencode/event-parse.d.ts.map +1 -0
- package/dist/providers/opencode/event-parse.js +128 -0
- package/dist/providers/opencode/event-parse.js.map +1 -0
- package/dist/providers/opencode/http-session.d.ts +4 -0
- package/dist/providers/opencode/http-session.d.ts.map +1 -0
- package/dist/providers/opencode/http-session.js +376 -0
- package/dist/providers/opencode/http-session.js.map +1 -0
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +8 -1
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/opencode/parse.d.ts.map +1 -1
- package/dist/providers/opencode/parse.js +1 -0
- package/dist/providers/opencode/parse.js.map +1 -1
- package/dist/providers/opencode/server.d.ts +8 -0
- package/dist/providers/opencode/server.d.ts.map +1 -0
- package/dist/providers/opencode/server.js +0 -0
- package/dist/providers/opencode/server.js.map +1 -0
- package/dist/providers/pi/index.d.ts.map +1 -1
- package/dist/providers/pi/index.js +8 -1
- package/dist/providers/pi/index.js.map +1 -1
- package/dist/providers/pi/parse.d.ts.map +1 -1
- package/dist/providers/pi/parse.js +1 -0
- package/dist/providers/pi/parse.js.map +1 -1
- package/dist/providers/pi/session.d.ts +40 -0
- package/dist/providers/pi/session.d.ts.map +1 -0
- package/dist/providers/pi/session.js +328 -0
- package/dist/providers/pi/session.js.map +1 -0
- package/dist/providers/process/index.d.ts.map +1 -1
- package/dist/providers/process/index.js +3 -0
- package/dist/providers/process/index.js.map +1 -1
- package/dist/registry.d.ts +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +6 -0
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +192 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/skill-commands.d.ts.map +1 -1
- package/dist/utils/skill-commands.js +4 -2
- package/dist/utils/skill-commands.js.map +1 -1
- package/dist/utils/tool-names.d.ts +24 -0
- package/dist/utils/tool-names.d.ts.map +1 -0
- package/dist/utils/tool-names.js +50 -0
- package/dist/utils/tool-names.js.map +1 -0
- package/package.json +22 -12
- package/dist/providers/claude/test.d.ts +0 -3
- package/dist/providers/claude/test.d.ts.map +0 -1
- package/dist/providers/claude/test.js +0 -167
- package/dist/providers/claude/test.js.map +0 -1
- package/dist/providers/codex/test.d.ts +0 -3
- package/dist/providers/codex/test.d.ts.map +0 -1
- package/dist/providers/codex/test.js +0 -74
- package/dist/providers/codex/test.js.map +0 -1
- package/dist/providers/cursor/test.d.ts +0 -3
- package/dist/providers/cursor/test.d.ts.map +0 -1
- package/dist/providers/cursor/test.js +0 -58
- package/dist/providers/cursor/test.js.map +0 -1
- package/dist/providers/gemini/codec.d.ts +0 -3
- package/dist/providers/gemini/codec.d.ts.map +0 -1
- package/dist/providers/gemini/codec.js +0 -47
- package/dist/providers/gemini/codec.js.map +0 -1
- package/dist/providers/gemini/execute.d.ts +0 -3
- package/dist/providers/gemini/execute.d.ts.map +0 -1
- package/dist/providers/gemini/execute.js +0 -256
- package/dist/providers/gemini/execute.js.map +0 -1
- package/dist/providers/gemini/parse.d.ts +0 -20
- package/dist/providers/gemini/parse.d.ts.map +0 -1
- package/dist/providers/gemini/parse.js +0 -235
- package/dist/providers/gemini/parse.js.map +0 -1
- package/dist/providers/gemini/test.d.ts +0 -3
- package/dist/providers/gemini/test.d.ts.map +0 -1
- package/dist/providers/gemini/test.js +0 -67
- package/dist/providers/gemini/test.js.map +0 -1
- package/dist/providers/openclaw/test.d.ts +0 -3
- package/dist/providers/openclaw/test.d.ts.map +0 -1
- package/dist/providers/openclaw/test.js +0 -54
- package/dist/providers/openclaw/test.js.map +0 -1
- package/dist/providers/opencode/test.d.ts +0 -3
- package/dist/providers/opencode/test.d.ts.map +0 -1
- package/dist/providers/opencode/test.js +0 -60
- package/dist/providers/opencode/test.js.map +0 -1
- package/dist/providers/pi/test.d.ts +0 -3
- package/dist/providers/pi/test.d.ts.map +0 -1
- package/dist/providers/pi/test.js +0 -60
- package/dist/providers/pi/test.js.map +0 -1
- package/dist/utils/model-cache.d.ts +0 -11
- package/dist/utils/model-cache.d.ts.map +0 -1
- package/dist/utils/model-cache.js +0 -17
- package/dist/utils/model-cache.js.map +0 -1
|
@@ -1,11 +1,90 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { findBinary } from "../../utils/binary.js";
|
|
3
4
|
import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
|
|
4
5
|
import { injectWorkspaceSkills } from "../../utils/skills.js";
|
|
5
6
|
import { resolveInstructions } from "../../utils/instructions.js";
|
|
7
|
+
import { createToolNameTracker } from "../../utils/tool-names.js";
|
|
6
8
|
import { parseCodexStreamLine } from "./parse.js";
|
|
7
9
|
import { withPlanModePreamble } from "./plan-mode.js";
|
|
8
10
|
import { scanCodexSessionUsage } from "./usage-scanner.js";
|
|
11
|
+
import { codexSessionCodec } from "./codec.js";
|
|
12
|
+
import { parseCollaborationModes, resolveCollaborationModeParam } from "./modes.js";
|
|
13
|
+
/**
|
|
14
|
+
* Extract a resume thread id from session params (reusing the codec's
|
|
15
|
+
* sessionId / session_id / thread_id alias handling), or null to start fresh.
|
|
16
|
+
*/
|
|
17
|
+
function readCodexResumeId(sessionParams) {
|
|
18
|
+
const decoded = codexSessionCodec.deserialize(sessionParams ?? null);
|
|
19
|
+
const id = decoded?.["sessionId"];
|
|
20
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
21
|
+
}
|
|
22
|
+
/** Pull and normalize the question list out of a `requestUserInput` params blob.
|
|
23
|
+
* Tolerant of missing/extra fields — drops anything without an id and at least
|
|
24
|
+
* a question or a header (Codex sometimes sends header-only prompts). */
|
|
25
|
+
function parseCodexQuestions(params) {
|
|
26
|
+
const raw = Array.isArray(params["questions"]) ? params["questions"] : [];
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const item of raw) {
|
|
29
|
+
if (typeof item !== "object" || item === null)
|
|
30
|
+
continue;
|
|
31
|
+
const q = item;
|
|
32
|
+
const id = typeof q["id"] === "string" ? q["id"] : "";
|
|
33
|
+
const questionText = typeof q["question"] === "string" ? q["question"] : "";
|
|
34
|
+
const header = typeof q["header"] === "string" ? q["header"] : "";
|
|
35
|
+
if (!id || (!questionText && !header))
|
|
36
|
+
continue;
|
|
37
|
+
// Fall back to the header as the prompt text so the bridged AskUserQuestion is
|
|
38
|
+
// never empty and the host can key its answer off the same `question` value.
|
|
39
|
+
const question = questionText || header;
|
|
40
|
+
const options = Array.isArray(q["options"])
|
|
41
|
+
? q["options"]
|
|
42
|
+
.filter((o) => typeof o === "object" && o !== null)
|
|
43
|
+
.map((o) => ({
|
|
44
|
+
label: typeof o["label"] === "string" ? o["label"] : "",
|
|
45
|
+
...(typeof o["description"] === "string" && o["description"]
|
|
46
|
+
? { description: o["description"] }
|
|
47
|
+
: {}),
|
|
48
|
+
}))
|
|
49
|
+
.filter((o) => o.label.length > 0)
|
|
50
|
+
: [];
|
|
51
|
+
out.push({
|
|
52
|
+
id,
|
|
53
|
+
header,
|
|
54
|
+
question,
|
|
55
|
+
options,
|
|
56
|
+
...(q["multiSelect"] === true ? { multiSelect: true } : {}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function normalizeAnswerValues(raw) {
|
|
62
|
+
if (typeof raw === "string")
|
|
63
|
+
return raw.length > 0 ? [raw] : [];
|
|
64
|
+
if (Array.isArray(raw))
|
|
65
|
+
return raw.filter((v) => typeof v === "string" && v.length > 0);
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
/** Translate a host AskUserQuestion answer (keyed by question text or header)
|
|
69
|
+
* into the Codex `requestUserInput` response shape:
|
|
70
|
+
* `{ [questionId]: { answers: string[] } }`. A denied response yields {}. */
|
|
71
|
+
function buildCodexUserInputAnswers(questions, resp) {
|
|
72
|
+
const out = {};
|
|
73
|
+
if (!resp.allow)
|
|
74
|
+
return out;
|
|
75
|
+
const updated = resp.updatedInput && typeof resp.updatedInput["answers"] === "object"
|
|
76
|
+
? resp.updatedInput["answers"]
|
|
77
|
+
: null;
|
|
78
|
+
if (!updated)
|
|
79
|
+
return out;
|
|
80
|
+
for (const q of questions) {
|
|
81
|
+
const raw = updated[q.question] ?? updated[q.header];
|
|
82
|
+
const values = normalizeAnswerValues(raw);
|
|
83
|
+
if (values.length > 0)
|
|
84
|
+
out[q.id] = { answers: values };
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
9
88
|
// ---------------------------------------------------------------------------
|
|
10
89
|
// JSON-RPC 2.0 helpers
|
|
11
90
|
// ---------------------------------------------------------------------------
|
|
@@ -34,15 +113,23 @@ function asObj(parent, key) {
|
|
|
34
113
|
: {};
|
|
35
114
|
}
|
|
36
115
|
function classifyMessage(msg) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
116
|
+
// Detect JSON-RPC by *structure*, not by the `jsonrpc:"2.0"` discriminator.
|
|
117
|
+
// codex-cli 0.130.0's `app-server` emits responses without the `jsonrpc`
|
|
118
|
+
// field (technically non-compliant with the spec, but it's what ships).
|
|
119
|
+
// Heuristic: a message is JSON-RPC if it has any of (jsonrpc, id, method).
|
|
120
|
+
const hasJsonRpc = msg["jsonrpc"] === "2.0";
|
|
121
|
+
const hasId = "id" in msg && (typeof msg["id"] === "number" || typeof msg["id"] === "string");
|
|
122
|
+
const hasMethod = "method" in msg && typeof msg["method"] === "string";
|
|
123
|
+
const hasResult = "result" in msg;
|
|
124
|
+
const hasError = "error" in msg;
|
|
125
|
+
if (hasJsonRpc || hasId || hasMethod) {
|
|
126
|
+
const id = hasId
|
|
127
|
+
? (typeof msg["id"] === "number" ? msg["id"] : parseInt(String(msg["id"]), 10))
|
|
128
|
+
: null;
|
|
42
129
|
if (hasId && hasMethod) {
|
|
43
|
-
return { kind: "request", id, method: msg["method"], params: asObj(msg, "params") };
|
|
130
|
+
return { kind: "request", id: id, method: msg["method"], params: asObj(msg, "params") };
|
|
44
131
|
}
|
|
45
|
-
if (hasId &&
|
|
132
|
+
if (hasId && (hasResult || hasError)) {
|
|
46
133
|
const errRaw = msg["error"];
|
|
47
134
|
const error = typeof errRaw === "object" && errRaw !== null
|
|
48
135
|
? { code: num(errRaw, "code"), message: str(errRaw, "message") }
|
|
@@ -50,12 +137,11 @@ function classifyMessage(msg) {
|
|
|
50
137
|
const result = typeof msg["result"] === "object" && msg["result"] !== null
|
|
51
138
|
? msg["result"]
|
|
52
139
|
: undefined;
|
|
53
|
-
return { kind: "response", id, result, error };
|
|
140
|
+
return { kind: "response", id: id, result, error };
|
|
54
141
|
}
|
|
55
142
|
if (hasMethod) {
|
|
56
143
|
return { kind: "notification", method: msg["method"], params: asObj(msg, "params") };
|
|
57
144
|
}
|
|
58
|
-
return null;
|
|
59
145
|
}
|
|
60
146
|
// Legacy NDJSON events (from `codex exec --json` format) — have a `type` field
|
|
61
147
|
if (typeof msg["type"] === "string") {
|
|
@@ -88,15 +174,20 @@ export async function createCodexSession(ctx) {
|
|
|
88
174
|
const instructions = config.planMode
|
|
89
175
|
? withPlanModePreamble(baseInstructions)
|
|
90
176
|
: baseInstructions;
|
|
91
|
-
// Spawn Codex in interactive JSON-RPC mode
|
|
92
|
-
//
|
|
93
|
-
|
|
177
|
+
// Spawn Codex in interactive JSON-RPC mode via the `app-server` subcommand
|
|
178
|
+
// (codex-cli 0.130.0+; the old top-level `--json` flag was removed).
|
|
179
|
+
//
|
|
180
|
+
// Args order matters: `--sandbox` and `--dangerously-bypass-approvals-and-sandbox`
|
|
181
|
+
// are TOP-LEVEL options and must come BEFORE the `app-server` subcommand.
|
|
182
|
+
// extraArgs land after the subcommand — semantics depend on the user's intent.
|
|
183
|
+
const args = [...resolved.prefixArgs];
|
|
94
184
|
if (config.planMode) {
|
|
95
185
|
args.push("--sandbox", "read-only");
|
|
96
186
|
}
|
|
97
187
|
else if (config.skipPermissions) {
|
|
98
188
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
99
189
|
}
|
|
190
|
+
args.push("app-server");
|
|
100
191
|
if (config.extraArgs)
|
|
101
192
|
args.push(...config.extraArgs);
|
|
102
193
|
const proc = spawn(resolved.bin, args, {
|
|
@@ -133,13 +224,29 @@ export class CodexSessionImpl {
|
|
|
133
224
|
instructions;
|
|
134
225
|
_state = "idle";
|
|
135
226
|
_threadId = null;
|
|
227
|
+
/** Thread id to resume (from ctx.sessionParams); null starts a fresh thread. */
|
|
228
|
+
_resumeThreadId;
|
|
136
229
|
_lineBuffer = "";
|
|
137
230
|
_nextId = 1;
|
|
138
231
|
// Pending outgoing RPC responses (keyed by request id)
|
|
139
232
|
_pendingRpc = new Map();
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
233
|
+
// Pending result-resolvers. With concurrent send, multiple in-flight send()
|
|
234
|
+
// Promises may share a single result event (when the CLI coalesces them
|
|
235
|
+
// into one turn) or get distinct results across turns. On each
|
|
236
|
+
// turn.completed / turn.failed we drain the entire list — every pending
|
|
237
|
+
// Promise resolves with the same TurnResult.
|
|
238
|
+
_pendingResults = [];
|
|
239
|
+
/** Result Promises for sends that haven't settled, tracked so `drain()` can
|
|
240
|
+
* await the in-flight turn(s) before closing. */
|
|
241
|
+
_inFlight = new Set();
|
|
242
|
+
/** Set by `drain()`: new `send()` calls are refused while true. */
|
|
243
|
+
_draining = false;
|
|
244
|
+
/** Shared promise so concurrent / repeated `drain()` calls coalesce. */
|
|
245
|
+
_drainPromise = null;
|
|
246
|
+
/** Stamps `tool_result.toolName` by correlating with prior `tool_call`s. */
|
|
247
|
+
_trackToolName = createToolNameTracker();
|
|
248
|
+
// Per-turn accumulators. Cleared after each result delivery so a subsequent
|
|
249
|
+
// turn's events don't inherit stale values.
|
|
143
250
|
_turnSummary = null;
|
|
144
251
|
_turnUsage = null;
|
|
145
252
|
_turnModel = null;
|
|
@@ -159,6 +266,7 @@ export class CodexSessionImpl {
|
|
|
159
266
|
this.cwd = cwd;
|
|
160
267
|
this.model = model;
|
|
161
268
|
this.instructions = instructions;
|
|
269
|
+
this._resumeThreadId = readCodexResumeId(ctx.sessionParams);
|
|
162
270
|
proc.stdout.setEncoding("utf-8");
|
|
163
271
|
proc.stdout.on("data", (chunk) => this.handleStdout(chunk));
|
|
164
272
|
proc.stderr.setEncoding("utf-8");
|
|
@@ -174,30 +282,30 @@ export class CodexSessionImpl {
|
|
|
174
282
|
if (this._state !== "closed") {
|
|
175
283
|
this._state = "closed";
|
|
176
284
|
const err = new Error(`Codex process exited unexpectedly (code=${code}, signal=${signal})`);
|
|
177
|
-
|
|
178
|
-
pending.reject(err);
|
|
179
|
-
this._pendingRpc.clear();
|
|
180
|
-
if (this._turnReject) {
|
|
181
|
-
this._turnReject(err);
|
|
182
|
-
this._turnResolve = null;
|
|
183
|
-
this._turnReject = null;
|
|
184
|
-
}
|
|
285
|
+
this.rejectAllPending(err);
|
|
185
286
|
}
|
|
186
287
|
});
|
|
187
288
|
proc.on("error", (err) => {
|
|
188
289
|
if (this._state !== "closed") {
|
|
189
290
|
this._state = "closed";
|
|
190
|
-
|
|
191
|
-
pending.reject(err);
|
|
192
|
-
this._pendingRpc.clear();
|
|
193
|
-
if (this._turnReject) {
|
|
194
|
-
this._turnReject(err);
|
|
195
|
-
this._turnResolve = null;
|
|
196
|
-
this._turnReject = null;
|
|
197
|
-
}
|
|
291
|
+
this.rejectAllPending(err);
|
|
198
292
|
}
|
|
199
293
|
});
|
|
200
294
|
}
|
|
295
|
+
/** Reject every pending send() Promise and outgoing JSON-RPC call. */
|
|
296
|
+
rejectAllPending(err) {
|
|
297
|
+
const pending = this._pendingResults.splice(0);
|
|
298
|
+
for (const p of pending) {
|
|
299
|
+
if (p.settled)
|
|
300
|
+
continue;
|
|
301
|
+
p.settled = true;
|
|
302
|
+
p.cleanup?.();
|
|
303
|
+
p.reject(err);
|
|
304
|
+
}
|
|
305
|
+
for (const [, p] of this._pendingRpc)
|
|
306
|
+
p.reject(err);
|
|
307
|
+
this._pendingRpc.clear();
|
|
308
|
+
}
|
|
201
309
|
get sessionId() { return this._threadId; }
|
|
202
310
|
get state() { return this._state; }
|
|
203
311
|
// -------------------------------------------------------------------------
|
|
@@ -225,42 +333,184 @@ export class CodexSessionImpl {
|
|
|
225
333
|
clientInfo: { name: "agentex", version: "1.0.0" },
|
|
226
334
|
capabilities: {},
|
|
227
335
|
});
|
|
228
|
-
// 2. thread
|
|
336
|
+
// 2. Resume an existing thread when the caller supplied sessionParams,
|
|
337
|
+
// otherwise start a fresh one. `thread/resume` continues the SAME thread
|
|
338
|
+
// with its full context retained — distinct from `thread/fork`, which is
|
|
339
|
+
// a divergent rewind copy. The thread keeps its original cwd/model, so we
|
|
340
|
+
// pass only the thread id (+ refreshed developer instructions).
|
|
341
|
+
if (this._resumeThreadId) {
|
|
342
|
+
const resumeParams = { threadId: this._resumeThreadId };
|
|
343
|
+
if (this.instructions)
|
|
344
|
+
resumeParams["developerInstructions"] = this.instructions;
|
|
345
|
+
try {
|
|
346
|
+
const res = await this.rpcRequest("thread/resume", resumeParams);
|
|
347
|
+
const thread = asObj(res, "thread");
|
|
348
|
+
// thread/resume may echo the thread back or return {}; fall back to the
|
|
349
|
+
// id we resumed with so `sessionId` is always populated.
|
|
350
|
+
this._threadId = str(thread, "id") || str(thread, "sessionId") || this._resumeThreadId;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
// The thread is unknown to this codex install (different machine, pruned
|
|
355
|
+
// history). Don't fail the whole session — fall back to a fresh thread
|
|
356
|
+
// and surface the downgrade on stderr. The new id flows back out via
|
|
357
|
+
// the next sessionParams snapshot so callers see the session changed.
|
|
358
|
+
if (this.ctx.onOutput) {
|
|
359
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
360
|
+
try {
|
|
361
|
+
void this.ctx.onOutput("stderr", `agentex: codex thread/resume failed for ${this._resumeThreadId}, starting a fresh thread: ${reason}\n`);
|
|
362
|
+
}
|
|
363
|
+
catch { /* swallow */ }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// thread/start (fresh)
|
|
229
368
|
const threadParams = { cwd: this.cwd };
|
|
230
369
|
if (this.model)
|
|
231
370
|
threadParams["model"] = this.model;
|
|
232
371
|
if (this.instructions)
|
|
233
372
|
threadParams["developerInstructions"] = this.instructions;
|
|
373
|
+
// Apply a chosen collaboration mode (config.modeId) by resolving it against
|
|
374
|
+
// the live mode list. Only on fresh threads — a resumed thread keeps the
|
|
375
|
+
// mode it was created with. Mode discovery is advisory: a failure or an
|
|
376
|
+
// unknown id just falls through to the default mode.
|
|
377
|
+
const modeId = this.ctx.config?.modeId;
|
|
378
|
+
if (modeId) {
|
|
379
|
+
try {
|
|
380
|
+
// Bound the discovery RPC: an older app-server that ignores
|
|
381
|
+
// `collaborationMode/list` would otherwise hang the whole handshake.
|
|
382
|
+
const modesResponse = await Promise.race([
|
|
383
|
+
this.rpcRequest("collaborationMode/list", {}),
|
|
384
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("collaborationMode/list timed out")), 10_000)),
|
|
385
|
+
]);
|
|
386
|
+
const collaborationMode = resolveCollaborationModeParam(parseCollaborationModes(modesResponse), modeId);
|
|
387
|
+
if (collaborationMode) {
|
|
388
|
+
// Avoid sending instructions twice: the caller's top-level
|
|
389
|
+
// `developerInstructions` wins, so drop the mode's copy.
|
|
390
|
+
if (this.instructions)
|
|
391
|
+
delete collaborationMode.settings["developer_instructions"];
|
|
392
|
+
threadParams["collaborationMode"] = collaborationMode;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch { /* modes are advisory — ignore discovery failures */ }
|
|
396
|
+
}
|
|
234
397
|
const res = await this.rpcRequest("thread/start", threadParams);
|
|
235
|
-
|
|
398
|
+
// codex-cli 0.130.0+ shape: { thread: { id, sessionId, ... }, model, ... }
|
|
399
|
+
const thread = asObj(res, "thread");
|
|
400
|
+
this._threadId = str(thread, "id") || str(thread, "sessionId") || null;
|
|
236
401
|
}
|
|
237
402
|
// -------------------------------------------------------------------------
|
|
238
403
|
// Public API
|
|
239
404
|
// -------------------------------------------------------------------------
|
|
240
|
-
async send(message) {
|
|
405
|
+
async send(message, options) {
|
|
241
406
|
if (this._state === "closed")
|
|
242
407
|
throw new Error("Session is closed");
|
|
243
|
-
if (this.
|
|
244
|
-
throw new Error("
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
408
|
+
if (this._draining)
|
|
409
|
+
throw new Error("Session is draining — no new sends accepted");
|
|
410
|
+
// No protocol-level guard — Codex's TUI demonstrates queueing of user
|
|
411
|
+
// messages during an active turn, and our wire test (transcript
|
|
412
|
+
// 019e33c3) confirms two `user_message` events recorded across a
|
|
413
|
+
// long-running turn. We bet on the JSON-RPC layer queueing similarly
|
|
414
|
+
// and pass through. If the second `turn/start` lands during the first
|
|
415
|
+
// turn, the per-turn accumulators continue collecting until the result
|
|
416
|
+
// event fires; the result then drains all pending resolvers.
|
|
417
|
+
if (this._state === "idle") {
|
|
418
|
+
this._state = "thinking";
|
|
419
|
+
this._turnStartedAt = new Date();
|
|
420
|
+
}
|
|
421
|
+
// UUID is for API parity with Claude — Codex's JSON-RPC doesn't carry it
|
|
422
|
+
// through the wire protocol, so cancel(uuid) is a no-op for Codex.
|
|
423
|
+
const uuid = randomUUID();
|
|
424
|
+
// Start a turn — the completion comes via notifications, not the RPC response.
|
|
425
|
+
// codex-cli 0.130.0+ expects `input` as a content-block array, not a plain
|
|
426
|
+
// string. The MCP-style shape: [{type:"text", text:"..."}].
|
|
427
|
+
const turnParams = {
|
|
428
|
+
input: [{ type: "text", text: message }],
|
|
429
|
+
};
|
|
254
430
|
if (this._threadId)
|
|
255
431
|
turnParams["threadId"] = this._threadId;
|
|
432
|
+
let resolveFn;
|
|
433
|
+
let rejectFn;
|
|
434
|
+
const result = new Promise((resolve, reject) => {
|
|
435
|
+
resolveFn = resolve;
|
|
436
|
+
rejectFn = reject;
|
|
437
|
+
});
|
|
438
|
+
const entry = { resolve: resolveFn, reject: rejectFn };
|
|
439
|
+
this._pendingResults.push(entry);
|
|
440
|
+
// Track the in-flight turn so drain() can await it; drop it on settle.
|
|
441
|
+
this._inFlight.add(result);
|
|
442
|
+
void result.catch(() => { }).finally(() => this._inFlight.delete(result));
|
|
443
|
+
// Per-send timeout / abort, falling back to the session-level
|
|
444
|
+
// ProviderConfig.timeoutSec default when no per-call timeout is given.
|
|
445
|
+
this.armSendDeadline(entry, options);
|
|
256
446
|
this.rpcRequest("turn/start", turnParams).catch(() => {
|
|
257
|
-
// Turn-level errors arrive via turn.failed notifications
|
|
447
|
+
// Turn-level errors arrive via turn.failed notifications.
|
|
258
448
|
});
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
|
|
449
|
+
return { uuid, result };
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Wire up this send's timeout and/or abort signal. On fire, the active turn
|
|
453
|
+
* is cancelled (`turn/cancel`) and the send settles with `timeout` /
|
|
454
|
+
* `aborted`. No-op when neither a timeout nor a signal applies.
|
|
455
|
+
*/
|
|
456
|
+
armSendDeadline(entry, options) {
|
|
457
|
+
const timeoutSec = options?.timeoutSec ?? this.ctx.config?.timeoutSec;
|
|
458
|
+
const signal = options?.signal;
|
|
459
|
+
const hasTimeout = typeof timeoutSec === "number" && timeoutSec > 0;
|
|
460
|
+
if (!hasTimeout && !signal)
|
|
461
|
+
return;
|
|
462
|
+
if (signal?.aborted) {
|
|
463
|
+
queueMicrotask(() => this.settleEarly(entry, "aborted"));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
let timer;
|
|
467
|
+
const onAbort = () => this.settleEarly(entry, "aborted");
|
|
468
|
+
if (hasTimeout) {
|
|
469
|
+
timer = setTimeout(() => this.settleEarly(entry, "timeout"), timeoutSec * 1000);
|
|
470
|
+
}
|
|
471
|
+
if (signal)
|
|
472
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
473
|
+
entry.cleanup = () => {
|
|
474
|
+
if (timer)
|
|
475
|
+
clearTimeout(timer);
|
|
476
|
+
if (signal)
|
|
477
|
+
signal.removeEventListener("abort", onAbort);
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Settle a still-pending send early (timeout or abort). Cancels the active
|
|
482
|
+
* turn best-effort and resolves the send's `result` with a synthetic
|
|
483
|
+
* TurnResult. A no-op if the entry already settled (the real result raced
|
|
484
|
+
* ahead). The late real turn-completion later finds the entry already gone.
|
|
485
|
+
*/
|
|
486
|
+
settleEarly(entry, kind) {
|
|
487
|
+
if (entry.settled)
|
|
488
|
+
return;
|
|
489
|
+
entry.settled = true;
|
|
490
|
+
entry.cleanup?.();
|
|
491
|
+
const idx = this._pendingResults.indexOf(entry);
|
|
492
|
+
if (idx >= 0)
|
|
493
|
+
this._pendingResults.splice(idx, 1);
|
|
494
|
+
// Best-effort cancel of the active turn. With concurrent sends this ends
|
|
495
|
+
// the single shared turn for all of them — see SendOptions JSDoc.
|
|
496
|
+
void this.interrupt();
|
|
497
|
+
entry.resolve({
|
|
498
|
+
summary: null,
|
|
499
|
+
usage: undefined,
|
|
500
|
+
costUsd: null,
|
|
501
|
+
status: kind,
|
|
502
|
+
errorCode: kind,
|
|
503
|
+
errorMessage: kind === "timeout"
|
|
504
|
+
? "Turn exceeded its timeout and was interrupted"
|
|
505
|
+
: "Turn was aborted",
|
|
262
506
|
});
|
|
263
507
|
}
|
|
508
|
+
async cancel(_uuid) {
|
|
509
|
+
// Codex's JSON-RPC protocol exposes no per-message cancel — only
|
|
510
|
+
// turn-wide `turn/cancel` (which is what `interrupt()` calls).
|
|
511
|
+
// capabilities.cancelQueuedMessage is false; this is a documented no-op.
|
|
512
|
+
return { cancelled: false };
|
|
513
|
+
}
|
|
264
514
|
async interrupt() {
|
|
265
515
|
if (this._state === "idle" || this._state === "closed")
|
|
266
516
|
return;
|
|
@@ -269,16 +519,34 @@ export class CodexSessionImpl {
|
|
|
269
519
|
}
|
|
270
520
|
catch { /* best effort */ }
|
|
271
521
|
}
|
|
522
|
+
async drain() {
|
|
523
|
+
if (this._state === "closed")
|
|
524
|
+
return;
|
|
525
|
+
// Coalesce concurrent / repeated drains onto one promise.
|
|
526
|
+
if (this._drainPromise)
|
|
527
|
+
return this._drainPromise;
|
|
528
|
+
this._draining = true;
|
|
529
|
+
this._drainPromise = (async () => {
|
|
530
|
+
// Let every in-flight turn settle (resolve or reject) before closing, so
|
|
531
|
+
// a running tool finishes rather than being killed mid-flight.
|
|
532
|
+
await Promise.allSettled([...this._inFlight]);
|
|
533
|
+
await this.close();
|
|
534
|
+
})();
|
|
535
|
+
return this._drainPromise;
|
|
536
|
+
}
|
|
272
537
|
async close() {
|
|
273
538
|
if (this._state === "closed")
|
|
274
539
|
return;
|
|
275
540
|
this._state = "closed";
|
|
276
541
|
this.proc.stdin.end();
|
|
542
|
+
// Grace window before SIGKILL is configurable via ProviderConfig.graceSec
|
|
543
|
+
// for sessions running long tools.
|
|
544
|
+
const graceSec = this.ctx.config?.graceSec ?? 5;
|
|
277
545
|
await new Promise((resolve) => {
|
|
278
546
|
const timeout = setTimeout(() => {
|
|
279
547
|
this.proc.kill("SIGKILL");
|
|
280
548
|
resolve();
|
|
281
|
-
},
|
|
549
|
+
}, graceSec * 1000);
|
|
282
550
|
this.proc.on("exit", () => {
|
|
283
551
|
clearTimeout(timeout);
|
|
284
552
|
resolve();
|
|
@@ -351,17 +619,35 @@ export class CodexSessionImpl {
|
|
|
351
619
|
method === "item/fileChange/requestApproval") {
|
|
352
620
|
void this.handleApproval(id, method, params);
|
|
353
621
|
}
|
|
622
|
+
else if (method === "item/tool/requestUserInput" ||
|
|
623
|
+
method === "tool/requestUserInput") {
|
|
624
|
+
// `tool/requestUserInput` is the legacy method name on older codex builds.
|
|
625
|
+
void this.handleUserInputRequest(id, params);
|
|
626
|
+
}
|
|
354
627
|
else {
|
|
355
|
-
// Unknown server request — ack to unblock
|
|
628
|
+
// Unknown server request — ack to unblock the turn.
|
|
356
629
|
this.rpcResponse(id, {});
|
|
357
630
|
}
|
|
358
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Leave a waiting-for-input/approval state correctly. A slow host handler can
|
|
634
|
+
* resolve after the turn already ended (deliverTurnResult → idle), so restore
|
|
635
|
+
* to `thinking` only when a turn is still in flight, else `idle` — never clobber
|
|
636
|
+
* a finished turn back to `thinking`.
|
|
637
|
+
*/
|
|
638
|
+
restoreStateAfter(waitingState) {
|
|
639
|
+
if (this._state !== waitingState)
|
|
640
|
+
return;
|
|
641
|
+
this._state = this._pendingResults.length > 0 ? "thinking" : "idle";
|
|
642
|
+
}
|
|
359
643
|
async handleApproval(id, method, params) {
|
|
360
644
|
this._state = "waiting_for_approval";
|
|
645
|
+
// Codex's app-server expects `{ decision: "accept" | "decline" | "cancel" }`
|
|
646
|
+
// (NOT `{ approved: boolean }`). agentex's UserInputResponse has no interrupt
|
|
647
|
+
// concept, so allow → accept and deny → decline.
|
|
361
648
|
if (!this.ctx.onUserInputRequest) {
|
|
362
|
-
this.rpcResponse(id, {
|
|
363
|
-
|
|
364
|
-
this._state = "thinking";
|
|
649
|
+
this.rpcResponse(id, { decision: "accept" });
|
|
650
|
+
this.restoreStateAfter("waiting_for_approval");
|
|
365
651
|
return;
|
|
366
652
|
}
|
|
367
653
|
const toolName = method === "item/commandExecution/requestApproval"
|
|
@@ -374,13 +660,43 @@ export class CodexSessionImpl {
|
|
|
374
660
|
toolUseId: str(params, "id"),
|
|
375
661
|
description: str(params, "command") || str(params, "path") || undefined,
|
|
376
662
|
});
|
|
377
|
-
this.rpcResponse(id, {
|
|
663
|
+
this.rpcResponse(id, { decision: resp.allow ? "accept" : "decline" });
|
|
378
664
|
}
|
|
379
665
|
catch {
|
|
380
|
-
this.rpcResponse(id, {
|
|
666
|
+
this.rpcResponse(id, { decision: "decline" });
|
|
381
667
|
}
|
|
382
|
-
|
|
383
|
-
|
|
668
|
+
this.restoreStateAfter("waiting_for_approval");
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Handle a Codex `requestUserInput` server→client request: the agent is asking
|
|
672
|
+
* the user one or more structured questions. Maps onto the cross-provider
|
|
673
|
+
* AskUserQuestion shape so callers reuse `parseAskUserQuestion`, then answers
|
|
674
|
+
* back in Codex's `{ answers: { [questionId]: { answers: string[] } } }` shape.
|
|
675
|
+
*/
|
|
676
|
+
async handleUserInputRequest(id, params) {
|
|
677
|
+
// Questions are user *input*, not a tool-permission gate — distinct state so a
|
|
678
|
+
// host UI can render a question form vs an approval prompt.
|
|
679
|
+
this._state = "waiting_for_input";
|
|
680
|
+
const questions = parseCodexQuestions(params);
|
|
681
|
+
// No host handler, or nothing answerable → return empty answers so the
|
|
682
|
+
// agent proceeds without hanging.
|
|
683
|
+
if (!this.ctx.onUserInputRequest || questions.length === 0) {
|
|
684
|
+
this.rpcResponse(id, { answers: {} });
|
|
685
|
+
this.restoreStateAfter("waiting_for_input");
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
const resp = await this.ctx.onUserInputRequest({
|
|
690
|
+
toolName: "AskUserQuestion",
|
|
691
|
+
input: { questions },
|
|
692
|
+
toolUseId: str(params, "id") || "codex-user-input",
|
|
693
|
+
});
|
|
694
|
+
this.rpcResponse(id, { answers: buildCodexUserInputAnswers(questions, resp) });
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
this.rpcResponse(id, { answers: {} });
|
|
698
|
+
}
|
|
699
|
+
this.restoreStateAfter("waiting_for_input");
|
|
384
700
|
}
|
|
385
701
|
// -------------------------------------------------------------------------
|
|
386
702
|
// Notification handling (v2 format)
|
|
@@ -399,7 +715,9 @@ export class CodexSessionImpl {
|
|
|
399
715
|
}
|
|
400
716
|
// Map v2 notification methods to processing
|
|
401
717
|
if (method === "thread/started") {
|
|
402
|
-
|
|
718
|
+
// codex-cli 0.130.0+ shape: { thread: { id, sessionId, ... } }
|
|
719
|
+
const thread = asObj(params, "thread");
|
|
720
|
+
this._threadId = str(thread, "id") || str(thread, "sessionId") || this._threadId;
|
|
403
721
|
this.emitStreamEvent(rawLine);
|
|
404
722
|
return;
|
|
405
723
|
}
|
|
@@ -540,7 +858,15 @@ export class CodexSessionImpl {
|
|
|
540
858
|
let usage = this._turnUsage && resolvedModel
|
|
541
859
|
? { [resolvedModel]: { inputTokens: this._turnUsage.inputTokens, outputTokens: this._turnUsage.outputTokens } }
|
|
542
860
|
: undefined;
|
|
543
|
-
//
|
|
861
|
+
// Usage precedence: the `turn.completed` payload is authoritative when
|
|
862
|
+
// present (captured above into _turnUsage). Only when the stream carried no
|
|
863
|
+
// usage do we fall back to scanning Codex's on-disk session logs.
|
|
864
|
+
//
|
|
865
|
+
// RACINESS: the disk scan is best-effort and inherently racy — the rollout
|
|
866
|
+
// file may still be flushing when we read it, so a fallback scan can miss the
|
|
867
|
+
// latest turn or read partially-written totals. We therefore scan ONLY when
|
|
868
|
+
// there is no in-band usage, and never let a scan failure fail the turn
|
|
869
|
+
// (usage simply stays undefined). Prefer the in-band payload always.
|
|
544
870
|
if (!usage && this._turnStartedAt) {
|
|
545
871
|
const startedAt = this._turnStartedAt;
|
|
546
872
|
const threadId = this._threadId ?? undefined;
|
|
@@ -578,11 +904,24 @@ export class CodexSessionImpl {
|
|
|
578
904
|
if (this._state === "closed")
|
|
579
905
|
return;
|
|
580
906
|
this._state = "idle";
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
907
|
+
// Drain ALL pending send() resolvers with this turn's result. Multiple
|
|
908
|
+
// concurrent sends coalesced into one turn share the same TurnResult.
|
|
909
|
+
const pending = this._pendingResults.splice(0);
|
|
910
|
+
// Clear per-turn accumulators so a subsequent turn doesn't inherit
|
|
911
|
+
// stale summary / usage / model.
|
|
912
|
+
this._turnSummary = null;
|
|
913
|
+
this._turnUsage = null;
|
|
914
|
+
this._turnModel = null;
|
|
915
|
+
this._turnIsError = false;
|
|
916
|
+
this._turnErrorMessage = null;
|
|
917
|
+
this._turnStartedAt = null;
|
|
918
|
+
for (const p of pending) {
|
|
919
|
+
// Skip sends already settled early by timeout / abort.
|
|
920
|
+
if (p.settled)
|
|
921
|
+
continue;
|
|
922
|
+
p.settled = true;
|
|
923
|
+
p.cleanup?.();
|
|
924
|
+
p.resolve(result);
|
|
586
925
|
}
|
|
587
926
|
}
|
|
588
927
|
emitStreamEvent(rawLine) {
|
|
@@ -605,9 +944,12 @@ export class CodexSessionImpl {
|
|
|
605
944
|
const cb = this.ctx.onEvent;
|
|
606
945
|
if (!cb)
|
|
607
946
|
return;
|
|
947
|
+
// Enrich synchronously (in stream order) so tool_result events carry the
|
|
948
|
+
// name of the tool_call they answer.
|
|
949
|
+
const enriched = this._trackToolName(event);
|
|
608
950
|
this._eventChain = this._eventChain.then(async () => {
|
|
609
951
|
try {
|
|
610
|
-
await cb(
|
|
952
|
+
await cb(enriched);
|
|
611
953
|
}
|
|
612
954
|
catch { /* swallow */ }
|
|
613
955
|
});
|