@agentshouse/kit 0.2.1-cde47fd72fe59 → 0.2.1-ce3d2bb67177c

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.
@@ -4,15 +4,16 @@ import { mkdir } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { unlessAborted } from './abort.js';
6
6
  import { startAdapter } from './acp.js';
7
- import { finalRefusal } from './api.js';
7
+ import { finalRefusal, HouseRefusal } from './api.js';
8
8
  import { openBridge } from './bridge.js';
9
9
  import { CLIS } from './clis.js';
10
10
  import { answeredFailure, readerFor } from './events.js';
11
- import { placeFiles, uploadBytes } from './files.js';
11
+ import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
12
12
  import { agentBase } from './home.js';
13
13
  import { instructions } from './instructions.js';
14
14
  import { Parts } from './parts.js';
15
15
  import { refusalOf } from './refusals.js';
16
+ import { relayed } from './relay.js';
16
17
  import { killMarked, Scope } from './scope.js';
17
18
  import { holdSecretInput } from './secret-input.js';
18
19
  import { createHowWeWork } from './skills.js';
@@ -20,6 +21,7 @@ import { transcribe } from './transcription.js';
20
21
  import { workFor } from './work.js';
21
22
  const SIGNED_OUT = RequestError.authRequired().code;
22
23
  const KILLED = 'the conversation was killed';
24
+ const INTERRUPTED = 'the conversation was interrupted';
23
25
  const REFUSED = 'the CLI refused the prompt';
24
26
  const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
25
27
  const GONE = 'House no longer holds this conversation; start a new one';
@@ -31,9 +33,12 @@ const ENDED = {
31
33
  const NO_START = "the Agent's CLI did not start";
32
34
  const NO_SESSION = "the Agent's CLI did not open a session";
33
35
  const NO_RESUME = "the Agent's CLI could not resume this conversation";
36
+ const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
34
37
  const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
35
38
  class NotStarted extends Error {
36
39
  }
40
+ class ResumeRefused extends NotStarted {
41
+ }
37
42
  async function step(reason, work) {
38
43
  try {
39
44
  return await work;
@@ -42,6 +47,14 @@ async function step(reason, work) {
42
47
  throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
43
48
  }
44
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;
45
58
  function unbridged(error) {
46
59
  return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
47
60
  }
@@ -53,13 +66,12 @@ const EXIT_PLAN_MODE = {
53
66
  return value;
54
67
  },
55
68
  };
56
- export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
69
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
57
70
  function causeOf(error) {
58
71
  return error instanceof Error ? error.message : String(error);
59
72
  }
60
73
  function answeredWith(error) {
61
- const signedOut = typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
62
- return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
74
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
63
75
  }
64
76
  function exitedWith(running, cause) {
65
77
  return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
@@ -67,16 +79,28 @@ function exitedWith(running, cause) {
67
79
  function answerMessage(request, response) {
68
80
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
69
81
  }
82
+ async function sentPart(url, bytes) {
83
+ const answer = await fetch(relayed(url), { method: 'PUT', body: bytes });
84
+ await answer.arrayBuffer();
85
+ if (!answer.ok)
86
+ throw storeRefusal('upload', answer.status);
87
+ }
70
88
  function logged(error) {
71
89
  process.stderr.write(`kit: ${causeOf(error)}\n`);
72
90
  }
73
91
  function refusal(conversation, error) {
74
92
  if (conversation.kills > 0)
75
93
  return KILLED;
76
- const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
94
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
77
95
  logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
78
96
  return causeOf(error);
79
97
  }
98
+ function audioOf(result) {
99
+ if (result.isError === true)
100
+ return null;
101
+ const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
102
+ return file?.media_type?.startsWith('audio/') === true ? file : null;
103
+ }
80
104
  function marker(bridge) {
81
105
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
82
106
  }
@@ -86,6 +110,7 @@ class Turn {
86
110
  questions = 0;
87
111
  secrets = 0;
88
112
  prompted = false;
113
+ taken = null;
89
114
  ended = false;
90
115
  images = 0;
91
116
  uploads = [];
@@ -99,6 +124,8 @@ class Conversation {
99
124
  session = null;
100
125
  turn = null;
101
126
  commands = null;
127
+ options = null;
128
+ exitUntold = false;
102
129
  context = null;
103
130
  leftovers = new Set();
104
131
  waiting = [];
@@ -106,6 +133,8 @@ class Conversation {
106
133
  subagents = new Map();
107
134
  held = [];
108
135
  opening = null;
136
+ bridge = null;
137
+ launch = null;
109
138
  prompted = null;
110
139
  kills = 0;
111
140
  killed = new AbortController();
@@ -114,6 +143,11 @@ class Conversation {
114
143
  open = 0;
115
144
  queue = Promise.resolve();
116
145
  reports = Promise.resolve();
146
+ starting = null;
147
+ preparing = null;
148
+ told = null;
149
+ untold = false;
150
+ shown = null;
117
151
  constructor(id) {
118
152
  this.id = id;
119
153
  }
@@ -137,23 +171,23 @@ export class Conversations {
137
171
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
138
172
  setInterval(() => this.look(), 2_000);
139
173
  }
140
- input(input) {
141
- void this.kit.inputs.carry(input.input_id, () => {
142
- const conversation = this.conversation(input.conversation_id);
143
- if (input.kind === 'kill') {
174
+ call(call) {
175
+ void this.kit.calls.carry(call.input_id, () => {
176
+ const conversation = this.conversation(call.conversation_id);
177
+ if (call.kind === 'kill') {
144
178
  conversation.kills++;
145
179
  conversation.killed.abort(new Error(KILLED));
146
180
  for (const waiting of conversation.waiting.splice(0))
147
181
  waiting.settle({ refused: KILLED });
148
182
  this.halt(conversation);
149
183
  }
150
- if (input.kind === 'interrupt') {
184
+ if (call.kind === 'interrupt') {
151
185
  conversation.interrupts++;
152
186
  conversation.interrupted.abort();
153
187
  }
154
- const handled = conversation.queue.then(() => this.carry(conversation, input));
188
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
155
189
  conversation.queue = handled.catch(() => undefined);
156
- return handled.then(async ({ ack }) => ({ ack: await ack }));
190
+ return handled.then(async ({ result }) => ({ result: await result }));
157
191
  });
158
192
  }
159
193
  idle() {
@@ -186,6 +220,14 @@ export class Conversations {
186
220
  }
187
221
  opened() {
188
222
  this.watched = false;
223
+ for (const conversation of this.conversations.values()) {
224
+ if (conversation.running !== null)
225
+ this.facts(conversation);
226
+ else if (conversation.exitUntold)
227
+ this.process(conversation, false);
228
+ if (conversation.untold || conversation.told !== null)
229
+ this.retell(conversation);
230
+ }
189
231
  }
190
232
  watching(watched) {
191
233
  const resend = watched && !this.watched;
@@ -205,63 +247,99 @@ export class Conversations {
205
247
  }
206
248
  return conversation;
207
249
  }
208
- async carry(conversation, input) {
209
- if (input.kind === 'open')
210
- return { ack: Promise.resolve(await this.open(conversation, input)) };
211
- if (input.kind === 'message')
212
- return this.message(conversation, input);
213
- if (input.kind === 'interrupt') {
250
+ async carry(conversation, call) {
251
+ if (call.kind === 'open')
252
+ return { result: this.open(conversation, call) };
253
+ if (call.kind === 'message')
254
+ return this.message(conversation, call);
255
+ await this.launched(conversation);
256
+ if (call.kind === 'interrupt') {
214
257
  if (--conversation.interrupts === 0)
215
258
  conversation.interrupted = new AbortController();
216
- await this.interrupt(conversation, String(input.turn_id));
259
+ await this.interrupt(conversation, String(call.turn_id));
217
260
  }
218
- if (input.kind === 'kill') {
261
+ if (call.kind === 'kill') {
219
262
  await this.kill(conversation);
220
263
  await conversation.sending;
221
264
  if (--conversation.kills === 0)
222
265
  conversation.killed = new AbortController();
223
266
  }
224
- if (input.kind === 'option')
225
- await this.option(conversation, String(input.option), input.value);
226
- if (input.kind === 'answer') {
227
- const question = this.questions.get(String(input.interaction_id));
267
+ if (call.kind === 'option')
268
+ await this.option(conversation, String(call.option), call.value);
269
+ if (call.kind === 'answer') {
270
+ const question = this.questions.get(String(call.interaction_id));
228
271
  if (question === undefined) {
229
- const text = answerMessage(input.request, input.response);
230
- return this.message(conversation, { ...input, text, files: [], first: false });
272
+ const text = answerMessage(call.request, call.response);
273
+ return this.message(conversation, { ...call, text, first: false, message_id: null });
231
274
  }
232
- question.answer(input.response);
275
+ question.answer(call.response);
233
276
  }
234
- return { ack: Promise.resolve({}) };
277
+ return { result: Promise.resolve({}) };
235
278
  }
236
- async open(conversation, input) {
279
+ open(conversation, call) {
237
280
  if (conversation.running !== null)
238
- return {};
239
- try {
240
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
241
- }
242
- catch (error) {
243
- return { refused: refusal(conversation, error) };
244
- }
281
+ return Promise.resolve({});
282
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
283
+ return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
284
+ }
285
+ launch(conversation, route, sessionId, empty) {
286
+ let opened;
287
+ let failed;
288
+ const bridge = new Promise((resolve, reject) => {
289
+ opened = resolve;
290
+ failed = reject;
291
+ });
292
+ bridge.catch(() => undefined);
293
+ const session = this.start(conversation, route, sessionId, empty, opened);
294
+ session.catch(failed);
295
+ const launch = { bridge, session };
296
+ conversation.launch = launch;
297
+ void session
298
+ .catch(() => undefined)
299
+ .then(() => {
300
+ if (conversation.launch !== launch)
301
+ return;
302
+ conversation.launch = null;
303
+ this.closeIdleBridge(conversation);
304
+ this.next(conversation);
305
+ });
306
+ this.next(conversation);
307
+ return launch;
245
308
  }
246
- async message(conversation, input) {
247
- const content = [{ type: 'text', text: String(input.text) }];
309
+ async launched(conversation) {
310
+ while (conversation.launch !== null)
311
+ await conversation.launch.session.catch(() => undefined);
312
+ }
313
+ instructed(conversation, route) {
314
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
315
+ if (bridge === undefined)
316
+ return null;
317
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
318
+ read.catch(() => undefined);
319
+ return read;
320
+ }
321
+ async message(conversation, call) {
322
+ const content = [{ type: 'text', text: String(call.text) }];
248
323
  let route;
324
+ let instructing = null;
249
325
  try {
250
- route = await this.route(conversation, input.agent_id);
251
- const files = input.files;
252
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
253
- const lines = await this.transcribed(conversation, files, paths);
254
- if (lines.length > 0)
255
- content.push({ type: 'text', text: lines.join('\n') });
326
+ route = await this.route(conversation, call.agent_id);
327
+ if (call.first === true)
328
+ instructing = this.instructed(conversation, route);
329
+ if (call.message_id !== null)
330
+ content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
256
331
  if (conversation.kills > 0)
257
332
  throw new Error(KILLED);
258
333
  }
259
334
  catch (error) {
260
- return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
335
+ this.closeIdleBridge(conversation);
336
+ this.prepares(conversation, null);
337
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
261
338
  }
339
+ conversation.preparing = null;
262
340
  return {
263
- ack: new Promise((settle) => {
264
- conversation.waiting.push({ input, route, content, settle });
341
+ result: new Promise((settle) => {
342
+ conversation.waiting.push({ call, route, content, instructing, settle });
265
343
  this.next(conversation);
266
344
  }),
267
345
  };
@@ -270,66 +348,148 @@ export class Conversations {
270
348
  if (conversation.sending !== null || conversation.open > 0)
271
349
  return;
272
350
  const waiting = conversation.waiting.shift();
273
- if (waiting === undefined)
351
+ if (waiting === undefined) {
352
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
353
+ if (conversation.starting !== step)
354
+ this.starts(conversation, step);
274
355
  return;
356
+ }
275
357
  conversation.sending = this.send(conversation, waiting).finally(() => {
276
358
  conversation.sending = null;
277
359
  this.next(conversation);
278
360
  });
279
361
  }
280
362
  async send(conversation, waiting) {
281
- const { input } = waiting;
282
- let ack = {};
363
+ const { call } = waiting;
364
+ let result = {};
283
365
  let started = false;
366
+ let instructing = waiting.instructing;
284
367
  const prompt = [];
285
368
  try {
286
369
  if (conversation.running === null) {
287
- const stored = conversation.session ?? input.provider_session_id;
288
- const empty = input.first === true && conversation.prompted !== stored;
289
- started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
370
+ this.starts(conversation, 'session');
371
+ await this.launched(conversation);
372
+ }
373
+ if (conversation.running === null) {
374
+ const stored = conversation.session ?? call.provider_session_id;
375
+ const empty = call.first === true && conversation.prompted !== stored;
376
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
377
+ if (call.first === true)
378
+ instructing = this.instructed(conversation, waiting.route);
379
+ started = (await launch.session) !== stored;
290
380
  }
291
381
  const session = conversation.running.sessionId;
292
- if (started || (input.first === true && session !== input.provider_session_id)) {
293
- ack = { provider_session_id: session };
382
+ if (started || (call.first === true && session !== call.provider_session_id)) {
383
+ result = { provider_session_id: session };
294
384
  }
295
- if (input.first === true) {
296
- prompt.push({
297
- type: 'text',
298
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
299
- });
385
+ if (call.first === true) {
386
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
300
387
  }
301
388
  if (conversation.kills > 0)
302
389
  throw new Error(KILLED);
303
390
  }
304
391
  catch (error) {
392
+ this.starts(conversation, null);
305
393
  if (started) {
306
394
  conversation.session = null;
307
395
  await this.kill(conversation);
308
396
  }
309
- waiting.settle({ refused: refusal(conversation, error) });
397
+ const refused = refusal(conversation, error);
398
+ const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
399
+ waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
400
+ return;
401
+ }
402
+ this.starts(conversation, null);
403
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
404
+ waiting.settle(result);
405
+ }
406
+ starts(conversation, step) {
407
+ conversation.starting = step;
408
+ if (conversation.shown !== null)
409
+ clearTimeout(conversation.shown);
410
+ conversation.shown = null;
411
+ if (step === null || conversation.told !== null) {
412
+ this.tell(conversation, step);
310
413
  return;
311
414
  }
312
- this.prompt(conversation, [...prompt, ...waiting.content]);
313
- waiting.settle(ack);
415
+ conversation.shown = setTimeout(() => {
416
+ conversation.shown = null;
417
+ if (conversation.starting === step)
418
+ this.tell(conversation, step);
419
+ }, STARTING_SHOWN_AFTER_MS);
314
420
  }
315
- async transcribed(conversation, files, paths) {
316
- const lines = [];
317
- for (const [index, path] of paths.entries()) {
318
- lines.push(path);
319
- if (files[index].media_type?.startsWith('audio/') !== true)
320
- continue;
321
- try {
322
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
323
- if (text !== null && text !== '')
324
- lines.push(`Transcript: ${text}`);
325
- }
326
- catch (error) {
327
- if (conversation.kills > 0)
328
- throw error;
329
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
421
+ prepares(conversation, step) {
422
+ conversation.preparing = step;
423
+ this.next(conversation);
424
+ }
425
+ tell(conversation, step) {
426
+ if (conversation.told === step && !conversation.untold)
427
+ return;
428
+ conversation.told = step;
429
+ this.retell(conversation);
430
+ }
431
+ retell(conversation) {
432
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
433
+ }
434
+ async transcribed(conversation, messageId, text) {
435
+ const paths = linkedPaths(text);
436
+ if (paths.length === 0)
437
+ return [];
438
+ const killed = conversation.killed.signal;
439
+ const interrupted = conversation.interrupted.signal;
440
+ const transcribing = AbortSignal.any([killed, interrupted]);
441
+ let signal = killed;
442
+ const blocks = [];
443
+ try {
444
+ const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
445
+ const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
446
+ for (const path of paths) {
447
+ let answered = await read(path);
448
+ if (answered === null)
449
+ continue;
450
+ signal = transcribing;
451
+ this.prepares(conversation, 'transcription');
452
+ const said = await transcribe(async () => {
453
+ const address = answered ?? (await read(path));
454
+ answered = null;
455
+ if (address === null)
456
+ throw new Error(`${path} is no longer an audio file`);
457
+ return address;
458
+ }, signal);
459
+ const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
460
+ await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
461
+ blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
330
462
  }
331
463
  }
332
- return lines;
464
+ catch (error) {
465
+ if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
466
+ throw new NotStarted(INTERRUPTED, { cause: error });
467
+ throw error;
468
+ }
469
+ return blocks;
470
+ }
471
+ bridged(conversation) {
472
+ if (conversation.bridge === null) {
473
+ const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
474
+ conversation.bridge = bridging;
475
+ bridging.catch(() => {
476
+ if (conversation.bridge === bridging)
477
+ conversation.bridge = null;
478
+ });
479
+ }
480
+ return conversation.bridge;
481
+ }
482
+ closeBridge(conversation, bridging, bridge) {
483
+ bridge.close();
484
+ if (conversation.bridge === bridging)
485
+ conversation.bridge = null;
486
+ }
487
+ closeIdleBridge(conversation) {
488
+ const bridging = conversation.bridge;
489
+ if (bridging === null || conversation.running !== null || conversation.launch !== null)
490
+ return;
491
+ conversation.bridge = null;
492
+ void bridging.then((bridge) => bridge.close(), () => undefined);
333
493
  }
334
494
  async interrupt(conversation, turnId) {
335
495
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -384,7 +544,8 @@ export class Conversations {
384
544
  }
385
545
  return route;
386
546
  }
387
- async start(conversation, route, sessionId, empty) {
547
+ async start(conversation, routed, sessionId, empty, bridged) {
548
+ const route = await routed;
388
549
  const reader = readerFor(route.kind);
389
550
  reader.session = sessionId;
390
551
  const app = client({ name: '@agentshouse/kit' })
@@ -392,9 +553,10 @@ export class Conversations {
392
553
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
393
554
  .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
394
555
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
395
- const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
556
+ const bridging = this.bridged(conversation);
557
+ const bridge = await step(unbridged, bridging);
396
558
  if (conversation.kills > 0) {
397
- bridge.close();
559
+ this.closeBridge(conversation, bridging, bridge);
398
560
  throw new Error(KILLED);
399
561
  }
400
562
  conversation.opening = bridge;
@@ -403,32 +565,31 @@ export class Conversations {
403
565
  let adapter = null;
404
566
  try {
405
567
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
568
+ bridged(bridge);
406
569
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
407
570
  const streamed = await step(NO_CONNECTION, work.open(cli));
408
571
  adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
409
572
  work.read(message);
410
573
  this.read(conversation, reader, message);
411
574
  }, { ...bridge.env, ...streamed }, undefined, scope));
412
- void adapter.exited.then(() => bridge.close());
575
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
413
576
  const agent = adapter.connection.agent;
414
577
  const cwd = route.working_directory;
415
578
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
416
579
  const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
417
- const opened = resumed ?? (await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta })));
580
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
418
581
  reader.session = opened.sessionId;
419
582
  if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
420
583
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
421
- await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
584
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
422
585
  }
423
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
586
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
424
587
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
425
588
  void work.settled().then(() => scope.record());
426
589
  conversation.running = running;
427
590
  conversation.session = running.sessionId;
428
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
429
- if (conversation.commands !== null)
430
- this.commands(conversation, conversation.commands);
431
- this.options(conversation, options);
591
+ conversation.options = options;
592
+ this.facts(conversation);
432
593
  void adapter.lost.then((cause) => this.exited(conversation, running, cause));
433
594
  return running.sessionId;
434
595
  }
@@ -436,7 +597,7 @@ export class Conversations {
436
597
  adapter?.retire();
437
598
  work.close();
438
599
  conversation.leftovers.add(scope);
439
- bridge.close();
600
+ this.closeBridge(conversation, bridging, bridge);
440
601
  throw conversation.kills > 0 ? new Error(KILLED) : error;
441
602
  }
442
603
  finally {
@@ -449,7 +610,7 @@ export class Conversations {
449
610
  }
450
611
  catch (error) {
451
612
  if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
452
- throw new NotStarted(NO_RESUME, { cause: error });
613
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
453
614
  }
454
615
  logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
455
616
  return null;
@@ -485,15 +646,28 @@ export class Conversations {
485
646
  this.kit.changed();
486
647
  }
487
648
  detach(conversation, running) {
649
+ if (conversation.bridge === running.bridging)
650
+ conversation.bridge = null;
488
651
  running.work.close();
489
652
  conversation.leftovers.add(running.scope);
490
653
  conversation.running = null;
491
654
  conversation.commands = null;
655
+ conversation.options = null;
492
656
  for (const [id, question] of this.questions) {
493
657
  if (question.conversation === conversation)
494
658
  this.questions.delete(id);
495
659
  }
496
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
660
+ this.process(conversation, false);
661
+ }
662
+ process(conversation, running) {
663
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
664
+ }
665
+ facts(conversation) {
666
+ this.process(conversation, true);
667
+ if (conversation.commands !== null)
668
+ this.commands(conversation, conversation.commands);
669
+ if (conversation.options !== null)
670
+ this.options(conversation, conversation.options);
497
671
  }
498
672
  commands(conversation, commands) {
499
673
  conversation.commands = commands;
@@ -504,6 +678,7 @@ export class Conversations {
504
678
  options(conversation, options) {
505
679
  if (conversation.running === null)
506
680
  return;
681
+ conversation.options = options;
507
682
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
508
683
  }
509
684
  deliver(conversation, turn, event, body) {
@@ -562,12 +737,14 @@ export class Conversations {
562
737
  this.next(conversation);
563
738
  this.kit.changed();
564
739
  }
565
- prompt(conversation, prompt) {
740
+ prompt(conversation, prompt, taken) {
566
741
  const running = conversation.running;
567
742
  const turn = this.begin(conversation);
568
743
  turn.prompted = true;
744
+ turn.taken = taken;
569
745
  conversation.prompted = running.sessionId;
570
746
  running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
747
+ this.took(turn);
571
748
  const notice = answeredFailure(answer);
572
749
  if (notice !== null) {
573
750
  this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
@@ -580,6 +757,7 @@ export class Conversations {
580
757
  this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
581
758
  }
582
759
  else {
760
+ this.took(turn);
583
761
  if (conversation.open === 1 && conversation.running === running) {
584
762
  running.adapter.retire();
585
763
  this.detach(conversation, running);
@@ -589,7 +767,9 @@ export class Conversations {
589
767
  });
590
768
  }
591
769
  started(conversation) {
592
- return conversation.turn ?? this.begin(conversation);
770
+ const turn = conversation.turn ?? this.begin(conversation);
771
+ this.took(turn);
772
+ return turn;
593
773
  }
594
774
  ended(conversation) {
595
775
  const turn = conversation.turn;
@@ -684,6 +864,11 @@ export class Conversations {
684
864
  }
685
865
  this.apply(conversation, this.started(conversation), event);
686
866
  }
867
+ took(turn) {
868
+ const taken = turn.taken;
869
+ turn.taken = null;
870
+ taken?.();
871
+ }
687
872
  apply(conversation, turn, event) {
688
873
  switch (event.kind) {
689
874
  case 'text':
@@ -738,13 +923,16 @@ export class Conversations {
738
923
  bytes: bytes.length,
739
924
  sha256: createHash('sha256').update(bytes).digest('hex'),
740
925
  }));
741
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
742
- if (saved.save.status !== 'saved')
743
- throw new Error(saved.save.failure ?? saved.save.status);
744
- turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
926
+ const saved = await uploaded(declared.upload, {
927
+ bytes: bytes.length,
928
+ send: (url, start, end) => sentPart(url, bytes.subarray(start, end)),
929
+ finish: (upload) => this.kit.house.deliver('/kit/uploads/finish', { upload }),
930
+ });
931
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
745
932
  }
746
933
  catch (error) {
747
- turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
934
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
935
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
748
936
  }
749
937
  }
750
938
  }