@agentshouse/kit 0.1.0-alpha.27 → 0.1.0-alpha.29
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 +136 -49
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/binding.js +11 -0
- package/src/blocks.js +2 -26
- package/src/bridge.js +23 -13
- package/src/contract.js +6 -9
- package/src/draft.js +4 -82
- package/src/house.js +63 -58
- package/src/kit-work-stream.js +57 -27
- package/src/launcher.js +21 -6
- package/src/pacing.js +41 -0
- package/src/plan.js +37 -45
- package/src/tasks.js +21 -18
- package/src/telegram-view.js +214 -0
- package/src/telegram.js +124 -53
- package/src/turn-frames.js +69 -0
- package/src/worker.js +144 -91
- package/src/working-copy.js +18 -9
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
TELEGRAM_RICH_MESSAGE_BLOCKS_MAX,
|
|
4
|
+
TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX,
|
|
5
|
+
} from "./contract.js";
|
|
6
|
+
|
|
7
|
+
const PARTS = Object.freeze({ draft: "blocks", plan: "steps" });
|
|
8
|
+
const UNCOUNTED = new Set(["type", "language"]);
|
|
9
|
+
|
|
10
|
+
function richCharacters(value, key = null) {
|
|
11
|
+
if (typeof value === "string") return UNCOUNTED.has(key) ? 0 : value.length;
|
|
12
|
+
if (Array.isArray(value)) return value.reduce((sum, item) => sum + richCharacters(item), 0);
|
|
13
|
+
if (typeof value === "object" && value !== null) {
|
|
14
|
+
return Object.entries(value).reduce((sum, [name, item]) => sum + richCharacters(item, name), 0);
|
|
15
|
+
}
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function richText(value, key = null) {
|
|
20
|
+
if (typeof value === "string") return UNCOUNTED.has(key) ? [] : [value];
|
|
21
|
+
if (Array.isArray(value)) return value.flatMap((item) => richText(item));
|
|
22
|
+
if (typeof value === "object" && value !== null) {
|
|
23
|
+
return Object.entries(value).flatMap(([name, item]) => richText(item, name));
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function slices(text) {
|
|
29
|
+
const pieces = [];
|
|
30
|
+
let start = 0;
|
|
31
|
+
while (start < text.length) {
|
|
32
|
+
let end = Math.min(start + TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX, text.length);
|
|
33
|
+
const last = text.charCodeAt(end - 1);
|
|
34
|
+
if (end < text.length && last >= 0xd800 && last <= 0xdbff) end -= 1;
|
|
35
|
+
pieces.push(text.slice(start, end));
|
|
36
|
+
start = end;
|
|
37
|
+
}
|
|
38
|
+
return pieces;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function grouped(block, listed) {
|
|
42
|
+
const groups = [];
|
|
43
|
+
let group = [];
|
|
44
|
+
let characters = 0;
|
|
45
|
+
for (const entry of block[listed]) {
|
|
46
|
+
const length = richCharacters(entry);
|
|
47
|
+
if (group.length > 0 && characters + length > TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX) {
|
|
48
|
+
groups.push(group);
|
|
49
|
+
group = [];
|
|
50
|
+
characters = 0;
|
|
51
|
+
}
|
|
52
|
+
group.push(entry);
|
|
53
|
+
characters += length;
|
|
54
|
+
}
|
|
55
|
+
if (group.length > 0) groups.push(group);
|
|
56
|
+
return groups.flatMap((entries) => fitted({ ...block, [listed]: entries }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fitted(block) {
|
|
60
|
+
if (richCharacters(block) <= TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX) return [block];
|
|
61
|
+
if (typeof block.text === "string") return slices(block.text).map((text) => ({ ...block, text }));
|
|
62
|
+
if (typeof block.expression === "string") {
|
|
63
|
+
return slices(block.expression).map((expression) => ({ ...block, expression }));
|
|
64
|
+
}
|
|
65
|
+
for (const listed of ["items", "cells"]) {
|
|
66
|
+
if (Array.isArray(block[listed]) && block[listed].length > 1) return grouped(block, listed);
|
|
67
|
+
}
|
|
68
|
+
return slices(richText(block).join("\n")).map((text) => ({ type: "paragraph", text }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function richPieces(message) {
|
|
72
|
+
const pieces = [];
|
|
73
|
+
let blocks = [];
|
|
74
|
+
let characters = 0;
|
|
75
|
+
for (const part of message.blocks.flatMap(fitted)) {
|
|
76
|
+
const length = richCharacters(part);
|
|
77
|
+
if (
|
|
78
|
+
blocks.length > 0 &&
|
|
79
|
+
(blocks.length >= TELEGRAM_RICH_MESSAGE_BLOCKS_MAX ||
|
|
80
|
+
characters + length > TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX)
|
|
81
|
+
) {
|
|
82
|
+
pieces.push({ ...message, blocks });
|
|
83
|
+
blocks = [];
|
|
84
|
+
characters = 0;
|
|
85
|
+
}
|
|
86
|
+
blocks.push(part);
|
|
87
|
+
characters += length;
|
|
88
|
+
}
|
|
89
|
+
pieces.push({ ...message, blocks });
|
|
90
|
+
return pieces;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function listItems(items, ordered) {
|
|
94
|
+
return items.map((item, index) => ({
|
|
95
|
+
blocks: [{ type: "paragraph", text: item }],
|
|
96
|
+
...(ordered ? { value: index + 1, type: "1" } : {}),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function telegramBlock(block) {
|
|
101
|
+
if (typeof block !== "object" || block === null) return null;
|
|
102
|
+
if (block.type === "paragraph") return { type: "paragraph", text: block.text };
|
|
103
|
+
if (block.type === "heading") return { type: "heading", size: block.level, text: block.text };
|
|
104
|
+
if (block.type === "code") {
|
|
105
|
+
return {
|
|
106
|
+
type: "pre",
|
|
107
|
+
...(typeof block.language === "string" ? { language: block.language } : {}),
|
|
108
|
+
text: block.text,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (block.type === "list" && Array.isArray(block.items)) {
|
|
112
|
+
return { type: "list", items: listItems(block.items, block.ordered === true) };
|
|
113
|
+
}
|
|
114
|
+
if (block.type === "table" && Array.isArray(block.headers) && Array.isArray(block.rows)) {
|
|
115
|
+
return {
|
|
116
|
+
type: "table",
|
|
117
|
+
cells: [
|
|
118
|
+
block.headers.map((text) => ({ text, is_header: true })),
|
|
119
|
+
...block.rows.map((row) => (Array.isArray(row) ? row : []).map((text) => ({ text }))),
|
|
120
|
+
],
|
|
121
|
+
is_bordered: true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (block.type === "formula") return { type: "mathematical_expression", expression: block.expression };
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function planBlocks(steps) {
|
|
129
|
+
return [
|
|
130
|
+
{ type: "heading", text: "Plan", size: 3 },
|
|
131
|
+
{
|
|
132
|
+
type: "list",
|
|
133
|
+
items: steps.map((step, index) => ({
|
|
134
|
+
blocks: [{ type: "paragraph", text: [`${step.label} — `, { type: "bold", text: step.status }] }],
|
|
135
|
+
value: index + 1,
|
|
136
|
+
type: "1",
|
|
137
|
+
})),
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function draftId(turn) {
|
|
143
|
+
return (createHash("sha256").update(turn).digest().readUInt32BE(0) % 2_147_483_647) + 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function viewFrame(frame) {
|
|
147
|
+
if (typeof frame !== "object" || frame === null || !Object.hasOwn(PARTS, frame.type)) return null;
|
|
148
|
+
for (const named of ["conversation_id", "turn_id"]) {
|
|
149
|
+
if (typeof frame[named] !== "string" || frame[named] === "") return null;
|
|
150
|
+
}
|
|
151
|
+
if (!Number.isSafeInteger(frame.sequence) || frame.sequence < 1) return null;
|
|
152
|
+
if (!Number.isSafeInteger(frame.from) || frame.from < 0) return null;
|
|
153
|
+
return Array.isArray(frame[PARTS[frame.type]]) ? frame : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function turnViews(options) {
|
|
157
|
+
let threads = new Map();
|
|
158
|
+
const views = new Map();
|
|
159
|
+
|
|
160
|
+
function applied(view, frame) {
|
|
161
|
+
const items = frame[PARTS[frame.type]];
|
|
162
|
+
const held = view[frame.type];
|
|
163
|
+
if (held !== null && frame.sequence <= held.sequence) return false;
|
|
164
|
+
if (frame.from === 0) {
|
|
165
|
+
view[frame.type] = { sequence: frame.sequence, items };
|
|
166
|
+
view.asked = false;
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (held === null || frame.sequence !== held.sequence + 1 || frame.from > held.items.length) {
|
|
170
|
+
if (!view.asked) {
|
|
171
|
+
view.asked = true;
|
|
172
|
+
options.gap({ type: "gap", conversation_id: frame.conversation_id, turn_id: frame.turn_id });
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
view[frame.type] = { sequence: frame.sequence, items: [...held.items.slice(0, frame.from), ...items] };
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
address(listed) {
|
|
182
|
+
threads = new Map(listed.map((thread) => [thread.conversation_id, thread]));
|
|
183
|
+
for (const conversation of views.keys()) {
|
|
184
|
+
if (!threads.has(conversation)) views.delete(conversation);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
receive(value) {
|
|
188
|
+
const frame = viewFrame(value);
|
|
189
|
+
if (frame === null || !threads.has(frame.conversation_id)) return null;
|
|
190
|
+
let view = views.get(frame.conversation_id);
|
|
191
|
+
if (view === undefined || view.turn !== frame.turn_id) {
|
|
192
|
+
view = { turn: frame.turn_id, draft: null, plan: null, asked: false };
|
|
193
|
+
views.set(frame.conversation_id, view);
|
|
194
|
+
}
|
|
195
|
+
return applied(view, frame) ? frame.conversation_id : null;
|
|
196
|
+
},
|
|
197
|
+
draft(conversation) {
|
|
198
|
+
const view = views.get(conversation);
|
|
199
|
+
const thread = threads.get(conversation);
|
|
200
|
+
if (view === undefined || thread === undefined) return null;
|
|
201
|
+
const blocks = [
|
|
202
|
+
...(view.plan === null ? [] : planBlocks(view.plan.items)),
|
|
203
|
+
...(view.draft === null ? [] : view.draft.items.map(telegramBlock).filter((block) => block !== null)),
|
|
204
|
+
];
|
|
205
|
+
if (blocks.length === 0) return null;
|
|
206
|
+
return {
|
|
207
|
+
chat_id: thread.chat_id,
|
|
208
|
+
topic_id: thread.topic_id,
|
|
209
|
+
draft_id: draftId(view.turn),
|
|
210
|
+
rich_message: richPieces({ blocks })[0],
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
package/src/telegram.js
CHANGED
|
@@ -24,8 +24,11 @@ import {
|
|
|
24
24
|
telegramFileUrl,
|
|
25
25
|
} from "./contract.js";
|
|
26
26
|
import { heldControl, houseRequest, maintainControl, openSession } from "./house.js";
|
|
27
|
+
import { watchKitWork } from "./kit-work-stream.js";
|
|
27
28
|
import { KitRefusal } from "./refusal.js";
|
|
28
29
|
import { writeText } from "./state.js";
|
|
30
|
+
import { richPieces, turnViews } from "./telegram-view.js";
|
|
31
|
+
import { kitStreamWriter } from "./turn-frames.js";
|
|
29
32
|
|
|
30
33
|
const TOKEN_PROMPT = "Paste the BotFather token for this Environment: ";
|
|
31
34
|
const AMBIGUOUS_DELIVERY = Object.freeze({ telegram_message_id: null, telegram_topic_id: null });
|
|
@@ -282,6 +285,15 @@ export function deliveryCall(delivery) {
|
|
|
282
285
|
return { method: "sendMessage", body: sendBody(delivery) };
|
|
283
286
|
}
|
|
284
287
|
|
|
288
|
+
function deliveryCalls(delivery) {
|
|
289
|
+
const call = deliveryCall(delivery);
|
|
290
|
+
if (call.method !== "sendRichMessage") return [call];
|
|
291
|
+
return richPieces(call.body.rich_message).map((rich_message) => ({
|
|
292
|
+
method: call.method,
|
|
293
|
+
body: { ...call.body, rich_message },
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
|
|
285
297
|
function acceptedDelivery(delivery, result) {
|
|
286
298
|
if (delivery.kind === "conversation_topic") {
|
|
287
299
|
const threadId = plain(result) ? result.message_thread_id : undefined;
|
|
@@ -353,12 +365,15 @@ function queuedDelivery(value) {
|
|
|
353
365
|
return value;
|
|
354
366
|
}
|
|
355
367
|
|
|
356
|
-
function
|
|
357
|
-
if (!
|
|
358
|
-
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
368
|
+
function threadsOf(answer) {
|
|
369
|
+
if (!Array.isArray(answer?.threads)) refuseAnswer();
|
|
370
|
+
return answer.threads.map((thread) => {
|
|
371
|
+
if (!plain(thread)) refuseAnswer();
|
|
372
|
+
for (const named of ["conversation_id", "chat_id", "topic_id"]) {
|
|
373
|
+
if (typeof thread[named] !== "string" || thread[named].length === 0) refuseAnswer();
|
|
374
|
+
}
|
|
375
|
+
return { conversation_id: thread.conversation_id, chat_id: thread.chat_id, topic_id: thread.topic_id };
|
|
376
|
+
});
|
|
362
377
|
}
|
|
363
378
|
|
|
364
379
|
function queuedUpload(value) {
|
|
@@ -374,7 +389,6 @@ function queueOf(answer) {
|
|
|
374
389
|
return answer.queue.map((item) => {
|
|
375
390
|
if (!plain(item)) refuseAnswer();
|
|
376
391
|
if (item.type === "message") return { type: "message", delivery: queuedDelivery(item.delivery) };
|
|
377
|
-
if (item.type === "draft") return { type: "draft", draft: queuedDraft(item.draft) };
|
|
378
392
|
if (item.type !== "file") refuseAnswer();
|
|
379
393
|
return { type: "file", upload: queuedUpload(item.upload) };
|
|
380
394
|
});
|
|
@@ -751,23 +765,30 @@ async function reportUpload(gateway, upload, settled) {
|
|
|
751
765
|
|
|
752
766
|
async function sendQueuedMessage(gateway, token, delivery) {
|
|
753
767
|
await houseCall(gateway, `deliveries/${delivery.id}/attempt`);
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
768
|
+
let first = null;
|
|
769
|
+
for (const call of deliveryCalls(delivery)) {
|
|
770
|
+
let answered;
|
|
771
|
+
try {
|
|
772
|
+
answered = await telegramPost(gateway, telegramApiUrl(token, call.method), call.body);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
if (cancelled(error)) throw error;
|
|
775
|
+
await reportDelivery(gateway, delivery, {
|
|
776
|
+
outcome: "delivery_unknown",
|
|
777
|
+
report: AMBIGUOUS_DELIVERY,
|
|
778
|
+
});
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
const settled = deliveryOutcome(delivery, answered);
|
|
782
|
+
const retryAfter = telegramRetryAfter(answered);
|
|
783
|
+
if (retryAfter !== null) gateway.gate.hold(retryAfter);
|
|
784
|
+
if (settled.outcome !== "delivered") {
|
|
785
|
+
await reportDelivery(gateway, delivery, settled);
|
|
786
|
+
return retryAfter !== null;
|
|
787
|
+
}
|
|
788
|
+
first ??= settled;
|
|
765
789
|
}
|
|
766
|
-
await reportDelivery(gateway, delivery,
|
|
767
|
-
|
|
768
|
-
if (retryAfter === null) return false;
|
|
769
|
-
gateway.gate.hold(retryAfter);
|
|
770
|
-
return true;
|
|
790
|
+
await reportDelivery(gateway, delivery, first);
|
|
791
|
+
return false;
|
|
771
792
|
}
|
|
772
793
|
|
|
773
794
|
async function sendQueuedUpload(gateway, token, upload) {
|
|
@@ -812,51 +833,85 @@ async function sendQueuedUpload(gateway, token, upload) {
|
|
|
812
833
|
return true;
|
|
813
834
|
}
|
|
814
835
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
836
|
+
function liveDrafts(gateway) {
|
|
837
|
+
const stream = kitStreamWriter();
|
|
838
|
+
const views = turnViews({ gap: (frame) => stream.write(frame) });
|
|
839
|
+
const dirty = new Set();
|
|
840
|
+
let pumping = null;
|
|
841
|
+
|
|
842
|
+
async function pump() {
|
|
843
|
+
while (dirty.size > 0) {
|
|
844
|
+
const [conversation] = dirty;
|
|
845
|
+
dirty.delete(conversation);
|
|
846
|
+
if (gateway.gate.open()) {
|
|
847
|
+
dirty.clear();
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const draft = views.draft(conversation);
|
|
851
|
+
if (draft === null) continue;
|
|
852
|
+
const call = draftCall(draft);
|
|
853
|
+
let answered;
|
|
854
|
+
try {
|
|
855
|
+
const token = await gatewayToken(gateway);
|
|
856
|
+
answered = await telegramPost(gateway, telegramApiUrl(token, call.method), call.body);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
if (!(error instanceof KitRefusal)) throw error;
|
|
859
|
+
if (cancelled(error)) dirty.clear();
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
const retryAfter = telegramRetryAfter(answered);
|
|
863
|
+
if (retryAfter !== null) {
|
|
864
|
+
gateway.gate.hold(retryAfter);
|
|
865
|
+
dirty.clear();
|
|
866
|
+
}
|
|
830
867
|
}
|
|
831
868
|
}
|
|
869
|
+
|
|
870
|
+
function start() {
|
|
871
|
+
if (pumping !== null || dirty.size === 0) return;
|
|
872
|
+
pumping = pump().finally(() => {
|
|
873
|
+
pumping = null;
|
|
874
|
+
start();
|
|
875
|
+
});
|
|
876
|
+
pumping.catch(() => undefined);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return {
|
|
880
|
+
heard: {
|
|
881
|
+
available: () => undefined,
|
|
882
|
+
changed: () => undefined,
|
|
883
|
+
opened: stream.opened,
|
|
884
|
+
closed: stream.closed,
|
|
885
|
+
viewed: (frame) => {
|
|
886
|
+
const conversation = views.receive(frame);
|
|
887
|
+
if (conversation === null) return;
|
|
888
|
+
dirty.add(conversation);
|
|
889
|
+
start();
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
address: (threads) => views.address(threads),
|
|
893
|
+
settled: () => pumping ?? Promise.resolve(),
|
|
894
|
+
};
|
|
832
895
|
}
|
|
833
896
|
|
|
834
|
-
|
|
835
|
-
if (
|
|
836
|
-
|
|
837
|
-
telegramPost(gateway, telegramApiUrl(token, call.method), call.body),
|
|
838
|
-
);
|
|
897
|
+
function watchGatewayStream(session, signal, heard) {
|
|
898
|
+
if (session.relay !== undefined) return session.relay(signal, heard);
|
|
899
|
+
return (session.dependencies.watchKitWork ?? watchKitWork)(session, signal, heard, session.dependencies);
|
|
839
900
|
}
|
|
840
901
|
|
|
841
902
|
async function drainOutboundQueue(gateway) {
|
|
842
903
|
if (gateway.gate.open()) return;
|
|
843
904
|
const token = await gatewayToken(gateway);
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
);
|
|
847
|
-
const drafts = [];
|
|
905
|
+
const answer = await houseCall(gateway, "queue/acquire", { max: TELEGRAM_DELIVERY_BATCH_MAX });
|
|
906
|
+
const queue = queueOf(answer);
|
|
907
|
+
gateway.live?.address(threadsOf(answer));
|
|
848
908
|
for (const item of queue) {
|
|
849
|
-
if (item.type === "draft") {
|
|
850
|
-
drafts.push(item.draft);
|
|
851
|
-
continue;
|
|
852
|
-
}
|
|
853
909
|
const gated =
|
|
854
910
|
item.type === "file"
|
|
855
911
|
? await sendQueuedUpload(gateway, token, item.upload)
|
|
856
912
|
: await sendQueuedMessage(gateway, token, item.delivery);
|
|
857
913
|
if (gated) return;
|
|
858
914
|
}
|
|
859
|
-
await streamQueuedDrafts(gateway, token, drafts);
|
|
860
915
|
}
|
|
861
916
|
|
|
862
917
|
async function onePass(gateway, activity, work) {
|
|
@@ -1052,7 +1107,23 @@ export async function serveTelegramGateway(options, dependencies = {}) {
|
|
|
1052
1107
|
const shared = AbortSignal.any([signal, stopping.signal]);
|
|
1053
1108
|
const gateway = gatewayContext(session, options, shared, summary);
|
|
1054
1109
|
summary.abandoned = abandonedOf(await houseCall(gateway, "deliveries/abandoned"));
|
|
1055
|
-
|
|
1110
|
+
gateway.live = liveDrafts(gateway);
|
|
1111
|
+
const watcher = new AbortController();
|
|
1112
|
+
const watching = watchGatewayStream(
|
|
1113
|
+
session,
|
|
1114
|
+
AbortSignal.any([shared, watcher.signal]),
|
|
1115
|
+
gateway.live.heard,
|
|
1116
|
+
).catch((error) => {
|
|
1117
|
+
if (cancelled(error)) return;
|
|
1118
|
+
summary.failures.push({ activity: "drafts", item: null, code: error?.code ?? "kit_stream_failed" });
|
|
1119
|
+
});
|
|
1120
|
+
try {
|
|
1121
|
+
await serveActivities(gateway, stopping);
|
|
1122
|
+
} finally {
|
|
1123
|
+
watcher.abort();
|
|
1124
|
+
await watching;
|
|
1125
|
+
await gateway.live.settled();
|
|
1126
|
+
}
|
|
1056
1127
|
},
|
|
1057
1128
|
options.signal,
|
|
1058
1129
|
);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { assistantBlocks } from "./blocks.js";
|
|
2
|
+
|
|
3
|
+
export function kitStreamWriter() {
|
|
4
|
+
let send = null;
|
|
5
|
+
let generation = 0;
|
|
6
|
+
return {
|
|
7
|
+
get generation() {
|
|
8
|
+
return generation;
|
|
9
|
+
},
|
|
10
|
+
opened(write) {
|
|
11
|
+
generation += 1;
|
|
12
|
+
send = write;
|
|
13
|
+
},
|
|
14
|
+
closed() {
|
|
15
|
+
send = null;
|
|
16
|
+
},
|
|
17
|
+
write(frame) {
|
|
18
|
+
if (send === null) return false;
|
|
19
|
+
send(frame);
|
|
20
|
+
return true;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function firstChange(written, items) {
|
|
26
|
+
const shared = Math.min(written.length, items.length);
|
|
27
|
+
for (let index = 0; index < shared; index += 1) {
|
|
28
|
+
if (JSON.stringify(written[index]) !== JSON.stringify(items[index])) return index;
|
|
29
|
+
}
|
|
30
|
+
return shared;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function turnFrames(stream) {
|
|
34
|
+
let identity = null;
|
|
35
|
+
const parts = {
|
|
36
|
+
draft: { key: "blocks", latest: null, written: null, generation: null },
|
|
37
|
+
plan: { key: "steps", latest: null, written: null, generation: null },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function write(type) {
|
|
41
|
+
const part = parts[type];
|
|
42
|
+
if (identity === null || part.latest === null) return;
|
|
43
|
+
const { sequence, items } = part.latest;
|
|
44
|
+
const whole = part.written === null || part.generation !== stream.generation;
|
|
45
|
+
const from = whole ? 0 : firstChange(part.written, items);
|
|
46
|
+
const generation = stream.generation;
|
|
47
|
+
const sent = stream.write({ type, ...identity, sequence, from, [part.key]: items.slice(from) });
|
|
48
|
+
part.written = sent ? items : null;
|
|
49
|
+
part.generation = generation;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
identify(named) {
|
|
54
|
+
identity = named;
|
|
55
|
+
write("draft");
|
|
56
|
+
write("plan");
|
|
57
|
+
},
|
|
58
|
+
draft(view) {
|
|
59
|
+
const blocks = assistantBlocks(view.text);
|
|
60
|
+
if (blocks === null) return;
|
|
61
|
+
parts.draft.latest = { sequence: view.sequence, items: blocks };
|
|
62
|
+
write("draft");
|
|
63
|
+
},
|
|
64
|
+
plan(snapshot) {
|
|
65
|
+
parts.plan.latest = { sequence: snapshot.sequence, items: snapshot.steps };
|
|
66
|
+
write("plan");
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|