@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.
- package/dist/cli.js +130 -16
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2523
|
+
const result = await client.submit(text);
|
|
2422
2524
|
this.consecutiveRestarts = 0;
|
|
2423
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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",
|