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

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