@ccmsg/cli 0.5.2 → 0.6.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/cli",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "The ccmsg daemon, CLI and agent plugins for one instance (= one config home)",
5
5
  "license": "MIT",
6
6
  "author": "kawaz",
@@ -20,7 +20,7 @@
20
20
  "test": "bun test"
21
21
  },
22
22
  "dependencies": {
23
- "@ccmsg/protocol": "1.14.0"
23
+ "@ccmsg/protocol": "1.15.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/bun": "^1.3.0",
@@ -105,7 +105,7 @@ export class Notify implements UpstreamResource {
105
105
  // raise another (§6.4).
106
106
  if (this.deps.publish(NOTIFY, notification, this.deps.self) === "rate_limited") {
107
107
  throw new OpError(
108
- "internal_error",
108
+ "rate_limited",
109
109
  "a watcher is behind on this topic; the notification was not taken",
110
110
  );
111
111
  }
@@ -80,21 +80,18 @@ export function itemsRead(
80
80
 
81
81
  /** Whether the range's end is the part to answer with.
82
82
  *
83
- * A caller that named where to start is reading forward from there; one that
84
- * named only where to stop is looking at the newest of what it asked for, and
85
- * answering with the oldest of that range would hand it the far side of a
86
- * transcript it is walking back through. With neither bound the range is the
87
- * whole transcript, which is read from its beginning. */
83
+ * A caller that named where to start is reading forward from there; anyone
84
+ * else is looking at the newest of what it asked for, and answering with the
85
+ * oldest of that range would hand it the far side of a transcript it is
86
+ * walking back through. Naming no bound at all is the ordinary first read and
87
+ * answers the tail the same way; a caller that wants the transcript from its
88
+ * beginning says so with `since_at: 0`. */
88
89
  function backwards(bounds: TranscriptItemsReadArgs): boolean {
89
90
  const lower =
90
91
  bounds.since_at !== undefined ||
91
92
  bounds.since_uuid !== undefined ||
92
93
  bounds.since_id !== undefined;
93
- const upper =
94
- bounds.until_at !== undefined ||
95
- bounds.until_uuid !== undefined ||
96
- bounds.until_id !== undefined;
97
- return upper && !lower;
94
+ return !lower;
98
95
  }
99
96
 
100
97
  /** As much of the range as one answer carries, and where the next one starts.
@@ -58,9 +58,21 @@ const NOT_ITEMS = new Set([
58
58
 
59
59
  /** The tools that start an agent, in the spellings the harness has used for
60
60
  * the one thing. Both are read the same way: the call is also a brief, and
61
- * what comes back is also an answer. */
61
+ * what comes back is also an answer.
62
+ *
63
+ * They arrive under one type. Two names for one thing would put the same item
64
+ * in the vocabulary twice, and a selection asking for the tool that starts an
65
+ * agent would have to know which spelling this transcript happened to use. The
66
+ * spelling the harness wrote stays on the item as `harness_name`, for a reader
67
+ * matching what it sees against what it ran. */
62
68
  const SPAWNS = new Set(["Agent", "Task"]);
63
69
 
70
+ /** The name an item is typed under, which is the harness's own except where
71
+ * two of its names are one thing. */
72
+ function typedAs(name: string): string {
73
+ return SPAWNS.has(name) ? "Agent" : name;
74
+ }
75
+
64
76
  /** How many calls awaiting an answer one reading holds. Reached only by calls
65
77
  * that are never answered, since an answered one is let go where it is
66
78
  * answered. */
@@ -246,15 +258,18 @@ export class Classification {
246
258
  const id = str(block["id"]) ?? "";
247
259
  const input = row(block["input"]) ?? {};
248
260
  const fields = useFields(name, input);
249
- const tool = make(`tool:${segment(name)}`, {
261
+ const called = typedAs(name);
262
+ const item = make(`tool:${segment(called)}`, {
250
263
  role: "use",
251
264
  tool_use_id: id,
265
+ ...(called === name ? {} : { harness_name: name }),
252
266
  ...(fields ?? { input }),
253
267
  });
254
268
  let message: Draft | undefined;
255
269
  if (SPAWNS.has(name)) {
256
270
  message = make("message:sub:out", {
257
271
  role: "use",
272
+ tool_use_id: id,
258
273
  prompt: str(input["prompt"]) ?? "",
259
274
  ...optional("subagent_type", str(input["subagent_type"])),
260
275
  ...optional("name", str(input["name"])),
@@ -265,7 +280,7 @@ export class Classification {
265
280
  // A sid is the harness's own uuid; anything else is a name, and a name
266
281
  // is how an agent below this session is addressed.
267
282
  make(addressed(to) ? "message:session:out" : "message:sub:out", {
268
- ...(addressed(to) ? {} : { role: "use" }),
283
+ ...(addressed(to) ? {} : { role: "use", tool_use_id: id }),
269
284
  ...(addressed(to)
270
285
  ? { text: text(input["message"]) ?? "", to }
271
286
  : // Writing to an agent is one direction of a correspondence, not a
@@ -287,7 +302,7 @@ export class Classification {
287
302
  const oldest = this.#calls.keys().next();
288
303
  if (oldest.done !== true) this.#calls.delete(oldest.value);
289
304
  }
290
- this.#calls.set(id, { tool, name, ...optional("message", message) });
305
+ this.#calls.set(id, { tool: item, name: called, ...optional("message", message) });
291
306
  }
292
307
 
293
308
  #user(record: Row, make: Make): void {
@@ -317,12 +332,18 @@ export class Classification {
317
332
  const id = str(block["tool_use_id"]) ?? "";
318
333
  const call = this.#calls.get(id);
319
334
  if (call === undefined) {
320
- // An answer to a call this reading never saw. A result item names the
321
- // call it answers and there is no id to name, so what is stated is the
322
- // record itself rather than a pointer to something that does not exist.
323
- // It happens where a reading starts part-way down a file: the whole file
324
- // is read before a dump's range is applied, so the call is there.
325
- make("system:unknown", { record });
335
+ // An answer to a call this reading never saw, which is what a reading
336
+ // that starts part-way down a file meets. The record says which call it
337
+ // answers and never which tool was called, so the type is the reserved
338
+ // name for a result whose tool this instance does not know rather than a
339
+ // name guessed from what came back. What ties it to the call is the key
340
+ // the harness paired them by, which a reader joins against the calls it
341
+ // holds.
342
+ make("tool:unknown", {
343
+ role: "result",
344
+ parent_tool_use_id: id,
345
+ result: genericResult(record["toolUseResult"]),
346
+ });
326
347
  return;
327
348
  }
328
349
  const failed = block["is_error"] === true;
@@ -331,7 +352,7 @@ export class Classification {
331
352
  const item = make(`tool:${segment(call.name)}`, {
332
353
  role: "result",
333
354
  parent_item: call.tool.id,
334
- tool_use_id: id,
355
+ parent_tool_use_id: id,
335
356
  ...(fields ?? { result: genericResult(answer) }),
336
357
  });
337
358
  call.tool["result_item"] = item.id;
@@ -357,6 +378,7 @@ export class Classification {
357
378
  const reply = make("message:sub:in", {
358
379
  role: "result",
359
380
  parent_item: call.message.id,
381
+ parent_tool_use_id: id,
360
382
  text: said,
361
383
  ...optional("agent_id", agent),
362
384
  ...optional("status", str(result["status"])),
@@ -452,6 +474,7 @@ export class Classification {
452
474
  const item = make("message:sub:in", {
453
475
  role: "result",
454
476
  parent_item: asked.id,
477
+ parent_tool_use_id: key,
455
478
  text: answer,
456
479
  ...optional("agent_id", str(asked["agent_id"]) ?? tagged(said, "task-id")),
457
480
  ...optional("status", tagged(said, "status")),
@@ -44,7 +44,15 @@ export function document(file: SessionDumpFile, view: DumpView = {}): string {
44
44
  if (paired.folded.has(at)) continue;
45
45
  const item = items[at] as Item;
46
46
  const child = paired.child.get(at);
47
- lines.push(...draw(item, child === undefined ? undefined : (items[child] as Item), view), "");
47
+ lines.push(
48
+ ...draw(
49
+ item,
50
+ child === undefined ? undefined : (items[child] as Item),
51
+ view,
52
+ paired.parent.get(at),
53
+ ),
54
+ "",
55
+ );
48
56
  }
49
57
  lines.push(...ledger(file.ids));
50
58
  return `${lines.join("\n").trimEnd()}\n`;
@@ -72,12 +80,12 @@ function heading(file: SessionDumpFile, view: DumpView): string[] {
72
80
  * A call keeps its own heading and the answer's words are put at the end of
73
81
  * it, so `→` reads as "and this came back". An answer drawn where it arrived
74
82
  * points the other way, at a call the reader has already gone past. */
75
- function draw(item: Item, child: Item | undefined, view: DumpView): string[] {
83
+ function draw(item: Item, child: Item | undefined, view: DumpView, parent?: string): string[] {
76
84
  const own = fragment(item);
77
85
  const answer = child === undefined ? undefined : fragment(child);
78
86
  const nested = child !== undefined && child.type.startsWith("message:sub");
79
87
  const link = isResult(item)
80
- ? arrow("←", fields(item)["parent_item"])
88
+ ? arrow("←", parent)
81
89
  : (arrow("→", fields(item)["result_item"]) ?? waiting(item));
82
90
  const head = isResult(item)
83
91
  ? words(prefix(item), link, own.head, clock(item))
@@ -185,27 +193,49 @@ function cell(text: string): string {
185
193
  return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
186
194
  }
187
195
 
188
- /** Which answer belongs to which call, and which of those are drawn together.
196
+ /** Which answer belongs to which call, which call each answer points back at,
197
+ * and which of those pairs are drawn together.
189
198
  *
190
199
  * An answer names the call it answers, so the matching is a lookup: no two
191
200
  * calls in one record are confused for one another, and a tool and an agent
192
201
  * are paired by the same rule rather than by the ids each of them happens to
193
- * carry. */
202
+ * carry. An answer read where its call was not says instead which key the
203
+ * harness paired them by, and the call that names that key is the one it
204
+ * belongs to. */
194
205
  function pair(items: readonly Item[]): {
195
206
  child: Map<number, number>;
196
207
  folded: Set<number>;
208
+ parent: Map<number, string>;
197
209
  } {
198
210
  const child = new Map<number, number>();
199
211
  const folded = new Set<number>();
212
+ const parent = new Map<number, string>();
200
213
  const where = new Map<string, number>();
201
- for (let at = 0; at < items.length; at += 1) where.set((items[at] as Item).id, at);
214
+ const called = new Map<string, string>();
215
+ for (let at = 0; at < items.length; at += 1) {
216
+ const item = items[at] as Item;
217
+ where.set(item.id, at);
218
+ const key = fields(item)["tool_use_id"];
219
+ if (fields(item)["role"] === "use" && typeof key === "string" && key !== "") {
220
+ called.set(joined(item, key), item.id);
221
+ }
222
+ }
202
223
  for (let at = 0; at < items.length; at += 1) {
203
224
  const item = items[at] as Item;
204
225
  // Which half of an exchange this is, which the contract calls an item's
205
226
  // role and nothing here confuses with who is allowed to ask for one.
206
227
  if (fields(item)["role"] !== "result") continue;
207
- const parent = fields(item)["parent_item"];
208
- const call = typeof parent === "string" ? where.get(parent) : undefined;
228
+ const named = fields(item)["parent_item"];
229
+ const key = fields(item)["parent_tool_use_id"];
230
+ const to =
231
+ typeof named === "string"
232
+ ? named
233
+ : typeof key === "string"
234
+ ? called.get(joined(item, key))
235
+ : undefined;
236
+ if (to === undefined) continue;
237
+ parent.set(at, to);
238
+ const call = where.get(to);
209
239
  if (call === undefined) continue;
210
240
  // A pair the reader would have to scroll between is left where each half
211
241
  // happened, unless it is an agent's: what an agent was asked and what it
@@ -214,5 +244,14 @@ function pair(items: readonly Item[]): {
214
244
  child.set(call, at);
215
245
  folded.add(at);
216
246
  }
217
- return { child, folded };
247
+ return { child, folded, parent };
248
+ }
249
+
250
+ /** The key an exchange is joined on. One call the harness gave a key to is two
251
+ * items where it started an agent — the call and the brief beside it — so the
252
+ * side of the exchange goes into the key: a tool's answer belongs to the call
253
+ * and an agent's to the brief, and the harness's key alone would not say
254
+ * which. */
255
+ function joined(item: Item, key: string): string {
256
+ return `${item.type.startsWith("message:sub") ? "sub" : "tool"}\n${key}`;
218
257
  }