@agentshouse/kit 0.2.1-c15804a0fa28d → 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 +153 -44
  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 +137 -65
  10. package/dist/codex-main.js +58 -0
  11. package/dist/conversations.js +352 -244
  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 +4 -1
  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 -9
  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,39 +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
- 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));
289
406
  void adapter.exited.then(() => bridge.close());
290
407
  const agent = adapter.connection.agent;
291
408
  const cwd = route.working_directory;
292
- 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
- };
303
- 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());
304
420
  conversation.running = running;
421
+ conversation.session = running.sessionId;
305
422
  this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
306
423
  if (conversation.commands !== null)
307
424
  this.commands(conversation, conversation.commands);
@@ -310,7 +427,9 @@ export class Conversations {
310
427
  return running.sessionId;
311
428
  }
312
429
  catch (error) {
313
- stop(bridge);
430
+ scope.kill();
431
+ work.close();
432
+ conversation.leftovers.add(scope);
314
433
  bridge.close();
315
434
  throw conversation.kills > 0 ? new Error(KILLED) : error;
316
435
  }
@@ -318,9 +437,22 @@ export class Conversations {
318
437
  conversation.opening = null;
319
438
  }
320
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
+ }
321
452
  async launchSettings(running, route, offered) {
322
453
  let options = offered;
323
454
  const settings = [
455
+ ['mode', route.mode ?? CLIS[route.kind].fullAccess],
324
456
  ['model', route.model],
325
457
  ['thought_level', route.effort],
326
458
  ];
@@ -328,6 +460,7 @@ export class Conversations {
328
460
  if (value === null)
329
461
  continue;
330
462
  const option = options.find((candidate) => candidate.category === category);
463
+ running.reader.requested(option?.id ?? category, value);
331
464
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
332
465
  sessionId: running.sessionId,
333
466
  configId: option?.id ?? category,
@@ -341,19 +474,23 @@ export class Conversations {
341
474
  const running = conversation.running;
342
475
  if (running?.adapter !== adapter)
343
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);
344
487
  conversation.running = null;
345
488
  conversation.commands = null;
346
- conversation.jobs.clear();
347
- conversation.queued.length = 0;
348
489
  for (const [id, question] of this.questions) {
349
490
  if (question.conversation === conversation)
350
491
  this.questions.delete(id);
351
492
  }
352
493
  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
494
  }
358
495
  commands(conversation, commands) {
359
496
  conversation.commands = commands;
@@ -368,7 +505,7 @@ export class Conversations {
368
505
  }
369
506
  deliver(conversation, turn, event, body) {
370
507
  this.reporting++;
371
- 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));
372
509
  conversation.reports = delivered
373
510
  .catch(() => undefined)
374
511
  .finally(() => {
@@ -377,119 +514,89 @@ export class Conversations {
377
514
  });
378
515
  return delivered;
379
516
  }
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;
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 });
387
521
  }
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);
522
+ replay(conversation, turn) {
523
+ for (const operation of turn.parts.operations())
524
+ this.write(conversation, turn, operation);
398
525
  }
399
526
  begin(conversation) {
400
- 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);
401
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);
402
540
  this.kit.changed();
403
541
  return turn;
404
542
  }
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) {
543
+ end(conversation, turn, failed, notice = null) {
417
544
  if (turn.ended)
418
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 });
419
552
  turn.ended = true;
420
553
  conversation.open--;
421
554
  if (conversation.turn === turn)
422
555
  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);
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);
427
563
  this.kit.changed();
428
564
  }
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
565
  prompt(conversation, prompt) {
436
566
  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 });
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 });
577
+ }
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) });
449
584
  }
450
585
  });
451
586
  }
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
587
  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;
588
+ return conversation.turn ?? this.begin(conversation);
484
589
  }
485
590
  ended(conversation) {
486
591
  const turn = conversation.turn;
487
592
  if (turn === null)
488
593
  return;
489
- if (turn.prompt === null) {
490
- this.finish(conversation, turn);
594
+ if (!turn.prompted) {
595
+ this.end(conversation, turn, null);
491
596
  }
492
597
  else {
598
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
599
+ this.apply(conversation, turn, event);
493
600
  conversation.turn = null;
494
601
  this.kit.changed();
495
602
  }
@@ -538,101 +645,102 @@ export class Conversations {
538
645
  if (content !== null)
539
646
  this.questions.get(interactionId)?.answer({ action: 'accept', content });
540
647
  }
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
- }
648
+ read(conversation, reader, message) {
649
+ for (const event of reader.read(message))
650
+ this.event(conversation, event);
566
651
  }
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();
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);
573
682
  }
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;
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;
582
710
  }
583
- turn.segment ??= { messageId, phase, text: '' };
584
- turn.segment.text += text;
585
711
  }
586
- close(conversation, turn) {
587
- const segment = turn.segment;
588
- if (segment === null)
712
+ subagentDone(conversation, id, result) {
713
+ const subagent = conversation.subagents.get(id);
714
+ if (subagent === undefined)
589
715
  return;
590
- turn.segment = null;
591
- this.note(conversation, turn, segment.text);
592
- }
593
- note(conversation, turn, text) {
594
- if (text.trim() === '')
716
+ conversation.subagents.delete(id);
717
+ if (subagent.turn === conversation.turn) {
718
+ subagent.turn.parts.done(subagent.index, result);
595
719
  return;
596
- const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
597
- if (turn.admitted)
598
- 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);
599
724
  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;
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);
621
741
  }
622
- else {
623
- turn.unsentFrom = from;
742
+ catch (error) {
743
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
624
744
  }
625
745
  }
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
746
  }