@dudousxd/nestjs-agent-opencode 0.4.0 → 0.5.0

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/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ var index_exports = {};
23
23
  __export(index_exports, {
24
24
  FormAnswerError: () => FormAnswerError,
25
25
  InMemoryOpenCodeSessionStore: () => InMemoryOpenCodeSessionStore,
26
+ LOST_ANSWER_ERROR: () => LOST_ANSWER_ERROR,
26
27
  OPENCODE_HOST: () => OPENCODE_HOST,
27
28
  OPENCODE_MCP_ENDPOINT: () => OPENCODE_MCP_ENDPOINT,
28
29
  OPENCODE_OPTIONS: () => OPENCODE_OPTIONS,
@@ -326,6 +327,9 @@ function subUsage(a, b) {
326
327
  };
327
328
  }
328
329
  __name(subUsage, "subUsage");
330
+ var LOST_ANSWER_ERROR = "The answer was lost while the agent restarted; ask again.";
331
+ var RECOVER_PAGES = 10;
332
+ var RECOVER_PAGE_SIZE = 100;
329
333
  var ASK_TOOL_NAME = "ask";
330
334
  var FAILED_STATUSES = /* @__PURE__ */ new Set([
331
335
  "error",
@@ -414,6 +418,14 @@ var OpenCodeTurn = class {
414
418
  wroteMessage = false;
415
419
  ended = false;
416
420
  stepError;
421
+ /** What this turn object took from the session's assistant messages (see {@link TurnProgress}). */
422
+ took = {
423
+ text: /* @__PURE__ */ new Map(),
424
+ reasoning: /* @__PURE__ */ new Map(),
425
+ called: /* @__PURE__ */ new Set(),
426
+ settled: /* @__PURE__ */ new Set(),
427
+ steps: /* @__PURE__ */ new Set()
428
+ };
417
429
  constructor(a) {
418
430
  this.a = a;
419
431
  this.model = modelOfLabel(a.input.model);
@@ -431,6 +443,30 @@ var OpenCodeTurn = class {
431
443
  tokens: info.tokens
432
444
  };
433
445
  }
446
+ /** What the turn took so far: carried from before this turn object, plus what it took. */
447
+ progress() {
448
+ const before = this.a.progress;
449
+ const merged = /* @__PURE__ */ __name((carried, own) => {
450
+ const out = {
451
+ ...carried ?? {}
452
+ };
453
+ for (const [id, n] of own) out[id] = (out[id] ?? 0) + n;
454
+ return out;
455
+ }, "merged");
456
+ const union = /* @__PURE__ */ __name((carried, own) => [
457
+ .../* @__PURE__ */ new Set([
458
+ ...carried ?? [],
459
+ ...own
460
+ ])
461
+ ], "union");
462
+ return {
463
+ text: merged(before?.text, this.took.text),
464
+ reasoning: merged(before?.reasoning, this.took.reasoning),
465
+ called: union(before?.called, this.took.called),
466
+ settled: union(before?.settled, this.took.settled),
467
+ steps: union(before?.steps, this.took.steps)
468
+ };
469
+ }
434
470
  /** What the run spent so far: carried from before this turn object, plus what it saw. */
435
471
  totalUsage() {
436
472
  return addUsage(this.a.carried, this.spent);
@@ -521,7 +557,11 @@ var OpenCodeTurn = class {
521
557
  this.reported = {
522
558
  ...this.spent
523
559
  };
524
- const milestone = {
560
+ const milestone = reached.kind === "ask" ? {
561
+ ...reached,
562
+ usage,
563
+ progress: this.progress()
564
+ } : {
525
565
  ...reached,
526
566
  usage
527
567
  };
@@ -576,6 +616,7 @@ var OpenCodeTurn = class {
576
616
  innerSettled: /* @__PURE__ */ new Set()
577
617
  };
578
618
  this.tools.set(id, state);
619
+ this.took.called.add(id);
579
620
  this.segment.calls.push({
580
621
  id,
581
622
  name,
@@ -703,6 +744,7 @@ var OpenCodeTurn = class {
703
744
  case "session.text.delta": {
704
745
  const delta = typeof d.delta === "string" ? d.delta : "";
705
746
  if (!delta) return;
747
+ this.count(this.took.text, d.assistantMessageID, delta.length);
706
748
  this.endReasoning();
707
749
  if (!this.sawText && !delta.trim()) return;
708
750
  await this.step();
@@ -724,6 +766,7 @@ ${delta}` : delta;
724
766
  case "session.reasoning.delta": {
725
767
  const delta = typeof d.delta === "string" ? d.delta : "";
726
768
  if (!delta) return;
769
+ this.count(this.took.reasoning, d.assistantMessageID, delta.length);
727
770
  this.reasoningSince ??= Date.now();
728
771
  await this.step();
729
772
  this.segment.reasoning += delta;
@@ -778,6 +821,7 @@ ${delta}` : delta;
778
821
  }
779
822
  case "session.step.ended":
780
823
  this.endReasoning();
824
+ if (typeof d.assistantMessageID === "string") this.took.steps.add(d.assistantMessageID);
781
825
  await this.stepEnded(d);
782
826
  return;
783
827
  case "session.step.failed":
@@ -818,10 +862,16 @@ ${delta}` : delta;
818
862
  return;
819
863
  }
820
864
  }
865
+ /** Characters taken from an assistant message's text or reasoning. */
866
+ count(into, messageId, n) {
867
+ if (typeof messageId !== "string") return;
868
+ into.set(messageId, (into.get(messageId) ?? 0) + n);
869
+ }
821
870
  async settleTool(id, ok, error = "failed") {
822
871
  const state = this.tool(id);
823
872
  if (state.settled) return;
824
873
  state.settled = true;
874
+ this.took.settled.add(id);
825
875
  if (ok) {
826
876
  this.segment.results.push({
827
877
  id,
@@ -1371,9 +1421,171 @@ ${delta}` : delta;
1371
1421
  cost: cost < 1e-9 ? 0 : cost
1372
1422
  }, this.compacted ? "history_summary" : "title");
1373
1423
  }
1374
- async end(outcome) {
1424
+ /**
1425
+ * The session's messages since this turn's prompt, oldest first: everything after its newest user
1426
+ * message. `undefined` when they cannot be read (no `message.list`, no prompt within reach); empty
1427
+ * when the newest prompt is another run's (this turn's never reached the session).
1428
+ */
1429
+ async messagesSincePrompt() {
1430
+ const messages = this.a.client.message;
1431
+ if (messages === void 0) return void 0;
1432
+ const since = [];
1433
+ let cursor;
1434
+ for (let page = 0; page < RECOVER_PAGES; page += 1) {
1435
+ const listed = await messages.list({
1436
+ sessionID: this.a.sessionId,
1437
+ order: "desc",
1438
+ limit: RECOVER_PAGE_SIZE,
1439
+ ...cursor !== void 0 ? {
1440
+ cursor
1441
+ } : {}
1442
+ });
1443
+ for (const message of listed.data) {
1444
+ if (message.type === "user") {
1445
+ const runId = message.metadata?.runId;
1446
+ if (typeof runId === "string" && runId !== this.a.runId) return [];
1447
+ return since.reverse();
1448
+ }
1449
+ since.push(message);
1450
+ }
1451
+ const next = listed.cursor?.next;
1452
+ if (typeof next !== "string" || next.length === 0 || listed.data.length === 0) break;
1453
+ cursor = next;
1454
+ }
1455
+ return void 0;
1456
+ }
1457
+ /**
1458
+ * A turn rebuilt after a restart heard only what the session said once it re-attached: what it
1459
+ * said in between — read back from the session's messages since the prompt, less what an earlier
1460
+ * process already took ({@link TurnProgress}) and what this one heard — is taken now, as its
1461
+ * events would have been: text, reasoning, tool calls and results, each step's usage. A session
1462
+ * that left nothing for the turn fails it ({@link LOST_ANSWER_ERROR}) rather than reading as an
1463
+ * empty answer.
1464
+ */
1465
+ async recover(outcome) {
1466
+ const messages = await this.messagesSincePrompt().catch((error) => {
1467
+ this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not read the session messages")}`);
1468
+ return void 0;
1469
+ });
1470
+ const before = this.a.progress;
1471
+ const text = [];
1472
+ let reasoning = "";
1473
+ let lastError;
1474
+ for (const message of messages ?? []) {
1475
+ if (message.type !== "assistant") continue;
1476
+ const id = message.id;
1477
+ const parts = message.content ?? [];
1478
+ text.push(...missed(parts, "text", before?.text[id] ?? 0, this.took.text.get(id) ?? 0));
1479
+ reasoning += missed(parts, "reasoning", before?.reasoning[id] ?? 0, this.took.reasoning.get(id) ?? 0).map((piece) => piece.text).join("");
1480
+ for (const part of parts) if (part.type === "tool") await this.missedTool(part);
1481
+ if (message.tokens !== void 0 && !before?.steps.includes(id) && !this.took.steps.has(id)) {
1482
+ this.took.steps.add(id);
1483
+ const model = message.model;
1484
+ if (typeof model?.id === "string" && typeof model.providerID === "string") this.model = {
1485
+ providerID: model.providerID,
1486
+ id: model.id
1487
+ };
1488
+ await this.spend({
1489
+ tokens: message.tokens,
1490
+ cost: message.cost
1491
+ }, "chat");
1492
+ }
1493
+ if (message.error?.message) lastError = message.error.message;
1494
+ }
1495
+ const recovered = text.map((piece) => piece.text).join("\n\n").trimStart();
1496
+ if (recovered.trim()) {
1497
+ await this.step();
1498
+ const joint = this.segment.text && !text.at(-1)?.continues ? "\n\n" : "";
1499
+ this.segment.text = `${recovered}${joint}${this.segment.text}`;
1500
+ this.sawText = true;
1501
+ await this.write({
1502
+ kind: "text",
1503
+ text: recovered
1504
+ });
1505
+ }
1506
+ if (reasoning) {
1507
+ await this.step();
1508
+ this.segment.reasoning = `${reasoning}${this.segment.reasoning}`;
1509
+ await this.write({
1510
+ kind: "reasoning",
1511
+ text: reasoning
1512
+ });
1513
+ }
1514
+ if (outcome.status !== "succeeded") return outcome;
1515
+ const idle = (messages ?? []).filter((m) => m.type === "idle").at(-1);
1516
+ if (idle?.outcome === "failed") return {
1517
+ status: "failed",
1518
+ error: lastError ?? this.stepError ?? "the execution failed"
1519
+ };
1520
+ if (idle?.outcome === "interrupted") return {
1521
+ status: "interrupted"
1522
+ };
1523
+ const s = this.segment;
1524
+ const said = s.text || s.reasoning || s.calls.length > 0 || s.ui.length > 0;
1525
+ const progress = this.progress();
1526
+ const tookAny = Object.keys(progress.text).length > 0 || progress.called.length > 0 || progress.steps.length > 0;
1527
+ if (!said && !this.wroteMessage && !tookAny) return {
1528
+ status: "failed",
1529
+ error: LOST_ANSWER_ERROR
1530
+ };
1531
+ return outcome;
1532
+ }
1533
+ /** A tool part of a message the turn did not hear: its call, its result, whichever it lacks. */
1534
+ async missedTool(part) {
1535
+ const id = part.id;
1536
+ const name = part.name ?? "tool";
1537
+ if (id === void 0 || HIDDEN_TOOLS.has(name)) return;
1538
+ const state = part.state ?? {};
1539
+ const before = this.a.progress;
1540
+ if (!before?.called.includes(id) && !this.tools.has(id)) {
1541
+ await this.step();
1542
+ this.setInput(id, state.input, name);
1543
+ await this.makeAvailable(id);
1544
+ }
1545
+ const ok = state.status === "completed";
1546
+ if (!ok && state.status !== "error" || before?.settled.includes(id)) return;
1547
+ const error = errorText(state.error, "tool failed");
1548
+ if (this.tools.has(id)) {
1549
+ await this.settleTool(id, ok, error);
1550
+ return;
1551
+ }
1552
+ this.took.settled.add(id);
1553
+ const result = ok ? {
1554
+ id,
1555
+ name,
1556
+ output: {
1557
+ ok: true
1558
+ }
1559
+ } : {
1560
+ id,
1561
+ name,
1562
+ output: {
1563
+ ok: false
1564
+ },
1565
+ error
1566
+ };
1567
+ const messageId = await this.messageOf(id);
1568
+ if (messageId !== void 0) await this.addResult(messageId, result);
1569
+ else this.segment.results.push(result);
1570
+ await this.write(ok ? {
1571
+ kind: "tool-output",
1572
+ id,
1573
+ output: {
1574
+ ok: true
1575
+ }
1576
+ } : {
1577
+ kind: "tool-output-error",
1578
+ id,
1579
+ error
1580
+ });
1581
+ }
1582
+ async end(ended) {
1375
1583
  if (this.ended) return;
1376
1584
  this.ended = true;
1585
+ const outcome = this.a.resumed === true ? await this.recover(ended).catch((error) => {
1586
+ this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not recover the turn")}`);
1587
+ return ended;
1588
+ }) : ended;
1377
1589
  await this.unreported().catch((error) => this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not read the session usage")}`));
1378
1590
  if (outcome.status !== "failed" || this.segment.text || this.segment.calls.length > 0 || this.segment.ui.length > 0) {
1379
1591
  await this.flush(true);
@@ -1390,6 +1602,26 @@ ${delta}` : delta;
1390
1602
  });
1391
1603
  }
1392
1604
  };
1605
+ function missed(parts, type, head, tail) {
1606
+ const texts = parts.flatMap((p) => p.type === type && typeof p.text === "string" ? [
1607
+ p.text
1608
+ ] : []);
1609
+ const total = texts.reduce((n, t) => n + t.length, 0);
1610
+ const end = total - tail;
1611
+ const out = [];
1612
+ let at = 0;
1613
+ for (const t of texts) {
1614
+ const from = Math.max(head, at);
1615
+ const to = Math.min(end, at + t.length);
1616
+ if (to > from) out.push({
1617
+ text: t.slice(from - at, to - at),
1618
+ continues: to < at + t.length
1619
+ });
1620
+ at += t.length;
1621
+ }
1622
+ return out;
1623
+ }
1624
+ __name(missed, "missed");
1393
1625
  function modelOfLabel(label) {
1394
1626
  const slash = label?.indexOf("/") ?? -1;
1395
1627
  if (label === void 0 || slash <= 0 || slash === label.length - 1) return void 0;
@@ -2535,7 +2767,11 @@ ${body}
2535
2767
  text: prompt.text,
2536
2768
  ...prompt.files?.length ? {
2537
2769
  files: prompt.files
2538
- } : {}
2770
+ } : {},
2771
+ // Marks the prompt as this run's: a turn resumed after a restart reads what came after it.
2772
+ metadata: {
2773
+ runId
2774
+ }
2539
2775
  });
2540
2776
  } catch (error) {
2541
2777
  live.turn.fail(`OpenCode refused the message: ${error.message}`);
@@ -2548,9 +2784,11 @@ ${body}
2548
2784
  *
2549
2785
  * `spent` is what the run spent before (the sum of the `usage` of its earlier milestones): a turn
2550
2786
  * rebuilt here starts from it, so the answer it persists carries the whole run's usage.
2787
+ * `progress` is what the turn took before (its last `ask` milestone's): a turn rebuilt here reads
2788
+ * back from the session's messages, when the execution ends, what nobody heard after that.
2551
2789
  */
2552
- async observe(runId, input, handle, spent) {
2553
- const live = await this.ensureLive(runId, input, handle, true, spent);
2790
+ async observe(runId, input, handle, spent, progress) {
2791
+ const live = await this.ensureLive(runId, input, handle, true, spent, progress);
2554
2792
  const { turn, client } = live;
2555
2793
  if (turn.hasMilestone()) return this.reached(runId, input, handle, await turn.next());
2556
2794
  let done = false;
@@ -2589,12 +2827,12 @@ ${body}
2589
2827
  * request is gone with it: a new session is opened, told what the person decided, and prompted
2590
2828
  * to go on.
2591
2829
  */
2592
- async reply(runId, input, handle, ask, reply, spent) {
2830
+ async reply(runId, input, handle, ask, reply, spent, progress) {
2593
2831
  const server = await this.host.server(input.actor);
2594
2832
  if (server.key !== handle.serverKey || server.bootId !== handle.bootId) {
2595
2833
  return this.continueAfterRestart(runId, input, server, ask, reply, spent);
2596
2834
  }
2597
- const live = await this.ensureLive(runId, input, handle, false, spent);
2835
+ const live = await this.ensureLive(runId, input, handle, false, spent, progress);
2598
2836
  await live.turn.decide(ask, reply);
2599
2837
  return handle;
2600
2838
  }
@@ -2612,7 +2850,10 @@ ${body}
2612
2850
  live.turn.setBaseline(await this.sessionUsage(live.client, handle.sessionId));
2613
2851
  await live.client.session.prompt({
2614
2852
  sessionID: handle.sessionId,
2615
- text: note
2853
+ text: note,
2854
+ metadata: {
2855
+ runId
2856
+ }
2616
2857
  });
2617
2858
  return handle;
2618
2859
  }
@@ -2636,9 +2877,10 @@ ${body}
2636
2877
  sessionID: known.sessionId
2637
2878
  });
2638
2879
  }
2639
- async ensureLive(runId, input, handle, catchUp, spent) {
2880
+ async ensureLive(runId, input, handle, catchUp, spent, progress) {
2640
2881
  const current = this.live.get(runId);
2641
2882
  if (current !== void 0 && current.handle.sessionId === handle.sessionId) return current;
2883
+ const resumed = catchUp && current === void 0;
2642
2884
  current?.stop();
2643
2885
  const server = await this.host.server(input.actor);
2644
2886
  const deps = this.deps.forAgent(input.agentName);
@@ -2669,6 +2911,10 @@ ${body}
2669
2911
  ...spent !== void 0 ? {
2670
2912
  carried: spent
2671
2913
  } : {},
2914
+ ...progress !== void 0 ? {
2915
+ progress
2916
+ } : {},
2917
+ resumed,
2672
2918
  logger: this.logger,
2673
2919
  onGranted: /* @__PURE__ */ __name((action, id) => {
2674
2920
  const grants = this.grants.get(runId) ?? [];
@@ -2688,7 +2934,7 @@ ${body}
2688
2934
  stop
2689
2935
  };
2690
2936
  this.live.set(runId, live);
2691
- if (catchUp && current === void 0) await turn.catchUp().catch(() => void 0);
2937
+ if (resumed) await turn.catchUp().catch(() => void 0);
2692
2938
  return live;
2693
2939
  }
2694
2940
  /** Stop following a run in this process. */
@@ -2802,6 +3048,12 @@ ${body}
2802
3048
  kind: "cancelled"
2803
3049
  }));
2804
3050
  await writer.end();
3051
+ (0, import_nestjs_agent_core4.publishAgentRunCancelled)({
3052
+ runId,
3053
+ threadId: input.threadId,
3054
+ durationMs,
3055
+ context: (0, import_nestjs_agent_core4.agentRunContext)(input)
3056
+ });
2805
3057
  await this.store.recordRunEnd?.({
2806
3058
  runId,
2807
3059
  status: "cancelled",
@@ -2832,12 +3084,16 @@ ${body}
2832
3084
  this.logger.error(`agent run ${runId} failed: ${error}`);
2833
3085
  (0, import_nestjs_agent_core4.publishAgentRunFailed)({
2834
3086
  runId,
3087
+ threadId: input.threadId,
2835
3088
  code: "opencode_failed",
2836
- message: error
3089
+ message: error,
3090
+ durationMs,
3091
+ context: (0, import_nestjs_agent_core4.agentRunContext)(input)
2837
3092
  });
2838
3093
  await this.store.recordRunEnd?.({
2839
3094
  runId,
2840
3095
  status: "failed",
3096
+ durationMs,
2841
3097
  errorCode: "opencode_failed",
2842
3098
  errorMessage: error
2843
3099
  });
@@ -3231,6 +3487,7 @@ __name(openCodeProviders, "openCodeProviders");
3231
3487
  0 && (module.exports = {
3232
3488
  FormAnswerError,
3233
3489
  InMemoryOpenCodeSessionStore,
3490
+ LOST_ANSWER_ERROR,
3234
3491
  OPENCODE_HOST,
3235
3492
  OPENCODE_MCP_ENDPOINT,
3236
3493
  OPENCODE_OPTIONS,