@agentex/agent 0.0.38 → 0.0.40
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 +44 -0
- package/README.md +47 -13
- 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/codec.d.ts +4 -0
- package/dist/providers/antigravity/codec.d.ts.map +1 -0
- package/dist/providers/antigravity/codec.js +27 -0
- package/dist/providers/antigravity/codec.js.map +1 -0
- package/dist/providers/antigravity/connection.d.ts +100 -0
- package/dist/providers/antigravity/connection.d.ts.map +1 -0
- package/dist/providers/antigravity/connection.js +267 -0
- package/dist/providers/antigravity/connection.js.map +1 -0
- package/dist/providers/antigravity/discovery.d.ts +11 -0
- package/dist/providers/antigravity/discovery.d.ts.map +1 -0
- package/dist/providers/antigravity/discovery.js +92 -0
- package/dist/providers/antigravity/discovery.js.map +1 -0
- package/dist/providers/antigravity/execute.d.ts +9 -0
- package/dist/providers/antigravity/execute.d.ts.map +1 -0
- package/dist/providers/antigravity/execute.js +206 -0
- package/dist/providers/antigravity/execute.js.map +1 -0
- package/dist/providers/antigravity/index.d.ts +12 -0
- package/dist/providers/antigravity/index.d.ts.map +1 -0
- package/dist/providers/antigravity/index.js +46 -0
- package/dist/providers/antigravity/index.js.map +1 -0
- package/dist/providers/antigravity/parse.d.ts +114 -0
- package/dist/providers/antigravity/parse.d.ts.map +1 -0
- package/dist/providers/antigravity/parse.js +369 -0
- package/dist/providers/antigravity/parse.js.map +1 -0
- package/dist/providers/antigravity/probe.d.ts +3 -0
- package/dist/providers/antigravity/probe.d.ts.map +1 -0
- package/dist/providers/antigravity/probe.js +84 -0
- package/dist/providers/antigravity/probe.js.map +1 -0
- package/dist/providers/antigravity/runtime.d.ts +30 -0
- package/dist/providers/antigravity/runtime.d.ts.map +1 -0
- package/dist/providers/antigravity/runtime.js +69 -0
- package/dist/providers/antigravity/runtime.js.map +1 -0
- package/dist/providers/antigravity/session.d.ts +7 -0
- package/dist/providers/antigravity/session.d.ts.map +1 -0
- package/dist/providers/antigravity/session.js +317 -0
- package/dist/providers/antigravity/session.js.map +1 -0
- 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/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/providers/gemini/index.d.ts.map +1 -1
- package/dist/providers/gemini/index.js +6 -0
- package/dist/providers/gemini/index.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +2 -0
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +6 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/auth.d.ts +1 -1
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +79 -1
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/binary.d.ts.map +1 -1
- package/dist/utils/binary.js +10 -0
- package/dist/utils/binary.js.map +1 -1
- package/dist/utils/instructions.d.ts.map +1 -1
- package/dist/utils/instructions.js +12 -9
- package/dist/utils/instructions.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/dist/utils/runtime-homes.d.ts.map +1 -1
- package/dist/utils/runtime-homes.js +4 -0
- package/dist/utils/runtime-homes.js.map +1 -1
- package/dist/utils/skills.d.ts +3 -3
- package/dist/utils/skills.d.ts.map +1 -1
- package/dist/utils/skills.js +16 -2
- package/dist/utils/skills.js.map +1 -1
- package/package.json +3 -2
- package/src/history/fs.ts +6 -12
- package/src/providers/antigravity/codec.ts +29 -0
- package/src/providers/antigravity/connection.ts +304 -0
- package/src/providers/antigravity/discovery.ts +90 -0
- package/src/providers/antigravity/execute.ts +214 -0
- package/src/providers/antigravity/index.ts +48 -0
- package/src/providers/antigravity/parse.ts +439 -0
- package/src/providers/antigravity/probe.ts +89 -0
- package/src/providers/antigravity/runtime.ts +64 -0
- package/src/providers/antigravity/session.ts +359 -0
- 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/transcript-normalize.ts +85 -10
- package/src/providers/codex/transcript.ts +5 -11
- package/src/providers/codex/usage-scanner.ts +4 -7
- package/src/providers/gemini/index.ts +6 -0
- package/src/registry.ts +2 -0
- package/src/types.ts +6 -5
- package/src/utils/auth.ts +82 -1
- package/src/utils/binary.ts +10 -0
- package/src/utils/instructions.ts +10 -9
- package/src/utils/jsonl-lines.ts +80 -0
- package/src/utils/runtime-homes.ts +4 -0
- package/src/utils/skills.ts +17 -4
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentSession,
|
|
3
|
+
CancelResult,
|
|
4
|
+
ClearGoalResult,
|
|
5
|
+
GoalOptions,
|
|
6
|
+
GoalState,
|
|
7
|
+
SendHandle,
|
|
8
|
+
SendOptions,
|
|
9
|
+
SessionContext,
|
|
10
|
+
SessionState,
|
|
11
|
+
SetGoalResult,
|
|
12
|
+
StopTaskResult,
|
|
13
|
+
StreamEvent,
|
|
14
|
+
TurnResult,
|
|
15
|
+
TurnTrigger,
|
|
16
|
+
} from "../../types.js";
|
|
17
|
+
import { GoalController, EMULATED_GOAL_CAPABILITY } from "../../goals/index.js";
|
|
18
|
+
import type { ResolvedBinary } from "../../utils/binary.js";
|
|
19
|
+
import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
|
|
20
|
+
import { resolveInstructions } from "../../utils/instructions.js";
|
|
21
|
+
import { injectHomeSkills } from "../../utils/skills.js";
|
|
22
|
+
import { uuidv7 } from "../../utils/uuid.js";
|
|
23
|
+
import {
|
|
24
|
+
AgyConnection,
|
|
25
|
+
firstAgyErrorLine,
|
|
26
|
+
interruptAgyTurn,
|
|
27
|
+
type AgyTurnOutcome,
|
|
28
|
+
} from "./connection.js";
|
|
29
|
+
import {
|
|
30
|
+
ANTIGRAVITY_PROVIDER_TYPE,
|
|
31
|
+
AgyStreamParser,
|
|
32
|
+
agyUsageRecord,
|
|
33
|
+
classifyAgyFailure,
|
|
34
|
+
mapAgyTurnStatus,
|
|
35
|
+
} from "./parse.js";
|
|
36
|
+
import { AGY_SIGN_IN_MESSAGE, buildAgyArgs, findAgyBinary, readAgyResumeId } from "./runtime.js";
|
|
37
|
+
|
|
38
|
+
interface ActiveTurn {
|
|
39
|
+
turnId: string;
|
|
40
|
+
abort: AbortController;
|
|
41
|
+
done: Promise<TurnResult>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Open a multi-turn Antigravity session. Throws when `agy` is not installed.
|
|
46
|
+
* The process starts on the first `send()`.
|
|
47
|
+
*/
|
|
48
|
+
export async function createAntigravitySession(ctx: SessionContext): Promise<AgentSession> {
|
|
49
|
+
const binary = await findAgyBinary(ctx);
|
|
50
|
+
const config = ctx.config ?? {};
|
|
51
|
+
const instructions = await resolveInstructions(config.instructionsFile);
|
|
52
|
+
if (config.skillDirs && config.skillDirs.length > 0) {
|
|
53
|
+
try {
|
|
54
|
+
await injectHomeSkills(config.skillDirs, "antigravity");
|
|
55
|
+
} catch {
|
|
56
|
+
// Non-fatal
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return new AntigravitySession(ctx, binary, instructions);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* One `agy --input-format stream-json --output-format stream-json` process
|
|
64
|
+
* holds the conversation across turns. The CLI has no control protocol, so
|
|
65
|
+
* an interrupt is a SIGINT (Ctrl+C); when that ends the process, the next
|
|
66
|
+
* `send()` starts a new one on the same conversation with `--conversation`.
|
|
67
|
+
*/
|
|
68
|
+
class AntigravitySession implements AgentSession {
|
|
69
|
+
private _state: SessionState = "idle";
|
|
70
|
+
private conversationId: string | null;
|
|
71
|
+
private connection: AgyConnection | null = null;
|
|
72
|
+
private parser: AgyStreamParser | null = null;
|
|
73
|
+
private active: ActiveTurn | null = null;
|
|
74
|
+
/** Events received from agy processes, to tell whether one saw a message. */
|
|
75
|
+
private connectionEvents = 0;
|
|
76
|
+
private draining = false;
|
|
77
|
+
/** Instructions go into the first message of a conversation this session starts. */
|
|
78
|
+
private pendingInstructions: string | null;
|
|
79
|
+
private readonly cwd: string;
|
|
80
|
+
private readonly goals: GoalController;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
private readonly ctx: SessionContext,
|
|
84
|
+
private readonly binary: ResolvedBinary,
|
|
85
|
+
instructions: string | null,
|
|
86
|
+
) {
|
|
87
|
+
this.cwd = ctx.cwd ?? process.cwd();
|
|
88
|
+
this.conversationId = readAgyResumeId(ctx.sessionParams, this.cwd);
|
|
89
|
+
this.pendingInstructions = this.conversationId ? null : instructions;
|
|
90
|
+
this.goals = new GoalController({
|
|
91
|
+
providerType: ANTIGRAVITY_PROVIDER_TYPE,
|
|
92
|
+
capability: EMULATED_GOAL_CAPABILITY,
|
|
93
|
+
getSessionId: () => this.sessionId,
|
|
94
|
+
send: (message) => this.send(message),
|
|
95
|
+
dispatch: (event) => void this.dispatch(event),
|
|
96
|
+
});
|
|
97
|
+
if (ctx.signal) {
|
|
98
|
+
if (ctx.signal.aborted) void this.close();
|
|
99
|
+
else ctx.signal.addEventListener("abort", () => void this.close(), { once: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get sessionId(): string | null {
|
|
104
|
+
return this.parser?.conversationId ?? this.conversationId;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get state(): SessionState {
|
|
108
|
+
return this._state;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async send(message: string, options?: SendOptions): Promise<SendHandle> {
|
|
112
|
+
if (this._state === "closed") throw new Error("Session is closed");
|
|
113
|
+
if (this.draining) throw new Error("Session is draining — no new sends accepted");
|
|
114
|
+
if (this.active) {
|
|
115
|
+
throw new Error("Antigravity session is busy — a turn is already in progress (concurrentSend not supported)");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const uuid = uuidv7();
|
|
119
|
+
const turnId = uuidv7();
|
|
120
|
+
const abort = new AbortController();
|
|
121
|
+
const text = this.pendingInstructions ? `${this.pendingInstructions}\n\n${message}` : message;
|
|
122
|
+
this.pendingInstructions = null;
|
|
123
|
+
|
|
124
|
+
this._state = "thinking";
|
|
125
|
+
const done = this.runTurn(text, turnId, abort, options);
|
|
126
|
+
this.active = { turnId, abort, done };
|
|
127
|
+
void done.then((result) => this.goals.onTurnSettled(result)).catch(() => undefined);
|
|
128
|
+
return { uuid, result: done };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private ensureConnection(): AgyConnection {
|
|
132
|
+
if (this.connection?.alive) return this.connection;
|
|
133
|
+
const config = this.ctx.config ?? {};
|
|
134
|
+
// A replacement process picks the conversation back up where it stopped.
|
|
135
|
+
this.conversationId = this.sessionId;
|
|
136
|
+
const parser = new AgyStreamParser({
|
|
137
|
+
includePartialMessages: config.includePartialMessages,
|
|
138
|
+
conversationId: this.conversationId,
|
|
139
|
+
});
|
|
140
|
+
const env = buildEnv(this.ctx.env);
|
|
141
|
+
ensurePathInEnv(env);
|
|
142
|
+
this.ctx.onLifecycle?.({ phase: "spawning" });
|
|
143
|
+
const connection = new AgyConnection({
|
|
144
|
+
command: this.binary.bin,
|
|
145
|
+
args: [
|
|
146
|
+
...this.binary.prefixArgs,
|
|
147
|
+
...buildAgyArgs(config, { resumeId: this.conversationId, model: config.model ?? null }),
|
|
148
|
+
],
|
|
149
|
+
cwd: this.cwd,
|
|
150
|
+
env,
|
|
151
|
+
parser,
|
|
152
|
+
// Lines a replaced process prints on its way out belong to no turn.
|
|
153
|
+
onEvent: async (event): Promise<void> => {
|
|
154
|
+
if (this.connection === connection) await this.onEvent(event);
|
|
155
|
+
},
|
|
156
|
+
...(this.ctx.onOutput ? { onOutput: this.ctx.onOutput } : {}),
|
|
157
|
+
});
|
|
158
|
+
if (connection.pid != null) this.ctx.onLifecycle?.({ phase: "running", pid: connection.pid });
|
|
159
|
+
this.connection = connection;
|
|
160
|
+
this.parser = parser;
|
|
161
|
+
return connection;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async runTurn(
|
|
165
|
+
text: string,
|
|
166
|
+
turnId: string,
|
|
167
|
+
abort: AbortController,
|
|
168
|
+
options?: SendOptions,
|
|
169
|
+
): Promise<TurnResult> {
|
|
170
|
+
await this.emitTurnEdge({ type: "turn_start", turnId, trigger: "send" });
|
|
171
|
+
|
|
172
|
+
const timeoutSec = options?.timeoutSec ?? this.ctx.config?.timeoutSec;
|
|
173
|
+
const graceSec = this.ctx.config?.graceSec;
|
|
174
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
175
|
+
const guards: Promise<"timeout" | "aborted">[] = [
|
|
176
|
+
new Promise((resolve) => abort.signal.addEventListener("abort", () => resolve("aborted"), { once: true })),
|
|
177
|
+
];
|
|
178
|
+
if (timeoutSec && timeoutSec > 0) {
|
|
179
|
+
guards.push(new Promise((resolve) => { timer = setTimeout(() => resolve("timeout"), timeoutSec * 1000); }));
|
|
180
|
+
}
|
|
181
|
+
const onSendAbort = () => abort.abort();
|
|
182
|
+
if (options?.signal) {
|
|
183
|
+
if (options.signal.aborted) abort.abort();
|
|
184
|
+
else options.signal.addEventListener("abort", onSendAbort, { once: true });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let connection = this.ensureConnection();
|
|
188
|
+
try {
|
|
189
|
+
let result: TurnResult | null = null;
|
|
190
|
+
let reason: "result" | "cancelled" | "session_closed" = "result";
|
|
191
|
+
for (let attempt = 0; result === null; attempt++) {
|
|
192
|
+
const eventsBefore = this.connectionEvents;
|
|
193
|
+
const pending = connection.nextTurn();
|
|
194
|
+
connection.write(text);
|
|
195
|
+
const first = await Promise.race([pending, ...guards]);
|
|
196
|
+
if (first === "timeout" || first === "aborted") {
|
|
197
|
+
const outcome = await interruptAgyTurn(connection, pending, graceSec);
|
|
198
|
+
// agy may report INTERRUPTED and then exit. Learn which before the
|
|
199
|
+
// next send, so it never lands on a process that is going away.
|
|
200
|
+
if (outcome?.result) await connection.waitForExit(500);
|
|
201
|
+
// Without an INTERRUPTED result the process was stopped; a late result
|
|
202
|
+
// from it must never settle the next turn.
|
|
203
|
+
if ((!outcome?.result || !connection.alive) && this.connection === connection) this.connection = null;
|
|
204
|
+
result = {
|
|
205
|
+
summary: outcome?.result?.response.trim() || null,
|
|
206
|
+
...this.usageOf(outcome),
|
|
207
|
+
costUsd: null,
|
|
208
|
+
status: first,
|
|
209
|
+
errorCode: first,
|
|
210
|
+
errorMessage: first === "timeout" ? "Turn exceeded its timeout" : "Turn aborted",
|
|
211
|
+
};
|
|
212
|
+
reason = "cancelled";
|
|
213
|
+
} else if (
|
|
214
|
+
attempt === 0 && !first.result && first.exited && !first.authRequired
|
|
215
|
+
&& this.connectionEvents === eventsBefore && this._state !== "closed"
|
|
216
|
+
) {
|
|
217
|
+
// The process exited without ever acknowledging the message (it was
|
|
218
|
+
// already shutting down). Run it on a fresh process for the same
|
|
219
|
+
// conversation instead of failing a turn agy never started.
|
|
220
|
+
if (this.connection === connection) this.connection = null;
|
|
221
|
+
connection = this.ensureConnection();
|
|
222
|
+
} else {
|
|
223
|
+
result = this.toTurnResult(first);
|
|
224
|
+
reason = first.result ? "result" : "session_closed";
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
await this.emitTurnEdge({ type: "turn_end", turnId, trigger: "send", reason });
|
|
228
|
+
return result;
|
|
229
|
+
} finally {
|
|
230
|
+
if (timer) clearTimeout(timer);
|
|
231
|
+
options?.signal?.removeEventListener("abort", onSendAbort);
|
|
232
|
+
this.active = null;
|
|
233
|
+
if (!connection.alive && this.connection === connection) this.connection = null;
|
|
234
|
+
if (this._state !== "closed") this._state = "idle";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private usageOf(outcome: AgyTurnOutcome | null): { usage?: TurnResult["usage"] } {
|
|
239
|
+
const usage = agyUsageRecord(outcome?.result?.turnUsage ?? null, this.parser?.model ?? this.ctx.config?.model ?? null);
|
|
240
|
+
return usage ? { usage } : {};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private toTurnResult(outcome: AgyTurnOutcome): TurnResult {
|
|
244
|
+
const { result } = outcome;
|
|
245
|
+
if (result) {
|
|
246
|
+
const status = mapAgyTurnStatus(result.status);
|
|
247
|
+
if (status === "completed") {
|
|
248
|
+
return { summary: result.response.trim() || null, ...this.usageOf(outcome), costUsd: null, status, errorCode: null, errorMessage: null };
|
|
249
|
+
}
|
|
250
|
+
const errorCode = status === "aborted"
|
|
251
|
+
? "aborted"
|
|
252
|
+
: outcome.authRequired ? "auth_required" : classifyAgyFailure(result, outcome.agyError);
|
|
253
|
+
return {
|
|
254
|
+
summary: result.response.trim() || null,
|
|
255
|
+
...this.usageOf(outcome),
|
|
256
|
+
costUsd: null,
|
|
257
|
+
status,
|
|
258
|
+
errorCode,
|
|
259
|
+
errorMessage: errorCode === "auth_required"
|
|
260
|
+
? AGY_SIGN_IN_MESSAGE
|
|
261
|
+
: result.error ?? outcome.agyError?.message ?? `Antigravity turn ended with ${result.status}`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (outcome.authRequired) {
|
|
265
|
+
return { summary: null, costUsd: null, status: "failed", errorCode: "auth_required", errorMessage: AGY_SIGN_IN_MESSAGE };
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
summary: null,
|
|
269
|
+
costUsd: null,
|
|
270
|
+
status: "failed",
|
|
271
|
+
errorCode: "process_exited",
|
|
272
|
+
errorMessage: outcome.agyError?.message
|
|
273
|
+
?? firstAgyErrorLine(outcome.stderr)
|
|
274
|
+
?? `Antigravity CLI exited (code ${outcome.exitCode ?? "null"}${outcome.signal ? `, ${outcome.signal}` : ""}) before finishing the turn`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private async onEvent(event: StreamEvent): Promise<void> {
|
|
279
|
+
this.connectionEvents++;
|
|
280
|
+
if (this._state !== "closed") {
|
|
281
|
+
if (event.type === "tool_call") this._state = "tool_executing";
|
|
282
|
+
else if (event.type === "assistant" || event.type === "thinking" || event.type === "tool_result") this._state = "thinking";
|
|
283
|
+
}
|
|
284
|
+
await this.dispatch(event);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async emitTurnEdge(
|
|
288
|
+
edge:
|
|
289
|
+
| { type: "turn_start"; turnId: string; trigger: TurnTrigger }
|
|
290
|
+
| { type: "turn_end"; turnId: string; trigger: TurnTrigger; reason: "result" | "cancelled" | "session_closed" },
|
|
291
|
+
): Promise<void> {
|
|
292
|
+
await this.dispatch({
|
|
293
|
+
...edge,
|
|
294
|
+
timestamp: new Date().toISOString(),
|
|
295
|
+
providerType: ANTIGRAVITY_PROVIDER_TYPE,
|
|
296
|
+
sessionId: this.sessionId,
|
|
297
|
+
messageId: null,
|
|
298
|
+
eventId: null,
|
|
299
|
+
parentToolCallId: null,
|
|
300
|
+
raw: {},
|
|
301
|
+
} as StreamEvent);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private async dispatch(event: StreamEvent): Promise<void> {
|
|
305
|
+
if (!this.ctx.onEvent) return;
|
|
306
|
+
try {
|
|
307
|
+
await this.ctx.onEvent(event);
|
|
308
|
+
} catch {
|
|
309
|
+
/* a throwing handler must not break the stream */
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async cancel(_uuid: string): Promise<CancelResult> {
|
|
314
|
+
// One message per turn and no queue, so there is never a queued message to drop.
|
|
315
|
+
return { cancelled: false };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async stopTask(_taskId: string): Promise<StopTaskResult> {
|
|
319
|
+
return { stopped: false };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
setGoal(objective: string, options?: GoalOptions): Promise<SetGoalResult> {
|
|
323
|
+
return this.goals.setGoal(objective, options);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
clearGoal(options?: { reason?: "cleared" | "blocked" }): Promise<ClearGoalResult> {
|
|
327
|
+
return this.goals.clearGoal(options);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getGoal(): GoalState | null {
|
|
331
|
+
return this.goals.getGoal();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async interrupt(): Promise<void> {
|
|
335
|
+
const active = this.active;
|
|
336
|
+
if (!active) return;
|
|
337
|
+
active.abort.abort();
|
|
338
|
+
await active.done.catch(() => undefined);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async drain(): Promise<void> {
|
|
342
|
+
this.draining = true;
|
|
343
|
+
if (this.active) await this.active.done.catch(() => undefined);
|
|
344
|
+
await this.close();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async close(): Promise<void> {
|
|
348
|
+
if (this._state === "closed" && !this.connection) return;
|
|
349
|
+
this._state = "closed";
|
|
350
|
+
const active = this.active;
|
|
351
|
+
if (active) {
|
|
352
|
+
active.abort.abort();
|
|
353
|
+
await active.done.catch(() => undefined);
|
|
354
|
+
}
|
|
355
|
+
const connection = this.connection;
|
|
356
|
+
this.connection = null;
|
|
357
|
+
if (connection) await connection.terminate(this.ctx.config?.graceSec);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createReadStream } from "node:fs";
|
|
2
2
|
import { stat } from "node:fs/promises";
|
|
3
|
-
import * as readline from "node:readline";
|
|
4
3
|
import type {
|
|
5
4
|
AttachOptions,
|
|
6
5
|
CatchUpOptions,
|
|
@@ -16,6 +15,7 @@ import {
|
|
|
16
15
|
createSessionRecord,
|
|
17
16
|
MalformedSessionRecordError,
|
|
18
17
|
} from "../../sessions/record.js";
|
|
18
|
+
import { readJsonlLines } from "../../utils/jsonl-lines.js";
|
|
19
19
|
import { getRuntimeHomeEnvVar } from "../../utils/runtime-homes.js";
|
|
20
20
|
import { claudeSessionCodec } from "./codec.js";
|
|
21
21
|
import {
|
|
@@ -86,16 +86,15 @@ const EMPTY: AsyncIterable<CatchUpYield> = {
|
|
|
86
86
|
* preceding completed one.
|
|
87
87
|
*/
|
|
88
88
|
async function classifyLastTurn(filePath: string): Promise<LastTurnStatus> {
|
|
89
|
-
const stream = createReadStream(filePath
|
|
89
|
+
const stream = createReadStream(filePath);
|
|
90
90
|
stream.on("error", () => {});
|
|
91
|
-
const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
92
91
|
let lastTurn: LastTurnStatus = "unknown";
|
|
93
92
|
|
|
94
93
|
try {
|
|
95
|
-
for await (const line of
|
|
94
|
+
for await (const line of readJsonlLines(stream)) {
|
|
96
95
|
let raw: unknown;
|
|
97
96
|
try {
|
|
98
|
-
raw = JSON.parse(line);
|
|
97
|
+
raw = JSON.parse(line.text);
|
|
99
98
|
} catch {
|
|
100
99
|
continue;
|
|
101
100
|
}
|
|
@@ -109,7 +108,6 @@ async function classifyLastTurn(filePath: string): Promise<LastTurnStatus> {
|
|
|
109
108
|
if (fsError?.code !== "ENOENT") throw error;
|
|
110
109
|
return "unknown";
|
|
111
110
|
} finally {
|
|
112
|
-
lines.close();
|
|
113
111
|
stream.destroy();
|
|
114
112
|
}
|
|
115
113
|
|
|
@@ -23,8 +23,8 @@ import { createReadStream } from "node:fs";
|
|
|
23
23
|
import { readdir, realpath, stat, open as fsOpen } from "node:fs/promises";
|
|
24
24
|
import * as os from "node:os";
|
|
25
25
|
import * as path from "node:path";
|
|
26
|
-
import * as readline from "node:readline";
|
|
27
26
|
|
|
27
|
+
import { readJsonlLines } from "../../utils/jsonl-lines.js";
|
|
28
28
|
import { getDefaultRuntimeHome, getRuntimeHomeEnvVar } from "../../utils/runtime-homes.js";
|
|
29
29
|
import type { FoundTranscript, StreamEvent, TranscriptOps } from "../../types.js";
|
|
30
30
|
import { parseStreamLine } from "./parse.js";
|
|
@@ -273,13 +273,12 @@ async function readCwdFromTranscript(filePath: string): Promise<string | null> {
|
|
|
273
273
|
if (!fh) return null;
|
|
274
274
|
|
|
275
275
|
try {
|
|
276
|
-
const stream = fh.createReadStream({
|
|
277
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
276
|
+
const stream = fh.createReadStream({ autoClose: false });
|
|
278
277
|
let count = 0;
|
|
279
278
|
try {
|
|
280
|
-
for await (const
|
|
279
|
+
for await (const line of readJsonlLines(stream)) {
|
|
281
280
|
if (++count > 50) break;
|
|
282
|
-
const trimmed =
|
|
281
|
+
const trimmed = line.text.trim();
|
|
283
282
|
if (!trimmed) continue;
|
|
284
283
|
try {
|
|
285
284
|
const obj = JSON.parse(trimmed) as Record<string, unknown>;
|
|
@@ -300,7 +299,6 @@ async function readCwdFromTranscript(filePath: string): Promise<string | null> {
|
|
|
300
299
|
}
|
|
301
300
|
}
|
|
302
301
|
} finally {
|
|
303
|
-
rl.close();
|
|
304
302
|
stream.destroy();
|
|
305
303
|
}
|
|
306
304
|
} finally {
|
|
@@ -377,26 +375,16 @@ export async function* readClaudeTranscript(
|
|
|
377
375
|
|
|
378
376
|
const stream = createReadStream(filePath, { start: fromOffset, encoding: undefined });
|
|
379
377
|
|
|
380
|
-
// Suppress stray ENOENT (file deleted between stat and open) —
|
|
378
|
+
// Suppress stray ENOENT (file deleted between stat and open) — the stream
|
|
381
379
|
// raises the same condition through its own iterator, which we catch below.
|
|
382
380
|
stream.on("error", () => {});
|
|
383
381
|
|
|
384
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
385
|
-
|
|
386
|
-
let pos = fromOffset;
|
|
387
382
|
let stillSkippingPastSince = !!sinceEventId;
|
|
388
383
|
|
|
389
384
|
try {
|
|
390
|
-
for await (const line of
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
// would yield offsets 1 byte short per line — accepted as a corner
|
|
394
|
-
// case; resume from such an offset would skip one stray `\r`.
|
|
395
|
-
const lineByteLen = Buffer.byteLength(line, "utf8");
|
|
396
|
-
pos += lineByteLen + 1;
|
|
397
|
-
|
|
398
|
-
if (!line) continue;
|
|
399
|
-
const trimmed = line.trim();
|
|
385
|
+
for await (const line of readJsonlLines(stream, fromOffset)) {
|
|
386
|
+
if (!line.text) continue;
|
|
387
|
+
const trimmed = line.text.trim();
|
|
400
388
|
if (!trimmed) continue;
|
|
401
389
|
|
|
402
390
|
if (looksLikeSkippedType(trimmed)) continue;
|
|
@@ -413,7 +401,7 @@ export async function* readClaudeTranscript(
|
|
|
413
401
|
}
|
|
414
402
|
|
|
415
403
|
for (const event of events) {
|
|
416
|
-
yield { event, offset:
|
|
404
|
+
yield { event, offset: line.end };
|
|
417
405
|
}
|
|
418
406
|
}
|
|
419
407
|
} catch (err) {
|
|
@@ -421,7 +409,6 @@ export async function* readClaudeTranscript(
|
|
|
421
409
|
const e = err as NodeJS.ErrnoException;
|
|
422
410
|
if (e?.code !== "ENOENT") throw err;
|
|
423
411
|
} finally {
|
|
424
|
-
rl.close();
|
|
425
412
|
stream.destroy();
|
|
426
413
|
}
|
|
427
414
|
}
|
|
@@ -132,6 +132,10 @@ export async function attachCodexSession(
|
|
|
132
132
|
if (transcript) {
|
|
133
133
|
const lastEvent = await latestTurnBoundary(transcript.filePath, sessionId);
|
|
134
134
|
if (lastEvent === null) lastTurn = "unknown";
|
|
135
|
+
// turn_aborted replays as an interrupted result, but the turn never
|
|
136
|
+
// finished, so it stays "interrupted" for hosts deciding whether to re-send.
|
|
137
|
+
else if (lastEvent.type === "event_msg" && lastEvent.payload?.["type"] === "turn_aborted")
|
|
138
|
+
lastTurn = "interrupted";
|
|
135
139
|
else if (codexLineToStreamEvents(lastEvent, { sessionId }).some((event) => event.type === "result"))
|
|
136
140
|
lastTurn = "completed";
|
|
137
141
|
else lastTurn = "interrupted";
|
|
@@ -57,6 +57,17 @@ function codexUserText(line: CodexTranscriptLine): string | null {
|
|
|
57
57
|
if (line.type === "event_msg" && line.payload?.["type"] === "user_message") {
|
|
58
58
|
return meaningfulHumanText(asString(line.payload["message"]));
|
|
59
59
|
}
|
|
60
|
+
// Paginated rollouts (`session_meta.history_mode: "paginated"`, written by
|
|
61
|
+
// Codex 0.142 and later) never write `user_message`. What the person typed
|
|
62
|
+
// is a completed `UserMessage` item instead, and legacy rollouts never
|
|
63
|
+
// persist that item, so each message is read once. The `response_item` user
|
|
64
|
+
// message beside it also carries injected context, so it is not the one read.
|
|
65
|
+
if (line.type === "event_msg" && line.payload?.["type"] === "item_completed") {
|
|
66
|
+
const item = asRecord(line.payload["item"]);
|
|
67
|
+
if (item?.["type"] === "UserMessage") {
|
|
68
|
+
return meaningfulHumanText(textFromContent(item["content"], new Set(["text", "input_text"])));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
60
71
|
// Older unwrapped rollouts have no event_msg mirror.
|
|
61
72
|
if (line.type === "message" && line.raw["role"] === "user") {
|
|
62
73
|
return meaningfulHumanText(textFromContent(
|
|
@@ -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;
|