@dudousxd/nestjs-agent-opencode 0.2.0 → 0.2.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/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"
@@ -1257,9 +1292,39 @@ ${delta}` : delta;
1257
1292
  this.messageResults.set(messageId, results);
1258
1293
  await this.a.store.setMessageToolResults(messageId, results);
1259
1294
  }
1295
+ /** What the session spent since the baseline that no event of this turn reported. */
1296
+ async unreported() {
1297
+ const baseline = this.baseline;
1298
+ if (baseline === void 0) return;
1299
+ this.baseline = void 0;
1300
+ const info = await this.a.client.session.get?.({
1301
+ sessionID: this.a.sessionId
1302
+ }).catch(() => void 0);
1303
+ const after = info?.tokens;
1304
+ if (after === void 0) return;
1305
+ const before = baseline.tokens;
1306
+ const left = /* @__PURE__ */ __name((now, then, seen) => Math.max(0, (Number(now) || 0) - (Number(then) || 0) - seen), "left");
1307
+ const tokens = {
1308
+ input: left(after.input, before.input, this.seen.input),
1309
+ output: left(after.output, before.output, this.seen.output),
1310
+ reasoning: left(after.reasoning, before.reasoning, this.seen.reasoning),
1311
+ cache: {
1312
+ read: left(after.cache?.read, before.cache?.read, this.seen.read),
1313
+ write: left(after.cache?.write, before.cache?.write, this.seen.write)
1314
+ }
1315
+ };
1316
+ const total = tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write;
1317
+ if (total === 0) return;
1318
+ const cost = left(info?.cost, baseline.cost, this.seen.cost);
1319
+ await this.spend({
1320
+ tokens,
1321
+ cost: cost < 1e-9 ? 0 : cost
1322
+ }, this.compacted ? "history_summary" : "title");
1323
+ }
1260
1324
  async end(outcome) {
1261
1325
  if (this.ended) return;
1262
1326
  this.ended = true;
1327
+ await this.unreported().catch((error) => this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not read the session usage")}`));
1263
1328
  if (outcome.status !== "failed" || this.segment.text || this.segment.calls.length > 0 || this.segment.ui.length > 0) {
1264
1329
  await this.flush(true);
1265
1330
  }
@@ -2376,6 +2441,7 @@ ${body}
2376
2441
  })
2377
2442
  } : {}
2378
2443
  };
2444
+ live.turn.setBaseline(await this.sessionUsage(live.client, handle.sessionId));
2379
2445
  await live.client.session.prompt({
2380
2446
  sessionID: handle.sessionId,
2381
2447
  text: prompt.text,
@@ -2455,12 +2521,23 @@ ${body}
2455
2521
  await live.turn.decide(ask, reply, {
2456
2522
  tellOpenCode: false
2457
2523
  });
2524
+ live.turn.setBaseline(await this.sessionUsage(live.client, handle.sessionId));
2458
2525
  await live.client.session.prompt({
2459
2526
  sessionID: handle.sessionId,
2460
2527
  text: note
2461
2528
  });
2462
2529
  return handle;
2463
2530
  }
2531
+ /** What a session spent so far (`session.get`), or nothing when OpenCode does not say. */
2532
+ async sessionUsage(client, sessionId) {
2533
+ try {
2534
+ return await client.session.get?.({
2535
+ sessionID: sessionId
2536
+ });
2537
+ } catch {
2538
+ return void 0;
2539
+ }
2540
+ }
2464
2541
  /** Interrupt the thread's session (cancel). */
2465
2542
  async interrupt(input) {
2466
2543
  const known = await this.sessions.get(input.threadId);