@dudousxd/nestjs-agent-opencode 0.2.0 → 0.3.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/README.md CHANGED
@@ -54,7 +54,7 @@ export class AppModule {}
54
54
  | --- | --- |
55
55
  | `session.text.delta` / `session.reasoning.delta` | `text` / `reasoning` |
56
56
  | a step (`session.step.ended`) | `step-start` … `step-finish` with usage and cost; a `chat` usage row per step (see [Cost and usage](#cost-and-usage)) |
57
- | `session.usage.recorded` (the title, a compaction) | a `title` / `history_summary` usage row |
57
+ | the title OpenCode generates, a compaction | a `title` / `history_summary` usage row (see [Cost and usage](#cost-and-usage)) |
58
58
  | tools (`session.tool.*`) | `tool-input-*` / `tool-output*`, code-mode inner calls nested by `parentId` |
59
59
  | `permission.asked` | an `action` call + `approval-requested`, recorded `pending_approval`; `approve` / `reject` → `permission.reply` |
60
60
  | `form.created` | `elicitation`; `answer` / `skip` → `session.form.reply` / `cancel` |
@@ -72,6 +72,11 @@ compaction — goes to the same token-usage ledger the library's own loop writes
72
72
  `agent_token_usage` with `cost_usd` and `cost_source`), so the ledger quota (`quota: { limits }`),
73
73
  the dashboard and `GET /quota` see an OpenCode turn like any other.
74
74
 
75
+ - **The title and compactions.** OpenCode records those calls on the session without streaming
76
+ them. The turn reads what the session had spent (`session.get`) before it prompts, and when the
77
+ execution ends records what the session spent since that its steps did not report, as a `title`
78
+ row (`history_summary` when a compaction ran). A `session.usage.recorded` event, from an OpenCode
79
+ that streams it, is recorded as it arrives and not counted twice.
75
80
  - **Tokens.** OpenCode reports the uncached input in `tokens.input` and the cache beside it; the row
76
81
  counts the whole input side in `inputTokens`, with `cacheReadTokens` / `cacheWriteTokens` as
77
82
  subsets (the library's `MessageUsage`). The row's model is the one OpenCode names for the step
@@ -354,6 +354,17 @@ var OpenCodeTurn = class {
354
354
  reported = emptyUsage();
355
355
  priced = false;
356
356
  model;
357
+ /** What the session had spent when this turn prompted it; what OpenCode reported since. */
358
+ baseline;
359
+ seen = {
360
+ cost: 0,
361
+ input: 0,
362
+ output: 0,
363
+ reasoning: 0,
364
+ read: 0,
365
+ write: 0
366
+ };
367
+ compacted = false;
357
368
  stepOpen = false;
358
369
  sawText = false;
359
370
  separator = false;
@@ -365,6 +376,19 @@ var OpenCodeTurn = class {
365
376
  this.a = a;
366
377
  this.model = modelOfLabel(a.input.model);
367
378
  }
379
+ /**
380
+ * What the session had spent before this turn prompted it (`session.get`). When the execution
381
+ * ends, what the session spent since that this turn's events did not report — the title OpenCode
382
+ * generates, a compaction: OpenCode records those calls on the session without streaming them — is
383
+ * recorded as a usage row of its own.
384
+ */
385
+ setBaseline(info) {
386
+ if (info?.tokens === void 0) return;
387
+ this.baseline = {
388
+ cost: Number(info.cost) || 0,
389
+ tokens: info.tokens
390
+ };
391
+ }
368
392
  /** What the run spent so far: carried from before this turn object, plus what it saw. */
369
393
  totalUsage() {
370
394
  return addUsage(this.a.carried, this.spent);
@@ -621,6 +645,10 @@ var OpenCodeTurn = class {
621
645
  }
622
646
  if (this.ended) return;
623
647
  switch (e.type) {
648
+ case "session.compaction.started":
649
+ case "session.compaction.ended":
650
+ this.compacted = true;
651
+ return;
624
652
  case "session.step.started":
625
653
  case "session.model.selected": {
626
654
  const model = d.model;
@@ -790,6 +818,13 @@ ${delta}` : delta;
790
818
  async spend(d, purpose) {
791
819
  const usage = usageOf(d.tokens);
792
820
  const reportedCostUsd = typeof d.cost === "number" && Number.isFinite(d.cost) && d.cost >= 0 ? d.cost : void 0;
821
+ const n = /* @__PURE__ */ __name((v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0, "n");
822
+ this.seen.cost += reportedCostUsd ?? 0;
823
+ this.seen.input += n(d.tokens?.input);
824
+ this.seen.output += n(d.tokens?.output);
825
+ this.seen.reasoning += n(d.tokens?.reasoning);
826
+ this.seen.read += n(d.tokens?.cache?.read);
827
+ this.seen.write += n(d.tokens?.cache?.write);
793
828
  const fallback = reportedCostUsd !== void 0 ? {
794
829
  costUsd: reportedCostUsd,
795
830
  costSource: "provider"
@@ -995,6 +1030,8 @@ ${delta}` : delta;
995
1030
  }
996
1031
  const ttlMs = requirement.ttlMs !== void 0 && Number.isFinite(requirement.ttlMs) && requirement.ttlMs > 0 ? Math.floor(requirement.ttlMs) : void 0;
997
1032
  const expiresAt = ttlMs !== void 0 ? new Date(Date.now() + ttlMs).toISOString() : void 0;
1033
+ const tool = this.a.approvalPresentation?.(action);
1034
+ const confirmation = tool !== void 0 ? (0, import_nestjs_agent_core2.resolveToolConfirmation)(tool.name, args, tool.presentation) : void 0;
998
1035
  this.segment.calls.push({
999
1036
  id: req.id,
1000
1037
  name: action,
@@ -1009,6 +1046,9 @@ ${delta}` : delta;
1009
1046
  toolType: "action",
1010
1047
  input: args,
1011
1048
  status: "pending_approval",
1049
+ ...confirmation !== void 0 ? {
1050
+ confirmation
1051
+ } : {},
1012
1052
  runId: this.a.runId,
1013
1053
  approver: requirement.approver,
1014
1054
  ...expiresAt !== void 0 ? {
@@ -1026,6 +1066,9 @@ ${delta}` : delta;
1026
1066
  kind: "approval-requested",
1027
1067
  id: req.id,
1028
1068
  approver: requirement.approver,
1069
+ ...confirmation !== void 0 ? {
1070
+ confirmation
1071
+ } : {},
1029
1072
  ...expiresAt !== void 0 ? {
1030
1073
  expiresAt
1031
1074
  } : {}
@@ -1257,9 +1300,39 @@ ${delta}` : delta;
1257
1300
  this.messageResults.set(messageId, results);
1258
1301
  await this.a.store.setMessageToolResults(messageId, results);
1259
1302
  }
1303
+ /** What the session spent since the baseline that no event of this turn reported. */
1304
+ async unreported() {
1305
+ const baseline = this.baseline;
1306
+ if (baseline === void 0) return;
1307
+ this.baseline = void 0;
1308
+ const info = await this.a.client.session.get?.({
1309
+ sessionID: this.a.sessionId
1310
+ }).catch(() => void 0);
1311
+ const after = info?.tokens;
1312
+ if (after === void 0) return;
1313
+ const before = baseline.tokens;
1314
+ const left = /* @__PURE__ */ __name((now, then, seen) => Math.max(0, (Number(now) || 0) - (Number(then) || 0) - seen), "left");
1315
+ const tokens = {
1316
+ input: left(after.input, before.input, this.seen.input),
1317
+ output: left(after.output, before.output, this.seen.output),
1318
+ reasoning: left(after.reasoning, before.reasoning, this.seen.reasoning),
1319
+ cache: {
1320
+ read: left(after.cache?.read, before.cache?.read, this.seen.read),
1321
+ write: left(after.cache?.write, before.cache?.write, this.seen.write)
1322
+ }
1323
+ };
1324
+ const total = tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write;
1325
+ if (total === 0) return;
1326
+ const cost = left(info?.cost, baseline.cost, this.seen.cost);
1327
+ await this.spend({
1328
+ tokens,
1329
+ cost: cost < 1e-9 ? 0 : cost
1330
+ }, this.compacted ? "history_summary" : "title");
1331
+ }
1260
1332
  async end(outcome) {
1261
1333
  if (this.ended) return;
1262
1334
  this.ended = true;
1335
+ await this.unreported().catch((error) => this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not read the session usage")}`));
1263
1336
  if (outcome.status !== "failed" || this.segment.text || this.segment.calls.length > 0 || this.segment.ui.length > 0) {
1264
1337
  await this.flush(true);
1265
1338
  }
@@ -1776,6 +1849,25 @@ var OpenCodeTurns = class {
1776
1849
  get toolsServer() {
1777
1850
  return this.settings.tools?.server ?? "aviary";
1778
1851
  }
1852
+ /** The app tool an OpenCode permission `action` names, and its presentation — `undefined` for a built-in. */
1853
+ presentationOf(action) {
1854
+ if (this.settings.tools === void 0) return void 0;
1855
+ const server = this.toolsServer;
1856
+ for (const prefix of [
1857
+ `${server}.`,
1858
+ `${server}_`
1859
+ ]) {
1860
+ if (!action.startsWith(prefix)) continue;
1861
+ const spec = this.registry.spec(action.slice(prefix.length));
1862
+ if (spec !== void 0) return {
1863
+ name: spec.name,
1864
+ ...spec.presentation !== void 0 ? {
1865
+ presentation: spec.presentation
1866
+ } : {}
1867
+ };
1868
+ }
1869
+ return void 0;
1870
+ }
1779
1871
  /** The roles policy a turn's tools are gated by (the agent's own). */
1780
1872
  rolesPolicyFor(input) {
1781
1873
  return this.deps.forAgent(input.agentName).rolesPolicy;
@@ -2376,6 +2468,7 @@ ${body}
2376
2468
  })
2377
2469
  } : {}
2378
2470
  };
2471
+ live.turn.setBaseline(await this.sessionUsage(live.client, handle.sessionId));
2379
2472
  await live.client.session.prompt({
2380
2473
  sessionID: handle.sessionId,
2381
2474
  text: prompt.text,
@@ -2455,12 +2548,23 @@ ${body}
2455
2548
  await live.turn.decide(ask, reply, {
2456
2549
  tellOpenCode: false
2457
2550
  });
2551
+ live.turn.setBaseline(await this.sessionUsage(live.client, handle.sessionId));
2458
2552
  await live.client.session.prompt({
2459
2553
  sessionID: handle.sessionId,
2460
2554
  text: note
2461
2555
  });
2462
2556
  return handle;
2463
2557
  }
2558
+ /** What a session spent so far (`session.get`), or nothing when OpenCode does not say. */
2559
+ async sessionUsage(client, sessionId) {
2560
+ try {
2561
+ return await client.session.get?.({
2562
+ sessionID: sessionId
2563
+ });
2564
+ } catch {
2565
+ return void 0;
2566
+ }
2567
+ }
2464
2568
  /** Interrupt the thread's session (cancel). */
2465
2569
  async interrupt(input) {
2466
2570
  const known = await this.sessions.get(input.threadId);
@@ -2496,6 +2600,9 @@ ${body}
2496
2600
  agentName: input.agentName
2497
2601
  } : {}
2498
2602
  }), "approvalFor"),
2603
+ // An app tool reaches OpenCode as `<server>.<name>` / `<server>_<name>`: its own presentation
2604
+ // words the approval card. OpenCode's built-ins (`bash`, `edit`) have none.
2605
+ approvalPresentation: /* @__PURE__ */ __name((action) => this.presentationOf(action), "approvalPresentation"),
2499
2606
  modelLabel: input.model ?? "opencode",
2500
2607
  priceStep: /* @__PURE__ */ __name((step) => this.priceStep(step), "priceStep"),
2501
2608
  ...spent !== void 0 ? {