@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.
Files changed (142) hide show
  1. package/README.md +697 -260
  2. package/bin/connect-linux.sh +200 -25
  3. package/bin/kit.js +179 -157
  4. package/bootstrap.json +17 -0
  5. package/package.json +7 -2
  6. package/src/adapter-mcp.js +76 -12
  7. package/src/agents.js +63 -8
  8. package/src/binding.js +52 -0
  9. package/src/blocks.js +2 -26
  10. package/src/bridge.js +28 -14
  11. package/src/bytes.js +19 -8
  12. package/src/configuration.js +13 -209
  13. package/src/connector-login.js +3 -84
  14. package/src/contract.js +36 -11
  15. package/src/delivery.js +4 -7
  16. package/src/directory.js +6 -26
  17. package/src/draft.js +4 -82
  18. package/src/git-main.js +138 -0
  19. package/src/git.js +62 -0
  20. package/src/google/calendar.js +39 -45
  21. package/src/google/drive.js +61 -186
  22. package/src/google/entries.js +346 -0
  23. package/src/google/gateway.js +3 -6
  24. package/src/google/gmail.js +33 -23
  25. package/src/google/oauth.js +106 -66
  26. package/src/google/source.js +21 -23
  27. package/src/host.js +6 -2
  28. package/src/house-main.js +65 -8
  29. package/src/house.js +446 -127
  30. package/src/how-we-work.md +25 -0
  31. package/src/inbound.js +2 -18
  32. package/src/kit-work-stream.js +118 -0
  33. package/src/launcher.js +46 -10
  34. package/src/local-files.js +3 -11
  35. package/src/login.js +23 -11
  36. package/src/mcp.js +18 -0
  37. package/src/mount.js +13 -21
  38. package/src/original.js +4 -4
  39. package/src/pacing.js +41 -0
  40. package/src/plan.js +37 -45
  41. package/src/repository.js +3 -61
  42. package/src/session-bin/git +2 -0
  43. package/src/setup.js +14 -15
  44. package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
  45. package/src/skill-set/ask-matt/SKILL.md +91 -0
  46. package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
  47. package/src/skill-set/code-review/LICENSE +21 -0
  48. package/src/skill-set/code-review/SKILL.md +89 -0
  49. package/src/skill-set/code-review/agents/openai.yaml +3 -0
  50. package/src/skill-set/codebase-design/DEEPENING.md +37 -0
  51. package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
  52. package/src/skill-set/codebase-design/LICENSE +21 -0
  53. package/src/skill-set/codebase-design/SKILL.md +116 -0
  54. package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
  55. package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
  56. package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
  57. package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
  58. package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  59. package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
  60. package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
  61. package/src/skill-set/domain-modeling/LICENSE +21 -0
  62. package/src/skill-set/domain-modeling/SKILL.md +76 -0
  63. package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
  64. package/src/skill-set/grill-me/LICENSE +21 -0
  65. package/src/skill-set/grill-me/SKILL.md +8 -0
  66. package/src/skill-set/grill-me/agents/openai.yaml +4 -0
  67. package/src/skill-set/grill-with-docs/LICENSE +21 -0
  68. package/src/skill-set/grill-with-docs/SKILL.md +8 -0
  69. package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
  70. package/src/skill-set/grilling/LICENSE +21 -0
  71. package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
  72. package/src/skill-set/handoff/LICENSE +21 -0
  73. package/src/skill-set/handoff/SKILL.md +17 -0
  74. package/src/skill-set/handoff/agents/openai.yaml +4 -0
  75. package/src/skill-set/implement/LICENSE +21 -0
  76. package/src/skill-set/implement/SKILL.md +16 -0
  77. package/src/skill-set/implement/agents/openai.yaml +4 -0
  78. package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
  79. package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
  80. package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
  81. package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
  82. package/src/skill-set/prototype/LICENSE +21 -0
  83. package/src/skill-set/prototype/LOGIC.md +67 -0
  84. package/src/skill-set/prototype/SKILL.md +28 -0
  85. package/src/skill-set/prototype/UI.md +112 -0
  86. package/src/skill-set/prototype/agents/openai.yaml +3 -0
  87. package/src/skill-set/research/LICENSE +21 -0
  88. package/src/skill-set/research/SKILL.md +14 -0
  89. package/src/skill-set/research/agents/openai.yaml +3 -0
  90. package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
  91. package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
  92. package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
  93. package/src/skill-set/tdd/LICENSE +21 -0
  94. package/src/skill-set/tdd/SKILL.md +40 -0
  95. package/src/skill-set/tdd/agents/openai.yaml +3 -0
  96. package/src/skill-set/tdd/mocking.md +59 -0
  97. package/src/skill-set/tdd/tests.md +77 -0
  98. package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
  99. package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
  100. package/src/skill-set/teach/LICENSE +21 -0
  101. package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
  102. package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
  103. package/src/skill-set/teach/SKILL.md +141 -0
  104. package/src/skill-set/teach/agents/openai.yaml +4 -0
  105. package/src/skill-set/to-questionnaire/LICENSE +21 -0
  106. package/src/skill-set/to-questionnaire/SKILL.md +55 -0
  107. package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
  108. package/src/skill-set/to-spec/LICENSE +21 -0
  109. package/src/skill-set/to-spec/SKILL.md +76 -0
  110. package/src/skill-set/to-spec/agents/openai.yaml +4 -0
  111. package/src/skill-set/to-tickets/LICENSE +21 -0
  112. package/src/skill-set/to-tickets/SKILL.md +106 -0
  113. package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
  114. package/src/skill-set/wait-what/LICENSE +21 -0
  115. package/src/skill-set/wait-what/SKILL.md +8 -0
  116. package/src/skill-set/wait-what/agents/openai.yaml +4 -0
  117. package/src/skill-set/wayfinder/LICENSE +21 -0
  118. package/src/skill-set/wayfinder/SKILL.md +129 -0
  119. package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
  120. package/src/skill-set/writing-for-agents/LICENSE +21 -0
  121. package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
  122. package/src/skill-set/writing-for-agents/SKILL.md +83 -0
  123. package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
  124. package/src/skill-set.js +85 -0
  125. package/src/source.js +93 -54
  126. package/src/state.js +19 -1
  127. package/src/sync-library.js +7 -11
  128. package/src/sync-state.js +16 -3
  129. package/src/sync.js +36 -21
  130. package/src/tasks.js +75 -0
  131. package/src/telegram-view.js +214 -0
  132. package/src/telegram.js +161 -66
  133. package/src/turn-frames.js +69 -0
  134. package/src/worker.js +674 -184
  135. package/src/working-copy.js +1221 -0
  136. package/src/workspace.js +40 -32
  137. package/src/google/declared.js +0 -146
  138. package/src/google/id.js +0 -1
  139. package/src/instance/google.js +0 -1
  140. package/src/starter-set.js +0 -17
  141. /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
  142. /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
package/src/house.js CHANGED
@@ -1,8 +1,10 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
1
+ import { existsSync } from "node:fs";
2
+ import { chmod, mkdir, readFile, rm } from "node:fs/promises";
3
+ import { connect, createServer } from "node:net";
4
+ import { dirname, join } from "node:path";
3
5
  import { assertAcquiredCommandAdapter } from "./adapter.js";
6
+ import { readFrames, writeFrame } from "./channel.js";
4
7
  import {
5
- assistantDraftsPath,
6
8
  assistantTextPath,
7
9
  ADAPTER_KINDS,
8
10
  INTERACTION_CORRELATION_MAX,
@@ -21,11 +23,14 @@ import {
21
23
  KIT_SESSIONS_REPORT_PATH,
22
24
  KIT_SETUP_ACQUIRE_PATH,
23
25
  KIT_SETUP_REPORT_PATH,
26
+ KIT_SOURCES_DESIRED_PATH,
27
+ KIT_SOURCES_REPORT_PATH,
24
28
  KIT_WORKSPACES_ACQUIRE_PATH,
25
29
  LEASE_RENEWAL_MARGIN_MS,
26
30
  MCP_PROTOCOL_VERSION,
27
31
  WORKER_RUN_TOKEN_MARGIN_MS,
28
32
  runOutcomePath,
33
+ runRequestPath,
29
34
  secretInputPath,
30
35
  SECRET_VALUE_MAX,
31
36
  sessionInteractionsPath,
@@ -35,7 +40,10 @@ import { declarationReader } from "./declaration.js";
35
40
  import { houseRefusal, KitRefusal, throwHouseAnswer } from "./refusal.js";
36
41
 
37
42
  const LEASE = /^ahl_[A-Za-z0-9_-]+$/;
38
- const ASSISTANT_DRAFT_STATES = ["streamed", "unsupported"];
43
+ const RESIDENT = "Resident";
44
+ const LEASE_ACTIVE = "environment_control_lease_active";
45
+ const UPGRADE_REQUIRED = "kit_upgrade_required";
46
+ const UPDATE_DRAINING = "kit_update_draining";
39
47
  const ITEM_OUTCOMES = ["accepted", "duplicate", "rejected"];
40
48
 
41
49
  async function readCredential(directory) {
@@ -49,6 +57,10 @@ async function readCredential(directory) {
49
57
  return stored;
50
58
  }
51
59
 
60
+ function latchRefusal(answer, dependencies) {
61
+ if (answer?.error?.code === UPGRADE_REQUIRED) dependencies.contractRefused?.();
62
+ }
63
+
52
64
  async function houseAnswer(house, path, init, dependencies, signal) {
53
65
  const fetcher = dependencies.fetch ?? fetch;
54
66
  let response;
@@ -66,6 +78,7 @@ async function houseAnswer(house, path, init, dependencies, signal) {
66
78
  } catch {
67
79
  answer = null;
68
80
  }
81
+ latchRefusal(answer, dependencies);
69
82
  throwHouseAnswer(response.status, answer, path);
70
83
  if (answer === null) throw new KitRefusal("kit_house_unavailable", path);
71
84
  return answer;
@@ -105,7 +118,291 @@ function heldLease(answer) {
105
118
  return { lease: answer.lease, expiresAt: Date.parse(answer.expires_at) };
106
119
  }
107
120
 
121
+ function residentSocket(directory) {
122
+ return join(directory, "run", "resident.sock");
123
+ }
124
+
125
+ export function updateDrain(directory) {
126
+ return join(directory, "state", "update-drain");
127
+ }
128
+
129
+ export function refuseDrained(directory) {
130
+ if (existsSync(updateDrain(directory))) throw new KitRefusal(UPDATE_DRAINING);
131
+ }
132
+
133
+ function residentAnswer(status, code) {
134
+ const body = Buffer.from(JSON.stringify({ error: { code } })).toString("base64");
135
+ return { status, type: "application/json", body };
136
+ }
137
+
138
+ function newWork(path) {
139
+ return path === "/kit/telegram/updates" || (path.endsWith("/acquire") && path !== "/kit/commands/acquire");
140
+ }
141
+
142
+ async function residentRelay(session, request, signal, draining) {
143
+ const path = typeof request.path === "string" ? request.path : "";
144
+ if (!path.startsWith("/kit/") || path.startsWith("/kit/lease")) {
145
+ return residentAnswer(403, "kit_resident_route_refused");
146
+ }
147
+ if (draining && newWork(path)) {
148
+ return residentAnswer(409, UPDATE_DRAINING);
149
+ }
150
+ let authorization;
151
+ try {
152
+ authorization = await heldControl(session, signal);
153
+ } catch (error) {
154
+ return residentAnswer(error.status ?? 401, error.code ?? "kit_lease_rejected");
155
+ }
156
+ try {
157
+ const response = await (session.dependencies.fetch ?? fetch)(`${session.house}${path}`, {
158
+ method: request.method,
159
+ headers: { ...request.headers, authorization },
160
+ body: request.text ?? (request.bytes === undefined ? undefined : Buffer.from(request.bytes, "base64")),
161
+ signal,
162
+ });
163
+ const body = Buffer.from(await response.arrayBuffer());
164
+ if (response.status === 409) latchRefusal(JSON.parse(body.toString("utf8")), session.dependencies);
165
+ return {
166
+ status: response.status,
167
+ type: response.headers.get("content-type"),
168
+ body: body.length === 0 ? null : body.toString("base64"),
169
+ };
170
+ } catch {
171
+ return residentAnswer(503, "kit_house_unavailable");
172
+ }
173
+ }
174
+
175
+ export async function serveResident(session, directory, signal, drain, push, capture, stream = null) {
176
+ const socket = residentSocket(directory);
177
+ await mkdir(dirname(socket), { recursive: true, mode: 0o700 });
178
+ await rm(socket, { force: true });
179
+ const connections = new Set();
180
+ let holder = null;
181
+ let listening = null;
182
+ let draining = false;
183
+ function answerDrain(connection) {
184
+ draining = true;
185
+ drain(holder !== null).then(
186
+ (answer) => {
187
+ if (!connection.destroyed) writeFrame(connection, answer);
188
+ connection.end();
189
+ },
190
+ () => connection.destroy(),
191
+ );
192
+ return false;
193
+ }
194
+ function hold(connection) {
195
+ if (holder !== null || draining || existsSync(updateDrain(directory))) {
196
+ writeFrame(connection, { refused: draining || holder === null ? UPDATE_DRAINING : LEASE_ACTIVE });
197
+ connection.end();
198
+ return false;
199
+ }
200
+ holder = connection;
201
+ renewControl(session, signal).then(
202
+ () => {
203
+ writeFrame(connection, { held: true });
204
+ if (holder === connection) listening = connection;
205
+ },
206
+ (error) => {
207
+ writeFrame(connection, { refused: error.code ?? "kit_lease_rejected" });
208
+ connection.end();
209
+ },
210
+ );
211
+ return true;
212
+ }
213
+ const server = createServer((connection) => {
214
+ connections.add(connection);
215
+ connection.on("error", () => connection.destroy());
216
+ const relayed = new Map();
217
+ let held = null;
218
+ connection.on("close", () => {
219
+ connections.delete(connection);
220
+ if (holder === connection) holder = null;
221
+ if (listening === connection) listening = null;
222
+ for (const cancelled of relayed.values()) cancelled.abort();
223
+ });
224
+ readFrames(connection, {
225
+ onFrame: (request) => {
226
+ if (held === null) {
227
+ held = request.drain === true ? answerDrain(connection) : hold(connection);
228
+ return;
229
+ }
230
+ if (!held) return;
231
+ if (request.stream !== undefined) {
232
+ if (request.stream?.type === "gap") stream?.write(request.stream);
233
+ return;
234
+ }
235
+ if (request.cancel === true) {
236
+ relayed.get(request.id)?.abort();
237
+ return;
238
+ }
239
+ const cancelled = new AbortController();
240
+ relayed.set(request.id, cancelled);
241
+ const relaySignal = AbortSignal.any([signal, cancelled.signal]);
242
+ const answered =
243
+ request.push !== undefined
244
+ ? push(request.push, relaySignal).then((pushed) => ({ pushed }))
245
+ : request.capture !== undefined
246
+ ? capture(request.capture, relaySignal).then(
247
+ (captured) => ({ captured }),
248
+ (error) => ({ refused: error?.code ?? "kit_resident_unavailable" }),
249
+ )
250
+ : residentRelay(session, request, relaySignal, draining);
251
+ void answered.then((answer) => {
252
+ relayed.delete(request.id);
253
+ if (!connection.destroyed) writeFrame(connection, { id: request.id, ...answer });
254
+ });
255
+ },
256
+ onRefusal: () => connection.destroy(),
257
+ });
258
+ });
259
+ await new Promise((resolve, reject) => {
260
+ server.once("error", reject);
261
+ server.listen(socket, resolve);
262
+ });
263
+ await chmod(socket, 0o600);
264
+ return {
265
+ serving: () => holder !== null,
266
+ forward(frame) {
267
+ if (listening !== null && !listening.destroyed) writeFrame(listening, { stream: frame });
268
+ },
269
+ async close() {
270
+ for (const connection of connections) connection.destroy();
271
+ await new Promise((resolve) => server.close(resolve));
272
+ await rm(socket, { force: true });
273
+ },
274
+ };
275
+ }
276
+
277
+ export function requestDrain(directory) {
278
+ const connection = connect(residentSocket(directory));
279
+ return new Promise((resolve, reject) => {
280
+ const unavailable = () => reject(new KitRefusal("kit_resident_unavailable"));
281
+ connection.on("error", unavailable);
282
+ connection.on("close", unavailable);
283
+ readFrames(connection, {
284
+ onFrame: (answer) => {
285
+ resolve(answer);
286
+ connection.end();
287
+ },
288
+ onRefusal: () => connection.destroy(),
289
+ });
290
+ writeFrame(connection, { drain: true });
291
+ });
292
+ }
293
+
294
+ async function residentDelegation(directory, fetcher) {
295
+ const connection = connect(residentSocket(directory));
296
+ const pending = new Map();
297
+ const watchers = new Set();
298
+ let greet;
299
+ const greeting = new Promise((resolve) => {
300
+ greet = resolve;
301
+ });
302
+ const unavailable = () => residentAnswer(503, "kit_resident_unavailable");
303
+ function settle(id, answer) {
304
+ pending.get(id)?.(answer);
305
+ pending.delete(id);
306
+ if (pending.size === 0) connection.unref();
307
+ }
308
+ connection.on("error", () => connection.destroy());
309
+ connection.on("close", () => {
310
+ greet(null);
311
+ for (const id of [...pending.keys()]) settle(id, unavailable());
312
+ });
313
+ readFrames(connection, {
314
+ onFrame: (frame) => {
315
+ if (frame.id !== undefined) settle(frame.id, frame);
316
+ else if (frame.stream !== undefined) for (const heard of watchers) heard.viewed?.(frame.stream);
317
+ else greet(frame);
318
+ },
319
+ onRefusal: () => connection.destroy(),
320
+ });
321
+ writeFrame(connection, { hold: true });
322
+ const held = await greeting;
323
+ if (held === null) return null;
324
+ if (held.held !== true) {
325
+ connection.destroy();
326
+ throw new KitRefusal(held.refused);
327
+ }
328
+ connection.unref();
329
+ let next = 0;
330
+ function exchange(frame, signal) {
331
+ signal?.throwIfAborted();
332
+ if (connection.destroyed) return Promise.resolve(unavailable());
333
+ return new Promise((resolve, reject) => {
334
+ const id = (next += 1);
335
+ const cancel = () => {
336
+ pending.delete(id);
337
+ settle(id);
338
+ if (!connection.destroyed) writeFrame(connection, { id, cancel: true });
339
+ reject(signal.reason);
340
+ };
341
+ signal?.addEventListener("abort", cancel, { once: true });
342
+ pending.set(id, (answer) => {
343
+ signal?.removeEventListener("abort", cancel);
344
+ resolve(answer);
345
+ });
346
+ connection.ref();
347
+ writeFrame(connection, { id, ...frame });
348
+ });
349
+ }
350
+ async function push(request, signal) {
351
+ const answer = await exchange({ push: request }, signal);
352
+ if (answer.pushed === undefined) throw new KitRefusal("kit_resident_unavailable");
353
+ return answer.pushed;
354
+ }
355
+ async function capture(source, signal) {
356
+ const answer = await exchange({ capture: source }, signal);
357
+ if (answer.refused !== undefined) throw new KitRefusal(answer.refused);
358
+ if (answer.captured === undefined) throw new KitRefusal("kit_resident_unavailable");
359
+ return answer.captured;
360
+ }
361
+ async function delegated(url, init = {}) {
362
+ const { authorization, ...headers } = init.headers ?? {};
363
+ if (authorization !== RESIDENT) return fetcher(url, init);
364
+ const address = new URL(url);
365
+ const answer = await exchange(
366
+ {
367
+ method: init.method ?? "GET",
368
+ path: `${address.pathname}${address.search}`,
369
+ headers,
370
+ ...(typeof init.body === "string"
371
+ ? { text: init.body }
372
+ : init.body === undefined
373
+ ? {}
374
+ : { bytes: Buffer.from(init.body).toString("base64") }),
375
+ },
376
+ init.signal,
377
+ );
378
+ return new Response(answer.body === null ? null : Buffer.from(answer.body, "base64"), {
379
+ status: answer.status,
380
+ headers: answer.type === null ? {} : { "content-type": answer.type },
381
+ });
382
+ }
383
+ function relay(signal, heard) {
384
+ return new Promise((resolve) => {
385
+ const end = () => {
386
+ signal.removeEventListener("abort", end);
387
+ connection.removeListener("close", end);
388
+ if (!watchers.delete(heard)) return;
389
+ heard.closed?.();
390
+ resolve();
391
+ };
392
+ watchers.add(heard);
393
+ heard.opened?.((frame) => {
394
+ if (!connection.destroyed) writeFrame(connection, { stream: frame });
395
+ });
396
+ signal.addEventListener("abort", end, { once: true });
397
+ connection.once("close", end);
398
+ if (signal.aborted || connection.destroyed) end();
399
+ });
400
+ }
401
+ return { connection, fetch: delegated, push, capture, relay };
402
+ }
403
+
108
404
  export async function openSession(options, dependencies = {}) {
405
+ if (options.resident !== true) refuseDrained(options.directory);
109
406
  const session = {
110
407
  house: options.house,
111
408
  dependencies,
@@ -114,27 +411,42 @@ export async function openSession(options, dependencies = {}) {
114
411
  expiresAt: 0,
115
412
  signal: options.signal,
116
413
  };
414
+ const resident =
415
+ options.acquire === true || options.resident === true
416
+ ? null
417
+ : await residentDelegation(options.directory, dependencies.fetch ?? fetch);
418
+ if (resident !== null) {
419
+ session.resident = resident.connection;
420
+ session.push = resident.push;
421
+ session.capture = resident.capture;
422
+ session.relay = resident.relay;
423
+ session.dependencies = { ...dependencies, fetch: resident.fetch };
424
+ return session;
425
+ }
426
+ session.dependencies = {
427
+ ...dependencies,
428
+ contractRefused: () => {
429
+ session.refused = true;
430
+ },
431
+ };
117
432
  const credential = await readCredential(options.directory);
118
- Object.assign(
119
- session,
120
- heldLease(
121
- await houseRequest(
122
- session.house,
123
- `Bearer ${credential}`,
124
- "/kit/lease",
125
- {},
126
- session.dependencies,
127
- session.signal,
128
- ),
129
- ),
130
- );
433
+ const answer = await houseRequest(
434
+ session.house,
435
+ `Bearer ${credential}`,
436
+ "/kit/lease",
437
+ {},
438
+ session.dependencies,
439
+ session.signal,
440
+ ).catch((error) => {
441
+ if (options.resident === true && session.refused) return null;
442
+ throw error;
443
+ });
444
+ if (answer !== null) Object.assign(session, heldLease(answer));
131
445
  return session;
132
446
  }
133
447
 
134
- export async function heldControl(session, signal = session.signal) {
135
- if (session.expiresAt - session.now().getTime() > LEASE_RENEWAL_MARGIN_MS) {
136
- return `Bearer ${session.lease}`;
137
- }
448
+ async function renewControl(session, signal) {
449
+ if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
138
450
  Object.assign(
139
451
  session,
140
452
  heldLease(
@@ -148,10 +460,18 @@ export async function heldControl(session, signal = session.signal) {
148
460
  ),
149
461
  ),
150
462
  );
463
+ }
464
+
465
+ export async function heldControl(session, signal = session.signal) {
466
+ if (session.resident !== undefined) return RESIDENT;
467
+ if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
468
+ if (session.expiresAt - session.now().getTime() <= LEASE_RENEWAL_MARGIN_MS) {
469
+ await renewControl(session, signal);
470
+ }
151
471
  return `Bearer ${session.lease}`;
152
472
  }
153
473
 
154
- function controlSleep(milliseconds, signal) {
474
+ export function controlSleep(milliseconds, signal) {
155
475
  return new Promise((resolve, reject) => {
156
476
  if (signal.aborted) {
157
477
  reject(new KitRefusal("kit_command_cancelled"));
@@ -195,9 +515,11 @@ export function mutatingCall(classification) {
195
515
  }
196
516
 
197
517
  export async function releaseControl(session, options = {}) {
518
+ session.resident?.end();
198
519
  if (session.lease === null) return;
199
520
  const held = session.lease;
200
521
  try {
522
+ if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
201
523
  await houseRequest(
202
524
  session.house,
203
525
  `Bearer ${held}`,
@@ -229,6 +551,7 @@ export async function maintainControl(session, operation, signal, { release = tr
229
551
  signal?.addEventListener("abort", cancelOperation, { once: true });
230
552
  });
231
553
  const renewal = (async () => {
554
+ if (session.resident !== undefined) return;
232
555
  const sleep = session.dependencies.controlSleep ?? controlSleep;
233
556
  while (!stopped) {
234
557
  const delay = Math.max(
@@ -245,7 +568,7 @@ export async function maintainControl(session, operation, signal, { release = tr
245
568
  throw new KitRefusal("kit_lease_rejected");
246
569
  }
247
570
  } catch (error) {
248
- if (stopped) return;
571
+ if (stopped || session.refused) return;
249
572
  rejectFailure(error);
250
573
  maintenance.abort();
251
574
  return;
@@ -253,20 +576,14 @@ export async function maintainControl(session, operation, signal, { release = tr
253
576
  }
254
577
  })();
255
578
  const running = Promise.resolve().then(() => operation(operationSignal));
256
- let ended = false;
257
579
  try {
258
- const answer = await Promise.race([running, failure, cancellation]);
259
- ended = true;
260
- return answer;
261
- } catch (error) {
262
- ended = error instanceof KitRefusal && error.code === "kit_command_cancelled";
263
- throw error;
580
+ return await Promise.race([running, failure, cancellation]);
264
581
  } finally {
265
582
  stopped = true;
266
583
  maintenance.abort();
267
584
  signal?.removeEventListener("abort", cancelOperation);
268
585
  await Promise.all([renewal, running.catch(() => undefined)]);
269
- if (ended && release) await releaseControl(session);
586
+ if (release) await releaseControl(session);
270
587
  }
271
588
  }
272
589
 
@@ -426,6 +743,30 @@ export async function desiredAgents(session, signal = session.signal) {
426
743
  return answer.agents;
427
744
  }
428
745
 
746
+ export async function desiredSources(session, signal = session.signal) {
747
+ const answer = await houseRequest(
748
+ session.house,
749
+ await heldControl(session, signal),
750
+ KIT_SOURCES_DESIRED_PATH,
751
+ {},
752
+ session.dependencies,
753
+ signal,
754
+ );
755
+ if (!Array.isArray(answer?.sources)) throw new KitRefusal("kit_sources_rejected");
756
+ return answer.sources;
757
+ }
758
+
759
+ export async function reportSources(session, sources, signal = session.signal) {
760
+ return houseRequest(
761
+ session.house,
762
+ await heldControl(session, signal),
763
+ KIT_SOURCES_REPORT_PATH,
764
+ { sources },
765
+ session.dependencies,
766
+ signal,
767
+ );
768
+ }
769
+
429
770
  const HOUSE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
430
771
 
431
772
  export async function acquireSetup(session, signal = session.signal) {
@@ -479,6 +820,7 @@ export async function reportLogin(session, login, outcome, signal = session.sign
479
820
  }
480
821
 
481
822
  const envelope = declarationReader("kit_acquire_rejected");
823
+ const input = declarationReader("kit_run_input_unreadable");
482
824
 
483
825
  function acquiredRoute(value, declaration) {
484
826
  const route = envelope.plain(value, declaration);
@@ -496,79 +838,82 @@ function acquiredRoute(value, declaration) {
496
838
 
497
839
  function conversationText(value, declaration) {
498
840
  if (value === null) return null;
499
- if (typeof value !== "string" || value.length > 4096) envelope.refuse(declaration);
841
+ if (typeof value !== "string" || value.length > 4096) input.refuse(declaration);
500
842
  return value;
501
843
  }
502
844
 
503
845
  function conversationMessage(value, declaration) {
504
- const message = envelope.plain(value, declaration);
846
+ const message = input.plain(value, declaration);
505
847
  const text = conversationText(message.text, `${declaration}.text`);
506
848
  const caption = conversationText(message.caption, `${declaration}.caption`);
507
- if (!Array.isArray(message.files)) envelope.refuse(`${declaration}.files`);
849
+ if (!Array.isArray(message.files)) input.refuse(`${declaration}.files`);
508
850
  const files = message.files.map((value, index) => {
509
851
  const name = `${declaration}.files[${index}]`;
510
- const file = envelope.plain(value, name);
852
+ const file = input.plain(value, name);
511
853
  if (file.bytes !== null && (!Number.isSafeInteger(file.bytes) || file.bytes < 0)) {
512
- envelope.refuse(`${name}.bytes`);
854
+ input.refuse(`${name}.bytes`);
513
855
  }
514
856
  return {
515
- path: envelope.text(file.path, `${name}.path`, 2048),
516
- media_kind: envelope.text(file.media_kind, `${name}.media_kind`, 128),
517
- media_type: file.media_type === null ? null : envelope.text(file.media_type, `${name}.media_type`, 255),
857
+ path: input.text(file.path, `${name}.path`, 2048),
858
+ media_kind: input.text(file.media_kind, `${name}.media_kind`, 128),
859
+ media_type: file.media_type === null ? null : input.text(file.media_type, `${name}.media_type`, 255),
518
860
  bytes: file.bytes,
519
861
  };
520
862
  });
521
- if (!text && !caption && files.length === 0) envelope.refuse(declaration);
863
+ if (!text && !caption && files.length === 0) input.refuse(declaration);
522
864
  return {
523
- message_id: envelope.text(message.message_id, `${declaration}.message_id`, 128),
865
+ message_id: input.text(message.message_id, `${declaration}.message_id`, 128),
524
866
  text, caption, files,
525
867
  };
526
868
  }
527
869
 
528
- function acquiredRequest(acquired, declaration) {
529
- if (acquired.kind === "conversation") {
530
- if (acquired.origin !== "user") envelope.refuse(`${declaration}.origin`);
531
- const conversation = envelope.plain(acquired.conversation, `${declaration}.conversation`);
532
- const turn = envelope.plain(conversation.turn, `${declaration}.conversation.turn`);
870
+ function runRequest(answer) {
871
+ const request = input.plain(answer, "request");
872
+ if (request.kind === "conversation") {
873
+ if (request.origin !== "user") input.refuse("request.origin");
874
+ const turn = input.plain(request.turn, "request.turn");
533
875
  return {
876
+ kind: "conversation",
534
877
  origin: "user",
535
- conversation: {
536
- turn: {
537
- id: envelope.text(turn.id, `${declaration}.conversation.turn.id`, 128),
538
- sequence: envelope.positiveCount(turn.sequence, `${declaration}.conversation.turn.sequence`),
539
- messages: envelope.filled(turn.messages, `${declaration}.conversation.turn.messages`)
540
- .map((message, index) => conversationMessage(message, `${declaration}.conversation.turn.messages[${index}]`)),
541
- },
878
+ turn: {
879
+ id: input.text(turn.id, "request.turn.id", 128),
880
+ sequence: input.positiveCount(turn.sequence, "request.turn.sequence"),
881
+ messages: input.filled(turn.messages, "request.turn.messages")
882
+ .map((message, index) => conversationMessage(message, `request.turn.messages[${index}]`)),
542
883
  },
543
884
  };
544
885
  }
545
- if (acquired.kind !== "routine") envelope.refuse(`${declaration}.kind`);
546
- const request = envelope.plain(acquired.request, `${declaration}.request`);
547
- const routine = envelope.plain(acquired.routine, `${declaration}.routine`);
886
+ if (request.kind !== "routine") input.refuse("request.kind");
887
+ const routine = input.plain(request.routine, "request.routine");
888
+ if (typeof request.payload !== "string" || Array.from(request.payload).length > 20_000) input.refuse("request.payload");
889
+ if (!Array.isArray(request.references)) input.refuse("request.references");
548
890
  return {
549
- origin: envelope.text(acquired.origin, `${declaration}.origin`, 32),
891
+ kind: "routine",
892
+ origin: input.text(request.origin, "request.origin", 32),
550
893
  routine: {
551
- name: envelope.text(routine.name, `${declaration}.routine.name`, 256),
552
- version: envelope.positiveCount(routine.version, `${declaration}.routine.version`),
894
+ name: input.text(routine.name, "request.routine.name", 256),
895
+ version: input.positiveCount(routine.version, "request.routine.version"),
553
896
  },
554
- payload: envelope.plain(request.payload, `${declaration}.request.payload`),
555
- requested_by: envelope.text(
556
- request.requested_by,
557
- `${declaration}.request.requested_by`,
558
- 128,
559
- ),
560
- requested_at: envelope.text(
561
- request.requested_at,
562
- `${declaration}.request.requested_at`,
563
- 64,
564
- ),
565
- deliver_at:
566
- request.deliver_at === null
567
- ? null
568
- : envelope.text(request.deliver_at, `${declaration}.request.deliver_at`, 64),
897
+ instruction: input.text(request.instruction, "request.instruction", 256 * 1024),
898
+ payload: request.payload,
899
+ references: request.references.map((reference, index) => input.plain(reference, `request.references[${index}]`)),
900
+ requested_by: input.text(request.requested_by, "request.requested_by", 128),
901
+ requested_at: input.text(request.requested_at, "request.requested_at", 64),
902
+ deliver_at: request.deliver_at === null ? null : input.text(request.deliver_at, "request.deliver_at", 64),
569
903
  };
570
904
  }
571
905
 
906
+ export async function readRunRequest(session, run, token, signal = session.signal) {
907
+ return runRequest(await houseRequest(
908
+ session.house,
909
+ `Bearer ${token}`,
910
+ runRequestPath(run),
911
+ {},
912
+ session.dependencies,
913
+ signal,
914
+ ));
915
+ }
916
+
572
917
  function acquiredAssignment(value, declaration) {
573
918
  const acquired = envelope.plain(value, declaration);
574
919
  const bridge = envelope.plain(acquired.session, `${declaration}.session`);
@@ -582,9 +927,13 @@ function acquiredAssignment(value, declaration) {
582
927
  if (Number.isNaN(Date.parse(expiresAt))) {
583
928
  envelope.refuse(`${declaration}.session.bridge_token_expires_at`);
584
929
  }
930
+ if (typeof plan.base_instructions !== "string") {
931
+ envelope.refuse(`${declaration}.plan.base_instructions`);
932
+ }
585
933
  return {
586
934
  run: envelope.text(acquired.run, `${declaration}.run`, 128),
587
935
  agentId: envelope.text(plan.agent_id, `${declaration}.plan.agent_id`, 128),
936
+ baseInstructions: plan.base_instructions,
588
937
  workingDirectory: envelope.text(
589
938
  plan.working_directory,
590
939
  `${declaration}.plan.working_directory`,
@@ -597,7 +946,6 @@ function acquiredAssignment(value, declaration) {
597
946
  token: envelope.text(bridge.bridge_token, `${declaration}.session.bridge_token`, 512),
598
947
  expiresAt: Date.parse(expiresAt),
599
948
  },
600
- request: acquiredRequest(acquired, declaration),
601
949
  };
602
950
  }
603
951
 
@@ -628,14 +976,17 @@ export async function commitRun(session, run) {
628
976
  return answer;
629
977
  }
630
978
 
631
- export async function reportRunOutcome(session, run, reported) {
979
+ export async function reportRunOutcome(session, run, reported, plan = null) {
632
980
  const answer = await houseRequest(
633
981
  session.house,
634
982
  await heldControl(session),
635
983
  runOutcomePath(run),
636
- reported.cause === null
637
- ? { operation_id: reported.operation, outcome: reported.outcome }
638
- : { operation_id: reported.operation, outcome: reported.outcome, cause: reported.cause },
984
+ withPlan(
985
+ reported.cause === null
986
+ ? { operation_id: reported.operation, outcome: reported.outcome }
987
+ : { operation_id: reported.operation, outcome: reported.outcome, cause: reported.cause },
988
+ plan,
989
+ ),
639
990
  session.dependencies,
640
991
  session.signal,
641
992
  );
@@ -668,27 +1019,16 @@ export async function reportSessions(session, held) {
668
1019
  return sessions;
669
1020
  }
670
1021
 
671
- export async function streamAssistantDraft(session, workerSession, blocks) {
672
- const answer = await houseRequest(
673
- session.house,
674
- await heldControl(session),
675
- assistantDraftsPath(workerSession),
676
- { blocks },
677
- session.dependencies,
678
- session.signal,
679
- );
680
- if (!ASSISTANT_DRAFT_STATES.includes(answer?.state)) {
681
- throw new KitRefusal("kit_assistant_draft_rejected", workerSession);
682
- }
683
- return { state: answer.state };
1022
+ function withPlan(body, plan) {
1023
+ return plan === null || plan === undefined ? body : { ...body, plan };
684
1024
  }
685
1025
 
686
- export async function acceptAssistantText(session, workerSession, sealed) {
1026
+ export async function acceptAssistantText(session, workerSession, sealed, plan = null) {
687
1027
  const answer = await houseRequest(
688
1028
  session.house,
689
1029
  await heldControl(session),
690
1030
  assistantTextPath(workerSession),
691
- { operation_id: sealed.operation, blocks: sealed.blocks },
1031
+ withPlan({ operation_id: sealed.operation, blocks: sealed.blocks }, plan),
692
1032
  session.dependencies,
693
1033
  session.signal,
694
1034
  );
@@ -703,27 +1043,12 @@ export async function acceptAssistantText(session, workerSession, sealed) {
703
1043
  return { turn: answer.turn, delivery: answer.delivery };
704
1044
  }
705
1045
 
706
- export async function admitPlanSnapshot(session, workerSession, snapshot) {
707
- const answer = await houseRequest(
708
- session.house,
709
- await heldControl(session),
710
- `/kit/sessions/${encodeURIComponent(workerSession)}/plans`,
711
- snapshot,
712
- session.dependencies,
713
- session.signal,
714
- );
715
- if (
716
- answer?.session !== workerSession ||
717
- typeof answer.plan !== "string" ||
718
- !["live", "frozen"].includes(answer.state)
719
- ) {
720
- throw new KitRefusal("kit_plan_rejected", workerSession);
721
- }
722
- return { plan: answer.plan, state: answer.state };
1046
+ function named(value) {
1047
+ return typeof value === "string" && value.length > 0 ? value : null;
723
1048
  }
724
1049
 
725
1050
  export async function observeSessionStart(session, workerSession) {
726
- return houseRequest(
1051
+ const answer = await houseRequest(
727
1052
  session.house,
728
1053
  await heldControl(session),
729
1054
  `/kit/sessions/${encodeURIComponent(workerSession)}/started`,
@@ -731,14 +1056,19 @@ export async function observeSessionStart(session, workerSession) {
731
1056
  session.dependencies,
732
1057
  session.signal,
733
1058
  );
1059
+ const conversation = named(answer?.conversation_id);
1060
+ const turn = named(answer?.turn_id);
1061
+ return conversation === null || turn === null
1062
+ ? null
1063
+ : { conversation_id: conversation, turn_id: turn, attempt_id: workerSession };
734
1064
  }
735
1065
 
736
- export async function observeSessionStop(session, workerSession, turnCompleted = false) {
1066
+ export async function observeSessionStop(session, workerSession, turnCompleted = false, plan = null) {
737
1067
  return houseRequest(
738
1068
  session.house,
739
1069
  await heldControl(session),
740
1070
  `/kit/sessions/${encodeURIComponent(workerSession)}/stopped`,
741
- turnCompleted ? { turn_completed: true } : {},
1071
+ withPlan(turnCompleted ? { turn_completed: true } : {}, plan),
742
1072
  session.dependencies,
743
1073
  session.signal,
744
1074
  );
@@ -803,17 +1133,6 @@ function acquiredInterrupt(value, declaration) {
803
1133
  };
804
1134
  }
805
1135
 
806
- export async function reportSessionHeartbeat(session, workerSession) {
807
- await houseRequest(
808
- session.house,
809
- await heldControl(session),
810
- `/kit/sessions/${encodeURIComponent(workerSession)}/heartbeat`,
811
- {},
812
- session.dependencies,
813
- session.signal,
814
- );
815
- }
816
-
817
1136
  function answeredValue(value, declaration) {
818
1137
  if (typeof value === "boolean") return value;
819
1138
  if (typeof value === "number") {
@@ -983,12 +1302,12 @@ export async function reportInteractionOutcome(session, interaction, outcome) {
983
1302
  return { disposition: answer.disposition ?? null };
984
1303
  }
985
1304
 
986
- export async function reportInterruptOutcome(session, interrupt, outcome) {
1305
+ export async function reportInterruptOutcome(session, interrupt, outcome, plan = null) {
987
1306
  const answer = await houseRequest(
988
1307
  session.house,
989
1308
  await heldControl(session),
990
1309
  `/kit/interrupts/${encodeURIComponent(interrupt)}/outcome`,
991
- { outcome },
1310
+ withPlan({ outcome }, outcome === "interrupted" ? plan : null),
992
1311
  session.dependencies,
993
1312
  session.signal,
994
1313
  );