@snowyroad/arp 0.4.0 → 0.5.1

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 +74 -23
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -781,14 +781,14 @@ ${fence("peer roster", joinUntrusted(lines, "\n"))}`;
781
781
  }
782
782
 
783
783
  // src/channelContext.ts
784
- var MAX_INJECTED_MEMORY_CHARS = 8e3;
784
+ var MAX_INJECTED_INSTRUCTIONS_CHARS = 8e3;
785
785
  var MAX_INJECTED_SOURCE_CHARS = 8e3;
786
786
  var MAX_INJECTED_SOURCES_TOTAL_CHARS = 24e3;
787
787
  function buildChannelContext(input) {
788
788
  let out = "";
789
- if (!isBlankText(input.memory)) {
790
- const raw = rawUntrusted(input.memory);
791
- const bounded = raw.length > MAX_INJECTED_MEMORY_CHARS ? untrusted(raw.slice(0, MAX_INJECTED_MEMORY_CHARS) + "\n[...truncated]") : input.memory;
789
+ if (!isBlankText(input.instructions)) {
790
+ const raw = rawUntrusted(input.instructions);
791
+ const bounded = raw.length > MAX_INJECTED_INSTRUCTIONS_CHARS ? untrusted(raw.slice(0, MAX_INJECTED_INSTRUCTIONS_CHARS) + "\n[...truncated]") : input.instructions;
792
792
  out += `## Channel Instructions (standing guidance for this channel)
793
793
  ${fence("channel instructions", bounded)}
794
794
  ---
@@ -1387,6 +1387,14 @@ var RelayClient = class {
1387
1387
  activity: catchingUp ? "catching_up" : "thinking"
1388
1388
  });
1389
1389
  }
1390
+ /** Forward one normalized agent-activity event (Agent Activity View, slice 1) to the
1391
+ * relay over the agent WS. The emitted frame is the normalized ActivityEvent spread
1392
+ * out with `type: "activity_event"` and the channelId added on top — exactly the shape
1393
+ * the relay ingest parses. No-op if the socket is closed (send() guards readyState),
1394
+ * matching sendActivity: a dropped activity event must never break a turn. */
1395
+ sendActivityEvent(channelId, event) {
1396
+ this.send({ type: "activity_event", channelId, ...event });
1397
+ }
1390
1398
  /** Publish this agent's partial A2A card; the relay fills url/version/provider. */
1391
1399
  async putAgentCard(card) {
1392
1400
  const url = `${this.cfg.relayHttpUrl}/agents/me/agent-card`;
@@ -1440,7 +1448,7 @@ var RelayClient = class {
1440
1448
  if (u.thoughtTokens !== void 0) b.thoughtTokens = u.thoughtTokens;
1441
1449
  return b;
1442
1450
  }
1443
- async postMessage(channelId, content, usage) {
1451
+ async postMessage(channelId, content, usage, turnId) {
1444
1452
  const ch = this.pathId(channelId, "channelId");
1445
1453
  if (!ch) return;
1446
1454
  const url = `${this.cfg.relayHttpUrl}/channels/${ch}/messages`;
@@ -1451,6 +1459,9 @@ var RelayClient = class {
1451
1459
  agentId: this.cfg.agentUuid,
1452
1460
  agentName: this.cfg.agentName,
1453
1461
  messageType: "agent",
1462
+ // Carry the turn's id so the relay/web can correlate this message with the
1463
+ // turn's activity events (Agent Activity View). Omit when absent.
1464
+ ...turnId ? { turnId } : {},
1454
1465
  ...this.usageBody(usage)
1455
1466
  });
1456
1467
  try {
@@ -1494,21 +1505,21 @@ var RelayClient = class {
1494
1505
  console.warn("[arp-bridge] flow post failed:", sanitizeForTty(String(err)));
1495
1506
  }
1496
1507
  }
1497
- /** Channel memory text (empty if none or on error — never throws). Branded (H2-4). */
1498
- async fetchChannelMemory(channelId) {
1508
+ /** Channel instructions text (empty if none or on error — never throws). Branded (H2-4). */
1509
+ async fetchChannelInstructions(channelId) {
1499
1510
  const ch = this.pathId(channelId, "channelId");
1500
1511
  if (!ch) return untrusted("");
1501
- const url = `${this.cfg.relayHttpUrl}/channels/${ch}/memory`;
1512
+ const url = `${this.cfg.relayHttpUrl}/channels/${ch}/instructions`;
1502
1513
  try {
1503
1514
  const res = await this.deps.fetchFn(url, { headers: { Authorization: `Bearer ${this.cfg.token}` } });
1504
1515
  if (!res.ok) {
1505
- console.warn("[arp-bridge] memory HTTP", res.status);
1516
+ console.warn("[arp-bridge] instructions HTTP", res.status);
1506
1517
  return untrusted("");
1507
1518
  }
1508
1519
  const data = await res.json();
1509
1520
  return untrusted(typeof data?.content === "string" ? data.content : "");
1510
1521
  } catch (err) {
1511
- console.warn("[arp-bridge] memory fetch failed:", sanitizeForTty(String(err)));
1522
+ console.warn("[arp-bridge] instructions fetch failed:", sanitizeForTty(String(err)));
1512
1523
  return untrusted("");
1513
1524
  }
1514
1525
  }
@@ -1593,18 +1604,18 @@ var RelayClient = class {
1593
1604
  return null;
1594
1605
  }
1595
1606
  }
1596
- /** Assemble the situational channel-context block (memory + sources + topics) for a
1607
+ /** Assemble the situational channel-context block (instructions + sources + topics) for a
1597
1608
  * passive message. Parallel fetch with per-source graceful degradation (each fetcher
1598
1609
  * swallows its own errors). Returns "" when there is nothing to inject.
1599
1610
  * The fetchers return raw structured data; untrusted-data fencing happens ONCE, in
1600
1611
  * buildChannelContext (the single-layer rule, see untrusted.ts). Do not fence here. */
1601
1612
  async fetchChannelContext(channelId) {
1602
- const [memory, topics, pins] = await Promise.all([
1603
- this.fetchChannelMemory(channelId),
1613
+ const [instructions, topics, pins] = await Promise.all([
1614
+ this.fetchChannelInstructions(channelId),
1604
1615
  this.fetchChannelTopics(channelId),
1605
1616
  this.fetchSourcesContext(channelId)
1606
1617
  ]);
1607
- return buildChannelContext({ memory, topics, pins });
1618
+ return buildChannelContext({ instructions, topics, pins });
1608
1619
  }
1609
1620
  /** Fetch a flow's transcript (used to backfill a minimal turn_notification). [] on error. */
1610
1621
  async fetchFlowMessages(channelId, flowId) {
@@ -1738,10 +1749,10 @@ ${toolStatusLine(this.toolMode)}
1738
1749
  }
1739
1750
  async start(opts) {
1740
1751
  this.session = await this.adapter.start(opts);
1741
- this.session.onTurn((full, usage) => {
1752
+ this.session.onTurn((full, usage, turnId) => {
1742
1753
  this.beacon?.end();
1743
1754
  if (full.replace(/<<silent>>/gi, "").trim() === "") return;
1744
- this.onReply(full.replace(/^\s*(?:<<silent>>\s*)+/i, "").trim(), usage);
1755
+ this.onReply(full.replace(/^\s*(?:<<silent>>\s*)+/i, "").trim(), usage, turnId);
1745
1756
  });
1746
1757
  }
1747
1758
  /**
@@ -1962,6 +1973,7 @@ function dropVendorNotifications(input) {
1962
1973
  }
1963
1974
 
1964
1975
  // src/acp/client.ts
1976
+ import { randomUUID as randomUUID2 } from "crypto";
1965
1977
  var MODEL_AUTH_ENV_KEYS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
1966
1978
  var BRIDGE_ENV_PREFIX = "ARP_";
1967
1979
  function buildAcpEnv(base, extra) {
@@ -2105,6 +2117,9 @@ var AcpClient = class {
2105
2117
  this.activeTurnBuffer.usage.costCurrency = u.cost.currency;
2106
2118
  }
2107
2119
  }
2120
+ if ((u.sessionUpdate === "tool_call" || u.sessionUpdate === "tool_call_update") && this.launch.onActivity && this.activeTurnBuffer) {
2121
+ this.emitActivity(u.sessionUpdate, u, this.activeTurnBuffer.turnId);
2122
+ }
2108
2123
  },
2109
2124
  requestPermission: async (req) => {
2110
2125
  const verdict = evaluateAcpPermission(this.policy.mode, this.policy.configDirAbs, req);
@@ -2181,12 +2196,41 @@ var AcpClient = class {
2181
2196
  });
2182
2197
  return run;
2183
2198
  }
2199
+ /**
2200
+ * Normalize one ACP tool-call update (metadata only) and hand it to the activity
2201
+ * sink, stamped with this turn's turnId. Never throws: a sink fault is caught and
2202
+ * logged so it cannot break the turn. SLICE 1: deliberately drops content,
2203
+ * rawInput, and rawOutput — only kind/title/status/locations are carried.
2204
+ */
2205
+ emitActivity(eventType, u, turnId) {
2206
+ const sink = this.launch.onActivity;
2207
+ if (!sink) return;
2208
+ const locations = u.locations && u.locations.length > 0 ? u.locations.map((l) => l.line == null ? { path: l.path } : { path: l.path, line: l.line }) : null;
2209
+ const event = {
2210
+ turnId,
2211
+ toolCallId: u.toolCallId,
2212
+ eventType,
2213
+ kind: u.kind ?? null,
2214
+ title: u.title ?? null,
2215
+ status: u.status ?? null,
2216
+ locations,
2217
+ ts: Date.now()
2218
+ };
2219
+ try {
2220
+ sink(event);
2221
+ } catch (err) {
2222
+ console.warn(
2223
+ `[arp-bridge] onActivity sink threw (ignored): ${sanitizeForTty(String(err?.message ?? err))}`
2224
+ );
2225
+ }
2226
+ }
2184
2227
  /** Execute exactly one prompt turn with its own isolated reply buffer. */
2185
2228
  async runTurn(text) {
2186
2229
  if (!this.conn || !this._sessionId) {
2187
2230
  throw new Error("AcpClient.submit called before start()");
2188
2231
  }
2189
- const buffer = { text: "" };
2232
+ const turnId = randomUUID2();
2233
+ const buffer = { text: "", turnId };
2190
2234
  this.activeTurnBuffer = buffer;
2191
2235
  try {
2192
2236
  const resp = await this.guard(
@@ -2209,7 +2253,7 @@ var AcpClient = class {
2209
2253
  }
2210
2254
  };
2211
2255
  }
2212
- return { text: buffer.text, usage: buffer.usage };
2256
+ return { text: buffer.text, usage: buffer.usage, turnId };
2213
2257
  } finally {
2214
2258
  if (this.activeTurnBuffer === buffer) this.activeTurnBuffer = null;
2215
2259
  }
@@ -2480,6 +2524,9 @@ var AcpAdapter = class {
2480
2524
  cwd,
2481
2525
  mcpServers: opts.mcpServers,
2482
2526
  env: { ...this.launch.env, ...opts.env },
2527
+ // Agent Activity View: forward normalized tool-call events to the bridge-supplied
2528
+ // sink (wired to RelayClient.sendActivityEvent). Absent => no activity surfaced.
2529
+ onActivity: opts.onActivity,
2483
2530
  session: this.session ? {
2484
2531
  persistedId: rec?.sessionId ?? null,
2485
2532
  save: (id) => this.session.save(mergeSessionPointer(this.session.load(), id, cwd))
@@ -2547,7 +2594,7 @@ var AcpAdapter = class {
2547
2594
  const result = await client.submit(text);
2548
2595
  this.consecutiveRestarts = 0;
2549
2596
  const usage = this.usageSource?.forTurn(result.usage);
2550
- this.turnCbs.forEach((cb) => cb(result.text, usage));
2597
+ this.turnCbs.forEach((cb) => cb(result.text, usage, result.turnId));
2551
2598
  return true;
2552
2599
  } catch (err) {
2553
2600
  if (this.stopped) {
@@ -2830,7 +2877,7 @@ import http from "http";
2830
2877
  import os from "os";
2831
2878
  import path from "path";
2832
2879
  import fs from "fs";
2833
- import { randomUUID as randomUUID2, randomBytes } from "crypto";
2880
+ import { randomUUID as randomUUID3, randomBytes } from "crypto";
2834
2881
  var SourceBroker = class {
2835
2882
  constructor(channelId, reader) {
2836
2883
  this.channelId = channelId;
@@ -2843,7 +2890,7 @@ var SourceBroker = class {
2843
2890
  socketPath = "";
2844
2891
  async start() {
2845
2892
  if (this.server) throw new Error("SourceBroker already started");
2846
- this.socketPath = path.join(os.tmpdir(), `arp-broker-${randomUUID2()}.sock`);
2893
+ this.socketPath = path.join(os.tmpdir(), `arp-broker-${randomUUID3()}.sock`);
2847
2894
  try {
2848
2895
  fs.unlinkSync(this.socketPath);
2849
2896
  } catch {
@@ -2939,7 +2986,7 @@ async function startBridge(cfg, relay, deps) {
2939
2986
  const beacon = new ActivityBeacon((state) => relay.sendActivity(channelId, state));
2940
2987
  const session = new ChannelSession(
2941
2988
  adapter,
2942
- (text, usage) => void relay.postMessage(channelId, text, usage),
2989
+ (text, usage, turnId) => void relay.postMessage(channelId, text, usage, turnId),
2943
2990
  cfg.agentName,
2944
2991
  channelId,
2945
2992
  {
@@ -2962,7 +3009,11 @@ async function startBridge(cfg, relay, deps) {
2962
3009
  ]
2963
3010
  }];
2964
3011
  try {
2965
- await session.start({ model: cfg.model, mcpServers });
3012
+ await session.start({
3013
+ model: cfg.model,
3014
+ mcpServers,
3015
+ onActivity: (event) => relay.sendActivityEvent(channelId, event)
3016
+ });
2966
3017
  } catch (err) {
2967
3018
  void broker.stop();
2968
3019
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowyroad/arp",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
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",