@ccmsg/protocol 1.11.0 → 1.13.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 +1 -1
- package/src/attributes.ts +9 -1
- package/src/common/topics.ts +5 -1
- package/src/control/dump.ts +79 -17
- package/src/control/transcript.ts +75 -1
- package/src/fixtures/control.ts +63 -8
- package/src/fixtures/index.ts +5 -0
- package/src/fixtures/topics.ts +20 -1
- package/src/schemas.ts +8 -0
package/package.json
CHANGED
package/src/attributes.ts
CHANGED
|
@@ -196,7 +196,7 @@ export const OP_ATTRIBUTES = {
|
|
|
196
196
|
errors: [],
|
|
197
197
|
},
|
|
198
198
|
|
|
199
|
-
// --- control: session observation and operation (
|
|
199
|
+
// --- control: session observation and operation (10) ---
|
|
200
200
|
session_kill: {
|
|
201
201
|
plane: "control",
|
|
202
202
|
roles: USER_ONLY,
|
|
@@ -248,6 +248,14 @@ export const OP_ATTRIBUTES = {
|
|
|
248
248
|
scope: "role",
|
|
249
249
|
errors: ["not_found"],
|
|
250
250
|
},
|
|
251
|
+
transcript_items_read: {
|
|
252
|
+
plane: "control",
|
|
253
|
+
roles: AGENT_AND_USER,
|
|
254
|
+
needs_hello: true,
|
|
255
|
+
locality: "instance-local",
|
|
256
|
+
scope: "role",
|
|
257
|
+
errors: ["not_found"],
|
|
258
|
+
},
|
|
251
259
|
session_fork_origin: {
|
|
252
260
|
plane: "control",
|
|
253
261
|
roles: USER_ONLY,
|
package/src/common/topics.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const PLAIN_TOPICS = [
|
|
|
15
15
|
] as const;
|
|
16
16
|
|
|
17
17
|
/** Topics naming one session, written `<topic>:<sid>`. */
|
|
18
|
-
export const SESSION_SCOPED_TOPICS = ["session_status", "transcript"] as const;
|
|
18
|
+
export const SESSION_SCOPED_TOPICS = ["session_status", "transcript", "transcript_items"] as const;
|
|
19
19
|
|
|
20
20
|
/** Topics naming one namespace, written `<topic>:<ns>`. The parameter is a name
|
|
21
21
|
* its users choose rather than an identifier this contract issues, so it is
|
|
@@ -102,6 +102,10 @@ export const TOPIC_ATTRIBUTES = {
|
|
|
102
102
|
// half to leave alone: the frame is simply the whole of it.
|
|
103
103
|
session_status: { roles: ["user"], granularity: "whole" },
|
|
104
104
|
transcript: { roles: ["user"], granularity: "append" },
|
|
105
|
+
// The same appending, in items rather than in bytes. Both are offered because
|
|
106
|
+
// they answer different needs: one draws the conversation, the other shows a
|
|
107
|
+
// record as it was written.
|
|
108
|
+
transcript_items: { roles: ["user"], granularity: "append" },
|
|
105
109
|
kv: { roles: ["user"], granularity: "element" },
|
|
106
110
|
// The only topic no person may subscribe to: its elements are the secrets
|
|
107
111
|
// that authenticate them. A relay carries it as the instance it is, not on a
|
package/src/control/dump.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Static, type TSchema, Type } from "@sinclair/typebox";
|
|
2
2
|
import { request, response } from "../envelope.ts";
|
|
3
|
-
import { Timestamp } from "../identifiers.ts";
|
|
3
|
+
import { Sid, Timestamp } from "../identifiers.ts";
|
|
4
4
|
|
|
5
5
|
/** The vocabulary a transcript is read in: item types, the shape one item
|
|
6
6
|
* takes, the ledger of ids they carry, and the named selections a person dumps
|
|
@@ -65,14 +65,47 @@ export const TranscriptItemSelector = Type.String({
|
|
|
65
65
|
});
|
|
66
66
|
export type TranscriptItemSelector = Static<typeof TranscriptItemSelector>;
|
|
67
67
|
|
|
68
|
+
/** Where in the transcript the record an item was read from begins, and how far
|
|
69
|
+
* it runs.
|
|
70
|
+
*
|
|
71
|
+
* An item is what a reader made of a record, and a reader is fallible: the one
|
|
72
|
+
* question it cannot answer is what the record actually said. These two numbers
|
|
73
|
+
* are that answer's address — `transcript_read` bounded to end at `offset +
|
|
74
|
+
* bytes` and to carry `bytes` returns the record itself — which is what lets a
|
|
75
|
+
* client show items and still let a person open the line behind one. Several
|
|
76
|
+
* items read out of a single record share the address, so what comes back is
|
|
77
|
+
* the record and not a slice of it. */
|
|
78
|
+
export const TranscriptItemSource = Type.Object(
|
|
79
|
+
{
|
|
80
|
+
offset: Type.Integer({ minimum: 0 }),
|
|
81
|
+
bytes: Type.Integer({ minimum: 1 }),
|
|
82
|
+
},
|
|
83
|
+
{ $id: "TranscriptItemSource" },
|
|
84
|
+
);
|
|
85
|
+
export type TranscriptItemSource = Static<typeof TranscriptItemSource>;
|
|
86
|
+
|
|
87
|
+
/** An item's own identity, `<uuid>:<index>` — the record it was read from and
|
|
88
|
+
* where in that record it stood.
|
|
89
|
+
*
|
|
90
|
+
* The record's id alone does not identify an item: one assistant record becomes
|
|
91
|
+
* the thinking, the text and each call it held, and a link that pointed by
|
|
92
|
+
* `uuid` would name all of them at once. */
|
|
93
|
+
export const TranscriptItemId = Type.String({
|
|
94
|
+
pattern: "^[^\\s:]+:(?:0|[1-9][0-9]*)$",
|
|
95
|
+
$id: "TranscriptItemId",
|
|
96
|
+
});
|
|
97
|
+
export type TranscriptItemId = Static<typeof TranscriptItemId>;
|
|
98
|
+
|
|
68
99
|
/** What every item carries, whatever its type.
|
|
69
100
|
*
|
|
70
|
-
* `
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
101
|
+
* `id` is what the links below point with — never a position in the array,
|
|
102
|
+
* since a selection or a range decides which items exist in a given read — and
|
|
103
|
+
* `uuid` stays beside it as the record the item came out of, which is what a
|
|
104
|
+
* reader groups by when it wants the whole of one line. */
|
|
74
105
|
const BASE_FIELDS = {
|
|
106
|
+
id: TranscriptItemId,
|
|
75
107
|
uuid: Type.String({ minLength: 1 }),
|
|
108
|
+
source: TranscriptItemSource,
|
|
76
109
|
/** The item's own instant. A call and its result each keep their own. */
|
|
77
110
|
at: Timestamp,
|
|
78
111
|
/** Which turn of the session the item fell in, when the reader could place
|
|
@@ -86,22 +119,25 @@ const BASE_FIELDS = {
|
|
|
86
119
|
* Some results arrive many turns later — an agent runs, a monitor waits — with
|
|
87
120
|
* other items in between, so folding them into a single item would make the
|
|
88
121
|
* reader decide which of the two instants the fold happens at. Two items each
|
|
89
|
-
* keep their own, linked by
|
|
90
|
-
*
|
|
91
|
-
*
|
|
122
|
+
* keep their own, linked by id, and folding is left to whoever draws them.
|
|
123
|
+
*
|
|
124
|
+
* A link is written from the whole transcript and not from the slice it travels
|
|
125
|
+
* in, so a `result_item` naming an item outside the range asked for is the
|
|
126
|
+
* ordinary case rather than a broken pointer: the reader knows the id and can
|
|
127
|
+
* ask for it. A `use` without one is a call that has not come back. */
|
|
92
128
|
const USE_FIELDS = {
|
|
93
129
|
role: Type.Literal("use"),
|
|
94
|
-
/** The result's
|
|
95
|
-
result_item: Type.Optional(
|
|
130
|
+
/** The result's id, once there is one. */
|
|
131
|
+
result_item: Type.Optional(TranscriptItemId),
|
|
96
132
|
};
|
|
97
133
|
|
|
98
134
|
const RESULT_FIELDS = {
|
|
99
135
|
role: Type.Literal("result"),
|
|
100
136
|
/** The call this answers. Always known: a result exists because a call did. */
|
|
101
|
-
parent_item:
|
|
137
|
+
parent_item: TranscriptItemId,
|
|
102
138
|
};
|
|
103
139
|
|
|
104
|
-
function item
|
|
140
|
+
function item<T extends TSchema, F extends Record<string, TSchema>>(type: T, fields: F) {
|
|
105
141
|
return Type.Object({ ...BASE_FIELDS, type, ...fields });
|
|
106
142
|
}
|
|
107
143
|
|
|
@@ -220,16 +256,16 @@ const Hook = item(Type.String({ pattern: "^hook:[A-Za-z0-9_.-]+$" }), {
|
|
|
220
256
|
|
|
221
257
|
const ToolType = Type.String({ pattern: "^tool:[A-Za-z0-9_.-]+$" });
|
|
222
258
|
|
|
223
|
-
function toolUse
|
|
224
|
-
return item(Type.Literal(`tool:${name}`), {
|
|
259
|
+
function toolUse<N extends string, F extends Record<string, TSchema>>(name: N, fields: F) {
|
|
260
|
+
return item(Type.Literal(`tool:${name}` as const), {
|
|
225
261
|
...USE_FIELDS,
|
|
226
262
|
tool_use_id: Type.String(),
|
|
227
263
|
...fields,
|
|
228
264
|
});
|
|
229
265
|
}
|
|
230
266
|
|
|
231
|
-
function toolResult
|
|
232
|
-
return item(Type.Literal(`tool:${name}`), {
|
|
267
|
+
function toolResult<N extends string, F extends Record<string, TSchema>>(name: N, fields: F) {
|
|
268
|
+
return item(Type.Literal(`tool:${name}` as const), {
|
|
233
269
|
...RESULT_FIELDS,
|
|
234
270
|
tool_use_id: Type.String(),
|
|
235
271
|
...fields,
|
|
@@ -253,7 +289,7 @@ const ToolResultGeneric = item(ToolType, {
|
|
|
253
289
|
result: Type.Record(Type.String(), Type.Unknown()),
|
|
254
290
|
});
|
|
255
291
|
|
|
256
|
-
const TOOL_ITEMS
|
|
292
|
+
const TOOL_ITEMS = [
|
|
257
293
|
/** No exit code: what the harness records of a shell call is its output and
|
|
258
294
|
* whether it was interrupted, so that is what a reader has to judge by. */
|
|
259
295
|
toolUse("Bash", { command: Type.String(), description: OptText }),
|
|
@@ -396,6 +432,32 @@ export type DumpIdEntry = Static<typeof DumpIdEntry>;
|
|
|
396
432
|
export const DumpIds = Type.Array(DumpIdEntry, { $id: "DumpIds" });
|
|
397
433
|
export type DumpIds = Static<typeof DumpIds>;
|
|
398
434
|
|
|
435
|
+
/** The file a dump is written to.
|
|
436
|
+
*
|
|
437
|
+
* The reply to a dump names a path rather than carrying the items, so the file
|
|
438
|
+
* is where they actually travel — which makes its shape as much a part of the
|
|
439
|
+
* contract as the reply is: a successor session handed the path, or a client
|
|
440
|
+
* that fetches it, would otherwise be reading a format nothing states. It
|
|
441
|
+
* repeats what it was asked for, because a file outlives the request that made
|
|
442
|
+
* it and has to say on its own what it is a dump of and what was left out. */
|
|
443
|
+
export const SessionDumpFile = Type.Object(
|
|
444
|
+
{
|
|
445
|
+
sid: Sid,
|
|
446
|
+
/** The agent the dump is of, absent when it is of the session itself. */
|
|
447
|
+
agent_id: Type.Optional(Type.String()),
|
|
448
|
+
written_at: Timestamp,
|
|
449
|
+
/** The selection as applied: presets expanded and exclusions kept in
|
|
450
|
+
* place, so the file states what it holds without the instance's config
|
|
451
|
+
* having to be read beside it. */
|
|
452
|
+
types: Type.Array(TranscriptItemSelector),
|
|
453
|
+
/** Oldest first, as the transcript had them. */
|
|
454
|
+
items: Type.Array(TranscriptItem),
|
|
455
|
+
ids: DumpIds,
|
|
456
|
+
},
|
|
457
|
+
{ $id: "SessionDumpFile" },
|
|
458
|
+
);
|
|
459
|
+
export type SessionDumpFile = Static<typeof SessionDumpFile>;
|
|
460
|
+
|
|
399
461
|
/** A selection an operator named and can ask for by name.
|
|
400
462
|
*
|
|
401
463
|
* Presets are configured on the instance rather than fixed here, because what
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Static, Type } from "@sinclair/typebox";
|
|
2
2
|
import { request, response, topicFrame } from "../envelope.ts";
|
|
3
|
-
import { Sid } from "../identifiers.ts";
|
|
3
|
+
import { Sid, Timestamp } from "../identifiers.ts";
|
|
4
|
+
import { DumpIds, TranscriptItem, TranscriptItemId, TranscriptItemSelector } from "./dump.ts";
|
|
4
5
|
|
|
5
6
|
/** Reads a slice of a session's transcript.
|
|
6
7
|
*
|
|
@@ -50,6 +51,79 @@ export type TranscriptReadResult = Static<typeof TranscriptReadResult>;
|
|
|
50
51
|
export const TranscriptReadRequest = request("transcript_read", TranscriptReadArgs);
|
|
51
52
|
export const TranscriptReadResponse = response("transcript_read", TranscriptReadResult);
|
|
52
53
|
|
|
54
|
+
/** Reads a slice of a transcript as the items it was read into.
|
|
55
|
+
*
|
|
56
|
+
* The read above answers with the file's own lines, which leaves whoever asked
|
|
57
|
+
* holding a harness's private format; this answers with what those lines were
|
|
58
|
+
* classified as, so a client draws items and never learns how a record is
|
|
59
|
+
* shaped. Both stay: the typed read is what a client works in, and the raw one
|
|
60
|
+
* is how it fetches the record behind an item it wants to see verbatim, by the
|
|
61
|
+
* `source` that item carries.
|
|
62
|
+
*
|
|
63
|
+
* The range is cut the way a dump's is — an instant or a record on either side
|
|
64
|
+
* — and `since_id` resumes a read that stopped at a limit. The role decides how
|
|
65
|
+
* much is visible, as it does for the raw read. */
|
|
66
|
+
export const TranscriptItemsReadArgs = Type.Object({
|
|
67
|
+
sid: Sid,
|
|
68
|
+
/** Read one agent below the session instead of the session itself. */
|
|
69
|
+
agent_id: Type.Optional(Type.String()),
|
|
70
|
+
/** Inclusive lower bound in time. */
|
|
71
|
+
since_at: Type.Optional(Timestamp),
|
|
72
|
+
/** Inclusive lower bound as a transcript record id, which cuts at that
|
|
73
|
+
* record's position rather than at its clock. Give one lower bound only. */
|
|
74
|
+
since_uuid: Type.Optional(Type.String()),
|
|
75
|
+
/** Resume at this item, the one a previous reply named as `next`. Finer than
|
|
76
|
+
* `since_uuid`, which would start again at the first item of a record whose
|
|
77
|
+
* later items were already read. */
|
|
78
|
+
since_id: Type.Optional(TranscriptItemId),
|
|
79
|
+
until_at: Type.Optional(Timestamp),
|
|
80
|
+
until_uuid: Type.Optional(Type.String()),
|
|
81
|
+
/** Which item types to keep, applied left to right. Absent keeps everything
|
|
82
|
+
* but the attachments, as a dump's absent selection does. */
|
|
83
|
+
types: Type.Optional(Type.Array(TranscriptItemSelector)),
|
|
84
|
+
/** How many items to answer with; the instance narrows this to its own
|
|
85
|
+
* limit. */
|
|
86
|
+
limit: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
87
|
+
});
|
|
88
|
+
export type TranscriptItemsReadArgs = Static<typeof TranscriptItemsReadArgs>;
|
|
89
|
+
|
|
90
|
+
export const TranscriptItemsReadResult = Type.Object({
|
|
91
|
+
/** Oldest first, as the transcript had them. */
|
|
92
|
+
items: Type.Array(TranscriptItem),
|
|
93
|
+
/** The first item left out, when a limit cut the answer short. Absent means
|
|
94
|
+
* the range was answered whole. */
|
|
95
|
+
next: Type.Optional(TranscriptItemId),
|
|
96
|
+
/** The ids the answered items carried, gathered as a dump gathers them.
|
|
97
|
+
* Absent when the caller did not ask the instance to collect them. */
|
|
98
|
+
ids: Type.Optional(DumpIds),
|
|
99
|
+
});
|
|
100
|
+
export type TranscriptItemsReadResult = Static<typeof TranscriptItemsReadResult>;
|
|
101
|
+
|
|
102
|
+
export const TranscriptItemsReadRequest = request("transcript_items_read", TranscriptItemsReadArgs);
|
|
103
|
+
export const TranscriptItemsReadResponse = response(
|
|
104
|
+
"transcript_items_read",
|
|
105
|
+
TranscriptItemsReadResult,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/** The `transcript_items:<sid>` topic.
|
|
109
|
+
*
|
|
110
|
+
* What `transcript:<sid>` carries as appended bytes, carried as the items those
|
|
111
|
+
* bytes were read as. A subscriber holds a list it only ever appends to, so the
|
|
112
|
+
* opening frame is the tail of it — the last items the instance kept, in a
|
|
113
|
+
* count it decides — and every frame after carries what has since been
|
|
114
|
+
* classified. A client that wants further back asks for it by range rather than
|
|
115
|
+
* waiting for a snapshot to grow.
|
|
116
|
+
*
|
|
117
|
+
* A record still being written is not classified until its line ends, which is
|
|
118
|
+
* the same rule the raw topic sends whole lines under. */
|
|
119
|
+
export const TranscriptItemsFrame = topicFrame(
|
|
120
|
+
"transcript_items",
|
|
121
|
+
Type.Object({
|
|
122
|
+
sid: Sid,
|
|
123
|
+
items: Type.Array(TranscriptItem),
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
|
|
53
127
|
/** The `transcript:<sid>` topic.
|
|
54
128
|
*
|
|
55
129
|
* The one topic whose frames are not a whole value: a transcript is appended
|
package/src/fixtures/control.ts
CHANGED
|
@@ -48,6 +48,7 @@ import type {
|
|
|
48
48
|
import type {
|
|
49
49
|
DumpPresetsReadRequest,
|
|
50
50
|
DumpPresetsReadResponse,
|
|
51
|
+
SessionDumpFile,
|
|
51
52
|
TranscriptItem,
|
|
52
53
|
} from "../control/dump.ts";
|
|
53
54
|
import type {
|
|
@@ -66,7 +67,12 @@ import type {
|
|
|
66
67
|
SessionSearchRequest,
|
|
67
68
|
SessionSearchResponse,
|
|
68
69
|
} from "../control/session.ts";
|
|
69
|
-
import type {
|
|
70
|
+
import type {
|
|
71
|
+
TranscriptItemsReadRequest,
|
|
72
|
+
TranscriptItemsReadResponse,
|
|
73
|
+
TranscriptReadRequest,
|
|
74
|
+
TranscriptReadResponse,
|
|
75
|
+
} from "../control/transcript.ts";
|
|
70
76
|
import type { TranslateRunRequest, TranslateRunResponse } from "../control/translate.ts";
|
|
71
77
|
import { FIXTURE_IDS, FIXTURE_NOW } from "./ids.ts";
|
|
72
78
|
|
|
@@ -163,7 +169,7 @@ export const SESSION_DUMP_WRITE_REQUEST: Static<typeof SessionDumpWriteRequest>
|
|
|
163
169
|
export const SESSION_DUMP_WRITE_RESPONSE: Static<typeof SessionDumpWriteResponse> = {
|
|
164
170
|
ok: true,
|
|
165
171
|
request_id,
|
|
166
|
-
path: "/transcripts/6f1a2b3c.dump.
|
|
172
|
+
path: "/transcripts/6f1a2b3c.dump.json",
|
|
167
173
|
instance,
|
|
168
174
|
entries: { thinking: 41, "tool:Bash": 62, "tool:Read": 25 },
|
|
169
175
|
ids: [
|
|
@@ -180,6 +186,16 @@ export const SESSION_DUMP_WRITE_RESPONSE: Static<typeof SessionDumpWriteResponse
|
|
|
180
186
|
bytes: 65_536,
|
|
181
187
|
};
|
|
182
188
|
|
|
189
|
+
/** The file the reply above names, holding the items themselves. */
|
|
190
|
+
export const SESSION_DUMP_FILE: Static<typeof SessionDumpFile> = {
|
|
191
|
+
sid,
|
|
192
|
+
agent_id: "a471372f2",
|
|
193
|
+
written_at: FIXTURE_NOW,
|
|
194
|
+
types: ["tool:Read", "tool:Write", "tool:Edit", "tool:Glob", "thinking"],
|
|
195
|
+
items: [],
|
|
196
|
+
ids: [{ kind: "agent", id: "a471372f2", label: "dump-kinds-design", status: "ok" }],
|
|
197
|
+
};
|
|
198
|
+
|
|
183
199
|
export const DUMP_PRESETS_READ_REQUEST: Static<typeof DumpPresetsReadRequest> = {
|
|
184
200
|
request_id,
|
|
185
201
|
op: "dump_presets_read",
|
|
@@ -206,48 +222,66 @@ export const DUMP_PRESETS_READ_RESPONSE: Static<typeof DumpPresetsReadResponse>
|
|
|
206
222
|
*
|
|
207
223
|
* A call and its result appear as the two items they are, linked both ways, so
|
|
208
224
|
* an implementation can check that it draws the pair without assuming they are
|
|
209
|
-
* adjacent.
|
|
225
|
+
* adjacent. The thinking and the call after it were read out of one record and
|
|
226
|
+
* carry the one address it sits at, which is the case an id exists for. */
|
|
210
227
|
export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
|
|
211
228
|
{
|
|
229
|
+
id: "3f9a21c4:0",
|
|
212
230
|
uuid: "3f9a21c4",
|
|
231
|
+
source: { offset: 180_000, bytes: 420 },
|
|
213
232
|
type: "message:user:in",
|
|
214
233
|
at: FIXTURE_NOW - 3_600_000,
|
|
215
234
|
turn: 1,
|
|
216
235
|
text: "dump のアイテム型を整理して",
|
|
217
236
|
},
|
|
218
|
-
{ uuid: "f10b6d43", type: "thinking", at: FIXTURE_NOW - 3_500_000, text: "台帳として分ける" },
|
|
219
237
|
{
|
|
220
|
-
|
|
238
|
+
id: "f10b6d43:0",
|
|
239
|
+
uuid: "f10b6d43",
|
|
240
|
+
source: { offset: 180_420, bytes: 980 },
|
|
241
|
+
type: "thinking",
|
|
242
|
+
at: FIXTURE_NOW - 3_500_000,
|
|
243
|
+
text: "台帳として分ける",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "f10b6d43:1",
|
|
247
|
+
uuid: "f10b6d43",
|
|
248
|
+
source: { offset: 180_420, bytes: 980 },
|
|
221
249
|
type: "tool:Bash",
|
|
222
250
|
at: FIXTURE_NOW - 3_400_000,
|
|
223
251
|
role: "use",
|
|
224
252
|
tool_use_id: "toolu_01Ne9BDS",
|
|
225
|
-
result_item: "18d6f2c9",
|
|
253
|
+
result_item: "18d6f2c9:0",
|
|
226
254
|
command: "jq -r '.type' session.jsonl | sort | uniq -c",
|
|
227
255
|
description: "count the record types",
|
|
228
256
|
},
|
|
229
257
|
{
|
|
258
|
+
id: "18d6f2c9:0",
|
|
230
259
|
uuid: "18d6f2c9",
|
|
260
|
+
source: { offset: 181_400, bytes: 260 },
|
|
231
261
|
type: "tool:Bash",
|
|
232
262
|
at: FIXTURE_NOW - 3_399_000,
|
|
233
263
|
role: "result",
|
|
234
264
|
tool_use_id: "toolu_01Ne9BDS",
|
|
235
|
-
parent_item: "
|
|
265
|
+
parent_item: "f10b6d43:1",
|
|
236
266
|
stdout: "1174 assistant\n753 user\n",
|
|
237
267
|
interrupted: false,
|
|
238
268
|
},
|
|
239
269
|
{
|
|
270
|
+
id: "b7e41d09:0",
|
|
240
271
|
uuid: "b7e41d09",
|
|
272
|
+
source: { offset: 181_660, bytes: 640 },
|
|
241
273
|
type: "message:sub:out",
|
|
242
274
|
at: FIXTURE_NOW - 3_300_000,
|
|
243
275
|
role: "use",
|
|
244
|
-
result_item: "c2d80f16",
|
|
276
|
+
result_item: "c2d80f16:0",
|
|
245
277
|
prompt: "docs/design/dump-kinds.md を書き直す",
|
|
246
278
|
agent_id: "a471372f2",
|
|
247
279
|
subagent_type: "opus5-worker-high",
|
|
248
280
|
},
|
|
249
281
|
{
|
|
282
|
+
id: "92e6d4f5:0",
|
|
250
283
|
uuid: "92e6d4f5",
|
|
284
|
+
source: { offset: 182_300, bytes: 310 },
|
|
251
285
|
type: "hook:PreToolUse",
|
|
252
286
|
at: FIXTURE_NOW - 3_200_000,
|
|
253
287
|
hook_name: "PreToolUse:Bash",
|
|
@@ -256,7 +290,9 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
|
|
|
256
290
|
tool_use_id: "toolu_01Ne9BDS",
|
|
257
291
|
},
|
|
258
292
|
{
|
|
293
|
+
id: "81d5c3e4:0",
|
|
259
294
|
uuid: "81d5c3e4",
|
|
295
|
+
source: { offset: 182_610, bytes: 190 },
|
|
260
296
|
type: "system:attachment:queued_command",
|
|
261
297
|
at: FIXTURE_NOW - 3_100_000,
|
|
262
298
|
attachment: { type: "queued_command", command: "/pre-clear" },
|
|
@@ -281,6 +317,25 @@ export const TRANSCRIPT_READ_RESPONSE: Static<typeof TranscriptReadResponse> = {
|
|
|
281
317
|
size: 182_400,
|
|
282
318
|
};
|
|
283
319
|
|
|
320
|
+
export const TRANSCRIPT_ITEMS_READ_REQUEST: Static<typeof TranscriptItemsReadRequest> = {
|
|
321
|
+
request_id,
|
|
322
|
+
op: "transcript_items_read",
|
|
323
|
+
sid,
|
|
324
|
+
since_at: FIXTURE_NOW - 3_600_000,
|
|
325
|
+
types: ["message", "thinking", "tool:Bash"],
|
|
326
|
+
limit: 200,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/** The answer stops where the limit did and names what comes next, so the
|
|
330
|
+
* caller asks for the rest with `since_id` and reads nothing twice. */
|
|
331
|
+
export const TRANSCRIPT_ITEMS_READ_RESPONSE: Static<typeof TranscriptItemsReadResponse> = {
|
|
332
|
+
ok: true,
|
|
333
|
+
request_id,
|
|
334
|
+
items: TRANSCRIPT_ITEMS,
|
|
335
|
+
next: "c2d80f16:0",
|
|
336
|
+
ids: [{ kind: "agent", id: "a471372f2", label: "dump-kinds-design", status: "running" }],
|
|
337
|
+
};
|
|
338
|
+
|
|
284
339
|
export const SESSION_FORK_ORIGIN_REQUEST: Static<typeof SessionForkOriginRequest> = {
|
|
285
340
|
request_id,
|
|
286
341
|
op: "session_fork_origin",
|
package/src/fixtures/index.ts
CHANGED
|
@@ -100,6 +100,10 @@ export const OP_FIXTURES = {
|
|
|
100
100
|
request: control.TRANSCRIPT_READ_REQUEST,
|
|
101
101
|
response: control.TRANSCRIPT_READ_RESPONSE,
|
|
102
102
|
},
|
|
103
|
+
transcript_items_read: {
|
|
104
|
+
request: control.TRANSCRIPT_ITEMS_READ_REQUEST,
|
|
105
|
+
response: control.TRANSCRIPT_ITEMS_READ_RESPONSE,
|
|
106
|
+
},
|
|
103
107
|
session_fork_origin: {
|
|
104
108
|
request: control.SESSION_FORK_ORIGIN_REQUEST,
|
|
105
109
|
response: control.SESSION_FORK_ORIGIN_RESPONSE,
|
|
@@ -162,6 +166,7 @@ export const TOPIC_FIXTURES = {
|
|
|
162
166
|
agents: topics.AGENTS_FRAME,
|
|
163
167
|
session_status: topics.SESSION_STATUS_FRAME,
|
|
164
168
|
transcript: topics.TRANSCRIPT_FRAME,
|
|
169
|
+
transcript_items: topics.TRANSCRIPT_ITEMS_FRAME,
|
|
165
170
|
session_errors: topics.SESSION_ERRORS_FRAME,
|
|
166
171
|
llm_requests: topics.LLM_REQUESTS_FRAME,
|
|
167
172
|
llm_status: topics.LLM_STATUS_FRAME,
|
package/src/fixtures/topics.ts
CHANGED
|
@@ -6,7 +6,8 @@ import type { LlmRequestsFrame, LlmStatusFrame } from "../control/llm.ts";
|
|
|
6
6
|
import type { PeersFrame } from "../control/peers.ts";
|
|
7
7
|
import type { SessionErrorsFrame } from "../control/session-errors.ts";
|
|
8
8
|
import type { SessionStatusFrame } from "../control/session-status.ts";
|
|
9
|
-
import type { TranscriptFrame } from "../control/transcript.ts";
|
|
9
|
+
import type { TranscriptFrame, TranscriptItemsFrame } from "../control/transcript.ts";
|
|
10
|
+
import { TRANSCRIPT_ITEMS } from "./control.ts";
|
|
10
11
|
import type { InboxFrame } from "../messaging/message.ts";
|
|
11
12
|
import type { NotifyFrame } from "../messaging/notify.ts";
|
|
12
13
|
import { FIXTURE_IDS, FIXTURE_NOW } from "./ids.ts";
|
|
@@ -264,6 +265,24 @@ export const TRANSCRIPT_SIZE_FRAME = {
|
|
|
264
265
|
data: { sid, size: 182_400 },
|
|
265
266
|
} satisfies Static<typeof TranscriptFrame>;
|
|
266
267
|
|
|
268
|
+
/** The typed topic, whose frames carry items rather than bytes. */
|
|
269
|
+
export const TRANSCRIPT_ITEMS_FRAME = {
|
|
270
|
+
ev: "topic",
|
|
271
|
+
topic: `transcript_items:${sid}`,
|
|
272
|
+
instance,
|
|
273
|
+
data: { sid, items: TRANSCRIPT_ITEMS.slice(-2) },
|
|
274
|
+
} satisfies Static<typeof TranscriptItemsFrame>;
|
|
275
|
+
|
|
276
|
+
/** Its opening frame, which is the tail the instance kept — the same shape as
|
|
277
|
+
* every frame after it, so a subscriber appends both the same way. */
|
|
278
|
+
export const TRANSCRIPT_ITEMS_SNAPSHOT_FRAME = {
|
|
279
|
+
ev: "topic",
|
|
280
|
+
topic: `transcript_items:${sid}`,
|
|
281
|
+
snapshot: true,
|
|
282
|
+
instance,
|
|
283
|
+
data: { sid, items: TRANSCRIPT_ITEMS },
|
|
284
|
+
} satisfies Static<typeof TranscriptItemsFrame>;
|
|
285
|
+
|
|
267
286
|
export const SESSION_ERRORS_FRAME: Static<typeof SessionErrorsFrame> = {
|
|
268
287
|
ev: "topic",
|
|
269
288
|
topic: "session_errors",
|
package/src/schemas.ts
CHANGED
|
@@ -104,6 +104,9 @@ import {
|
|
|
104
104
|
} from "./control/session.ts";
|
|
105
105
|
import {
|
|
106
106
|
TranscriptFrame,
|
|
107
|
+
TranscriptItemsFrame,
|
|
108
|
+
TranscriptItemsReadRequest,
|
|
109
|
+
TranscriptItemsReadResponse,
|
|
107
110
|
TranscriptReadRequest,
|
|
108
111
|
TranscriptReadResponse,
|
|
109
112
|
} from "./control/transcript.ts";
|
|
@@ -153,6 +156,10 @@ export const OP_SCHEMAS: Record<OpName, OpSchemas> = {
|
|
|
153
156
|
session_dump_write: { request: SessionDumpWriteRequest, response: SessionDumpWriteResponse },
|
|
154
157
|
dump_presets_read: { request: DumpPresetsReadRequest, response: DumpPresetsReadResponse },
|
|
155
158
|
transcript_read: { request: TranscriptReadRequest, response: TranscriptReadResponse },
|
|
159
|
+
transcript_items_read: {
|
|
160
|
+
request: TranscriptItemsReadRequest,
|
|
161
|
+
response: TranscriptItemsReadResponse,
|
|
162
|
+
},
|
|
156
163
|
session_fork_origin: { request: SessionForkOriginRequest, response: SessionForkOriginResponse },
|
|
157
164
|
session_last_live_remove: {
|
|
158
165
|
request: SessionLastLiveRemoveRequest,
|
|
@@ -197,6 +204,7 @@ export const TOPIC_SCHEMAS = {
|
|
|
197
204
|
agents: AgentsFrame,
|
|
198
205
|
session_status: SessionStatusFrame,
|
|
199
206
|
transcript: TranscriptFrame,
|
|
207
|
+
transcript_items: TranscriptItemsFrame,
|
|
200
208
|
session_errors: SessionErrorsFrame,
|
|
201
209
|
llm_requests: LlmRequestsFrame,
|
|
202
210
|
llm_status: LlmStatusFrame,
|