@agentshouse/kit 0.2.1-c23b83622c698 → 0.2.1-c23bd93c02c83

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