@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +484 -1
  2. package/README.md +23 -3
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/providers/claude/parse.d.ts +3 -0
  7. package/dist/providers/claude/parse.d.ts.map +1 -1
  8. package/dist/providers/claude/parse.js +23 -1
  9. package/dist/providers/claude/parse.js.map +1 -1
  10. package/dist/providers/claude/session.d.ts +174 -4
  11. package/dist/providers/claude/session.d.ts.map +1 -1
  12. package/dist/providers/claude/session.js +541 -29
  13. package/dist/providers/claude/session.js.map +1 -1
  14. package/dist/providers/codex/execute.d.ts.map +1 -1
  15. package/dist/providers/codex/execute.js +3 -0
  16. package/dist/providers/codex/execute.js.map +1 -1
  17. package/dist/providers/codex/parse.d.ts +12 -1
  18. package/dist/providers/codex/parse.d.ts.map +1 -1
  19. package/dist/providers/codex/parse.js +21 -0
  20. package/dist/providers/codex/parse.js.map +1 -1
  21. package/dist/providers/codex/session.d.ts.map +1 -1
  22. package/dist/providers/codex/session.js +21 -1
  23. package/dist/providers/codex/session.js.map +1 -1
  24. package/dist/providers/opencode/event-parse.d.ts +8 -2
  25. package/dist/providers/opencode/event-parse.d.ts.map +1 -1
  26. package/dist/providers/opencode/event-parse.js +22 -11
  27. package/dist/providers/opencode/event-parse.js.map +1 -1
  28. package/dist/providers/opencode/http-session.js +13 -0
  29. package/dist/providers/opencode/http-session.js.map +1 -1
  30. package/dist/types.d.ts +95 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/index.ts +2 -0
  34. package/src/providers/claude/parse.ts +23 -1
  35. package/src/providers/claude/session.ts +556 -30
  36. package/src/providers/codex/execute.ts +3 -0
  37. package/src/providers/codex/parse.ts +25 -0
  38. package/src/providers/codex/session.ts +24 -1
  39. package/src/providers/opencode/event-parse.ts +27 -13
  40. package/src/providers/opencode/http-session.ts +13 -0
  41. 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 lands in the error branch below.
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 status = turnStatusFromMessage(info);
98
- if (status === "failed") {
99
- const m = rec(info);
100
- return {
101
- status,
102
- errorCode: "agent_error",
103
- errorMessage: JSON.stringify(m?.["error"] ?? null),
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
- const finish = str(rec(info)?.["finish"] ?? null);
108
- const cleanStop = finish !== null && CLEAN_FINISH_REASONS.has(finish);
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";