@snowyroad/arp 0.3.6 → 0.3.8

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.
Files changed (2) hide show
  1. package/dist/cli.js +134 -17
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1439,7 +1439,27 @@ var RelayClient = class {
1439
1439
  return [];
1440
1440
  }
1441
1441
  }
1442
- async postMessage(channelId, content) {
1442
+ /** Spread optional usage into the outbound body. Omits undefined fields so a no-usage
1443
+ * call produces a body byte-identical to the pre-billing shape. */
1444
+ usageBody(u) {
1445
+ if (!u) return {};
1446
+ const b = { costReported: u.costReported };
1447
+ if (u.model !== void 0) b.model = u.model;
1448
+ if (u.provider !== void 0) b.provider = u.provider;
1449
+ if (u.authMode !== void 0) b.authMode = u.authMode;
1450
+ if (u.costUsed !== void 0) b.costUsed = u.costUsed;
1451
+ if (u.costCurrency !== void 0) b.costCurrency = u.costCurrency;
1452
+ if (u.contextUsed !== void 0) b.contextUsed = u.contextUsed;
1453
+ if (u.contextSize !== void 0) b.contextSize = u.contextSize;
1454
+ if (u.tokensUsed !== void 0) b.tokensUsed = u.tokensUsed;
1455
+ if (u.inputTokens !== void 0) b.inputTokens = u.inputTokens;
1456
+ if (u.outputTokens !== void 0) b.outputTokens = u.outputTokens;
1457
+ if (u.cachedReadTokens !== void 0) b.cachedReadTokens = u.cachedReadTokens;
1458
+ if (u.cachedWriteTokens !== void 0) b.cachedWriteTokens = u.cachedWriteTokens;
1459
+ if (u.thoughtTokens !== void 0) b.thoughtTokens = u.thoughtTokens;
1460
+ return b;
1461
+ }
1462
+ async postMessage(channelId, content, usage) {
1443
1463
  const ch = this.pathId(channelId, "channelId");
1444
1464
  if (!ch) return;
1445
1465
  const url = `${this.cfg.relayHttpUrl}/channels/${ch}/messages`;
@@ -1449,7 +1469,8 @@ var RelayClient = class {
1449
1469
  content,
1450
1470
  agentId: this.cfg.agentUuid,
1451
1471
  agentName: this.cfg.agentName,
1452
- messageType: "agent"
1472
+ messageType: "agent",
1473
+ ...this.usageBody(usage)
1453
1474
  });
1454
1475
  try {
1455
1476
  const res = await this.deps.fetchFn(url, {
@@ -1467,7 +1488,7 @@ var RelayClient = class {
1467
1488
  /** Post a bounded-flow reply (turn or synthesis) to the flow-scoped endpoint.
1468
1489
  * agentId MUST be the agent NAME — the relay's flow gate resolves turn ownership and
1469
1490
  * synthesis role via resolveAgentUUID (a name lookup); a UUID resolves to uuid.Nil -> 403. */
1470
- async postFlowMessage(channelId, flowId, content) {
1491
+ async postFlowMessage(channelId, flowId, content, usage) {
1471
1492
  const ch = this.pathId(channelId, "channelId");
1472
1493
  const fl = this.pathId(flowId, "flowId");
1473
1494
  if (!ch || !fl) return;
@@ -1478,7 +1499,8 @@ var RelayClient = class {
1478
1499
  // The relay's flow gate resolves turn ownership + synthesis role by NAME
1479
1500
  // (resolveAgentUUID is a name lookup), so the flow reply must carry the agent name.
1480
1501
  agentId: this.cfg.agentName,
1481
- agentName: this.cfg.agentName
1502
+ agentName: this.cfg.agentName,
1503
+ ...this.usageBody(usage)
1482
1504
  });
1483
1505
  try {
1484
1506
  const res = await this.deps.fetchFn(url, {
@@ -1697,10 +1719,10 @@ ${toolStatusLine(this.toolMode)}
1697
1719
  }
1698
1720
  async start(opts) {
1699
1721
  this.session = await this.adapter.start(opts);
1700
- this.session.onTurn((full) => {
1722
+ this.session.onTurn((full, usage) => {
1701
1723
  this.beacon?.end();
1702
1724
  if (full.replace(/<<silent>>/gi, "").trim() === "") return;
1703
- this.onReply(full.replace(/^\s*(?:<<silent>>\s*)+/i, "").trim());
1725
+ this.onReply(full.replace(/^\s*(?:<<silent>>\s*)+/i, "").trim(), usage);
1704
1726
  });
1705
1727
  }
1706
1728
  /**
@@ -2055,6 +2077,15 @@ var AcpClient = class {
2055
2077
  if (u.sessionUpdate === "agent_message_chunk" && u.content?.type === "text" && this.activeTurnBuffer) {
2056
2078
  this.activeTurnBuffer.text += u.content.text;
2057
2079
  }
2080
+ if (u.sessionUpdate === "usage_update" && this.activeTurnBuffer) {
2081
+ this.activeTurnBuffer.usage ??= {};
2082
+ this.activeTurnBuffer.usage.contextUsed = u.used;
2083
+ this.activeTurnBuffer.usage.contextSize = u.size;
2084
+ if (u.cost) {
2085
+ this.activeTurnBuffer.usage.costCumulative = u.cost.amount;
2086
+ this.activeTurnBuffer.usage.costCurrency = u.cost.currency;
2087
+ }
2088
+ }
2058
2089
  },
2059
2090
  requestPermission: async (req) => {
2060
2091
  const verdict = evaluateAcpPermission(this.policy.mode, this.policy.configDirAbs, req);
@@ -2139,13 +2170,27 @@ var AcpClient = class {
2139
2170
  const buffer = { text: "" };
2140
2171
  this.activeTurnBuffer = buffer;
2141
2172
  try {
2142
- await this.guard(
2173
+ const resp = await this.guard(
2143
2174
  this.conn.prompt({
2144
2175
  sessionId: this._sessionId,
2145
2176
  prompt: [{ type: "text", text }]
2146
2177
  })
2147
2178
  );
2148
- return buffer.text;
2179
+ const u = resp?.usage;
2180
+ if (u) {
2181
+ buffer.usage = {
2182
+ ...buffer.usage ?? {},
2183
+ tokenBreakdown: {
2184
+ inputTokens: u.inputTokens,
2185
+ outputTokens: u.outputTokens,
2186
+ cachedReadTokens: u.cachedReadTokens ?? void 0,
2187
+ cachedWriteTokens: u.cachedWriteTokens ?? void 0,
2188
+ thoughtTokens: u.thoughtTokens ?? void 0,
2189
+ totalTokens: u.totalTokens
2190
+ }
2191
+ };
2192
+ }
2193
+ return { text: buffer.text, usage: buffer.usage };
2149
2194
  } finally {
2150
2195
  if (this.activeTurnBuffer === buffer) this.activeTurnBuffer = null;
2151
2196
  }
@@ -2212,6 +2257,9 @@ var AcpClient = class {
2212
2257
  // src/sessionStore.ts
2213
2258
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, chmodSync as chmodSync2 } from "fs";
2214
2259
  import { join as join3, dirname as dirname2 } from "path";
2260
+ function mergeSessionPointer(existing, sessionId, cwd) {
2261
+ return existing ? { ...existing, sessionId, cwd } : { sessionId, cwd };
2262
+ }
2215
2263
  function safe(s) {
2216
2264
  return s.replace(/[^a-zA-Z0-9._-]/g, "_");
2217
2265
  }
@@ -2229,7 +2277,10 @@ function loadStoredSession(dir, relayUrl, agentName, channelId) {
2229
2277
  try {
2230
2278
  const p = JSON.parse(raw);
2231
2279
  if (typeof p.sessionId === "string" && p.sessionId.trim() !== "" && typeof p.cwd === "string" && p.cwd.trim() !== "") {
2232
- return { sessionId: p.sessionId, cwd: p.cwd };
2280
+ const rec = { sessionId: p.sessionId, cwd: p.cwd };
2281
+ if (typeof p.costCumulativeLastSeen === "number") rec.costCumulativeLastSeen = p.costCumulativeLastSeen;
2282
+ if (typeof p.costCurrency === "string") rec.costCurrency = p.costCurrency;
2283
+ return rec;
2233
2284
  }
2234
2285
  return null;
2235
2286
  } catch {
@@ -2247,6 +2298,52 @@ function saveStoredSession(dir, relayUrl, agentName, channelId, rec) {
2247
2298
  } catch {
2248
2299
  }
2249
2300
  }
2301
+ function updateStoredCost(dir, relayUrl, agentName, channelId, cumulative, currency) {
2302
+ const cur = loadStoredSession(dir, relayUrl, agentName, channelId);
2303
+ if (!cur) return;
2304
+ cur.costCumulativeLastSeen = cumulative;
2305
+ if (currency) cur.costCurrency = currency;
2306
+ saveStoredSession(dir, relayUrl, agentName, channelId, cur);
2307
+ }
2308
+
2309
+ // src/usage/source.ts
2310
+ var AcpUsageSource = class {
2311
+ constructor(deps) {
2312
+ this.deps = deps;
2313
+ }
2314
+ deps;
2315
+ forTurn(raw) {
2316
+ if (!raw || raw.contextUsed === void 0 && raw.contextSize === void 0 && raw.costCumulative === void 0 && raw.tokenBreakdown === void 0) {
2317
+ return void 0;
2318
+ }
2319
+ const out = {
2320
+ costReported: false,
2321
+ model: this.deps.model,
2322
+ provider: this.deps.provider,
2323
+ authMode: this.deps.authMode
2324
+ };
2325
+ if (raw.contextUsed !== void 0) out.contextUsed = raw.contextUsed;
2326
+ if (raw.contextSize !== void 0) out.contextSize = raw.contextSize;
2327
+ if (raw.costCumulative !== void 0) {
2328
+ out.costReported = true;
2329
+ out.costCurrency = raw.costCurrency;
2330
+ const prior = this.deps.loadBaseline() ?? 0;
2331
+ const delta = Math.max(0, raw.costCumulative - prior);
2332
+ out.costUsed = delta;
2333
+ this.deps.saveBaseline(raw.costCumulative, raw.costCurrency);
2334
+ }
2335
+ if (raw.tokenBreakdown) {
2336
+ const b = raw.tokenBreakdown;
2337
+ out.inputTokens = b.inputTokens;
2338
+ out.outputTokens = b.outputTokens;
2339
+ out.cachedReadTokens = b.cachedReadTokens;
2340
+ out.cachedWriteTokens = b.cachedWriteTokens;
2341
+ out.thoughtTokens = b.thoughtTokens;
2342
+ out.tokensUsed = b.totalTokens;
2343
+ }
2344
+ return out;
2345
+ }
2346
+ };
2250
2347
 
2251
2348
  // src/adapter.ts
2252
2349
  function defaultToolPolicy() {
@@ -2333,15 +2430,17 @@ var defaultAcpClientFactory = (launch) => new AcpClient(launch);
2333
2430
  var MAX_CONSECUTIVE_RESTARTS = 3;
2334
2431
  var RESTART_BACKOFF_MS = 250;
2335
2432
  var AcpAdapter = class {
2336
- constructor(agent, makeClient = defaultAcpClientFactory, backoffMs = RESTART_BACKOFF_MS, policy = defaultToolPolicy(), session) {
2433
+ constructor(agent, makeClient = defaultAcpClientFactory, backoffMs = RESTART_BACKOFF_MS, policy = defaultToolPolicy(), session, usageSource) {
2337
2434
  this.makeClient = makeClient;
2338
2435
  this.backoffMs = backoffMs;
2339
2436
  this.session = session;
2437
+ this.usageSource = usageSource;
2340
2438
  this.launch = { ...launchSpecFor(agent), policy };
2341
2439
  }
2342
2440
  makeClient;
2343
2441
  backoffMs;
2344
2442
  session;
2443
+ usageSource;
2345
2444
  launch;
2346
2445
  // --- supervised live state (set in start()) ---
2347
2446
  client = null;
@@ -2362,7 +2461,7 @@ var AcpAdapter = class {
2362
2461
  cwd,
2363
2462
  session: this.session ? {
2364
2463
  persistedId: rec?.sessionId ?? null,
2365
- save: (id) => this.session.save({ sessionId: id, cwd })
2464
+ save: (id) => this.session.save(mergeSessionPointer(this.session.load(), id, cwd))
2366
2465
  } : void 0
2367
2466
  };
2368
2467
  this.client = this.makeClient(launch);
@@ -2393,6 +2492,12 @@ var AcpAdapter = class {
2393
2492
  * never posted to the channel. A local aside on a dead/gave-up client returns a
2394
2493
  * clear "agent unavailable" string rather than restarting (restart supervision is
2395
2494
  * reserved for channel turns; a local REPL caller sees the message inline).
2495
+ *
2496
+ * BILLING NOTE: local asides deliberately bypass handleTurn, so they do NOT advance
2497
+ * the cost baseline. Any cost they accrue in the warm session surfaces in the NEXT
2498
+ * channel turn's cumulative-delta. Do NOT add a usageSource.forTurn call here — that
2499
+ * would double-count (the same cumulative gets billed once locally and again on the
2500
+ * next channel turn).
2396
2501
  */
2397
2502
  async converseLocal(text) {
2398
2503
  const client = this.client;
@@ -2400,7 +2505,7 @@ var AcpAdapter = class {
2400
2505
  return "[arp-bridge] agent unavailable for local conversation";
2401
2506
  }
2402
2507
  try {
2403
- return await client.submit(text);
2508
+ return (await client.submit(text)).text;
2404
2509
  } catch (err) {
2405
2510
  return `[arp-bridge] local turn failed: ${err?.message ?? String(err)}`;
2406
2511
  }
@@ -2418,9 +2523,10 @@ var AcpAdapter = class {
2418
2523
  const client = this.client;
2419
2524
  if (!client) return false;
2420
2525
  try {
2421
- const reply = await client.submit(text);
2526
+ const result = await client.submit(text);
2422
2527
  this.consecutiveRestarts = 0;
2423
- this.turnCbs.forEach((cb) => cb(reply));
2528
+ const usage = this.usageSource?.forTurn(result.usage);
2529
+ this.turnCbs.forEach((cb) => cb(result.text, usage));
2424
2530
  return true;
2425
2531
  } catch (err) {
2426
2532
  if (this.stopped) {
@@ -2600,7 +2706,8 @@ function createAdapter(cfg, channelId) {
2600
2706
  };
2601
2707
  if (cfg.agentMode !== "acp") return new ClaudeAdapter(policy);
2602
2708
  const session = channelId ? makeSessionPersistence(cfg, channelId) : void 0;
2603
- return new AcpAdapter(cfg.agent, void 0, void 0, policy, session);
2709
+ const usageSource = channelId ? makeUsageSource(cfg, channelId) : void 0;
2710
+ return new AcpAdapter(cfg.agent, void 0, void 0, policy, session, usageSource);
2604
2711
  }
2605
2712
  function makeSessionPersistence(cfg, channelId) {
2606
2713
  const dir = configDir(process.env);
@@ -2609,6 +2716,16 @@ function makeSessionPersistence(cfg, channelId) {
2609
2716
  save: (rec) => saveStoredSession(dir, cfg.relayWsUrl, cfg.agentName, channelId, rec)
2610
2717
  };
2611
2718
  }
2719
+ function makeUsageSource(cfg, channelId) {
2720
+ const dir = configDir(process.env);
2721
+ return new AcpUsageSource({
2722
+ model: cfg.model,
2723
+ provider: cfg.agent,
2724
+ authMode: cfg.agentMode === "acp" ? "oauth" : "apikey",
2725
+ loadBaseline: () => loadStoredSession(dir, cfg.relayWsUrl, cfg.agentName, channelId)?.costCumulativeLastSeen,
2726
+ saveBaseline: (cumulative, currency) => updateStoredCost(dir, cfg.relayWsUrl, cfg.agentName, channelId, cumulative, currency)
2727
+ });
2728
+ }
2612
2729
 
2613
2730
  // src/elicit.ts
2614
2731
  var CARD_PROMPT = 'Answer IMMEDIATELY with ONLY a JSON object \u2014 do not deliberate, plan, or explain: { "description": string, "skills": [{ "id": string, "name": string, "description": string, "tags": string[] }] }. description = ONE short sentence on what you do. List ALL your skills (do not omit any), each with a SHORT description. No prose outside the JSON.';
@@ -2696,11 +2813,11 @@ async function startBridge(cfg, relay, deps) {
2696
2813
  const beacon = new ActivityBeacon((state) => relay.sendActivity(channelId, state));
2697
2814
  const session = new ChannelSession(
2698
2815
  adapter,
2699
- (text) => void relay.postMessage(channelId, text),
2816
+ (text, usage) => void relay.postMessage(channelId, text, usage),
2700
2817
  cfg.agentName,
2701
2818
  channelId,
2702
2819
  {
2703
- postReply: (flowId, content) => relay.postFlowMessage(channelId, flowId, content),
2820
+ postReply: (flowId, content, usage) => relay.postFlowMessage(channelId, flowId, content, usage),
2704
2821
  fetchHistory: (flowId) => relay.fetchFlowMessages(channelId, flowId)
2705
2822
  },
2706
2823
  () => relay.fetchChannelContext(channelId),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowyroad/arp",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Connect your own coding agent (Claude Code, Codex, Gemini, Grok) to an Agent Relay Protocol channel and collaborate with other agents and humans.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "SnowyRoad",