@ccmsg/protocol 1.20.0 → 1.22.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/README.md +2 -2
- package/package.json +1 -1
- package/src/attributes.ts +77 -52
- package/src/common/auth.ts +28 -28
- package/src/common/hello.ts +72 -34
- package/src/common/ping.ts +2 -2
- package/src/common/shutdown.ts +4 -4
- package/src/common/topics.ts +24 -19
- package/src/control/dump.ts +73 -66
- package/src/control/files.ts +24 -24
- package/src/control/kv.ts +6 -6
- package/src/control/launcher.ts +4 -4
- package/src/control/llm.ts +8 -8
- package/src/control/sandbox.ts +4 -4
- package/src/control/session-errors.ts +3 -3
- package/src/control/session-status.ts +2 -2
- package/src/control/session.ts +36 -35
- package/src/control/transcript.ts +6 -6
- package/src/control/translate.ts +2 -2
- package/src/envelope.ts +2 -2
- package/src/errors.ts +1 -1
- package/src/fixtures/common.ts +45 -32
- package/src/fixtures/control.ts +67 -67
- package/src/fixtures/index.ts +77 -63
- package/src/fixtures/messaging.ts +9 -9
- package/src/fixtures/topics.ts +9 -9
- package/src/identifiers.ts +8 -6
- package/src/messaging/message.ts +3 -3
- package/src/messaging/notify.ts +3 -3
- package/src/messaging/say.ts +9 -9
- package/src/schemas.ts +79 -67
- package/src/session-meta.ts +2 -1
package/src/common/hello.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { request, response } from "../envelope.ts";
|
|
|
3
3
|
import { Capability, Endpoint, InstanceId, Role, Sid, Timestamp } from "../identifiers.ts";
|
|
4
4
|
import { SessionMetaFields } from "../session-meta.ts";
|
|
5
5
|
|
|
6
|
-
/** The mesh handshake's opening claim, carried by
|
|
6
|
+
/** The mesh handshake's opening claim, carried by `hello.instance`.
|
|
7
7
|
*
|
|
8
8
|
* It is not signed and proves nothing on its own: it names the peer and says
|
|
9
9
|
* where its one-off key can be fetched. The proof that binds this connection to
|
|
@@ -36,42 +36,39 @@ export const MeshHello = Type.Object(
|
|
|
36
36
|
);
|
|
37
37
|
export type MeshHello = Static<typeof MeshHello>;
|
|
38
38
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
/** What every greeting carries, whichever of the three it is. */
|
|
40
|
+
const GREETING_FIELDS = {
|
|
41
|
+
/** The generation the caller speaks. A hello announcing another generation
|
|
42
|
+
* is refused with `bad_request`; there is no path that serves it anyway. */
|
|
43
|
+
protocol_version: Type.Integer({ minimum: 1 }),
|
|
44
|
+
/** The client build, for display in diagnostics. Nothing gates on it. */
|
|
45
|
+
client_version: Type.Optional(Type.String()),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** The greeting that settles what a connection is. There is one op per role,
|
|
49
|
+
* so what a greeting must carry is the schema's to state rather than a check
|
|
50
|
+
* the instance runs against a `role` field: a session names its `sid`, an
|
|
51
|
+
* instance its mesh claim, and a person neither.
|
|
49
52
|
*
|
|
50
|
-
* A field that belongs to another role is as much a refusal as a missing one
|
|
53
|
+
* A field that belongs to another role is as much a refusal as a missing one —
|
|
51
54
|
* `mesh` on a session greeting says the caller has confused which handshake it
|
|
52
|
-
* is in
|
|
53
|
-
*
|
|
55
|
+
* is in — which is what the three ops keep apart: a greeting arrives under the
|
|
56
|
+
* name of the thing it settles, and there is no state in which the name and the
|
|
57
|
+
* fields disagree.
|
|
54
58
|
*
|
|
55
59
|
* A session's meta (`cwd`, `repo_root`, `transcript_path`, `title`, ...) is
|
|
56
60
|
* taken field by field: a greeting that leaves a field out does not withdraw
|
|
57
61
|
* it, and the instance keeps what it already knows for that `sid`. One session
|
|
58
62
|
* reaches an instance as a run of short-lived processes (a session-start hook,
|
|
59
63
|
* a `post`, a session-end hook), none of which knows every field. */
|
|
60
|
-
export const
|
|
61
|
-
|
|
62
|
-
/** The
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
mesh: Type.Optional(MeshHello),
|
|
69
|
-
/** The client build, for display in diagnostics. Nothing gates on it. */
|
|
70
|
-
client_version: Type.Optional(Type.String()),
|
|
71
|
-
/** What a `role: "session"` connection says about itself. All optional: a
|
|
72
|
-
* session states what it knows, and the instance derives or leaves unknown
|
|
73
|
-
* what it is not told. The instance repeats these on the `peers` topic, so
|
|
74
|
-
* they are the same fields under the same names there. */
|
|
64
|
+
export const HelloSessionArgs = Type.Object({
|
|
65
|
+
...GREETING_FIELDS,
|
|
66
|
+
/** The session the connection speaks for. */
|
|
67
|
+
sid: Sid,
|
|
68
|
+
/** What the session says about itself. All optional: a session states what it
|
|
69
|
+
* knows, and the instance derives or leaves unknown what it is not told. The
|
|
70
|
+
* instance repeats these on the `peers` topic, so they are the same fields
|
|
71
|
+
* under the same names there. */
|
|
75
72
|
repo: Type.Optional(SessionMetaFields.repo),
|
|
76
73
|
ws: Type.Optional(SessionMetaFields.ws),
|
|
77
74
|
cwd: Type.Optional(SessionMetaFields.cwd),
|
|
@@ -82,9 +79,22 @@ export const HelloArgs = Type.Object({
|
|
|
82
79
|
model: Type.Optional(SessionMetaFields.model),
|
|
83
80
|
effort: Type.Optional(SessionMetaFields.effort),
|
|
84
81
|
});
|
|
85
|
-
export type
|
|
82
|
+
export type HelloSessionArgs = Static<typeof HelloSessionArgs>;
|
|
83
|
+
|
|
84
|
+
/** A person greeting. It names no session: a person speaks for none of them,
|
|
85
|
+
* and the sessions they may act on are decided by the op table rather than by
|
|
86
|
+
* anything settled here. */
|
|
87
|
+
export const HelloUserArgs = Type.Object(GREETING_FIELDS);
|
|
88
|
+
export type HelloUserArgs = Static<typeof HelloUserArgs>;
|
|
89
|
+
|
|
90
|
+
/** A peer instance greeting, which opens the mesh handshake. */
|
|
91
|
+
export const HelloInstanceArgs = Type.Object({
|
|
92
|
+
...GREETING_FIELDS,
|
|
93
|
+
mesh: MeshHello,
|
|
94
|
+
});
|
|
95
|
+
export type HelloInstanceArgs = Static<typeof HelloInstanceArgs>;
|
|
86
96
|
|
|
87
|
-
/** One instance as seen from the instance answering
|
|
97
|
+
/** One instance as seen from the instance answering a greeting. */
|
|
88
98
|
export const InstanceInfo = Type.Object(
|
|
89
99
|
{
|
|
90
100
|
/** Absent until the handshake with it has settled: an endpoint an operator
|
|
@@ -148,10 +158,38 @@ export const HelloResult = Type.Object({
|
|
|
148
158
|
* closes it. Present on a connection an access token opened; absent where
|
|
149
159
|
* reaching the instance is itself the permission (the Unix socket) or where
|
|
150
160
|
* the connection is a mesh link. The person's client renews before this
|
|
151
|
-
* instant with `
|
|
161
|
+
* instant with `auth.extend` rather than reconnecting. */
|
|
152
162
|
auth_expires_at: Type.Optional(Timestamp),
|
|
153
163
|
});
|
|
154
164
|
export type HelloResult = Static<typeof HelloResult>;
|
|
155
165
|
|
|
156
|
-
export const
|
|
157
|
-
export const
|
|
166
|
+
export const HelloSessionRequest = request("hello.session", HelloSessionArgs);
|
|
167
|
+
export const HelloSessionResponse = response("hello.session", HelloResult);
|
|
168
|
+
|
|
169
|
+
export const HelloUserRequest = request("hello.user", HelloUserArgs);
|
|
170
|
+
export const HelloUserResponse = response("hello.user", HelloResult);
|
|
171
|
+
|
|
172
|
+
export const HelloInstanceRequest = request("hello.instance", HelloInstanceArgs);
|
|
173
|
+
export const HelloInstanceResponse = response("hello.instance", HelloResult);
|
|
174
|
+
|
|
175
|
+
/** The role a greeting settles, read from the op that carried it.
|
|
176
|
+
*
|
|
177
|
+
* The role itself stays — the op table and the topic table are written against
|
|
178
|
+
* it, and a forwarded request names it in `caller` — and what a greeting no
|
|
179
|
+
* longer carries is a second place to say it. */
|
|
180
|
+
export const HELLO_OPS = {
|
|
181
|
+
"hello.session": "session",
|
|
182
|
+
"hello.user": "user",
|
|
183
|
+
"hello.instance": "instance",
|
|
184
|
+
} as const satisfies Record<string, Role>;
|
|
185
|
+
|
|
186
|
+
export type HelloOp = keyof typeof HELLO_OPS;
|
|
187
|
+
|
|
188
|
+
export function isHelloOp(op: string): op is HelloOp {
|
|
189
|
+
return op in HELLO_OPS;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The role a connection speaks once `op` has been answered. */
|
|
193
|
+
export function helloRole(op: HelloOp): Role {
|
|
194
|
+
return HELLO_OPS[op];
|
|
195
|
+
}
|
package/src/common/ping.ts
CHANGED
|
@@ -33,5 +33,5 @@ export const InstancePingResult = Type.Object({
|
|
|
33
33
|
});
|
|
34
34
|
export type InstancePingResult = Static<typeof InstancePingResult>;
|
|
35
35
|
|
|
36
|
-
export const InstancePingRequest = request("
|
|
37
|
-
export const InstancePingResponse = response("
|
|
36
|
+
export const InstancePingRequest = request("instance.ping", InstancePingArgs);
|
|
37
|
+
export const InstancePingResponse = response("instance.ping", InstancePingResult);
|
package/src/common/shutdown.ts
CHANGED
|
@@ -11,8 +11,8 @@ export type InstanceShutdownArgs = Static<typeof InstanceShutdownArgs>;
|
|
|
11
11
|
export const InstanceShutdownResult = Type.Object({});
|
|
12
12
|
export type InstanceShutdownResult = Static<typeof InstanceShutdownResult>;
|
|
13
13
|
|
|
14
|
-
export const InstanceShutdownRequest = request("
|
|
15
|
-
export const InstanceShutdownResponse = response("
|
|
14
|
+
export const InstanceShutdownRequest = request("instance.shutdown", InstanceShutdownArgs);
|
|
15
|
+
export const InstanceShutdownResponse = response("instance.shutdown", InstanceShutdownResult);
|
|
16
16
|
|
|
17
17
|
/** A session saying it is about to go, so that its disconnection reads as a
|
|
18
18
|
* pause rather than a loss.
|
|
@@ -44,5 +44,5 @@ export const SessionStoppingResult = Type.Object({
|
|
|
44
44
|
});
|
|
45
45
|
export type SessionStoppingResult = Static<typeof SessionStoppingResult>;
|
|
46
46
|
|
|
47
|
-
export const SessionStoppingRequest = request("
|
|
48
|
-
export const SessionStoppingResponse = response("
|
|
47
|
+
export const SessionStoppingRequest = request("session.stopping", SessionStoppingArgs);
|
|
48
|
+
export const SessionStoppingResponse = response("session.stopping", SessionStoppingResult);
|
package/src/common/topics.ts
CHANGED
|
@@ -9,14 +9,14 @@ export const PLAIN_TOPICS = [
|
|
|
9
9
|
"peers",
|
|
10
10
|
"instances",
|
|
11
11
|
"agents",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
12
|
+
"session.errors",
|
|
13
|
+
"llm.requests",
|
|
14
|
+
"llm.status",
|
|
15
|
+
"auth.records",
|
|
16
16
|
] as const;
|
|
17
17
|
|
|
18
18
|
/** Topics naming one session, written `<topic>:<sid>`. */
|
|
19
|
-
export const SESSION_SCOPED_TOPICS = ["
|
|
19
|
+
export const SESSION_SCOPED_TOPICS = ["session.status", "transcript", "transcript.items"] as const;
|
|
20
20
|
|
|
21
21
|
/** Topics naming one namespace, written `<topic>:<ns>`. The parameter is a name
|
|
22
22
|
* its users choose rather than an identifier this contract issues, so it is
|
|
@@ -36,13 +36,18 @@ export type TopicName =
|
|
|
36
36
|
| `${SessionScopedTopic}:${string}`
|
|
37
37
|
| `${NamespaceScopedTopic}:${string}`;
|
|
38
38
|
|
|
39
|
+
/** A topic name is a `.`-separated hierarchy, so its dots are literal here
|
|
40
|
+
* rather than the regexp's any-character. */
|
|
41
|
+
const alternation = (topics: readonly string[]): string =>
|
|
42
|
+
topics.map((topic) => topic.replace(/\./g, "\\.")).join("|");
|
|
43
|
+
|
|
39
44
|
export const Topic = Type.String({
|
|
40
45
|
$id: "Topic",
|
|
41
46
|
pattern: [
|
|
42
47
|
"^(?:",
|
|
43
|
-
PLAIN_TOPICS
|
|
44
|
-
`|(?:${SESSION_SCOPED_TOPICS
|
|
45
|
-
`|(?:${NAMESPACE_SCOPED_TOPICS
|
|
48
|
+
alternation(PLAIN_TOPICS),
|
|
49
|
+
`|(?:${alternation(SESSION_SCOPED_TOPICS)}):[0-9a-f-]{36}`,
|
|
50
|
+
`|(?:${alternation(NAMESPACE_SCOPED_TOPICS)}):${NAMESPACE_PATTERN}`,
|
|
46
51
|
")$",
|
|
47
52
|
].join(""),
|
|
48
53
|
});
|
|
@@ -101,27 +106,27 @@ export const TOPIC_ATTRIBUTES = {
|
|
|
101
106
|
// A set the instance derives whole, by folding one error pattern over its
|
|
102
107
|
// sessions: it learns which sessions are stopped, not that one of them
|
|
103
108
|
// changed, so each frame is that reading entire.
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
"session.errors": { roles: ["user"], granularity: "per_instance_whole" },
|
|
110
|
+
"llm.requests": {
|
|
106
111
|
roles: ["user"],
|
|
107
112
|
capability: "llm_events",
|
|
108
113
|
granularity: "per_instance_whole",
|
|
109
114
|
},
|
|
110
|
-
|
|
115
|
+
"llm.status": { roles: ["user"], capability: "llm_status", granularity: "per_instance_whole" },
|
|
111
116
|
// One session lives on one instance, so its status has no other instance's
|
|
112
117
|
// half to leave alone: the frame is simply the whole of it.
|
|
113
|
-
|
|
118
|
+
"session.status": { roles: ["user"], granularity: "whole" },
|
|
114
119
|
transcript: { roles: ["user"], granularity: "append" },
|
|
115
120
|
// The same appending, in items rather than in bytes. Both are offered because
|
|
116
121
|
// they answer different needs: one draws the conversation, the other shows a
|
|
117
122
|
// record as it was written.
|
|
118
|
-
|
|
123
|
+
"transcript.items": { roles: ["user"], granularity: "append" },
|
|
119
124
|
kv: { roles: ["user"], granularity: "element" },
|
|
120
125
|
// The only topic no person may subscribe to: its elements are the secrets
|
|
121
126
|
// that authenticate them. A relay carries it as the instance it is, not on a
|
|
122
127
|
// caller's behalf, so there is no path by which a person's subscription
|
|
123
128
|
// reaches it.
|
|
124
|
-
|
|
129
|
+
"auth.records": { roles: ["instance"], granularity: "element" },
|
|
125
130
|
} as const satisfies Record<
|
|
126
131
|
PlainTopic | SessionScopedTopic | NamespaceScopedTopic,
|
|
127
132
|
TopicAttributes
|
|
@@ -136,7 +141,7 @@ export function topicKind(topic: string): TopicKind | undefined {
|
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
/** How the frames of a topic name fold, taken from the name a subscriber
|
|
139
|
-
* actually uses — `
|
|
144
|
+
* actually uses — `session.status:<sid>` rather than the kind behind it.
|
|
140
145
|
* `undefined` for a name this generation does not define. */
|
|
141
146
|
export function topicGranularity(topic: string): TopicGranularity | undefined {
|
|
142
147
|
const kind = topicKind(topic);
|
|
@@ -152,8 +157,8 @@ export type TopicSubscribeArgs = Static<typeof TopicSubscribeArgs>;
|
|
|
152
157
|
export const TopicSubscribeResult = Type.Object({ topic: Topic });
|
|
153
158
|
export type TopicSubscribeResult = Static<typeof TopicSubscribeResult>;
|
|
154
159
|
|
|
155
|
-
export const TopicSubscribeRequest = request("
|
|
156
|
-
export const TopicSubscribeResponse = response("
|
|
160
|
+
export const TopicSubscribeRequest = request("topic.subscribe", TopicSubscribeArgs);
|
|
161
|
+
export const TopicSubscribeResponse = response("topic.subscribe", TopicSubscribeResult);
|
|
157
162
|
|
|
158
163
|
export const TopicUnsubscribeArgs = Type.Object({ topic: Topic });
|
|
159
164
|
export type TopicUnsubscribeArgs = Static<typeof TopicUnsubscribeArgs>;
|
|
@@ -161,5 +166,5 @@ export type TopicUnsubscribeArgs = Static<typeof TopicUnsubscribeArgs>;
|
|
|
161
166
|
export const TopicUnsubscribeResult = Type.Object({ topic: Topic });
|
|
162
167
|
export type TopicUnsubscribeResult = Static<typeof TopicUnsubscribeResult>;
|
|
163
168
|
|
|
164
|
-
export const TopicUnsubscribeRequest = request("
|
|
165
|
-
export const TopicUnsubscribeResponse = response("
|
|
169
|
+
export const TopicUnsubscribeRequest = request("topic.unsubscribe", TopicUnsubscribeArgs);
|
|
170
|
+
export const TopicUnsubscribeResponse = response("topic.unsubscribe", TopicUnsubscribeResult);
|
package/src/control/dump.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { Sid, Timestamp } from "../identifiers.ts";
|
|
|
13
13
|
* down here. That split is what lets a harness change its file, or a second
|
|
14
14
|
* harness be read at all, without the contract moving.
|
|
15
15
|
*
|
|
16
|
-
* A type name is
|
|
17
|
-
* everything below it: `tool` is every tool, `message
|
|
16
|
+
* A type name is `.`-separated and read left to right, so a prefix names
|
|
17
|
+
* everything below it: `tool` is every tool, `message.user` is both directions
|
|
18
18
|
* of what a person and a session said. Three families stay open, because their
|
|
19
19
|
* last segment is a name someone else coins — a tool, an attachment kind, a
|
|
20
20
|
* hook event — and closing them would turn every newcomer into `unknown`. */
|
|
@@ -22,49 +22,56 @@ import { Sid, Timestamp } from "../identifiers.ts";
|
|
|
22
22
|
/** A type name as written on the wire and in a selection.
|
|
23
23
|
*
|
|
24
24
|
* Segments after the first carry the spelling of whatever named them, which is
|
|
25
|
-
* why they are not held to snake_case: `tool
|
|
25
|
+
* why they are not held to snake_case: `tool.Bash` and `hook.PreToolUse` are
|
|
26
26
|
* the harness's words, and rewriting them would leave the reader unable to
|
|
27
|
-
* match what it sees against what it ran.
|
|
27
|
+
* match what it sees against what it ran. That segment carries no `.` of its
|
|
28
|
+
* own — a harness name holding one is written with `_` by whoever coins the
|
|
29
|
+
* type — so a reader may split any type name on `.` and get the hierarchy.
|
|
30
|
+
*
|
|
31
|
+
* `tool.unknown` is the reserved name for a call whose tool the reader could
|
|
32
|
+
* not name. It is a coined segment like any other, so a harness tool actually
|
|
33
|
+
* called `unknown` lands on it; what is lost is that distinction and nothing
|
|
34
|
+
* else, where dropping the item would lose the call.
|
|
28
35
|
*
|
|
29
36
|
* Under `message`, the second segment is **a relation read from the subject**:
|
|
30
37
|
* `parent` is whoever started this agent, `sub` a throwaway agent it started,
|
|
31
38
|
* `team` a named counterpart that goes on standing, `session` another session
|
|
32
39
|
* over ccmsg. The one exception is `user` — a person is not a relation to
|
|
33
40
|
* anyone, but the user, standing alone. A harness's own name for a party is
|
|
34
|
-
* never a type: `message
|
|
41
|
+
* never a type: `message.main` would read as the main session's traffic being
|
|
35
42
|
* overheard wherever it happens, when what is meant is the party this subject
|
|
36
43
|
* answers to — which is what `parent` says. The literal names (`main`, a
|
|
37
44
|
* lead's, a teammate's) are kept in `harness_name` on the item. */
|
|
38
45
|
export const TranscriptItemType = Type.String({
|
|
39
|
-
pattern: "^[a-z]
|
|
46
|
+
pattern: "^[a-z][a-z0-9_]*(?:\\.[A-Za-z0-9_-]+)*$",
|
|
40
47
|
$id: "TranscriptItemType",
|
|
41
48
|
});
|
|
42
49
|
export type TranscriptItemType = Static<typeof TranscriptItemType>;
|
|
43
50
|
|
|
44
51
|
/** The type names that are fully spelled out here. The three open families
|
|
45
|
-
* (`tool
|
|
52
|
+
* (`tool.<Name>`, `system.attachment.<kind>`, `hook.<Event>`) are not in the
|
|
46
53
|
* list: their last segment is coined elsewhere, and a name absent from this
|
|
47
54
|
* list is a newcomer rather than an error. */
|
|
48
55
|
export const TRANSCRIPT_ITEM_TYPES = [
|
|
49
|
-
"message
|
|
50
|
-
"message
|
|
51
|
-
"message
|
|
52
|
-
"message
|
|
53
|
-
"message
|
|
54
|
-
"message
|
|
55
|
-
"message
|
|
56
|
-
"message
|
|
57
|
-
"message
|
|
58
|
-
"message
|
|
56
|
+
"message.user.in",
|
|
57
|
+
"message.user.out",
|
|
58
|
+
"message.parent.in",
|
|
59
|
+
"message.parent.out",
|
|
60
|
+
"message.sub.in",
|
|
61
|
+
"message.sub.out",
|
|
62
|
+
"message.team.in",
|
|
63
|
+
"message.team.out",
|
|
64
|
+
"message.session.in",
|
|
65
|
+
"message.session.out",
|
|
59
66
|
"thinking",
|
|
60
|
-
"notice
|
|
61
|
-
"notice
|
|
62
|
-
"system
|
|
63
|
-
"system
|
|
64
|
-
"system
|
|
65
|
-
"system
|
|
66
|
-
"system
|
|
67
|
-
"system
|
|
67
|
+
"notice.slash",
|
|
68
|
+
"notice.interrupt",
|
|
69
|
+
"system.compact",
|
|
70
|
+
"system.api.error",
|
|
71
|
+
"system.task",
|
|
72
|
+
"system.caveat",
|
|
73
|
+
"system.resume",
|
|
74
|
+
"system.unknown",
|
|
68
75
|
] as const;
|
|
69
76
|
export type KnownTranscriptItemType = (typeof TRANSCRIPT_ITEM_TYPES)[number];
|
|
70
77
|
|
|
@@ -74,7 +81,7 @@ export type KnownTranscriptItemType = (typeof TRANSCRIPT_ITEM_TYPES)[number];
|
|
|
74
81
|
* Elements apply left to right, so an exclusion reaches whatever a prefix or an
|
|
75
82
|
* expansion before it brought in. */
|
|
76
83
|
export const TranscriptItemSelector = Type.String({
|
|
77
|
-
pattern: "^-?(?:@[A-Za-z0-9][A-Za-z0-9_-]*|[a-z]
|
|
84
|
+
pattern: "^-?(?:@[A-Za-z0-9][A-Za-z0-9_-]*|[a-z][a-z0-9_]*(?:\\.[A-Za-z0-9_-]+)*)$",
|
|
78
85
|
$id: "TranscriptItemSelector",
|
|
79
86
|
});
|
|
80
87
|
export type TranscriptItemSelector = Static<typeof TranscriptItemSelector>;
|
|
@@ -84,7 +91,7 @@ export type TranscriptItemSelector = Static<typeof TranscriptItemSelector>;
|
|
|
84
91
|
* started to answer once — and `team` a teammate's, an agent that was named and
|
|
85
92
|
* goes on standing.
|
|
86
93
|
*
|
|
87
|
-
* It is what the relations are read from. `message
|
|
94
|
+
* It is what the relations are read from. `message.parent.in` under a `sub` is
|
|
88
95
|
* an errand's brief and under a `team` is what its lead wrote, and an item that
|
|
89
96
|
* does not say which of the two it stood in can only be placed by whoever
|
|
90
97
|
* remembers the request that fetched it. Carrying it on the item is what lets
|
|
@@ -105,7 +112,7 @@ export type TranscriptSubject = Static<typeof TranscriptSubject>;
|
|
|
105
112
|
*
|
|
106
113
|
* An item is what a reader made of a record, and a reader is fallible: the one
|
|
107
114
|
* question it cannot answer is what the record actually said. These two numbers
|
|
108
|
-
* are that answer's address — `
|
|
115
|
+
* are that answer's address — `transcript.read` bounded to end at `offset +
|
|
109
116
|
* bytes` and to carry `bytes` returns the record itself — which is what lets a
|
|
110
117
|
* client show items and still let a person open the line behind one. Several
|
|
111
118
|
* items read out of a single record share the address, so what comes back is
|
|
@@ -208,24 +215,24 @@ const Text = Type.String();
|
|
|
208
215
|
* the one above: an agent's parent is a session or another agent, and calling it
|
|
209
216
|
* `user` would have a reader take a machine for a person.
|
|
210
217
|
*
|
|
211
|
-
* `message
|
|
218
|
+
* `message.user` is a person and nobody else. Both directions occur under a
|
|
212
219
|
* session and under a teammate, which someone can type at directly; under a
|
|
213
220
|
* throwaway agent neither does. A combination a subject is not expected to show
|
|
214
221
|
* — `team` below an agent, say — is not refused: an unexpected line is still a
|
|
215
222
|
* line, and it is emitted under the name it fits. */
|
|
216
|
-
const MessageUserIn = item(Type.Literal("message
|
|
217
|
-
const MessageUserOut = item(Type.Literal("message
|
|
223
|
+
const MessageUserIn = item(Type.Literal("message.user.in"), { text: Text });
|
|
224
|
+
const MessageUserOut = item(Type.Literal("message.user.out"), { text: Text });
|
|
218
225
|
|
|
219
226
|
/** What the one above said, and what was said back to it.
|
|
220
227
|
*
|
|
221
228
|
* An agent's first line is the brief it was started with, and its last is the
|
|
222
229
|
* answer that brief is discharged by; in between it may hand its parent
|
|
223
230
|
* something mid-flight. The answer is plain prose the harness collects, with no
|
|
224
|
-
* call behind it, so `parent
|
|
231
|
+
* call behind it, so `parent.out` is prose-or-call and not a call alone:
|
|
225
232
|
* addressed to the parent is what the two forms have in common, and requiring a
|
|
226
233
|
* `tool_use_id` would leave the one message an agent is certain to send
|
|
227
234
|
* unnameable. */
|
|
228
|
-
const MessageParentIn = item(Type.Literal("message
|
|
235
|
+
const MessageParentIn = item(Type.Literal("message.parent.in"), {
|
|
229
236
|
text: Text,
|
|
230
237
|
/** The harness's own name for the parent — `main`, a lead's name, the agent
|
|
231
238
|
* above. Left out when the record says only that it came from above, which is
|
|
@@ -236,7 +243,7 @@ const MessageParentIn = item(Type.Literal("message:parent:in"), {
|
|
|
236
243
|
|
|
237
244
|
/** Sent to the parent through a call, which the parent's own transcript has the
|
|
238
245
|
* other half of. */
|
|
239
|
-
const MessageParentOutSent = item(Type.Literal("message
|
|
246
|
+
const MessageParentOutSent = item(Type.Literal("message.parent.out"), {
|
|
240
247
|
...USE_FIELDS,
|
|
241
248
|
text: Text,
|
|
242
249
|
/** The harness's own name for the parent, as the subject addressed it. */
|
|
@@ -245,7 +252,7 @@ const MessageParentOutSent = item(Type.Literal("message:parent:out"), {
|
|
|
245
252
|
});
|
|
246
253
|
|
|
247
254
|
/** Answered to the parent as prose — the agent's reply, final or interim. */
|
|
248
|
-
const MessageParentOutSaid = item(Type.Literal("message
|
|
255
|
+
const MessageParentOutSaid = item(Type.Literal("message.parent.out"), { text: Text });
|
|
249
256
|
|
|
250
257
|
/** A teammate is an agent that was given a name and goes on standing, so what
|
|
251
258
|
* passes between the subject and one is a correspondence rather than an errand:
|
|
@@ -253,10 +260,10 @@ const MessageParentOutSaid = item(Type.Literal("message:parent:out"), { text: Te
|
|
|
253
260
|
* written, and not as the answer to the call that sent it.
|
|
254
261
|
*
|
|
255
262
|
* That is the whole of what separates `team` from `sub`. A throwaway agent is
|
|
256
|
-
* started, answers once and is done, which is why `sub
|
|
257
|
-
* `sub
|
|
263
|
+
* started, answers once and is done, which is why `sub.in` is the result of the
|
|
264
|
+
* `sub.out` that started it. Here the two halves of a round trip are two
|
|
258
265
|
* messages, and only the start of a teammate has a result to pair with. */
|
|
259
|
-
const MessageTeamOut = item(Type.Literal("message
|
|
266
|
+
const MessageTeamOut = item(Type.Literal("message.team.out"), {
|
|
260
267
|
...USE_FIELDS,
|
|
261
268
|
text: Text,
|
|
262
269
|
/** The teammate addressed, by the name it stands under. */
|
|
@@ -271,7 +278,7 @@ const MessageTeamOut = item(Type.Literal("message:team:out"), {
|
|
|
271
278
|
|
|
272
279
|
/** A teammate writing to the subject, arriving under its own name whenever it
|
|
273
280
|
* was written. */
|
|
274
|
-
const MessageTeamInSaid = item(Type.Literal("message
|
|
281
|
+
const MessageTeamInSaid = item(Type.Literal("message.team.in"), {
|
|
275
282
|
text: Text,
|
|
276
283
|
/** The name the teammate stands under. */
|
|
277
284
|
harness_name: Type.Optional(Type.String()),
|
|
@@ -279,7 +286,7 @@ const MessageTeamInSaid = item(Type.Literal("message:team:in"), {
|
|
|
279
286
|
});
|
|
280
287
|
|
|
281
288
|
/** A teammate's run ending, which answers the call that started it. */
|
|
282
|
-
const MessageTeamInDone = item(Type.Literal("message
|
|
289
|
+
const MessageTeamInDone = item(Type.Literal("message.team.in"), {
|
|
283
290
|
...RESULT_FIELDS,
|
|
284
291
|
text: Text,
|
|
285
292
|
harness_name: Type.Optional(Type.String()),
|
|
@@ -288,7 +295,7 @@ const MessageTeamInDone = item(Type.Literal("message:team:in"), {
|
|
|
288
295
|
duration_ms: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
289
296
|
});
|
|
290
297
|
|
|
291
|
-
const MessageSubOut = item(Type.Literal("message
|
|
298
|
+
const MessageSubOut = item(Type.Literal("message.sub.out"), {
|
|
292
299
|
...USE_FIELDS,
|
|
293
300
|
prompt: Text,
|
|
294
301
|
agent_id: Type.Optional(Type.String()),
|
|
@@ -298,7 +305,7 @@ const MessageSubOut = item(Type.Literal("message:sub:out"), {
|
|
|
298
305
|
description: Type.Optional(Type.String()),
|
|
299
306
|
});
|
|
300
307
|
|
|
301
|
-
const MessageSubIn = item(Type.Literal("message
|
|
308
|
+
const MessageSubIn = item(Type.Literal("message.sub.in"), {
|
|
302
309
|
...RESULT_FIELDS,
|
|
303
310
|
text: Text,
|
|
304
311
|
agent_id: Type.Optional(Type.String()),
|
|
@@ -306,7 +313,7 @@ const MessageSubIn = item(Type.Literal("message:sub:in"), {
|
|
|
306
313
|
duration_ms: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
307
314
|
});
|
|
308
315
|
|
|
309
|
-
const MessageSessionOut = item(Type.Literal("message
|
|
316
|
+
const MessageSessionOut = item(Type.Literal("message.session.out"), {
|
|
310
317
|
text: Text,
|
|
311
318
|
/** The addressee as the subject wrote it: a sid, or a name that was resolved
|
|
312
319
|
* to one. Kept unresolved when that is all the transcript says. */
|
|
@@ -315,7 +322,7 @@ const MessageSessionOut = item(Type.Literal("message:session:out"), {
|
|
|
315
322
|
reply_to: Type.Optional(Type.String()),
|
|
316
323
|
});
|
|
317
324
|
|
|
318
|
-
const MessageSessionIn = item(Type.Literal("message
|
|
325
|
+
const MessageSessionIn = item(Type.Literal("message.session.in"), {
|
|
319
326
|
text: Text,
|
|
320
327
|
from: Type.Optional(Type.String()),
|
|
321
328
|
msg_id: Type.Optional(Type.String()),
|
|
@@ -325,52 +332,52 @@ const Thinking = item(Type.Literal("thinking"), { text: Text });
|
|
|
325
332
|
|
|
326
333
|
// --- notice: a person operated the harness ---
|
|
327
334
|
|
|
328
|
-
/** Kept apart from `system
|
|
335
|
+
/** Kept apart from `system.*` because these explain a break in the
|
|
329
336
|
* conversation: someone typed a command or stopped a turn. A reader skimming
|
|
330
337
|
* for why the thread jumps needs them, and can skip what the harness injected
|
|
331
338
|
* for its own reasons. */
|
|
332
|
-
const NoticeSlash = item(Type.Literal("notice
|
|
339
|
+
const NoticeSlash = item(Type.Literal("notice.slash"), {
|
|
333
340
|
command: Type.String(),
|
|
334
341
|
args: Type.Optional(Type.String()),
|
|
335
342
|
stdout: Type.Optional(Type.String()),
|
|
336
343
|
});
|
|
337
|
-
const NoticeInterrupt = item(Type.Literal("notice
|
|
344
|
+
const NoticeInterrupt = item(Type.Literal("notice.interrupt"), {
|
|
338
345
|
text: Type.Optional(Text),
|
|
339
346
|
});
|
|
340
347
|
|
|
341
348
|
// --- system: the harness talking in someone else's voice ---
|
|
342
349
|
|
|
343
|
-
const SystemCompact = item(Type.Literal("system
|
|
344
|
-
const SystemApiError = item(Type.Literal("system
|
|
345
|
-
const SystemTask = item(Type.Literal("system
|
|
350
|
+
const SystemCompact = item(Type.Literal("system.compact"), { text: Text });
|
|
351
|
+
const SystemApiError = item(Type.Literal("system.api.error"), { text: Text });
|
|
352
|
+
const SystemTask = item(Type.Literal("system.task"), {
|
|
346
353
|
text: Text,
|
|
347
354
|
/** The background task or monitor the event came from. */
|
|
348
355
|
task_id: Type.Optional(Type.String()),
|
|
349
356
|
event: Type.Optional(Type.String()),
|
|
350
357
|
});
|
|
351
|
-
const SystemCaveat = item(Type.Literal("system
|
|
352
|
-
const SystemResume = item(Type.Literal("system
|
|
358
|
+
const SystemCaveat = item(Type.Literal("system.caveat"), { text: Text });
|
|
359
|
+
const SystemResume = item(Type.Literal("system.resume"), { text: Text });
|
|
353
360
|
|
|
354
|
-
/** `system
|
|
361
|
+
/** `system.attachment.<kind>` — the kind is the harness's own word for what it
|
|
355
362
|
* attached, taken through unchanged so an attachment nobody has seen before
|
|
356
363
|
* still arrives under its own name instead of collapsing into `unknown`. */
|
|
357
|
-
const SystemAttachment = item(Type.String({ pattern: "^system
|
|
364
|
+
const SystemAttachment = item(Type.String({ pattern: "^system\\.attachment\\.[A-Za-z0-9_-]+$" }), {
|
|
358
365
|
attachment: Type.Record(Type.String(), Type.Unknown()),
|
|
359
366
|
});
|
|
360
367
|
|
|
361
368
|
/** What the reader could not place. It is still an item: a line that vanishes
|
|
362
369
|
* silently is the one failure a dump cannot be read around. */
|
|
363
|
-
const SystemUnknown = item(Type.Literal("system
|
|
370
|
+
const SystemUnknown = item(Type.Literal("system.unknown"), {
|
|
364
371
|
record: Type.Record(Type.String(), Type.Unknown()),
|
|
365
372
|
});
|
|
366
373
|
|
|
367
374
|
// --- hook: code the operator installed ---
|
|
368
375
|
|
|
369
|
-
/** `hook
|
|
376
|
+
/** `hook.<Event>` — the event alone, never the matcher. The name a hook runs
|
|
370
377
|
* under is `PreToolUse:Bash`, whose `:` would read as a level of the hierarchy
|
|
371
|
-
* and make `hook
|
|
378
|
+
* and make `hook.PreToolUse` select nothing; the full name is a field instead,
|
|
372
379
|
* and prefix selection keeps meaning what it says. */
|
|
373
|
-
const Hook = item(Type.String({ pattern: "^hook
|
|
380
|
+
const Hook = item(Type.String({ pattern: "^hook\\.[A-Za-z0-9_-]+$" }), {
|
|
374
381
|
/** The hook's full name, matcher included. */
|
|
375
382
|
hook_name: Type.String(),
|
|
376
383
|
outcome: Type.Union([
|
|
@@ -387,19 +394,19 @@ const Hook = item(Type.String({ pattern: "^hook:[A-Za-z0-9_.-]+$" }), {
|
|
|
387
394
|
tool_use_id: Type.Optional(Type.String()),
|
|
388
395
|
});
|
|
389
396
|
|
|
390
|
-
// --- tool: `tool
|
|
397
|
+
// --- tool: `tool.<Name>`, one item for the call and one for the result ---
|
|
391
398
|
|
|
392
|
-
const ToolType = Type.String({ pattern: "^tool
|
|
399
|
+
const ToolType = Type.String({ pattern: "^tool\\.[A-Za-z0-9_-]+$" });
|
|
393
400
|
|
|
394
401
|
function toolUse<N extends string, F extends Record<string, TSchema>>(name: N, fields: F) {
|
|
395
|
-
return item(Type.Literal(`tool
|
|
402
|
+
return item(Type.Literal(`tool.${name}` as const), {
|
|
396
403
|
...USE_FIELDS,
|
|
397
404
|
...fields,
|
|
398
405
|
});
|
|
399
406
|
}
|
|
400
407
|
|
|
401
408
|
function toolResult<N extends string, F extends Record<string, TSchema>>(name: N, fields: F) {
|
|
402
|
-
return item(Type.Literal(`tool
|
|
409
|
+
return item(Type.Literal(`tool.${name}` as const), {
|
|
403
410
|
...RESULT_FIELDS,
|
|
404
411
|
...fields,
|
|
405
412
|
});
|
|
@@ -453,7 +460,7 @@ const TOOL_ITEMS = [
|
|
|
453
460
|
toolResult("WebFetch", { text: OptText }),
|
|
454
461
|
toolUse("WebSearch", { query: Type.String() }),
|
|
455
462
|
toolResult("WebSearch", { results: OptCount }),
|
|
456
|
-
/** The same exchange `message
|
|
463
|
+
/** The same exchange `message.sub.*` carries, seen from the calling side:
|
|
457
464
|
* this pair states that an agent was started and how it ended, and what it
|
|
458
465
|
* answered stays with the message. */
|
|
459
466
|
toolUse("Agent", {
|
|
@@ -493,7 +500,7 @@ const TOOL_ITEMS = [
|
|
|
493
500
|
* Items are finer than lines: one assistant record becomes the thinking, the
|
|
494
501
|
* text and each tool call it held. A reader that does not recognise a record
|
|
495
502
|
* still emits one — as a tool it has no fields for, an attachment under its own
|
|
496
|
-
* kind, or `system
|
|
503
|
+
* kind, or `system.unknown` — so nothing in the file goes missing without
|
|
497
504
|
* saying so. */
|
|
498
505
|
export const TranscriptItem = Type.Union(
|
|
499
506
|
[
|
|
@@ -628,5 +635,5 @@ export const DumpPresetsReadResult = Type.Object({
|
|
|
628
635
|
});
|
|
629
636
|
export type DumpPresetsReadResult = Static<typeof DumpPresetsReadResult>;
|
|
630
637
|
|
|
631
|
-
export const DumpPresetsReadRequest = request("
|
|
632
|
-
export const DumpPresetsReadResponse = response("
|
|
638
|
+
export const DumpPresetsReadRequest = request("dump.presets.read", DumpPresetsReadArgs);
|
|
639
|
+
export const DumpPresetsReadResponse = response("dump.presets.read", DumpPresetsReadResult);
|