@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.
- package/dist/cli.js +134 -17
- 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
|
}
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
2526
|
+
const result = await client.submit(text);
|
|
2422
2527
|
this.consecutiveRestarts = 0;
|
|
2423
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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",
|