@agentex/agent 0.0.39 → 0.0.42
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 +50 -0
- package/README.md +26 -4
- package/dist/history/fs.d.ts.map +1 -1
- package/dist/history/fs.js +6 -12
- package/dist/history/fs.js.map +1 -1
- package/dist/providers/antigravity/execute.d.ts.map +1 -1
- package/dist/providers/antigravity/execute.js +9 -3
- package/dist/providers/antigravity/execute.js.map +1 -1
- package/dist/providers/antigravity/runtime.d.ts +4 -2
- package/dist/providers/antigravity/runtime.d.ts.map +1 -1
- package/dist/providers/antigravity/runtime.js +7 -4
- package/dist/providers/antigravity/runtime.js.map +1 -1
- package/dist/providers/antigravity/session.d.ts.map +1 -1
- package/dist/providers/antigravity/session.js +59 -17
- package/dist/providers/antigravity/session.js.map +1 -1
- package/dist/providers/claude/attach.d.ts.map +1 -1
- package/dist/providers/claude/attach.js +4 -6
- package/dist/providers/claude/attach.js.map +1 -1
- package/dist/providers/claude/transcript.d.ts.map +1 -1
- package/dist/providers/claude/transcript.js +9 -20
- package/dist/providers/claude/transcript.js.map +1 -1
- package/dist/providers/codex/attach.d.ts.map +1 -1
- package/dist/providers/codex/attach.js +4 -0
- package/dist/providers/codex/attach.js.map +1 -1
- package/dist/providers/codex/history.d.ts.map +1 -1
- package/dist/providers/codex/history.js +11 -0
- package/dist/providers/codex/history.js.map +1 -1
- package/dist/providers/codex/parse.d.ts +7 -0
- package/dist/providers/codex/parse.d.ts.map +1 -1
- package/dist/providers/codex/parse.js +56 -0
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/permissions.d.ts +40 -0
- package/dist/providers/codex/permissions.d.ts.map +1 -0
- package/dist/providers/codex/permissions.js +50 -0
- package/dist/providers/codex/permissions.js.map +1 -0
- package/dist/providers/codex/session.d.ts +34 -0
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +213 -20
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/codex/transcript-normalize.d.ts +12 -3
- package/dist/providers/codex/transcript-normalize.d.ts.map +1 -1
- package/dist/providers/codex/transcript-normalize.js +87 -10
- package/dist/providers/codex/transcript-normalize.js.map +1 -1
- package/dist/providers/codex/transcript.d.ts.map +1 -1
- package/dist/providers/codex/transcript.js +5 -10
- package/dist/providers/codex/transcript.js.map +1 -1
- package/dist/providers/codex/usage-scanner.d.ts.map +1 -1
- package/dist/providers/codex/usage-scanner.js +4 -6
- package/dist/providers/codex/usage-scanner.js.map +1 -1
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +12 -10
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/jsonl-lines.d.ts +35 -0
- package/dist/utils/jsonl-lines.d.ts.map +1 -0
- package/dist/utils/jsonl-lines.js +61 -0
- package/dist/utils/jsonl-lines.js.map +1 -0
- package/package.json +1 -1
- package/src/history/fs.ts +6 -12
- package/src/providers/antigravity/execute.ts +10 -3
- package/src/providers/antigravity/runtime.ts +7 -4
- package/src/providers/antigravity/session.ts +59 -18
- package/src/providers/claude/attach.ts +4 -6
- package/src/providers/claude/transcript.ts +9 -22
- package/src/providers/codex/attach.ts +4 -0
- package/src/providers/codex/history.ts +11 -0
- package/src/providers/codex/parse.ts +59 -0
- package/src/providers/codex/permissions.ts +61 -0
- package/src/providers/codex/session.ts +238 -19
- package/src/providers/codex/transcript-normalize.ts +85 -10
- package/src/providers/codex/transcript.ts +5 -11
- package/src/providers/codex/usage-scanner.ts +4 -7
- package/src/utils/auth.ts +12 -11
- package/src/utils/jsonl-lines.ts +80 -0
|
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import type {
|
|
5
5
|
BackgroundTaskReport,
|
|
6
6
|
AgentSession,
|
|
7
|
+
ElicitationResponse,
|
|
7
8
|
CancelResult,
|
|
8
9
|
ClearGoalResult,
|
|
9
10
|
GoalOptions,
|
|
@@ -28,11 +29,16 @@ import { translateEndpoint } from "../../utils/endpoint.js";
|
|
|
28
29
|
import { injectWorkspaceSkills } from "../../utils/skills.js";
|
|
29
30
|
import { resolveInstructions } from "../../utils/instructions.js";
|
|
30
31
|
import { createToolNameTracker } from "../../utils/tool-names.js";
|
|
31
|
-
import { parseCodexStreamLines, codexBackgroundTaskReport } from "./parse.js";
|
|
32
|
+
import { parseCodexStreamLines, codexBackgroundTaskReport, codexMcpToolName } from "./parse.js";
|
|
32
33
|
import { withPlanModePreamble } from "./plan-mode.js";
|
|
33
34
|
import { scanCodexSessionUsage } from "./usage-scanner.js";
|
|
34
35
|
import { codexSessionCodec } from "./codec.js";
|
|
35
36
|
import { parseCollaborationModes, resolveCollaborationModeParam } from "./modes.js";
|
|
37
|
+
import {
|
|
38
|
+
codexPermissionConfigArgs,
|
|
39
|
+
codexPermissionThreadParams,
|
|
40
|
+
codexThreadPermissions,
|
|
41
|
+
} from "./permissions.js";
|
|
36
42
|
|
|
37
43
|
/**
|
|
38
44
|
* Extract a resume thread id from session params (reusing the codec's
|
|
@@ -122,6 +128,52 @@ function buildCodexUserInputAnswers(
|
|
|
122
128
|
return out;
|
|
123
129
|
}
|
|
124
130
|
|
|
131
|
+
/** An MCP tool call Codex has started and not finished (`item/started`, `mcpToolCall`). */
|
|
132
|
+
interface InFlightMcpCall {
|
|
133
|
+
itemId: string;
|
|
134
|
+
threadId: string | null;
|
|
135
|
+
server: string;
|
|
136
|
+
tool: string;
|
|
137
|
+
arguments: unknown;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* One line for an MCP tool approval from Codex's `tool_params_display`
|
|
142
|
+
* (`[{ name, display_name, value }]`), e.g.
|
|
143
|
+
* `Repository: acme/web · Branch name: fix/login · Base ref: main`.
|
|
144
|
+
*/
|
|
145
|
+
function describeMcpToolParams(display: unknown): string | undefined {
|
|
146
|
+
if (!Array.isArray(display)) return undefined;
|
|
147
|
+
const parts: string[] = [];
|
|
148
|
+
for (const entry of display) {
|
|
149
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
150
|
+
const param = entry as Record<string, unknown>;
|
|
151
|
+
const label = str(param, "display_name") || str(param, "name");
|
|
152
|
+
if (!label) continue;
|
|
153
|
+
const raw = param["value"];
|
|
154
|
+
const value = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
155
|
+
if (value === undefined) continue;
|
|
156
|
+
parts.push(`${label}: ${value.length > 120 ? `${value.slice(0, 117)}...` : value}`);
|
|
157
|
+
}
|
|
158
|
+
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The elicitation reply Codex expects: `action`, plus `content` only on an accept. */
|
|
162
|
+
function elicitationReply(resp: ElicitationResponse): Record<string, unknown> {
|
|
163
|
+
return {
|
|
164
|
+
action: resp.action,
|
|
165
|
+
...(resp.action === "accept" && resp.content ? { content: resp.content } : {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** JSON with object keys sorted, so two argument objects compare by value. */
|
|
170
|
+
function stableJson(value: unknown): string {
|
|
171
|
+
return JSON.stringify(value, (_key, v: unknown) =>
|
|
172
|
+
v && typeof v === "object" && !Array.isArray(v)
|
|
173
|
+
? Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)))
|
|
174
|
+
: v) ?? "";
|
|
175
|
+
}
|
|
176
|
+
|
|
125
177
|
/** A pending `send()` whose `result` Promise hasn't settled yet. */
|
|
126
178
|
interface PendingResult {
|
|
127
179
|
resolve: (result: TurnResult) => void;
|
|
@@ -328,15 +380,13 @@ export async function createCodexSession(ctx: SessionContext): Promise<AgentSess
|
|
|
328
380
|
// Spawn Codex in interactive JSON-RPC mode via the `app-server` subcommand
|
|
329
381
|
// (codex-cli 0.130.0+; the old top-level `--json` flag was removed).
|
|
330
382
|
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
383
|
+
// Approval policy and sandbox go in as root `-c` overrides, BEFORE the
|
|
384
|
+
// subcommand: app-server ignores `--sandbox` and
|
|
385
|
+
// `--dangerously-bypass-approvals-and-sandbox` (see ./permissions.ts). The
|
|
386
|
+
// handshake repeats them on the thread itself.
|
|
333
387
|
// extraArgs land after the subcommand — semantics depend on the user's intent.
|
|
334
388
|
const args = [...resolved.prefixArgs];
|
|
335
|
-
|
|
336
|
-
args.push("--sandbox", "read-only");
|
|
337
|
-
} else if (config.skipPermissions) {
|
|
338
|
-
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
339
|
-
}
|
|
389
|
+
args.push(...codexPermissionConfigArgs(codexThreadPermissions(config)));
|
|
340
390
|
// Custom endpoint model_providers overrides are top-level `-c` options, placed
|
|
341
391
|
// with the other top-level flags before the `app-server` subcommand (the
|
|
342
392
|
// position that is always valid for global options).
|
|
@@ -437,6 +487,9 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
437
487
|
/** Stamps `tool_result.toolName` by correlating with prior `tool_call`s. */
|
|
438
488
|
private readonly _trackToolName = createToolNameTracker();
|
|
439
489
|
|
|
490
|
+
/** MCP tool calls started and not yet completed, by item id, for pairing approvals. */
|
|
491
|
+
private readonly _mcpCallsInFlight = new Map<string, InFlightMcpCall>();
|
|
492
|
+
|
|
440
493
|
// Per-turn accumulators. Cleared after each result delivery so a subsequent
|
|
441
494
|
// turn's events don't inherit stale values.
|
|
442
495
|
private _turnSummary: string | null = null;
|
|
@@ -771,6 +824,10 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
771
824
|
this.proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
772
825
|
}
|
|
773
826
|
|
|
827
|
+
private rpcError(id: number, code: number, message: string): void {
|
|
828
|
+
this.proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
|
|
829
|
+
}
|
|
830
|
+
|
|
774
831
|
// -------------------------------------------------------------------------
|
|
775
832
|
// Handshake
|
|
776
833
|
// -------------------------------------------------------------------------
|
|
@@ -791,9 +848,12 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
791
848
|
// otherwise start a fresh one. `thread/resume` continues the SAME thread
|
|
792
849
|
// with its full context retained — distinct from `thread/fork`, which is
|
|
793
850
|
// a divergent rewind copy. The thread keeps its original cwd/model, so we
|
|
794
|
-
// pass only the thread id (+ refreshed developer instructions
|
|
851
|
+
// pass only the thread id (+ refreshed developer instructions, and the
|
|
852
|
+
// approval policy and sandbox this session runs with, which may differ
|
|
853
|
+
// from the ones the thread was created under).
|
|
854
|
+
const permissionParams = codexPermissionThreadParams(codexThreadPermissions(this.ctx.config));
|
|
795
855
|
if (this._resumeThreadId) {
|
|
796
|
-
const resumeParams: Record<string, unknown> = { threadId: this._resumeThreadId };
|
|
856
|
+
const resumeParams: Record<string, unknown> = { threadId: this._resumeThreadId, ...permissionParams };
|
|
797
857
|
if (this.instructions) resumeParams["developerInstructions"] = this.instructions;
|
|
798
858
|
try {
|
|
799
859
|
const res = await this.rpcRequest("thread/resume", resumeParams);
|
|
@@ -830,7 +890,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
830
890
|
}
|
|
831
891
|
|
|
832
892
|
// thread/start (fresh)
|
|
833
|
-
const threadParams: Record<string, unknown> = { cwd: this.cwd };
|
|
893
|
+
const threadParams: Record<string, unknown> = { cwd: this.cwd, ...permissionParams };
|
|
834
894
|
if (this.model) threadParams["model"] = this.model;
|
|
835
895
|
if (this.instructions) threadParams["developerInstructions"] = this.instructions;
|
|
836
896
|
|
|
@@ -1236,16 +1296,174 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1236
1296
|
) {
|
|
1237
1297
|
// `tool/requestUserInput` is the legacy method name on older codex builds.
|
|
1238
1298
|
void this.handleUserInputRequest(id, params);
|
|
1299
|
+
} else if (method === "mcpServer/elicitation/request") {
|
|
1300
|
+
void this.handleMcpElicitation(id, params);
|
|
1301
|
+
} else {
|
|
1302
|
+
// A request this file does not handle. Every server request the protocol
|
|
1303
|
+
// defines declares required response fields, so the `{}` this used to
|
|
1304
|
+
// send was schema-invalid, and Codex read it as a refusal: that is how
|
|
1305
|
+
// MCP tool approvals were silently declined before they were handled
|
|
1306
|
+
// here. "Method not found" is the honest answer and fails visibly.
|
|
1307
|
+
// What's left is opt-in (`attestation/generate`), never sent to a client
|
|
1308
|
+
// that registers no dynamic tools and no external auth (`item/tool/call`,
|
|
1309
|
+
// `account/chatgptAuthTokens/refresh`), or v1-only (`applyPatchApproval`,
|
|
1310
|
+
// `execCommandApproval`).
|
|
1311
|
+
this.rpcError(id, -32601, `agentex does not handle the ${method} request`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Handle `mcpServer/elicitation/request`. Codex sends two kinds.
|
|
1317
|
+
*
|
|
1318
|
+
* With `_meta.codex_approval_kind: "mcp_tool_call"` it is a tool permission:
|
|
1319
|
+
* an MCP tool (a ChatGPT app such as GitHub, or a server in the user's
|
|
1320
|
+
* config.toml) that needs a yes before it runs. Codex asks only while the
|
|
1321
|
+
* thread's approval policy is on; under `never` with full access it approves
|
|
1322
|
+
* these itself. It goes to `onUserInputRequest` like every other approval.
|
|
1323
|
+
*
|
|
1324
|
+
* Anything else is an MCP elicitation proper, a form or a URL an MCP server
|
|
1325
|
+
* asks the user to fill in or open, and goes to `onElicitation`.
|
|
1326
|
+
*
|
|
1327
|
+
* The reply must carry `action`. Before this was handled, the generic `{}`
|
|
1328
|
+
* reply was read as a decline, so every app write failed as "user rejected
|
|
1329
|
+
* MCP tool call" with nobody asked.
|
|
1330
|
+
*/
|
|
1331
|
+
private async handleMcpElicitation(id: number, params: Record<string, unknown>): Promise<void> {
|
|
1332
|
+
const meta = asObj(params, "_meta");
|
|
1333
|
+
if (str(meta, "codex_approval_kind") === "mcp_tool_call") {
|
|
1334
|
+
await this.handleMcpToolApproval(id, params, meta);
|
|
1239
1335
|
} else {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1336
|
+
await this.handleElicitation(id, params);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
private async handleMcpToolApproval(
|
|
1341
|
+
id: number,
|
|
1342
|
+
params: Record<string, unknown>,
|
|
1343
|
+
meta: Record<string, unknown>,
|
|
1344
|
+
): Promise<void> {
|
|
1345
|
+
const requestThreadId = str(params, "threadId") || null;
|
|
1346
|
+
const foreign = this.isForeignThread(requestThreadId);
|
|
1347
|
+
if (!foreign) this._state = "waiting_for_approval";
|
|
1348
|
+
const restore = () => { if (!foreign) this.restoreStateAfter("waiting_for_approval"); };
|
|
1349
|
+
|
|
1350
|
+
// No host handler: allowed, like the command and file-change approvals.
|
|
1351
|
+
if (!this.ctx.onUserInputRequest) {
|
|
1352
|
+
this.rpcResponse(id, { action: "accept" });
|
|
1353
|
+
restore();
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// The request names neither the tool nor the call it gates. The call has
|
|
1358
|
+
// already started (Codex reports `item/started` before it asks), so pair
|
|
1359
|
+
// the two through the in-flight MCP calls.
|
|
1360
|
+
const server = str(params, "serverName");
|
|
1361
|
+
const toolParams = meta["tool_params"];
|
|
1362
|
+
const call = this.findMcpCallForApproval(requestThreadId, server, toolParams);
|
|
1363
|
+
const input = call?.arguments ?? toolParams;
|
|
1364
|
+
|
|
1365
|
+
try {
|
|
1366
|
+
const resp = await this.ctx.onUserInputRequest({
|
|
1367
|
+
toolName: codexMcpToolName(call?.server ?? server, call?.tool ?? null),
|
|
1368
|
+
input: input && typeof input === "object" && !Array.isArray(input)
|
|
1369
|
+
? input as Record<string, unknown>
|
|
1370
|
+
: {},
|
|
1371
|
+
// The call's item id lines the approval up with its tool_call event,
|
|
1372
|
+
// as `itemId` does for command approvals. Without a match, a fresh id:
|
|
1373
|
+
// hosts key pending approvals on this, so it must never repeat.
|
|
1374
|
+
toolUseId: call?.itemId ?? `codex-mcp-approval-${randomUUID()}`,
|
|
1375
|
+
// Codex's own question, e.g. `Allow GitHub to run tool "create_branch"?`.
|
|
1376
|
+
title: str(params, "message") || undefined,
|
|
1377
|
+
displayName: str(meta, "tool_title") || str(meta, "connector_name") || undefined,
|
|
1378
|
+
description: describeMcpToolParams(meta["tool_params_display"]),
|
|
1379
|
+
...(foreign && requestThreadId ? { agentId: requestThreadId } : {}),
|
|
1380
|
+
});
|
|
1381
|
+
// A plain accept runs this one call. Codex also offers "for this
|
|
1382
|
+
// session" and "always" (`_meta.persist`), which agentex's yes/no
|
|
1383
|
+
// permission has no way to express, so the host is asked each time.
|
|
1384
|
+
this.rpcResponse(id, { action: resp.allow ? "accept" : "decline" });
|
|
1385
|
+
} catch {
|
|
1386
|
+
this.rpcResponse(id, { action: "decline" });
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
restore();
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/** An MCP server's form or URL request, for the host's `onElicitation`. Declined without one. */
|
|
1393
|
+
private async handleElicitation(id: number, params: Record<string, unknown>): Promise<void> {
|
|
1394
|
+
if (!this.ctx.onElicitation) {
|
|
1395
|
+
this.rpcResponse(id, { action: "decline" });
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const requestThreadId = str(params, "threadId") || null;
|
|
1400
|
+
const foreign = this.isForeignThread(requestThreadId);
|
|
1401
|
+
if (!foreign) this._state = "waiting_for_input";
|
|
1402
|
+
const restore = () => { if (!foreign) this.restoreStateAfter("waiting_for_input"); };
|
|
1403
|
+
|
|
1404
|
+
const url = str(params, "url");
|
|
1405
|
+
const elicitationId = str(params, "elicitationId");
|
|
1406
|
+
const schema = params["requestedSchema"];
|
|
1407
|
+
try {
|
|
1408
|
+
const resp = await this.ctx.onElicitation({
|
|
1409
|
+
mcpServerName: str(params, "serverName"),
|
|
1410
|
+
message: str(params, "message"),
|
|
1411
|
+
mode: str(params, "mode") === "url" ? "url" : "form",
|
|
1412
|
+
...(url ? { url } : {}),
|
|
1413
|
+
...(elicitationId ? { elicitationId } : {}),
|
|
1414
|
+
...(schema && typeof schema === "object" && !Array.isArray(schema)
|
|
1415
|
+
? { requestedSchema: schema as Record<string, unknown> }
|
|
1416
|
+
: {}),
|
|
1417
|
+
});
|
|
1418
|
+
this.rpcResponse(id, elicitationReply(resp));
|
|
1419
|
+
} catch {
|
|
1420
|
+
this.rpcResponse(id, { action: "cancel" });
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
restore();
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Remember the MCP tool calls Codex has started and not finished, on every
|
|
1428
|
+
* thread, so an approval request can be paired with the call it gates.
|
|
1429
|
+
*/
|
|
1430
|
+
private trackMcpToolCall(method: string, params: Record<string, unknown>): void {
|
|
1431
|
+
if (method !== "item/started" && method !== "item/completed") return;
|
|
1432
|
+
const item = asObj(params, "item");
|
|
1433
|
+
if (str(item, "type") !== "mcpToolCall") return;
|
|
1434
|
+
const itemId = str(item, "id");
|
|
1435
|
+
if (!itemId) return;
|
|
1436
|
+
if (method === "item/completed") {
|
|
1437
|
+
this._mcpCallsInFlight.delete(itemId);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
this._mcpCallsInFlight.set(itemId, {
|
|
1441
|
+
itemId,
|
|
1442
|
+
threadId: str(params, "threadId") || null,
|
|
1443
|
+
server: str(item, "server"),
|
|
1444
|
+
tool: str(item, "tool"),
|
|
1445
|
+
arguments: item["arguments"],
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* The in-flight call an approval is about: on the asking thread and server,
|
|
1451
|
+
* the one whose arguments match the approval's `tool_params`, else the most
|
|
1452
|
+
* recently started (Codex asks right after it starts a call).
|
|
1453
|
+
*/
|
|
1454
|
+
private findMcpCallForApproval(
|
|
1455
|
+
threadId: string | null,
|
|
1456
|
+
server: string,
|
|
1457
|
+
toolParams: unknown,
|
|
1458
|
+
): InFlightMcpCall | null {
|
|
1459
|
+
const candidates = [...this._mcpCallsInFlight.values()].filter((call) =>
|
|
1460
|
+
call.server === server && (!threadId || !call.threadId || call.threadId === threadId));
|
|
1461
|
+
if (toolParams !== undefined) {
|
|
1462
|
+
const wanted = stableJson(toolParams);
|
|
1463
|
+
const exact = candidates.find((call) => stableJson(call.arguments) === wanted);
|
|
1464
|
+
if (exact) return exact;
|
|
1248
1465
|
}
|
|
1466
|
+
return candidates.at(-1) ?? null;
|
|
1249
1467
|
}
|
|
1250
1468
|
|
|
1251
1469
|
/**
|
|
@@ -1965,6 +2183,7 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1965
2183
|
|
|
1966
2184
|
private handleNotification(method: string, params: Record<string, unknown>, rawLine: string): void {
|
|
1967
2185
|
if (this._state === "closed") return;
|
|
2186
|
+
this.trackMcpToolCall(method, params);
|
|
1968
2187
|
// codex/event — legacy wrapper
|
|
1969
2188
|
if (method === "codex/event") {
|
|
1970
2189
|
const innerMsg = str(params, "msg");
|
|
@@ -12,11 +12,20 @@ import type { CodexTranscriptLine } from "./transcript.js";
|
|
|
12
12
|
* response_item / reasoning → thinking
|
|
13
13
|
* response_item / function_call → tool_call
|
|
14
14
|
* response_item / function_call_output → tool_result
|
|
15
|
-
*
|
|
15
|
+
* response_item / custom_tool_call → tool_call
|
|
16
|
+
* response_item / custom_tool_call_output → tool_result
|
|
17
|
+
* event_msg / task_complete → result (completed, or failed with `error`)
|
|
18
|
+
* event_msg / turn_aborted → result (interrupted)
|
|
19
|
+
*
|
|
20
|
+
* Current Codex runs shell work through freeform tools: `exec` takes a script
|
|
21
|
+
* and `apply_patch` takes a patch, so most tool activity in a current rollout
|
|
22
|
+
* is a `custom_tool_call`. Its `input` is the raw string the model wrote.
|
|
16
23
|
*
|
|
17
24
|
* Everything else — `session_meta`, `turn_context`, `task_started`,
|
|
18
|
-
* `token_count`, `agent_message`/`agent_reasoning` duplicates,
|
|
19
|
-
*
|
|
25
|
+
* `token_count`, `agent_message`/`agent_reasoning` duplicates, the
|
|
26
|
+
* `item_completed` mirrors of response items, inter-agent `agent_message`
|
|
27
|
+
* items, user/developer messages, unwrapped legacy lines, unknown types —
|
|
28
|
+
* yields `[]`.
|
|
20
29
|
*
|
|
21
30
|
* The on-disk vocabulary is Codex-internal and version-shifting, so every field
|
|
22
31
|
* is read defensively and a weird line NEVER throws — it returns `[]`. Codex
|
|
@@ -89,7 +98,19 @@ function mapLine(line: CodexTranscriptLine, sessionId: string | null): StreamEve
|
|
|
89
98
|
];
|
|
90
99
|
}
|
|
91
100
|
|
|
92
|
-
if (innerType === "
|
|
101
|
+
if (innerType === "custom_tool_call") {
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
type: "tool_call",
|
|
105
|
+
toolCallId: str(payload["call_id"]) ?? str(payload["id"]),
|
|
106
|
+
name: str(payload["name"]) ?? "custom_tool_call",
|
|
107
|
+
input: typeof payload["input"] === "string" ? payload["input"] : null,
|
|
108
|
+
...base,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (innerType === "function_call_output" || innerType === "custom_tool_call_output") {
|
|
93
114
|
return [
|
|
94
115
|
{
|
|
95
116
|
type: "tool_result",
|
|
@@ -110,16 +131,36 @@ function mapLine(line: CodexTranscriptLine, sessionId: string | null): StreamEve
|
|
|
110
131
|
|
|
111
132
|
if (line.type === "event_msg") {
|
|
112
133
|
if (innerType === "task_complete") {
|
|
134
|
+
// A turn that failed (for example a model the account cannot use) still
|
|
135
|
+
// ends in task_complete, with `error` set and no agent message.
|
|
136
|
+
const error = turnErrorMessage(payload["error"]);
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
type: "result",
|
|
140
|
+
text: error ?? str(payload["last_agent_message"]) ?? "",
|
|
141
|
+
costUsd: null,
|
|
142
|
+
isError: error !== null,
|
|
143
|
+
stopReason: null,
|
|
144
|
+
terminalReason: error !== null ? "failed" : "completed",
|
|
145
|
+
numTurns: null,
|
|
146
|
+
durationMs: num(payload["duration_ms"]),
|
|
147
|
+
...base,
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (innerType === "turn_aborted") {
|
|
153
|
+
// Same shape as a live `turn/completed` with status "interrupted".
|
|
113
154
|
return [
|
|
114
155
|
{
|
|
115
156
|
type: "result",
|
|
116
|
-
text:
|
|
157
|
+
text: "",
|
|
117
158
|
costUsd: null,
|
|
118
159
|
isError: false,
|
|
119
160
|
stopReason: null,
|
|
120
|
-
terminalReason: "
|
|
161
|
+
terminalReason: str(payload["reason"]) ?? "interrupted",
|
|
121
162
|
numTurns: null,
|
|
122
|
-
durationMs:
|
|
163
|
+
durationMs: num(payload["duration_ms"]),
|
|
123
164
|
...base,
|
|
124
165
|
},
|
|
125
166
|
];
|
|
@@ -135,6 +176,28 @@ function str(v: unknown): string | null {
|
|
|
135
176
|
return typeof v === "string" && v.length > 0 ? v : null;
|
|
136
177
|
}
|
|
137
178
|
|
|
179
|
+
/** Finite number or null. */
|
|
180
|
+
function num(v: unknown): number | null {
|
|
181
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* `task_complete.error` is `{ message, codex_error_info }`. The message is
|
|
186
|
+
* often the API's JSON error body, so prefer its inner `error.message`.
|
|
187
|
+
*/
|
|
188
|
+
function turnErrorMessage(error: unknown): string | null {
|
|
189
|
+
if (typeof error === "string") return str(error);
|
|
190
|
+
if (typeof error !== "object" || error === null) return null;
|
|
191
|
+
const message = str((error as Record<string, unknown>)["message"]);
|
|
192
|
+
if (!message) return "Turn failed";
|
|
193
|
+
try {
|
|
194
|
+
const body = JSON.parse(message) as { error?: { message?: unknown }; message?: unknown };
|
|
195
|
+
return str(body?.error?.message) ?? str(body?.message) ?? message;
|
|
196
|
+
} catch {
|
|
197
|
+
return message;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
138
201
|
function messagePhase(v: unknown): "commentary" | "final_answer" | undefined {
|
|
139
202
|
return v === "commentary" || v === "final_answer" ? v : undefined;
|
|
140
203
|
}
|
|
@@ -171,13 +234,25 @@ function extractReasoningSummary(summary: unknown): string | null {
|
|
|
171
234
|
}
|
|
172
235
|
|
|
173
236
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
237
|
+
* Tool output is usually a string. Some versions wrap it as
|
|
238
|
+
* `{ output: "...", metadata: {...} }`, and Codex 0.142+ also writes a list of
|
|
239
|
+
* content parts (`input_text`, `input_image`). Extract the readable text, one
|
|
240
|
+
* part per line, with images left in `raw`. Fall back to a JSON dump
|
|
241
|
+
* so nothing is silently lost. Returns "" when unreadable
|
|
177
242
|
* (`tool_result.content` is a required string).
|
|
178
243
|
*/
|
|
179
244
|
function extractOutputText(output: unknown): string {
|
|
180
245
|
if (typeof output === "string") return output;
|
|
246
|
+
if (Array.isArray(output)) {
|
|
247
|
+
let text = "";
|
|
248
|
+
for (const part of output) {
|
|
249
|
+
if (typeof part !== "object" || part === null) continue;
|
|
250
|
+
const partText = (part as { text?: unknown }).text;
|
|
251
|
+
if (typeof partText !== "string" || partText.length === 0) continue;
|
|
252
|
+
text += text && !text.endsWith("\n") ? `\n${partText}` : partText;
|
|
253
|
+
}
|
|
254
|
+
return text;
|
|
255
|
+
}
|
|
181
256
|
if (output && typeof output === "object" && !Array.isArray(output)) {
|
|
182
257
|
const inner = (output as Record<string, unknown>)["output"];
|
|
183
258
|
if (typeof inner === "string") return inner;
|
|
@@ -28,10 +28,10 @@ import { createReadStream } from "node:fs";
|
|
|
28
28
|
import { readdir, stat, open as fsOpen } from "node:fs/promises";
|
|
29
29
|
import * as os from "node:os";
|
|
30
30
|
import * as path from "node:path";
|
|
31
|
-
import * as readline from "node:readline";
|
|
32
31
|
|
|
33
32
|
import { getDefaultRuntimeHome, getRuntimeHomeEnvVar } from "../../utils/runtime-homes.js";
|
|
34
33
|
import type { FoundTranscript, TranscriptOps } from "../../types.js";
|
|
34
|
+
import { readJsonlLines } from "../../utils/jsonl-lines.js";
|
|
35
35
|
|
|
36
36
|
/** Bytes scanned from the tail of the file in {@link peekCodexTranscript}. */
|
|
37
37
|
const PEEK_TAIL_BYTES = 16 * 1024;
|
|
@@ -316,16 +316,11 @@ export async function* readCodexTranscript(
|
|
|
316
316
|
|
|
317
317
|
const stream = createReadStream(filePath, { start: fromOffset, encoding: undefined });
|
|
318
318
|
stream.on("error", () => {});
|
|
319
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
320
319
|
|
|
321
320
|
const fileIdentity = rolloutIdentityFromPath(filePath);
|
|
322
|
-
let pos = fromOffset;
|
|
323
321
|
try {
|
|
324
|
-
for await (const line of
|
|
325
|
-
const
|
|
326
|
-
pos += Buffer.byteLength(line, "utf8") + 1;
|
|
327
|
-
|
|
328
|
-
const trimmed = line.trim();
|
|
322
|
+
for await (const line of readJsonlLines(stream, fromOffset)) {
|
|
323
|
+
const trimmed = line.text.trim();
|
|
329
324
|
if (!trimmed) continue;
|
|
330
325
|
|
|
331
326
|
const parsed = parseCodexLine(trimmed);
|
|
@@ -334,15 +329,14 @@ export async function* readCodexTranscript(
|
|
|
334
329
|
// Replay-stable synthetic identity: (rollout identity, line start offset).
|
|
335
330
|
// Codex emits no native per-event uuid, so this is the idempotency key
|
|
336
331
|
// hosts use to dedup transcript replays. Deterministic across reads.
|
|
337
|
-
parsed.eventId = `codex:${fileIdentity}:${
|
|
332
|
+
parsed.eventId = `codex:${fileIdentity}:${line.start}`;
|
|
338
333
|
|
|
339
|
-
yield { event: parsed, offset:
|
|
334
|
+
yield { event: parsed, offset: line.end };
|
|
340
335
|
}
|
|
341
336
|
} catch (err) {
|
|
342
337
|
const e = err as NodeJS.ErrnoException;
|
|
343
338
|
if (e?.code !== "ENOENT") throw err;
|
|
344
339
|
} finally {
|
|
345
|
-
rl.close();
|
|
346
340
|
stream.destroy();
|
|
347
341
|
}
|
|
348
342
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as os from "node:os";
|
|
4
|
-
import * as readline from "node:readline";
|
|
5
4
|
import { createReadStream } from "node:fs";
|
|
6
5
|
import type { TokenUsage } from "../../types.js";
|
|
6
|
+
import { readJsonlLines } from "../../utils/jsonl-lines.js";
|
|
7
7
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
// Log path resolution
|
|
@@ -104,16 +104,14 @@ async function scanFile(
|
|
|
104
104
|
): Promise<void> {
|
|
105
105
|
let stream: ReturnType<typeof createReadStream>;
|
|
106
106
|
try {
|
|
107
|
-
stream = createReadStream(filePath
|
|
107
|
+
stream = createReadStream(filePath);
|
|
108
108
|
} catch {
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
113
|
-
|
|
114
112
|
try {
|
|
115
|
-
for await (const line of
|
|
116
|
-
const trimmed = line.trim();
|
|
113
|
+
for await (const line of readJsonlLines(stream)) {
|
|
114
|
+
const trimmed = line.text.trim();
|
|
117
115
|
if (!trimmed) continue;
|
|
118
116
|
|
|
119
117
|
let parsed: Record<string, unknown>;
|
|
@@ -172,7 +170,6 @@ async function scanFile(
|
|
|
172
170
|
usageByModel.set(model, existing);
|
|
173
171
|
}
|
|
174
172
|
} finally {
|
|
175
|
-
rl.close();
|
|
176
173
|
stream.destroy();
|
|
177
174
|
}
|
|
178
175
|
}
|
package/src/utils/auth.ts
CHANGED
|
@@ -96,7 +96,7 @@ export function detectAuth(providerType: string, env: Record<string, string>): R
|
|
|
96
96
|
return { method: "subscription", billingType: "subscription" };
|
|
97
97
|
}
|
|
98
98
|
case "antigravity": {
|
|
99
|
-
if (hasEnv(env, "GEMINI_API_KEY") && readAgyModelProvider() === "gemini") {
|
|
99
|
+
if (hasEnv(env, "GEMINI_API_KEY") && readAgyModelProvider(env) === "gemini") {
|
|
100
100
|
return { method: "api_key", billingType: "api" };
|
|
101
101
|
}
|
|
102
102
|
return { method: "subscription", billingType: "subscription" };
|
|
@@ -448,19 +448,20 @@ async function resolveGeminiAuth(ctx?: AuthResolveContext): Promise<AuthReport>
|
|
|
448
448
|
// API (`"modelProvider": "gemini"`); the CLI ignores the variable otherwise.
|
|
449
449
|
// ---------------------------------------------------------------------------
|
|
450
450
|
|
|
451
|
-
/**
|
|
452
|
-
|
|
453
|
-
|
|
451
|
+
/**
|
|
452
|
+
* Home directory as the spawned agy (a Go binary) resolves it: os.UserHomeDir
|
|
453
|
+
* semantics against the child's environment, so a host that gives agy its own
|
|
454
|
+
* HOME reads that home's settings.
|
|
455
|
+
*/
|
|
456
|
+
function agyHomeDir(env: Record<string, string | undefined>): string {
|
|
457
|
+
const fromEnv = process.platform === "win32" ? env["USERPROFILE"] : env["HOME"];
|
|
454
458
|
return fromEnv && fromEnv.trim() ? fromEnv : os.homedir();
|
|
455
459
|
}
|
|
456
460
|
|
|
457
|
-
function
|
|
458
|
-
return path.join(agyHomeDir(), ".gemini", "antigravity-cli", "settings.json");
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function readAgyModelProvider(): string | null {
|
|
461
|
+
function readAgyModelProvider(env: Record<string, string | undefined>): string | null {
|
|
462
462
|
try {
|
|
463
|
-
const
|
|
463
|
+
const settingsPath = path.join(agyHomeDir(env), ".gemini", "antigravity-cli", "settings.json");
|
|
464
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record<string, unknown>;
|
|
464
465
|
return typeof settings["modelProvider"] === "string" ? settings["modelProvider"] : null;
|
|
465
466
|
} catch {
|
|
466
467
|
return null;
|
|
@@ -470,7 +471,7 @@ function readAgyModelProvider(): string | null {
|
|
|
470
471
|
async function resolveAntigravityAuth(ctx?: AuthResolveContext): Promise<AuthReport> {
|
|
471
472
|
const env = buildEnv(ctx?.env);
|
|
472
473
|
const binary = await checkBinary("agy", ctx?.command, env);
|
|
473
|
-
const apiKeyProvider = readAgyModelProvider() === "gemini";
|
|
474
|
+
const apiKeyProvider = readAgyModelProvider(env) === "gemini";
|
|
474
475
|
let signedIn = false;
|
|
475
476
|
let usedCli = false;
|
|
476
477
|
if (binary.installed && binary.resolvedPath) {
|