@agentshouse/kit 0.2.1-c2c600f2d2b30 → 0.2.1-c2fb1c24bcdd5

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 (135) hide show
  1. package/README.md +42 -41
  2. package/dist/acp.js +50 -106
  3. package/dist/agents.js +149 -43
  4. package/dist/api.js +26 -4
  5. package/dist/app-git.js +48 -0
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +37 -59
  8. package/dist/building.js +46 -0
  9. package/dist/calls.js +63 -0
  10. package/dist/chatgpt.js +32 -0
  11. package/dist/clis-main.js +2 -2
  12. package/dist/clis.js +156 -78
  13. package/dist/codex-main.js +58 -0
  14. package/dist/connection.js +126 -0
  15. package/dist/conversations.js +623 -325
  16. package/dist/copies.js +188 -126
  17. package/dist/end.js +24 -5
  18. package/dist/enrol-main.js +7 -1
  19. package/dist/events.js +525 -0
  20. package/dist/files.js +139 -17
  21. package/dist/git.js +13 -11
  22. package/dist/home.js +96 -7
  23. package/dist/house-cli.js +299 -0
  24. package/dist/house-main.js +7 -159
  25. package/dist/instructions.js +7 -9
  26. package/dist/kit-cli.js +68 -0
  27. package/dist/kit-main.js +8 -19
  28. package/dist/login.js +117 -104
  29. package/dist/logout.js +14 -13
  30. package/dist/mcp.js +4 -2
  31. package/dist/parts.js +82 -0
  32. package/dist/placement.js +39 -0
  33. package/dist/refusals.js +12 -0
  34. package/dist/relay.js +33 -2
  35. package/dist/resident.js +112 -50
  36. package/dist/scope.js +206 -0
  37. package/dist/secret-input.js +2 -2
  38. package/dist/service-main.js +9 -0
  39. package/dist/service.js +55 -0
  40. package/dist/shell.js +3 -0
  41. package/dist/sign-in.js +40 -33
  42. package/dist/skills.js +20 -32
  43. package/dist/transcription.js +129 -43
  44. package/dist/translate.js +2 -8
  45. package/dist/update.js +98 -53
  46. package/dist/web-wizard.js +101 -0
  47. package/dist/wire.js +22 -0
  48. package/dist/work.js +346 -0
  49. package/package.json +15 -15
  50. package/dist/blocks.js +0 -51
  51. package/dist/bootstrap.json +0 -24
  52. package/dist/stream.js +0 -72
  53. package/how-we-work.md +0 -23
  54. package/skills/ask-matt/LICENSE +0 -21
  55. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  56. package/skills/ask-matt/SKILL.md +0 -91
  57. package/skills/ask-matt/agents/openai.yaml +0 -4
  58. package/skills/code-review/LICENSE +0 -21
  59. package/skills/code-review/SKILL.md +0 -89
  60. package/skills/code-review/agents/openai.yaml +0 -3
  61. package/skills/codebase-design/DEEPENING.md +0 -37
  62. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  63. package/skills/codebase-design/LICENSE +0 -21
  64. package/skills/codebase-design/SKILL.md +0 -116
  65. package/skills/codebase-design/agents/openai.yaml +0 -3
  66. package/skills/diagnosing-bugs/LICENSE +0 -21
  67. package/skills/diagnosing-bugs/SKILL.md +0 -140
  68. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  69. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  70. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  71. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  72. package/skills/domain-modeling/LICENSE +0 -21
  73. package/skills/domain-modeling/SKILL.md +0 -76
  74. package/skills/domain-modeling/agents/openai.yaml +0 -3
  75. package/skills/grill-me/LICENSE +0 -21
  76. package/skills/grill-me/SKILL.md +0 -8
  77. package/skills/grill-me/agents/openai.yaml +0 -4
  78. package/skills/grill-with-docs/LICENSE +0 -21
  79. package/skills/grill-with-docs/SKILL.md +0 -8
  80. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  81. package/skills/grilling/LICENSE +0 -21
  82. package/skills/grilling/SKILL.md +0 -30
  83. package/skills/grilling/agents/openai.yaml +0 -3
  84. package/skills/handoff/LICENSE +0 -21
  85. package/skills/handoff/SKILL.md +0 -17
  86. package/skills/handoff/agents/openai.yaml +0 -4
  87. package/skills/implement/LICENSE +0 -21
  88. package/skills/implement/SKILL.md +0 -16
  89. package/skills/implement/agents/openai.yaml +0 -4
  90. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  91. package/skills/improve-codebase-architecture/LICENSE +0 -21
  92. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  93. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  94. package/skills/prototype/LICENSE +0 -21
  95. package/skills/prototype/LOGIC.md +0 -67
  96. package/skills/prototype/SKILL.md +0 -28
  97. package/skills/prototype/UI.md +0 -112
  98. package/skills/prototype/agents/openai.yaml +0 -3
  99. package/skills/research/LICENSE +0 -21
  100. package/skills/research/SKILL.md +0 -14
  101. package/skills/research/agents/openai.yaml +0 -3
  102. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  103. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  104. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  105. package/skills/tdd/LICENSE +0 -21
  106. package/skills/tdd/SKILL.md +0 -40
  107. package/skills/tdd/agents/openai.yaml +0 -3
  108. package/skills/tdd/mocking.md +0 -59
  109. package/skills/tdd/tests.md +0 -77
  110. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  111. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  112. package/skills/teach/LICENSE +0 -21
  113. package/skills/teach/MISSION-FORMAT.md +0 -31
  114. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  115. package/skills/teach/SKILL.md +0 -141
  116. package/skills/teach/agents/openai.yaml +0 -4
  117. package/skills/to-questionnaire/LICENSE +0 -21
  118. package/skills/to-questionnaire/SKILL.md +0 -55
  119. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  120. package/skills/to-spec/LICENSE +0 -21
  121. package/skills/to-spec/SKILL.md +0 -76
  122. package/skills/to-spec/agents/openai.yaml +0 -4
  123. package/skills/to-tickets/LICENSE +0 -21
  124. package/skills/to-tickets/SKILL.md +0 -106
  125. package/skills/to-tickets/agents/openai.yaml +0 -4
  126. package/skills/wait-what/LICENSE +0 -21
  127. package/skills/wait-what/SKILL.md +0 -8
  128. package/skills/wait-what/agents/openai.yaml +0 -4
  129. package/skills/wayfinder/LICENSE +0 -21
  130. package/skills/wayfinder/SKILL.md +0 -129
  131. package/skills/wayfinder/agents/openai.yaml +0 -4
  132. package/skills/writing-for-agents/LICENSE +0 -21
  133. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  134. package/skills/writing-for-agents/SKILL.md +0 -83
  135. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -1,56 +1,140 @@
1
- import { client, } from '@agentclientprotocol/sdk';
2
- import { randomUUID } from 'node:crypto';
1
+ import { client, RequestError, } from '@agentclientprotocol/sdk';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { unlessAborted } from './abort.js';
6
- import { killMarked, startAdapter, TURN_ENDED, TURN_STARTED } from './acp.js';
7
- import { blocksOf, firstChange } from './blocks.js';
6
+ import { startAdapter } from './acp.js';
7
+ import { finalRefusal, HouseRefusal } from './api.js';
8
8
  import { openBridge } from './bridge.js';
9
9
  import { CLIS } from './clis.js';
10
- import { placeFiles } from './files.js';
10
+ import { answeredFailure, readerFor } from './events.js';
11
+ import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
11
12
  import { agentBase } from './home.js';
12
13
  import { instructions } from './instructions.js';
14
+ import { Parts } from './parts.js';
15
+ import { refusalOf } from './refusals.js';
16
+ import { relayed } from './relay.js';
17
+ import { killMarked, Scope } from './scope.js';
13
18
  import { holdSecretInput } from './secret-input.js';
14
- import { createHowWeWork } from './skills.js';
15
19
  import { transcribe } from './transcription.js';
20
+ import { workFor } from './work.js';
21
+ const SIGNED_OUT = RequestError.authRequired().code;
16
22
  const KILLED = 'the conversation was killed';
23
+ const INTERRUPTED = 'the conversation was interrupted';
24
+ const REFUSED = 'the CLI refused the prompt';
25
+ const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
26
+ const GONE = 'House no longer holds this conversation; start a new one';
27
+ const RETIRED = "this conversation's Agent is retired, so it cannot start";
28
+ const ENDED = {
29
+ conversation_not_found: GONE,
30
+ conversation_historical: RETIRED,
31
+ };
32
+ const NO_START = "the Agent's CLI did not start";
33
+ const NO_SESSION = "the Agent's CLI did not open a session";
34
+ const NO_RESUME = "the Agent's CLI could not resume this conversation";
35
+ const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
36
+ const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
37
+ class NotStarted extends Error {
38
+ }
39
+ class ResumeRefused extends NotStarted {
40
+ }
41
+ async function step(reason, work) {
42
+ try {
43
+ return await work;
44
+ }
45
+ catch (error) {
46
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
47
+ }
48
+ }
49
+ function signedOut(error) {
50
+ return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
51
+ }
52
+ function unopened(reason) {
53
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
54
+ }
55
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
56
+ const STARTING_SHOWN_AFTER_MS = 1_000;
57
+ function unbridged(error) {
58
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
59
+ }
60
+ const EXIT_PLAN_MODE = {
61
+ parse(value) {
62
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
63
+ throw new Error('_x.ai/exit_plan_mode needs an object');
64
+ }
65
+ return value;
66
+ },
67
+ };
68
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
17
69
  function causeOf(error) {
18
70
  return error instanceof Error ? error.message : String(error);
19
71
  }
72
+ function answeredWith(error) {
73
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
74
+ }
75
+ function exitedWith(running, cause) {
76
+ return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
77
+ }
20
78
  function answerMessage(request, response) {
21
79
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
22
80
  }
81
+ async function sentPart(url, bytes, headers) {
82
+ const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
83
+ await answer.arrayBuffer();
84
+ if (!answer.ok)
85
+ throw storeRefusal('upload', answer.status);
86
+ }
23
87
  function logged(error) {
24
88
  process.stderr.write(`kit: ${causeOf(error)}\n`);
25
89
  }
26
- function stop(bridge) {
27
- killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
90
+ function refusal(conversation, error) {
91
+ if (conversation.kills > 0)
92
+ return KILLED;
93
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
94
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
95
+ return causeOf(error);
96
+ }
97
+ function audioOf(result) {
98
+ if (result.isError === true)
99
+ return null;
100
+ const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
101
+ return file?.media_type?.startsWith('audio/') === true ? file : null;
102
+ }
103
+ function marker(bridge) {
104
+ return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
28
105
  }
29
106
  class Turn {
30
107
  id = randomUUID();
31
- text = '';
32
- segment = null;
33
- admitted = false;
34
- notes = [];
35
- sent = [];
36
- unsentFrom = null;
37
- draftSequence = 0;
38
- planSequence = 0;
39
- entries = null;
108
+ parts;
40
109
  questions = 0;
41
110
  secrets = 0;
42
- prompt = null;
111
+ prompted = false;
112
+ taken = null;
43
113
  ended = false;
114
+ images = 0;
115
+ uploads = [];
116
+ constructor(emit) {
117
+ this.parts = new Parts(emit);
118
+ }
44
119
  }
45
120
  class Conversation {
46
121
  id;
47
122
  running = null;
123
+ session = null;
48
124
  turn = null;
49
125
  commands = null;
50
- jobs = new Set();
51
- queued = [];
52
- late = [];
126
+ options = null;
127
+ exitUntold = false;
128
+ context = null;
129
+ leftovers = new Set();
130
+ waiting = [];
131
+ sending = null;
132
+ subagents = new Map();
133
+ held = [];
53
134
  opening = null;
135
+ bridge = null;
136
+ launch = null;
137
+ prompted = null;
54
138
  kills = 0;
55
139
  killed = new AbortController();
56
140
  interrupts = 0;
@@ -58,6 +142,11 @@ class Conversation {
58
142
  open = 0;
59
143
  queue = Promise.resolve();
60
144
  reports = Promise.resolve();
145
+ starting = null;
146
+ preparing = null;
147
+ told = null;
148
+ untold = false;
149
+ shown = null;
61
150
  constructor(id) {
62
151
  this.id = id;
63
152
  }
@@ -73,55 +162,80 @@ function asksSecret(request) {
73
162
  export class Conversations {
74
163
  kit;
75
164
  conversations = new Map();
76
- acks = new Map();
77
165
  questions = new Map();
78
- carrying = 0;
79
166
  reporting = 0;
167
+ watched = false;
80
168
  constructor(kit) {
81
169
  this.kit = kit;
82
- }
83
- input(input) {
84
- this.carrying++;
85
- const carried = this.acks.get(input.input_id) ??
86
- (() => {
87
- const conversation = this.conversation(input.conversation_id);
88
- if (input.kind === 'kill') {
89
- conversation.kills++;
90
- conversation.killed.abort(new Error(KILLED));
91
- this.halt(conversation);
92
- }
93
- if (input.kind === 'interrupt') {
94
- conversation.interrupts++;
95
- conversation.interrupted.abort();
96
- }
97
- const work = conversation.queue.then(() => this.carry(conversation, input));
98
- conversation.queue = work.catch(() => undefined);
99
- this.acks.set(input.input_id, work);
100
- return work;
101
- })();
102
- carried
103
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
104
- .catch(logged)
105
- .finally(() => {
106
- this.carrying--;
107
- this.kit.changed();
170
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
171
+ setInterval(() => this.look(), 2_000);
172
+ }
173
+ call(call) {
174
+ void this.kit.calls.carry(call.input_id, () => {
175
+ const conversation = this.conversation(call.conversation_id);
176
+ if (call.kind === 'kill') {
177
+ conversation.kills++;
178
+ conversation.killed.abort(new Error(KILLED));
179
+ for (const waiting of conversation.waiting.splice(0))
180
+ waiting.settle({ refused: KILLED });
181
+ this.halt(conversation);
182
+ }
183
+ if (call.kind === 'interrupt') {
184
+ conversation.interrupts++;
185
+ conversation.interrupted.abort();
186
+ }
187
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
188
+ conversation.queue = handled.catch(() => undefined);
189
+ return handled.then(async ({ result }) => ({ result: await result }));
108
190
  });
109
191
  }
110
192
  idle() {
111
- if (this.carrying > 0 || this.reporting > 0)
193
+ if (this.reporting > 0)
112
194
  return false;
113
195
  for (const conversation of this.conversations.values()) {
114
196
  const turn = conversation.turn;
115
197
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
116
- if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
198
+ if (conversation.open > (asking ? 1 : 0))
117
199
  return false;
118
200
  }
119
- return true;
201
+ const conversations = [...this.conversations.values()];
202
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
203
+ return false;
204
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
205
+ }
206
+ quiet() {
207
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
208
+ }
209
+ look() {
210
+ for (const conversation of this.conversations.values()) {
211
+ for (const scope of conversation.leftovers) {
212
+ if (!scope.empty())
213
+ continue;
214
+ scope.close();
215
+ conversation.leftovers.delete(scope);
216
+ }
217
+ }
218
+ this.kit.changed();
120
219
  }
121
220
  opened() {
221
+ this.watched = false;
222
+ for (const conversation of this.conversations.values()) {
223
+ if (conversation.running !== null)
224
+ this.facts(conversation);
225
+ else if (conversation.exitUntold)
226
+ this.process(conversation, false);
227
+ if (conversation.untold || conversation.told !== null)
228
+ this.retell(conversation);
229
+ }
230
+ }
231
+ watching(watched) {
232
+ const resend = watched && !this.watched;
233
+ this.watched = watched;
234
+ if (!resend)
235
+ return;
122
236
  for (const conversation of this.conversations.values()) {
123
237
  if (conversation.turn !== null)
124
- conversation.turn.unsentFrom = 0;
238
+ this.replay(conversation, conversation.turn);
125
239
  }
126
240
  }
127
241
  conversation(id) {
@@ -132,93 +246,249 @@ export class Conversations {
132
246
  }
133
247
  return conversation;
134
248
  }
135
- async carry(conversation, input) {
136
- if (input.kind === 'open')
137
- return this.open(conversation, input);
138
- if (input.kind === 'message')
139
- return this.message(conversation, input);
140
- if (input.kind === 'interrupt') {
249
+ async carry(conversation, call) {
250
+ if (call.kind === 'open')
251
+ return { result: this.open(conversation, call) };
252
+ if (call.kind === 'message')
253
+ return this.message(conversation, call);
254
+ await this.launched(conversation);
255
+ if (call.kind === 'interrupt') {
141
256
  if (--conversation.interrupts === 0)
142
257
  conversation.interrupted = new AbortController();
143
- await this.interrupt(conversation, String(input.turn_id));
258
+ await this.interrupt(conversation, String(call.turn_id));
144
259
  }
145
- if (input.kind === 'kill') {
260
+ if (call.kind === 'kill') {
146
261
  await this.kill(conversation);
262
+ await conversation.sending;
147
263
  if (--conversation.kills === 0)
148
264
  conversation.killed = new AbortController();
149
265
  }
150
- if (input.kind === 'option')
151
- await this.option(conversation, String(input.option), input.value);
152
- if (input.kind === 'answer') {
153
- const question = this.questions.get(String(input.interaction_id));
266
+ if (call.kind === 'option')
267
+ await this.option(conversation, String(call.option), call.value);
268
+ if (call.kind === 'answer') {
269
+ const question = this.questions.get(String(call.interaction_id));
154
270
  if (question === undefined) {
155
- return this.message(conversation, { ...input, text: answerMessage(input.request, input.response), files: [], first: false });
271
+ const text = answerMessage(call.request, call.response);
272
+ return this.message(conversation, { ...call, text, first: false, message_id: null });
156
273
  }
157
- question.answer(input.response);
274
+ question.answer(call.response);
158
275
  }
159
- return {};
276
+ return { result: Promise.resolve({}) };
160
277
  }
161
- async open(conversation, input) {
278
+ open(conversation, call) {
162
279
  if (conversation.running !== null)
163
- return {};
280
+ return Promise.resolve({});
281
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
282
+ return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
283
+ }
284
+ launch(conversation, route, sessionId, empty) {
285
+ let opened;
286
+ let failed;
287
+ const bridge = new Promise((resolve, reject) => {
288
+ opened = resolve;
289
+ failed = reject;
290
+ });
291
+ bridge.catch(() => undefined);
292
+ const session = this.start(conversation, route, sessionId, empty, opened);
293
+ session.catch(failed);
294
+ const launch = { bridge, session };
295
+ conversation.launch = launch;
296
+ void session
297
+ .catch(() => undefined)
298
+ .then(() => {
299
+ if (conversation.launch !== launch)
300
+ return;
301
+ conversation.launch = null;
302
+ this.closeIdleBridge(conversation);
303
+ this.next(conversation);
304
+ });
305
+ this.next(conversation);
306
+ return launch;
307
+ }
308
+ async launched(conversation) {
309
+ while (conversation.launch !== null)
310
+ await conversation.launch.session.catch(() => undefined);
311
+ }
312
+ instructed(conversation, route) {
313
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
314
+ if (bridge === undefined)
315
+ return null;
316
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
317
+ read.catch(() => undefined);
318
+ return read;
319
+ }
320
+ async message(conversation, call) {
321
+ const content = [{ type: 'text', text: String(call.text) }];
322
+ let route;
323
+ let instructing = null;
164
324
  try {
165
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
325
+ route = await this.route(conversation, call.agent_id);
326
+ if (call.first === true)
327
+ instructing = this.instructed(conversation, route);
328
+ if (call.message_id !== null)
329
+ content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
330
+ if (conversation.kills > 0)
331
+ throw new Error(KILLED);
166
332
  }
167
333
  catch (error) {
168
- return { refused: causeOf(error) };
334
+ this.closeIdleBridge(conversation);
335
+ this.prepares(conversation, null);
336
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
337
+ }
338
+ conversation.preparing = null;
339
+ return {
340
+ result: new Promise((settle) => {
341
+ conversation.waiting.push({ call, route, content, instructing, settle });
342
+ this.next(conversation);
343
+ }),
344
+ };
345
+ }
346
+ next(conversation) {
347
+ if (conversation.sending !== null || conversation.open > 0)
348
+ return;
349
+ const waiting = conversation.waiting.shift();
350
+ if (waiting === undefined) {
351
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
352
+ if (conversation.starting !== step)
353
+ this.starts(conversation, step);
354
+ return;
169
355
  }
356
+ conversation.sending = this.send(conversation, waiting).finally(() => {
357
+ conversation.sending = null;
358
+ this.next(conversation);
359
+ });
170
360
  }
171
- async message(conversation, input) {
172
- let ack = {};
361
+ async send(conversation, waiting) {
362
+ const { call } = waiting;
363
+ let result = {};
364
+ let started = false;
365
+ let instructing = waiting.instructing;
173
366
  const prompt = [];
174
367
  try {
175
- const route = await this.route(conversation, input.agent_id);
176
- const files = input.files;
177
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
178
- const lines = await this.transcribed(conversation, files, paths);
179
368
  if (conversation.running === null) {
180
- const opened = await this.start(conversation, route, input.provider_session_id);
181
- if (input.provider_session_id === null)
182
- ack = { provider_session_id: opened };
369
+ this.starts(conversation, 'session');
370
+ await this.launched(conversation);
371
+ }
372
+ if (conversation.running === null) {
373
+ const stored = conversation.session ?? call.provider_session_id;
374
+ const empty = call.first === true && conversation.prompted !== stored;
375
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
376
+ if (call.first === true)
377
+ instructing = this.instructed(conversation, waiting.route);
378
+ started = (await launch.session) !== stored;
379
+ }
380
+ const session = conversation.running.sessionId;
381
+ if (started || (call.first === true && session !== call.provider_session_id)) {
382
+ result = { provider_session_id: session };
183
383
  }
184
- if (input.first === true) {
185
- prompt.push({
186
- type: 'text',
187
- text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
188
- });
384
+ if (call.first === true) {
385
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
189
386
  }
190
- prompt.push({ type: 'text', text: String(input.text) });
191
- if (lines.length > 0)
192
- prompt.push({ type: 'text', text: lines.join('\n') });
193
387
  if (conversation.kills > 0)
194
388
  throw new Error(KILLED);
195
389
  }
196
390
  catch (error) {
197
- if ('provider_session_id' in ack)
391
+ this.starts(conversation, null);
392
+ if (started) {
393
+ conversation.session = null;
198
394
  await this.kill(conversation);
199
- return { refused: conversation.kills > 0 ? KILLED : causeOf(error) };
395
+ }
396
+ const refused = refusal(conversation, error);
397
+ const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
398
+ waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
399
+ return;
400
+ }
401
+ this.starts(conversation, null);
402
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
403
+ waiting.settle(result);
404
+ }
405
+ starts(conversation, step) {
406
+ conversation.starting = step;
407
+ if (conversation.shown !== null)
408
+ clearTimeout(conversation.shown);
409
+ conversation.shown = null;
410
+ if (step === null || conversation.told !== null) {
411
+ this.tell(conversation, step);
412
+ return;
200
413
  }
201
- this.prompt(conversation, prompt);
202
- return ack;
414
+ conversation.shown = setTimeout(() => {
415
+ conversation.shown = null;
416
+ if (conversation.starting === step)
417
+ this.tell(conversation, step);
418
+ }, STARTING_SHOWN_AFTER_MS);
203
419
  }
204
- async transcribed(conversation, files, paths) {
205
- const lines = [];
206
- for (const [index, path] of paths.entries()) {
207
- lines.push(path);
208
- if (files[index].media_type?.startsWith('audio/') !== true)
209
- continue;
210
- try {
211
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
212
- if (text !== '')
213
- lines.push(`Transcript: ${text}`);
214
- }
215
- catch (error) {
216
- if (conversation.kills > 0)
217
- throw error;
218
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
420
+ prepares(conversation, step) {
421
+ conversation.preparing = step;
422
+ this.next(conversation);
423
+ }
424
+ tell(conversation, step) {
425
+ if (conversation.told === step && !conversation.untold)
426
+ return;
427
+ conversation.told = step;
428
+ this.retell(conversation);
429
+ }
430
+ retell(conversation) {
431
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
432
+ }
433
+ async transcribed(conversation, messageId, text) {
434
+ const paths = linkedPaths(text);
435
+ if (paths.length === 0)
436
+ return [];
437
+ const killed = conversation.killed.signal;
438
+ const interrupted = conversation.interrupted.signal;
439
+ const transcribing = AbortSignal.any([killed, interrupted]);
440
+ let signal = killed;
441
+ const blocks = [];
442
+ try {
443
+ const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
444
+ const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
445
+ for (const path of paths) {
446
+ let answered = await read(path);
447
+ if (answered === null)
448
+ continue;
449
+ signal = transcribing;
450
+ this.prepares(conversation, 'transcription');
451
+ const said = await transcribe(async () => {
452
+ const address = answered ?? (await read(path));
453
+ answered = null;
454
+ if (address === null)
455
+ throw new Error(`${path} is no longer an audio file`);
456
+ return address;
457
+ }, signal);
458
+ const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
459
+ await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
460
+ blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
219
461
  }
220
462
  }
221
- return lines;
463
+ catch (error) {
464
+ if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
465
+ throw new NotStarted(INTERRUPTED, { cause: error });
466
+ throw error;
467
+ }
468
+ return blocks;
469
+ }
470
+ bridged(conversation) {
471
+ if (conversation.bridge === null) {
472
+ const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
473
+ conversation.bridge = bridging;
474
+ bridging.catch(() => {
475
+ if (conversation.bridge === bridging)
476
+ conversation.bridge = null;
477
+ });
478
+ }
479
+ return conversation.bridge;
480
+ }
481
+ closeBridge(conversation, bridging, bridge) {
482
+ bridge.close();
483
+ if (conversation.bridge === bridging)
484
+ conversation.bridge = null;
485
+ }
486
+ closeIdleBridge(conversation) {
487
+ const bridging = conversation.bridge;
488
+ if (bridging === null || conversation.running !== null || conversation.launch !== null)
489
+ return;
490
+ conversation.bridge = null;
491
+ void bridging.then((bridge) => bridge.close(), () => undefined);
222
492
  }
223
493
  async interrupt(conversation, turnId) {
224
494
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -229,11 +499,13 @@ export class Conversations {
229
499
  }
230
500
  halt(conversation) {
231
501
  if (conversation.opening !== null)
232
- stop(conversation.opening);
502
+ killMarked(marker(conversation.opening));
503
+ for (const scope of conversation.leftovers)
504
+ scope.kill();
233
505
  if (conversation.running === null)
234
506
  return;
235
507
  conversation.running.killed = true;
236
- stop(conversation.running.bridge);
508
+ conversation.running.scope.kill();
237
509
  }
238
510
  async kill(conversation) {
239
511
  const running = conversation.running;
@@ -245,6 +517,7 @@ export class Conversations {
245
517
  if (running === null)
246
518
  return;
247
519
  try {
520
+ running.reader.requested(option, value);
248
521
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
249
522
  sessionId: running.sessionId,
250
523
  configId: option,
@@ -253,6 +526,7 @@ export class Conversations {
253
526
  this.options(conversation, answered.configOptions);
254
527
  }
255
528
  catch (error) {
529
+ running.reader.refused(option);
256
530
  logged(error);
257
531
  }
258
532
  }
@@ -269,57 +543,77 @@ export class Conversations {
269
543
  }
270
544
  return route;
271
545
  }
272
- async start(conversation, route, sessionId) {
546
+ async start(conversation, routed, sessionId, empty, bridged) {
547
+ const route = await routed;
548
+ const reader = readerFor(route.kind);
549
+ reader.session = sessionId;
273
550
  const app = client({ name: '@agentshouse/kit' })
274
- .onNotification('session/update', ({ params }) => this.update(conversation, params))
275
- .onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
276
- .onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
277
551
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
278
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
552
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
553
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
279
554
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
280
- const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
555
+ const bridging = this.bridged(conversation);
556
+ const bridge = await step(unbridged, bridging);
281
557
  if (conversation.kills > 0) {
282
- bridge.close();
558
+ this.closeBridge(conversation, bridging, bridge);
283
559
  throw new Error(KILLED);
284
560
  }
285
561
  conversation.opening = bridge;
562
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
563
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
564
+ let adapter = null;
286
565
  try {
287
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
288
- await CLIS[route.kind].allowHouse();
289
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
290
- void adapter.exited.then(() => bridge.close());
566
+ bridged(bridge);
567
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
568
+ const streamed = await step(NO_CONNECTION, work.open(cli));
569
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
570
+ work.read(message);
571
+ this.read(conversation, reader, message);
572
+ }, { ...bridge.env, ...streamed }, undefined, scope));
573
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
291
574
  const agent = adapter.connection.agent;
292
575
  const cwd = route.working_directory;
293
- const _meta = CLIS[route.kind].sessionMeta;
294
- const opened = sessionId === null
295
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
296
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
297
- const running = {
298
- adapter,
299
- bridge,
300
- sessionId: opened.sessionId,
301
- killed: false,
302
- queues: CLIS[route.kind].queues,
303
- phase: CLIS[route.kind].phase,
304
- };
305
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
576
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
577
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
578
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
579
+ reader.session = opened.sessionId;
580
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
581
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
582
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
583
+ }
584
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
585
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
586
+ void work.settled().then(() => scope.record());
306
587
  conversation.running = running;
307
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
308
- if (conversation.commands !== null)
309
- this.commands(conversation, conversation.commands);
310
- this.options(conversation, options);
311
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
588
+ conversation.session = running.sessionId;
589
+ conversation.options = options;
590
+ this.facts(conversation);
591
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
312
592
  return running.sessionId;
313
593
  }
314
594
  catch (error) {
315
- stop(bridge);
316
- bridge.close();
595
+ adapter?.retire();
596
+ work.close();
597
+ conversation.leftovers.add(scope);
598
+ this.closeBridge(conversation, bridging, bridge);
317
599
  throw conversation.kills > 0 ? new Error(KILLED) : error;
318
600
  }
319
601
  finally {
320
602
  conversation.opening = null;
321
603
  }
322
604
  }
605
+ async resume(conversation, kind, adapter, request, empty) {
606
+ try {
607
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
608
+ }
609
+ catch (error) {
610
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
611
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
612
+ }
613
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
614
+ return null;
615
+ }
616
+ }
323
617
  async launchSettings(running, route, offered) {
324
618
  let options = offered;
325
619
  const settings = [
@@ -331,6 +625,7 @@ export class Conversations {
331
625
  if (value === null)
332
626
  continue;
333
627
  const option = options.find((candidate) => candidate.category === category);
628
+ running.reader.requested(option?.id ?? category, value);
334
629
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
335
630
  sessionId: running.sessionId,
336
631
  configId: option?.id ?? category,
@@ -340,23 +635,37 @@ export class Conversations {
340
635
  }
341
636
  return options;
342
637
  }
343
- exited(conversation, adapter, cause) {
344
- const running = conversation.running;
345
- if (running?.adapter !== adapter)
346
- return;
638
+ exited(conversation, running, cause) {
639
+ this.detach(conversation, running);
640
+ if (conversation.turn !== null) {
641
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
642
+ }
643
+ this.next(conversation);
644
+ this.kit.changed();
645
+ }
646
+ detach(conversation, running) {
647
+ if (conversation.bridge === running.bridging)
648
+ conversation.bridge = null;
649
+ running.work.close();
650
+ conversation.leftovers.add(running.scope);
347
651
  conversation.running = null;
348
652
  conversation.commands = null;
349
- conversation.jobs.clear();
350
- conversation.queued.length = 0;
653
+ conversation.options = null;
351
654
  for (const [id, question] of this.questions) {
352
655
  if (question.conversation === conversation)
353
656
  this.questions.delete(id);
354
657
  }
355
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
356
- if (conversation.turn !== null) {
357
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
358
- }
359
- this.kit.changed();
658
+ this.process(conversation, false);
659
+ }
660
+ process(conversation, running) {
661
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
662
+ }
663
+ facts(conversation) {
664
+ this.process(conversation, true);
665
+ if (conversation.commands !== null)
666
+ this.commands(conversation, conversation.commands);
667
+ if (conversation.options !== null)
668
+ this.options(conversation, conversation.options);
360
669
  }
361
670
  commands(conversation, commands) {
362
671
  conversation.commands = commands;
@@ -367,11 +676,12 @@ export class Conversations {
367
676
  options(conversation, options) {
368
677
  if (conversation.running === null)
369
678
  return;
679
+ conversation.options = options;
370
680
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
371
681
  }
372
682
  deliver(conversation, turn, event, body) {
373
683
  this.reporting++;
374
- const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
684
+ const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
375
685
  conversation.reports = delivered
376
686
  .catch(() => undefined)
377
687
  .finally(() => {
@@ -380,120 +690,95 @@ export class Conversations {
380
690
  });
381
691
  return delivered;
382
692
  }
383
- report(conversation) {
384
- const turn = new Turn();
385
- conversation.open++;
386
- void this.deliver(conversation, turn, 'started', {})
387
- .then(() => this.admitted(conversation, turn))
388
- .catch(logged);
389
- return turn;
693
+ write(conversation, turn, operation) {
694
+ if (turn.ended || !this.watched)
695
+ return;
696
+ this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
390
697
  }
391
- admitted(conversation, turn) {
392
- turn.admitted = true;
393
- for (const note of turn.notes.splice(0))
394
- this.kit.send(note);
395
- turn.unsentFrom = 0;
396
- this.draft(conversation, turn);
397
- if (turn.entries !== null)
398
- this.plan(conversation, turn);
698
+ replay(conversation, turn) {
699
+ for (const operation of turn.parts.operations())
700
+ this.write(conversation, turn, operation);
399
701
  }
400
702
  begin(conversation) {
401
- const turn = this.report(conversation);
703
+ const turn = new Turn((operation) => this.write(conversation, turn, operation));
704
+ conversation.open++;
705
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
402
706
  conversation.turn = turn;
707
+ if (conversation.context !== null)
708
+ turn.parts.measure(conversation.context);
709
+ for (const part of conversation.held.splice(0))
710
+ turn.parts.start(part);
403
711
  this.kit.changed();
404
712
  return turn;
405
713
  }
406
- finish(conversation, turn) {
407
- if (turn.ended)
408
- return;
409
- const segment = turn.segment;
410
- turn.segment = null;
411
- if (segment?.phase === 'note')
412
- this.note(conversation, turn, segment.text);
413
- else if (segment !== null)
414
- this.answer(conversation, turn, segment.text);
415
- this.end(conversation, turn, { text: turn.text });
416
- }
417
- end(conversation, turn, outcome) {
714
+ end(conversation, turn, failed, notice = null) {
418
715
  if (turn.ended)
419
716
  return;
717
+ if (failed !== null)
718
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
719
+ if (conversation.turn === turn) {
720
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
721
+ this.apply(conversation, turn, event);
722
+ }
723
+ if (notice !== null)
724
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
420
725
  turn.ended = true;
421
726
  conversation.open--;
422
727
  if (conversation.turn === turn)
423
728
  conversation.turn = null;
424
- void this.deliver(conversation, turn, 'ended', outcome).catch(logged);
425
- const late = conversation.open === 0 ? conversation.late.shift() : undefined;
426
- if (late !== undefined)
427
- this.failed(conversation, late);
729
+ const body = Promise.allSettled(turn.uploads).then(() => ({
730
+ parts: turn.parts.saved(),
731
+ context: turn.parts.context,
732
+ ...(failed === null ? {} : { failed: failed.failure }),
733
+ }));
734
+ void this.deliver(conversation, turn, 'ended', body).catch(logged);
735
+ this.next(conversation);
428
736
  this.kit.changed();
429
737
  }
430
- failed(conversation, cause) {
431
- if (conversation.open > 0)
432
- conversation.late.push(cause);
433
- else
434
- this.end(conversation, this.report(conversation), { failed: cause });
435
- }
436
- prompt(conversation, prompt) {
738
+ prompt(conversation, prompt, taken) {
437
739
  const running = conversation.running;
438
- const sent = { turn: null };
439
- if (running.queues && (conversation.turn !== null || conversation.queued.length > 0)) {
440
- conversation.queued.push(sent);
441
- }
442
- else {
443
- this.attach(conversation.turn ?? this.begin(conversation), sent);
444
- }
445
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
446
- if (!running.adapter.connection.signal.aborted) {
447
- this.answered(conversation, sent, causeOf(error));
448
- if (conversation.open === 0 && conversation.running === running)
449
- stop(running.bridge);
740
+ const turn = this.begin(conversation);
741
+ turn.prompted = true;
742
+ turn.taken = taken;
743
+ conversation.prompted = running.sessionId;
744
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
745
+ this.took(turn);
746
+ const notice = answeredFailure(answer);
747
+ if (notice !== null) {
748
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
749
+ }
750
+ else {
751
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
450
752
  }
451
- else if (sent.turn !== null) {
452
- this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
753
+ }, async (error) => {
754
+ if (running.adapter.connection.signal.aborted) {
755
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
756
+ }
757
+ else {
758
+ this.took(turn);
759
+ if (conversation.open === 1 && conversation.running === running) {
760
+ running.adapter.retire();
761
+ this.detach(conversation, running);
762
+ }
763
+ this.end(conversation, turn, answeredWith(error));
453
764
  }
454
765
  });
455
766
  }
456
- attach(turn, sent) {
457
- turn.prompt = sent;
458
- sent.turn = turn;
459
- }
460
- answered(conversation, sent, failed) {
461
- const turn = sent.turn;
462
- if (turn === null) {
463
- const queued = conversation.queued.indexOf(sent);
464
- if (queued < 0)
465
- return;
466
- conversation.queued.splice(queued, 1);
467
- if (failed !== null)
468
- this.failed(conversation, failed);
469
- }
470
- else if (failed !== null) {
471
- if (turn.ended)
472
- this.failed(conversation, failed);
473
- else
474
- this.end(conversation, turn, { failed });
475
- }
476
- else if (turn.prompt === sent) {
477
- this.finish(conversation, turn);
478
- }
479
- }
480
767
  started(conversation) {
481
- if (conversation.turn !== null)
482
- return conversation.turn;
483
- const turn = this.begin(conversation);
484
- const queued = conversation.queued.shift();
485
- if (queued !== undefined)
486
- this.attach(turn, queued);
768
+ const turn = conversation.turn ?? this.begin(conversation);
769
+ this.took(turn);
487
770
  return turn;
488
771
  }
489
772
  ended(conversation) {
490
773
  const turn = conversation.turn;
491
774
  if (turn === null)
492
775
  return;
493
- if (turn.prompt === null) {
494
- this.finish(conversation, turn);
776
+ if (!turn.prompted) {
777
+ this.end(conversation, turn, null);
495
778
  }
496
779
  else {
780
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
781
+ this.apply(conversation, turn, event);
497
782
  conversation.turn = null;
498
783
  this.kit.changed();
499
784
  }
@@ -542,101 +827,114 @@ export class Conversations {
542
827
  if (content !== null)
543
828
  this.questions.get(interactionId)?.answer({ action: 'accept', content });
544
829
  }
545
- update(conversation, notification) {
546
- const update = notification.update;
547
- if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
548
- this.chunk(conversation, this.started(conversation), update, update.content.text);
549
- }
550
- else if (update.sessionUpdate === 'plan') {
551
- const turn = this.started(conversation);
552
- this.close(conversation, turn);
553
- turn.entries = update.entries;
554
- this.plan(conversation, turn);
555
- }
556
- else if (update.sessionUpdate === 'tool_call') {
557
- this.close(conversation, this.started(conversation));
558
- }
559
- else if (update.sessionUpdate === 'agent_thought_chunk') {
560
- const turn = this.started(conversation);
561
- if (turn.segment?.messageId !== (update.messageId ?? null))
562
- this.close(conversation, turn);
563
- }
564
- else if (update.sessionUpdate === 'available_commands_update') {
565
- this.commands(conversation, update.availableCommands);
566
- }
567
- else if (update.sessionUpdate === 'config_option_update') {
568
- this.options(conversation, update.configOptions);
830
+ read(conversation, reader, message) {
831
+ for (const event of reader.read(message))
832
+ this.event(conversation, event);
833
+ }
834
+ event(conversation, event) {
835
+ switch (event.kind) {
836
+ case 'started':
837
+ this.started(conversation);
838
+ return;
839
+ case 'ended':
840
+ this.ended(conversation);
841
+ return;
842
+ case 'commands':
843
+ this.commands(conversation, event.commands);
844
+ return;
845
+ case 'options':
846
+ this.options(conversation, event.options);
847
+ return;
848
+ case 'context':
849
+ conversation.context = { used: event.used, window: event.window };
850
+ conversation.turn?.parts.measure(conversation.context);
851
+ return;
852
+ case 'subagent-done':
853
+ this.subagentDone(conversation, event.id, event.result);
854
+ return;
855
+ case 'marker':
856
+ case 'image':
857
+ if (conversation.turn !== null)
858
+ break;
859
+ if (event.kind === 'marker' && event.marker === 'job')
860
+ conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
861
+ return;
569
862
  }
863
+ this.apply(conversation, this.started(conversation), event);
570
864
  }
571
- job(conversation, job) {
572
- if (job.running)
573
- conversation.jobs.add(job.id);
574
- else
575
- conversation.jobs.delete(job.id);
576
- this.kit.changed();
865
+ took(turn) {
866
+ const taken = turn.taken;
867
+ turn.taken = null;
868
+ taken?.();
577
869
  }
578
- chunk(conversation, turn, chunk, text) {
579
- const phase = conversation.running?.phase(chunk) ?? null;
580
- const messageId = chunk.messageId ?? null;
581
- if (turn.segment?.messageId !== messageId || turn.segment.phase !== phase)
582
- this.close(conversation, turn);
583
- if (phase === 'answer') {
584
- this.answer(conversation, turn, text);
585
- return;
870
+ apply(conversation, turn, event) {
871
+ switch (event.kind) {
872
+ case 'text':
873
+ case 'reasoning':
874
+ turn.parts.write(event.kind, event.id, event.text);
875
+ return;
876
+ case 'command':
877
+ turn.parts.command();
878
+ return;
879
+ case 'subagent': {
880
+ const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
881
+ conversation.subagents.set(event.id, { description: event.description, turn, index });
882
+ return;
883
+ }
884
+ case 'message':
885
+ turn.parts.start({ type: 'message', command: event.command, target: event.target });
886
+ return;
887
+ case 'marker':
888
+ turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
889
+ return;
890
+ case 'plan':
891
+ turn.parts.plan(event.entries);
892
+ return;
893
+ case 'image':
894
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
895
+ this.kit.changed();
896
+ return;
586
897
  }
587
- turn.segment ??= { messageId, phase, text: '' };
588
- turn.segment.text += text;
589
898
  }
590
- close(conversation, turn) {
591
- const segment = turn.segment;
592
- if (segment === null)
899
+ subagentDone(conversation, id, result) {
900
+ const subagent = conversation.subagents.get(id);
901
+ if (subagent === undefined)
593
902
  return;
594
- turn.segment = null;
595
- this.note(conversation, turn, segment.text);
596
- }
597
- note(conversation, turn, text) {
598
- if (text.trim() === '')
903
+ conversation.subagents.delete(id);
904
+ if (subagent.turn === conversation.turn) {
905
+ subagent.turn.parts.done(subagent.index, result);
599
906
  return;
600
- const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
601
- if (turn.admitted)
602
- this.kit.send(note);
907
+ }
908
+ const part = { type: 'subagent', description: subagent.description, status: 'done', result };
909
+ if (conversation.turn === null)
910
+ conversation.held.push(part);
603
911
  else
604
- turn.notes.push(note);
605
- }
606
- answer(conversation, turn, text) {
607
- turn.text += text;
608
- this.draft(conversation, turn);
609
- }
610
- draft(conversation, turn) {
611
- const blocks = blocksOf(turn.text);
612
- const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
613
- const sent = this.kit.send({
614
- type: 'draft',
615
- conversation_id: conversation.id,
616
- turn_id: turn.id,
617
- sequence: turn.draftSequence + 1,
618
- from,
619
- blocks: blocks.slice(from),
620
- });
621
- if (sent) {
622
- turn.draftSequence++;
623
- turn.sent = blocks;
624
- turn.unsentFrom = null;
912
+ conversation.turn.parts.start(part);
913
+ }
914
+ async image(conversation, turn, reserved, data, mediaType) {
915
+ const bytes = Buffer.from(data, 'base64');
916
+ const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
917
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
918
+ try {
919
+ const declared = (await this.deliver(conversation, turn, 'images', {
920
+ name,
921
+ media_type: mediaType,
922
+ bytes: bytes.length,
923
+ sha256,
924
+ }));
925
+ const saved = await uploaded(declared.upload, {
926
+ name,
927
+ bytes: bytes.length,
928
+ sha256,
929
+ read: (start, end) => [bytes.subarray(start, end)],
930
+ send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
931
+ finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
932
+ });
933
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
625
934
  }
626
- else {
627
- turn.unsentFrom = from;
935
+ catch (error) {
936
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
937
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
628
938
  }
629
939
  }
630
- plan(conversation, turn) {
631
- const sent = this.kit.send({
632
- type: 'plan',
633
- conversation_id: conversation.id,
634
- turn_id: turn.id,
635
- sequence: turn.planSequence + 1,
636
- from: 0,
637
- steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
638
- });
639
- if (sent)
640
- turn.planSequence++;
641
- }
642
940
  }