@agentshouse/kit 0.2.1-cd9cbd8b6428c → 0.2.1-cfe564e35b4a5

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,18 +1,21 @@
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 { 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';
14
+ import { killMarked, Scope } from './scope.js';
13
15
  import { holdSecretInput } from './secret-input.js';
14
16
  import { createHowWeWork } from './skills.js';
15
17
  import { transcribe } from './transcription.js';
18
+ import { workFor } from './work.js';
16
19
  const KILLED = 'the conversation was killed';
17
20
  function causeOf(error) {
18
21
  return error instanceof Error ? error.message : String(error);
@@ -23,33 +26,35 @@ function answerMessage(request, response) {
23
26
  function logged(error) {
24
27
  process.stderr.write(`kit: ${causeOf(error)}\n`);
25
28
  }
26
- function stop(bridge) {
27
- killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
29
+ function marker(bridge) {
30
+ return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
28
31
  }
29
32
  class Turn {
30
33
  id = randomUUID();
31
- text = '';
32
- segment = null;
34
+ parts;
33
35
  admitted = false;
34
- notes = [];
35
- sent = [];
36
- unsentFrom = null;
37
- draftSequence = 0;
38
- planSequence = 0;
39
- entries = null;
40
36
  questions = 0;
41
37
  secrets = 0;
42
- prompt = null;
38
+ prompted = false;
43
39
  ended = false;
40
+ images = 0;
41
+ uploads = [];
42
+ constructor(emit) {
43
+ this.parts = new Parts(emit);
44
+ }
44
45
  }
45
46
  class Conversation {
46
47
  id;
47
48
  running = null;
49
+ session = null;
48
50
  turn = null;
49
51
  commands = null;
50
- jobs = new Set();
51
- queued = [];
52
- late = [];
52
+ context = null;
53
+ leftovers = new Set();
54
+ waiting = [];
55
+ sending = null;
56
+ subagents = new Map();
57
+ held = [];
53
58
  opening = null;
54
59
  kills = 0;
55
60
  killed = new AbortController();
@@ -77,8 +82,11 @@ export class Conversations {
77
82
  questions = new Map();
78
83
  carrying = 0;
79
84
  reporting = 0;
85
+ watched = false;
80
86
  constructor(kit) {
81
87
  this.kit = kit;
88
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
89
+ setInterval(() => this.look(), 2_000);
82
90
  }
83
91
  input(input) {
84
92
  this.carrying++;
@@ -88,14 +96,17 @@ export class Conversations {
88
96
  if (input.kind === 'kill') {
89
97
  conversation.kills++;
90
98
  conversation.killed.abort(new Error(KILLED));
99
+ for (const waiting of conversation.waiting.splice(0))
100
+ waiting.settle({ refused: KILLED });
91
101
  this.halt(conversation);
92
102
  }
93
103
  if (input.kind === 'interrupt') {
94
104
  conversation.interrupts++;
95
105
  conversation.interrupted.abort();
96
106
  }
97
- const work = conversation.queue.then(() => this.carry(conversation, input));
98
- conversation.queue = work.catch(() => undefined);
107
+ const handled = conversation.queue.then(() => this.carry(conversation, input));
108
+ conversation.queue = handled.catch(() => undefined);
109
+ const work = handled.then(({ ack }) => ack);
99
110
  this.acks.set(input.input_id, work);
100
111
  return work;
101
112
  })();
@@ -113,15 +124,36 @@ export class Conversations {
113
124
  for (const conversation of this.conversations.values()) {
114
125
  const turn = conversation.turn;
115
126
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
116
- if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
127
+ if (conversation.open > (asking ? 1 : 0))
117
128
  return false;
118
129
  }
119
- return true;
130
+ const conversations = [...this.conversations.values()];
131
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
132
+ return false;
133
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
134
+ }
135
+ look() {
136
+ for (const conversation of this.conversations.values()) {
137
+ for (const scope of conversation.leftovers) {
138
+ if (!scope.empty())
139
+ continue;
140
+ scope.close();
141
+ conversation.leftovers.delete(scope);
142
+ }
143
+ }
144
+ this.kit.changed();
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) {
@@ -229,11 +295,13 @@ export class Conversations {
229
295
  }
230
296
  halt(conversation) {
231
297
  if (conversation.opening !== null)
232
- stop(conversation.opening);
298
+ killMarked(marker(conversation.opening));
299
+ for (const scope of conversation.leftovers)
300
+ scope.kill();
233
301
  if (conversation.running === null)
234
302
  return;
235
303
  conversation.running.killed = true;
236
- stop(conversation.running.bridge);
304
+ conversation.running.scope.kill();
237
305
  }
238
306
  async kill(conversation) {
239
307
  const running = conversation.running;
@@ -245,6 +313,7 @@ export class Conversations {
245
313
  if (running === null)
246
314
  return;
247
315
  try {
316
+ running.reader.requested(option, value);
248
317
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
249
318
  sessionId: running.sessionId,
250
319
  configId: option,
@@ -253,6 +322,7 @@ export class Conversations {
253
322
  this.options(conversation, answered.configOptions);
254
323
  }
255
324
  catch (error) {
325
+ running.reader.refused(option);
256
326
  logged(error);
257
327
  }
258
328
  }
@@ -270,10 +340,9 @@ export class Conversations {
270
340
  return route;
271
341
  }
272
342
  async start(conversation, route, sessionId) {
343
+ const reader = readerFor(route.kind);
344
+ reader.session = sessionId;
273
345
  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
346
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
278
347
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
279
348
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
@@ -283,25 +352,32 @@ export class Conversations {
283
352
  throw new Error(KILLED);
284
353
  }
285
354
  conversation.opening = bridge;
355
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
356
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
286
357
  try {
287
358
  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);
359
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
360
+ const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
361
+ work.read(message);
362
+ this.read(conversation, reader, message);
363
+ }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
289
364
  void adapter.exited.then(() => bridge.close());
290
365
  const agent = adapter.connection.agent;
291
366
  const cwd = route.working_directory;
367
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
292
368
  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
- };
369
+ ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
370
+ : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
371
+ reader.session = opened.sessionId;
372
+ if (sessionId === null && CLIS[route.kind].renames) {
373
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
374
+ await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
375
+ }
376
+ const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
303
377
  const options = await this.launchSettings(running, route, opened.configOptions ?? []);
378
+ void work.settled().then(() => scope.record());
304
379
  conversation.running = running;
380
+ conversation.session = running.sessionId;
305
381
  this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
306
382
  if (conversation.commands !== null)
307
383
  this.commands(conversation, conversation.commands);
@@ -310,7 +386,9 @@ export class Conversations {
310
386
  return running.sessionId;
311
387
  }
312
388
  catch (error) {
313
- stop(bridge);
389
+ scope.kill();
390
+ work.close();
391
+ conversation.leftovers.add(scope);
314
392
  bridge.close();
315
393
  throw conversation.kills > 0 ? new Error(KILLED) : error;
316
394
  }
@@ -321,6 +399,7 @@ export class Conversations {
321
399
  async launchSettings(running, route, offered) {
322
400
  let options = offered;
323
401
  const settings = [
402
+ ['mode', route.mode ?? CLIS[route.kind].fullAccess],
324
403
  ['model', route.model],
325
404
  ['thought_level', route.effort],
326
405
  ];
@@ -328,6 +407,7 @@ export class Conversations {
328
407
  if (value === null)
329
408
  continue;
330
409
  const option = options.find((candidate) => candidate.category === category);
410
+ running.reader.requested(option?.id ?? category, value);
331
411
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
332
412
  sessionId: running.sessionId,
333
413
  configId: option?.id ?? category,
@@ -341,19 +421,23 @@ export class Conversations {
341
421
  const running = conversation.running;
342
422
  if (running?.adapter !== adapter)
343
423
  return;
424
+ this.detach(conversation, running);
425
+ if (conversation.turn !== null) {
426
+ this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
427
+ }
428
+ this.next(conversation);
429
+ this.kit.changed();
430
+ }
431
+ detach(conversation, running) {
432
+ running.work.close();
433
+ conversation.leftovers.add(running.scope);
344
434
  conversation.running = null;
345
435
  conversation.commands = null;
346
- conversation.jobs.clear();
347
- conversation.queued.length = 0;
348
436
  for (const [id, question] of this.questions) {
349
437
  if (question.conversation === conversation)
350
438
  this.questions.delete(id);
351
439
  }
352
440
  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
441
  }
358
442
  commands(conversation, commands) {
359
443
  conversation.commands = commands;
@@ -368,7 +452,7 @@ export class Conversations {
368
452
  }
369
453
  deliver(conversation, turn, event, body) {
370
454
  this.reporting++;
371
- const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
455
+ const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
372
456
  conversation.reports = delivered
373
457
  .catch(() => undefined)
374
458
  .finally(() => {
@@ -377,119 +461,82 @@ export class Conversations {
377
461
  });
378
462
  return delivered;
379
463
  }
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;
464
+ write(conversation, turn, operation) {
465
+ if (!turn.admitted || turn.ended || !this.watched)
466
+ return;
467
+ this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
387
468
  }
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);
469
+ replay(conversation, turn) {
470
+ for (const operation of turn.parts.operations())
471
+ this.write(conversation, turn, operation);
398
472
  }
399
473
  begin(conversation) {
400
- const turn = this.report(conversation);
474
+ const turn = new Turn((operation) => this.write(conversation, turn, operation));
475
+ conversation.open++;
476
+ void this.deliver(conversation, turn, 'started', {})
477
+ .then(() => {
478
+ turn.admitted = true;
479
+ this.replay(conversation, turn);
480
+ })
481
+ .catch(logged);
401
482
  conversation.turn = turn;
483
+ if (conversation.context !== null)
484
+ turn.parts.measure(conversation.context);
485
+ for (const part of conversation.held.splice(0))
486
+ turn.parts.start(part);
402
487
  this.kit.changed();
403
488
  return turn;
404
489
  }
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) {
490
+ end(conversation, turn, failed) {
417
491
  if (turn.ended)
418
492
  return;
493
+ if (conversation.turn === turn) {
494
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
495
+ this.apply(conversation, turn, event);
496
+ }
419
497
  turn.ended = true;
420
498
  conversation.open--;
421
499
  if (conversation.turn === turn)
422
500
  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);
501
+ const body = Promise.allSettled(turn.uploads).then(() => ({
502
+ parts: turn.parts.parts,
503
+ context: turn.parts.context,
504
+ ...failed,
505
+ }));
506
+ void this.deliver(conversation, turn, 'ended', body).catch(logged);
507
+ this.next(conversation);
427
508
  this.kit.changed();
428
509
  }
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
510
  prompt(conversation, prompt) {
436
511
  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 });
512
+ const turn = this.begin(conversation);
513
+ turn.prompted = true;
514
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
515
+ if (running.adapter.connection.signal.aborted) {
516
+ this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
517
+ }
518
+ else {
519
+ if (conversation.open === 1 && conversation.running === running) {
520
+ running.scope.kill();
521
+ this.detach(conversation, running);
522
+ }
523
+ this.end(conversation, turn, { failed: causeOf(error) });
449
524
  }
450
525
  });
451
526
  }
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
527
  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;
528
+ return conversation.turn ?? this.begin(conversation);
484
529
  }
485
530
  ended(conversation) {
486
531
  const turn = conversation.turn;
487
532
  if (turn === null)
488
533
  return;
489
- if (turn.prompt === null) {
490
- this.finish(conversation, turn);
534
+ if (!turn.prompted) {
535
+ this.end(conversation, turn, null);
491
536
  }
492
537
  else {
538
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
539
+ this.apply(conversation, turn, event);
493
540
  conversation.turn = null;
494
541
  this.kit.changed();
495
542
  }
@@ -538,101 +585,102 @@ export class Conversations {
538
585
  if (content !== null)
539
586
  this.questions.get(interactionId)?.answer({ action: 'accept', content });
540
587
  }
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
- }
588
+ read(conversation, reader, message) {
589
+ for (const event of reader.read(message))
590
+ this.event(conversation, event);
566
591
  }
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();
592
+ event(conversation, event) {
593
+ switch (event.kind) {
594
+ case 'started':
595
+ this.started(conversation);
596
+ return;
597
+ case 'ended':
598
+ this.ended(conversation);
599
+ return;
600
+ case 'commands':
601
+ this.commands(conversation, event.commands);
602
+ return;
603
+ case 'options':
604
+ this.options(conversation, event.options);
605
+ return;
606
+ case 'context':
607
+ conversation.context = { used: event.used, window: event.window };
608
+ conversation.turn?.parts.measure(conversation.context);
609
+ return;
610
+ case 'subagent-done':
611
+ this.subagentDone(conversation, event.id, event.result);
612
+ return;
613
+ case 'marker':
614
+ case 'image':
615
+ if (conversation.turn !== null)
616
+ break;
617
+ if (event.kind === 'marker' && event.marker === 'job')
618
+ conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
619
+ return;
620
+ }
621
+ this.apply(conversation, this.started(conversation), event);
573
622
  }
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;
623
+ apply(conversation, turn, event) {
624
+ switch (event.kind) {
625
+ case 'text':
626
+ case 'reasoning':
627
+ turn.parts.write(event.kind, event.id, event.text);
628
+ return;
629
+ case 'command':
630
+ turn.parts.command();
631
+ return;
632
+ case 'subagent': {
633
+ const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
634
+ conversation.subagents.set(event.id, { description: event.description, turn, index });
635
+ return;
636
+ }
637
+ case 'message':
638
+ turn.parts.start({ type: 'message', command: event.command, target: event.target });
639
+ return;
640
+ case 'marker':
641
+ turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
642
+ return;
643
+ case 'plan':
644
+ turn.parts.plan(event.entries);
645
+ return;
646
+ case 'image':
647
+ turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
648
+ this.kit.changed();
649
+ return;
582
650
  }
583
- turn.segment ??= { messageId, phase, text: '' };
584
- turn.segment.text += text;
585
651
  }
586
- close(conversation, turn) {
587
- const segment = turn.segment;
588
- if (segment === null)
652
+ subagentDone(conversation, id, result) {
653
+ const subagent = conversation.subagents.get(id);
654
+ if (subagent === undefined)
589
655
  return;
590
- turn.segment = null;
591
- this.note(conversation, turn, segment.text);
592
- }
593
- note(conversation, turn, text) {
594
- if (text.trim() === '')
656
+ conversation.subagents.delete(id);
657
+ if (subagent.turn === conversation.turn) {
658
+ subagent.turn.parts.done(subagent.index, result);
595
659
  return;
596
- const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
597
- if (turn.admitted)
598
- this.kit.send(note);
660
+ }
661
+ const part = { type: 'subagent', description: subagent.description, status: 'done', result };
662
+ if (conversation.turn === null)
663
+ conversation.held.push(part);
599
664
  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;
665
+ conversation.turn.parts.start(part);
666
+ }
667
+ async image(conversation, turn, data, mediaType) {
668
+ const bytes = Buffer.from(data, 'base64');
669
+ const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
670
+ try {
671
+ const declared = (await this.deliver(conversation, turn, 'images', {
672
+ name,
673
+ media_type: mediaType,
674
+ bytes: bytes.length,
675
+ sha256: createHash('sha256').update(bytes).digest('hex'),
676
+ }));
677
+ const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
678
+ if (saved.save.status !== 'saved')
679
+ throw new Error(saved.save.failure ?? saved.save.status);
680
+ turn.parts.start({ type: 'image', file: saved.version, name, media_type: mediaType });
621
681
  }
622
- else {
623
- turn.unsentFrom = from;
682
+ catch (error) {
683
+ turn.parts.start({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` });
624
684
  }
625
685
  }
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
686
  }