@agentconnect.md/daemon 1.0.0-rc.52 → 1.0.0-rc.53

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.js CHANGED
@@ -17142,11 +17142,18 @@ var AcpHost = class {
17142
17142
  this.loadingSessions.delete(sessionId);
17143
17143
  }
17144
17144
  }
17145
+ /** Drive one turn to completion. Returns the stop reason plus the agent's token
17146
+ * `usage` when the runtime reports it (ACP's experimental per-turn usage, which
17147
+ * carries session-cumulative counts) — `undefined` when it doesn't. */
17145
17148
  async prompt(sessionId, blocks) {
17146
- return (await this.conn.agent.request(methods.agent.session.prompt, {
17149
+ const res = await this.conn.agent.request(methods.agent.session.prompt, {
17147
17150
  sessionId,
17148
17151
  prompt: blocks
17149
- })).stopReason;
17152
+ });
17153
+ return {
17154
+ stopReason: res.stopReason,
17155
+ usage: res.usage ?? void 0
17156
+ };
17150
17157
  }
17151
17158
  async cancel(sessionId) {
17152
17159
  await this.conn.agent.notify(methods.agent.session.cancel, { sessionId });
@@ -17638,6 +17645,25 @@ const RegisterOk = object({
17638
17645
  * online daemons and merges, for "all sessions across the workspace").
17639
17646
  * - `session/history`: one cursor-paginated page of a session's transcript.
17640
17647
  */
17648
+ /**
17649
+ * Per-session token accounting, metered by the daemon from the agent's ACP usage
17650
+ * stream. Token counts are CUMULATIVE across the session's turns (summed from each
17651
+ * turn's `PromptResponse.usage`); context/cost are the LATEST snapshot from the
17652
+ * agent's `usage_update` stream. Every field is optional — a runtime that reports
17653
+ * no usage yields an absent field, not a zero.
17654
+ */
17655
+ const SessionUsage = object({
17656
+ totalTokens: number().int().optional(),
17657
+ inputTokens: number().int().optional(),
17658
+ outputTokens: number().int().optional(),
17659
+ thoughtTokens: number().int().optional(),
17660
+ cachedReadTokens: number().int().optional(),
17661
+ cachedWriteTokens: number().int().optional(),
17662
+ contextUsed: number().int().optional(),
17663
+ contextSize: number().int().optional(),
17664
+ costAmount: number().optional(),
17665
+ costCurrency: string().optional()
17666
+ });
17641
17667
  /** One row in the session list (metadata + console metrics; NOT the transcript). */
17642
17668
  const SessionListItem = object({
17643
17669
  sessionId: string(),
@@ -17646,7 +17672,7 @@ const SessionListItem = object({
17646
17672
  title: string().optional(),
17647
17673
  status: string().optional(),
17648
17674
  lastActivityAt: string().optional(),
17649
- tokenUsage: number().int().optional(),
17675
+ usage: SessionUsage.optional(),
17650
17676
  triggeredBy: string().optional()
17651
17677
  });
17652
17678
  /** C→D REQ: list the daemon's sessions (optionally just one agent's). */
@@ -17739,6 +17765,14 @@ const FRAME_SCHEMAS = {
17739
17765
  summary: string().optional(),
17740
17766
  ts: string().datetime()
17741
17767
  }),
17768
+ "usage/report": object({
17769
+ sessionId: string(),
17770
+ agentId: string().uuid(),
17771
+ platform: string().optional(),
17772
+ channel: string().optional(),
17773
+ lastActivityAt: string(),
17774
+ usage: SessionUsage
17775
+ }),
17742
17776
  "facts/runtime-profile": object({
17743
17777
  runtime: string(),
17744
17778
  version: string(),
@@ -17842,6 +17876,7 @@ discriminatedUnion("type", [
17842
17876
  frame("secrets/revoke", FRAME_SCHEMAS["secrets/revoke"]),
17843
17877
  frame("scope-attestation", FRAME_SCHEMAS["scope-attestation"]),
17844
17878
  frame("event/session", FRAME_SCHEMAS["event/session"]),
17879
+ frame("usage/report", FRAME_SCHEMAS["usage/report"]),
17845
17880
  frame("facts/runtime-profile", FRAME_SCHEMAS["facts/runtime-profile"]),
17846
17881
  frame("session/list", FRAME_SCHEMAS["session/list"]),
17847
17882
  frame("session/list/page", FRAME_SCHEMAS["session/list/page"]),
@@ -23568,7 +23603,8 @@ var LocalStore = class {
23568
23603
  this.db.exec(`
23569
23604
  CREATE TABLE IF NOT EXISTS sessions (
23570
23605
  key TEXT PRIMARY KEY, agentId TEXT, platform TEXT, channel TEXT, thread TEXT,
23571
- acpSessionId TEXT, state TEXT, lastDeliveredTs TEXT, updatedAt INTEGER
23606
+ acpSessionId TEXT, state TEXT, lastDeliveredTs TEXT, updatedAt INTEGER,
23607
+ usage TEXT
23572
23608
  );
23573
23609
  CREATE TABLE IF NOT EXISTS transcript (
23574
23610
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -23585,6 +23621,13 @@ var LocalStore = class {
23585
23621
  routingEpoch INTEGER, assignments TEXT, globalRules TEXT
23586
23622
  );
23587
23623
  `);
23624
+ this.migrateSessionUsage();
23625
+ }
23626
+ /** Add the `sessions.usage` column (JSON token accounting) to a pre-existing DB.
23627
+ * CREATE TABLE IF NOT EXISTS above adds it for fresh DBs but never alters an
23628
+ * existing table, so upgrade in place. No-op once the column is present. */
23629
+ migrateSessionUsage() {
23630
+ if (!this.db.prepare("PRAGMA table_info(sessions)").all().some((c) => c.name === "usage")) this.db.exec("ALTER TABLE sessions ADD COLUMN usage TEXT");
23588
23631
  }
23589
23632
  /**
23590
23633
  * Pre-`kind` transcript tables keyed on (channel, thread, ts) hold only conversational
@@ -23610,7 +23653,8 @@ var LocalStore = class {
23610
23653
  return this.db.prepare("SELECT * FROM sessions WHERE key = ?").get(key);
23611
23654
  }
23612
23655
  /** All sessions that have an ACP id (i.e. are addressable by sessionId), newest
23613
- * first. Optionally scoped to one agent. Backs `session/list` read-back. */
23656
+ * first. Optionally scoped to one agent. Backs `session/list` read-back — the
23657
+ * `usage` column comes back as raw JSON (parsed by the reader). */
23614
23658
  listSessions(agentId) {
23615
23659
  if (agentId !== void 0) return this.db.prepare("SELECT * FROM sessions WHERE acpSessionId IS NOT NULL AND agentId = ? ORDER BY updatedAt DESC").all(agentId);
23616
23660
  return this.db.prepare("SELECT * FROM sessions WHERE acpSessionId IS NOT NULL ORDER BY updatedAt DESC").all();
@@ -23637,7 +23681,17 @@ var LocalStore = class {
23637
23681
  VALUES (@key, @agentId, @platform, @channel, @thread, @acpSessionId, @state, @lastDeliveredTs, @updatedAt)
23638
23682
  ON CONFLICT(key) DO UPDATE SET
23639
23683
  acpSessionId=excluded.acpSessionId, state=excluded.state,
23640
- lastDeliveredTs=excluded.lastDeliveredTs, updatedAt=excluded.updatedAt`).run(rec);
23684
+ lastDeliveredTs=excluded.lastDeliveredTs, updatedAt=excluded.updatedAt`).run({
23685
+ key: rec.key,
23686
+ agentId: rec.agentId,
23687
+ platform: rec.platform,
23688
+ channel: rec.channel,
23689
+ thread: rec.thread,
23690
+ acpSessionId: rec.acpSessionId,
23691
+ state: rec.state,
23692
+ lastDeliveredTs: rec.lastDeliveredTs,
23693
+ updatedAt: rec.updatedAt
23694
+ });
23641
23695
  }
23642
23696
  /** Targeted state transition for an existing session (§7.3), stamping `updatedAt`
23643
23697
  * so the change counts as activity for the TTL/idle clocks. No-op if the key is
@@ -23645,6 +23699,45 @@ var LocalStore = class {
23645
23699
  setSessionState(key, state, updatedAt) {
23646
23700
  this.db.prepare("UPDATE sessions SET state = ?, updatedAt = ? WHERE key = ?").run(state, updatedAt, key);
23647
23701
  }
23702
+ /** Current token accounting for a session (parsed from the `usage` JSON column),
23703
+ * or `{}` if none has been recorded / the JSON is unreadable. */
23704
+ getUsage(key) {
23705
+ const row = this.db.prepare("SELECT usage FROM sessions WHERE key = ?").get(key);
23706
+ if (!row?.usage) return {};
23707
+ try {
23708
+ return JSON.parse(row.usage);
23709
+ } catch {
23710
+ return {};
23711
+ }
23712
+ }
23713
+ writeUsage(key, u) {
23714
+ this.db.prepare("UPDATE sessions SET usage = ? WHERE key = ?").run(JSON.stringify(u), key);
23715
+ }
23716
+ /** Record the latest token counts for a session. ACP's `Usage` is already the
23717
+ * running session total (not a per-turn delta), so this is latest-wins over the
23718
+ * token fields — never additive. Only provided fields are updated; the
23719
+ * context/cost snapshot is left intact. No-op on an unknown key (the row is
23720
+ * created by the SessionManager first). */
23721
+ setTokenUsage(key, counts) {
23722
+ const u = this.getUsage(key);
23723
+ if (counts.totalTokens !== void 0) u.totalTokens = counts.totalTokens;
23724
+ if (counts.inputTokens !== void 0) u.inputTokens = counts.inputTokens;
23725
+ if (counts.outputTokens !== void 0) u.outputTokens = counts.outputTokens;
23726
+ if (counts.thoughtTokens !== void 0) u.thoughtTokens = counts.thoughtTokens;
23727
+ if (counts.cachedReadTokens !== void 0) u.cachedReadTokens = counts.cachedReadTokens;
23728
+ if (counts.cachedWriteTokens !== void 0) u.cachedWriteTokens = counts.cachedWriteTokens;
23729
+ this.writeUsage(key, u);
23730
+ }
23731
+ /** Overwrite the session's context-window + cost snapshot (latest `usage_update`
23732
+ * wins). Only provided fields are updated. No-op on an unknown key. */
23733
+ setUsageSnapshot(key, snap) {
23734
+ const u = this.getUsage(key);
23735
+ if (snap.contextUsed !== void 0) u.contextUsed = snap.contextUsed;
23736
+ if (snap.contextSize !== void 0) u.contextSize = snap.contextSize;
23737
+ if (snap.costAmount !== void 0) u.costAmount = snap.costAmount;
23738
+ if (snap.costCurrency !== void 0) u.costCurrency = snap.costCurrency;
23739
+ this.writeUsage(key, u);
23740
+ }
23648
23741
  /** Most-recent activity across an agent's non-closed sessions (epoch ms), or null
23649
23742
  * if it has none. Drives idle-host reaping (#111): a host with no recent session
23650
23743
  * activity AND no in-flight turn is past its idle window. */
@@ -81942,6 +82035,17 @@ var CpClient = class {
81942
82035
  if (this.state !== "READY" && this.state !== "DRAINING") return;
81943
82036
  this.transport?.send(encode(buildEnvelope("facts/runtime-profile", profile)));
81944
82037
  }
82038
+ /**
82039
+ * Report a session's cumulative token usage (D→C `usage/report` EVT,
82040
+ * fire-and-forget). No-op unless READY/DRAINING — usage is dashboard telemetry,
82041
+ * so a dropped report just means that turn's delta is absent from the CP's
82042
+ * historical aggregates until the next report (which re-sends the cumulative
82043
+ * snapshot, latest-wins). Never blocks the turn.
82044
+ */
82045
+ emitUsageReport(report) {
82046
+ if (this.state !== "READY" && this.state !== "DRAINING") return;
82047
+ this.transport?.send(encode(buildEnvelope("usage/report", report)));
82048
+ }
81945
82049
  request(type, payload) {
81946
82050
  const frame = buildEnvelope(type, payload);
81947
82051
  return this.correlator.request(frame, (e) => this.transport.send(e));
@@ -82114,20 +82218,35 @@ var CpClient = class {
82114
82218
  };
82115
82219
  //#endregion
82116
82220
  //#region src/cp/session-reader.ts
82221
+ /** Parse a session row's `usage` JSON into the wire shape. Returns undefined when
82222
+ * absent, empty, or unparseable so the optional field is simply omitted. */
82223
+ function parseUsage(raw) {
82224
+ if (!raw) return void 0;
82225
+ try {
82226
+ const u = JSON.parse(raw);
82227
+ return u && Object.keys(u).length > 0 ? u : void 0;
82228
+ } catch {
82229
+ return;
82230
+ }
82231
+ }
82117
82232
  function createSessionReader(store) {
82118
82233
  return {
82119
82234
  list(req) {
82120
- return { sessions: store.listSessions(req.agentId).map((r) => ({
82121
- sessionId: r.acpSessionId,
82122
- sessionKey: {
82123
- platform: r.platform,
82124
- channel: r.channel,
82125
- thread: r.thread
82126
- },
82127
- agentId: r.agentId,
82128
- status: r.state,
82129
- lastActivityAt: new Date(r.updatedAt).toISOString()
82130
- })) };
82235
+ return { sessions: store.listSessions(req.agentId).map((r) => {
82236
+ const usage = parseUsage(r.usage);
82237
+ return {
82238
+ sessionId: r.acpSessionId,
82239
+ sessionKey: {
82240
+ platform: r.platform,
82241
+ channel: r.channel,
82242
+ thread: r.thread
82243
+ },
82244
+ agentId: r.agentId,
82245
+ status: r.state,
82246
+ lastActivityAt: new Date(r.updatedAt).toISOString(),
82247
+ ...usage ? { usage } : {}
82248
+ };
82249
+ }) };
82131
82250
  },
82132
82251
  history(req) {
82133
82252
  const rec = store.getSessionByAcpId(req.sessionId);
@@ -83118,7 +83237,28 @@ var Daemon = class Daemon {
83118
83237
  try {
83119
83238
  const host = await this.ensureHostAsync(agentId);
83120
83239
  if (!wasRunning) replyConn?.setStatus(msg.channel, statusThread, "is thinking…");
83121
- await host.prompt(sessionId, blocks);
83240
+ const { usage } = await host.prompt(sessionId, blocks);
83241
+ if (usage) this.store.setTokenUsage(key, {
83242
+ totalTokens: usage.totalTokens,
83243
+ inputTokens: usage.inputTokens,
83244
+ outputTokens: usage.outputTokens,
83245
+ thoughtTokens: usage.thoughtTokens ?? void 0,
83246
+ cachedReadTokens: usage.cachedReadTokens ?? void 0,
83247
+ cachedWriteTokens: usage.cachedWriteTokens ?? void 0
83248
+ });
83249
+ const mergedUsage = this.store.getUsage(key);
83250
+ if (Object.keys(mergedUsage).length > 0) try {
83251
+ this.cpClient?.emitUsageReport({
83252
+ sessionId,
83253
+ agentId,
83254
+ platform: msg.platform,
83255
+ channel: msg.channel,
83256
+ lastActivityAt: new Date(this.clock.now()).toISOString(),
83257
+ usage: mergedUsage
83258
+ });
83259
+ } catch (err) {
83260
+ this.log.debug(`usage report emit failed (session ${sessionId}): ${err.message}`);
83261
+ }
83122
83262
  this.clearIdle(p);
83123
83263
  const link = this.cfg.webAppUrl ? `${this.cfg.webAppUrl.replace(/\/$/, "")}/sessions/${sessionId}` : void 0;
83124
83264
  for (const action of conv.onFinal(link)) this.enqueueApply(p, action);
@@ -83227,6 +83367,12 @@ var Daemon = class Daemon {
83227
83367
  onAcpUpdate(_agentId, sessionId, update) {
83228
83368
  const p = this.pending.get(sessionId);
83229
83369
  if (!p) return;
83370
+ if (update?.sessionUpdate === "usage_update") this.store.setUsageSnapshot(p.sessionKey, {
83371
+ contextUsed: update.used,
83372
+ contextSize: update.size,
83373
+ costAmount: update.cost?.amount ?? void 0,
83374
+ costCurrency: update.cost?.currency ?? void 0
83375
+ });
83230
83376
  for (const action of p.conv.onUpdate(update)) this.enqueueApply(p, action);
83231
83377
  this.armIdle(p);
83232
83378
  for (const ev of p.rec.onUpdate(update)) this.recordEvent(p.agentId, p.channel, p.statusThread, ev);