@snowyroad/arp 0.3.6 → 0.3.7

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 +130 -16
  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
  }
@@ -2229,7 +2274,10 @@ function loadStoredSession(dir, relayUrl, agentName, channelId) {
2229
2274
  try {
2230
2275
  const p = JSON.parse(raw);
2231
2276
  if (typeof p.sessionId === "string" && p.sessionId.trim() !== "" && typeof p.cwd === "string" && p.cwd.trim() !== "") {
2232
- return { sessionId: p.sessionId, cwd: p.cwd };
2277
+ const rec = { sessionId: p.sessionId, cwd: p.cwd };
2278
+ if (typeof p.costCumulativeLastSeen === "number") rec.costCumulativeLastSeen = p.costCumulativeLastSeen;
2279
+ if (typeof p.costCurrency === "string") rec.costCurrency = p.costCurrency;
2280
+ return rec;
2233
2281
  }
2234
2282
  return null;
2235
2283
  } catch {
@@ -2247,6 +2295,52 @@ function saveStoredSession(dir, relayUrl, agentName, channelId, rec) {
2247
2295
  } catch {
2248
2296
  }
2249
2297
  }
2298
+ function updateStoredCost(dir, relayUrl, agentName, channelId, cumulative, currency) {
2299
+ const cur = loadStoredSession(dir, relayUrl, agentName, channelId);
2300
+ if (!cur) return;
2301
+ cur.costCumulativeLastSeen = cumulative;
2302
+ if (currency) cur.costCurrency = currency;
2303
+ saveStoredSession(dir, relayUrl, agentName, channelId, cur);
2304
+ }
2305
+
2306
+ // src/usage/source.ts
2307
+ var AcpUsageSource = class {
2308
+ constructor(deps) {
2309
+ this.deps = deps;
2310
+ }
2311
+ deps;
2312
+ forTurn(raw) {
2313
+ if (!raw || raw.contextUsed === void 0 && raw.contextSize === void 0 && raw.costCumulative === void 0 && raw.tokenBreakdown === void 0) {
2314
+ return void 0;
2315
+ }
2316
+ const out = {
2317
+ costReported: false,
2318
+ model: this.deps.model,
2319
+ provider: this.deps.provider,
2320
+ authMode: this.deps.authMode
2321
+ };
2322
+ if (raw.contextUsed !== void 0) out.contextUsed = raw.contextUsed;
2323
+ if (raw.contextSize !== void 0) out.contextSize = raw.contextSize;
2324
+ if (raw.costCumulative !== void 0) {
2325
+ out.costReported = true;
2326
+ out.costCurrency = raw.costCurrency;
2327
+ const prior = this.deps.loadBaseline() ?? 0;
2328
+ const delta = Math.max(0, raw.costCumulative - prior);
2329
+ out.costUsed = delta;
2330
+ this.deps.saveBaseline(raw.costCumulative, raw.costCurrency);
2331
+ }
2332
+ if (raw.tokenBreakdown) {
2333
+ const b = raw.tokenBreakdown;
2334
+ out.inputTokens = b.inputTokens;
2335
+ out.outputTokens = b.outputTokens;
2336
+ out.cachedReadTokens = b.cachedReadTokens;
2337
+ out.cachedWriteTokens = b.cachedWriteTokens;
2338
+ out.thoughtTokens = b.thoughtTokens;
2339
+ out.tokensUsed = b.totalTokens;
2340
+ }
2341
+ return out;
2342
+ }
2343
+ };
2250
2344
 
2251
2345
  // src/adapter.ts
2252
2346
  function defaultToolPolicy() {
@@ -2333,15 +2427,17 @@ var defaultAcpClientFactory = (launch) => new AcpClient(launch);
2333
2427
  var MAX_CONSECUTIVE_RESTARTS = 3;
2334
2428
  var RESTART_BACKOFF_MS = 250;
2335
2429
  var AcpAdapter = class {
2336
- constructor(agent, makeClient = defaultAcpClientFactory, backoffMs = RESTART_BACKOFF_MS, policy = defaultToolPolicy(), session) {
2430
+ constructor(agent, makeClient = defaultAcpClientFactory, backoffMs = RESTART_BACKOFF_MS, policy = defaultToolPolicy(), session, usageSource) {
2337
2431
  this.makeClient = makeClient;
2338
2432
  this.backoffMs = backoffMs;
2339
2433
  this.session = session;
2434
+ this.usageSource = usageSource;
2340
2435
  this.launch = { ...launchSpecFor(agent), policy };
2341
2436
  }
2342
2437
  makeClient;
2343
2438
  backoffMs;
2344
2439
  session;
2440
+ usageSource;
2345
2441
  launch;
2346
2442
  // --- supervised live state (set in start()) ---
2347
2443
  client = null;
@@ -2393,6 +2489,12 @@ var AcpAdapter = class {
2393
2489
  * never posted to the channel. A local aside on a dead/gave-up client returns a
2394
2490
  * clear "agent unavailable" string rather than restarting (restart supervision is
2395
2491
  * reserved for channel turns; a local REPL caller sees the message inline).
2492
+ *
2493
+ * BILLING NOTE: local asides deliberately bypass handleTurn, so they do NOT advance
2494
+ * the cost baseline. Any cost they accrue in the warm session surfaces in the NEXT
2495
+ * channel turn's cumulative-delta. Do NOT add a usageSource.forTurn call here — that
2496
+ * would double-count (the same cumulative gets billed once locally and again on the
2497
+ * next channel turn).
2396
2498
  */
2397
2499
  async converseLocal(text) {
2398
2500
  const client = this.client;
@@ -2400,7 +2502,7 @@ var AcpAdapter = class {
2400
2502
  return "[arp-bridge] agent unavailable for local conversation";
2401
2503
  }
2402
2504
  try {
2403
- return await client.submit(text);
2505
+ return (await client.submit(text)).text;
2404
2506
  } catch (err) {
2405
2507
  return `[arp-bridge] local turn failed: ${err?.message ?? String(err)}`;
2406
2508
  }
@@ -2418,9 +2520,10 @@ var AcpAdapter = class {
2418
2520
  const client = this.client;
2419
2521
  if (!client) return false;
2420
2522
  try {
2421
- const reply = await client.submit(text);
2523
+ const result = await client.submit(text);
2422
2524
  this.consecutiveRestarts = 0;
2423
- this.turnCbs.forEach((cb) => cb(reply));
2525
+ const usage = this.usageSource?.forTurn(result.usage);
2526
+ this.turnCbs.forEach((cb) => cb(result.text, usage));
2424
2527
  return true;
2425
2528
  } catch (err) {
2426
2529
  if (this.stopped) {
@@ -2600,7 +2703,8 @@ function createAdapter(cfg, channelId) {
2600
2703
  };
2601
2704
  if (cfg.agentMode !== "acp") return new ClaudeAdapter(policy);
2602
2705
  const session = channelId ? makeSessionPersistence(cfg, channelId) : void 0;
2603
- return new AcpAdapter(cfg.agent, void 0, void 0, policy, session);
2706
+ const usageSource = channelId ? makeUsageSource(cfg, channelId) : void 0;
2707
+ return new AcpAdapter(cfg.agent, void 0, void 0, policy, session, usageSource);
2604
2708
  }
2605
2709
  function makeSessionPersistence(cfg, channelId) {
2606
2710
  const dir = configDir(process.env);
@@ -2609,6 +2713,16 @@ function makeSessionPersistence(cfg, channelId) {
2609
2713
  save: (rec) => saveStoredSession(dir, cfg.relayWsUrl, cfg.agentName, channelId, rec)
2610
2714
  };
2611
2715
  }
2716
+ function makeUsageSource(cfg, channelId) {
2717
+ const dir = configDir(process.env);
2718
+ return new AcpUsageSource({
2719
+ model: cfg.model,
2720
+ provider: cfg.agent,
2721
+ authMode: cfg.agentMode === "acp" ? "oauth" : "apikey",
2722
+ loadBaseline: () => loadStoredSession(dir, cfg.relayWsUrl, cfg.agentName, channelId)?.costCumulativeLastSeen,
2723
+ saveBaseline: (cumulative, currency) => updateStoredCost(dir, cfg.relayWsUrl, cfg.agentName, channelId, cumulative, currency)
2724
+ });
2725
+ }
2612
2726
 
2613
2727
  // src/elicit.ts
2614
2728
  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 +2810,11 @@ async function startBridge(cfg, relay, deps) {
2696
2810
  const beacon = new ActivityBeacon((state) => relay.sendActivity(channelId, state));
2697
2811
  const session = new ChannelSession(
2698
2812
  adapter,
2699
- (text) => void relay.postMessage(channelId, text),
2813
+ (text, usage) => void relay.postMessage(channelId, text, usage),
2700
2814
  cfg.agentName,
2701
2815
  channelId,
2702
2816
  {
2703
- postReply: (flowId, content) => relay.postFlowMessage(channelId, flowId, content),
2817
+ postReply: (flowId, content, usage) => relay.postFlowMessage(channelId, flowId, content, usage),
2704
2818
  fetchHistory: (flowId) => relay.fetchFlowMessages(channelId, flowId)
2705
2819
  },
2706
2820
  () => 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.7",
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",