@ccmsg/cli 0.8.2 → 0.9.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.
- package/package.json +2 -2
- package/src/auth/auth.ts +14 -14
- package/src/auth/http.ts +6 -6
- package/src/auth/records.ts +1 -1
- package/src/auth/topic.ts +1 -1
- package/src/cli.ts +21 -24
- package/src/daemon/control.ts +2 -2
- package/src/daemon/registry.ts +7 -7
- package/src/daemon/snapshot.ts +12 -10
- package/src/daemon/supervise.ts +1 -1
- package/src/files/containment.ts +5 -5
- package/src/files/files.ts +12 -12
- package/src/files/sandbox.ts +0 -0
- package/src/greeting/meta.ts +5 -2
- package/src/instance/config.ts +21 -9
- package/src/instance/instance.ts +15 -13
- package/src/kv/store.ts +4 -3
- package/src/launcher/launcher.ts +3 -3
- package/src/mesh/mesh.ts +7 -8
- package/src/mesh/relay.ts +1 -1
- package/src/messaging/delivery.ts +4 -4
- package/src/messaging/direct.ts +2 -2
- package/src/messaging/handlers.ts +4 -4
- package/src/messaging/inbox.ts +1 -1
- package/src/messaging/notify.ts +11 -11
- package/src/sessions/dump.ts +1 -1
- package/src/sessions/handlers.ts +17 -17
- package/src/sessions/harness.ts +1 -1
- package/src/sessions/last-live.ts +1 -1
- package/src/sessions/registry.ts +60 -77
- package/src/sessions/search.ts +1 -1
- package/src/sessions/status.ts +7 -7
- package/src/topics/handlers.ts +2 -2
- package/src/topics/topics.ts +0 -0
- package/src/transcript/items/classify.ts +42 -37
- package/src/transcript/items/document.ts +2 -2
- package/src/transcript/items/ids.ts +0 -0
- package/src/transcript/items/render.ts +24 -24
- package/src/transcript/items/select.ts +5 -5
- package/src/transcript/transcripts.ts +3 -3
- package/src/translate/translate.ts +1 -1
- package/src/transport/driver.ts +13 -11
- package/src/upstream/events.ts +108 -10
- package/src/upstream/gateway.ts +10 -5
- package/src/upstream/requests.ts +103 -9
- package/src/upstream/status.ts +1 -1
- package/src/version.ts +1 -1
package/src/sessions/registry.ts
CHANGED
|
@@ -3,8 +3,10 @@ import { basename, dirname, isAbsolute, join } from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
type AgentInfo,
|
|
5
5
|
type Capability,
|
|
6
|
-
type
|
|
6
|
+
type HelloInstanceArgs,
|
|
7
7
|
type HelloResult,
|
|
8
|
+
type HelloSessionArgs,
|
|
9
|
+
type HelloUserArgs,
|
|
8
10
|
type Endpoint,
|
|
9
11
|
type AgentElement,
|
|
10
12
|
type InstanceId,
|
|
@@ -115,7 +117,7 @@ export interface MeshSource {
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
/** The mesh claim a peer greets with, as the contract states it. */
|
|
118
|
-
type MeshClaim =
|
|
120
|
+
type MeshClaim = HelloInstanceArgs["mesh"];
|
|
119
121
|
|
|
120
122
|
/** The fold, as the sessions domain reads it: two values about one session,
|
|
121
123
|
* asked for when a payload is built rather than copied here when they change
|
|
@@ -137,7 +139,7 @@ export interface GatewaySource {
|
|
|
137
139
|
* are the same fields under the same names — nothing is renamed on the way
|
|
138
140
|
* through, and nothing is invented for a field the session left unsaid. */
|
|
139
141
|
type SessionMeta = Pick<
|
|
140
|
-
|
|
142
|
+
HelloSessionArgs,
|
|
141
143
|
"repo" | "ws" | "cwd" | "transcript_path" | "repo_root" | "branch" | "title" | "model" | "effort"
|
|
142
144
|
>;
|
|
143
145
|
|
|
@@ -208,7 +210,7 @@ export class Sessions implements UpstreamResource {
|
|
|
208
210
|
* Held here rather than written to `last_live`, because the declaration
|
|
209
211
|
* arrives while the session is still connected and `last_live` holds what is
|
|
210
212
|
* gone: the entry is written when the connection closes, and this is what
|
|
211
|
-
* stamps it then (contract, `
|
|
213
|
+
* stamps it then (contract, `session.stopping`). A session that declares and
|
|
212
214
|
* then carries on stays connected and keeps its declaration, which is spent
|
|
213
215
|
* whenever it does leave. */
|
|
214
216
|
readonly #stopping = new Map<Sid, Timestamp>();
|
|
@@ -252,55 +254,62 @@ export class Sessions implements UpstreamResource {
|
|
|
252
254
|
for (const sid of live.keys()) this.#lastLive.remove(sid);
|
|
253
255
|
}
|
|
254
256
|
|
|
255
|
-
/** `hello`, which is where a session becomes something this instance
|
|
256
|
-
* speak about, and where everything this instance knows about where that
|
|
257
|
-
* session lives comes from.
|
|
257
|
+
/** `hello.session`, which is where a session becomes something this instance
|
|
258
|
+
* can speak about, and where everything this instance knows about where that
|
|
259
|
+
* session lives comes from. The greeting names its sid because the op it
|
|
260
|
+
* arrived under is the one whose schema asks for one. */
|
|
261
|
+
helloSession = (input: HandlerInput): HelloResult => {
|
|
262
|
+
const args = input.args as unknown as HelloSessionArgs;
|
|
263
|
+
this.#greetable(input, args.protocol_version);
|
|
264
|
+
this.register(args.sid, args);
|
|
265
|
+
input.conn.onClose(() => this.release(args.sid));
|
|
266
|
+
return this.#greeted(input);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/** `hello.user`. A person speaks for no session, so there is nothing to
|
|
270
|
+
* register: the greeting settles a role and answers what the instance is. */
|
|
271
|
+
helloUser = (input: HandlerInput): HelloResult => {
|
|
272
|
+
const args = input.args as unknown as HelloUserArgs;
|
|
273
|
+
this.#greetable(input, args.protocol_version);
|
|
274
|
+
return this.#greeted(input);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/** `hello.instance`. A peer's greeting is answered only once the connection
|
|
278
|
+
* has been proven to be the endpoint it names. The verification rejects when
|
|
279
|
+
* it is not, and the connection stays anonymous because nothing settles an
|
|
280
|
+
* identity but a reply (mesh-peer-auth §5, daemon-v2 §3.2 step 7). This is
|
|
281
|
+
* the one greeting that has to wait for something, which is why it is the one
|
|
282
|
+
* that answers with a promise. */
|
|
283
|
+
helloInstance = (input: HandlerInput): Promise<HelloResult> => {
|
|
284
|
+
const args = input.args as unknown as HelloInstanceArgs;
|
|
285
|
+
this.#greetable(input, args.protocol_version);
|
|
286
|
+
const mesh = this.deps.mesh;
|
|
287
|
+
if (mesh === undefined) {
|
|
288
|
+
throw new OpError(
|
|
289
|
+
"capability_unavailable",
|
|
290
|
+
"this instance has no mesh, so no peer connection can be proven",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return mesh.greet(input.conn, args.mesh).then(() => this.#greeted(input));
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/** What each of the three greetings checks before it settles anything.
|
|
258
297
|
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
|
|
263
|
-
hello = (input: HandlerInput): HelloResult | Promise<HelloResult> => {
|
|
264
|
-
const args = input.args as unknown as HelloArgs;
|
|
265
|
-
// A role is set once and fixed for the connection's life (contract, `Role`),
|
|
266
|
-
// so a second greeting is not a re-identification: it is a request to be
|
|
267
|
-
// somebody else on a connection that already is somebody.
|
|
298
|
+
* A role is set once and fixed for the connection's life (contract, `Role`),
|
|
299
|
+
* so a second greeting is not a re-identification: it is a request to be
|
|
300
|
+
* somebody else on a connection that already is somebody. */
|
|
301
|
+
#greetable(input: HandlerInput, protocolVersion: number): void {
|
|
268
302
|
if (input.conn.identity.state === "settled") {
|
|
269
303
|
throw new OpError("bad_request", "a connection greets once, and this one already has");
|
|
270
304
|
}
|
|
271
|
-
if (
|
|
305
|
+
if (protocolVersion !== PROTOCOL_VERSION) {
|
|
272
306
|
throw new OpError("bad_request", `this instance speaks protocol ${PROTOCOL_VERSION}`);
|
|
273
307
|
}
|
|
274
|
-
|
|
275
|
-
// A peer's greeting is answered only once the connection has been proven
|
|
276
|
-
// to be the endpoint it names. The verification rejects when it is not,
|
|
277
|
-
// and the connection stays anonymous because nothing settles an identity
|
|
278
|
-
// but a reply (mesh-peer-auth §5, daemon-v2 §3.2 step 7). This is the one
|
|
279
|
-
// greeting that has to wait for something, which is why it is the one
|
|
280
|
-
// that answers with a promise.
|
|
281
|
-
if (args.mesh === undefined) {
|
|
282
|
-
throw new OpError("invalid_args", "an instance greets with its mesh claim");
|
|
283
|
-
}
|
|
284
|
-
const mesh = this.deps.mesh;
|
|
285
|
-
if (mesh === undefined) {
|
|
286
|
-
throw new OpError(
|
|
287
|
-
"capability_unavailable",
|
|
288
|
-
"this instance has no mesh, so no peer connection can be proven",
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
return mesh.greet(input.conn, args.mesh).then(() => this.#greeted(args, input));
|
|
292
|
-
}
|
|
293
|
-
return this.#greeted(args, input);
|
|
294
|
-
};
|
|
308
|
+
}
|
|
295
309
|
|
|
296
310
|
/** What every greeting answers, once whatever had to be settled has been. */
|
|
297
|
-
#greeted(
|
|
311
|
+
#greeted(input: HandlerInput): HelloResult {
|
|
298
312
|
const expiresAt = this.deps.authExpiresAt?.(input.conn);
|
|
299
|
-
const sid = requiredSid(args);
|
|
300
|
-
if (sid !== undefined) {
|
|
301
|
-
this.register(sid, args);
|
|
302
|
-
input.conn.onClose(() => this.release(sid));
|
|
303
|
-
}
|
|
304
313
|
return {
|
|
305
314
|
protocol_version: PROTOCOL_VERSION,
|
|
306
315
|
instance: this.deps.self,
|
|
@@ -439,7 +448,7 @@ export class Sessions implements UpstreamResource {
|
|
|
439
448
|
}
|
|
440
449
|
|
|
441
450
|
/** Drop one entry from `last_live`, which is what
|
|
442
|
-
* `
|
|
451
|
+
* `session.forget` asks for. The removal touches that list alone:
|
|
443
452
|
* the session stays resumable by every other route. */
|
|
444
453
|
forget(sid: Sid): boolean {
|
|
445
454
|
const removed = this.#lastLive.remove(sid);
|
|
@@ -453,7 +462,7 @@ export class Sessions implements UpstreamResource {
|
|
|
453
462
|
this.changed();
|
|
454
463
|
}
|
|
455
464
|
|
|
456
|
-
/** `
|
|
465
|
+
/** `session.stopping`: a session saying it is about to go, which is what
|
|
457
466
|
* makes it Paused rather than Disappeared once it is gone (§5.2).
|
|
458
467
|
*
|
|
459
468
|
* Nothing is recorded now and nothing is published: the session is still
|
|
@@ -600,7 +609,7 @@ export class Sessions implements UpstreamResource {
|
|
|
600
609
|
* silence for a retraction would let each of them erase what the last one
|
|
601
610
|
* knew, and the session would be described by whichever process spoke most
|
|
602
611
|
* recently rather than by everything it has said. */
|
|
603
|
-
private register(sid: Sid, args:
|
|
612
|
+
private register(sid: Sid, args: HelloSessionArgs): void {
|
|
604
613
|
const now = Date.now();
|
|
605
614
|
const held = this.#connected.get(sid);
|
|
606
615
|
const meta = {
|
|
@@ -652,7 +661,7 @@ export class Sessions implements UpstreamResource {
|
|
|
652
661
|
for (const [sid, entry] of this.#live) {
|
|
653
662
|
if (live.has(sid)) continue;
|
|
654
663
|
// The declaration came first and the departure has now arrived, which is
|
|
655
|
-
// the order the two are one event in (contract, `
|
|
664
|
+
// the order the two are one event in (contract, `session.stopping`).
|
|
656
665
|
const stoppedAt = this.#stopping.get(sid);
|
|
657
666
|
this.#stopping.delete(sid);
|
|
658
667
|
this.#lastLive.record({
|
|
@@ -769,11 +778,11 @@ export class Sessions implements UpstreamResource {
|
|
|
769
778
|
* id, so what it reports is not by itself evidence about *this* instance's
|
|
770
779
|
* sessions: a session id belonging to another config home would otherwise
|
|
771
780
|
* classify as live here, put a row on this instance's `peers`, and make
|
|
772
|
-
* `
|
|
781
|
+
* `message.send` accept a message for a session that has no inbox here and
|
|
773
782
|
* never will. So the reading is narrowed to the sids this instance knows —
|
|
774
783
|
* one that has greeted us, still connected or remembered in `last_live`, or
|
|
775
784
|
* one the harness's own `sessions/` names. The events themselves are not
|
|
776
|
-
* dropped: `
|
|
785
|
+
* dropped: `llm.requests` carries what the gateway saw whoever it was for,
|
|
777
786
|
* because that topic is a view of the gateway rather than of this instance's
|
|
778
787
|
* sessions. */
|
|
779
788
|
#gatewayActiveAt(sid: Sid, inHarness: boolean): Timestamp | undefined {
|
|
@@ -834,7 +843,7 @@ export class Sessions implements UpstreamResource {
|
|
|
834
843
|
* that is simply absent from what `peers` says. */
|
|
835
844
|
function metaOf(
|
|
836
845
|
deps: Pick<SessionsDeps, "configHome" | "harness">,
|
|
837
|
-
args:
|
|
846
|
+
args: HelloSessionArgs,
|
|
838
847
|
refused: (reason: string) => void,
|
|
839
848
|
): SessionMeta {
|
|
840
849
|
const meta: Record<string, string> = {};
|
|
@@ -924,29 +933,3 @@ function resolveAsFarAsItGoes(path: string): string | undefined {
|
|
|
924
933
|
}
|
|
925
934
|
}
|
|
926
935
|
}
|
|
927
|
-
|
|
928
|
-
/** What each role must and must not say when it greets.
|
|
929
|
-
*
|
|
930
|
-
* The sid is what registers a session, so which role is entitled to name one is
|
|
931
|
-
* decided here rather than left to whoever reads the field: a `user` naming a
|
|
932
|
-
* sid would be a person registering as the session, and a `session` without one
|
|
933
|
-
* is a session this instance cannot speak about. */
|
|
934
|
-
function requiredSid(args: HelloArgs): Sid | undefined {
|
|
935
|
-
switch (args.role) {
|
|
936
|
-
case "session":
|
|
937
|
-
if (args.sid === undefined) throw new OpError("invalid_args", "a session names its sid");
|
|
938
|
-
return args.sid;
|
|
939
|
-
case "user":
|
|
940
|
-
if (args.sid !== undefined) {
|
|
941
|
-
throw new OpError("invalid_args", "a sid is the greeting of a session, not of a person");
|
|
942
|
-
}
|
|
943
|
-
return undefined;
|
|
944
|
-
case "instance":
|
|
945
|
-
// A peer speaks for no session: what it is has already been settled by
|
|
946
|
-
// the handshake, and a sid here would be it registering as one.
|
|
947
|
-
if (args.sid !== undefined) {
|
|
948
|
-
throw new OpError("invalid_args", "a sid is the greeting of a session, not of an instance");
|
|
949
|
-
}
|
|
950
|
-
return undefined;
|
|
951
|
-
}
|
|
952
|
-
}
|
package/src/sessions/search.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { readRecord, type TranscriptFile, type TranscriptFiles } from "../transc
|
|
|
20
20
|
const SCAN_BUDGET_BYTES = 64 * 1024 * 1024;
|
|
21
21
|
const HITS = 50;
|
|
22
22
|
/** What one hit shows of what it matched. Enough to recognise the passage;
|
|
23
|
-
* the transcript itself is one `
|
|
23
|
+
* the transcript itself is one `transcript.read` away. */
|
|
24
24
|
const MATCHES_PER_HIT = 5;
|
|
25
25
|
const MATCH_CHARS = 400;
|
|
26
26
|
|
package/src/sessions/status.ts
CHANGED
|
@@ -14,14 +14,14 @@ import { workspaceFolders } from "./workspace.ts";
|
|
|
14
14
|
/** What the fold says stopped a session, read in one place.
|
|
15
15
|
*
|
|
16
16
|
* Three values rest on it: whether a live session is Waiting (§5.2), what
|
|
17
|
-
* `
|
|
17
|
+
* `session.errors` lists, and the `api_error` of `session.status:<sid>`. They
|
|
18
18
|
* ask this rather than each reading the fold's field, so the three cannot come
|
|
19
19
|
* to different answers about the same session (§7.4, M5). */
|
|
20
20
|
export function stoppedOn(facts: TranscriptFacts): SessionApiError | undefined {
|
|
21
21
|
return facts.api_error;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/** The `
|
|
24
|
+
/** The `session.status:<sid>` payload.
|
|
25
25
|
*
|
|
26
26
|
* Almost every field is the fold's, stated as the fold left it: one pass over
|
|
27
27
|
* the transcript settles the error, the task list, the files it named and what
|
|
@@ -61,7 +61,7 @@ export function sessionStatusOf(
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** Where a session works, as it greeted (§5.1). The same two values the file
|
|
64
|
-
* surfaces are decided against, asked for here so that what `
|
|
64
|
+
* surfaces are decided against, asked for here so that what `session.status`
|
|
65
65
|
* says and what a read is admitted by come from one answer. */
|
|
66
66
|
export interface SessionWhere {
|
|
67
67
|
readonly root?: string;
|
|
@@ -88,7 +88,7 @@ export interface SessionStatusDeps {
|
|
|
88
88
|
/** The two topics the fold's error state feeds, and the tails they keep
|
|
89
89
|
* running (§6.3).
|
|
90
90
|
*
|
|
91
|
-
* `
|
|
91
|
+
* `session.errors` is one list for the instance and `session.status:<sid>` is
|
|
92
92
|
* one session, so what they hold differs: the first wants every session's fold
|
|
93
93
|
* and the second wants one. Both wants are the same mechanism — a subscription
|
|
94
94
|
* arrives, the tails it needs are held, and the last subscription to go
|
|
@@ -170,7 +170,7 @@ export class SessionStatus implements UpstreamResource {
|
|
|
170
170
|
/** What a topic of this owner currently says, for a snapshot and for a
|
|
171
171
|
* change alike — built here and nowhere else, so the two cannot drift. */
|
|
172
172
|
private value(topic: string): unknown {
|
|
173
|
-
if (topic === "
|
|
173
|
+
if (topic === "session.errors") return this.errors();
|
|
174
174
|
const sid = topicParam(topic);
|
|
175
175
|
return sid === undefined
|
|
176
176
|
? undefined
|
|
@@ -196,11 +196,11 @@ export class SessionStatus implements UpstreamResource {
|
|
|
196
196
|
const wanted = new Set<Sid>();
|
|
197
197
|
// One list for the instance means every session's fold; the list is what
|
|
198
198
|
// the subscriber asked for, and it cannot be built from a subset of it.
|
|
199
|
-
if (this.#wanted.has("
|
|
199
|
+
if (this.#wanted.has("session.errors")) {
|
|
200
200
|
for (const sid of this.deps.sessions()) wanted.add(sid);
|
|
201
201
|
}
|
|
202
202
|
for (const topic of this.#wanted) {
|
|
203
|
-
if (topic === "
|
|
203
|
+
if (topic === "session.errors") continue;
|
|
204
204
|
const sid = topicParam(topic);
|
|
205
205
|
if (sid !== undefined) wanted.add(sid);
|
|
206
206
|
}
|
package/src/topics/handlers.ts
CHANGED
|
@@ -7,12 +7,12 @@ import type { SubscribeOutcome, Topics } from "./topics.ts";
|
|
|
7
7
|
* it to `Topics`, and turns the outcome into the contract's own answer. */
|
|
8
8
|
export function topicHandlers(topics: Topics) {
|
|
9
9
|
return {
|
|
10
|
-
|
|
10
|
+
"topic.subscribe": (input: HandlerInput): TopicSubscribeResult => {
|
|
11
11
|
const topic = topicOf(input);
|
|
12
12
|
answer(topics.subscribe(input.conn, topic), topic);
|
|
13
13
|
return { topic };
|
|
14
14
|
},
|
|
15
|
-
|
|
15
|
+
"topic.unsubscribe": (input: HandlerInput): TopicSubscribeResult => {
|
|
16
16
|
const topic = topicOf(input);
|
|
17
17
|
answer(topics.unsubscribe(input.conn, topic), topic);
|
|
18
18
|
return { topic };
|
package/src/topics/topics.ts
CHANGED
|
Binary file
|
|
@@ -31,7 +31,7 @@ import { genericResult, resultFields, useFields } from "./tools.ts";
|
|
|
31
31
|
*
|
|
32
32
|
* Nothing is dropped for being unrecognised. A tool nobody wrote fields for
|
|
33
33
|
* arrives with what it was called with, an attachment arrives under its own
|
|
34
|
-
* kind, and a record that fits nothing arrives as `system
|
|
34
|
+
* kind, and a record that fits nothing arrives as `system.unknown`. The one
|
|
35
35
|
* failure a dump cannot be read around is a line that vanished quietly. */
|
|
36
36
|
|
|
37
37
|
/** Record types that are the interface and the session's own bookkeeping
|
|
@@ -112,8 +112,8 @@ export function classify(records: Iterable<Located>, subject: TranscriptSubject
|
|
|
112
112
|
export class Classification {
|
|
113
113
|
#items: Draft[] = [];
|
|
114
114
|
/** The call each tool result belongs to, by the id the harness pairs them
|
|
115
|
-
* with. Holds the `tool
|
|
116
|
-
* `message
|
|
115
|
+
* with. Holds the `tool.*` item and, for an `Agent` call, the
|
|
116
|
+
* `message.sub.out` beside it — the same exchange seen from the two sides
|
|
117
117
|
* the contract names it from. */
|
|
118
118
|
readonly #calls = new Map<string, { tool: Draft; message?: Draft; name: string }>();
|
|
119
119
|
#turn = 0;
|
|
@@ -198,7 +198,7 @@ export class Classification {
|
|
|
198
198
|
if (type === "system") return this.#system(record, make);
|
|
199
199
|
if (type === "assistant") return this.#assistant(record, make);
|
|
200
200
|
if (type === "user") return this.#user(record, make);
|
|
201
|
-
make("system
|
|
201
|
+
make("system.unknown", { record });
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
/** An attachment is either the operator's own code speaking or the harness
|
|
@@ -210,10 +210,10 @@ export class Classification {
|
|
|
210
210
|
if (kind === "hook_additional_context" || kind === "hook_success") {
|
|
211
211
|
const name = str(attachment["hookName"]) ?? "";
|
|
212
212
|
// The event alone is the type. A hook runs under `PreToolUse:Bash`,
|
|
213
|
-
// whose
|
|
214
|
-
// `hook
|
|
213
|
+
// whose matcher is no level of a hierarchy: carrying it would make
|
|
214
|
+
// `hook.PreToolUse` select nothing, so it is a field instead.
|
|
215
215
|
const event = str(attachment["hookEvent"]) ?? name.split(":")[0] ?? "";
|
|
216
|
-
make(`hook
|
|
216
|
+
make(`hook.${segment(event)}`, {
|
|
217
217
|
hook_name: name,
|
|
218
218
|
outcome: kind === "hook_success" ? "output" : "additionalContext",
|
|
219
219
|
...optional("content", text(attachment["content"])),
|
|
@@ -225,7 +225,7 @@ export class Classification {
|
|
|
225
225
|
});
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
|
-
make(`system
|
|
228
|
+
make(`system.attachment.${segment(kind ?? "unknown")}`, { attachment });
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
/** The harness files a slash command's output as a line of its own, which
|
|
@@ -235,19 +235,19 @@ export class Classification {
|
|
|
235
235
|
#system(record: Row, make: Make): void {
|
|
236
236
|
if (str(record["subtype"]) === "local_command") {
|
|
237
237
|
const content = str(record["content"]) ?? "";
|
|
238
|
-
make("notice
|
|
238
|
+
make("notice.slash", {
|
|
239
239
|
command: this.#slash ?? "",
|
|
240
240
|
...optional("stdout", tagged(content, "local-command-stdout") ?? content),
|
|
241
241
|
});
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
244
|
-
make("system
|
|
244
|
+
make("system.unknown", { record });
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
#assistant(record: Row, make: Make): void {
|
|
248
248
|
const message = row(record["message"]) ?? {};
|
|
249
249
|
if (record["isApiErrorMessage"] === true) {
|
|
250
|
-
make("system
|
|
250
|
+
make("system.api.error", { text: text(message["content"]) ?? "" });
|
|
251
251
|
return;
|
|
252
252
|
}
|
|
253
253
|
for (const block of list(message["content"])) {
|
|
@@ -265,7 +265,7 @@ export class Classification {
|
|
|
265
265
|
// them is the answer it was started for, and the ones before are what
|
|
266
266
|
// it hands back mid-flight. No call carries them, which is why
|
|
267
267
|
// `parent:out` is prose as well as a call.
|
|
268
|
-
const kind = this.#subject === "main" ? "message
|
|
268
|
+
const kind = this.#subject === "main" ? "message.user.out" : "message.parent.out";
|
|
269
269
|
if (said !== undefined && said !== "") make(kind, { text: said });
|
|
270
270
|
continue;
|
|
271
271
|
}
|
|
@@ -286,7 +286,7 @@ export class Classification {
|
|
|
286
286
|
const input = row(block["input"]) ?? {};
|
|
287
287
|
const fields = useFields(name, input);
|
|
288
288
|
const called = typedAs(name);
|
|
289
|
-
const item = make(`tool
|
|
289
|
+
const item = make(`tool.${segment(called)}`, {
|
|
290
290
|
role: "use",
|
|
291
291
|
tool_use_id: id,
|
|
292
292
|
...(called === name ? {} : { harness_name: name }),
|
|
@@ -301,14 +301,14 @@ export class Classification {
|
|
|
301
301
|
const named = str(input["name"]) ?? str(input["team_name"]);
|
|
302
302
|
message =
|
|
303
303
|
named === undefined
|
|
304
|
-
? make("message
|
|
304
|
+
? make("message.sub.out", {
|
|
305
305
|
role: "use",
|
|
306
306
|
tool_use_id: id,
|
|
307
307
|
prompt: str(input["prompt"]) ?? "",
|
|
308
308
|
...optional("subagent_type", str(input["subagent_type"])),
|
|
309
309
|
...optional("description", str(input["description"])),
|
|
310
310
|
})
|
|
311
|
-
: make("message
|
|
311
|
+
: make("message.team.out", {
|
|
312
312
|
role: "use",
|
|
313
313
|
tool_use_id: id,
|
|
314
314
|
text: str(input["prompt"]) ?? "",
|
|
@@ -323,14 +323,14 @@ export class Classification {
|
|
|
323
323
|
// the names a harness gives a lead, and any other name is somebody
|
|
324
324
|
// standing alongside.
|
|
325
325
|
if (addressed(to)) {
|
|
326
|
-
make("message
|
|
326
|
+
make("message.session.out", { text: text(input["message"]) ?? "", to });
|
|
327
327
|
} else {
|
|
328
328
|
// Writing to an agent is one direction of a correspondence, not a call
|
|
329
329
|
// that returns: what it says back arrives as its own message whenever
|
|
330
330
|
// it chooses to send one, under nothing that names this. So the
|
|
331
331
|
// message says it is waiting for nothing, and a reader is not left
|
|
332
332
|
// watching for an answer that has no way in.
|
|
333
|
-
make(LEADS.has(to) ? "message
|
|
333
|
+
make(LEADS.has(to) ? "message.parent.out" : "message.team.out", {
|
|
334
334
|
role: "use",
|
|
335
335
|
tool_use_id: id,
|
|
336
336
|
text: text(input["message"]) ?? "",
|
|
@@ -340,7 +340,7 @@ export class Classification {
|
|
|
340
340
|
});
|
|
341
341
|
}
|
|
342
342
|
} else if (name === "Bash" && isCcmsgSend(str(input["command"]))) {
|
|
343
|
-
make("message
|
|
343
|
+
make("message.session.out", { text: str(input["command"]) ?? "" });
|
|
344
344
|
}
|
|
345
345
|
if (id === "") return;
|
|
346
346
|
// A call is dropped from the pairing once it has been answered, so what is
|
|
@@ -388,7 +388,7 @@ export class Classification {
|
|
|
388
388
|
// name guessed from what came back. What ties it to the call is the key
|
|
389
389
|
// the harness paired them by, which a reader joins against the calls it
|
|
390
390
|
// holds.
|
|
391
|
-
make("tool
|
|
391
|
+
make("tool.unknown", {
|
|
392
392
|
role: "result",
|
|
393
393
|
parent_tool_use_id: id,
|
|
394
394
|
result: genericResult(record["toolUseResult"]),
|
|
@@ -398,7 +398,7 @@ export class Classification {
|
|
|
398
398
|
const failed = block["is_error"] === true;
|
|
399
399
|
const answer = record["toolUseResult"];
|
|
400
400
|
const fields = resultFields(call.name, answer, failed);
|
|
401
|
-
const item = make(`tool
|
|
401
|
+
const item = make(`tool.${segment(call.name)}`, {
|
|
402
402
|
role: "result",
|
|
403
403
|
parent_item: call.tool.id,
|
|
404
404
|
parent_tool_use_id: id,
|
|
@@ -446,17 +446,17 @@ export class Classification {
|
|
|
446
446
|
* so nothing the harness injected is mistaken for someone speaking. */
|
|
447
447
|
#said(record: Row, said: string, make: Make): void {
|
|
448
448
|
if (record["isCompactSummary"] === true) {
|
|
449
|
-
make("system
|
|
449
|
+
make("system.compact", { text: said });
|
|
450
450
|
return;
|
|
451
451
|
}
|
|
452
452
|
if (said.startsWith("<local-command-caveat>")) {
|
|
453
|
-
make("system
|
|
453
|
+
make("system.caveat", { text: said });
|
|
454
454
|
return;
|
|
455
455
|
}
|
|
456
456
|
const command = tagged(said, "command-name");
|
|
457
457
|
if (command !== undefined) {
|
|
458
458
|
this.#slash = command;
|
|
459
|
-
make("notice
|
|
459
|
+
make("notice.slash", {
|
|
460
460
|
command,
|
|
461
461
|
...optional("args", tagged(said, "command-args") ?? tagged(said, "command-message")),
|
|
462
462
|
...optional("stdout", tagged(said, "local-command-stdout")),
|
|
@@ -464,11 +464,11 @@ export class Classification {
|
|
|
464
464
|
return;
|
|
465
465
|
}
|
|
466
466
|
if (said.startsWith("[Request interrupted")) {
|
|
467
|
-
make("notice
|
|
467
|
+
make("notice.interrupt", { text: said });
|
|
468
468
|
return;
|
|
469
469
|
}
|
|
470
470
|
if (said.startsWith("Resume the paused workflow by calling: Workflow({")) {
|
|
471
|
-
make("system
|
|
471
|
+
make("system.resume", { text: said });
|
|
472
472
|
return;
|
|
473
473
|
}
|
|
474
474
|
if (said.startsWith("<task-notification>")) {
|
|
@@ -483,14 +483,14 @@ export class Classification {
|
|
|
483
483
|
// stands, being told what to do is not the same as being written to.
|
|
484
484
|
if (record["parentUuid"] === null) {
|
|
485
485
|
this.#turn += 1;
|
|
486
|
-
make(this.#subject === "main" ? "message
|
|
486
|
+
make(this.#subject === "main" ? "message.user.in" : "message.parent.in", {
|
|
487
487
|
text: said,
|
|
488
488
|
...(this.#subject === "main" ? {} : envelope(said)),
|
|
489
489
|
});
|
|
490
490
|
return;
|
|
491
491
|
}
|
|
492
492
|
if (said.includes("<cross-session-message")) {
|
|
493
|
-
make("message
|
|
493
|
+
make("message.session.in", {
|
|
494
494
|
text: said,
|
|
495
495
|
...optional("from", attribute(said, "from")),
|
|
496
496
|
...optional("msg_id", attribute(said, "mid")),
|
|
@@ -503,14 +503,14 @@ export class Classification {
|
|
|
503
503
|
// name is somebody standing alongside, and what they send is a message
|
|
504
504
|
// of its own rather than the answer to anything.
|
|
505
505
|
const from = attribute(said, "teammate_id");
|
|
506
|
-
make(LEADS.has(from ?? "") ? "message
|
|
506
|
+
make(LEADS.has(from ?? "") ? "message.parent.in" : "message.team.in", {
|
|
507
507
|
text: said,
|
|
508
508
|
...envelope(said),
|
|
509
509
|
});
|
|
510
510
|
return;
|
|
511
511
|
}
|
|
512
512
|
if (record["isMeta"] === true) {
|
|
513
|
-
make("system
|
|
513
|
+
make("system.unknown", { record });
|
|
514
514
|
return;
|
|
515
515
|
}
|
|
516
516
|
// What is left is somebody writing to the subject in their own words, and
|
|
@@ -522,7 +522,7 @@ export class Classification {
|
|
|
522
522
|
// A turn begins where the subject is addressed, which is the only place a
|
|
523
523
|
// dump can count turns from — the harness numbers nothing.
|
|
524
524
|
this.#turn += 1;
|
|
525
|
-
make(this.#subject === "sub" ? "message
|
|
525
|
+
make(this.#subject === "sub" ? "message.parent.in" : "message.user.in", { text: said });
|
|
526
526
|
}
|
|
527
527
|
|
|
528
528
|
/** A background task reporting, or an agent handing back its answer.
|
|
@@ -554,7 +554,7 @@ export class Classification {
|
|
|
554
554
|
this.#calls.delete(key);
|
|
555
555
|
return;
|
|
556
556
|
}
|
|
557
|
-
make("system
|
|
557
|
+
make("system.task", {
|
|
558
558
|
text: said,
|
|
559
559
|
...optional("task_id", tagged(said, "task-id")),
|
|
560
560
|
...optional("event", tagged(said, "event") ?? tagged(said, "summary")),
|
|
@@ -594,7 +594,7 @@ function envelope(said: string): Record<string, unknown> {
|
|
|
594
594
|
* whatever asked for it: a teammate's run ending answers the call that started
|
|
595
595
|
* it, an errand's answer is the errand's result. */
|
|
596
596
|
function answers(asked: Draft): string {
|
|
597
|
-
return asked.type === "message
|
|
597
|
+
return asked.type === "message.team.out" ? "message.team.in" : "message.sub.in";
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
/** Whether a shell command is this session speaking to another one. */
|
|
@@ -603,12 +603,17 @@ function isCcmsgSend(command: string | undefined): boolean {
|
|
|
603
603
|
return /\bccmsg\s+(post|reply)\b/.test(command);
|
|
604
604
|
}
|
|
605
605
|
|
|
606
|
-
/**
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
606
|
+
/** The last segment of an open family's type name (`tool.<Name>`,
|
|
607
|
+
* `system.attachment.<kind>`, `hook.<Event>`). The harness's own spellings pass
|
|
608
|
+
* through — they are what a reader matches against what it ran — and a
|
|
609
|
+
* character the name could not carry is replaced rather than the segment being
|
|
610
|
+
* refused, so a newcomer still arrives under something close to its own name.
|
|
611
|
+
*
|
|
612
|
+
* A `.` in the harness's name is written `_`: the reader splits a type on `.`
|
|
613
|
+
* to walk its hierarchy, and a name carrying one of its own would read as a
|
|
614
|
+
* level this contract never coined (contract, `TranscriptItemType`). */
|
|
610
615
|
function segment(name: string): string {
|
|
611
|
-
const cleaned = name.replace(/[^A-Za-z0-9_
|
|
616
|
+
const cleaned = name.replace(/\./g, "_").replace(/[^A-Za-z0-9_-]/g, "-");
|
|
612
617
|
return cleaned === "" ? "unknown" : cleaned;
|
|
613
618
|
}
|
|
614
619
|
|
|
@@ -253,7 +253,7 @@ function pair(items: readonly Item[]): {
|
|
|
253
253
|
* the call and an agent's to the message, and the harness's key alone would
|
|
254
254
|
* not say which. */
|
|
255
255
|
function joined(item: Item, key: string): string {
|
|
256
|
-
return `${item.type.startsWith("message
|
|
256
|
+
return `${item.type.startsWith("message.") ? "message" : "tool"}\n${key}`;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
/** Whether an item is the conversation half of starting an agent, as opposed
|
|
@@ -262,5 +262,5 @@ function joined(item: Item, key: string): string {
|
|
|
262
262
|
* they ended up — a conversation split across the page is one nobody can
|
|
263
263
|
* follow. */
|
|
264
264
|
function spoken(type: string): boolean {
|
|
265
|
-
return type.startsWith("message
|
|
265
|
+
return type.startsWith("message.sub") || type.startsWith("message.team");
|
|
266
266
|
}
|
|
Binary file
|