@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 +164 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
17149
|
+
const res = await this.conn.agent.request(methods.agent.session.prompt, {
|
|
17147
17150
|
sessionId,
|
|
17148
17151
|
prompt: blocks
|
|
17149
|
-
})
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
82122
|
-
|
|
82123
|
-
|
|
82124
|
-
|
|
82125
|
-
|
|
82126
|
-
|
|
82127
|
-
|
|
82128
|
-
|
|
82129
|
-
|
|
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);
|