@agentex/agent 0.0.35 → 0.0.37
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 +484 -1
- package/README.md +23 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/parse.d.ts +3 -0
- package/dist/providers/claude/parse.d.ts.map +1 -1
- package/dist/providers/claude/parse.js +23 -1
- package/dist/providers/claude/parse.js.map +1 -1
- package/dist/providers/claude/session.d.ts +174 -4
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +541 -29
- 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 +3 -0
- package/dist/providers/codex/execute.js.map +1 -1
- package/dist/providers/codex/parse.d.ts +12 -1
- package/dist/providers/codex/parse.d.ts.map +1 -1
- package/dist/providers/codex/parse.js +21 -0
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +21 -1
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/opencode/event-parse.d.ts +8 -2
- package/dist/providers/opencode/event-parse.d.ts.map +1 -1
- package/dist/providers/opencode/event-parse.js +22 -11
- package/dist/providers/opencode/event-parse.js.map +1 -1
- package/dist/providers/opencode/http-session.js +13 -0
- package/dist/providers/opencode/http-session.js.map +1 -1
- package/dist/types.d.ts +95 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/providers/claude/parse.ts +23 -1
- package/src/providers/claude/session.ts +556 -30
- package/src/providers/codex/execute.ts +3 -0
- package/src/providers/codex/parse.ts +25 -0
- package/src/providers/codex/session.ts +24 -1
- package/src/providers/opencode/event-parse.ts +27 -13
- package/src/providers/opencode/http-session.ts +13 -0
- package/src/types.ts +97 -0
|
@@ -250,6 +250,9 @@ export async function executeCodexProvider(ctx: ExecutionContext): Promise<Execu
|
|
|
250
250
|
status: "stopped",
|
|
251
251
|
description: task.description,
|
|
252
252
|
summary: null,
|
|
253
|
+
// Cut short, so there is no result to hand back.
|
|
254
|
+
toolUseId: null,
|
|
255
|
+
report: null,
|
|
253
256
|
parentTaskId: task.parentTaskId,
|
|
254
257
|
timestamp: new Date().toISOString(),
|
|
255
258
|
providerType: "codex",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
BackgroundTaskReport,
|
|
2
3
|
BaseStreamEventFields,
|
|
3
4
|
GoalSource,
|
|
4
5
|
StreamEvent,
|
|
@@ -70,6 +71,26 @@ function parseStringArray(value: unknown): string[] {
|
|
|
70
71
|
: [];
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The delivered-result payload for a background task, or null.
|
|
76
|
+
*
|
|
77
|
+
* One rule for every Codex emitter: a child that reached a terminal outcome
|
|
78
|
+
* handed something back; one that was cut short did not. Defined here rather
|
|
79
|
+
* than at each construction site because there are five of them and four
|
|
80
|
+
* previously forgot, so a host keying on `report !== null` — which the README
|
|
81
|
+
* tells it to do — rendered no row for completions that reached it by the
|
|
82
|
+
* reconcile path. Mirrors the Claude gate in `claude/parse.ts`.
|
|
83
|
+
*/
|
|
84
|
+
export function codexBackgroundTaskReport(
|
|
85
|
+
phase: "started" | "progress" | "completed",
|
|
86
|
+
status: "pending" | "running" | "paused" | "completed" | "failed" | "stopped" | null,
|
|
87
|
+
summary: string | null,
|
|
88
|
+
): BackgroundTaskReport | null {
|
|
89
|
+
if (phase !== "completed") return null;
|
|
90
|
+
if (status !== "completed" && status !== "failed") return null;
|
|
91
|
+
return { summary, outputFile: null, usage: null };
|
|
92
|
+
}
|
|
93
|
+
|
|
73
94
|
function codexBackgroundTaskState(
|
|
74
95
|
value: unknown,
|
|
75
96
|
): { phase: "started" | "completed"; status: "running" | "completed" | "failed" | "stopped"; summary: string | null } {
|
|
@@ -454,6 +475,8 @@ function parseV2Notification(event: Record<string, unknown>): StreamEvent | Stre
|
|
|
454
475
|
description: asNullableString(item["agentPath"]),
|
|
455
476
|
summary: null,
|
|
456
477
|
parentTaskId: null,
|
|
478
|
+
toolUseId: null,
|
|
479
|
+
report: null,
|
|
457
480
|
...base,
|
|
458
481
|
};
|
|
459
482
|
}
|
|
@@ -491,6 +514,8 @@ function parseV2Notification(event: Record<string, unknown>): StreamEvent | Stre
|
|
|
491
514
|
description,
|
|
492
515
|
summary: state.summary,
|
|
493
516
|
parentTaskId: null,
|
|
517
|
+
toolUseId: null,
|
|
518
|
+
report: codexBackgroundTaskReport(state.phase, state.status, state.summary),
|
|
494
519
|
...base,
|
|
495
520
|
};
|
|
496
521
|
});
|
|
@@ -2,6 +2,7 @@ import type { ChildProcess } from "node:child_process";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import type {
|
|
5
|
+
BackgroundTaskReport,
|
|
5
6
|
AgentSession,
|
|
6
7
|
CancelResult,
|
|
7
8
|
ClearGoalResult,
|
|
@@ -27,7 +28,7 @@ import { translateEndpoint } from "../../utils/endpoint.js";
|
|
|
27
28
|
import { injectWorkspaceSkills } from "../../utils/skills.js";
|
|
28
29
|
import { resolveInstructions } from "../../utils/instructions.js";
|
|
29
30
|
import { createToolNameTracker } from "../../utils/tool-names.js";
|
|
30
|
-
import { parseCodexStreamLines } from "./parse.js";
|
|
31
|
+
import { parseCodexStreamLines, codexBackgroundTaskReport } from "./parse.js";
|
|
31
32
|
import { withPlanModePreamble } from "./plan-mode.js";
|
|
32
33
|
import { scanCodexSessionUsage } from "./usage-scanner.js";
|
|
33
34
|
import { codexSessionCodec } from "./codec.js";
|
|
@@ -1448,6 +1449,8 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1448
1449
|
parentTaskId?: string | null;
|
|
1449
1450
|
turnId?: string | null;
|
|
1450
1451
|
eventId?: string | null;
|
|
1452
|
+
/** Set on the edge that actually hands the child's result back. */
|
|
1453
|
+
report?: BackgroundTaskReport | null;
|
|
1451
1454
|
raw: Record<string, unknown>;
|
|
1452
1455
|
},
|
|
1453
1456
|
): Extract<StreamEvent, { type: "background_task" }> {
|
|
@@ -1461,6 +1464,16 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1461
1464
|
description: options.description ?? null,
|
|
1462
1465
|
summary: options.summary ?? null,
|
|
1463
1466
|
parentTaskId: options.parentTaskId ?? null,
|
|
1467
|
+
// Codex identifies children by thread id, not by the id of the
|
|
1468
|
+
// `spawn_agent` call that created them, so there is no tool-call link to
|
|
1469
|
+
// report here.
|
|
1470
|
+
toolUseId: null,
|
|
1471
|
+
// Derived, not passed. Four of the five emitters that reach this helper
|
|
1472
|
+
// never set it, and the omission was invisible until a host asked why
|
|
1473
|
+
// reconciled completions produced no row.
|
|
1474
|
+
report: options.report !== undefined
|
|
1475
|
+
? options.report
|
|
1476
|
+
: codexBackgroundTaskReport(phase, status, options.summary ?? null),
|
|
1464
1477
|
timestamp: new Date().toISOString(),
|
|
1465
1478
|
providerType: "codex",
|
|
1466
1479
|
sessionId: rootThreadId,
|
|
@@ -1833,6 +1846,8 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1833
1846
|
status: "running",
|
|
1834
1847
|
description,
|
|
1835
1848
|
summary: null,
|
|
1849
|
+
toolUseId: null,
|
|
1850
|
+
report: null,
|
|
1836
1851
|
parentTaskId: parentThreadId === rootThreadId ? null : parentThreadId,
|
|
1837
1852
|
timestamp: new Date().toISOString(),
|
|
1838
1853
|
providerType: "codex",
|
|
@@ -1867,6 +1882,8 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1867
1882
|
status: "running",
|
|
1868
1883
|
description: task.description,
|
|
1869
1884
|
summary: null,
|
|
1885
|
+
toolUseId: null,
|
|
1886
|
+
report: null,
|
|
1870
1887
|
parentTaskId: task.parentTaskId,
|
|
1871
1888
|
timestamp: new Date().toISOString(),
|
|
1872
1889
|
providerType: "codex",
|
|
@@ -1920,6 +1937,12 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1920
1937
|
description: task.description,
|
|
1921
1938
|
summary: task.summary ?? (errorMessage || null),
|
|
1922
1939
|
parentTaskId: task.parentTaskId,
|
|
1940
|
+
toolUseId: null,
|
|
1941
|
+
// The child's turn ended and its result is being handed back — the same
|
|
1942
|
+
// edge Claude expresses as `task_notification`. Same rule as every other
|
|
1943
|
+
// emitter, so a host can render "one row per delivered result" across
|
|
1944
|
+
// providers instead of special-casing each one.
|
|
1945
|
+
report: codexBackgroundTaskReport("completed", status, task.summary ?? (errorMessage || null)),
|
|
1923
1946
|
timestamp: new Date().toISOString(),
|
|
1924
1947
|
providerType: "codex",
|
|
1925
1948
|
sessionId: rootThreadId,
|
|
@@ -73,7 +73,7 @@ export function incompleteTurnMessage(finish: string | null): string {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export interface TerminalOutcome {
|
|
76
|
-
status: "completed" | "failed";
|
|
76
|
+
status: "completed" | "failed" | "aborted";
|
|
77
77
|
errorCode: string | null;
|
|
78
78
|
errorMessage: string | null;
|
|
79
79
|
/**
|
|
@@ -81,7 +81,7 @@ export interface TerminalOutcome {
|
|
|
81
81
|
* "silent empty turn" opencode records as a normal completion (typically after
|
|
82
82
|
* an upstream stream error, surfaced as `finish: "unknown"`). Distinct from a
|
|
83
83
|
* user interrupt, which opencode marks with a `MessageAbortedError` in
|
|
84
|
-
* `info.error` and so
|
|
84
|
+
* `info.error` and so reports as `aborted` below.
|
|
85
85
|
*/
|
|
86
86
|
incomplete: boolean;
|
|
87
87
|
}
|
|
@@ -92,20 +92,34 @@ export interface TerminalOutcome {
|
|
|
92
92
|
* visible note) instead of rendering as a stall. Keyed on the message having no
|
|
93
93
|
* visible text and a non-clean finish reason; never fires when the model
|
|
94
94
|
* produced an answer or stopped cleanly, and never fires on an interrupt.
|
|
95
|
+
*
|
|
96
|
+
* Both the live and reconcile paths call this, so the "finished" guard lives
|
|
97
|
+
* here: a message with neither a finish reason nor an error is still running (or
|
|
98
|
+
* a malformed response) and stays `completed`. Without it, an empty `/message`
|
|
99
|
+
* body would flip to `failed` with no note — the exact silent stall this exists
|
|
100
|
+
* to remove, reached from the other direction.
|
|
95
101
|
*/
|
|
96
102
|
export function terminalOutcome(info: unknown, hasVisibleText: boolean): TerminalOutcome {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
incomplete: false,
|
|
105
|
-
};
|
|
103
|
+
const m = rec(info);
|
|
104
|
+
const rawError = m?.["error"] ?? null;
|
|
105
|
+
const finish = m ? str(m["finish"] ?? null) : null;
|
|
106
|
+
|
|
107
|
+
// Not a finished turn (no finish reason, no error): don't reclassify.
|
|
108
|
+
if (rawError == null && finish === null) {
|
|
109
|
+
return { status: "completed", errorCode: null, errorMessage: null, incomplete: false };
|
|
106
110
|
}
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
|
|
112
|
+
if (rawError != null) {
|
|
113
|
+
// A user interrupt (through opencode's own UI, on a session this process is
|
|
114
|
+
// attached to) is recorded as a MessageAbortedError. Report it as `aborted`
|
|
115
|
+
// to match self-aborts and Codex, not a generic error with a JSON blob.
|
|
116
|
+
if (rec(rawError)?.["name"] === "MessageAbortedError") {
|
|
117
|
+
return { status: "aborted", errorCode: "aborted", errorMessage: "The turn was interrupted.", incomplete: false };
|
|
118
|
+
}
|
|
119
|
+
return { status: "failed", errorCode: "agent_error", errorMessage: JSON.stringify(rawError), incomplete: false };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cleanStop = CLEAN_FINISH_REASONS.has(finish!);
|
|
109
123
|
if (!hasVisibleText && !cleanStop) {
|
|
110
124
|
return {
|
|
111
125
|
status: "failed",
|
|
@@ -491,6 +491,12 @@ class OpenCodeSession implements AgentSession {
|
|
|
491
491
|
private finishTurn(): void {
|
|
492
492
|
this._turnActive = false;
|
|
493
493
|
this._inFlight = null;
|
|
494
|
+
// Cleared at turn end, not with the sibling dedup maps at turn start: the
|
|
495
|
+
// user message's role has to survive from its `message.updated` frame into
|
|
496
|
+
// the part stream that follows within the same turn, so clearing at the
|
|
497
|
+
// start would reintroduce the prompt echo. Bounding it here keeps the map
|
|
498
|
+
// from growing for the whole life of a long-running session.
|
|
499
|
+
this._messageRoles.clear();
|
|
494
500
|
if (this._state !== "closed") this._state = "idle";
|
|
495
501
|
}
|
|
496
502
|
|
|
@@ -615,6 +621,13 @@ class OpenCodeSession implements AgentSession {
|
|
|
615
621
|
* stable `:incomplete` event id is shared with the reconcile path
|
|
616
622
|
* (`historicalEvents`) so a later catch-up dedups against this live note
|
|
617
623
|
* instead of duplicating it.
|
|
624
|
+
*
|
|
625
|
+
* This is library-authored prose, not model output. It is emitted as
|
|
626
|
+
* `type: "assistant"` because that is the only surface a host renders, and is
|
|
627
|
+
* tagged `raw.synthetic: "incomplete_turn"`. A host that replays transcript
|
|
628
|
+
* history back into a model (context rebuilding, summarization) should filter
|
|
629
|
+
* events carrying `raw.synthetic` so this note is never fed back as assistant
|
|
630
|
+
* turn content.
|
|
618
631
|
*/
|
|
619
632
|
private async emitIncompleteNote(info: Record<string, unknown>): Promise<void> {
|
|
620
633
|
const messageId = str(info["id"]);
|
package/src/types.ts
CHANGED
|
@@ -998,6 +998,48 @@ export type BackgroundTaskType = "subagent" | "process" | "unknown";
|
|
|
998
998
|
/** Lifecycle edge represented by one `background_task` StreamEvent. */
|
|
999
999
|
export type BackgroundTaskPhase = "started" | "progress" | "completed";
|
|
1000
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* A background task's delivered output.
|
|
1003
|
+
*
|
|
1004
|
+
* Present only on the event where the provider actually *hands the result
|
|
1005
|
+
* back* — distinct from a state patch that merely says the task reached a
|
|
1006
|
+
* terminal status. Claude emits both for one completion (`task_updated` then
|
|
1007
|
+
* `task_notification`), and they are different records, not duplicates: only
|
|
1008
|
+
* the delivery carries the summary, the output file, and the `toolUseId`
|
|
1009
|
+
* linking the task to the call that launched it.
|
|
1010
|
+
*
|
|
1011
|
+
* Collapsing the two into one indistinguishable "completed" event is what
|
|
1012
|
+
* made hosts render every finished task twice: once with its report and once
|
|
1013
|
+
* with nothing. Only the delivery carries the summary and the output file
|
|
1014
|
+
* (`toolUseId` is also on the task's `started` record).
|
|
1015
|
+
*
|
|
1016
|
+
* Absent on a task that was cut short — a stop or a kill delivers no result.
|
|
1017
|
+
*/
|
|
1018
|
+
export interface BackgroundTaskReport {
|
|
1019
|
+
/** The task's final output as the provider summarized it. */
|
|
1020
|
+
summary: string | null;
|
|
1021
|
+
/** Path to the task's full transcript/output on disk, when the provider writes one. */
|
|
1022
|
+
outputFile: string | null;
|
|
1023
|
+
/** What the task consumed, when reported. */
|
|
1024
|
+
usage: {
|
|
1025
|
+
totalTokens: number | null;
|
|
1026
|
+
toolUses: number | null;
|
|
1027
|
+
durationMs: number | null;
|
|
1028
|
+
} | null;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Why a turn began.
|
|
1033
|
+
*
|
|
1034
|
+
* - `send` — dispatched through `send()`. Usually the host; agentex's own
|
|
1035
|
+
* emulated goal loop also continues a session this way, so this means
|
|
1036
|
+
* "someone called send()", not strictly "the user".
|
|
1037
|
+
* - `resume` — the provider started it on its own. Claude does this when a
|
|
1038
|
+
* background task finishes: it enqueues a task-notification as user input,
|
|
1039
|
+
* which opens a fresh turn with no host involvement.
|
|
1040
|
+
*/
|
|
1041
|
+
export type TurnTrigger = "send" | "resume";
|
|
1042
|
+
|
|
1001
1043
|
/** Current normalized state carried by a `background_task` event. */
|
|
1002
1044
|
export type BackgroundTaskStatus =
|
|
1003
1045
|
| "pending"
|
|
@@ -1229,6 +1271,61 @@ export type StreamEvent =
|
|
|
1229
1271
|
description: string | null;
|
|
1230
1272
|
summary: string | null;
|
|
1231
1273
|
parentTaskId: string | null;
|
|
1274
|
+
/**
|
|
1275
|
+
* The `tool_call` id that launched this task, when the provider reports
|
|
1276
|
+
* it. This is the structured link between a task and the tool call it
|
|
1277
|
+
* came from — the same id Claude writes into a subagent's `meta.json`.
|
|
1278
|
+
*/
|
|
1279
|
+
toolUseId: string | null;
|
|
1280
|
+
/**
|
|
1281
|
+
* The task's delivered output, on the event that delivers it, else null.
|
|
1282
|
+
* A terminal `status` says the task finished; a non-null `report` says
|
|
1283
|
+
* its result is being handed back. See `BackgroundTaskReport`.
|
|
1284
|
+
*/
|
|
1285
|
+
report: BackgroundTaskReport | null;
|
|
1286
|
+
} & BaseStreamEventFields)
|
|
1287
|
+
/**
|
|
1288
|
+
* A turn opened. Pairs with `result`, which closes one.
|
|
1289
|
+
*
|
|
1290
|
+
* Hosts that track "is the agent working" cannot derive it from their own
|
|
1291
|
+
* dispatch alone, because not every turn is theirs: when a background task
|
|
1292
|
+
* finishes, Claude enqueues a notification as user input and starts a turn
|
|
1293
|
+
* by itself. A host keying off its own `send()` sees that turn as idle and
|
|
1294
|
+
* reports the session as finished while it is visibly working.
|
|
1295
|
+
*
|
|
1296
|
+
* Emitted for host-initiated turns too, so `turn_start` → `result` describes
|
|
1297
|
+
* turn liveness straight off the stream.
|
|
1298
|
+
*
|
|
1299
|
+
* Deliberately does not name the background task behind a `resume`. Claude
|
|
1300
|
+
* delivers a task's result and opens the turn as two unlinked records, and
|
|
1301
|
+
* with several tasks in flight the pairing is not recoverable from the wire
|
|
1302
|
+
* — every attempt to infer it produced a plausible id that was sometimes
|
|
1303
|
+
* simply wrong. Correlate through `background_task.report` and `toolUseId`,
|
|
1304
|
+
* which the provider does state.
|
|
1305
|
+
*/
|
|
1306
|
+
| ({
|
|
1307
|
+
type: "turn_start";
|
|
1308
|
+
turnId: string;
|
|
1309
|
+
trigger: TurnTrigger;
|
|
1310
|
+
} & BaseStreamEventFields)
|
|
1311
|
+
/**
|
|
1312
|
+
* A turn closed. Every `turn_start` is followed by exactly one of these.
|
|
1313
|
+
*
|
|
1314
|
+
* `result` cannot serve as the close signal on its own: a message the CLI
|
|
1315
|
+
* cancels, discards, or refuses opens a turn and produces no result at all,
|
|
1316
|
+
* so a host tracking `turn_start` → `result` would stay busy forever. This
|
|
1317
|
+
* is the guaranteed counterpart; `result` remains the outcome payload.
|
|
1318
|
+
*/
|
|
1319
|
+
| ({
|
|
1320
|
+
type: "turn_end";
|
|
1321
|
+
turnId: string;
|
|
1322
|
+
trigger: TurnTrigger;
|
|
1323
|
+
/**
|
|
1324
|
+
* Why the turn ended. `result` means a normal completion whose payload
|
|
1325
|
+
* arrives as the accompanying `result` event; the rest are CLI verdicts
|
|
1326
|
+
* on the message that produced no result.
|
|
1327
|
+
*/
|
|
1328
|
+
reason: "result" | "cancelled" | "discarded" | "refused" | "session_closed";
|
|
1232
1329
|
} & BaseStreamEventFields)
|
|
1233
1330
|
| ({
|
|
1234
1331
|
type: "result";
|