@agentshouse/kit 0.2.1-cd9cbd8b6428c → 0.2.1-cfe80fd914176

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.
@@ -1,19 +1,22 @@
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
5
  import { unlessAborted } from './abort.js';
6
- import { killMarked, startAdapter, TURN_ENDED, TURN_STARTED } from './acp.js';
7
- import { blocksOf, firstChange } from './blocks.js';
6
+ import { killMarked, processes, startAdapter } from './acp.js';
8
7
  import { openBridge } from './bridge.js';
9
8
  import { CLIS } from './clis.js';
10
- import { placeFiles } from './files.js';
9
+ import { readerFor } from './events.js';
10
+ import { placeFiles, uploadBytes } from './files.js';
11
11
  import { agentBase } from './home.js';
12
12
  import { instructions } from './instructions.js';
13
+ import { Parts } from './parts.js';
13
14
  import { holdSecretInput } from './secret-input.js';
14
15
  import { createHowWeWork } from './skills.js';
15
16
  import { transcribe } from './transcription.js';
16
17
  const KILLED = 'the conversation was killed';
18
+ // A process a conversation left running reports nothing when it exits, so the Kit looks again every five seconds while one runs.
19
+ const PROCESS_RECHECK_MS = 5000;
17
20
  function causeOf(error) {
18
21
  return error instanceof Error ? error.message : String(error);
19
22
  }
@@ -23,33 +26,38 @@ function answerMessage(request, response) {
23
26
  function logged(error) {
24
27
  process.stderr.write(`kit: ${causeOf(error)}\n`);
25
28
  }
29
+ function marker(bridge) {
30
+ return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
+ }
26
32
  function stop(bridge) {
27
- killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
33
+ killMarked(marker(bridge));
28
34
  }
29
35
  class Turn {
30
36
  id = randomUUID();
31
- text = '';
32
- segment = null;
37
+ parts;
33
38
  admitted = false;
34
- notes = [];
35
- sent = [];
36
- unsentFrom = null;
37
- draftSequence = 0;
38
- planSequence = 0;
39
- entries = null;
40
39
  questions = 0;
41
40
  secrets = 0;
42
- prompt = null;
41
+ prompted = false;
43
42
  ended = false;
43
+ images = 0;
44
+ uploads = [];
45
+ constructor(emit) {
46
+ this.parts = new Parts(emit);
47
+ }
44
48
  }
45
49
  class Conversation {
46
50
  id;
47
51
  running = null;
52
+ session = null;
48
53
  turn = null;
49
54
  commands = null;
50
- jobs = new Set();
51
- queued = [];
52
- late = [];
55
+ context = null;
56
+ busy = new Set();
57
+ waiting = [];
58
+ sending = null;
59
+ subagents = new Map();
60
+ held = [];
53
61
  opening = null;
54
62
  kills = 0;
55
63
  killed = new AbortController();
@@ -77,6 +85,8 @@ export class Conversations {
77
85
  questions = new Map();
78
86
  carrying = 0;
79
87
  reporting = 0;
88
+ watched = false;
89
+ recheck = null;
80
90
  constructor(kit) {
81
91
  this.kit = kit;
82
92
  }
@@ -88,14 +98,17 @@ export class Conversations {
88
98
  if (input.kind === 'kill') {
89
99
  conversation.kills++;
90
100
  conversation.killed.abort(new Error(KILLED));
101
+ for (const waiting of conversation.waiting.splice(0))
102
+ waiting.settle({ refused: KILLED });
91
103
  this.halt(conversation);
92
104
  }
93
105
  if (input.kind === 'interrupt') {
94
106
  conversation.interrupts++;
95
107
  conversation.interrupted.abort();
96
108
  }
97
- const work = conversation.queue.then(() => this.carry(conversation, input));
98
- conversation.queue = work.catch(() => undefined);
109
+ const handled = conversation.queue.then(() => this.carry(conversation, input));
110
+ conversation.queue = handled.catch(() => undefined);
111
+ const work = handled.then(({ ack }) => ack);
99
112
  this.acks.set(input.input_id, work);
100
113
  return work;
101
114
  })();
@@ -113,15 +126,34 @@ export class Conversations {
113
126
  for (const conversation of this.conversations.values()) {
114
127
  const turn = conversation.turn;
115
128
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
116
- if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
129
+ if (conversation.busy.size > 0 || conversation.open > (asking ? 1 : 0))
117
130
  return false;
118
131
  }
132
+ for (const conversation of this.conversations.values()) {
133
+ const running = conversation.running;
134
+ if (running === null || running.baseline === null)
135
+ continue;
136
+ if ([...processes(marker(running.bridge))].every((found) => running.baseline.has(found)))
137
+ continue;
138
+ this.recheck ??= setTimeout(() => {
139
+ this.recheck = null;
140
+ this.kit.changed();
141
+ }, PROCESS_RECHECK_MS);
142
+ return false;
143
+ }
119
144
  return true;
120
145
  }
121
146
  opened() {
147
+ this.watched = false;
148
+ }
149
+ watching(watched) {
150
+ const resend = watched && !this.watched;
151
+ this.watched = watched;
152
+ if (!resend)
153
+ return;
122
154
  for (const conversation of this.conversations.values()) {
123
155
  if (conversation.turn !== null)
124
- conversation.turn.unsentFrom = 0;
156
+ this.replay(conversation, conversation.turn);
125
157
  }
126
158
  }
127
159
  conversation(id) {
@@ -134,7 +166,7 @@ export class Conversations {
134
166
  }
135
167
  async carry(conversation, input) {
136
168
  if (input.kind === 'open')
137
- return this.open(conversation, input);
169
+ return { ack: Promise.resolve(await this.open(conversation, input)) };
138
170
  if (input.kind === 'message')
139
171
  return this.message(conversation, input);
140
172
  if (input.kind === 'interrupt') {
@@ -144,6 +176,7 @@ export class Conversations {
144
176
  }
145
177
  if (input.kind === 'kill') {
146
178
  await this.kill(conversation);
179
+ await conversation.sending;
147
180
  if (--conversation.kills === 0)
148
181
  conversation.killed = new AbortController();
149
182
  }
@@ -152,11 +185,12 @@ export class Conversations {
152
185
  if (input.kind === 'answer') {
153
186
  const question = this.questions.get(String(input.interaction_id));
154
187
  if (question === undefined) {
155
- return this.message(conversation, { ...input, text: answerMessage(input.request, input.response), files: [], first: false });
188
+ const text = answerMessage(input.request, input.response);
189
+ return this.message(conversation, { ...input, text, files: [], first: false });
156
190
  }
157
191
  question.answer(input.response);
158
192
  }
159
- return {};
193
+ return { ack: Promise.resolve({}) };
160
194
  }
161
195
  async open(conversation, input) {
162
196
  if (conversation.running !== null)
@@ -169,37 +203,69 @@ export class Conversations {
169
203
  }
170
204
  }
171
205
  async message(conversation, input) {
172
- let ack = {};
173
- const prompt = [];
206
+ const content = [{ type: 'text', text: String(input.text) }];
207
+ let route;
174
208
  try {
175
- const route = await this.route(conversation, input.agent_id);
209
+ route = await this.route(conversation, input.agent_id);
176
210
  const files = input.files;
177
211
  const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
178
212
  const lines = await this.transcribed(conversation, files, paths);
213
+ if (lines.length > 0)
214
+ content.push({ type: 'text', text: lines.join('\n') });
215
+ if (conversation.kills > 0)
216
+ throw new Error(KILLED);
217
+ }
218
+ catch (error) {
219
+ return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
220
+ }
221
+ return {
222
+ ack: new Promise((settle) => {
223
+ conversation.waiting.push({ input, route, content, settle });
224
+ this.next(conversation);
225
+ }),
226
+ };
227
+ }
228
+ next(conversation) {
229
+ if (conversation.sending !== null || conversation.open > 0)
230
+ return;
231
+ const waiting = conversation.waiting.shift();
232
+ if (waiting === undefined)
233
+ return;
234
+ conversation.sending = this.send(conversation, waiting).finally(() => {
235
+ conversation.sending = null;
236
+ this.next(conversation);
237
+ });
238
+ }
239
+ async send(conversation, waiting) {
240
+ const { input } = waiting;
241
+ let ack = {};
242
+ const prompt = [];
243
+ try {
179
244
  if (conversation.running === null) {
180
- const opened = await this.start(conversation, route, input.provider_session_id);
181
- if (input.provider_session_id === null)
245
+ const resumed = input.provider_session_id ?? conversation.session;
246
+ const opened = await this.start(conversation, waiting.route, resumed);
247
+ if (resumed === null)
182
248
  ack = { provider_session_id: opened };
183
249
  }
184
250
  if (input.first === true) {
185
251
  prompt.push({
186
252
  type: 'text',
187
- text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
253
+ text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
188
254
  });
189
255
  }
190
- prompt.push({ type: 'text', text: String(input.text) });
191
- if (lines.length > 0)
192
- prompt.push({ type: 'text', text: lines.join('\n') });
193
256
  if (conversation.kills > 0)
194
257
  throw new Error(KILLED);
195
258
  }
196
259
  catch (error) {
197
- if ('provider_session_id' in ack)
260
+ if ('provider_session_id' in ack) {
261
+ conversation.session = null;
198
262
  await this.kill(conversation);
199
- return { refused: conversation.kills > 0 ? KILLED : causeOf(error) };
263
+ }
264
+ waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
265
+ return;
200
266
  }
201
- this.prompt(conversation, prompt);
202
- return ack;
267
+ this.prompt(conversation, [...prompt, ...waiting.content]);
268
+ waiting.settle(ack);
203
269
  }
204
270
  async transcribed(conversation, files, paths) {
205
271
  const lines = [];
@@ -209,7 +275,7 @@ export class Conversations {
209
275
  continue;
210
276
  try {
211
277
  const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
212
- if (text !== '')
278
+ if (text !== null && text !== '')
213
279
  lines.push(`Transcript: ${text}`);
214
280
  }
215
281
  catch (error) {
@@ -245,6 +311,7 @@ export class Conversations {
245
311
  if (running === null)
246
312
  return;
247
313
  try {
314
+ running.reader.requested(option, value);
248
315
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
249
316
  sessionId: running.sessionId,
250
317
  configId: option,
@@ -253,6 +320,7 @@ export class Conversations {
253
320
  this.options(conversation, answered.configOptions);
254
321
  }
255
322
  catch (error) {
323
+ running.reader.refused(option);
256
324
  logged(error);
257
325
  }
258
326
  }
@@ -270,10 +338,9 @@ export class Conversations {
270
338
  return route;
271
339
  }
272
340
  async start(conversation, route, sessionId) {
341
+ const reader = readerFor(route.kind);
342
+ reader.session = sessionId;
273
343
  const app = client({ name: '@agentshouse/kit' })
274
- .onNotification('session/update', ({ params }) => this.update(conversation, params))
275
- .onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
276
- .onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
277
344
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
278
345
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
279
346
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
@@ -285,23 +352,22 @@ export class Conversations {
285
352
  conversation.opening = bridge;
286
353
  try {
287
354
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
288
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
355
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
356
+ conversation.busy.clear();
357
+ const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => this.read(conversation, reader, message), bridge.env);
289
358
  void adapter.exited.then(() => bridge.close());
290
359
  const agent = adapter.connection.agent;
291
360
  const cwd = route.working_directory;
361
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE);
292
362
  const opened = sessionId === null
293
- ? await agent.request('session/new', { cwd, mcpServers: [] })
294
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [] })), sessionId };
295
- const running = {
296
- adapter,
297
- bridge,
298
- sessionId: opened.sessionId,
299
- killed: false,
300
- queues: CLIS[route.kind].queues,
301
- phase: CLIS[route.kind].phase,
302
- };
363
+ ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
364
+ : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
365
+ reader.session = opened.sessionId;
366
+ const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, baseline: null };
303
367
  const options = await this.launchSettings(running, route, opened.configOptions ?? []);
368
+ running.baseline = processes(marker(bridge));
304
369
  conversation.running = running;
370
+ conversation.session = running.sessionId;
305
371
  this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
306
372
  if (conversation.commands !== null)
307
373
  this.commands(conversation, conversation.commands);
@@ -321,6 +387,7 @@ export class Conversations {
321
387
  async launchSettings(running, route, offered) {
322
388
  let options = offered;
323
389
  const settings = [
390
+ ['mode', route.mode ?? CLIS[route.kind].fullAccess],
324
391
  ['model', route.model],
325
392
  ['thought_level', route.effort],
326
393
  ];
@@ -328,6 +395,7 @@ export class Conversations {
328
395
  if (value === null)
329
396
  continue;
330
397
  const option = options.find((candidate) => candidate.category === category);
398
+ running.reader.requested(option?.id ?? category, value);
331
399
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
332
400
  sessionId: running.sessionId,
333
401
  configId: option?.id ?? category,
@@ -341,19 +409,22 @@ export class Conversations {
341
409
  const running = conversation.running;
342
410
  if (running?.adapter !== adapter)
343
411
  return;
412
+ this.detach(conversation);
413
+ if (conversation.turn !== null) {
414
+ this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
415
+ }
416
+ this.next(conversation);
417
+ this.kit.changed();
418
+ }
419
+ detach(conversation) {
344
420
  conversation.running = null;
345
421
  conversation.commands = null;
346
- conversation.jobs.clear();
347
- conversation.queued.length = 0;
422
+ conversation.busy.clear();
348
423
  for (const [id, question] of this.questions) {
349
424
  if (question.conversation === conversation)
350
425
  this.questions.delete(id);
351
426
  }
352
427
  this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
353
- if (conversation.turn !== null) {
354
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
355
- }
356
- this.kit.changed();
357
428
  }
358
429
  commands(conversation, commands) {
359
430
  conversation.commands = commands;
@@ -368,7 +439,7 @@ export class Conversations {
368
439
  }
369
440
  deliver(conversation, turn, event, body) {
370
441
  this.reporting++;
371
- const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
442
+ const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
372
443
  conversation.reports = delivered
373
444
  .catch(() => undefined)
374
445
  .finally(() => {
@@ -377,119 +448,84 @@ export class Conversations {
377
448
  });
378
449
  return delivered;
379
450
  }
380
- report(conversation) {
381
- const turn = new Turn();
382
- conversation.open++;
383
- void this.deliver(conversation, turn, 'started', {})
384
- .then(() => this.admitted(conversation, turn))
385
- .catch(logged);
386
- return turn;
451
+ write(conversation, turn, operation) {
452
+ if (!turn.admitted || turn.ended || !this.watched)
453
+ return;
454
+ this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
387
455
  }
388
- admitted(conversation, turn) {
389
- turn.admitted = true;
390
- for (const note of turn.notes.splice(0))
391
- this.kit.send(note);
392
- if (turn.text !== '') {
393
- turn.unsentFrom = 0;
394
- this.draft(conversation, turn);
395
- }
396
- if (turn.entries !== null)
397
- this.plan(conversation, turn);
456
+ replay(conversation, turn) {
457
+ for (const operation of turn.parts.operations())
458
+ this.write(conversation, turn, operation);
398
459
  }
399
460
  begin(conversation) {
400
- const turn = this.report(conversation);
461
+ const turn = new Turn((operation) => this.write(conversation, turn, operation));
462
+ conversation.open++;
463
+ void this.deliver(conversation, turn, 'started', {})
464
+ .then(() => {
465
+ turn.admitted = true;
466
+ this.replay(conversation, turn);
467
+ })
468
+ .catch(logged);
401
469
  conversation.turn = turn;
470
+ if (conversation.context !== null)
471
+ turn.parts.measure(conversation.context);
472
+ for (const part of conversation.held.splice(0))
473
+ turn.parts.start(part);
474
+ for (const event of conversation.running?.reader.turnStarted() ?? [])
475
+ this.event(conversation, event);
402
476
  this.kit.changed();
403
477
  return turn;
404
478
  }
405
- finish(conversation, turn) {
406
- if (turn.ended)
407
- return;
408
- const segment = turn.segment;
409
- turn.segment = null;
410
- if (segment?.phase === 'note')
411
- this.note(conversation, turn, segment.text);
412
- else if (segment !== null)
413
- this.answer(conversation, turn, segment.text);
414
- this.end(conversation, turn, { text: turn.text });
415
- }
416
- end(conversation, turn, outcome) {
479
+ end(conversation, turn, failed) {
417
480
  if (turn.ended)
418
481
  return;
482
+ if (conversation.turn === turn) {
483
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
484
+ this.apply(conversation, turn, event);
485
+ }
419
486
  turn.ended = true;
420
487
  conversation.open--;
421
488
  if (conversation.turn === turn)
422
489
  conversation.turn = null;
423
- void this.deliver(conversation, turn, 'ended', outcome).catch(logged);
424
- const late = conversation.open === 0 ? conversation.late.shift() : undefined;
425
- if (late !== undefined)
426
- this.failed(conversation, late);
490
+ const body = Promise.allSettled(turn.uploads).then(() => ({
491
+ parts: turn.parts.parts,
492
+ context: turn.parts.context,
493
+ ...failed,
494
+ }));
495
+ void this.deliver(conversation, turn, 'ended', body).catch(logged);
496
+ this.next(conversation);
427
497
  this.kit.changed();
428
498
  }
429
- failed(conversation, cause) {
430
- if (conversation.open > 0)
431
- conversation.late.push(cause);
432
- else
433
- this.end(conversation, this.report(conversation), { failed: cause });
434
- }
435
499
  prompt(conversation, prompt) {
436
500
  const running = conversation.running;
437
- const sent = { turn: null };
438
- if (running.queues && (conversation.turn !== null || conversation.queued.length > 0)) {
439
- conversation.queued.push(sent);
440
- }
441
- else {
442
- this.attach(conversation.turn ?? this.begin(conversation), sent);
443
- }
444
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
445
- if (!running.adapter.connection.signal.aborted)
446
- this.answered(conversation, sent, causeOf(error));
447
- else if (sent.turn !== null) {
448
- this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
501
+ const turn = this.begin(conversation);
502
+ turn.prompted = true;
503
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
504
+ if (running.adapter.connection.signal.aborted) {
505
+ this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
506
+ }
507
+ else {
508
+ if (conversation.open === 1 && conversation.running === running) {
509
+ stop(running.bridge);
510
+ this.detach(conversation);
511
+ }
512
+ this.end(conversation, turn, { failed: causeOf(error) });
449
513
  }
450
514
  });
451
515
  }
452
- attach(turn, sent) {
453
- turn.prompt = sent;
454
- sent.turn = turn;
455
- }
456
- answered(conversation, sent, failed) {
457
- const turn = sent.turn;
458
- if (turn === null) {
459
- const queued = conversation.queued.indexOf(sent);
460
- if (queued < 0)
461
- return;
462
- conversation.queued.splice(queued, 1);
463
- if (failed !== null)
464
- this.failed(conversation, failed);
465
- }
466
- else if (failed !== null) {
467
- if (turn.ended)
468
- this.failed(conversation, failed);
469
- else
470
- this.end(conversation, turn, { failed });
471
- }
472
- else if (turn.prompt === sent) {
473
- this.finish(conversation, turn);
474
- }
475
- }
476
516
  started(conversation) {
477
- if (conversation.turn !== null)
478
- return conversation.turn;
479
- const turn = this.begin(conversation);
480
- const queued = conversation.queued.shift();
481
- if (queued !== undefined)
482
- this.attach(turn, queued);
483
- return turn;
517
+ return conversation.turn ?? this.begin(conversation);
484
518
  }
485
519
  ended(conversation) {
486
520
  const turn = conversation.turn;
487
521
  if (turn === null)
488
522
  return;
489
- if (turn.prompt === null) {
490
- this.finish(conversation, turn);
523
+ if (!turn.prompted) {
524
+ this.end(conversation, turn, null);
491
525
  }
492
526
  else {
527
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
528
+ this.apply(conversation, turn, event);
493
529
  conversation.turn = null;
494
530
  this.kit.changed();
495
531
  }
@@ -538,101 +574,116 @@ export class Conversations {
538
574
  if (content !== null)
539
575
  this.questions.get(interactionId)?.answer({ action: 'accept', content });
540
576
  }
541
- update(conversation, notification) {
542
- const update = notification.update;
543
- if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
544
- this.chunk(conversation, this.started(conversation), update, update.content.text);
545
- }
546
- else if (update.sessionUpdate === 'plan') {
547
- const turn = this.started(conversation);
548
- this.close(conversation, turn);
549
- turn.entries = update.entries;
550
- this.plan(conversation, turn);
551
- }
552
- else if (update.sessionUpdate === 'tool_call') {
553
- this.close(conversation, this.started(conversation));
554
- }
555
- else if (update.sessionUpdate === 'agent_thought_chunk') {
556
- const turn = this.started(conversation);
557
- if (turn.segment?.messageId !== (update.messageId ?? null))
558
- this.close(conversation, turn);
559
- }
560
- else if (update.sessionUpdate === 'available_commands_update') {
561
- this.commands(conversation, update.availableCommands);
562
- }
563
- else if (update.sessionUpdate === 'config_option_update') {
564
- this.options(conversation, update.configOptions);
565
- }
577
+ read(conversation, reader, message) {
578
+ for (const event of reader.read(message))
579
+ this.event(conversation, event);
566
580
  }
567
- job(conversation, job) {
568
- if (job.running)
569
- conversation.jobs.add(job.id);
570
- else
571
- conversation.jobs.delete(job.id);
572
- this.kit.changed();
581
+ event(conversation, event) {
582
+ switch (event.kind) {
583
+ case 'started':
584
+ this.started(conversation);
585
+ return;
586
+ case 'ended':
587
+ this.ended(conversation);
588
+ return;
589
+ case 'commands':
590
+ this.commands(conversation, event.commands);
591
+ return;
592
+ case 'options':
593
+ this.options(conversation, event.options);
594
+ return;
595
+ case 'busy':
596
+ if (event.running)
597
+ conversation.busy.add(event.key);
598
+ else
599
+ conversation.busy.delete(event.key);
600
+ this.kit.changed();
601
+ return;
602
+ case 'servers': {
603
+ const running = conversation.running;
604
+ if (running?.baseline)
605
+ for (const found of processes(marker(running.bridge)))
606
+ running.baseline.add(found);
607
+ return;
608
+ }
609
+ case 'context':
610
+ conversation.context = { used: event.used, window: event.window };
611
+ conversation.turn?.parts.measure(conversation.context);
612
+ return;
613
+ case 'subagent-done':
614
+ this.subagentDone(conversation, event.id, event.result);
615
+ return;
616
+ case 'marker':
617
+ case 'image':
618
+ if (conversation.turn !== null)
619
+ break;
620
+ if (event.kind === 'marker' && event.marker === 'job')
621
+ conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
622
+ return;
623
+ }
624
+ this.apply(conversation, this.started(conversation), event);
573
625
  }
574
- chunk(conversation, turn, chunk, text) {
575
- const phase = conversation.running?.phase(chunk) ?? null;
576
- const messageId = chunk.messageId ?? null;
577
- if (turn.segment?.messageId !== messageId || turn.segment.phase !== phase)
578
- this.close(conversation, turn);
579
- if (phase === 'answer') {
580
- this.answer(conversation, turn, text);
581
- return;
626
+ apply(conversation, turn, event) {
627
+ switch (event.kind) {
628
+ case 'text':
629
+ case 'reasoning':
630
+ turn.parts.write(event.kind, event.id, event.text);
631
+ return;
632
+ case 'command':
633
+ turn.parts.command();
634
+ return;
635
+ case 'subagent': {
636
+ const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
637
+ conversation.subagents.set(event.id, { description: event.description, turn, index });
638
+ return;
639
+ }
640
+ case 'message':
641
+ turn.parts.start({ type: 'message', command: event.command, target: event.target });
642
+ return;
643
+ case 'marker':
644
+ turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
645
+ return;
646
+ case 'plan':
647
+ turn.parts.plan(event.entries);
648
+ return;
649
+ case 'image':
650
+ turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
651
+ this.kit.changed();
652
+ return;
582
653
  }
583
- turn.segment ??= { messageId, phase, text: '' };
584
- turn.segment.text += text;
585
654
  }
586
- close(conversation, turn) {
587
- const segment = turn.segment;
588
- if (segment === null)
655
+ subagentDone(conversation, id, result) {
656
+ const subagent = conversation.subagents.get(id);
657
+ if (subagent === undefined)
589
658
  return;
590
- turn.segment = null;
591
- this.note(conversation, turn, segment.text);
592
- }
593
- note(conversation, turn, text) {
594
- if (text.trim() === '')
659
+ conversation.subagents.delete(id);
660
+ if (subagent.turn === conversation.turn) {
661
+ subagent.turn.parts.done(subagent.index, result);
595
662
  return;
596
- const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
597
- if (turn.admitted)
598
- this.kit.send(note);
663
+ }
664
+ const part = { type: 'subagent', description: subagent.description, status: 'done', result };
665
+ if (conversation.turn === null)
666
+ conversation.held.push(part);
599
667
  else
600
- turn.notes.push(note);
601
- }
602
- answer(conversation, turn, text) {
603
- turn.text += text;
604
- this.draft(conversation, turn);
605
- }
606
- draft(conversation, turn) {
607
- const blocks = blocksOf(turn.text);
608
- const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
609
- const sent = this.kit.send({
610
- type: 'draft',
611
- conversation_id: conversation.id,
612
- turn_id: turn.id,
613
- sequence: turn.draftSequence + 1,
614
- from,
615
- blocks: blocks.slice(from),
616
- });
617
- if (sent) {
618
- turn.draftSequence++;
619
- turn.sent = blocks;
620
- turn.unsentFrom = null;
668
+ conversation.turn.parts.start(part);
669
+ }
670
+ async image(conversation, turn, data, mediaType) {
671
+ const bytes = Buffer.from(data, 'base64');
672
+ const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
673
+ try {
674
+ const declared = (await this.deliver(conversation, turn, 'images', {
675
+ name,
676
+ media_type: mediaType,
677
+ bytes: bytes.length,
678
+ sha256: createHash('sha256').update(bytes).digest('hex'),
679
+ }));
680
+ const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
681
+ if (saved.save.status !== 'saved')
682
+ throw new Error(saved.save.failure ?? saved.save.status);
683
+ turn.parts.start({ type: 'image', file: saved.version, name, media_type: mediaType });
621
684
  }
622
- else {
623
- turn.unsentFrom = from;
685
+ catch (error) {
686
+ turn.parts.start({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` });
624
687
  }
625
688
  }
626
- plan(conversation, turn) {
627
- const sent = this.kit.send({
628
- type: 'plan',
629
- conversation_id: conversation.id,
630
- turn_id: turn.id,
631
- sequence: turn.planSequence + 1,
632
- from: 0,
633
- steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
634
- });
635
- if (sent)
636
- turn.planSequence++;
637
- }
638
689
  }