@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.
|
|
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.
|
|
23
|
+
"@ccmsg/protocol": "1.15.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "^1.3.0",
|
package/src/messaging/notify.ts
CHANGED
|
@@ -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
|
-
"
|
|
108
|
+
"rate_limited",
|
|
109
109
|
"a watcher is behind on this topic; the notification was not taken",
|
|
110
110
|
);
|
|
111
111
|
}
|
package/src/sessions/items.ts
CHANGED
|
@@ -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;
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
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
|
-
|
|
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(
|
|
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("←",
|
|
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,
|
|
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
|
-
|
|
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
|
|
208
|
-
const
|
|
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
|
}
|