@agentshouse/kit 0.1.0-alpha.2 → 0.1.0-alpha.21

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/src/house.js CHANGED
@@ -1,6 +1,9 @@
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
8
  assistantDraftsPath,
6
9
  assistantTextPath,
@@ -26,6 +29,7 @@ import {
26
29
  MCP_PROTOCOL_VERSION,
27
30
  WORKER_RUN_TOKEN_MARGIN_MS,
28
31
  runOutcomePath,
32
+ runRequestPath,
29
33
  secretInputPath,
30
34
  SECRET_VALUE_MAX,
31
35
  sessionInteractionsPath,
@@ -35,6 +39,10 @@ import { declarationReader } from "./declaration.js";
35
39
  import { houseRefusal, KitRefusal, throwHouseAnswer } from "./refusal.js";
36
40
 
37
41
  const LEASE = /^ahl_[A-Za-z0-9_-]+$/;
42
+ const RESIDENT = "Resident";
43
+ const LEASE_ACTIVE = "environment_control_lease_active";
44
+ const UPGRADE_REQUIRED = "kit_upgrade_required";
45
+ const UPDATE_DRAINING = "kit_update_draining";
38
46
  const ASSISTANT_DRAFT_STATES = ["streamed", "unsupported"];
39
47
  const ITEM_OUTCOMES = ["accepted", "duplicate", "rejected"];
40
48
 
@@ -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,245 @@ 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) {
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 draining = false;
182
+ function answerDrain(connection) {
183
+ draining = true;
184
+ drain(holder !== null).then(
185
+ (answer) => {
186
+ if (!connection.destroyed) writeFrame(connection, answer);
187
+ connection.end();
188
+ },
189
+ () => connection.destroy(),
190
+ );
191
+ return false;
192
+ }
193
+ function hold(connection) {
194
+ if (holder !== null || draining || existsSync(updateDrain(directory))) {
195
+ writeFrame(connection, { refused: draining || holder === null ? UPDATE_DRAINING : LEASE_ACTIVE });
196
+ connection.end();
197
+ return false;
198
+ }
199
+ holder = connection;
200
+ renewControl(session, signal).then(
201
+ () => writeFrame(connection, { held: true }),
202
+ (error) => {
203
+ writeFrame(connection, { refused: error.code ?? "kit_lease_rejected" });
204
+ connection.end();
205
+ },
206
+ );
207
+ return true;
208
+ }
209
+ const server = createServer((connection) => {
210
+ connections.add(connection);
211
+ connection.on("error", () => connection.destroy());
212
+ const relayed = new Map();
213
+ let held = null;
214
+ connection.on("close", () => {
215
+ connections.delete(connection);
216
+ if (holder === connection) holder = null;
217
+ for (const cancelled of relayed.values()) cancelled.abort();
218
+ });
219
+ readFrames(connection, {
220
+ onFrame: (request) => {
221
+ if (held === null) {
222
+ held = request.drain === true ? answerDrain(connection) : hold(connection);
223
+ return;
224
+ }
225
+ if (!held) return;
226
+ if (request.cancel === true) {
227
+ relayed.get(request.id)?.abort();
228
+ return;
229
+ }
230
+ const cancelled = new AbortController();
231
+ relayed.set(request.id, cancelled);
232
+ const relaySignal = AbortSignal.any([signal, cancelled.signal]);
233
+ const answered =
234
+ request.push === undefined
235
+ ? residentRelay(session, request, relaySignal, draining)
236
+ : push(request.push, relaySignal).then((pushed) => ({ pushed }));
237
+ void answered.then((answer) => {
238
+ relayed.delete(request.id);
239
+ if (!connection.destroyed) writeFrame(connection, { id: request.id, ...answer });
240
+ });
241
+ },
242
+ onRefusal: () => connection.destroy(),
243
+ });
244
+ });
245
+ await new Promise((resolve, reject) => {
246
+ server.once("error", reject);
247
+ server.listen(socket, resolve);
248
+ });
249
+ await chmod(socket, 0o600);
250
+ return {
251
+ serving: () => holder !== null,
252
+ async close() {
253
+ for (const connection of connections) connection.destroy();
254
+ await new Promise((resolve) => server.close(resolve));
255
+ await rm(socket, { force: true });
256
+ },
257
+ };
258
+ }
259
+
260
+ export function requestDrain(directory) {
261
+ const connection = connect(residentSocket(directory));
262
+ return new Promise((resolve, reject) => {
263
+ const unavailable = () => reject(new KitRefusal("kit_resident_unavailable"));
264
+ connection.on("error", unavailable);
265
+ connection.on("close", unavailable);
266
+ readFrames(connection, {
267
+ onFrame: (answer) => {
268
+ resolve(answer);
269
+ connection.end();
270
+ },
271
+ onRefusal: () => connection.destroy(),
272
+ });
273
+ writeFrame(connection, { drain: true });
274
+ });
275
+ }
276
+
277
+ async function residentDelegation(directory, fetcher) {
278
+ const connection = connect(residentSocket(directory));
279
+ const pending = new Map();
280
+ let greet;
281
+ const greeting = new Promise((resolve) => {
282
+ greet = resolve;
283
+ });
284
+ const unavailable = () => residentAnswer(503, "kit_resident_unavailable");
285
+ function settle(id, answer) {
286
+ pending.get(id)?.(answer);
287
+ pending.delete(id);
288
+ if (pending.size === 0) connection.unref();
289
+ }
290
+ connection.on("error", () => connection.destroy());
291
+ connection.on("close", () => {
292
+ greet(null);
293
+ for (const id of [...pending.keys()]) settle(id, unavailable());
294
+ });
295
+ readFrames(connection, {
296
+ onFrame: (frame) => (frame.id === undefined ? greet(frame) : settle(frame.id, frame)),
297
+ onRefusal: () => connection.destroy(),
298
+ });
299
+ writeFrame(connection, { hold: true });
300
+ const held = await greeting;
301
+ if (held === null) return null;
302
+ if (held.held !== true) {
303
+ connection.destroy();
304
+ throw new KitRefusal(held.refused);
305
+ }
306
+ connection.unref();
307
+ let next = 0;
308
+ function exchange(frame, signal) {
309
+ signal?.throwIfAborted();
310
+ if (connection.destroyed) return Promise.resolve(unavailable());
311
+ return new Promise((resolve, reject) => {
312
+ const id = (next += 1);
313
+ const cancel = () => {
314
+ pending.delete(id);
315
+ settle(id);
316
+ if (!connection.destroyed) writeFrame(connection, { id, cancel: true });
317
+ reject(signal.reason);
318
+ };
319
+ signal?.addEventListener("abort", cancel, { once: true });
320
+ pending.set(id, (answer) => {
321
+ signal?.removeEventListener("abort", cancel);
322
+ resolve(answer);
323
+ });
324
+ connection.ref();
325
+ writeFrame(connection, { id, ...frame });
326
+ });
327
+ }
328
+ async function push(request, signal) {
329
+ const answer = await exchange({ push: request }, signal);
330
+ if (answer.pushed === undefined) throw new KitRefusal("kit_resident_unavailable");
331
+ return answer.pushed;
332
+ }
333
+ async function delegated(url, init = {}) {
334
+ const { authorization, ...headers } = init.headers ?? {};
335
+ if (authorization !== RESIDENT) return fetcher(url, init);
336
+ const address = new URL(url);
337
+ const answer = await exchange(
338
+ {
339
+ method: init.method ?? "GET",
340
+ path: `${address.pathname}${address.search}`,
341
+ headers,
342
+ ...(typeof init.body === "string"
343
+ ? { text: init.body }
344
+ : init.body === undefined
345
+ ? {}
346
+ : { bytes: Buffer.from(init.body).toString("base64") }),
347
+ },
348
+ init.signal,
349
+ );
350
+ return new Response(answer.body === null ? null : Buffer.from(answer.body, "base64"), {
351
+ status: answer.status,
352
+ headers: answer.type === null ? {} : { "content-type": answer.type },
353
+ });
354
+ }
355
+ return { connection, fetch: delegated, push };
356
+ }
357
+
108
358
  export async function openSession(options, dependencies = {}) {
359
+ if (options.resident !== true) refuseDrained(options.directory);
109
360
  const session = {
110
361
  house: options.house,
111
362
  dependencies,
@@ -114,27 +365,40 @@ export async function openSession(options, dependencies = {}) {
114
365
  expiresAt: 0,
115
366
  signal: options.signal,
116
367
  };
368
+ const resident =
369
+ options.acquire === true || options.resident === true
370
+ ? null
371
+ : await residentDelegation(options.directory, dependencies.fetch ?? fetch);
372
+ if (resident !== null) {
373
+ session.resident = resident.connection;
374
+ session.push = resident.push;
375
+ session.dependencies = { ...dependencies, fetch: resident.fetch };
376
+ return session;
377
+ }
378
+ session.dependencies = {
379
+ ...dependencies,
380
+ contractRefused: () => {
381
+ session.refused = true;
382
+ },
383
+ };
117
384
  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
- );
385
+ const answer = await houseRequest(
386
+ session.house,
387
+ `Bearer ${credential}`,
388
+ "/kit/lease",
389
+ {},
390
+ session.dependencies,
391
+ session.signal,
392
+ ).catch((error) => {
393
+ if (options.resident === true && session.refused) return null;
394
+ throw error;
395
+ });
396
+ if (answer !== null) Object.assign(session, heldLease(answer));
131
397
  return session;
132
398
  }
133
399
 
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
- }
400
+ async function renewControl(session, signal) {
401
+ if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
138
402
  Object.assign(
139
403
  session,
140
404
  heldLease(
@@ -148,10 +412,18 @@ export async function heldControl(session, signal = session.signal) {
148
412
  ),
149
413
  ),
150
414
  );
415
+ }
416
+
417
+ export async function heldControl(session, signal = session.signal) {
418
+ if (session.resident !== undefined) return RESIDENT;
419
+ if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
420
+ if (session.expiresAt - session.now().getTime() <= LEASE_RENEWAL_MARGIN_MS) {
421
+ await renewControl(session, signal);
422
+ }
151
423
  return `Bearer ${session.lease}`;
152
424
  }
153
425
 
154
- function controlSleep(milliseconds, signal) {
426
+ export function controlSleep(milliseconds, signal) {
155
427
  return new Promise((resolve, reject) => {
156
428
  if (signal.aborted) {
157
429
  reject(new KitRefusal("kit_command_cancelled"));
@@ -195,9 +467,11 @@ export function mutatingCall(classification) {
195
467
  }
196
468
 
197
469
  export async function releaseControl(session, options = {}) {
470
+ session.resident?.end();
198
471
  if (session.lease === null) return;
199
472
  const held = session.lease;
200
473
  try {
474
+ if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
201
475
  await houseRequest(
202
476
  session.house,
203
477
  `Bearer ${held}`,
@@ -229,6 +503,7 @@ export async function maintainControl(session, operation, signal, { release = tr
229
503
  signal?.addEventListener("abort", cancelOperation, { once: true });
230
504
  });
231
505
  const renewal = (async () => {
506
+ if (session.resident !== undefined) return;
232
507
  const sleep = session.dependencies.controlSleep ?? controlSleep;
233
508
  while (!stopped) {
234
509
  const delay = Math.max(
@@ -245,7 +520,7 @@ export async function maintainControl(session, operation, signal, { release = tr
245
520
  throw new KitRefusal("kit_lease_rejected");
246
521
  }
247
522
  } catch (error) {
248
- if (stopped) return;
523
+ if (stopped || session.refused) return;
249
524
  rejectFailure(error);
250
525
  maintenance.abort();
251
526
  return;
@@ -253,20 +528,14 @@ export async function maintainControl(session, operation, signal, { release = tr
253
528
  }
254
529
  })();
255
530
  const running = Promise.resolve().then(() => operation(operationSignal));
256
- let ended = false;
257
531
  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;
532
+ return await Promise.race([running, failure, cancellation]);
264
533
  } finally {
265
534
  stopped = true;
266
535
  maintenance.abort();
267
536
  signal?.removeEventListener("abort", cancelOperation);
268
537
  await Promise.all([renewal, running.catch(() => undefined)]);
269
- if (ended && release) await releaseControl(session);
538
+ if (release) await releaseControl(session);
270
539
  }
271
540
  }
272
541
 
@@ -479,6 +748,7 @@ export async function reportLogin(session, login, outcome, signal = session.sign
479
748
  }
480
749
 
481
750
  const envelope = declarationReader("kit_acquire_rejected");
751
+ const input = declarationReader("kit_run_input_unreadable");
482
752
 
483
753
  function acquiredRoute(value, declaration) {
484
754
  const route = envelope.plain(value, declaration);
@@ -496,79 +766,82 @@ function acquiredRoute(value, declaration) {
496
766
 
497
767
  function conversationText(value, declaration) {
498
768
  if (value === null) return null;
499
- if (typeof value !== "string" || value.length > 4096) envelope.refuse(declaration);
769
+ if (typeof value !== "string" || value.length > 4096) input.refuse(declaration);
500
770
  return value;
501
771
  }
502
772
 
503
773
  function conversationMessage(value, declaration) {
504
- const message = envelope.plain(value, declaration);
774
+ const message = input.plain(value, declaration);
505
775
  const text = conversationText(message.text, `${declaration}.text`);
506
776
  const caption = conversationText(message.caption, `${declaration}.caption`);
507
- if (!Array.isArray(message.files)) envelope.refuse(`${declaration}.files`);
777
+ if (!Array.isArray(message.files)) input.refuse(`${declaration}.files`);
508
778
  const files = message.files.map((value, index) => {
509
779
  const name = `${declaration}.files[${index}]`;
510
- const file = envelope.plain(value, name);
780
+ const file = input.plain(value, name);
511
781
  if (file.bytes !== null && (!Number.isSafeInteger(file.bytes) || file.bytes < 0)) {
512
- envelope.refuse(`${name}.bytes`);
782
+ input.refuse(`${name}.bytes`);
513
783
  }
514
784
  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),
785
+ path: input.text(file.path, `${name}.path`, 2048),
786
+ media_kind: input.text(file.media_kind, `${name}.media_kind`, 128),
787
+ media_type: file.media_type === null ? null : input.text(file.media_type, `${name}.media_type`, 255),
518
788
  bytes: file.bytes,
519
789
  };
520
790
  });
521
- if (!text && !caption && files.length === 0) envelope.refuse(declaration);
791
+ if (!text && !caption && files.length === 0) input.refuse(declaration);
522
792
  return {
523
- message_id: envelope.text(message.message_id, `${declaration}.message_id`, 128),
793
+ message_id: input.text(message.message_id, `${declaration}.message_id`, 128),
524
794
  text, caption, files,
525
795
  };
526
796
  }
527
797
 
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`);
798
+ function runRequest(answer) {
799
+ const request = input.plain(answer, "request");
800
+ if (request.kind === "conversation") {
801
+ if (request.origin !== "user") input.refuse("request.origin");
802
+ const turn = input.plain(request.turn, "request.turn");
533
803
  return {
804
+ kind: "conversation",
534
805
  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
- },
806
+ turn: {
807
+ id: input.text(turn.id, "request.turn.id", 128),
808
+ sequence: input.positiveCount(turn.sequence, "request.turn.sequence"),
809
+ messages: input.filled(turn.messages, "request.turn.messages")
810
+ .map((message, index) => conversationMessage(message, `request.turn.messages[${index}]`)),
542
811
  },
543
812
  };
544
813
  }
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`);
814
+ if (request.kind !== "routine") input.refuse("request.kind");
815
+ const routine = input.plain(request.routine, "request.routine");
816
+ if (typeof request.payload !== "string" || Array.from(request.payload).length > 20_000) input.refuse("request.payload");
817
+ if (!Array.isArray(request.references)) input.refuse("request.references");
548
818
  return {
549
- origin: envelope.text(acquired.origin, `${declaration}.origin`, 32),
819
+ kind: "routine",
820
+ origin: input.text(request.origin, "request.origin", 32),
550
821
  routine: {
551
- name: envelope.text(routine.name, `${declaration}.routine.name`, 256),
552
- version: envelope.positiveCount(routine.version, `${declaration}.routine.version`),
822
+ name: input.text(routine.name, "request.routine.name", 256),
823
+ version: input.positiveCount(routine.version, "request.routine.version"),
553
824
  },
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),
825
+ instruction: input.text(request.instruction, "request.instruction", 256 * 1024),
826
+ payload: request.payload,
827
+ references: request.references.map((reference, index) => input.plain(reference, `request.references[${index}]`)),
828
+ requested_by: input.text(request.requested_by, "request.requested_by", 128),
829
+ requested_at: input.text(request.requested_at, "request.requested_at", 64),
830
+ deliver_at: request.deliver_at === null ? null : input.text(request.deliver_at, "request.deliver_at", 64),
569
831
  };
570
832
  }
571
833
 
834
+ export async function readRunRequest(session, run, token, signal = session.signal) {
835
+ return runRequest(await houseRequest(
836
+ session.house,
837
+ `Bearer ${token}`,
838
+ runRequestPath(run),
839
+ {},
840
+ session.dependencies,
841
+ signal,
842
+ ));
843
+ }
844
+
572
845
  function acquiredAssignment(value, declaration) {
573
846
  const acquired = envelope.plain(value, declaration);
574
847
  const bridge = envelope.plain(acquired.session, `${declaration}.session`);
@@ -597,7 +870,6 @@ function acquiredAssignment(value, declaration) {
597
870
  token: envelope.text(bridge.bridge_token, `${declaration}.session.bridge_token`, 512),
598
871
  expiresAt: Date.parse(expiresAt),
599
872
  },
600
- request: acquiredRequest(acquired, declaration),
601
873
  };
602
874
  }
603
875
 
package/src/inbound.js CHANGED
@@ -134,13 +134,9 @@ export async function reconcileInbound(directory, session) {
134
134
  }
135
135
 
136
136
  export async function localRequest(directory, run, request) {
137
- const turn = request?.conversation?.turn ?? (request?.kind === "conversation" ? request.turn : null);
138
- if (turn === null || turn === undefined) return request;
137
+ if (request.kind !== "conversation") return request;
139
138
  const result = structuredClone(request);
140
- const localTurn = result.conversation?.turn ?? result.turn;
141
- if (!Array.isArray(localTurn.messages)) throw new KitRefusal("kit_run_input_unreadable");
142
- for (const message of localTurn.messages) {
143
- if (!Array.isArray(message.files)) throw new KitRefusal("kit_run_input_unreadable");
139
+ for (const message of result.turn.messages) {
144
140
  for (const file of message.files) {
145
141
  const parts = typeof file.path === "string" ? file.path.split("/") : [];
146
142
  if (parts.length !== 4 || parts[0] !== ROOT || parts[1] !== run || !UUID.test(parts[1]) || !UUID.test(parts[2]) || !segment(parts[3])) {
@@ -161,15 +157,3 @@ export async function localRequest(directory, run, request) {
161
157
  }
162
158
  return result;
163
159
  }
164
-
165
- export async function localReply(directory, run, frame, reply) {
166
- if (frame.method !== "tools/call" || frame.params?.name !== "session_get_request" || reply.error || reply.result?.isError) return reply;
167
- const result = structuredClone(reply);
168
- for (const item of result.result?.content ?? []) {
169
- if (item.type !== "text") continue;
170
- let input;
171
- try { input = JSON.parse(item.text); } catch { throw new KitRefusal("kit_run_input_unreadable"); }
172
- item.text = JSON.stringify(await localRequest(directory, run, input));
173
- }
174
- return result;
175
- }
@@ -0,0 +1,88 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import WebSocket from "ws";
3
+ import { KIT_CONTRACT_VERSION } from "./contract.js";
4
+ import { heldControl } from "./house.js";
5
+ import { KitRefusal } from "./refusal.js";
6
+
7
+ const PROTOCOL = "house.kit.stream.1";
8
+ const WATCHED_SUBJECTS = ["agents", "working_copy"];
9
+
10
+ function streamAddress(house) {
11
+ const url = new URL("/kit/stream", house);
12
+ if (url.protocol === "https:") url.protocol = "wss:";
13
+ else if (url.protocol === "http:") url.protocol = "ws:";
14
+ else throw new KitRefusal("kit_house_unavailable", "/kit/stream");
15
+ return url;
16
+ }
17
+
18
+ async function connected(session, signal, available, dependencies) {
19
+ const authorization = await heldControl(session, signal);
20
+ return new Promise((resolve, reject) => {
21
+ const Socket = dependencies.workSocket ?? WebSocket;
22
+ const socket = new Socket(streamAddress(session.house), PROTOCOL, {
23
+ headers: { authorization, "x-house-kit-version": KIT_CONTRACT_VERSION },
24
+ handshakeTimeout: 10_000,
25
+ maxPayload: 4096,
26
+ perMessageDeflate: false,
27
+ });
28
+ let finished = false;
29
+ const finish = (error = null) => {
30
+ if (finished) return;
31
+ finished = true;
32
+ signal.removeEventListener("abort", stop);
33
+ if (error === null) resolve();
34
+ else reject(error);
35
+ };
36
+ const stop = () => {
37
+ socket.terminate();
38
+ finish();
39
+ };
40
+ signal.addEventListener("abort", stop, { once: true });
41
+ socket.on("open", () => available(null));
42
+ socket.on("message", (data) => {
43
+ let frame;
44
+ try {
45
+ frame = JSON.parse(data.toString());
46
+ } catch {
47
+ return;
48
+ }
49
+ if (frame?.type === "work_available" && WATCHED_SUBJECTS.includes(frame.subject)) {
50
+ available(frame.subject);
51
+ }
52
+ });
53
+ socket.on("unexpected-response", (_request, response) => {
54
+ response.resume();
55
+ if (response.statusCode === 409) session.dependencies.contractRefused?.();
56
+ const refusal =
57
+ response.statusCode === 409 ? new KitRefusal("kit_upgrade_required") : null;
58
+ socket.terminate();
59
+ finish(refusal);
60
+ });
61
+ socket.on("error", () => {
62
+ socket.terminate();
63
+ finish();
64
+ });
65
+ socket.on("close", () => finish());
66
+ if (signal.aborted) stop();
67
+ });
68
+ }
69
+
70
+ export async function watchKitWork(session, signal, available, dependencies = {}) {
71
+ let retryMs = 500;
72
+ while (!signal.aborted) {
73
+ const began = Date.now();
74
+ try {
75
+ await connected(session, signal, available, dependencies);
76
+ } catch (error) {
77
+ if (error instanceof KitRefusal && error.code === "kit_upgrade_required") throw error;
78
+ if (signal.aborted) return;
79
+ }
80
+ if (signal.aborted) return;
81
+ retryMs = Date.now() - began >= 30_000 ? 1000 : Math.min(retryMs * 2, 30_000);
82
+ try {
83
+ await delay(dependencies.workReconnectMs ?? retryMs, undefined, { signal });
84
+ } catch (error) {
85
+ if (!signal.aborted) throw error;
86
+ }
87
+ }
88
+ }