@agentshouse/kit 0.2.1-cb0bd1cadcfed → 0.2.1-cb345f4157685

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