@agentshouse/kit 0.0.1 → 0.1.0-alpha.2
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/LICENSE +21 -0
- package/README.md +1696 -2
- package/bin/connect-linux.sh +127 -0
- package/bin/kit.js +1023 -0
- package/package.json +33 -3
- package/src/acp.js +99 -0
- package/src/adapter-mcp.js +44 -0
- package/src/adapter.js +252 -0
- package/src/agent-logins.js +65 -0
- package/src/agents.js +449 -0
- package/src/apply.js +29 -0
- package/src/attach.js +52 -0
- package/src/binding.js +90 -0
- package/src/blocks.js +156 -0
- package/src/bridge.js +449 -0
- package/src/bytes.js +89 -0
- package/src/channel.js +37 -0
- package/src/command.js +109 -0
- package/src/configuration.js +465 -0
- package/src/connector-login.js +91 -0
- package/src/connector.js +107 -0
- package/src/continuation.js +61 -0
- package/src/contract.js +363 -0
- package/src/conversation-originals.js +143 -0
- package/src/custom/authorization.js +63 -0
- package/src/declaration.js +35 -0
- package/src/delivery.js +191 -0
- package/src/directory.js +102 -0
- package/src/draft.js +89 -0
- package/src/google/calendar.js +177 -0
- package/src/google/declared.js +146 -0
- package/src/google/drive.js +435 -0
- package/src/google/gateway.js +166 -0
- package/src/google/gmail.js +249 -0
- package/src/google/id.js +1 -0
- package/src/google/oauth.js +273 -0
- package/src/google/source.js +25 -0
- package/src/hook.js +376 -0
- package/src/host.js +121 -0
- package/src/house-main.js +79 -0
- package/src/house.js +1085 -0
- package/src/inbound.js +175 -0
- package/src/instance/google.js +1 -0
- package/src/launcher-main.js +19 -0
- package/src/launcher.js +276 -0
- package/src/local-files.js +113 -0
- package/src/login.js +274 -0
- package/src/mcp.js +71 -0
- package/src/migrator.js +248 -0
- package/src/mount.js +582 -0
- package/src/operation.js +20 -0
- package/src/original.js +631 -0
- package/src/peer.js +124 -0
- package/src/plan.js +57 -0
- package/src/question.js +316 -0
- package/src/refusal.js +79 -0
- package/src/repository.js +521 -0
- package/src/session-bin/house +2 -0
- package/src/settlement.js +57 -0
- package/src/setup.js +185 -0
- package/src/source-originals.js +116 -0
- package/src/source.js +286 -0
- package/src/starter-set/grilling/LICENSE +21 -0
- package/src/starter-set/grilling/SKILL.md +30 -0
- package/src/starter-set/grilling/agents/openai.yaml +3 -0
- package/src/starter-set.js +17 -0
- package/src/state.js +157 -0
- package/src/sync-library.js +293 -0
- package/src/sync-state.js +116 -0
- package/src/sync.js +477 -0
- package/src/telegram.js +1045 -0
- package/src/terminal.js +57 -0
- package/src/tmux.js +119 -0
- package/src/worker.js +756 -0
- package/src/workspace.js +500 -0
package/src/worker.js
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { agentReadiness, installedAgentCommand, updateDeclaredAgents } from "./agents.js";
|
|
6
|
+
import { readBindings, removeBinding, writeBinding } from "./binding.js";
|
|
7
|
+
import { readContinuation, readContinuationRuns, writeContinuation, removeContinuation } from "./continuation.js";
|
|
8
|
+
import { openBridge } from "./bridge.js";
|
|
9
|
+
import { assistantBlocks } from "./blocks.js";
|
|
10
|
+
import { retryConversationFiles } from "./conversation-originals.js";
|
|
11
|
+
import { openAssistantDrafts } from "./draft.js";
|
|
12
|
+
import { localReply, localRequest, reconcileInbound } from "./inbound.js";
|
|
13
|
+
import { localFiles } from "./local-files.js";
|
|
14
|
+
import { inheritedEnvironment } from "./command.js";
|
|
15
|
+
import { readConfiguration } from "./configuration.js";
|
|
16
|
+
import { launchBase, readyLaunchDirectory } from "./directory.js";
|
|
17
|
+
import {
|
|
18
|
+
BRIDGE_NONCE_ENVIRONMENT,
|
|
19
|
+
BRIDGE_SOCKET_ENVIRONMENT,
|
|
20
|
+
PROVIDER_CONTINUATION_UNAVAILABLE,
|
|
21
|
+
WORKER_COMMAND_BATCH_MAX,
|
|
22
|
+
WORKER_COMMAND_POLL_MS,
|
|
23
|
+
WORKER_EXIT_GRACE_MS,
|
|
24
|
+
WORKER_EXIT_POLL_MS,
|
|
25
|
+
WORKER_HEARTBEAT_MS,
|
|
26
|
+
WORKER_INHERITED_ENVIRONMENT,
|
|
27
|
+
WORKER_SUPERVISION_POLL_MS,
|
|
28
|
+
} from "./contract.js";
|
|
29
|
+
import { EpochReportRequired, houseFailed, KitRefusal } from "./refusal.js";
|
|
30
|
+
import {
|
|
31
|
+
acceptAssistantText,
|
|
32
|
+
acquireCommands,
|
|
33
|
+
acquireSetup,
|
|
34
|
+
reportAgentCapabilities,
|
|
35
|
+
acquireOneRun,
|
|
36
|
+
acquireOneResumption,
|
|
37
|
+
admitInteraction,
|
|
38
|
+
admitPlanSnapshot,
|
|
39
|
+
awaitSecretInput,
|
|
40
|
+
rejectResumption,
|
|
41
|
+
callHouseMcp,
|
|
42
|
+
commitRun,
|
|
43
|
+
desiredAgents,
|
|
44
|
+
maintainControl,
|
|
45
|
+
mutatingCall,
|
|
46
|
+
observeSessionStart,
|
|
47
|
+
observeSessionStop,
|
|
48
|
+
openSession,
|
|
49
|
+
replaceSessionAuthority,
|
|
50
|
+
reportContinuation,
|
|
51
|
+
reportInterruptOutcome,
|
|
52
|
+
reportInteractionOutcome,
|
|
53
|
+
toolMutations,
|
|
54
|
+
reportRunOutcome,
|
|
55
|
+
reportSessionHeartbeat,
|
|
56
|
+
reportSessions,
|
|
57
|
+
sessionAuthority,
|
|
58
|
+
streamAssistantDraft,
|
|
59
|
+
} from "./house.js";
|
|
60
|
+
import { operationId } from "./operation.js";
|
|
61
|
+
import { runSetup } from "./setup.js";
|
|
62
|
+
import { readSettlements, removeSettlement, writeSettlement } from "./settlement.js";
|
|
63
|
+
import { failedReport, terminalReport } from "./terminal.js";
|
|
64
|
+
import {
|
|
65
|
+
createSession,
|
|
66
|
+
destroySession,
|
|
67
|
+
readyTmux,
|
|
68
|
+
sessionPresent,
|
|
69
|
+
workerRuntime,
|
|
70
|
+
workerSessionName,
|
|
71
|
+
} from "./tmux.js";
|
|
72
|
+
|
|
73
|
+
const LAUNCHER_MAIN = fileURLToPath(new URL("launcher-main.js", import.meta.url));
|
|
74
|
+
|
|
75
|
+
async function dispatchSeal(context, bound) {
|
|
76
|
+
try {
|
|
77
|
+
const accepted = await acceptAssistantText(context.session, bound.session, bound.sealed);
|
|
78
|
+
await resolveSeal(context, bound);
|
|
79
|
+
return { accepted, failure: null, unresolved: false, refused: null };
|
|
80
|
+
} catch (refusal) {
|
|
81
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
82
|
+
const named = { code: refusal.code, detail: refusal.message };
|
|
83
|
+
if (undecided(refusal)) return { accepted: null, failure: null, unresolved: true, refused: named };
|
|
84
|
+
await resolveSeal(context, bound);
|
|
85
|
+
return { accepted: null, failure: named, unresolved: false, refused: null };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function resolveSeal(context, bound) {
|
|
90
|
+
bound.sealed = null;
|
|
91
|
+
await writeBinding(context.directory, bound);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function sealAssistantText(context, bound, said) {
|
|
95
|
+
const blocks = assistantBlocks(said.join("\n\n"));
|
|
96
|
+
if (blocks === null) return { accepted: null, failure: null, unresolved: false, refused: null };
|
|
97
|
+
bound.sealed = { operation: operationId(), blocks };
|
|
98
|
+
await writeBinding(context.directory, bound);
|
|
99
|
+
return dispatchSeal(context, bound);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function recoverAssistantSeal(context, binding) {
|
|
103
|
+
if (binding.sealed === null) return { unresolved: false, refused: null };
|
|
104
|
+
const sealed = await dispatchSeal(context, binding);
|
|
105
|
+
return { unresolved: sealed.unresolved, refused: sealed.refused };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function unresolvedRun(bound, refused = null) {
|
|
109
|
+
return { run: bound.run, session: bound.session, refused };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function undecided(refusal) {
|
|
113
|
+
return (
|
|
114
|
+
houseFailed(refusal) ||
|
|
115
|
+
(refusal instanceof KitRefusal &&
|
|
116
|
+
["kit_command_cancelled", "kit_session_token_rejected"].includes(refusal.code))
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function pendingCommands(context, kinds = ["stop"]) {
|
|
121
|
+
try {
|
|
122
|
+
return await acquireCommands(context.session, WORKER_COMMAND_BATCH_MAX, kinds);
|
|
123
|
+
} catch (refusal) {
|
|
124
|
+
if (houseFailed(refusal)) return { stops: [], answers: [], interrupts: [] };
|
|
125
|
+
throw refusal;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function settleInteraction(context, interaction, outcome) {
|
|
130
|
+
try {
|
|
131
|
+
await reportInteractionOutcome(context.session, interaction, outcome);
|
|
132
|
+
} catch (refusal) {
|
|
133
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function settleInterrupt(context, interrupt, outcome) {
|
|
138
|
+
try {
|
|
139
|
+
await reportInterruptOutcome(context.session, interrupt, outcome);
|
|
140
|
+
} catch (refusal) {
|
|
141
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function reportHeartbeat(context, bound) {
|
|
146
|
+
try {
|
|
147
|
+
await reportSessionHeartbeat(context.session, bound.session);
|
|
148
|
+
} catch (refusal) {
|
|
149
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function retryFiles(context) {
|
|
154
|
+
if (context.retrying !== undefined) return;
|
|
155
|
+
context.retrying = retryConversationFiles(context)
|
|
156
|
+
.then((originals) => {
|
|
157
|
+
context.files.originals.push(...originals);
|
|
158
|
+
})
|
|
159
|
+
.catch((error) => {
|
|
160
|
+
const code = error instanceof KitRefusal ? error.code : "kit_conversation_files_failed";
|
|
161
|
+
if (code !== "kit_command_cancelled" && context.files.failures.at(-1) !== code) {
|
|
162
|
+
context.files.failures.push(code);
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
.finally(() => {
|
|
166
|
+
context.retrying = undefined;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function settleFiles(context) {
|
|
171
|
+
await context.retrying;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function supervise(context, bound, bridge) {
|
|
175
|
+
const terminal = bridge.departed;
|
|
176
|
+
let live = true;
|
|
177
|
+
terminal.then(() => {
|
|
178
|
+
live = false;
|
|
179
|
+
});
|
|
180
|
+
let commanded = Date.now() + WORKER_COMMAND_POLL_MS;
|
|
181
|
+
let pulsed = Date.now() + WORKER_HEARTBEAT_MS;
|
|
182
|
+
while (live) {
|
|
183
|
+
await Promise.race([terminal, delay(WORKER_SUPERVISION_POLL_MS)]);
|
|
184
|
+
if (!live) return null;
|
|
185
|
+
if (Date.now() >= commanded) {
|
|
186
|
+
commanded = Date.now() + WORKER_COMMAND_POLL_MS;
|
|
187
|
+
const polled = await pendingCommands(context, ["stop", "interrupt", "answer"]);
|
|
188
|
+
for (const answer of polled.answers) {
|
|
189
|
+
if (answer.session === bound.session) bridge.answered(answer);
|
|
190
|
+
}
|
|
191
|
+
for (const interrupt of polled.interrupts) {
|
|
192
|
+
if (interrupt.session !== bound.session || interrupt.run !== bound.run) continue;
|
|
193
|
+
if (!bridge.interrupt(interrupt)) {
|
|
194
|
+
await settleInterrupt(context, interrupt.interrupt, "nothing_to_interrupt");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const stop = polled.stops.find((command) => command.run === bound.run);
|
|
198
|
+
if (stop !== undefined) return stop;
|
|
199
|
+
}
|
|
200
|
+
if (Date.now() >= pulsed) {
|
|
201
|
+
pulsed = Date.now() + WORKER_HEARTBEAT_MS;
|
|
202
|
+
retryFiles(context);
|
|
203
|
+
await reportHeartbeat(context, bound);
|
|
204
|
+
}
|
|
205
|
+
if (!(await sessionPresent(context.runtime, bound.tmux))) return null;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function grantExitGrace(runtime, handle) {
|
|
211
|
+
const deadline = Date.now() + WORKER_EXIT_GRACE_MS;
|
|
212
|
+
for (;;) {
|
|
213
|
+
if (!(await sessionPresent(runtime, handle))) return false;
|
|
214
|
+
if (Date.now() >= deadline) return destroySession(runtime, handle);
|
|
215
|
+
await delay(WORKER_EXIT_POLL_MS);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function completeOwnerStop(context, bound, requestedAt) {
|
|
220
|
+
await writeBinding(context.directory, { ...bound, ownerStop: requestedAt });
|
|
221
|
+
const destroyed = await destroySession(context.runtime, bound.tmux);
|
|
222
|
+
let failure = null;
|
|
223
|
+
try {
|
|
224
|
+
await observeSessionStop(context.session, bound.session);
|
|
225
|
+
} catch (refusal) {
|
|
226
|
+
if (houseFailed(refusal)) return { destroyed, confirmed: false, failure: null };
|
|
227
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
228
|
+
failure = { code: refusal.code, detail: refusal.message };
|
|
229
|
+
}
|
|
230
|
+
await removeBinding(context.directory, bound.tmux);
|
|
231
|
+
await removeContinuation(context.directory, bound.run);
|
|
232
|
+
return { destroyed, confirmed: true, failure };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function settleReported(context, bound) {
|
|
236
|
+
try {
|
|
237
|
+
await reportRunOutcome(context.session, bound.run, bound.reported);
|
|
238
|
+
return { settled: true, failure: null };
|
|
239
|
+
} catch (refusal) {
|
|
240
|
+
if (undecided(refusal)) return { settled: false, failure: null };
|
|
241
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
242
|
+
if (refusal.code === "worker_run_settled") return { settled: true, failure: null };
|
|
243
|
+
return { settled: false, failure: { code: refusal.code, detail: refusal.message } };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function reportTerminalState(context, bound, report) {
|
|
248
|
+
const settling = { ...bound, reported: { operation: operationId(), ...report } };
|
|
249
|
+
await writeBinding(context.directory, settling);
|
|
250
|
+
const settled = await settleReported(context, settling);
|
|
251
|
+
if (settled.settled) await removeBinding(context.directory, settling.tmux);
|
|
252
|
+
return { ...settled, reported: settling.reported };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function retainedContinuation(directory, run) {
|
|
256
|
+
try {
|
|
257
|
+
return await readContinuation(directory, run);
|
|
258
|
+
} catch (refusal) {
|
|
259
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function endOnEpochReport(ended, act) {
|
|
265
|
+
return async (...args) => {
|
|
266
|
+
try {
|
|
267
|
+
return await act(...args);
|
|
268
|
+
} catch (refusal) {
|
|
269
|
+
if (refusal instanceof EpochReportRequired) ended.abort(refusal);
|
|
270
|
+
throw refusal;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function openRunBridge(context, bound, authority, start) {
|
|
276
|
+
const ended = new AbortController();
|
|
277
|
+
const handed = (act) => endOnEpochReport(ended, act);
|
|
278
|
+
const drafts = openAssistantDrafts({
|
|
279
|
+
send: handed((blocks) => streamAssistantDraft(context.session, bound.session, blocks)),
|
|
280
|
+
});
|
|
281
|
+
const bridge = await openBridge({
|
|
282
|
+
socket: bound.socket,
|
|
283
|
+
nonce: bound.nonce,
|
|
284
|
+
signal: AbortSignal.any([context.signal, ended.signal]),
|
|
285
|
+
mutates: mutatingCall(await toolMutations(context.session, context.signal)),
|
|
286
|
+
alive: () => sessionPresent(context.runtime, bound.tmux),
|
|
287
|
+
draft: (text) => drafts.update(text),
|
|
288
|
+
start,
|
|
289
|
+
question: handed((batch) => admitInteraction(context.session, bound.session, batch)),
|
|
290
|
+
plan: handed((snapshot) => admitPlanSnapshot(context.session, bound.session, snapshot)),
|
|
291
|
+
interrupt: handed((interrupt, outcome) =>
|
|
292
|
+
reportInterruptOutcome(context.session, interrupt, outcome),
|
|
293
|
+
),
|
|
294
|
+
secret: (interaction, ceremony) => awaitSecretInput(context.session, interaction, ceremony),
|
|
295
|
+
settle: handed((interaction, outcome) => settleInteraction(context, interaction, outcome)),
|
|
296
|
+
forward: handed((frame, signal) =>
|
|
297
|
+
localFiles(async (sent) =>
|
|
298
|
+
localReply(context.directory, bound.run, sent, await callHouseMcp(
|
|
299
|
+
context.session,
|
|
300
|
+
await sessionAuthority(context.session, bound.session, authority, signal),
|
|
301
|
+
sent,
|
|
302
|
+
signal,
|
|
303
|
+
)), context.session.dependencies, signal)(frame)),
|
|
304
|
+
});
|
|
305
|
+
return { bridge, drafts, ended };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function superviseBound(context, bound, opened, start) {
|
|
309
|
+
const { bridge, drafts, ended } = opened;
|
|
310
|
+
const spawn = { committed: false };
|
|
311
|
+
let unsupervised = null;
|
|
312
|
+
let destroyed = false;
|
|
313
|
+
let owner = null;
|
|
314
|
+
try {
|
|
315
|
+
await start(spawn);
|
|
316
|
+
owner = await supervise(context, bound, bridge);
|
|
317
|
+
if (owner !== null) await bridge.close();
|
|
318
|
+
} catch (refusal) {
|
|
319
|
+
if (!spawn.committed || !(refusal instanceof KitRefusal)) throw refusal;
|
|
320
|
+
unsupervised = { code: refusal.code, detail: refusal.message };
|
|
321
|
+
} finally {
|
|
322
|
+
try {
|
|
323
|
+
try {
|
|
324
|
+
await settleFiles(context);
|
|
325
|
+
for (const interaction of bridge.unsettledInteractions()) {
|
|
326
|
+
await settleInteraction(context, interaction, "expired");
|
|
327
|
+
}
|
|
328
|
+
} finally {
|
|
329
|
+
bridge.stopRecovery();
|
|
330
|
+
if (owner === null) {
|
|
331
|
+
try {
|
|
332
|
+
destroyed = await grantExitGrace(context.runtime, bound.tmux);
|
|
333
|
+
} finally {
|
|
334
|
+
await bridge.close();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} finally {
|
|
339
|
+
drafts.close();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
ended.signal.throwIfAborted();
|
|
343
|
+
const outcome = {
|
|
344
|
+
run: bound.run,
|
|
345
|
+
session: bound.session,
|
|
346
|
+
tmux: bound.tmux,
|
|
347
|
+
assistant: bridge.assistant(),
|
|
348
|
+
failure: bridge.failure() ?? unsupervised,
|
|
349
|
+
reported: null,
|
|
350
|
+
refused: null,
|
|
351
|
+
settled: true,
|
|
352
|
+
destroyed,
|
|
353
|
+
stopped: null,
|
|
354
|
+
};
|
|
355
|
+
if (owner !== null) {
|
|
356
|
+
const finished = await completeOwnerStop(context, bound, owner.requestedAt);
|
|
357
|
+
return {
|
|
358
|
+
...outcome,
|
|
359
|
+
destroyed: finished.destroyed,
|
|
360
|
+
failure: finished.failure,
|
|
361
|
+
stopped: owner.requestedAt,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const stopped = bridge.stop();
|
|
365
|
+
const held = await retainedContinuation(context.directory, bound.run);
|
|
366
|
+
const capacityExhausted = stopped?.stopOutcome === "exhausted";
|
|
367
|
+
const providerRefused = stopped?.stopOutcome === "refused";
|
|
368
|
+
const continuable =
|
|
369
|
+
!capacityExhausted &&
|
|
370
|
+
!providerRefused &&
|
|
371
|
+
held !== null &&
|
|
372
|
+
stopped?.providerContinuation === true &&
|
|
373
|
+
stopped.providerSessionId !== null;
|
|
374
|
+
const turnObserved = stopped?.stopOutcome === "completed";
|
|
375
|
+
const sealed = turnObserved
|
|
376
|
+
? await sealAssistantText(context, bound, outcome.assistant)
|
|
377
|
+
: { accepted: null, failure: null, unresolved: false, refused: null };
|
|
378
|
+
if (sealed.unresolved) return { ...outcome, refused: sealed.refused, settled: false };
|
|
379
|
+
if (sealed.failure !== null) outcome.failure ??= sealed.failure;
|
|
380
|
+
const execution = outcome.failure;
|
|
381
|
+
try {
|
|
382
|
+
await observeSessionStop(context.session, bound.session, sealed.accepted !== null);
|
|
383
|
+
} catch (refusal) {
|
|
384
|
+
if (!(refusal instanceof KitRefusal) || refusal.code !== "conversation_final_text_pending") throw refusal;
|
|
385
|
+
outcome.failure ??= { code: refusal.code, detail: refusal.message };
|
|
386
|
+
await observeSessionStop(context.session, bound.session);
|
|
387
|
+
}
|
|
388
|
+
const report = terminalReport({
|
|
389
|
+
conversation: bound.conversation === true,
|
|
390
|
+
turnCompleted: turnObserved,
|
|
391
|
+
capacityExhausted,
|
|
392
|
+
providerRefused,
|
|
393
|
+
failure: execution,
|
|
394
|
+
continuable,
|
|
395
|
+
});
|
|
396
|
+
const retained = report === null;
|
|
397
|
+
if (retained) {
|
|
398
|
+
await writeContinuation(context.directory, {
|
|
399
|
+
...held,
|
|
400
|
+
providerSessionId: stopped.providerSessionId,
|
|
401
|
+
});
|
|
402
|
+
} else await removeContinuation(context.directory, bound.run);
|
|
403
|
+
await reportContinuation(context.session, bound.session, {
|
|
404
|
+
providerContinuation: retained,
|
|
405
|
+
providerCapacityExhausted: capacityExhausted,
|
|
406
|
+
});
|
|
407
|
+
if (retained) {
|
|
408
|
+
await removeBinding(context.directory, bound.tmux);
|
|
409
|
+
return outcome;
|
|
410
|
+
}
|
|
411
|
+
const settled = await reportTerminalState(context, bound, report);
|
|
412
|
+
return {
|
|
413
|
+
...outcome,
|
|
414
|
+
reported: settled.reported,
|
|
415
|
+
refused: settled.failure,
|
|
416
|
+
settled: settled.settled,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function assignmentRoute(context, assignment) {
|
|
421
|
+
const kind = assignment.route.adapter.kind;
|
|
422
|
+
if (!context.configuration.agents.includes(kind)) {
|
|
423
|
+
throw new KitRefusal("kit_agent_undeclared", `${kind}; this Environment does not declare it`);
|
|
424
|
+
}
|
|
425
|
+
return assignment.route;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function prepareAssignment(context, assignment, resumed) {
|
|
429
|
+
const route = assignmentRoute(context, assignment);
|
|
430
|
+
const command = await installedAgentCommand(context.directory, route.adapter.kind);
|
|
431
|
+
await readyLaunchDirectory(assignment.workingDirectory, context.launchBase, assignment.agentId, {
|
|
432
|
+
container: context.container,
|
|
433
|
+
});
|
|
434
|
+
if (!resumed) await writeContinuation(context.directory, {
|
|
435
|
+
run: assignment.run, agentId: assignment.agentId,
|
|
436
|
+
workingDirectory: assignment.workingDirectory,
|
|
437
|
+
routeId: assignment.route.id, conversation: assignment.conversation === true,
|
|
438
|
+
providerSessionId: null,
|
|
439
|
+
});
|
|
440
|
+
const handle = workerSessionName();
|
|
441
|
+
const bound = {
|
|
442
|
+
run: assignment.run,
|
|
443
|
+
session: assignment.session.id,
|
|
444
|
+
tmux: handle,
|
|
445
|
+
socket: join(context.directory, "run", `${handle}.sock`),
|
|
446
|
+
nonce: randomBytes(24).toString("base64url"),
|
|
447
|
+
conversation: assignment.conversation === true,
|
|
448
|
+
ownerStop: null,
|
|
449
|
+
sealed: null,
|
|
450
|
+
reported: null,
|
|
451
|
+
};
|
|
452
|
+
const authority = { token: assignment.session.token, expiresAt: assignment.session.expiresAt };
|
|
453
|
+
const opened = await openRunBridge(context, bound, authority, {
|
|
454
|
+
working_directory: assignment.workingDirectory,
|
|
455
|
+
execution_authority: context.configuration.executionAuthority,
|
|
456
|
+
adapter: {
|
|
457
|
+
kind: route.adapter.kind,
|
|
458
|
+
command,
|
|
459
|
+
arguments: route.arguments,
|
|
460
|
+
model: route.model,
|
|
461
|
+
effort: route.effort,
|
|
462
|
+
environment: {},
|
|
463
|
+
},
|
|
464
|
+
request: await localRequest(context.directory, assignment.run, assignment.request),
|
|
465
|
+
provider_session: assignment.providerSessionId ?? null,
|
|
466
|
+
});
|
|
467
|
+
return { bound, opened };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function refuseContinuation(context, command, failure) {
|
|
471
|
+
await rejectResumption(context.session, command.session.id);
|
|
472
|
+
return settleRefused(context, command, failure);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function refuseAssignment(context, assignment, failure) {
|
|
476
|
+
await commitRun(context.session, assignment.run);
|
|
477
|
+
return settleRefused(context, assignment, failure);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function settleRefused(context, command, failure) {
|
|
481
|
+
await removeContinuation(context.directory, command.run);
|
|
482
|
+
const settling = {
|
|
483
|
+
run: command.run,
|
|
484
|
+
session: command.session.id,
|
|
485
|
+
reported: { operation: operationId(), ...failedReport(failure) },
|
|
486
|
+
};
|
|
487
|
+
await writeSettlement(context.directory, settling);
|
|
488
|
+
const settled = await settleReported(context, settling);
|
|
489
|
+
if (settled.settled) await removeSettlement(context.directory, settling.run);
|
|
490
|
+
return { run: settling.run, session: settling.session, tmux: null, assistant: [],
|
|
491
|
+
failure, reported: settling.reported, refused: settled.failure,
|
|
492
|
+
settled: settled.settled, destroyed: false, stopped: null };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function superviseAssignment(context, assignment, resumed = false) {
|
|
496
|
+
let prepared;
|
|
497
|
+
try {
|
|
498
|
+
prepared = await prepareAssignment(context, assignment, resumed);
|
|
499
|
+
} catch (refusal) {
|
|
500
|
+
const failure = {
|
|
501
|
+
code: refusal instanceof KitRefusal ? refusal.code : "kit_worker_preparation_failed",
|
|
502
|
+
detail: refusal instanceof KitRefusal ? refusal.message : null,
|
|
503
|
+
};
|
|
504
|
+
if (resumed) return refuseContinuation(context, assignment, failure);
|
|
505
|
+
if (undecided(refusal)) throw refusal;
|
|
506
|
+
return refuseAssignment(context, assignment, failure);
|
|
507
|
+
}
|
|
508
|
+
const { bound, opened } = prepared;
|
|
509
|
+
return superviseBound(context, bound, opened, async (spawn) => {
|
|
510
|
+
if (!resumed) await commitRun(context.session, assignment.run);
|
|
511
|
+
spawn.committed = true;
|
|
512
|
+
await writeBinding(context.directory, bound);
|
|
513
|
+
await createSession(context.runtime, {
|
|
514
|
+
session: bound.tmux,
|
|
515
|
+
workingDirectory: assignment.workingDirectory,
|
|
516
|
+
environment: {
|
|
517
|
+
...inheritedEnvironment(WORKER_INHERITED_ENVIRONMENT),
|
|
518
|
+
[BRIDGE_SOCKET_ENVIRONMENT]: bound.socket,
|
|
519
|
+
[BRIDGE_NONCE_ENVIRONMENT]: bound.nonce,
|
|
520
|
+
},
|
|
521
|
+
command: context.launcher,
|
|
522
|
+
});
|
|
523
|
+
await observeSessionStart(context.session, bound.session);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function superviseResumption(context, command) {
|
|
528
|
+
const held = await retainedContinuation(context.directory, command.run);
|
|
529
|
+
if (held === null || held.providerSessionId === null || held.routeId !== command.route.id) {
|
|
530
|
+
return refuseContinuation(context, command, { code: "kit_provider_continuation_unavailable", detail: null });
|
|
531
|
+
}
|
|
532
|
+
return superviseAssignment(context, {
|
|
533
|
+
...command, agentId: held.agentId, workingDirectory: held.workingDirectory,
|
|
534
|
+
conversation: held.conversation, providerSessionId: held.providerSessionId, request: null,
|
|
535
|
+
}, true);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function superviseRestored(context, binding, authority) {
|
|
539
|
+
const opened = await openRunBridge(context, binding, authority);
|
|
540
|
+
return superviseBound(context, binding, opened, async (spawn) => {
|
|
541
|
+
spawn.committed = true;
|
|
542
|
+
await observeSessionStart(context.session, binding.session);
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function settleAbsentSession(context, binding) {
|
|
547
|
+
try {
|
|
548
|
+
await observeSessionStop(context.session, binding.session);
|
|
549
|
+
await reportContinuation(context.session, binding.session, {
|
|
550
|
+
providerContinuation: false,
|
|
551
|
+
providerCapacityExhausted: false,
|
|
552
|
+
});
|
|
553
|
+
} catch (refusal) {
|
|
554
|
+
if (undecided(refusal)) return { settled: false, failure: null };
|
|
555
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
556
|
+
}
|
|
557
|
+
await removeContinuation(context.directory, binding.run);
|
|
558
|
+
return reportTerminalState(
|
|
559
|
+
context,
|
|
560
|
+
binding,
|
|
561
|
+
failedReport({ code: PROVIDER_CONTINUATION_UNAVAILABLE, detail: null }),
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function confirmRestorable(context, binding) {
|
|
566
|
+
let authority;
|
|
567
|
+
try {
|
|
568
|
+
authority = await replaceSessionAuthority(context.session, binding.session);
|
|
569
|
+
} catch (refusal) {
|
|
570
|
+
if (undecided(refusal)) return { authority: null, unresolved: true };
|
|
571
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
572
|
+
await destroySession(context.runtime, binding.tmux);
|
|
573
|
+
await removeBinding(context.directory, binding.tmux);
|
|
574
|
+
return { authority: null, unresolved: false };
|
|
575
|
+
}
|
|
576
|
+
return { authority, unresolved: false };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function reconcile(context) {
|
|
580
|
+
const stopped = [];
|
|
581
|
+
const unresolved = [];
|
|
582
|
+
for (const settlement of await readSettlements(context.directory)) {
|
|
583
|
+
const settled = await settleReported(context, settlement);
|
|
584
|
+
if (settled.settled) await removeSettlement(context.directory, settlement.run);
|
|
585
|
+
else unresolved.push(unresolvedRun(settlement, settled.failure));
|
|
586
|
+
}
|
|
587
|
+
const bindings = await readBindings(context.directory);
|
|
588
|
+
if (bindings.length === 0) return { run: null, stopped, unresolved };
|
|
589
|
+
const commands = (await pendingCommands(context)).stops;
|
|
590
|
+
const carried = [];
|
|
591
|
+
for (const binding of bindings) {
|
|
592
|
+
const resealed = await recoverAssistantSeal(context, binding);
|
|
593
|
+
if (resealed.unresolved) {
|
|
594
|
+
unresolved.push(unresolvedRun(binding, resealed.refused));
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (binding.reported !== null) {
|
|
598
|
+
const settled = await settleReported(context, binding);
|
|
599
|
+
if (settled.settled) await removeBinding(context.directory, binding.tmux);
|
|
600
|
+
else unresolved.push(unresolvedRun(binding, settled.failure));
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const command = commands.find((pending) => pending.run === binding.run) ?? null;
|
|
604
|
+
const requestedAt = binding.ownerStop ?? command?.requestedAt ?? null;
|
|
605
|
+
if (requestedAt === null) {
|
|
606
|
+
carried.push(binding);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const finished = await completeOwnerStop(context, binding, requestedAt);
|
|
610
|
+
if (!finished.confirmed) unresolved.push(unresolvedRun(binding));
|
|
611
|
+
else stopped.push({ run: binding.run, session: binding.session, requestedAt });
|
|
612
|
+
}
|
|
613
|
+
let restorable = null;
|
|
614
|
+
for (const binding of carried) {
|
|
615
|
+
if (restorable !== null) {
|
|
616
|
+
unresolved.push(unresolvedRun(binding));
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (!(await sessionPresent(context.runtime, binding.tmux))) {
|
|
620
|
+
const settled = await settleAbsentSession(context, binding);
|
|
621
|
+
if (!settled.settled) unresolved.push(unresolvedRun(binding, settled.failure));
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const confirmed = await confirmRestorable(context, binding);
|
|
625
|
+
if (confirmed.unresolved) unresolved.push(unresolvedRun(binding));
|
|
626
|
+
else if (confirmed.authority !== null) restorable = { binding, authority: confirmed.authority };
|
|
627
|
+
}
|
|
628
|
+
if (restorable === null) return { run: null, stopped, unresolved };
|
|
629
|
+
return {
|
|
630
|
+
run: await superviseRestored(context, restorable.binding, restorable.authority),
|
|
631
|
+
stopped,
|
|
632
|
+
unresolved,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function reportHeldSessions(context) {
|
|
637
|
+
const settlements = await readSettlements(context.directory);
|
|
638
|
+
const bindings = await readBindings(context.directory);
|
|
639
|
+
const continued = (await readContinuationRuns(context.directory)).map((run) => ({ run, reported: null }));
|
|
640
|
+
const held = new Map();
|
|
641
|
+
for (const entry of [...settlements, ...bindings, ...continued]) {
|
|
642
|
+
held.set(entry.run, held.get(entry.run) ?? entry.reported);
|
|
643
|
+
}
|
|
644
|
+
const reported = await reportSessions(
|
|
645
|
+
context.session,
|
|
646
|
+
[...held].map(([run, outcome]) => ({ run, reported: outcome })),
|
|
647
|
+
);
|
|
648
|
+
for (const binding of bindings) {
|
|
649
|
+
await destroySession(context.runtime, binding.tmux);
|
|
650
|
+
await removeBinding(context.directory, binding.tmux);
|
|
651
|
+
}
|
|
652
|
+
for (const run of held.keys()) {
|
|
653
|
+
await removeSettlement(context.directory, run);
|
|
654
|
+
await removeContinuation(context.directory, run);
|
|
655
|
+
}
|
|
656
|
+
return reported;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function afterEpochReport(context, pass) {
|
|
660
|
+
try {
|
|
661
|
+
return await pass();
|
|
662
|
+
} catch (refusal) {
|
|
663
|
+
if (!(refusal instanceof EpochReportRequired)) throw refusal;
|
|
664
|
+
}
|
|
665
|
+
context.epochReport = await reportHeldSessions(context);
|
|
666
|
+
return pass();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function workerPass(context, dependencies) {
|
|
670
|
+
const { configuration, session, signal } = context;
|
|
671
|
+
const reconciled = await reconcile(context);
|
|
672
|
+
if (reconciled.run !== null || reconciled.unresolved.length > 0) return reconciled;
|
|
673
|
+
context.inbound = await reconcileInbound(context.directory, session);
|
|
674
|
+
const setup = await acquireSetup(session, signal);
|
|
675
|
+
if (setup !== null) {
|
|
676
|
+
context.setup = await runSetup(
|
|
677
|
+
{ session, configuration, directory: context.directory, dependencies, signal },
|
|
678
|
+
setup.request,
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
context.agents = await updateDeclaredAgents(
|
|
682
|
+
{ directory: context.directory, at: dependencies.now?.() },
|
|
683
|
+
configuration,
|
|
684
|
+
dependencies,
|
|
685
|
+
);
|
|
686
|
+
const readiness = await agentReadiness({ directory: context.directory }, configuration, dependencies);
|
|
687
|
+
await reportAgentCapabilities(session, readiness.agents);
|
|
688
|
+
if (readiness.refused !== null) throw readiness.refused;
|
|
689
|
+
const resumption = await acquireOneResumption(session);
|
|
690
|
+
if (resumption !== null) return { ...reconciled, run: await superviseResumption(context, resumption) };
|
|
691
|
+
const assignment = await acquireOneRun(session);
|
|
692
|
+
if (assignment === null) return reconciled;
|
|
693
|
+
return { ...reconciled, run: await superviseAssignment(context, assignment) };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async function workerContext(options, dependencies) {
|
|
697
|
+
const configuration = await readConfiguration(options.configuration);
|
|
698
|
+
const runtime = workerRuntime(options.directory, dependencies.tmux ?? "tmux");
|
|
699
|
+
const launcher = dependencies.launcher ?? [process.execPath, LAUNCHER_MAIN];
|
|
700
|
+
const tmux = await readyTmux(runtime);
|
|
701
|
+
const session = await openSession(options, dependencies);
|
|
702
|
+
const context = {
|
|
703
|
+
configuration,
|
|
704
|
+
runtime,
|
|
705
|
+
session,
|
|
706
|
+
launcher,
|
|
707
|
+
launchBase: launchBase(dependencies),
|
|
708
|
+
container: dependencies.container ?? process.env.HOUSE_KIT_CONTAINER === "1",
|
|
709
|
+
directory: options.directory,
|
|
710
|
+
files: { originals: [], failures: [] },
|
|
711
|
+
epochReport: null,
|
|
712
|
+
};
|
|
713
|
+
return { context, tmux };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function controlledPass(context, dependencies, configuration, signal) {
|
|
717
|
+
context.configuration = { ...configuration, agents: await desiredAgents(context.session, signal) };
|
|
718
|
+
context.signal = signal;
|
|
719
|
+
context.session.signal = signal;
|
|
720
|
+
return afterEpochReport(context, () => workerPass(context, dependencies));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export async function runWorkers(options, dependencies = {}) {
|
|
724
|
+
const { context, tmux } = await workerContext(options, dependencies);
|
|
725
|
+
const { session, runtime } = context;
|
|
726
|
+
const supervised = await maintainControl(session, async (signal) => {
|
|
727
|
+
return controlledPass(context, dependencies, context.configuration, signal);
|
|
728
|
+
}, session.signal);
|
|
729
|
+
return {
|
|
730
|
+
tmux,
|
|
731
|
+
namespace: runtime.namespace,
|
|
732
|
+
agents: context.agents ?? [],
|
|
733
|
+
inbound: context.inbound ?? null,
|
|
734
|
+
setup: context.setup ?? null,
|
|
735
|
+
files: context.files,
|
|
736
|
+
epochReport: context.epochReport,
|
|
737
|
+
run: supervised.run,
|
|
738
|
+
stopped: supervised.stopped,
|
|
739
|
+
unresolved: supervised.unresolved,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export async function runResident(options, dependencies = {}) {
|
|
744
|
+
const { context } = await workerContext(options, dependencies);
|
|
745
|
+
await maintainControl(context.session, async (signal) => {
|
|
746
|
+
while (!signal.aborted) {
|
|
747
|
+
const configuration = await readConfiguration(options.configuration);
|
|
748
|
+
await controlledPass(context, dependencies, configuration, signal);
|
|
749
|
+
try {
|
|
750
|
+
await delay(dependencies.residentIntervalMs ?? 30_000, undefined, { signal });
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (!signal.aborted) throw error;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}, options.signal);
|
|
756
|
+
}
|