@agentshouse/kit 0.2.1-cca9e1f43de44 → 0.2.1-ccb257fa8a318

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 (132) hide show
  1. package/README.md +42 -40
  2. package/dist/acp.js +92 -115
  3. package/dist/agents.js +112 -24
  4. package/dist/api.js +10 -6
  5. package/dist/app-git.js +10 -26
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +11 -25
  8. package/dist/building.js +46 -0
  9. package/dist/calls.js +71 -0
  10. package/dist/chatgpt.js +32 -0
  11. package/dist/clis-main.js +2 -2
  12. package/dist/clis.js +131 -27
  13. package/dist/codex-main.js +58 -0
  14. package/dist/connection.js +116 -0
  15. package/dist/conversations.js +523 -202
  16. package/dist/copies.js +185 -123
  17. package/dist/end.js +24 -5
  18. package/dist/enrol-main.js +7 -1
  19. package/dist/events.js +16 -78
  20. package/dist/files.js +139 -31
  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 -177
  25. package/dist/instructions.js +8 -10
  26. package/dist/kit-cli.js +68 -0
  27. package/dist/kit-main.js +8 -22
  28. package/dist/login.js +118 -107
  29. package/dist/logout.js +14 -16
  30. package/dist/mcp.js +6 -2
  31. package/dist/parts.js +34 -16
  32. package/dist/placement.js +39 -0
  33. package/dist/relay.js +33 -2
  34. package/dist/resident.js +118 -51
  35. package/dist/scope.js +206 -0
  36. package/dist/scripts.js +255 -0
  37. package/dist/service-main.js +9 -0
  38. package/dist/service.js +55 -0
  39. package/dist/sign-in.js +23 -40
  40. package/dist/silence.js +28 -0
  41. package/dist/skills.js +49 -35
  42. package/dist/transcription.js +85 -45
  43. package/dist/translate.js +2 -8
  44. package/dist/update.js +115 -53
  45. package/dist/wire.js +22 -0
  46. package/dist/work.js +346 -0
  47. package/package.json +20 -20
  48. package/dist/bootstrap.json +0 -24
  49. package/dist/stream.js +0 -72
  50. package/how-we-work.md +0 -23
  51. package/skills/ask-matt/LICENSE +0 -21
  52. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  53. package/skills/ask-matt/SKILL.md +0 -91
  54. package/skills/ask-matt/agents/openai.yaml +0 -4
  55. package/skills/code-review/LICENSE +0 -21
  56. package/skills/code-review/SKILL.md +0 -89
  57. package/skills/code-review/agents/openai.yaml +0 -3
  58. package/skills/codebase-design/DEEPENING.md +0 -37
  59. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  60. package/skills/codebase-design/LICENSE +0 -21
  61. package/skills/codebase-design/SKILL.md +0 -116
  62. package/skills/codebase-design/agents/openai.yaml +0 -3
  63. package/skills/diagnosing-bugs/LICENSE +0 -21
  64. package/skills/diagnosing-bugs/SKILL.md +0 -140
  65. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  66. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  67. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  68. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  69. package/skills/domain-modeling/LICENSE +0 -21
  70. package/skills/domain-modeling/SKILL.md +0 -76
  71. package/skills/domain-modeling/agents/openai.yaml +0 -3
  72. package/skills/grill-me/LICENSE +0 -21
  73. package/skills/grill-me/SKILL.md +0 -8
  74. package/skills/grill-me/agents/openai.yaml +0 -4
  75. package/skills/grill-with-docs/LICENSE +0 -21
  76. package/skills/grill-with-docs/SKILL.md +0 -8
  77. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  78. package/skills/grilling/LICENSE +0 -21
  79. package/skills/grilling/SKILL.md +0 -30
  80. package/skills/grilling/agents/openai.yaml +0 -3
  81. package/skills/handoff/LICENSE +0 -21
  82. package/skills/handoff/SKILL.md +0 -17
  83. package/skills/handoff/agents/openai.yaml +0 -4
  84. package/skills/implement/LICENSE +0 -21
  85. package/skills/implement/SKILL.md +0 -16
  86. package/skills/implement/agents/openai.yaml +0 -4
  87. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  88. package/skills/improve-codebase-architecture/LICENSE +0 -21
  89. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  90. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  91. package/skills/prototype/LICENSE +0 -21
  92. package/skills/prototype/LOGIC.md +0 -67
  93. package/skills/prototype/SKILL.md +0 -28
  94. package/skills/prototype/UI.md +0 -112
  95. package/skills/prototype/agents/openai.yaml +0 -3
  96. package/skills/research/LICENSE +0 -21
  97. package/skills/research/SKILL.md +0 -14
  98. package/skills/research/agents/openai.yaml +0 -3
  99. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  100. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  101. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  102. package/skills/tdd/LICENSE +0 -21
  103. package/skills/tdd/SKILL.md +0 -40
  104. package/skills/tdd/agents/openai.yaml +0 -3
  105. package/skills/tdd/mocking.md +0 -59
  106. package/skills/tdd/tests.md +0 -77
  107. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  108. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  109. package/skills/teach/LICENSE +0 -21
  110. package/skills/teach/MISSION-FORMAT.md +0 -31
  111. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  112. package/skills/teach/SKILL.md +0 -141
  113. package/skills/teach/agents/openai.yaml +0 -4
  114. package/skills/to-questionnaire/LICENSE +0 -21
  115. package/skills/to-questionnaire/SKILL.md +0 -55
  116. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  117. package/skills/to-spec/LICENSE +0 -21
  118. package/skills/to-spec/SKILL.md +0 -76
  119. package/skills/to-spec/agents/openai.yaml +0 -4
  120. package/skills/to-tickets/LICENSE +0 -21
  121. package/skills/to-tickets/SKILL.md +0 -106
  122. package/skills/to-tickets/agents/openai.yaml +0 -4
  123. package/skills/wait-what/LICENSE +0 -21
  124. package/skills/wait-what/SKILL.md +0 -8
  125. package/skills/wait-what/agents/openai.yaml +0 -4
  126. package/skills/wayfinder/LICENSE +0 -21
  127. package/skills/wayfinder/SKILL.md +0 -129
  128. package/skills/wayfinder/agents/openai.yaml +0 -4
  129. package/skills/writing-for-agents/LICENSE +0 -21
  130. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  131. package/skills/writing-for-agents/SKILL.md +0 -83
  132. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -1,44 +1,121 @@
1
- import { client, } from '@agentclientprotocol/sdk';
1
+ import { client, RequestError, } from '@agentclientprotocol/sdk';
2
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, processes, startAdapter } from './acp.js';
6
+ import { startAdapter } from './acp.js';
7
+ import { finalRefusal, HouseRefusal } from './api.js';
7
8
  import { openBridge } from './bridge.js';
8
9
  import { CLIS } from './clis.js';
9
- import { readerFor } from './events.js';
10
- import { placeFiles, uploadBytes } 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';
13
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';
14
18
  import { holdSecretInput } from './secret-input.js';
15
- import { createHowWeWork } from './skills.js';
16
19
  import { transcribe } from './transcription.js';
20
+ import { workFor } from './work.js';
21
+ const SIGNED_OUT = RequestError.authRequired().code;
17
22
  const KILLED = 'the conversation was killed';
18
- // A process a conversation left running reports nothing when it exits, so the Kit looks again every five seconds while one runs.
19
- const PROCESS_RECHECK_MS = 5000;
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
+ const UNSTEERED = "the Agent's CLI did not take the message into its running turn";
38
+ class NotStarted extends Error {
39
+ }
40
+ class ResumeRefused extends NotStarted {
41
+ }
42
+ async function step(reason, work) {
43
+ try {
44
+ return await work;
45
+ }
46
+ catch (error) {
47
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
48
+ }
49
+ }
50
+ function signedOut(error) {
51
+ return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
52
+ }
53
+ function unopened(reason) {
54
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
55
+ }
56
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
57
+ const STARTING_SHOWN_AFTER_MS = 1_000;
58
+ function unbridged(error) {
59
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
60
+ }
61
+ const EXIT_PLAN_MODE = {
62
+ parse(value) {
63
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
64
+ throw new Error('_x.ai/exit_plan_mode needs an object');
65
+ }
66
+ return value;
67
+ },
68
+ };
69
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
20
70
  function causeOf(error) {
21
71
  return error instanceof Error ? error.message : String(error);
22
72
  }
73
+ function answeredWith(error) {
74
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
75
+ }
76
+ function exitedWith(running, cause) {
77
+ if (running.killed)
78
+ return { failure: 'killed', cause: KILLED };
79
+ return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
80
+ }
23
81
  function answerMessage(request, response) {
24
82
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
25
83
  }
84
+ async function sentPart(url, bytes, headers) {
85
+ const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
86
+ await answer.arrayBuffer();
87
+ if (!answer.ok)
88
+ throw storeRefusal('upload', answer.status);
89
+ }
26
90
  function logged(error) {
27
91
  process.stderr.write(`kit: ${causeOf(error)}\n`);
28
92
  }
93
+ function refusal(conversation, error) {
94
+ if (conversation.kills > 0)
95
+ return KILLED;
96
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
97
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
98
+ return causeOf(error);
99
+ }
100
+ function audioOf(result) {
101
+ if (result.isError === true)
102
+ return null;
103
+ const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
104
+ return file?.media_type?.startsWith('audio/') === true ? file : null;
105
+ }
29
106
  function marker(bridge) {
30
107
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
108
  }
32
- function stop(bridge) {
33
- killMarked(marker(bridge));
109
+ function steers(adapter) {
110
+ return adapter.initialized._meta?.steering?.supported === true;
34
111
  }
35
112
  class Turn {
36
113
  id = randomUUID();
37
114
  parts;
38
- admitted = false;
39
115
  questions = 0;
40
116
  secrets = 0;
41
117
  prompted = false;
118
+ taken = null;
42
119
  ended = false;
43
120
  images = 0;
44
121
  uploads = [];
@@ -52,13 +129,19 @@ class Conversation {
52
129
  session = null;
53
130
  turn = null;
54
131
  commands = null;
132
+ options = null;
133
+ exitUntold = false;
55
134
  context = null;
56
- busy = new Set();
135
+ leftovers = new Set();
57
136
  waiting = [];
137
+ done = false;
58
138
  sending = null;
59
139
  subagents = new Map();
60
140
  held = [];
61
141
  opening = null;
142
+ bridge = null;
143
+ launch = null;
144
+ prompted = null;
62
145
  kills = 0;
63
146
  killed = new AbortController();
64
147
  interrupts = 0;
@@ -66,6 +149,11 @@ class Conversation {
66
149
  open = 0;
67
150
  queue = Promise.resolve();
68
151
  reports = Promise.resolve();
152
+ starting = null;
153
+ preparing = null;
154
+ told = null;
155
+ untold = false;
156
+ shown = null;
69
157
  constructor(id) {
70
158
  this.id = id;
71
159
  }
@@ -81,70 +169,71 @@ function asksSecret(request) {
81
169
  export class Conversations {
82
170
  kit;
83
171
  conversations = new Map();
84
- acks = new Map();
85
172
  questions = new Map();
86
- carrying = 0;
87
173
  reporting = 0;
88
174
  watched = false;
89
- recheck = null;
90
175
  constructor(kit) {
91
176
  this.kit = kit;
92
- }
93
- input(input) {
94
- this.carrying++;
95
- const carried = this.acks.get(input.input_id) ??
96
- (() => {
97
- const conversation = this.conversation(input.conversation_id);
98
- if (input.kind === 'kill') {
99
- conversation.kills++;
100
- conversation.killed.abort(new Error(KILLED));
101
- for (const waiting of conversation.waiting.splice(0))
102
- waiting.settle({ refused: KILLED });
103
- this.halt(conversation);
104
- }
105
- if (input.kind === 'interrupt') {
106
- conversation.interrupts++;
107
- conversation.interrupted.abort();
108
- }
109
- const handled = conversation.queue.then(() => this.carry(conversation, input));
110
- conversation.queue = handled.catch(() => undefined);
111
- const work = handled.then(({ ack }) => ack);
112
- this.acks.set(input.input_id, work);
113
- return work;
114
- })();
115
- carried
116
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
117
- .catch(logged)
118
- .finally(() => {
119
- this.carrying--;
120
- this.kit.changed();
177
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
178
+ setInterval(() => this.look(), 2_000);
179
+ }
180
+ call(call) {
181
+ void this.kit.calls.carry(call.input_id, () => {
182
+ const conversation = this.conversation(call.conversation_id);
183
+ if (call.kind === 'kill') {
184
+ conversation.kills++;
185
+ conversation.killed.abort(new Error(KILLED));
186
+ for (const waiting of conversation.waiting.splice(0))
187
+ waiting.settle({ refused: KILLED });
188
+ this.halt(conversation);
189
+ }
190
+ if (call.kind === 'interrupt') {
191
+ conversation.interrupts++;
192
+ conversation.interrupted.abort();
193
+ }
194
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
195
+ conversation.queue = handled.catch(() => undefined);
196
+ return handled.then(async ({ result }) => ({ result: await result }));
121
197
  });
122
198
  }
123
199
  idle() {
124
- if (this.carrying > 0 || this.reporting > 0)
200
+ if (this.reporting > 0)
125
201
  return false;
126
202
  for (const conversation of this.conversations.values()) {
127
203
  const turn = conversation.turn;
128
204
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
129
- if (conversation.busy.size > 0 || conversation.open > (asking ? 1 : 0))
205
+ if (conversation.open > (asking ? 1 : 0))
130
206
  return false;
131
207
  }
132
- for (const conversation of this.conversations.values()) {
133
- const running = conversation.running;
134
- if (running === null || running.baseline === null)
135
- continue;
136
- if ([...processes(marker(running.bridge))].every((found) => running.baseline.has(found)))
137
- continue;
138
- this.recheck ??= setTimeout(() => {
139
- this.recheck = null;
140
- this.kit.changed();
141
- }, PROCESS_RECHECK_MS);
208
+ const conversations = [...this.conversations.values()];
209
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
142
210
  return false;
211
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
212
+ }
213
+ quiet() {
214
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
215
+ }
216
+ look() {
217
+ for (const conversation of this.conversations.values()) {
218
+ for (const scope of conversation.leftovers) {
219
+ if (!scope.empty())
220
+ continue;
221
+ scope.close();
222
+ conversation.leftovers.delete(scope);
223
+ }
143
224
  }
144
- return true;
225
+ this.kit.changed();
145
226
  }
146
227
  opened() {
147
228
  this.watched = false;
229
+ for (const conversation of this.conversations.values()) {
230
+ if (conversation.running !== null)
231
+ this.facts(conversation);
232
+ else if (conversation.exitUntold)
233
+ this.process(conversation, false);
234
+ if (conversation.untold || conversation.told !== null)
235
+ this.retell(conversation);
236
+ }
148
237
  }
149
238
  watching(watched) {
150
239
  const resend = watched && !this.watched;
@@ -164,127 +253,309 @@ export class Conversations {
164
253
  }
165
254
  return conversation;
166
255
  }
167
- async carry(conversation, input) {
168
- if (input.kind === 'open')
169
- return { ack: Promise.resolve(await this.open(conversation, input)) };
170
- if (input.kind === 'message')
171
- return this.message(conversation, input);
172
- if (input.kind === 'interrupt') {
256
+ async carry(conversation, call) {
257
+ if (call.kind === 'open')
258
+ return this.open(conversation, call);
259
+ if (call.kind === 'message')
260
+ return this.message(conversation, call);
261
+ if (call.kind === 'done') {
262
+ conversation.done = true;
263
+ this.next(conversation);
264
+ return { result: Promise.resolve({}) };
265
+ }
266
+ await this.launched(conversation);
267
+ if (call.kind === 'interrupt') {
173
268
  if (--conversation.interrupts === 0)
174
269
  conversation.interrupted = new AbortController();
175
- await this.interrupt(conversation, String(input.turn_id));
270
+ await this.interrupt(conversation, String(call.turn_id));
176
271
  }
177
- if (input.kind === 'kill') {
272
+ if (call.kind === 'kill') {
178
273
  await this.kill(conversation);
179
274
  await conversation.sending;
180
275
  if (--conversation.kills === 0)
181
276
  conversation.killed = new AbortController();
182
277
  }
183
- if (input.kind === 'option')
184
- await this.option(conversation, String(input.option), input.value);
185
- if (input.kind === 'answer') {
186
- const question = this.questions.get(String(input.interaction_id));
278
+ if (call.kind === 'option')
279
+ await this.option(conversation, String(call.option), call.value);
280
+ if (call.kind === 'answer') {
281
+ const question = this.questions.get(String(call.interaction_id));
187
282
  if (question === undefined) {
188
- const text = answerMessage(input.request, input.response);
189
- return this.message(conversation, { ...input, text, files: [], first: false });
283
+ const text = answerMessage(call.request, call.response);
284
+ return this.message(conversation, { ...call, text, first: false, message_id: null });
190
285
  }
191
- question.answer(input.response);
286
+ question.answer(call.response);
192
287
  }
193
- return { ack: Promise.resolve({}) };
288
+ return { result: Promise.resolve({}) };
289
+ }
290
+ async open(conversation, call) {
291
+ await this.stop(conversation);
292
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
293
+ return {
294
+ result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
295
+ };
194
296
  }
195
- async open(conversation, input) {
196
- if (conversation.running !== null)
197
- return {};
297
+ async stop(conversation) {
298
+ const running = conversation.running;
299
+ running?.scope.kill();
300
+ await running?.adapter.exited;
301
+ }
302
+ launch(conversation, route, sessionId, empty) {
303
+ let opened;
304
+ let failed;
305
+ const bridge = new Promise((resolve, reject) => {
306
+ opened = resolve;
307
+ failed = reject;
308
+ });
309
+ bridge.catch(() => undefined);
310
+ const session = this.start(conversation, route, sessionId, empty, opened);
311
+ session.catch(failed);
312
+ const launch = { bridge, session };
313
+ conversation.launch = launch;
314
+ void session
315
+ .catch(() => undefined)
316
+ .then(() => {
317
+ if (conversation.launch !== launch)
318
+ return;
319
+ conversation.launch = null;
320
+ this.closeIdleBridge(conversation);
321
+ this.next(conversation);
322
+ });
323
+ this.next(conversation);
324
+ return launch;
325
+ }
326
+ async launched(conversation) {
327
+ while (conversation.launch !== null)
328
+ await conversation.launch.session.catch(() => undefined);
329
+ }
330
+ instructed(conversation, route) {
331
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
332
+ if (bridge === undefined)
333
+ return null;
334
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
335
+ read.catch(() => undefined);
336
+ return read;
337
+ }
338
+ async message(conversation, call) {
339
+ const content = [
340
+ ...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
341
+ { type: 'text', text: String(call.text) },
342
+ ];
343
+ let route;
344
+ let instructing = null;
198
345
  try {
199
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
346
+ route = await this.route(conversation, call.agent_id);
347
+ if (call.first === true)
348
+ instructing = this.instructed(conversation, route);
349
+ if (call.message_id !== null)
350
+ content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
351
+ if (conversation.kills > 0)
352
+ throw new Error(KILLED);
200
353
  }
201
354
  catch (error) {
202
- return { refused: causeOf(error) };
355
+ this.closeIdleBridge(conversation);
356
+ this.prepares(conversation, null);
357
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
358
+ }
359
+ conversation.preparing = null;
360
+ const { promise, resolve: settle } = Promise.withResolvers();
361
+ const waiting = { call, route, content, instructing, settle };
362
+ if (call.at_once === true && conversation.open > 0)
363
+ await this.atOnce(conversation, waiting);
364
+ else
365
+ conversation.waiting.push(waiting);
366
+ this.next(conversation);
367
+ return { result: promise };
368
+ }
369
+ async atOnce(conversation, waiting) {
370
+ while (conversation.sending !== null)
371
+ await conversation.sending;
372
+ const running = conversation.running;
373
+ if (running !== null && conversation.open > 0 && steers(running.adapter)) {
374
+ conversation.sending = this.steer(conversation, running, waiting);
375
+ await conversation.sending;
376
+ conversation.sending = null;
377
+ return;
203
378
  }
379
+ this.ahead(conversation, waiting);
380
+ if (conversation.turn !== null)
381
+ await this.interrupt(conversation, conversation.turn.id);
204
382
  }
205
- async message(conversation, input) {
206
- const content = [{ type: 'text', text: String(input.text) }];
207
- let route;
383
+ ahead(conversation, waiting) {
384
+ const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
385
+ conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
386
+ }
387
+ async steer(conversation, running, waiting) {
388
+ let outcome;
208
389
  try {
209
- route = await this.route(conversation, input.agent_id);
210
- const files = input.files;
211
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
212
- const lines = await this.transcribed(conversation, files, paths);
213
- if (lines.length > 0)
214
- content.push({ type: 'text', text: lines.join('\n') });
215
- if (conversation.kills > 0)
216
- throw new Error(KILLED);
390
+ ({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
391
+ sessionId: running.sessionId,
392
+ prompt: waiting.content,
393
+ _meta: { steering: { idleBehavior: 'promptRequired' } },
394
+ }));
217
395
  }
218
396
  catch (error) {
219
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
397
+ outcome = error;
398
+ }
399
+ if (outcome === 'promptRequired') {
400
+ this.ahead(conversation, waiting);
401
+ }
402
+ else if (outcome === 'injected' || outcome === 'startedNewTurn') {
403
+ this.kit.calls.keepResult(String(waiting.call.input_id), {});
404
+ waiting.settle({});
405
+ }
406
+ else {
407
+ const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
408
+ waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
220
409
  }
221
- return {
222
- ack: new Promise((settle) => {
223
- conversation.waiting.push({ input, route, content, settle });
224
- this.next(conversation);
225
- }),
226
- };
227
410
  }
228
411
  next(conversation) {
229
412
  if (conversation.sending !== null || conversation.open > 0)
230
413
  return;
231
414
  const waiting = conversation.waiting.shift();
232
- if (waiting === undefined)
415
+ if (waiting === undefined) {
416
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
417
+ if (conversation.starting !== step)
418
+ this.starts(conversation, step);
419
+ if (conversation.done && conversation.launch === null)
420
+ void this.stop(conversation).catch(logged);
233
421
  return;
422
+ }
234
423
  conversation.sending = this.send(conversation, waiting).finally(() => {
235
424
  conversation.sending = null;
236
425
  this.next(conversation);
237
426
  });
238
427
  }
239
428
  async send(conversation, waiting) {
240
- const { input } = waiting;
241
- let ack = {};
429
+ const { call } = waiting;
430
+ let result = {};
431
+ let started = false;
432
+ let instructing = waiting.instructing;
242
433
  const prompt = [];
243
434
  try {
244
435
  if (conversation.running === null) {
245
- const resumed = input.provider_session_id ?? conversation.session;
246
- const opened = await this.start(conversation, waiting.route, resumed);
247
- if (resumed === null)
248
- ack = { provider_session_id: opened };
436
+ this.starts(conversation, 'session');
437
+ await this.launched(conversation);
249
438
  }
250
- if (input.first === true) {
251
- prompt.push({
252
- type: 'text',
253
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
254
- });
439
+ if (conversation.running === null) {
440
+ const stored = conversation.session ?? call.provider_session_id;
441
+ const empty = call.first === true && conversation.prompted !== stored;
442
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
443
+ if (call.first === true)
444
+ instructing = this.instructed(conversation, waiting.route);
445
+ started = (await launch.session) !== stored;
446
+ }
447
+ const session = conversation.running.sessionId;
448
+ if (started || (call.first === true && session !== call.provider_session_id)) {
449
+ result = { provider_session_id: session };
450
+ }
451
+ if (call.first === true) {
452
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
255
453
  }
256
454
  if (conversation.kills > 0)
257
455
  throw new Error(KILLED);
258
456
  }
259
457
  catch (error) {
260
- if ('provider_session_id' in ack) {
458
+ this.starts(conversation, null);
459
+ if (started) {
261
460
  conversation.session = null;
262
461
  await this.kill(conversation);
263
462
  }
264
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
463
+ const refused = refusal(conversation, error);
464
+ const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
465
+ waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
265
466
  return;
266
467
  }
267
- this.prompt(conversation, [...prompt, ...waiting.content]);
268
- waiting.settle(ack);
468
+ this.starts(conversation, null);
469
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
470
+ waiting.settle(result);
471
+ }
472
+ starts(conversation, step) {
473
+ conversation.starting = step;
474
+ if (conversation.shown !== null)
475
+ clearTimeout(conversation.shown);
476
+ conversation.shown = null;
477
+ if (step === null || conversation.told !== null) {
478
+ this.tell(conversation, step);
479
+ return;
480
+ }
481
+ conversation.shown = setTimeout(() => {
482
+ conversation.shown = null;
483
+ if (conversation.starting === step)
484
+ this.tell(conversation, step);
485
+ }, STARTING_SHOWN_AFTER_MS);
269
486
  }
270
- async transcribed(conversation, files, paths) {
271
- const lines = [];
272
- for (const [index, path] of paths.entries()) {
273
- lines.push(path);
274
- if (files[index].media_type?.startsWith('audio/') !== true)
275
- continue;
276
- try {
277
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
278
- if (text !== null && text !== '')
279
- lines.push(`Transcript: ${text}`);
280
- }
281
- catch (error) {
282
- if (conversation.kills > 0)
283
- throw error;
284
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
487
+ prepares(conversation, step) {
488
+ conversation.preparing = step;
489
+ this.next(conversation);
490
+ }
491
+ tell(conversation, step) {
492
+ if (conversation.told === step && !conversation.untold)
493
+ return;
494
+ conversation.told = step;
495
+ this.retell(conversation);
496
+ }
497
+ retell(conversation) {
498
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
499
+ }
500
+ async transcribed(conversation, messageId, text) {
501
+ const paths = linkedPaths(text);
502
+ if (paths.length === 0)
503
+ return [];
504
+ const killed = conversation.killed.signal;
505
+ const interrupted = conversation.interrupted.signal;
506
+ const transcribing = AbortSignal.any([killed, interrupted]);
507
+ let signal = killed;
508
+ const blocks = [];
509
+ try {
510
+ const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
511
+ const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
512
+ for (const path of paths) {
513
+ let answered = await read(path);
514
+ if (answered === null)
515
+ continue;
516
+ signal = transcribing;
517
+ this.prepares(conversation, 'transcription');
518
+ const said = await transcribe(async () => {
519
+ const address = answered ?? (await read(path));
520
+ answered = null;
521
+ if (address === null)
522
+ throw new Error(`${path} is no longer an audio file`);
523
+ return address;
524
+ }, signal);
525
+ const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
526
+ await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
527
+ blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
285
528
  }
286
529
  }
287
- return lines;
530
+ catch (error) {
531
+ if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
532
+ throw new NotStarted(INTERRUPTED, { cause: error });
533
+ throw error;
534
+ }
535
+ return blocks;
536
+ }
537
+ bridged(conversation) {
538
+ if (conversation.bridge === null) {
539
+ const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
540
+ conversation.bridge = bridging;
541
+ bridging.catch(() => {
542
+ if (conversation.bridge === bridging)
543
+ conversation.bridge = null;
544
+ });
545
+ }
546
+ return conversation.bridge;
547
+ }
548
+ closeBridge(conversation, bridging, bridge) {
549
+ bridge.close();
550
+ if (conversation.bridge === bridging)
551
+ conversation.bridge = null;
552
+ }
553
+ closeIdleBridge(conversation) {
554
+ const bridging = conversation.bridge;
555
+ if (bridging === null || conversation.running !== null || conversation.launch !== null)
556
+ return;
557
+ conversation.bridge = null;
558
+ void bridging.then((bridge) => bridge.close(), () => undefined);
288
559
  }
289
560
  async interrupt(conversation, turnId) {
290
561
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -295,11 +566,13 @@ export class Conversations {
295
566
  }
296
567
  halt(conversation) {
297
568
  if (conversation.opening !== null)
298
- stop(conversation.opening);
569
+ killMarked(marker(conversation.opening));
570
+ for (const scope of conversation.leftovers)
571
+ scope.kill();
299
572
  if (conversation.running === null)
300
573
  return;
301
574
  conversation.running.killed = true;
302
- stop(conversation.running.bridge);
575
+ conversation.running.scope.kill();
303
576
  }
304
577
  async kill(conversation) {
305
578
  const running = conversation.running;
@@ -337,53 +610,78 @@ export class Conversations {
337
610
  }
338
611
  return route;
339
612
  }
340
- async start(conversation, route, sessionId) {
613
+ async start(conversation, routed, sessionId, empty, bridged) {
614
+ const route = await routed;
341
615
  const reader = readerFor(route.kind);
342
616
  reader.session = sessionId;
343
617
  const app = client({ name: '@agentshouse/kit' })
344
618
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
345
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
619
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
620
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
346
621
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
347
- const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
622
+ const bridging = this.bridged(conversation);
623
+ const bridge = await step(unbridged, bridging);
348
624
  if (conversation.kills > 0) {
349
- bridge.close();
625
+ this.closeBridge(conversation, bridging, bridge);
350
626
  throw new Error(KILLED);
351
627
  }
352
628
  conversation.opening = bridge;
629
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
630
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
631
+ let adapter = null;
353
632
  try {
354
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
355
- await CLIS[route.kind].allowHouse();
356
- conversation.busy.clear();
357
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => this.read(conversation, reader, message), bridge.env);
358
- void adapter.exited.then(() => bridge.close());
633
+ bridged(bridge);
634
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
635
+ const streamed = await step(NO_CONNECTION, work.open(cli));
636
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
637
+ work.read(message);
638
+ this.read(conversation, reader, message);
639
+ }, { ...bridge.env, ...streamed }, undefined, scope));
640
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
359
641
  const agent = adapter.connection.agent;
360
642
  const cwd = route.working_directory;
361
- const _meta = CLIS[route.kind].sessionMeta;
362
- const opened = sessionId === null
363
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
364
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
643
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
644
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
645
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
365
646
  reader.session = opened.sessionId;
366
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, baseline: null };
367
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
368
- running.baseline = processes(marker(bridge));
647
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
648
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
649
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
650
+ }
651
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
652
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
653
+ void work.settled().then(() => scope.record());
369
654
  conversation.running = running;
655
+ adapter.turn(conversation.open > 0);
370
656
  conversation.session = running.sessionId;
371
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
372
- if (conversation.commands !== null)
373
- this.commands(conversation, conversation.commands);
374
- this.options(conversation, options);
375
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
657
+ conversation.options = options;
658
+ this.facts(conversation);
659
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
376
660
  return running.sessionId;
377
661
  }
378
662
  catch (error) {
379
- stop(bridge);
380
- bridge.close();
663
+ adapter?.retire();
664
+ work.close();
665
+ conversation.leftovers.add(scope);
666
+ this.closeBridge(conversation, bridging, bridge);
381
667
  throw conversation.kills > 0 ? new Error(KILLED) : error;
382
668
  }
383
669
  finally {
384
670
  conversation.opening = null;
385
671
  }
386
672
  }
673
+ async resume(conversation, kind, adapter, request, empty) {
674
+ try {
675
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
676
+ }
677
+ catch (error) {
678
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
679
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
680
+ }
681
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
682
+ return null;
683
+ }
684
+ }
387
685
  async launchSettings(running, route, offered) {
388
686
  let options = offered;
389
687
  const settings = [
@@ -405,26 +703,37 @@ export class Conversations {
405
703
  }
406
704
  return options;
407
705
  }
408
- exited(conversation, adapter, cause) {
409
- const running = conversation.running;
410
- if (running?.adapter !== adapter)
411
- return;
412
- this.detach(conversation);
706
+ exited(conversation, running, cause) {
707
+ this.detach(conversation, running);
413
708
  if (conversation.turn !== null) {
414
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
709
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
415
710
  }
416
711
  this.next(conversation);
417
712
  this.kit.changed();
418
713
  }
419
- detach(conversation) {
714
+ detach(conversation, running) {
715
+ if (conversation.bridge === running.bridging)
716
+ conversation.bridge = null;
717
+ running.work.close();
718
+ conversation.leftovers.add(running.scope);
420
719
  conversation.running = null;
421
720
  conversation.commands = null;
422
- conversation.busy.clear();
721
+ conversation.options = null;
423
722
  for (const [id, question] of this.questions) {
424
723
  if (question.conversation === conversation)
425
724
  this.questions.delete(id);
426
725
  }
427
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
726
+ this.process(conversation, false);
727
+ }
728
+ process(conversation, running) {
729
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
730
+ }
731
+ facts(conversation) {
732
+ this.process(conversation, true);
733
+ if (conversation.commands !== null)
734
+ this.commands(conversation, conversation.commands);
735
+ if (conversation.options !== null)
736
+ this.options(conversation, conversation.options);
428
737
  }
429
738
  commands(conversation, commands) {
430
739
  conversation.commands = commands;
@@ -435,6 +744,7 @@ export class Conversations {
435
744
  options(conversation, options) {
436
745
  if (conversation.running === null)
437
746
  return;
747
+ conversation.options = options;
438
748
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
439
749
  }
440
750
  deliver(conversation, turn, event, body) {
@@ -449,7 +759,7 @@ export class Conversations {
449
759
  return delivered;
450
760
  }
451
761
  write(conversation, turn, operation) {
452
- if (!turn.admitted || turn.ended || !this.watched)
762
+ if (turn.ended || !this.watched)
453
763
  return;
454
764
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
455
765
  }
@@ -460,61 +770,74 @@ export class Conversations {
460
770
  begin(conversation) {
461
771
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
462
772
  conversation.open++;
463
- void this.deliver(conversation, turn, 'started', {})
464
- .then(() => {
465
- turn.admitted = true;
466
- this.replay(conversation, turn);
467
- })
468
- .catch(logged);
773
+ conversation.running?.adapter.turn(true);
774
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
469
775
  conversation.turn = turn;
470
776
  if (conversation.context !== null)
471
777
  turn.parts.measure(conversation.context);
472
778
  for (const part of conversation.held.splice(0))
473
779
  turn.parts.start(part);
474
- for (const event of conversation.running?.reader.turnStarted() ?? [])
475
- this.event(conversation, event);
476
780
  this.kit.changed();
477
781
  return turn;
478
782
  }
479
- end(conversation, turn, failed) {
783
+ end(conversation, turn, failed, notice = null) {
480
784
  if (turn.ended)
481
785
  return;
786
+ if (failed !== null)
787
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
482
788
  if (conversation.turn === turn) {
483
789
  for (const event of conversation.running?.reader.closeTurn() ?? [])
484
790
  this.apply(conversation, turn, event);
485
791
  }
792
+ if (notice !== null)
793
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
486
794
  turn.ended = true;
487
795
  conversation.open--;
796
+ conversation.running?.adapter.turn(conversation.open > 0);
488
797
  if (conversation.turn === turn)
489
798
  conversation.turn = null;
490
799
  const body = Promise.allSettled(turn.uploads).then(() => ({
491
- parts: turn.parts.parts,
800
+ parts: turn.parts.saved(),
492
801
  context: turn.parts.context,
493
- ...failed,
802
+ ...(failed === null ? {} : { failed: failed.failure }),
494
803
  }));
495
804
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
496
805
  this.next(conversation);
497
806
  this.kit.changed();
498
807
  }
499
- prompt(conversation, prompt) {
808
+ prompt(conversation, prompt, taken) {
500
809
  const running = conversation.running;
501
810
  const turn = this.begin(conversation);
502
811
  turn.prompted = true;
503
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
812
+ turn.taken = taken;
813
+ conversation.prompted = running.sessionId;
814
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
815
+ this.took(turn);
816
+ const notice = answeredFailure(answer);
817
+ if (notice !== null) {
818
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
819
+ }
820
+ else {
821
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
822
+ }
823
+ }, async (error) => {
504
824
  if (running.adapter.connection.signal.aborted) {
505
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
825
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
506
826
  }
507
827
  else {
828
+ this.took(turn);
508
829
  if (conversation.open === 1 && conversation.running === running) {
509
- stop(running.bridge);
510
- this.detach(conversation);
830
+ running.adapter.retire();
831
+ this.detach(conversation, running);
511
832
  }
512
- this.end(conversation, turn, { failed: causeOf(error) });
833
+ this.end(conversation, turn, answeredWith(error));
513
834
  }
514
835
  });
515
836
  }
516
837
  started(conversation) {
517
- return conversation.turn ?? this.begin(conversation);
838
+ const turn = conversation.turn ?? this.begin(conversation);
839
+ this.took(turn);
840
+ return turn;
518
841
  }
519
842
  ended(conversation) {
520
843
  const turn = conversation.turn;
@@ -592,20 +915,6 @@ export class Conversations {
592
915
  case 'options':
593
916
  this.options(conversation, event.options);
594
917
  return;
595
- case 'busy':
596
- if (event.running)
597
- conversation.busy.add(event.key);
598
- else
599
- conversation.busy.delete(event.key);
600
- this.kit.changed();
601
- return;
602
- case 'servers': {
603
- const running = conversation.running;
604
- if (running?.baseline)
605
- for (const found of processes(marker(running.bridge)))
606
- running.baseline.add(found);
607
- return;
608
- }
609
918
  case 'context':
610
919
  conversation.context = { used: event.used, window: event.window };
611
920
  conversation.turn?.parts.measure(conversation.context);
@@ -623,6 +932,11 @@ export class Conversations {
623
932
  }
624
933
  this.apply(conversation, this.started(conversation), event);
625
934
  }
935
+ took(turn) {
936
+ const taken = turn.taken;
937
+ turn.taken = null;
938
+ taken?.();
939
+ }
626
940
  apply(conversation, turn, event) {
627
941
  switch (event.kind) {
628
942
  case 'text':
@@ -647,7 +961,7 @@ export class Conversations {
647
961
  turn.parts.plan(event.entries);
648
962
  return;
649
963
  case 'image':
650
- turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
964
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
651
965
  this.kit.changed();
652
966
  return;
653
967
  }
@@ -667,23 +981,30 @@ export class Conversations {
667
981
  else
668
982
  conversation.turn.parts.start(part);
669
983
  }
670
- async image(conversation, turn, data, mediaType) {
984
+ async image(conversation, turn, reserved, data, mediaType) {
671
985
  const bytes = Buffer.from(data, 'base64');
672
986
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
987
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
673
988
  try {
674
989
  const declared = (await this.deliver(conversation, turn, 'images', {
675
990
  name,
676
991
  media_type: mediaType,
677
992
  bytes: bytes.length,
678
- sha256: createHash('sha256').update(bytes).digest('hex'),
993
+ sha256,
679
994
  }));
680
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
681
- if (saved.save.status !== 'saved')
682
- throw new Error(saved.save.failure ?? saved.save.status);
683
- turn.parts.start({ type: 'image', file: saved.version, name, media_type: mediaType });
995
+ const saved = await uploaded(declared.upload, {
996
+ name,
997
+ bytes: bytes.length,
998
+ sha256,
999
+ read: (start, end) => [bytes.subarray(start, end)],
1000
+ send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
1001
+ finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
1002
+ });
1003
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
684
1004
  }
685
1005
  catch (error) {
686
- turn.parts.start({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` });
1006
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
1007
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
687
1008
  }
688
1009
  }
689
1010
  }