@agentshouse/kit 0.2.1-alpha.8 → 0.2.1-c031c7457a661

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 (54) hide show
  1. package/README.md +49 -24
  2. package/dist/abort.js +9 -0
  3. package/dist/acp.js +49 -86
  4. package/dist/agents.js +333 -52
  5. package/dist/api.js +40 -4
  6. package/dist/app-git.js +48 -0
  7. package/dist/bootstrap.json +10 -14
  8. package/dist/bridge.js +93 -78
  9. package/dist/calls.js +63 -0
  10. package/dist/chatgpt.js +32 -0
  11. package/dist/clis-main.js +23 -0
  12. package/dist/clis.js +217 -72
  13. package/dist/codex-main.js +58 -0
  14. package/dist/configure-main.js +6 -0
  15. package/dist/connection.js +126 -0
  16. package/dist/conversations.js +642 -261
  17. package/dist/copies.js +237 -173
  18. package/dist/end.js +27 -0
  19. package/dist/enrol-main.js +7 -1
  20. package/dist/events.js +525 -0
  21. package/dist/files.js +140 -17
  22. package/dist/git-main.js +2 -2
  23. package/dist/git.js +16 -10
  24. package/dist/home.js +141 -10
  25. package/dist/house-cli.js +300 -0
  26. package/dist/house-main.js +5 -102
  27. package/dist/instructions.js +16 -17
  28. package/dist/kit-cli.js +68 -0
  29. package/dist/kit-main.js +8 -16
  30. package/dist/login.js +128 -95
  31. package/dist/logout.js +19 -0
  32. package/dist/mcp.js +41 -0
  33. package/dist/operation.js +7 -4
  34. package/dist/parts.js +82 -0
  35. package/dist/placement.js +39 -0
  36. package/dist/refusals.js +53 -0
  37. package/dist/relay.js +20 -0
  38. package/dist/resident.js +140 -51
  39. package/dist/scope.js +206 -0
  40. package/dist/secret-input.js +7 -2
  41. package/dist/service-main.js +9 -0
  42. package/dist/service.js +55 -0
  43. package/dist/shell.js +3 -0
  44. package/dist/sign-in.js +50 -31
  45. package/dist/skills.js +66 -0
  46. package/dist/transcription.js +201 -0
  47. package/dist/translate.js +2 -8
  48. package/dist/update.js +138 -0
  49. package/dist/web-wizard.js +100 -0
  50. package/dist/wire.js +22 -0
  51. package/dist/work.js +346 -0
  52. package/package.json +8 -8
  53. package/dist/blocks.js +0 -51
  54. package/dist/stream.js +0 -47
@@ -1,64 +1,152 @@
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
- import { killMarked, startAdapter, TURN_ENDED, TURN_STARTED } from './acp.js';
6
- import { blocksOf, firstChange } from './blocks.js';
5
+ import { unlessAborted } from './abort.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 { placeFiles } from './files.js';
10
+ import { answeredFailure, readerFor } from './events.js';
11
+ import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
12
+ import { agentBase } from './home.js';
10
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';
11
18
  import { holdSecretInput } from './secret-input.js';
12
- const LAUNCH_BASE = '/agents/house';
19
+ import { transcribe } from './transcription.js';
20
+ import { workFor } from './work.js';
21
+ const SIGNED_OUT = RequestError.authRequired().code;
13
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']);
14
69
  function causeOf(error) {
15
70
  return error instanceof Error ? error.message : String(error);
16
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
+ }
17
78
  function answerMessage(request, response) {
18
79
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
19
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
+ }
20
87
  function logged(error) {
21
88
  process.stderr.write(`kit: ${causeOf(error)}\n`);
22
89
  }
23
- function unlessKilled(work, signal) {
24
- return new Promise((resolve, reject) => {
25
- const killed = () => reject(signal.reason);
26
- signal.addEventListener('abort', killed, { once: true });
27
- if (signal.aborted)
28
- killed();
29
- work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
30
- });
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);
31
96
  }
32
- function stop(bridge) {
33
- killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
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}`;
34
105
  }
35
106
  class Turn {
36
107
  id = randomUUID();
37
- text = '';
38
- sent = [];
39
- unsentFrom = null;
40
- draftSequence = 0;
41
- planSequence = 0;
42
- entries = null;
108
+ parts;
43
109
  questions = 0;
44
110
  secrets = 0;
45
- prompt = null;
111
+ prompted = false;
112
+ taken = null;
46
113
  ended = false;
114
+ images = 0;
115
+ uploads = [];
116
+ constructor(emit) {
117
+ this.parts = new Parts(emit);
118
+ }
47
119
  }
48
120
  class Conversation {
49
121
  id;
50
122
  running = null;
123
+ session = null;
51
124
  turn = null;
52
125
  commands = null;
53
- jobs = new Set();
54
- queued = [];
55
- 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 = [];
56
134
  opening = null;
135
+ bridge = null;
136
+ launch = null;
137
+ prompted = null;
57
138
  kills = 0;
58
139
  killed = new AbortController();
140
+ interrupts = 0;
141
+ interrupted = new AbortController();
59
142
  open = 0;
60
143
  queue = Promise.resolve();
61
144
  reports = Promise.resolve();
145
+ starting = null;
146
+ preparing = null;
147
+ told = null;
148
+ untold = false;
149
+ shown = null;
62
150
  constructor(id) {
63
151
  this.id = id;
64
152
  }
@@ -74,51 +162,80 @@ function asksSecret(request) {
74
162
  export class Conversations {
75
163
  kit;
76
164
  conversations = new Map();
77
- acks = new Map();
78
165
  questions = new Map();
79
- carrying = 0;
80
166
  reporting = 0;
167
+ watched = false;
81
168
  constructor(kit) {
82
169
  this.kit = kit;
83
- }
84
- input(input) {
85
- this.carrying++;
86
- const carried = this.acks.get(input.input_id) ??
87
- (() => {
88
- const conversation = this.conversation(input.conversation_id);
89
- if (input.kind === 'kill') {
90
- conversation.kills++;
91
- conversation.killed.abort(new Error(KILLED));
92
- this.halt(conversation);
93
- }
94
- const work = conversation.queue.then(() => this.carry(conversation, input));
95
- conversation.queue = work.catch(() => undefined);
96
- this.acks.set(input.input_id, work);
97
- return work;
98
- })();
99
- carried
100
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
101
- .catch(logged)
102
- .finally(() => {
103
- this.carrying--;
104
- 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 }));
105
190
  });
106
191
  }
107
192
  idle() {
108
- if (this.carrying > 0 || this.reporting > 0)
193
+ if (this.reporting > 0)
109
194
  return false;
110
195
  for (const conversation of this.conversations.values()) {
111
196
  const turn = conversation.turn;
112
197
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
113
- if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
198
+ if (conversation.open > (asking ? 1 : 0))
114
199
  return false;
115
200
  }
116
- 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();
117
219
  }
118
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;
119
236
  for (const conversation of this.conversations.values()) {
120
237
  if (conversation.turn !== null)
121
- conversation.turn.unsentFrom = 0;
238
+ this.replay(conversation, conversation.turn);
122
239
  }
123
240
  }
124
241
  conversation(id) {
@@ -129,69 +246,249 @@ export class Conversations {
129
246
  }
130
247
  return conversation;
131
248
  }
132
- async carry(conversation, input) {
133
- if (input.kind === 'open')
134
- return this.open(conversation, input);
135
- if (input.kind === 'message')
136
- return this.message(conversation, input);
137
- if (input.kind === 'interrupt')
138
- await this.interrupt(conversation, String(input.turn_id));
139
- if (input.kind === 'kill') {
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') {
256
+ if (--conversation.interrupts === 0)
257
+ conversation.interrupted = new AbortController();
258
+ await this.interrupt(conversation, String(call.turn_id));
259
+ }
260
+ if (call.kind === 'kill') {
140
261
  await this.kill(conversation);
262
+ await conversation.sending;
141
263
  if (--conversation.kills === 0)
142
264
  conversation.killed = new AbortController();
143
265
  }
144
- if (input.kind === 'option')
145
- await this.option(conversation, String(input.option), input.value);
146
- if (input.kind === 'answer') {
147
- 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));
148
270
  if (question === undefined) {
149
- 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 });
150
273
  }
151
- question.answer(input.response);
274
+ question.answer(call.response);
152
275
  }
153
- return {};
276
+ return { result: Promise.resolve({}) };
154
277
  }
155
- async open(conversation, input) {
278
+ open(conversation, call) {
156
279
  if (conversation.running !== null)
157
- 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;
158
324
  try {
159
- 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);
160
332
  }
161
333
  catch (error) {
162
- 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;
163
355
  }
356
+ conversation.sending = this.send(conversation, waiting).finally(() => {
357
+ conversation.sending = null;
358
+ this.next(conversation);
359
+ });
164
360
  }
165
- async message(conversation, input) {
166
- let ack = {};
361
+ async send(conversation, waiting) {
362
+ const { call } = waiting;
363
+ let result = {};
364
+ let started = false;
365
+ let instructing = waiting.instructing;
167
366
  const prompt = [];
168
367
  try {
169
- const route = await this.route(conversation, input.agent_id);
170
- const paths = await placeFiles(this.kit.house, route.working_directory, input.files, conversation.killed.signal);
171
368
  if (conversation.running === null) {
172
- const opened = await this.start(conversation, route, input.provider_session_id);
173
- if (input.provider_session_id === null)
174
- 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 };
175
383
  }
176
- if (input.first === true) {
177
- prompt.push({
178
- type: 'text',
179
- text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
180
- });
384
+ if (call.first === true) {
385
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
181
386
  }
182
- prompt.push({ type: 'text', text: String(input.text) });
183
- if (paths.length > 0)
184
- prompt.push({ type: 'text', text: paths.join('\n') });
185
387
  if (conversation.kills > 0)
186
388
  throw new Error(KILLED);
187
389
  }
188
390
  catch (error) {
189
- if ('provider_session_id' in ack)
391
+ this.starts(conversation, null);
392
+ if (started) {
393
+ conversation.session = null;
190
394
  await this.kill(conversation);
191
- 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;
413
+ }
414
+ conversation.shown = setTimeout(() => {
415
+ conversation.shown = null;
416
+ if (conversation.starting === step)
417
+ this.tell(conversation, step);
418
+ }, STARTING_SHOWN_AFTER_MS);
419
+ }
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}` });
461
+ }
462
+ }
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
+ });
192
478
  }
193
- this.prompt(conversation, prompt);
194
- return ack;
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);
195
492
  }
196
493
  async interrupt(conversation, turnId) {
197
494
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -202,11 +499,13 @@ export class Conversations {
202
499
  }
203
500
  halt(conversation) {
204
501
  if (conversation.opening !== null)
205
- stop(conversation.opening);
502
+ killMarked(marker(conversation.opening));
503
+ for (const scope of conversation.leftovers)
504
+ scope.kill();
206
505
  if (conversation.running === null)
207
506
  return;
208
507
  conversation.running.killed = true;
209
- stop(conversation.running.bridge);
508
+ conversation.running.scope.kill();
210
509
  }
211
510
  async kill(conversation) {
212
511
  const running = conversation.running;
@@ -218,6 +517,7 @@ export class Conversations {
218
517
  if (running === null)
219
518
  return;
220
519
  try {
520
+ running.reader.requested(option, value);
221
521
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
222
522
  sessionId: running.sessionId,
223
523
  configId: option,
@@ -226,72 +526,98 @@ export class Conversations {
226
526
  this.options(conversation, answered.configOptions);
227
527
  }
228
528
  catch (error) {
529
+ running.reader.refused(option);
229
530
  logged(error);
230
531
  }
231
532
  }
232
533
  async route(conversation, agentId) {
233
534
  const signal = conversation.killed.signal;
234
- await unlessKilled(this.kit.agents.read(), signal);
535
+ await unlessAborted(this.kit.agents.read(), signal);
235
536
  if (this.kit.agents.route(agentId) === undefined)
236
- await unlessKilled(this.kit.agents.refresh(), signal);
537
+ await unlessAborted(this.kit.agents.refresh(), signal);
237
538
  const route = this.kit.agents.route(agentId);
238
539
  if (route === undefined)
239
540
  throw new Error(`this Environment hosts no Agent ${agentId}`);
240
- if (route.working_directory === join(LAUNCH_BASE, route.agent_id)) {
541
+ if (route.working_directory === join(agentBase(), route.agent_id)) {
241
542
  await mkdir(route.working_directory, { recursive: true });
242
543
  }
243
544
  return route;
244
545
  }
245
- 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;
246
550
  const app = client({ name: '@agentshouse/kit' })
247
- .onNotification('session/update', ({ params }) => this.update(conversation, params))
248
- .onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
249
- .onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
250
551
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
251
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
252
- await unlessKilled(this.kit.agents.installed(), conversation.killed.signal);
253
- const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.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));
554
+ const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
555
+ const bridging = this.bridged(conversation);
556
+ const bridge = await step(unbridged, bridging);
254
557
  if (conversation.kills > 0) {
255
- bridge.close();
558
+ this.closeBridge(conversation, bridging, bridge);
256
559
  throw new Error(KILLED);
257
560
  }
258
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;
259
565
  try {
260
- const adapter = await startAdapter(route.kind, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
261
- 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));
262
574
  const agent = adapter.connection.agent;
263
575
  const cwd = route.working_directory;
264
- const opened = sessionId === null
265
- ? await agent.request('session/new', { cwd, mcpServers: [] })
266
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [] })), sessionId };
267
- const running = {
268
- adapter,
269
- bridge,
270
- sessionId: opened.sessionId,
271
- killed: false,
272
- queues: CLIS[route.kind].queues,
273
- };
274
- 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());
275
587
  conversation.running = running;
276
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
277
- if (conversation.commands !== null)
278
- this.commands(conversation, conversation.commands);
279
- this.options(conversation, options);
280
- 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));
281
592
  return running.sessionId;
282
593
  }
283
594
  catch (error) {
284
- stop(bridge);
285
- bridge.close();
595
+ adapter?.retire();
596
+ work.close();
597
+ conversation.leftovers.add(scope);
598
+ this.closeBridge(conversation, bridging, bridge);
286
599
  throw conversation.kills > 0 ? new Error(KILLED) : error;
287
600
  }
288
601
  finally {
289
602
  conversation.opening = null;
290
603
  }
291
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
+ }
292
617
  async launchSettings(running, route, offered) {
293
618
  let options = offered;
294
619
  const settings = [
620
+ ['mode', route.mode ?? CLIS[route.kind].fullAccess],
295
621
  ['model', route.model],
296
622
  ['thought_level', route.effort],
297
623
  ];
@@ -299,6 +625,7 @@ export class Conversations {
299
625
  if (value === null)
300
626
  continue;
301
627
  const option = options.find((candidate) => candidate.category === category);
628
+ running.reader.requested(option?.id ?? category, value);
302
629
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
303
630
  sessionId: running.sessionId,
304
631
  configId: option?.id ?? category,
@@ -308,23 +635,37 @@ export class Conversations {
308
635
  }
309
636
  return options;
310
637
  }
311
- exited(conversation, adapter, cause) {
312
- const running = conversation.running;
313
- if (running?.adapter !== adapter)
314
- 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);
315
651
  conversation.running = null;
316
652
  conversation.commands = null;
317
- conversation.jobs.clear();
318
- conversation.queued.length = 0;
653
+ conversation.options = null;
319
654
  for (const [id, question] of this.questions) {
320
655
  if (question.conversation === conversation)
321
656
  this.questions.delete(id);
322
657
  }
323
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
324
- if (conversation.turn !== null) {
325
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
326
- }
327
- 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);
328
669
  }
329
670
  commands(conversation, commands) {
330
671
  conversation.commands = commands;
@@ -335,11 +676,12 @@ export class Conversations {
335
676
  options(conversation, options) {
336
677
  if (conversation.running === null)
337
678
  return;
679
+ conversation.options = options;
338
680
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
339
681
  }
340
682
  deliver(conversation, turn, event, body) {
341
683
  this.reporting++;
342
- 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));
343
685
  conversation.reports = delivered
344
686
  .catch(() => undefined)
345
687
  .finally(() => {
@@ -348,105 +690,95 @@ export class Conversations {
348
690
  });
349
691
  return delivered;
350
692
  }
351
- report(conversation) {
352
- const turn = new Turn();
353
- conversation.open++;
354
- void this.deliver(conversation, turn, 'started', {})
355
- .then(() => this.admitted(conversation, turn))
356
- .catch(logged);
357
- 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 });
358
697
  }
359
- admitted(conversation, turn) {
360
- if (turn.text !== '') {
361
- turn.unsentFrom = 0;
362
- this.draft(conversation, turn);
363
- }
364
- if (turn.entries !== null)
365
- this.plan(conversation, turn);
698
+ replay(conversation, turn) {
699
+ for (const operation of turn.parts.operations())
700
+ this.write(conversation, turn, operation);
366
701
  }
367
702
  begin(conversation) {
368
- 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);
369
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);
370
711
  this.kit.changed();
371
712
  return turn;
372
713
  }
373
- end(conversation, turn, outcome) {
714
+ end(conversation, turn, failed, notice = null) {
374
715
  if (turn.ended)
375
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 });
376
725
  turn.ended = true;
377
726
  conversation.open--;
378
727
  if (conversation.turn === turn)
379
728
  conversation.turn = null;
380
- void this.deliver(conversation, turn, 'ended', outcome).catch(logged);
381
- const late = conversation.open === 0 ? conversation.late.shift() : undefined;
382
- if (late !== undefined)
383
- 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);
384
736
  this.kit.changed();
385
737
  }
386
- failed(conversation, cause) {
387
- if (conversation.open > 0)
388
- conversation.late.push(cause);
389
- else
390
- this.end(conversation, this.report(conversation), { failed: cause });
391
- }
392
- prompt(conversation, prompt) {
738
+ prompt(conversation, prompt, taken) {
393
739
  const running = conversation.running;
394
- const sent = { turn: null };
395
- if (running.queues && (conversation.turn !== null || conversation.queued.length > 0)) {
396
- conversation.queued.push(sent);
397
- }
398
- else {
399
- this.attach(conversation.turn ?? this.begin(conversation), sent);
400
- }
401
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
402
- if (!running.adapter.connection.signal.aborted)
403
- this.answered(conversation, sent, causeOf(error));
404
- else if (sent.turn !== null) {
405
- this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
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);
752
+ }
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));
406
764
  }
407
765
  });
408
766
  }
409
- attach(turn, sent) {
410
- turn.prompt = sent;
411
- sent.turn = turn;
412
- }
413
- answered(conversation, sent, failed) {
414
- const turn = sent.turn;
415
- if (turn === null) {
416
- const queued = conversation.queued.indexOf(sent);
417
- if (queued < 0)
418
- return;
419
- conversation.queued.splice(queued, 1);
420
- if (failed !== null)
421
- this.failed(conversation, failed);
422
- }
423
- else if (failed !== null) {
424
- if (turn.ended)
425
- this.failed(conversation, failed);
426
- else
427
- this.end(conversation, turn, { failed });
428
- }
429
- else if (turn.prompt === sent) {
430
- this.end(conversation, turn, { text: turn.text });
431
- }
432
- }
433
767
  started(conversation) {
434
- if (conversation.turn !== null)
435
- return conversation.turn;
436
- const turn = this.begin(conversation);
437
- const queued = conversation.queued.shift();
438
- if (queued !== undefined)
439
- this.attach(turn, queued);
768
+ const turn = conversation.turn ?? this.begin(conversation);
769
+ this.took(turn);
440
770
  return turn;
441
771
  }
442
772
  ended(conversation) {
443
773
  const turn = conversation.turn;
444
774
  if (turn === null)
445
775
  return;
446
- if (turn.prompt === null) {
447
- this.end(conversation, turn, { text: turn.text });
776
+ if (!turn.prompted) {
777
+ this.end(conversation, turn, null);
448
778
  }
449
779
  else {
780
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
781
+ this.apply(conversation, turn, event);
450
782
  conversation.turn = null;
451
783
  this.kit.changed();
452
784
  }
@@ -495,65 +827,114 @@ export class Conversations {
495
827
  if (content !== null)
496
828
  this.questions.get(interactionId)?.answer({ action: 'accept', content });
497
829
  }
498
- update(conversation, notification) {
499
- const update = notification.update;
500
- if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
501
- const turn = this.started(conversation);
502
- turn.text += update.content.text;
503
- this.draft(conversation, turn);
504
- }
505
- else if (update.sessionUpdate === 'plan') {
506
- const turn = this.started(conversation);
507
- turn.entries = update.entries;
508
- this.plan(conversation, turn);
509
- }
510
- else if (update.sessionUpdate === 'agent_thought_chunk' || update.sessionUpdate === 'tool_call') {
511
- this.started(conversation);
512
- }
513
- else if (update.sessionUpdate === 'available_commands_update') {
514
- this.commands(conversation, update.availableCommands);
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;
515
862
  }
516
- else if (update.sessionUpdate === 'config_option_update') {
517
- this.options(conversation, update.configOptions);
863
+ this.apply(conversation, this.started(conversation), event);
864
+ }
865
+ took(turn) {
866
+ const taken = turn.taken;
867
+ turn.taken = null;
868
+ taken?.();
869
+ }
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;
518
897
  }
519
898
  }
520
- job(conversation, job) {
521
- if (job.running)
522
- conversation.jobs.add(job.id);
899
+ subagentDone(conversation, id, result) {
900
+ const subagent = conversation.subagents.get(id);
901
+ if (subagent === undefined)
902
+ return;
903
+ conversation.subagents.delete(id);
904
+ if (subagent.turn === conversation.turn) {
905
+ subagent.turn.parts.done(subagent.index, result);
906
+ return;
907
+ }
908
+ const part = { type: 'subagent', description: subagent.description, status: 'done', result };
909
+ if (conversation.turn === null)
910
+ conversation.held.push(part);
523
911
  else
524
- conversation.jobs.delete(job.id);
525
- this.kit.changed();
912
+ conversation.turn.parts.start(part);
526
913
  }
527
- draft(conversation, turn) {
528
- const blocks = blocksOf(turn.text);
529
- const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
530
- const sent = this.kit.send({
531
- type: 'draft',
532
- conversation_id: conversation.id,
533
- turn_id: turn.id,
534
- sequence: turn.draftSequence + 1,
535
- from,
536
- blocks: blocks.slice(from),
537
- });
538
- if (sent) {
539
- turn.draftSequence++;
540
- turn.sent = blocks;
541
- turn.unsentFrom = null;
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);
542
934
  }
543
- else {
544
- 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);
545
938
  }
546
939
  }
547
- plan(conversation, turn) {
548
- const sent = this.kit.send({
549
- type: 'plan',
550
- conversation_id: conversation.id,
551
- turn_id: turn.id,
552
- sequence: turn.planSequence + 1,
553
- from: 0,
554
- steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
555
- });
556
- if (sent)
557
- turn.planSequence++;
558
- }
559
940
  }