@ccmsg/protocol 1.12.0 → 1.14.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 +52 -16
- package/src/control/transcript.ts +88 -1
- package/src/fixtures/control.ts +68 -7
- 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
|
@@ -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 }),
|
|
@@ -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,92 @@ 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 which end of it a limit keeps follows from which bound was given. A
|
|
65
|
+
* lower bound reads forward from it and `next` names the continuation; an upper
|
|
66
|
+
* bound alone reads the range's last items and `prev` names the continuation
|
|
67
|
+
* backwards, which is how a client that draws the newest items first walks back
|
|
68
|
+
* through a transcript it never has to read whole. The role decides how much is
|
|
69
|
+
* visible, as it does for the raw read. */
|
|
70
|
+
export const TranscriptItemsReadArgs = Type.Object({
|
|
71
|
+
sid: Sid,
|
|
72
|
+
/** Read one agent below the session instead of the session itself. */
|
|
73
|
+
agent_id: Type.Optional(Type.String()),
|
|
74
|
+
/** Inclusive lower bound in time. */
|
|
75
|
+
since_at: Type.Optional(Timestamp),
|
|
76
|
+
/** Inclusive lower bound as a transcript record id, which cuts at that
|
|
77
|
+
* record's position rather than at its clock. Give one lower bound only. */
|
|
78
|
+
since_uuid: Type.Optional(Type.String()),
|
|
79
|
+
/** Resume at this item, the one a previous reply named as `next`. Finer than
|
|
80
|
+
* `since_uuid`, which would start again at the first item of a record whose
|
|
81
|
+
* later items were already read. */
|
|
82
|
+
since_id: Type.Optional(TranscriptItemId),
|
|
83
|
+
until_at: Type.Optional(Timestamp),
|
|
84
|
+
until_uuid: Type.Optional(Type.String()),
|
|
85
|
+
/** Exclusive upper bound as an item id, the one a previous reply named as
|
|
86
|
+
* `prev`. Finer than `until_uuid`, which would stop at a record whose earlier
|
|
87
|
+
* items were already read. */
|
|
88
|
+
until_id: Type.Optional(TranscriptItemId),
|
|
89
|
+
/** Which item types to keep, applied left to right. Absent keeps everything
|
|
90
|
+
* but the attachments, as a dump's absent selection does. */
|
|
91
|
+
types: Type.Optional(Type.Array(TranscriptItemSelector)),
|
|
92
|
+
/** How many items to answer with, taken from the range's start when a lower
|
|
93
|
+
* bound was given and from its end when only an upper one was; the instance
|
|
94
|
+
* narrows this to its own limit. */
|
|
95
|
+
limit: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
96
|
+
});
|
|
97
|
+
export type TranscriptItemsReadArgs = Static<typeof TranscriptItemsReadArgs>;
|
|
98
|
+
|
|
99
|
+
export const TranscriptItemsReadResult = Type.Object({
|
|
100
|
+
/** Oldest first, as the transcript had them. */
|
|
101
|
+
items: Type.Array(TranscriptItem),
|
|
102
|
+
/** The first item left out after the ones answered, when a limit cut a
|
|
103
|
+
* forward read short. Absent means nothing follows within the range. */
|
|
104
|
+
next: Type.Optional(TranscriptItemId),
|
|
105
|
+
/** The first item answered, when a limit left older ones inside the range
|
|
106
|
+
* unread. Pass it back as `until_id` to read what came before. Absent means
|
|
107
|
+
* the range reaches back to its start. */
|
|
108
|
+
prev: Type.Optional(TranscriptItemId),
|
|
109
|
+
/** The ids the answered items carried, gathered as a dump gathers them.
|
|
110
|
+
* Absent when the caller did not ask the instance to collect them. */
|
|
111
|
+
ids: Type.Optional(DumpIds),
|
|
112
|
+
});
|
|
113
|
+
export type TranscriptItemsReadResult = Static<typeof TranscriptItemsReadResult>;
|
|
114
|
+
|
|
115
|
+
export const TranscriptItemsReadRequest = request("transcript_items_read", TranscriptItemsReadArgs);
|
|
116
|
+
export const TranscriptItemsReadResponse = response(
|
|
117
|
+
"transcript_items_read",
|
|
118
|
+
TranscriptItemsReadResult,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
/** The `transcript_items:<sid>` topic.
|
|
122
|
+
*
|
|
123
|
+
* What `transcript:<sid>` carries as appended bytes, carried as the items those
|
|
124
|
+
* bytes were read as. A subscriber holds a list it only ever appends to, so the
|
|
125
|
+
* opening frame is the tail of it — the last items the instance kept, in a
|
|
126
|
+
* count it decides — and every frame after carries what has since been
|
|
127
|
+
* classified. A client that wants further back asks for it by range rather than
|
|
128
|
+
* waiting for a snapshot to grow.
|
|
129
|
+
*
|
|
130
|
+
* A record still being written is not classified until its line ends, which is
|
|
131
|
+
* the same rule the raw topic sends whole lines under. */
|
|
132
|
+
export const TranscriptItemsFrame = topicFrame(
|
|
133
|
+
"transcript_items",
|
|
134
|
+
Type.Object({
|
|
135
|
+
sid: Sid,
|
|
136
|
+
items: Type.Array(TranscriptItem),
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
|
|
53
140
|
/** The `transcript:<sid>` topic.
|
|
54
141
|
*
|
|
55
142
|
* The one topic whose frames are not a whole value: a transcript is appended
|
package/src/fixtures/control.ts
CHANGED
|
@@ -67,7 +67,12 @@ import type {
|
|
|
67
67
|
SessionSearchRequest,
|
|
68
68
|
SessionSearchResponse,
|
|
69
69
|
} from "../control/session.ts";
|
|
70
|
-
import type {
|
|
70
|
+
import type {
|
|
71
|
+
TranscriptItemsReadRequest,
|
|
72
|
+
TranscriptItemsReadResponse,
|
|
73
|
+
TranscriptReadRequest,
|
|
74
|
+
TranscriptReadResponse,
|
|
75
|
+
} from "../control/transcript.ts";
|
|
71
76
|
import type { TranslateRunRequest, TranslateRunResponse } from "../control/translate.ts";
|
|
72
77
|
import { FIXTURE_IDS, FIXTURE_NOW } from "./ids.ts";
|
|
73
78
|
|
|
@@ -217,48 +222,66 @@ export const DUMP_PRESETS_READ_RESPONSE: Static<typeof DumpPresetsReadResponse>
|
|
|
217
222
|
*
|
|
218
223
|
* A call and its result appear as the two items they are, linked both ways, so
|
|
219
224
|
* an implementation can check that it draws the pair without assuming they are
|
|
220
|
-
* 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. */
|
|
221
227
|
export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
|
|
222
228
|
{
|
|
229
|
+
id: "3f9a21c4:0",
|
|
223
230
|
uuid: "3f9a21c4",
|
|
231
|
+
source: { offset: 180_000, bytes: 420 },
|
|
224
232
|
type: "message:user:in",
|
|
225
233
|
at: FIXTURE_NOW - 3_600_000,
|
|
226
234
|
turn: 1,
|
|
227
235
|
text: "dump のアイテム型を整理して",
|
|
228
236
|
},
|
|
229
|
-
{ uuid: "f10b6d43", type: "thinking", at: FIXTURE_NOW - 3_500_000, text: "台帳として分ける" },
|
|
230
237
|
{
|
|
231
|
-
|
|
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 },
|
|
232
249
|
type: "tool:Bash",
|
|
233
250
|
at: FIXTURE_NOW - 3_400_000,
|
|
234
251
|
role: "use",
|
|
235
252
|
tool_use_id: "toolu_01Ne9BDS",
|
|
236
|
-
result_item: "18d6f2c9",
|
|
253
|
+
result_item: "18d6f2c9:0",
|
|
237
254
|
command: "jq -r '.type' session.jsonl | sort | uniq -c",
|
|
238
255
|
description: "count the record types",
|
|
239
256
|
},
|
|
240
257
|
{
|
|
258
|
+
id: "18d6f2c9:0",
|
|
241
259
|
uuid: "18d6f2c9",
|
|
260
|
+
source: { offset: 181_400, bytes: 260 },
|
|
242
261
|
type: "tool:Bash",
|
|
243
262
|
at: FIXTURE_NOW - 3_399_000,
|
|
244
263
|
role: "result",
|
|
245
264
|
tool_use_id: "toolu_01Ne9BDS",
|
|
246
|
-
parent_item: "
|
|
265
|
+
parent_item: "f10b6d43:1",
|
|
247
266
|
stdout: "1174 assistant\n753 user\n",
|
|
248
267
|
interrupted: false,
|
|
249
268
|
},
|
|
250
269
|
{
|
|
270
|
+
id: "b7e41d09:0",
|
|
251
271
|
uuid: "b7e41d09",
|
|
272
|
+
source: { offset: 181_660, bytes: 640 },
|
|
252
273
|
type: "message:sub:out",
|
|
253
274
|
at: FIXTURE_NOW - 3_300_000,
|
|
254
275
|
role: "use",
|
|
255
|
-
result_item: "c2d80f16",
|
|
276
|
+
result_item: "c2d80f16:0",
|
|
256
277
|
prompt: "docs/design/dump-kinds.md を書き直す",
|
|
257
278
|
agent_id: "a471372f2",
|
|
258
279
|
subagent_type: "opus5-worker-high",
|
|
259
280
|
},
|
|
260
281
|
{
|
|
282
|
+
id: "92e6d4f5:0",
|
|
261
283
|
uuid: "92e6d4f5",
|
|
284
|
+
source: { offset: 182_300, bytes: 310 },
|
|
262
285
|
type: "hook:PreToolUse",
|
|
263
286
|
at: FIXTURE_NOW - 3_200_000,
|
|
264
287
|
hook_name: "PreToolUse:Bash",
|
|
@@ -267,7 +290,9 @@ export const TRANSCRIPT_ITEMS: Static<typeof TranscriptItem>[] = [
|
|
|
267
290
|
tool_use_id: "toolu_01Ne9BDS",
|
|
268
291
|
},
|
|
269
292
|
{
|
|
293
|
+
id: "81d5c3e4:0",
|
|
270
294
|
uuid: "81d5c3e4",
|
|
295
|
+
source: { offset: 182_610, bytes: 190 },
|
|
271
296
|
type: "system:attachment:queued_command",
|
|
272
297
|
at: FIXTURE_NOW - 3_100_000,
|
|
273
298
|
attachment: { type: "queued_command", command: "/pre-clear" },
|
|
@@ -292,6 +317,42 @@ export const TRANSCRIPT_READ_RESPONSE: Static<typeof TranscriptReadResponse> = {
|
|
|
292
317
|
size: 182_400,
|
|
293
318
|
};
|
|
294
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
|
+
|
|
339
|
+
/** A client drawing the newest items first asks with an upper bound alone, and
|
|
340
|
+
* walks back by handing the `prev` it was given to the next call. */
|
|
341
|
+
export const TRANSCRIPT_ITEMS_READ_BACKWARD_REQUEST: Static<typeof TranscriptItemsReadRequest> = {
|
|
342
|
+
request_id,
|
|
343
|
+
op: "transcript_items_read",
|
|
344
|
+
sid,
|
|
345
|
+
until_id: "18d6f2c9:0",
|
|
346
|
+
limit: 2,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export const TRANSCRIPT_ITEMS_READ_BACKWARD_RESPONSE: Static<typeof TranscriptItemsReadResponse> = {
|
|
350
|
+
ok: true,
|
|
351
|
+
request_id,
|
|
352
|
+
items: TRANSCRIPT_ITEMS.slice(1, 3),
|
|
353
|
+
prev: "f10b6d43:0",
|
|
354
|
+
};
|
|
355
|
+
|
|
295
356
|
export const SESSION_FORK_ORIGIN_REQUEST: Static<typeof SessionForkOriginRequest> = {
|
|
296
357
|
request_id,
|
|
297
358
|
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,
|