@agentshouse/kit 0.1.0-alpha.3 → 0.1.0-alpha.31

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 +722 -263
  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 +129 -41
  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 +685 -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.31",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,11 +9,16 @@
9
9
  "private": false,
10
10
  "type": "module",
11
11
  "bin": {
12
+ "house": "src/session-bin/house",
12
13
  "kit": "bin/kit.js"
13
14
  },
15
+ "exports": {
16
+ "./bootstrap": "./bootstrap.json"
17
+ },
14
18
  "dependencies": {
15
19
  "@agentclientprotocol/sdk": "1.5.0",
16
- "@agentshouse/mdmodel": "0.1.0-alpha.13",
20
+ "@agentshouse/mdmodel": "0.1.0-alpha.15",
21
+ "ws": "8.22.0",
17
22
  "yaml": "2.9.1",
18
23
  "zod": "4.6.5"
19
24
  },
@@ -1,7 +1,15 @@
1
- import { COMMAND_ADAPTER_INITIALIZE_TIMEOUT_MS, MCP_PROTOCOL_VERSION } from "./contract.js";
2
- import { bridgeCall } from "./mcp.js";
1
+ import { readFileSync } from "node:fs";
2
+ import {
3
+ COMMAND_ADAPTER_INITIALIZE_TIMEOUT_MS,
4
+ HOW_WE_WORK_PATH,
5
+ MCP_PROTOCOL_VERSION,
6
+ SHELL_RENDER,
7
+ } from "./contract.js";
8
+ import { answerText, bridgeCall, editRefusal } from "./mcp.js";
3
9
  import { KitRefusal } from "./refusal.js";
4
10
 
11
+ const HOW_WE_WORK_TEXT = readFileSync(new URL("how-we-work.md", import.meta.url), "utf8");
12
+
5
13
  function readableDiscovery(discovery) {
6
14
  const identity = discovery?._meta?.["io.modelcontextprotocol/serverInfo"];
7
15
  return Array.isArray(discovery?.supportedVersions) &&
@@ -30,15 +38,71 @@ export async function commandHouseInstructions(bridge) {
30
38
  return frame.result.instructions;
31
39
  }
32
40
 
33
- export async function commandHouseRequest(bridge) {
34
- const frame = await commandHouseReply(bridge, { method: "tools/call", params: { name: "session_get_request", arguments: {} } });
35
- if (frame.error !== undefined || frame.result?.isError === true) throw new KitRefusal("kit_run_input_unreadable");
36
- try {
37
- const content = frame.result.content;
38
- const input = JSON.parse(content.find((item) => item.type === "text").text);
39
- if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error();
40
- return input;
41
- } catch {
42
- throw new KitRefusal("kit_run_input_unreadable");
41
+ function shellAnswer(text) {
42
+ const lines = text.split("\n");
43
+ if (!lines[0].startsWith(`protocol: render=${SHELL_RENDER} `)) return null;
44
+ const exit = Number(lines[2].slice("exit: ".length));
45
+ let at = lines[5]?.startsWith("authority: ") ? 6 : 5;
46
+ if (exit === 0) return { exit, stderr: [], stdout: lines.slice(at).join("\n"), cursor: null };
47
+ const stderr = [];
48
+ while (lines[at]?.startsWith("stderr: ")) stderr.push(lines[at++].slice("stderr: ".length));
49
+ const stdout = lines.slice(at);
50
+ const cursor = stdout.at(-1)?.startsWith("cursor: ") ? stdout.pop().slice("cursor: ".length) : null;
51
+ return { exit, stderr, stdout: stdout.join("\n"), cursor };
52
+ }
53
+
54
+ async function howWeWorkPage(bridge, cursor) {
55
+ const frame = await bridgeCall(bridge, {
56
+ method: "tools/call",
57
+ params: {
58
+ name: "shell",
59
+ arguments: { command: `cat ${HOW_WE_WORK_PATH}`, ...(cursor === null ? {} : { cursor }) },
60
+ },
61
+ });
62
+ if (frame.error !== undefined) {
63
+ throw new KitRefusal("kit_how_we_work_unreadable", frame.error.message ?? null);
64
+ }
65
+ const text = answerText(frame.result);
66
+ if (frame.result?.isError === true) {
67
+ if (text.split(":")[0] === "room_not_found") return null;
68
+ throw new KitRefusal("kit_how_we_work_unreadable", text);
69
+ }
70
+ const page = shellAnswer(text);
71
+ if (page === null) throw new KitRefusal("kit_how_we_work_unreadable", text);
72
+ if (page.cursor === null && page.exit !== 0) {
73
+ if (page.stderr.length === 1 && page.stderr[0].split(" ")[1] === "path_not_found") return null;
74
+ throw new KitRefusal("kit_how_we_work_unreadable", page.stderr.join("\n"));
75
+ }
76
+ return page;
77
+ }
78
+
79
+ export async function commandHouseHowWeWork(bridge) {
80
+ const pages = [];
81
+ let cursor = null;
82
+ do {
83
+ const page = await howWeWorkPage(bridge, cursor);
84
+ if (page === null) return "";
85
+ pages.push(page.stdout);
86
+ cursor = page.cursor;
87
+ } while (cursor !== null);
88
+ return pages.join("\n");
89
+ }
90
+
91
+ export async function commandHouseCreateHowWeWork(bridge) {
92
+ const frame = await bridgeCall(bridge, {
93
+ method: "tools/call",
94
+ params: {
95
+ name: "edit",
96
+ arguments: { changes: [{ op: "create", path: HOW_WE_WORK_PATH, content: HOW_WE_WORK_TEXT }] },
97
+ },
98
+ });
99
+ if (frame.error !== undefined) {
100
+ throw new KitRefusal("kit_how_we_work_unwritable", frame.error.message ?? null);
43
101
  }
102
+ if (frame.result?.isError !== true) return true;
103
+ const text = answerText(frame.result);
104
+ const { code, conflicts } = editRefusal(text);
105
+ if (code === "room_not_found" || code === "operation_denied") return false;
106
+ if (code === "edit_conflict" && conflicts?.some((conflict) => conflict.code === "path_exists")) return true;
107
+ throw new KitRefusal("kit_how_we_work_unwritable", text);
44
108
  }
package/src/agents.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { readFile, rm } from "node:fs/promises";
2
+ import { readFile, readdir, rm } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import {
@@ -11,6 +11,7 @@ import {
11
11
  } from "./adapter.js";
12
12
  import { inheritedEnvironment } from "./command.js";
13
13
  import {
14
+ ADAPTER_KINDS,
14
15
  AGENT_HOME,
15
16
  AGENT_INSTALL_TIMEOUT_MS,
16
17
  AGENT_RELEASES_HOME,
@@ -261,6 +262,60 @@ export async function updateDeclaredAgents(options, configuration, dependencies
261
262
  return updated;
262
263
  }
263
264
 
265
+ export async function removeUndeclaredAgents(directory, wanted, keep = false) {
266
+ const home = join(directory, AGENT_HOME);
267
+ let entries;
268
+ try {
269
+ entries = await readdir(home, { withFileTypes: true });
270
+ } catch (error) {
271
+ if (error.code === "ENOENT") return [];
272
+ throw error;
273
+ }
274
+ const retained = [];
275
+ for (const entry of entries) {
276
+ if (!wanted.includes(entry.name)) {
277
+ if (keep && ADAPTER_KINDS.includes(entry.name)) retained.push(entry.name);
278
+ else await rm(join(home, entry.name), { recursive: true, force: true });
279
+ }
280
+ }
281
+ return retained;
282
+ }
283
+
284
+ export async function reconcileAgentKinds(
285
+ options,
286
+ session,
287
+ configuration,
288
+ dependencies = {},
289
+ signal = session.signal,
290
+ state = {},
291
+ ) {
292
+ const selected = { ...configuration, agents: await desiredAgents(session, signal) };
293
+ state.onDesired?.(selected.agents);
294
+ const newlyWanted = selected.agents.filter((kind) => !state.lastDesired?.includes(kind));
295
+ const retained = await removeUndeclaredAgents(options.directory, selected.agents, state.busy);
296
+ const pointerKinds = state.busy
297
+ ? (
298
+ await Promise.all(
299
+ newlyWanted.map(async (kind) =>
300
+ (await readAgentState(options.directory, kind)).release === null ? kind : null,
301
+ ),
302
+ )
303
+ ).filter((kind) => kind !== null)
304
+ : newlyWanted;
305
+ const updated = await updateDeclaredAgents(
306
+ { directory: options.directory, at: dependencies.now?.(), forced: state.forced },
307
+ { ...selected, agents: state.pointer ? pointerKinds : selected.agents },
308
+ dependencies,
309
+ );
310
+ const readiness = await agentReadiness(
311
+ { directory: options.directory },
312
+ { ...selected, agents: [...selected.agents, ...retained] },
313
+ dependencies,
314
+ );
315
+ await reportAgentCapabilities(session, readiness.agents, signal);
316
+ return { selected, updated, reported: readiness.agents, refused: readiness.refused, retained };
317
+ }
318
+
264
319
  export async function agentSignedIn(directory, kind, dependencies = {}) {
265
320
  const contract = compiledContract(kind);
266
321
  const state = await readAgentState(directory, kind);
@@ -341,15 +396,15 @@ export async function updateAgents(options, dependencies = {}) {
341
396
  return maintainControl(
342
397
  session,
343
398
  async (signal) => {
344
- const selected = { ...configuration, agents: await desiredAgents(session, signal) };
345
- const updated = await updateDeclaredAgents(
346
- { directory: options.directory, at: dependencies.now?.(), forced: true },
347
- selected,
399
+ const result = await reconcileAgentKinds(
400
+ options,
401
+ session,
402
+ configuration,
348
403
  dependencies,
404
+ signal,
405
+ { forced: true },
349
406
  );
350
- const { agents } = await agentReadiness(options, selected, dependencies);
351
- await reportAgentCapabilities(session, agents, signal);
352
- return { updated, reported: agents };
407
+ return { updated: result.updated, reported: result.reported };
353
408
  },
354
409
  options.signal,
355
410
  );
package/src/binding.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readdir, rm } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { finalPlan, planSnapshot } from "./plan.js";
3
4
  import { KitRefusal } from "./refusal.js";
4
5
  import { readNote, writeNote } from "./state.js";
5
6
  import { reportedNote, retainedReport } from "./terminal.js";
@@ -15,6 +16,18 @@ function bindingPath(directory, tmux) {
15
16
  return join(bindingDirectory(directory), `${tmux}.json`);
16
17
  }
17
18
 
19
+ function stoppedNote(stopped) {
20
+ if (stopped === null || stopped === undefined) return null;
21
+ return {
22
+ turn_completed: stopped.turnCompleted,
23
+ capacity_exhausted: stopped.capacityExhausted,
24
+ provider_refused: stopped.providerRefused,
25
+ continuable: stopped.continuable,
26
+ failure: stopped.failure,
27
+ text_accepted: stopped.textAccepted,
28
+ };
29
+ }
30
+
18
31
  export async function writeBinding(directory, binding) {
19
32
  const reported = binding.reported ?? null;
20
33
  await writeNote(bindingPath(directory, binding.tmux), {
@@ -29,7 +42,9 @@ export async function writeBinding(directory, binding) {
29
42
  sealed: binding.sealed === null || binding.sealed === undefined
30
43
  ? null
31
44
  : { operation: binding.sealed.operation, blocks: binding.sealed.blocks },
45
+ stopped: stoppedNote(binding.stopped),
32
46
  reported: reported === null ? null : reportedNote(reported),
47
+ plan: binding.plan ?? null,
33
48
  });
34
49
  }
35
50
 
@@ -45,6 +60,37 @@ function retainedSeal(stored) {
45
60
  return { operation: stored.operation, blocks: stored.blocks };
46
61
  }
47
62
 
63
+ function retainedPlan(stored) {
64
+ if (stored === null || stored === undefined) return null;
65
+ const plan = finalPlan(planSnapshot(stored));
66
+ return plan === null || JSON.stringify(plan) !== JSON.stringify(stored) ? undefined : plan;
67
+ }
68
+
69
+ function retainedFailure(stored) {
70
+ if (stored === null) return null;
71
+ if (typeof stored !== "object" || Array.isArray(stored)) return undefined;
72
+ if (typeof stored.code !== "string" || stored.code.length === 0) return undefined;
73
+ if (stored.detail !== null && typeof stored.detail !== "string") return undefined;
74
+ return { code: stored.code, detail: stored.detail };
75
+ }
76
+
77
+ function retainedStop(stored) {
78
+ if (stored === null || stored === undefined) return null;
79
+ if (typeof stored !== "object" || Array.isArray(stored)) return undefined;
80
+ const flags = ["turn_completed", "capacity_exhausted", "provider_refused", "continuable", "text_accepted"];
81
+ if (flags.some((flag) => typeof stored[flag] !== "boolean")) return undefined;
82
+ const failure = retainedFailure(stored.failure);
83
+ if (failure === undefined) return undefined;
84
+ return {
85
+ turnCompleted: stored.turn_completed,
86
+ capacityExhausted: stored.capacity_exhausted,
87
+ providerRefused: stored.provider_refused,
88
+ continuable: stored.continuable,
89
+ failure,
90
+ textAccepted: stored.text_accepted,
91
+ };
92
+ }
93
+
48
94
  function completeBinding(directory, stored) {
49
95
  if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return null;
50
96
  const named = [stored.run, stored.session, stored.tmux, stored.socket, stored.nonce];
@@ -54,8 +100,12 @@ function completeBinding(directory, stored) {
54
100
  if (stored.owner_stop !== null && typeof stored.owner_stop !== "string") return null;
55
101
  const sealed = retainedSeal(stored.sealed);
56
102
  if (sealed === undefined) return null;
103
+ const stopped = retainedStop(stored.stopped);
104
+ if (stopped === undefined) return null;
57
105
  const reported = retainedReport(stored.reported);
58
106
  if (reported === undefined) return null;
107
+ const plan = retainedPlan(stored.plan);
108
+ if (plan === undefined) return null;
59
109
  return {
60
110
  run: stored.run,
61
111
  session: stored.session,
@@ -65,7 +115,9 @@ function completeBinding(directory, stored) {
65
115
  conversation: stored.conversation === true,
66
116
  ownerStop: stored.owner_stop,
67
117
  sealed,
118
+ stopped,
68
119
  reported,
120
+ plan,
69
121
  };
70
122
  }
71
123
 
package/src/blocks.js CHANGED
@@ -1,8 +1,4 @@
1
- import {
2
- ASSISTANT_BLOCKS_MAX,
3
- ASSISTANT_TABLE_COLUMNS_MAX,
4
- ASSISTANT_TEXT_MAX_BYTES,
5
- } from "./contract.js";
1
+ import { ASSISTANT_TABLE_COLUMNS_MAX } from "./contract.js";
6
2
 
7
3
  const HEADING = /^(#{1,6})\s+(.+)$/;
8
4
  const BULLET = /^ {0,3}[-*+]\s+(.+)$/;
@@ -132,25 +128,5 @@ export function assistantBlocks(text) {
132
128
  index += 1;
133
129
  }
134
130
  endParagraph();
135
- if (blocks.length === 0) return null;
136
- if (blocks.length <= ASSISTANT_BLOCKS_MAX) return blocks;
137
- const whole = text.trim();
138
- return whole === "" ? null : [{ type: "paragraph", text: whole }];
139
- }
140
-
141
- export function blocksText(blocks) {
142
- return blocks
143
- .map((block) => {
144
- if (block.type === "formula") return block.expression;
145
- if (block.type === "list") return block.items.join("\n");
146
- if (block.type === "table") {
147
- return [block.headers, ...block.rows].map((row) => row.join("\t")).join("\n");
148
- }
149
- return block.text;
150
- })
151
- .join("\n\n");
152
- }
153
-
154
- export function withinAssistantBound(text) {
155
- return Buffer.byteLength(text, "utf8") <= ASSISTANT_TEXT_MAX_BYTES;
131
+ return blocks.length === 0 ? null : blocks;
156
132
  }
package/src/bridge.js CHANGED
@@ -16,9 +16,14 @@ import {
16
16
  TURN_OUTCOMES,
17
17
  } from "./contract.js";
18
18
  import { operationId } from "./operation.js";
19
+ import { planSnapshot } from "./plan.js";
19
20
  import { houseFailed } from "./refusal.js";
20
21
 
21
- function bridgedFrame(request, operation) {
22
+ function sequenced(value) {
23
+ return Number.isSafeInteger(value) && value > 0;
24
+ }
25
+
26
+ export function bridgedFrame(request, operation) {
22
27
  if (typeof request.method !== "string") return request;
23
28
  const params = typeof request.params === "object" && request.params !== null ? request.params : {};
24
29
  const declared = typeof params._meta === "object" && params._meta !== null ? params._meta : {};
@@ -50,6 +55,7 @@ export async function openBridge(options) {
50
55
  closed: false,
51
56
  questions: new Map(),
52
57
  interrupts: new Set(),
58
+ plan: null,
53
59
  };
54
60
  const connections = new Set();
55
61
  const clients = new Map();
@@ -201,38 +207,45 @@ export async function openBridge(options) {
201
207
  return;
202
208
  }
203
209
  if (frame.type === "draft") {
204
- if (typeof frame.text === "string" && frame.text !== "") options.draft?.(frame.text);
210
+ if (typeof frame.text === "string" && frame.text !== "" && sequenced(frame.sequence)) {
211
+ options.draft?.({ text: frame.text, sequence: frame.sequence });
212
+ }
205
213
  return;
206
214
  }
207
- if (
208
- frame.type === "plan" &&
209
- typeof frame.provider_turn === "string" &&
210
- Array.isArray(frame.steps) &&
211
- typeof frame.terminal === "boolean"
212
- ) {
213
- void options.plan?.({
214
- provider_turn: frame.provider_turn,
215
- steps: frame.steps,
216
- terminal: frame.terminal,
217
- });
215
+ if (frame.type === "plan") {
216
+ const snapshot = planSnapshot(frame);
217
+ if (snapshot === null || !sequenced(frame.sequence)) return;
218
+ state.plan = snapshot;
219
+ options.plan?.({ ...snapshot, sequence: frame.sequence });
218
220
  return;
219
221
  }
222
+ const carried = planSnapshot(frame.plan);
223
+ if (carried !== null) state.plan = carried;
220
224
  if (
221
225
  frame.type === "interrupt_applied" &&
222
226
  typeof frame.interrupt === "string" &&
223
227
  ["interrupted", "nothing_to_interrupt"].includes(frame.outcome) &&
224
228
  state.interrupts.has(frame.interrupt)
225
229
  ) {
226
- const reported = options.interrupt?.(frame.interrupt, frame.outcome);
230
+ const reported = options.interrupt?.(
231
+ frame.interrupt,
232
+ frame.outcome,
233
+ frame.outcome === "interrupted" ? state.plan : null,
234
+ );
227
235
  if (reported === undefined) state.interrupts.delete(frame.interrupt);
228
236
  else void Promise.resolve(reported).finally(() => state.interrupts.delete(frame.interrupt));
229
237
  return;
230
238
  }
239
+ if (frame.type === "how_we_work") {
240
+ void options.howWeWork?.();
241
+ return;
242
+ }
231
243
  if (frame.type === "turn_ended") {
232
244
  state.stop = {
233
245
  providerContinuation: frame.provider_continuation === true,
234
246
  providerSessionId: typeof frame.provider_session_id === "string" && frame.provider_session_id.length > 0 && frame.provider_session_id.length <= 512 ? frame.provider_session_id : null,
235
247
  stopOutcome: TURN_OUTCOMES.includes(frame.stop_outcome) ? frame.stop_outcome : null,
248
+ plan: carried,
236
249
  };
237
250
  return;
238
251
  }
@@ -432,6 +445,7 @@ export async function openBridge(options) {
432
445
  .filter((interaction) => interaction !== null),
433
446
  assistant: () => [...state.assistant],
434
447
  stop: () => state.stop,
448
+ plan: () => state.plan,
435
449
  failure: () => state.failure,
436
450
  stopRecovery: () => lifetime.abort(),
437
451
  async close() {
package/src/bytes.js CHANGED
@@ -31,6 +31,11 @@ async function downloadBody(response, signal) {
31
31
  return Buffer.concat(chunks, size);
32
32
  }
33
33
 
34
+ function framedBody(request) {
35
+ if (request.method !== "POST" || !Buffer.isBuffer(request.body)) throw new KitRefusal("kit_byte_body_invalid");
36
+ return Buffer.concat([Buffer.from(`${request.operation}\n`, "utf8"), request.body]);
37
+ }
38
+
34
39
  export async function transferBytes(request, dependencies = {}) {
35
40
  if (request.method !== "GET" && request.method !== "POST") {
36
41
  throw new KitRefusal("kit_byte_method_invalid", request.method);
@@ -38,26 +43,32 @@ export async function transferBytes(request, dependencies = {}) {
38
43
  if (typeof request.url !== "string" || request.url.length === 0) {
39
44
  throw new KitRefusal("kit_byte_url_invalid");
40
45
  }
41
- if (typeof request.operation !== "string" || request.operation.length === 0) {
46
+ if (request.method === "POST" && (typeof request.operation !== "string" || request.operation.length === 0)) {
42
47
  throw new KitRefusal("kit_byte_operation_invalid");
43
48
  }
49
+ const framed = request.framed === true;
50
+ const body = framed ? framedBody(request) : request.body;
44
51
  if (request.method === "POST") {
45
- if (!Buffer.isBuffer(request.body)) throw new KitRefusal("kit_byte_body_invalid");
46
- if (request.body.length > BYTE_GRANT_POST_BYTES_MAX) {
52
+ if (!Buffer.isBuffer(body)) throw new KitRefusal("kit_byte_body_invalid");
53
+ if (body.length > BYTE_GRANT_POST_BYTES_MAX) {
47
54
  throw new KitRefusal("original_bytes_exceeded");
48
55
  }
49
56
  }
50
57
  const fetcher = dependencies.fetch ?? fetch;
51
- const headers = {
52
- [BYTE_GRANT_OPERATION_HEADER]: request.operation,
53
- };
54
- if (request.method === "POST") headers["content-type"] = "application/octet-stream";
58
+ const headers = framed
59
+ ? { "content-type": "text/plain; charset=utf-8" }
60
+ : request.method === "POST"
61
+ ? {
62
+ [BYTE_GRANT_OPERATION_HEADER]: request.operation,
63
+ "content-type": "application/octet-stream",
64
+ }
65
+ : {};
55
66
  let response;
56
67
  try {
57
68
  response = await fetcher(request.url, {
58
69
  method: request.method,
59
70
  headers,
60
- body: request.method === "POST" ? request.body : undefined,
71
+ body: request.method === "POST" ? body : undefined,
61
72
  redirect: "manual",
62
73
  signal: request.signal,
63
74
  });