@dudousxd/nestjs-agent-opencode 0.3.0 → 0.4.1

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,
@@ -195,6 +196,7 @@ OpenCodeMcpController = _ts_decorate([
195
196
  // src/mcp.ts
196
197
  var import_node_crypto = require("crypto");
197
198
  var import_nestjs_agent_core3 = require("@dudousxd/nestjs-agent-core");
199
+ var import_genui = require("@dudousxd/nestjs-agent-core/genui");
198
200
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
199
201
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
200
202
  var import_zod_json_schema_compat = require("@modelcontextprotocol/sdk/server/zod-json-schema-compat.js");
@@ -325,6 +327,9 @@ function subUsage(a, b) {
325
327
  };
326
328
  }
327
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;
328
333
  var ASK_TOOL_NAME = "ask";
329
334
  var FAILED_STATUSES = /* @__PURE__ */ new Set([
330
335
  "error",
@@ -413,6 +418,14 @@ var OpenCodeTurn = class {
413
418
  wroteMessage = false;
414
419
  ended = false;
415
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
+ };
416
429
  constructor(a) {
417
430
  this.a = a;
418
431
  this.model = modelOfLabel(a.input.model);
@@ -430,6 +443,30 @@ var OpenCodeTurn = class {
430
443
  tokens: info.tokens
431
444
  };
432
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
+ }
433
470
  /** What the run spent so far: carried from before this turn object, plus what it saw. */
434
471
  totalUsage() {
435
472
  return addUsage(this.a.carried, this.spent);
@@ -520,7 +557,11 @@ var OpenCodeTurn = class {
520
557
  this.reported = {
521
558
  ...this.spent
522
559
  };
523
- const milestone = {
560
+ const milestone = reached.kind === "ask" ? {
561
+ ...reached,
562
+ usage,
563
+ progress: this.progress()
564
+ } : {
524
565
  ...reached,
525
566
  usage
526
567
  };
@@ -575,6 +616,7 @@ var OpenCodeTurn = class {
575
616
  innerSettled: /* @__PURE__ */ new Set()
576
617
  };
577
618
  this.tools.set(id, state);
619
+ this.took.called.add(id);
578
620
  this.segment.calls.push({
579
621
  id,
580
622
  name,
@@ -702,6 +744,7 @@ var OpenCodeTurn = class {
702
744
  case "session.text.delta": {
703
745
  const delta = typeof d.delta === "string" ? d.delta : "";
704
746
  if (!delta) return;
747
+ this.count(this.took.text, d.assistantMessageID, delta.length);
705
748
  this.endReasoning();
706
749
  if (!this.sawText && !delta.trim()) return;
707
750
  await this.step();
@@ -723,6 +766,7 @@ ${delta}` : delta;
723
766
  case "session.reasoning.delta": {
724
767
  const delta = typeof d.delta === "string" ? d.delta : "";
725
768
  if (!delta) return;
769
+ this.count(this.took.reasoning, d.assistantMessageID, delta.length);
726
770
  this.reasoningSince ??= Date.now();
727
771
  await this.step();
728
772
  this.segment.reasoning += delta;
@@ -777,6 +821,7 @@ ${delta}` : delta;
777
821
  }
778
822
  case "session.step.ended":
779
823
  this.endReasoning();
824
+ if (typeof d.assistantMessageID === "string") this.took.steps.add(d.assistantMessageID);
780
825
  await this.stepEnded(d);
781
826
  return;
782
827
  case "session.step.failed":
@@ -817,10 +862,16 @@ ${delta}` : delta;
817
862
  return;
818
863
  }
819
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
+ }
820
870
  async settleTool(id, ok, error = "failed") {
821
871
  const state = this.tool(id);
822
872
  if (state.settled) return;
823
873
  state.settled = true;
874
+ this.took.settled.add(id);
824
875
  if (ok) {
825
876
  this.segment.results.push({
826
877
  id,
@@ -1370,9 +1421,171 @@ ${delta}` : delta;
1370
1421
  cost: cost < 1e-9 ? 0 : cost
1371
1422
  }, this.compacted ? "history_summary" : "title");
1372
1423
  }
1373
- 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) {
1374
1583
  if (this.ended) return;
1375
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;
1376
1589
  await this.unreported().catch((error) => this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not read the session usage")}`));
1377
1590
  if (outcome.status !== "failed" || this.segment.text || this.segment.calls.length > 0 || this.segment.ui.length > 0) {
1378
1591
  await this.flush(true);
@@ -1389,6 +1602,26 @@ ${delta}` : delta;
1389
1602
  });
1390
1603
  }
1391
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");
1392
1625
  function modelOfLabel(label) {
1393
1626
  const slash = label?.indexOf("/") ?? -1;
1394
1627
  if (label === void 0 || slash <= 0 || slash === label.length - 1) return void 0;
@@ -1515,6 +1748,8 @@ var OpenCodeMcpEndpoint2 = class {
1515
1748
  }
1516
1749
  turns;
1517
1750
  tokens;
1751
+ /** Genui telemetry of the UI calls OpenCode makes (its code mode calls tools here, not in the loop). */
1752
+ genui = new import_nestjs_agent_core3.GenuiRunRecorder();
1518
1753
  constructor(turns, tokens) {
1519
1754
  this.turns = turns;
1520
1755
  this.tokens = tokens;
@@ -1646,7 +1881,7 @@ var OpenCodeMcpEndpoint2 = class {
1646
1881
  if (spec.kind === "action" && !await this.turns.spendApproval(turn.runId, name, turn.input)) {
1647
1882
  throw new OpenCodeToolRefusedError(`"${name}" is an action and nobody approved this call; ask for the permission first.`);
1648
1883
  }
1649
- return registry.invoke(name, args ?? {}, {
1884
+ const invoke = /* @__PURE__ */ __name(() => registry.invoke(name, args ?? {}, {
1650
1885
  actor,
1651
1886
  threadId: turn.ctx.threadId,
1652
1887
  runId: turn.runId,
@@ -1663,7 +1898,24 @@ var OpenCodeMcpEndpoint2 = class {
1663
1898
  } : {}
1664
1899
  }, this.turns.rolesPolicyFor(turn.input), allowed !== void 0 ? {
1665
1900
  allowedTools: allowed
1666
- } : {});
1901
+ } : {}), "invoke");
1902
+ return this.genui.track({
1903
+ store: this.turns.store,
1904
+ marker: spec.genui,
1905
+ runId: turn.runId,
1906
+ threadId: turn.ctx.threadId,
1907
+ actorRef: actor.id,
1908
+ ...turn.input.agentName !== void 0 ? {
1909
+ agentName: turn.input.agentName
1910
+ } : {},
1911
+ channel: (0, import_genui.turnChannel)(turn.input.pageContext),
1912
+ modelId: turn.input.model ?? "opencode",
1913
+ call: {
1914
+ id: `mcp:${turn.runId}:${requestId}`,
1915
+ name,
1916
+ input: args ?? {}
1917
+ }
1918
+ }, invoke);
1667
1919
  }
1668
1920
  /** `remember`: one fact, at the actor's own scope only — as the loop serves it. */
1669
1921
  async remember(turn, args) {
@@ -2515,7 +2767,11 @@ ${body}
2515
2767
  text: prompt.text,
2516
2768
  ...prompt.files?.length ? {
2517
2769
  files: prompt.files
2518
- } : {}
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
+ }
2519
2775
  });
2520
2776
  } catch (error) {
2521
2777
  live.turn.fail(`OpenCode refused the message: ${error.message}`);
@@ -2528,9 +2784,11 @@ ${body}
2528
2784
  *
2529
2785
  * `spent` is what the run spent before (the sum of the `usage` of its earlier milestones): a turn
2530
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.
2531
2789
  */
2532
- async observe(runId, input, handle, spent) {
2533
- 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);
2534
2792
  const { turn, client } = live;
2535
2793
  if (turn.hasMilestone()) return this.reached(runId, input, handle, await turn.next());
2536
2794
  let done = false;
@@ -2569,12 +2827,12 @@ ${body}
2569
2827
  * request is gone with it: a new session is opened, told what the person decided, and prompted
2570
2828
  * to go on.
2571
2829
  */
2572
- async reply(runId, input, handle, ask, reply, spent) {
2830
+ async reply(runId, input, handle, ask, reply, spent, progress) {
2573
2831
  const server = await this.host.server(input.actor);
2574
2832
  if (server.key !== handle.serverKey || server.bootId !== handle.bootId) {
2575
2833
  return this.continueAfterRestart(runId, input, server, ask, reply, spent);
2576
2834
  }
2577
- const live = await this.ensureLive(runId, input, handle, false, spent);
2835
+ const live = await this.ensureLive(runId, input, handle, false, spent, progress);
2578
2836
  await live.turn.decide(ask, reply);
2579
2837
  return handle;
2580
2838
  }
@@ -2592,7 +2850,10 @@ ${body}
2592
2850
  live.turn.setBaseline(await this.sessionUsage(live.client, handle.sessionId));
2593
2851
  await live.client.session.prompt({
2594
2852
  sessionID: handle.sessionId,
2595
- text: note
2853
+ text: note,
2854
+ metadata: {
2855
+ runId
2856
+ }
2596
2857
  });
2597
2858
  return handle;
2598
2859
  }
@@ -2616,9 +2877,10 @@ ${body}
2616
2877
  sessionID: known.sessionId
2617
2878
  });
2618
2879
  }
2619
- async ensureLive(runId, input, handle, catchUp, spent) {
2880
+ async ensureLive(runId, input, handle, catchUp, spent, progress) {
2620
2881
  const current = this.live.get(runId);
2621
2882
  if (current !== void 0 && current.handle.sessionId === handle.sessionId) return current;
2883
+ const resumed = catchUp && current === void 0;
2622
2884
  current?.stop();
2623
2885
  const server = await this.host.server(input.actor);
2624
2886
  const deps = this.deps.forAgent(input.agentName);
@@ -2649,6 +2911,10 @@ ${body}
2649
2911
  ...spent !== void 0 ? {
2650
2912
  carried: spent
2651
2913
  } : {},
2914
+ ...progress !== void 0 ? {
2915
+ progress
2916
+ } : {},
2917
+ resumed,
2652
2918
  logger: this.logger,
2653
2919
  onGranted: /* @__PURE__ */ __name((action, id) => {
2654
2920
  const grants = this.grants.get(runId) ?? [];
@@ -2668,7 +2934,7 @@ ${body}
2668
2934
  stop
2669
2935
  };
2670
2936
  this.live.set(runId, live);
2671
- if (catchUp && current === void 0) await turn.catchUp().catch(() => void 0);
2937
+ if (resumed) await turn.catchUp().catch(() => void 0);
2672
2938
  return live;
2673
2939
  }
2674
2940
  /** Stop following a run in this process. */
@@ -3211,6 +3477,7 @@ __name(openCodeProviders, "openCodeProviders");
3211
3477
  0 && (module.exports = {
3212
3478
  FormAnswerError,
3213
3479
  InMemoryOpenCodeSessionStore,
3480
+ LOST_ANSWER_ERROR,
3214
3481
  OPENCODE_HOST,
3215
3482
  OPENCODE_MCP_ENDPOINT,
3216
3483
  OPENCODE_OPTIONS,