@agentshouse/kit 0.2.1-c063e835a50ea → 0.2.1-c0b0ffa6c025b

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