@agentshouse/kit 0.2.1-c30728be20485 → 0.2.1-c35516351ef49

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