@agentshouse/kit 0.2.1-c7f53ff9eaf98 → 0.2.1-c845e172df1af

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,13 +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, HouseRefusal } from './api.js';
7
8
  import { openBridge } from './bridge.js';
8
9
  import { CLIS } from './clis.js';
9
10
  import { answeredFailure, readerFor } from './events.js';
10
- import { placeFiles, uploadBytes } from './files.js';
11
+ import { placeFiles, 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';
14
17
  import { killMarked, Scope } from './scope.js';
15
18
  import { holdSecretInput } from './secret-input.js';
16
19
  import { createHowWeWork } from './skills.js';
@@ -19,10 +22,17 @@ import { workFor } from './work.js';
19
22
  const SIGNED_OUT = RequestError.authRequired().code;
20
23
  const KILLED = 'the conversation was killed';
21
24
  const REFUSED = 'the CLI refused the prompt';
22
- const NO_CONNECTION = "the Kit could not open this conversation's connection";
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
+ };
23
32
  const NO_START = "the Agent's CLI did not start";
24
33
  const NO_SESSION = "the Agent's CLI did not open a session";
25
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";
26
36
  const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
27
37
  class NotStarted extends Error {
28
38
  }
@@ -31,9 +41,20 @@ async function step(reason, work) {
31
41
  return await work;
32
42
  }
33
43
  catch (error) {
34
- throw new NotStarted(reason, { cause: error });
44
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
35
45
  }
36
46
  }
47
+ function signedOut(error) {
48
+ return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
49
+ }
50
+ function unopened(reason) {
51
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
52
+ }
53
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
54
+ const STARTING_SHOWN_AFTER_MS = 1_000;
55
+ function unbridged(error) {
56
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
57
+ }
37
58
  const EXIT_PLAN_MODE = {
38
59
  parse(value) {
39
60
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
@@ -42,13 +63,12 @@ const EXIT_PLAN_MODE = {
42
63
  return value;
43
64
  },
44
65
  };
45
- export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
66
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
46
67
  function causeOf(error) {
47
68
  return error instanceof Error ? error.message : String(error);
48
69
  }
49
70
  function answeredWith(error) {
50
- const signedOut = typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
51
- return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
71
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
52
72
  }
53
73
  function exitedWith(running, cause) {
54
74
  return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
@@ -56,6 +76,12 @@ function exitedWith(running, cause) {
56
76
  function answerMessage(request, response) {
57
77
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
58
78
  }
79
+ async function sentPart(url, bytes) {
80
+ const answer = await fetch(relayed(url), { method: 'PUT', body: bytes });
81
+ await answer.arrayBuffer();
82
+ if (!answer.ok)
83
+ throw storeRefusal('upload', answer.status);
84
+ }
59
85
  function logged(error) {
60
86
  process.stderr.write(`kit: ${causeOf(error)}\n`);
61
87
  }
@@ -72,10 +98,10 @@ function marker(bridge) {
72
98
  class Turn {
73
99
  id = randomUUID();
74
100
  parts;
75
- admitted = false;
76
101
  questions = 0;
77
102
  secrets = 0;
78
103
  prompted = false;
104
+ taken = null;
79
105
  ended = false;
80
106
  images = 0;
81
107
  uploads = [];
@@ -89,6 +115,8 @@ class Conversation {
89
115
  session = null;
90
116
  turn = null;
91
117
  commands = null;
118
+ options = null;
119
+ exitUntold = false;
92
120
  context = null;
93
121
  leftovers = new Set();
94
122
  waiting = [];
@@ -96,6 +124,7 @@ class Conversation {
96
124
  subagents = new Map();
97
125
  held = [];
98
126
  opening = null;
127
+ launch = null;
99
128
  prompted = null;
100
129
  kills = 0;
101
130
  killed = new AbortController();
@@ -104,6 +133,11 @@ class Conversation {
104
133
  open = 0;
105
134
  queue = Promise.resolve();
106
135
  reports = Promise.resolve();
136
+ starting = null;
137
+ preparing = null;
138
+ told = null;
139
+ untold = false;
140
+ shown = null;
107
141
  constructor(id) {
108
142
  this.id = id;
109
143
  }
@@ -119,9 +153,7 @@ function asksSecret(request) {
119
153
  export class Conversations {
120
154
  kit;
121
155
  conversations = new Map();
122
- acks = new Map();
123
156
  questions = new Map();
124
- carrying = 0;
125
157
  reporting = 0;
126
158
  watched = false;
127
159
  constructor(kit) {
@@ -129,38 +161,27 @@ export class Conversations {
129
161
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
130
162
  setInterval(() => this.look(), 2_000);
131
163
  }
132
- input(input) {
133
- this.carrying++;
134
- const carried = this.acks.get(input.input_id) ??
135
- (() => {
136
- const conversation = this.conversation(input.conversation_id);
137
- if (input.kind === 'kill') {
138
- conversation.kills++;
139
- conversation.killed.abort(new Error(KILLED));
140
- for (const waiting of conversation.waiting.splice(0))
141
- waiting.settle({ refused: KILLED });
142
- this.halt(conversation);
143
- }
144
- if (input.kind === 'interrupt') {
145
- conversation.interrupts++;
146
- conversation.interrupted.abort();
147
- }
148
- const handled = conversation.queue.then(() => this.carry(conversation, input));
149
- conversation.queue = handled.catch(() => undefined);
150
- const work = handled.then(({ ack }) => ack);
151
- this.acks.set(input.input_id, work);
152
- return work;
153
- })();
154
- carried
155
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
156
- .catch(logged)
157
- .finally(() => {
158
- this.carrying--;
159
- this.kit.changed();
164
+ call(call) {
165
+ void this.kit.calls.carry(call.input_id, () => {
166
+ const conversation = this.conversation(call.conversation_id);
167
+ if (call.kind === 'kill') {
168
+ conversation.kills++;
169
+ conversation.killed.abort(new Error(KILLED));
170
+ for (const waiting of conversation.waiting.splice(0))
171
+ waiting.settle({ refused: KILLED });
172
+ this.halt(conversation);
173
+ }
174
+ if (call.kind === 'interrupt') {
175
+ conversation.interrupts++;
176
+ conversation.interrupted.abort();
177
+ }
178
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
179
+ conversation.queue = handled.catch(() => undefined);
180
+ return handled.then(async ({ result }) => ({ result: await result }));
160
181
  });
161
182
  }
162
183
  idle() {
163
- if (this.carrying > 0 || this.reporting > 0)
184
+ if (this.reporting > 0)
164
185
  return false;
165
186
  for (const conversation of this.conversations.values()) {
166
187
  const turn = conversation.turn;
@@ -189,6 +210,14 @@ export class Conversations {
189
210
  }
190
211
  opened() {
191
212
  this.watched = false;
213
+ for (const conversation of this.conversations.values()) {
214
+ if (conversation.running !== null)
215
+ this.facts(conversation);
216
+ else if (conversation.exitUntold)
217
+ this.process(conversation, false);
218
+ if (conversation.untold || conversation.told !== null)
219
+ this.retell(conversation);
220
+ }
192
221
  }
193
222
  watching(watched) {
194
223
  const resend = watched && !this.watched;
@@ -208,50 +237,87 @@ export class Conversations {
208
237
  }
209
238
  return conversation;
210
239
  }
211
- async carry(conversation, input) {
212
- if (input.kind === 'open')
213
- return { ack: Promise.resolve(await this.open(conversation, input)) };
214
- if (input.kind === 'message')
215
- return this.message(conversation, input);
216
- if (input.kind === 'interrupt') {
240
+ async carry(conversation, call) {
241
+ if (call.kind === 'open')
242
+ return { result: this.open(conversation, call) };
243
+ if (call.kind === 'message')
244
+ return this.message(conversation, call);
245
+ await this.launched(conversation);
246
+ if (call.kind === 'interrupt') {
217
247
  if (--conversation.interrupts === 0)
218
248
  conversation.interrupted = new AbortController();
219
- await this.interrupt(conversation, String(input.turn_id));
249
+ await this.interrupt(conversation, String(call.turn_id));
220
250
  }
221
- if (input.kind === 'kill') {
251
+ if (call.kind === 'kill') {
222
252
  await this.kill(conversation);
223
253
  await conversation.sending;
224
254
  if (--conversation.kills === 0)
225
255
  conversation.killed = new AbortController();
226
256
  }
227
- if (input.kind === 'option')
228
- await this.option(conversation, String(input.option), input.value);
229
- if (input.kind === 'answer') {
230
- const question = this.questions.get(String(input.interaction_id));
257
+ if (call.kind === 'option')
258
+ await this.option(conversation, String(call.option), call.value);
259
+ if (call.kind === 'answer') {
260
+ const question = this.questions.get(String(call.interaction_id));
231
261
  if (question === undefined) {
232
- const text = answerMessage(input.request, input.response);
233
- return this.message(conversation, { ...input, text, files: [], first: false });
262
+ const text = answerMessage(call.request, call.response);
263
+ return this.message(conversation, { ...call, text, files: [], first: false });
234
264
  }
235
- question.answer(input.response);
265
+ question.answer(call.response);
236
266
  }
237
- return { ack: Promise.resolve({}) };
267
+ return { result: Promise.resolve({}) };
238
268
  }
239
- async open(conversation, input) {
269
+ open(conversation, call) {
240
270
  if (conversation.running !== null)
241
- return {};
242
- try {
243
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
244
- }
245
- catch (error) {
246
- return { refused: refusal(conversation, error) };
247
- }
271
+ return Promise.resolve({});
272
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
273
+ return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
274
+ }
275
+ launch(conversation, route, sessionId, empty) {
276
+ let opened;
277
+ let failed;
278
+ const bridge = new Promise((resolve, reject) => {
279
+ opened = resolve;
280
+ failed = reject;
281
+ });
282
+ bridge.catch(() => undefined);
283
+ const session = this.start(conversation, route, sessionId, empty, opened);
284
+ session.catch(failed);
285
+ const launch = { bridge, session };
286
+ conversation.launch = launch;
287
+ void session
288
+ .catch(() => undefined)
289
+ .then(() => {
290
+ if (conversation.launch !== launch)
291
+ return;
292
+ conversation.launch = null;
293
+ this.next(conversation);
294
+ });
295
+ this.next(conversation);
296
+ return launch;
248
297
  }
249
- async message(conversation, input) {
250
- const content = [{ type: 'text', text: String(input.text) }];
298
+ async launched(conversation) {
299
+ while (conversation.launch !== null)
300
+ await conversation.launch.session.catch(() => undefined);
301
+ }
302
+ instructed(conversation, route) {
303
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
304
+ if (bridge === undefined)
305
+ return null;
306
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
307
+ read.catch(() => undefined);
308
+ return read;
309
+ }
310
+ async message(conversation, call) {
311
+ const content = [{ type: 'text', text: String(call.text) }];
251
312
  let route;
313
+ let instructing = null;
252
314
  try {
253
- route = await this.route(conversation, input.agent_id);
254
- const files = input.files;
315
+ route = await this.route(conversation, call.agent_id);
316
+ if (call.first === true)
317
+ instructing = this.instructed(conversation, route);
318
+ const files = call.files;
319
+ if (files.length > 0)
320
+ this.prepares(conversation, 'files');
255
321
  const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
256
322
  const lines = await this.transcribed(conversation, files, paths);
257
323
  if (lines.length > 0)
@@ -260,11 +326,13 @@ export class Conversations {
260
326
  throw new Error(KILLED);
261
327
  }
262
328
  catch (error) {
263
- return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
329
+ this.prepares(conversation, null);
330
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
264
331
  }
332
+ conversation.preparing = null;
265
333
  return {
266
- ack: new Promise((settle) => {
267
- conversation.waiting.push({ input, route, content, settle });
334
+ result: new Promise((settle) => {
335
+ conversation.waiting.push({ call, route, content, instructing, settle });
268
336
  this.next(conversation);
269
337
  }),
270
338
  };
@@ -273,38 +341,48 @@ export class Conversations {
273
341
  if (conversation.sending !== null || conversation.open > 0)
274
342
  return;
275
343
  const waiting = conversation.waiting.shift();
276
- if (waiting === undefined)
344
+ if (waiting === undefined) {
345
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
346
+ if (conversation.starting !== step)
347
+ this.starts(conversation, step);
277
348
  return;
349
+ }
278
350
  conversation.sending = this.send(conversation, waiting).finally(() => {
279
351
  conversation.sending = null;
280
352
  this.next(conversation);
281
353
  });
282
354
  }
283
355
  async send(conversation, waiting) {
284
- const { input } = waiting;
285
- let ack = {};
356
+ const { call } = waiting;
357
+ let result = {};
286
358
  let started = false;
359
+ let instructing = waiting.instructing;
287
360
  const prompt = [];
288
361
  try {
289
362
  if (conversation.running === null) {
290
- const stored = conversation.session ?? input.provider_session_id;
291
- const empty = input.first === true && conversation.prompted !== stored;
292
- started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
363
+ this.starts(conversation, 'session');
364
+ await this.launched(conversation);
365
+ }
366
+ if (conversation.running === null) {
367
+ const stored = conversation.session ?? call.provider_session_id;
368
+ const empty = call.first === true && conversation.prompted !== stored;
369
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
370
+ if (call.first === true)
371
+ instructing = this.instructed(conversation, waiting.route);
372
+ started = (await launch.session) !== stored;
293
373
  }
294
374
  const session = conversation.running.sessionId;
295
- if (started || (input.first === true && session !== input.provider_session_id)) {
296
- ack = { provider_session_id: session };
375
+ if (started || (call.first === true && session !== call.provider_session_id)) {
376
+ result = { provider_session_id: session };
297
377
  }
298
- if (input.first === true) {
299
- prompt.push({
300
- type: 'text',
301
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
302
- });
378
+ if (call.first === true) {
379
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
303
380
  }
304
381
  if (conversation.kills > 0)
305
382
  throw new Error(KILLED);
306
383
  }
307
384
  catch (error) {
385
+ this.starts(conversation, null);
308
386
  if (started) {
309
387
  conversation.session = null;
310
388
  await this.kill(conversation);
@@ -312,8 +390,37 @@ export class Conversations {
312
390
  waiting.settle({ refused: refusal(conversation, error) });
313
391
  return;
314
392
  }
315
- this.prompt(conversation, [...prompt, ...waiting.content]);
316
- waiting.settle(ack);
393
+ this.starts(conversation, null);
394
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
395
+ waiting.settle(result);
396
+ }
397
+ starts(conversation, step) {
398
+ conversation.starting = step;
399
+ if (conversation.shown !== null)
400
+ clearTimeout(conversation.shown);
401
+ conversation.shown = null;
402
+ if (step === null || conversation.told !== null) {
403
+ this.tell(conversation, step);
404
+ return;
405
+ }
406
+ conversation.shown = setTimeout(() => {
407
+ conversation.shown = null;
408
+ if (conversation.starting === step)
409
+ this.tell(conversation, step);
410
+ }, STARTING_SHOWN_AFTER_MS);
411
+ }
412
+ prepares(conversation, step) {
413
+ conversation.preparing = step;
414
+ this.next(conversation);
415
+ }
416
+ tell(conversation, step) {
417
+ if (conversation.told === step && !conversation.untold)
418
+ return;
419
+ conversation.told = step;
420
+ this.retell(conversation);
421
+ }
422
+ retell(conversation) {
423
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
317
424
  }
318
425
  async transcribed(conversation, files, paths) {
319
426
  const lines = [];
@@ -321,6 +428,7 @@ export class Conversations {
321
428
  lines.push(path);
322
429
  if (files[index].media_type?.startsWith('audio/') !== true)
323
430
  continue;
431
+ this.prepares(conversation, 'transcription');
324
432
  try {
325
433
  const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
326
434
  if (text !== null && text !== '')
@@ -387,7 +495,8 @@ export class Conversations {
387
495
  }
388
496
  return route;
389
497
  }
390
- async start(conversation, route, sessionId, empty) {
498
+ async start(conversation, routed, sessionId, empty, bridged) {
499
+ const route = await routed;
391
500
  const reader = readerFor(route.kind);
392
501
  reader.session = sessionId;
393
502
  const app = client({ name: '@agentshouse/kit' })
@@ -395,7 +504,7 @@ export class Conversations {
395
504
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
396
505
  .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
397
506
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
398
- const bridge = await step(NO_CONNECTION, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
507
+ const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
399
508
  if (conversation.kills > 0) {
400
509
  bridge.close();
401
510
  throw new Error(KILLED);
@@ -403,11 +512,13 @@ export class Conversations {
403
512
  conversation.opening = bridge;
404
513
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
405
514
  const scope = new Scope(marker(bridge), randomUUID(), cli);
515
+ let adapter = null;
406
516
  try {
407
517
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
518
+ bridged(bridge);
408
519
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
409
520
  const streamed = await step(NO_CONNECTION, work.open(cli));
410
- const adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
521
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
411
522
  work.read(message);
412
523
  this.read(conversation, reader, message);
413
524
  }, { ...bridge.env, ...streamed }, undefined, scope));
@@ -416,26 +527,24 @@ export class Conversations {
416
527
  const cwd = route.working_directory;
417
528
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
418
529
  const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
419
- const opened = resumed ?? (await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta })));
530
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
420
531
  reader.session = opened.sessionId;
421
532
  if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
422
533
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
423
- await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
534
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
424
535
  }
425
536
  const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
426
537
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
427
538
  void work.settled().then(() => scope.record());
428
539
  conversation.running = running;
429
540
  conversation.session = running.sessionId;
430
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
431
- if (conversation.commands !== null)
432
- this.commands(conversation, conversation.commands);
433
- this.options(conversation, options);
434
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
541
+ conversation.options = options;
542
+ this.facts(conversation);
543
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
435
544
  return running.sessionId;
436
545
  }
437
546
  catch (error) {
438
- scope.kill();
547
+ adapter?.retire();
439
548
  work.close();
440
549
  conversation.leftovers.add(scope);
441
550
  bridge.close();
@@ -451,7 +560,7 @@ export class Conversations {
451
560
  }
452
561
  catch (error) {
453
562
  if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
454
- throw new NotStarted(NO_RESUME, { cause: error });
563
+ throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
455
564
  }
456
565
  logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
457
566
  return null;
@@ -478,10 +587,7 @@ export class Conversations {
478
587
  }
479
588
  return options;
480
589
  }
481
- exited(conversation, adapter, cause) {
482
- const running = conversation.running;
483
- if (running?.adapter !== adapter)
484
- return;
590
+ exited(conversation, running, cause) {
485
591
  this.detach(conversation, running);
486
592
  if (conversation.turn !== null) {
487
593
  this.end(conversation, conversation.turn, exitedWith(running, cause));
@@ -494,11 +600,22 @@ export class Conversations {
494
600
  conversation.leftovers.add(running.scope);
495
601
  conversation.running = null;
496
602
  conversation.commands = null;
603
+ conversation.options = null;
497
604
  for (const [id, question] of this.questions) {
498
605
  if (question.conversation === conversation)
499
606
  this.questions.delete(id);
500
607
  }
501
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
608
+ this.process(conversation, false);
609
+ }
610
+ process(conversation, running) {
611
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
612
+ }
613
+ facts(conversation) {
614
+ this.process(conversation, true);
615
+ if (conversation.commands !== null)
616
+ this.commands(conversation, conversation.commands);
617
+ if (conversation.options !== null)
618
+ this.options(conversation, conversation.options);
502
619
  }
503
620
  commands(conversation, commands) {
504
621
  conversation.commands = commands;
@@ -509,6 +626,7 @@ export class Conversations {
509
626
  options(conversation, options) {
510
627
  if (conversation.running === null)
511
628
  return;
629
+ conversation.options = options;
512
630
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
513
631
  }
514
632
  deliver(conversation, turn, event, body) {
@@ -523,7 +641,7 @@ export class Conversations {
523
641
  return delivered;
524
642
  }
525
643
  write(conversation, turn, operation) {
526
- if (!turn.admitted || turn.ended || !this.watched)
644
+ if (turn.ended || !this.watched)
527
645
  return;
528
646
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
529
647
  }
@@ -534,12 +652,7 @@ export class Conversations {
534
652
  begin(conversation) {
535
653
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
536
654
  conversation.open++;
537
- void this.deliver(conversation, turn, 'started', {})
538
- .then(() => {
539
- turn.admitted = true;
540
- this.replay(conversation, turn);
541
- })
542
- .catch(logged);
655
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
543
656
  conversation.turn = turn;
544
657
  if (conversation.context !== null)
545
658
  turn.parts.measure(conversation.context);
@@ -572,12 +685,14 @@ export class Conversations {
572
685
  this.next(conversation);
573
686
  this.kit.changed();
574
687
  }
575
- prompt(conversation, prompt) {
688
+ prompt(conversation, prompt, taken) {
576
689
  const running = conversation.running;
577
690
  const turn = this.begin(conversation);
578
691
  turn.prompted = true;
692
+ turn.taken = taken;
579
693
  conversation.prompted = running.sessionId;
580
694
  running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
695
+ this.took(turn);
581
696
  const notice = answeredFailure(answer);
582
697
  if (notice !== null) {
583
698
  this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
@@ -590,8 +705,9 @@ export class Conversations {
590
705
  this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
591
706
  }
592
707
  else {
708
+ this.took(turn);
593
709
  if (conversation.open === 1 && conversation.running === running) {
594
- running.scope.kill();
710
+ running.adapter.retire();
595
711
  this.detach(conversation, running);
596
712
  }
597
713
  this.end(conversation, turn, answeredWith(error));
@@ -599,7 +715,9 @@ export class Conversations {
599
715
  });
600
716
  }
601
717
  started(conversation) {
602
- return conversation.turn ?? this.begin(conversation);
718
+ const turn = conversation.turn ?? this.begin(conversation);
719
+ this.took(turn);
720
+ return turn;
603
721
  }
604
722
  ended(conversation) {
605
723
  const turn = conversation.turn;
@@ -694,6 +812,11 @@ export class Conversations {
694
812
  }
695
813
  this.apply(conversation, this.started(conversation), event);
696
814
  }
815
+ took(turn) {
816
+ const taken = turn.taken;
817
+ turn.taken = null;
818
+ taken?.();
819
+ }
697
820
  apply(conversation, turn, event) {
698
821
  switch (event.kind) {
699
822
  case 'text':
@@ -748,13 +871,16 @@ export class Conversations {
748
871
  bytes: bytes.length,
749
872
  sha256: createHash('sha256').update(bytes).digest('hex'),
750
873
  }));
751
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
752
- if (saved.save.status !== 'saved')
753
- throw new Error(saved.save.failure ?? saved.save.status);
754
- turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
874
+ const saved = await uploaded(declared.upload, {
875
+ bytes: bytes.length,
876
+ send: (url, start, end) => sentPart(url, bytes.subarray(start, end)),
877
+ finish: (upload) => this.kit.house.deliver('/kit/uploads/finish', { upload }),
878
+ });
879
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
755
880
  }
756
881
  catch (error) {
757
- turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
882
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
883
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
758
884
  }
759
885
  }
760
886
  }