@agentshouse/kit 0.2.1-c5262a8cc063d → 0.2.1-c5883630417e1

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