@agentshouse/kit 0.2.1-cd646adba3f64 → 0.2.1-cde47fd72fe59

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