@agentshouse/kit 0.1.0-alpha.3 → 0.1.0-alpha.30
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 +697 -260
- package/bin/connect-linux.sh +200 -25
- package/bin/kit.js +179 -157
- package/bootstrap.json +17 -0
- package/package.json +7 -2
- package/src/adapter-mcp.js +76 -12
- package/src/agents.js +63 -8
- package/src/binding.js +52 -0
- package/src/blocks.js +2 -26
- package/src/bridge.js +28 -14
- package/src/bytes.js +19 -8
- package/src/configuration.js +13 -209
- package/src/connector-login.js +3 -84
- package/src/contract.js +36 -11
- package/src/delivery.js +4 -7
- package/src/directory.js +6 -26
- package/src/draft.js +4 -82
- package/src/git-main.js +138 -0
- package/src/git.js +62 -0
- package/src/google/calendar.js +39 -45
- package/src/google/drive.js +61 -186
- package/src/google/entries.js +346 -0
- package/src/google/gateway.js +3 -6
- package/src/google/gmail.js +33 -23
- package/src/google/oauth.js +106 -66
- package/src/google/source.js +21 -23
- package/src/host.js +6 -2
- package/src/house-main.js +65 -8
- package/src/house.js +446 -127
- package/src/how-we-work.md +25 -0
- package/src/inbound.js +2 -18
- package/src/kit-work-stream.js +118 -0
- package/src/launcher.js +46 -10
- package/src/local-files.js +3 -11
- package/src/login.js +23 -11
- package/src/mcp.js +18 -0
- package/src/mount.js +13 -21
- package/src/original.js +4 -4
- package/src/pacing.js +41 -0
- package/src/plan.js +37 -45
- package/src/repository.js +3 -61
- package/src/session-bin/git +2 -0
- package/src/setup.js +14 -15
- package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
- package/src/skill-set/ask-matt/SKILL.md +91 -0
- package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
- package/src/skill-set/code-review/LICENSE +21 -0
- package/src/skill-set/code-review/SKILL.md +89 -0
- package/src/skill-set/code-review/agents/openai.yaml +3 -0
- package/src/skill-set/codebase-design/DEEPENING.md +37 -0
- package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/src/skill-set/codebase-design/LICENSE +21 -0
- package/src/skill-set/codebase-design/SKILL.md +116 -0
- package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
- package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
- package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
- package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/src/skill-set/domain-modeling/LICENSE +21 -0
- package/src/skill-set/domain-modeling/SKILL.md +76 -0
- package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
- package/src/skill-set/grill-me/LICENSE +21 -0
- package/src/skill-set/grill-me/SKILL.md +8 -0
- package/src/skill-set/grill-me/agents/openai.yaml +4 -0
- package/src/skill-set/grill-with-docs/LICENSE +21 -0
- package/src/skill-set/grill-with-docs/SKILL.md +8 -0
- package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
- package/src/skill-set/grilling/LICENSE +21 -0
- package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
- package/src/skill-set/handoff/LICENSE +21 -0
- package/src/skill-set/handoff/SKILL.md +17 -0
- package/src/skill-set/handoff/agents/openai.yaml +4 -0
- package/src/skill-set/implement/LICENSE +21 -0
- package/src/skill-set/implement/SKILL.md +16 -0
- package/src/skill-set/implement/agents/openai.yaml +4 -0
- package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
- package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
- package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
- package/src/skill-set/prototype/LICENSE +21 -0
- package/src/skill-set/prototype/LOGIC.md +67 -0
- package/src/skill-set/prototype/SKILL.md +28 -0
- package/src/skill-set/prototype/UI.md +112 -0
- package/src/skill-set/prototype/agents/openai.yaml +3 -0
- package/src/skill-set/research/LICENSE +21 -0
- package/src/skill-set/research/SKILL.md +14 -0
- package/src/skill-set/research/agents/openai.yaml +3 -0
- package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
- package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
- package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/LICENSE +21 -0
- package/src/skill-set/tdd/SKILL.md +40 -0
- package/src/skill-set/tdd/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/mocking.md +59 -0
- package/src/skill-set/tdd/tests.md +77 -0
- package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
- package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/src/skill-set/teach/LICENSE +21 -0
- package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
- package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
- package/src/skill-set/teach/SKILL.md +141 -0
- package/src/skill-set/teach/agents/openai.yaml +4 -0
- package/src/skill-set/to-questionnaire/LICENSE +21 -0
- package/src/skill-set/to-questionnaire/SKILL.md +55 -0
- package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
- package/src/skill-set/to-spec/LICENSE +21 -0
- package/src/skill-set/to-spec/SKILL.md +76 -0
- package/src/skill-set/to-spec/agents/openai.yaml +4 -0
- package/src/skill-set/to-tickets/LICENSE +21 -0
- package/src/skill-set/to-tickets/SKILL.md +106 -0
- package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
- package/src/skill-set/wait-what/LICENSE +21 -0
- package/src/skill-set/wait-what/SKILL.md +8 -0
- package/src/skill-set/wait-what/agents/openai.yaml +4 -0
- package/src/skill-set/wayfinder/LICENSE +21 -0
- package/src/skill-set/wayfinder/SKILL.md +129 -0
- package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
- package/src/skill-set/writing-for-agents/LICENSE +21 -0
- package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/src/skill-set/writing-for-agents/SKILL.md +83 -0
- package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
- package/src/skill-set.js +85 -0
- package/src/source.js +93 -54
- package/src/state.js +19 -1
- package/src/sync-library.js +7 -11
- package/src/sync-state.js +16 -3
- package/src/sync.js +36 -21
- package/src/tasks.js +75 -0
- package/src/telegram-view.js +214 -0
- package/src/telegram.js +161 -66
- package/src/turn-frames.js +69 -0
- package/src/worker.js +674 -184
- package/src/working-copy.js +1221 -0
- package/src/workspace.js +40 -32
- package/src/google/declared.js +0 -146
- package/src/google/id.js +0 -1
- package/src/instance/google.js +0 -1
- package/src/starter-set.js +0 -17
- /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
- /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
|
@@ -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
|
});
|
|
@@ -722,7 +736,7 @@ async function relayInboundFiles(gateway) {
|
|
|
722
736
|
gateway.summary.relayed.push({ attachment: file.id, bytes: bytes.length });
|
|
723
737
|
} catch (error) {
|
|
724
738
|
if (cancelled(error) || !(error instanceof KitRefusal)) throw error;
|
|
725
|
-
gateway
|
|
739
|
+
refusedPass(gateway, { activity: "inbound", item: file.id, code: error.code });
|
|
726
740
|
}
|
|
727
741
|
}
|
|
728
742
|
}
|
|
@@ -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,71 +833,111 @@ 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
|
-
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function refusedPass(gateway, failure) {
|
|
918
|
+
gateway.summary.failures.push(failure);
|
|
919
|
+
gateway.refused(failure);
|
|
860
920
|
}
|
|
861
921
|
|
|
862
922
|
async function onePass(gateway, activity, work) {
|
|
863
923
|
try {
|
|
864
924
|
await work(gateway);
|
|
865
925
|
} catch (error) {
|
|
866
|
-
if (cancelled(error) || !(error instanceof KitRefusal)) throw error;
|
|
867
|
-
gateway
|
|
926
|
+
if (cancelled(error) || !(error instanceof KitRefusal) || error.code === "kit_update_draining") throw error;
|
|
927
|
+
refusedPass(gateway, { activity, item: null, code: error.code });
|
|
868
928
|
}
|
|
869
929
|
}
|
|
870
930
|
|
|
871
931
|
async function paceActivity(gateway, stopping, activity, work) {
|
|
872
932
|
try {
|
|
873
|
-
for (let index = 0; index < gateway.passes; index += 1) {
|
|
933
|
+
for (let index = 0; index < gateway.passes && !gateway.draining; index += 1) {
|
|
874
934
|
if (gateway.signal?.aborted === true) throw new KitRefusal("kit_command_cancelled");
|
|
875
935
|
await onePass(gateway, activity, work);
|
|
876
936
|
if (index + 1 < gateway.passes) await gateway.wait(TELEGRAM_GATEWAY_POLL_MS, gateway.signal);
|
|
877
937
|
}
|
|
878
938
|
} catch (error) {
|
|
879
|
-
|
|
939
|
+
if (error.code === "kit_update_draining") gateway.draining = true;
|
|
940
|
+
else stopping.abort();
|
|
880
941
|
throw error;
|
|
881
942
|
}
|
|
882
943
|
}
|
|
@@ -955,22 +1016,32 @@ async function proveOwner(gateway, token, telegramBotUserId) {
|
|
|
955
1016
|
throw new KitRefusal("telegram_owner_not_proven");
|
|
956
1017
|
}
|
|
957
1018
|
|
|
958
|
-
function gatewayContext(session, options,
|
|
1019
|
+
function gatewayContext(session, options, signal, summary = emptySummary()) {
|
|
959
1020
|
return {
|
|
960
1021
|
session,
|
|
961
1022
|
directory: options.directory,
|
|
962
|
-
dependencies,
|
|
1023
|
+
dependencies: session.dependencies,
|
|
963
1024
|
signal,
|
|
964
1025
|
gate: rateGate(() => session.now().getTime()),
|
|
965
|
-
wait: dependencies.wait ?? pause,
|
|
1026
|
+
wait: session.dependencies.wait ?? pause,
|
|
966
1027
|
passes: options.passes ?? Number.POSITIVE_INFINITY,
|
|
1028
|
+
draining: false,
|
|
967
1029
|
summary,
|
|
1030
|
+
refused: options.refused,
|
|
968
1031
|
};
|
|
969
1032
|
}
|
|
970
1033
|
|
|
971
1034
|
export async function connectPersonalBot(options, dependencies = {}) {
|
|
972
1035
|
const session = await openSession(options, dependencies);
|
|
973
|
-
|
|
1036
|
+
return maintainControl(
|
|
1037
|
+
session,
|
|
1038
|
+
(signal) => connectBot(session, options, dependencies, signal),
|
|
1039
|
+
options.signal,
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async function connectBot(session, options, dependencies, signal) {
|
|
1044
|
+
const gateway = gatewayContext(session, options, signal);
|
|
974
1045
|
const ask = dependencies.prompt ?? (() => askForToken(dependencies));
|
|
975
1046
|
const token = (await ask()).trim();
|
|
976
1047
|
if (token.length === 0) throw new KitRefusal("telegram_bot_token_required");
|
|
@@ -989,7 +1060,7 @@ export async function connectPersonalBot(options, dependencies = {}) {
|
|
|
989
1060
|
owner_telegram_user_id: await proveOwner(gateway, token, identity.telegramBotUserId),
|
|
990
1061
|
has_topics_enabled: true,
|
|
991
1062
|
};
|
|
992
|
-
await writeOwned(tokenPath(options.directory), `${token}\n`,
|
|
1063
|
+
await writeOwned(tokenPath(options.directory), `${token}\n`, signal);
|
|
993
1064
|
try {
|
|
994
1065
|
await houseCall(gateway, "personal-bot", setup);
|
|
995
1066
|
} catch (error) {
|
|
@@ -1001,7 +1072,15 @@ export async function connectPersonalBot(options, dependencies = {}) {
|
|
|
1001
1072
|
|
|
1002
1073
|
export async function disconnectPersonalBot(options, dependencies = {}) {
|
|
1003
1074
|
const session = await openSession(options, dependencies);
|
|
1004
|
-
|
|
1075
|
+
return maintainControl(
|
|
1076
|
+
session,
|
|
1077
|
+
(signal) => disconnectBot(session, options, signal),
|
|
1078
|
+
options.signal,
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
async function disconnectBot(session, options, signal) {
|
|
1083
|
+
const gateway = gatewayContext(session, options, signal);
|
|
1005
1084
|
const directory = options.directory;
|
|
1006
1085
|
const marked = await disconnecting(directory);
|
|
1007
1086
|
let token = null;
|
|
@@ -1010,7 +1089,7 @@ export async function disconnectPersonalBot(options, dependencies = {}) {
|
|
|
1010
1089
|
} catch (error) {
|
|
1011
1090
|
if (!marked) throw error;
|
|
1012
1091
|
}
|
|
1013
|
-
if (!marked) await writeOwned(disconnectPath(directory), "{}\n",
|
|
1092
|
+
if (!marked) await writeOwned(disconnectPath(directory), "{}\n", signal);
|
|
1014
1093
|
if (token !== null) {
|
|
1015
1094
|
await telegramCall(gateway, token, "deleteWebhook", { drop_pending_updates: false });
|
|
1016
1095
|
await Promise.all([
|
|
@@ -1032,9 +1111,25 @@ export async function serveTelegramGateway(options, dependencies = {}) {
|
|
|
1032
1111
|
async (signal) => {
|
|
1033
1112
|
const stopping = new AbortController();
|
|
1034
1113
|
const shared = AbortSignal.any([signal, stopping.signal]);
|
|
1035
|
-
const gateway = gatewayContext(session, options,
|
|
1114
|
+
const gateway = gatewayContext(session, options, shared, summary);
|
|
1036
1115
|
summary.abandoned = abandonedOf(await houseCall(gateway, "deliveries/abandoned"));
|
|
1037
|
-
|
|
1116
|
+
gateway.live = liveDrafts(gateway);
|
|
1117
|
+
const watcher = new AbortController();
|
|
1118
|
+
const watching = watchGatewayStream(
|
|
1119
|
+
session,
|
|
1120
|
+
AbortSignal.any([shared, watcher.signal]),
|
|
1121
|
+
gateway.live.heard,
|
|
1122
|
+
).catch((error) => {
|
|
1123
|
+
if (cancelled(error)) return;
|
|
1124
|
+
refusedPass(gateway, { activity: "drafts", item: null, code: error?.code ?? "kit_stream_failed" });
|
|
1125
|
+
});
|
|
1126
|
+
try {
|
|
1127
|
+
await serveActivities(gateway, stopping);
|
|
1128
|
+
} finally {
|
|
1129
|
+
watcher.abort();
|
|
1130
|
+
await watching;
|
|
1131
|
+
await gateway.live.settled();
|
|
1132
|
+
}
|
|
1038
1133
|
},
|
|
1039
1134
|
options.signal,
|
|
1040
1135
|
);
|
|
@@ -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
|
+
}
|