@agentshouse/kit 0.2.1-ce145e9aadbf6 → 0.2.1-cf6326dfa5c3e

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 (103) hide show
  1. package/README.md +32 -31
  2. package/dist/acp.js +46 -103
  3. package/dist/agents.js +151 -41
  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 +36 -57
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +126 -69
  10. package/dist/codex-main.js +58 -0
  11. package/dist/conversations.js +457 -277
  12. package/dist/copies.js +198 -124
  13. package/dist/end.js +23 -0
  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 +63 -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/refusals.js +12 -0
  30. package/dist/relay.js +12 -2
  31. package/dist/resident.js +91 -35
  32. package/dist/scope.js +203 -0
  33. package/dist/secret-input.js +3 -2
  34. package/dist/service-main.js +9 -0
  35. package/dist/service.js +55 -0
  36. package/dist/shell.js +3 -0
  37. package/dist/sign-in.js +34 -28
  38. package/dist/skills.js +81 -12
  39. package/dist/stream.js +87 -11
  40. package/dist/transcription.js +57 -9
  41. package/dist/translate.js +2 -8
  42. package/dist/update.js +50 -29
  43. package/dist/wire.js +6 -0
  44. package/dist/work.js +346 -0
  45. package/how-we-work.md +13 -3
  46. package/package.json +3 -2
  47. package/skills/ask-matt/SKILL.md +17 -11
  48. package/skills/ask-matt/agents/openai.yaml +1 -0
  49. package/skills/code-review/SKILL.md +5 -5
  50. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  51. package/skills/codebase-design/SKILL.md +0 -2
  52. package/skills/diagnosing-bugs/SKILL.md +2 -4
  53. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  54. package/skills/domain-modeling/SKILL.md +12 -14
  55. package/skills/grill-me/SKILL.md +1 -2
  56. package/skills/grill-me/agents/openai.yaml +1 -0
  57. package/skills/grill-with-docs/SKILL.md +1 -2
  58. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  59. package/skills/grilling/SKILL.md +2 -2
  60. package/skills/handoff/SKILL.md +2 -3
  61. package/skills/handoff/agents/openai.yaml +1 -0
  62. package/skills/implement/SKILL.md +5 -4
  63. package/skills/implement/agents/openai.yaml +1 -0
  64. package/skills/implement-spec/SKILL.md +40 -0
  65. package/skills/implement-spec/agents/openai.yaml +5 -0
  66. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  67. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  68. package/skills/pr/CREDITS.md +3 -0
  69. package/skills/pr/LICENSE +21 -0
  70. package/skills/pr/SKILL.md +170 -0
  71. package/skills/pr/agents/openai.yaml +3 -0
  72. package/skills/prototype/SKILL.md +0 -2
  73. package/skills/research/SKILL.md +0 -2
  74. package/skills/retro/LICENSE +21 -0
  75. package/skills/retro/SKILL.md +44 -0
  76. package/skills/retro/agents/openai.yaml +5 -0
  77. package/skills/tdd/SKILL.md +2 -4
  78. package/skills/teach/SKILL.md +3 -4
  79. package/skills/teach/agents/openai.yaml +1 -0
  80. package/skills/to-questionnaire/SKILL.md +1 -2
  81. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  82. package/skills/to-spec/SKILL.md +1 -2
  83. package/skills/to-spec/agents/openai.yaml +1 -0
  84. package/skills/to-tickets/SKILL.md +3 -4
  85. package/skills/to-tickets/agents/openai.yaml +1 -0
  86. package/skills/triage/AGENT-BRIEF.md +207 -0
  87. package/skills/triage/LICENSE +21 -0
  88. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  89. package/skills/triage/SKILL.md +112 -0
  90. package/skills/triage/agents/openai.yaml +5 -0
  91. package/skills/wait-what/SKILL.md +2 -3
  92. package/skills/wait-what/agents/openai.yaml +1 -0
  93. package/skills/wayfinder/SKILL.md +5 -6
  94. package/skills/wayfinder/agents/openai.yaml +1 -0
  95. package/skills/wizard/LICENSE +21 -0
  96. package/skills/wizard/SKILL.md +44 -0
  97. package/skills/wizard/agents/openai.yaml +3 -0
  98. package/skills/wizard/template.sh +223 -0
  99. package/skills/writing-for-agents/SKILL.md +0 -2
  100. package/dist/blocks.js +0 -51
  101. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  102. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  103. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
@@ -1,56 +1,121 @@
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 NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
35
+ const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
36
+ class NotStarted extends Error {
37
+ }
38
+ async function step(reason, work) {
39
+ try {
40
+ return await work;
41
+ }
42
+ catch (error) {
43
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
44
+ }
45
+ }
46
+ function signedOut(error) {
47
+ return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
48
+ }
49
+ function unopened(reason) {
50
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
51
+ }
52
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
53
+ const STARTING_SHOWN_AFTER_MS = 1_000;
54
+ function unbridged(error) {
55
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
56
+ }
57
+ const EXIT_PLAN_MODE = {
58
+ parse(value) {
59
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
60
+ throw new Error('_x.ai/exit_plan_mode needs an object');
61
+ }
62
+ return value;
63
+ },
64
+ };
65
+ export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
17
66
  function causeOf(error) {
18
67
  return error instanceof Error ? error.message : String(error);
19
68
  }
69
+ function answeredWith(error) {
70
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
71
+ }
72
+ function exitedWith(running, cause) {
73
+ return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
74
+ }
20
75
  function answerMessage(request, response) {
21
76
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
22
77
  }
23
78
  function logged(error) {
24
79
  process.stderr.write(`kit: ${causeOf(error)}\n`);
25
80
  }
26
- function stop(bridge) {
27
- killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
81
+ function refusal(conversation, error) {
82
+ if (conversation.kills > 0)
83
+ return KILLED;
84
+ const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
85
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
86
+ return causeOf(error);
87
+ }
88
+ function marker(bridge) {
89
+ return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
28
90
  }
29
91
  class Turn {
30
92
  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;
93
+ parts;
40
94
  questions = 0;
41
95
  secrets = 0;
42
- prompt = null;
96
+ prompted = false;
97
+ taken = null;
43
98
  ended = false;
99
+ images = 0;
100
+ uploads = [];
101
+ constructor(emit) {
102
+ this.parts = new Parts(emit);
103
+ }
44
104
  }
45
105
  class Conversation {
46
106
  id;
47
107
  running = null;
108
+ session = null;
48
109
  turn = null;
49
110
  commands = null;
50
- jobs = new Set();
51
- queued = [];
52
- late = [];
111
+ context = null;
112
+ leftovers = new Set();
113
+ waiting = [];
114
+ sending = null;
115
+ subagents = new Map();
116
+ held = [];
53
117
  opening = null;
118
+ prompted = null;
54
119
  kills = 0;
55
120
  killed = new AbortController();
56
121
  interrupts = 0;
@@ -58,6 +123,11 @@ class Conversation {
58
123
  open = 0;
59
124
  queue = Promise.resolve();
60
125
  reports = Promise.resolve();
126
+ starting = null;
127
+ preparing = null;
128
+ told = null;
129
+ untold = false;
130
+ shown = null;
61
131
  constructor(id) {
62
132
  this.id = id;
63
133
  }
@@ -73,55 +143,76 @@ function asksSecret(request) {
73
143
  export class Conversations {
74
144
  kit;
75
145
  conversations = new Map();
76
- acks = new Map();
77
146
  questions = new Map();
78
- carrying = 0;
79
147
  reporting = 0;
148
+ watched = false;
80
149
  constructor(kit) {
81
150
  this.kit = kit;
151
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
152
+ setInterval(() => this.look(), 2_000);
82
153
  }
83
154
  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();
155
+ void this.kit.inputs.carry(input.input_id, () => {
156
+ const conversation = this.conversation(input.conversation_id);
157
+ if (input.kind === 'kill') {
158
+ conversation.kills++;
159
+ conversation.killed.abort(new Error(KILLED));
160
+ for (const waiting of conversation.waiting.splice(0))
161
+ waiting.settle({ refused: KILLED });
162
+ this.halt(conversation);
163
+ }
164
+ if (input.kind === 'interrupt') {
165
+ conversation.interrupts++;
166
+ conversation.interrupted.abort();
167
+ }
168
+ const handled = conversation.queue.then(() => this.carry(conversation, input));
169
+ conversation.queue = handled.catch(() => undefined);
170
+ return handled.then(async ({ ack }) => ({ ack: await ack }));
108
171
  });
109
172
  }
110
173
  idle() {
111
- if (this.carrying > 0 || this.reporting > 0)
174
+ if (this.reporting > 0)
112
175
  return false;
113
176
  for (const conversation of this.conversations.values()) {
114
177
  const turn = conversation.turn;
115
178
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
116
- if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
179
+ if (conversation.open > (asking ? 1 : 0))
117
180
  return false;
118
181
  }
119
- return true;
182
+ const conversations = [...this.conversations.values()];
183
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
184
+ return false;
185
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
186
+ }
187
+ quiet() {
188
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
189
+ }
190
+ look() {
191
+ for (const conversation of this.conversations.values()) {
192
+ for (const scope of conversation.leftovers) {
193
+ if (!scope.empty())
194
+ continue;
195
+ scope.close();
196
+ conversation.leftovers.delete(scope);
197
+ }
198
+ }
199
+ this.kit.changed();
120
200
  }
121
201
  opened() {
202
+ this.watched = false;
203
+ for (const conversation of this.conversations.values()) {
204
+ if (conversation.untold || conversation.told !== null)
205
+ this.retell(conversation);
206
+ }
207
+ }
208
+ watching(watched) {
209
+ const resend = watched && !this.watched;
210
+ this.watched = watched;
211
+ if (!resend)
212
+ return;
122
213
  for (const conversation of this.conversations.values()) {
123
214
  if (conversation.turn !== null)
124
- conversation.turn.unsentFrom = 0;
215
+ this.replay(conversation, conversation.turn);
125
216
  }
126
217
  }
127
218
  conversation(id) {
@@ -134,7 +225,7 @@ export class Conversations {
134
225
  }
135
226
  async carry(conversation, input) {
136
227
  if (input.kind === 'open')
137
- return this.open(conversation, input);
228
+ return { ack: Promise.resolve(await this.open(conversation, input)) };
138
229
  if (input.kind === 'message')
139
230
  return this.message(conversation, input);
140
231
  if (input.kind === 'interrupt') {
@@ -144,6 +235,7 @@ export class Conversations {
144
235
  }
145
236
  if (input.kind === 'kill') {
146
237
  await this.kill(conversation);
238
+ await conversation.sending;
147
239
  if (--conversation.kills === 0)
148
240
  conversation.killed = new AbortController();
149
241
  }
@@ -152,54 +244,133 @@ export class Conversations {
152
244
  if (input.kind === 'answer') {
153
245
  const question = this.questions.get(String(input.interaction_id));
154
246
  if (question === undefined) {
155
- return this.message(conversation, { ...input, text: answerMessage(input.request, input.response), files: [], first: false });
247
+ const text = answerMessage(input.request, input.response);
248
+ return this.message(conversation, { ...input, text, files: [], first: false });
156
249
  }
157
250
  question.answer(input.response);
158
251
  }
159
- return {};
252
+ return { ack: Promise.resolve({}) };
160
253
  }
161
254
  async open(conversation, input) {
162
255
  if (conversation.running !== null)
163
256
  return {};
257
+ this.prepares(conversation, 'session');
164
258
  try {
165
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
259
+ return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
166
260
  }
167
261
  catch (error) {
168
- return { refused: causeOf(error) };
262
+ return { refused: refusal(conversation, error) };
263
+ }
264
+ finally {
265
+ this.prepares(conversation, null);
169
266
  }
170
267
  }
171
268
  async message(conversation, input) {
172
- let ack = {};
173
- const prompt = [];
269
+ const content = [{ type: 'text', text: String(input.text) }];
270
+ let route;
174
271
  try {
175
- const route = await this.route(conversation, input.agent_id);
272
+ route = await this.route(conversation, input.agent_id);
176
273
  const files = input.files;
274
+ if (files.length > 0)
275
+ this.prepares(conversation, 'files');
177
276
  const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
178
277
  const lines = await this.transcribed(conversation, files, paths);
278
+ if (lines.length > 0)
279
+ content.push({ type: 'text', text: lines.join('\n') });
280
+ if (conversation.kills > 0)
281
+ throw new Error(KILLED);
282
+ }
283
+ catch (error) {
284
+ this.prepares(conversation, null);
285
+ return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
286
+ }
287
+ conversation.preparing = null;
288
+ return {
289
+ ack: new Promise((settle) => {
290
+ conversation.waiting.push({ input, route, content, settle });
291
+ this.next(conversation);
292
+ }),
293
+ };
294
+ }
295
+ next(conversation) {
296
+ if (conversation.sending !== null || conversation.open > 0)
297
+ return;
298
+ const waiting = conversation.waiting.shift();
299
+ if (waiting === undefined) {
300
+ if (conversation.starting !== conversation.preparing)
301
+ this.starts(conversation, conversation.preparing);
302
+ return;
303
+ }
304
+ conversation.sending = this.send(conversation, waiting).finally(() => {
305
+ conversation.sending = null;
306
+ this.next(conversation);
307
+ });
308
+ }
309
+ async send(conversation, waiting) {
310
+ const { input } = waiting;
311
+ let ack = {};
312
+ let started = false;
313
+ const prompt = [];
314
+ try {
179
315
  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 };
316
+ this.starts(conversation, 'session');
317
+ const stored = conversation.session ?? input.provider_session_id;
318
+ const empty = input.first === true && conversation.prompted !== stored;
319
+ started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
320
+ }
321
+ const session = conversation.running.sessionId;
322
+ if (started || (input.first === true && session !== input.provider_session_id)) {
323
+ ack = { provider_session_id: session };
183
324
  }
184
325
  if (input.first === true) {
185
326
  prompt.push({
186
327
  type: 'text',
187
- text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
328
+ text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
188
329
  });
189
330
  }
190
- prompt.push({ type: 'text', text: String(input.text) });
191
- if (lines.length > 0)
192
- prompt.push({ type: 'text', text: lines.join('\n') });
193
331
  if (conversation.kills > 0)
194
332
  throw new Error(KILLED);
195
333
  }
196
334
  catch (error) {
197
- if ('provider_session_id' in ack)
335
+ this.starts(conversation, null);
336
+ if (started) {
337
+ conversation.session = null;
198
338
  await this.kill(conversation);
199
- return { refused: conversation.kills > 0 ? KILLED : causeOf(error) };
339
+ }
340
+ waiting.settle({ refused: refusal(conversation, error) });
341
+ return;
342
+ }
343
+ this.starts(conversation, null);
344
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.inputs.hands(String(input.input_id), ack));
345
+ waiting.settle(ack);
346
+ }
347
+ starts(conversation, step) {
348
+ conversation.starting = step;
349
+ if (conversation.shown !== null)
350
+ clearTimeout(conversation.shown);
351
+ conversation.shown = null;
352
+ if (step === null || conversation.told !== null) {
353
+ this.tell(conversation, step);
354
+ return;
200
355
  }
201
- this.prompt(conversation, prompt);
202
- return ack;
356
+ conversation.shown = setTimeout(() => {
357
+ conversation.shown = null;
358
+ if (conversation.starting === step)
359
+ this.tell(conversation, step);
360
+ }, STARTING_SHOWN_AFTER_MS);
361
+ }
362
+ prepares(conversation, step) {
363
+ conversation.preparing = step;
364
+ this.next(conversation);
365
+ }
366
+ tell(conversation, step) {
367
+ if (conversation.told === step && !conversation.untold)
368
+ return;
369
+ conversation.told = step;
370
+ this.retell(conversation);
371
+ }
372
+ retell(conversation) {
373
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
203
374
  }
204
375
  async transcribed(conversation, files, paths) {
205
376
  const lines = [];
@@ -207,9 +378,10 @@ export class Conversations {
207
378
  lines.push(path);
208
379
  if (files[index].media_type?.startsWith('audio/') !== true)
209
380
  continue;
381
+ this.prepares(conversation, 'transcription');
210
382
  try {
211
383
  const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
212
- if (text !== '')
384
+ if (text !== null && text !== '')
213
385
  lines.push(`Transcript: ${text}`);
214
386
  }
215
387
  catch (error) {
@@ -229,11 +401,13 @@ export class Conversations {
229
401
  }
230
402
  halt(conversation) {
231
403
  if (conversation.opening !== null)
232
- stop(conversation.opening);
404
+ killMarked(marker(conversation.opening));
405
+ for (const scope of conversation.leftovers)
406
+ scope.kill();
233
407
  if (conversation.running === null)
234
408
  return;
235
409
  conversation.running.killed = true;
236
- stop(conversation.running.bridge);
410
+ conversation.running.scope.kill();
237
411
  }
238
412
  async kill(conversation) {
239
413
  const running = conversation.running;
@@ -245,6 +419,7 @@ export class Conversations {
245
419
  if (running === null)
246
420
  return;
247
421
  try {
422
+ running.reader.requested(option, value);
248
423
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
249
424
  sessionId: running.sessionId,
250
425
  configId: option,
@@ -253,6 +428,7 @@ export class Conversations {
253
428
  this.options(conversation, answered.configOptions);
254
429
  }
255
430
  catch (error) {
431
+ running.reader.refused(option);
256
432
  logged(error);
257
433
  }
258
434
  }
@@ -269,50 +445,58 @@ export class Conversations {
269
445
  }
270
446
  return route;
271
447
  }
272
- async start(conversation, route, sessionId) {
448
+ async start(conversation, route, sessionId, empty) {
449
+ const reader = readerFor(route.kind);
450
+ reader.session = sessionId;
273
451
  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
452
  .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));
453
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
454
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
279
455
  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);
456
+ const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
281
457
  if (conversation.kills > 0) {
282
458
  bridge.close();
283
459
  throw new Error(KILLED);
284
460
  }
285
461
  conversation.opening = bridge;
462
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
463
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
464
+ let adapter = null;
286
465
  try {
287
466
  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);
467
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
468
+ const streamed = await step(NO_CONNECTION, work.open(cli));
469
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
470
+ work.read(message);
471
+ this.read(conversation, reader, message);
472
+ }, { ...bridge.env, ...streamed }, undefined, scope));
290
473
  void adapter.exited.then(() => bridge.close());
291
474
  const agent = adapter.connection.agent;
292
475
  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 ?? []);
476
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
477
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
478
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
479
+ reader.session = opened.sessionId;
480
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
481
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
482
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
483
+ }
484
+ const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
485
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
486
+ void work.settled().then(() => scope.record());
306
487
  conversation.running = running;
488
+ conversation.session = running.sessionId;
307
489
  this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
308
490
  if (conversation.commands !== null)
309
491
  this.commands(conversation, conversation.commands);
310
492
  this.options(conversation, options);
311
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
493
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
312
494
  return running.sessionId;
313
495
  }
314
496
  catch (error) {
315
- stop(bridge);
497
+ adapter?.retire();
498
+ work.close();
499
+ conversation.leftovers.add(scope);
316
500
  bridge.close();
317
501
  throw conversation.kills > 0 ? new Error(KILLED) : error;
318
502
  }
@@ -320,6 +504,18 @@ export class Conversations {
320
504
  conversation.opening = null;
321
505
  }
322
506
  }
507
+ async resume(conversation, kind, adapter, request, empty) {
508
+ try {
509
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
510
+ }
511
+ catch (error) {
512
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
513
+ throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
514
+ }
515
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
516
+ return null;
517
+ }
518
+ }
323
519
  async launchSettings(running, route, offered) {
324
520
  let options = offered;
325
521
  const settings = [
@@ -331,6 +527,7 @@ export class Conversations {
331
527
  if (value === null)
332
528
  continue;
333
529
  const option = options.find((candidate) => candidate.category === category);
530
+ running.reader.requested(option?.id ?? category, value);
334
531
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
335
532
  sessionId: running.sessionId,
336
533
  configId: option?.id ?? category,
@@ -340,23 +537,24 @@ export class Conversations {
340
537
  }
341
538
  return options;
342
539
  }
343
- exited(conversation, adapter, cause) {
344
- const running = conversation.running;
345
- if (running?.adapter !== adapter)
346
- return;
540
+ exited(conversation, running, cause) {
541
+ this.detach(conversation, running);
542
+ if (conversation.turn !== null) {
543
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
544
+ }
545
+ this.next(conversation);
546
+ this.kit.changed();
547
+ }
548
+ detach(conversation, running) {
549
+ running.work.close();
550
+ conversation.leftovers.add(running.scope);
347
551
  conversation.running = null;
348
552
  conversation.commands = null;
349
- conversation.jobs.clear();
350
- conversation.queued.length = 0;
351
553
  for (const [id, question] of this.questions) {
352
554
  if (question.conversation === conversation)
353
555
  this.questions.delete(id);
354
556
  }
355
557
  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
558
  }
361
559
  commands(conversation, commands) {
362
560
  conversation.commands = commands;
@@ -371,7 +569,7 @@ export class Conversations {
371
569
  }
372
570
  deliver(conversation, turn, event, body) {
373
571
  this.reporting++;
374
- const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
572
+ const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
375
573
  conversation.reports = delivered
376
574
  .catch(() => undefined)
377
575
  .finally(() => {
@@ -380,119 +578,95 @@ export class Conversations {
380
578
  });
381
579
  return delivered;
382
580
  }
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;
581
+ write(conversation, turn, operation) {
582
+ if (turn.ended || !this.watched)
583
+ return;
584
+ this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
390
585
  }
391
- admitted(conversation, turn) {
392
- turn.admitted = true;
393
- for (const note of turn.notes.splice(0))
394
- this.kit.send(note);
395
- if (turn.text !== '') {
396
- turn.unsentFrom = 0;
397
- this.draft(conversation, turn);
398
- }
399
- if (turn.entries !== null)
400
- this.plan(conversation, turn);
586
+ replay(conversation, turn) {
587
+ for (const operation of turn.parts.operations())
588
+ this.write(conversation, turn, operation);
401
589
  }
402
590
  begin(conversation) {
403
- const turn = this.report(conversation);
591
+ const turn = new Turn((operation) => this.write(conversation, turn, operation));
592
+ conversation.open++;
593
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
404
594
  conversation.turn = turn;
595
+ if (conversation.context !== null)
596
+ turn.parts.measure(conversation.context);
597
+ for (const part of conversation.held.splice(0))
598
+ turn.parts.start(part);
405
599
  this.kit.changed();
406
600
  return turn;
407
601
  }
408
- finish(conversation, turn) {
409
- if (turn.ended)
410
- return;
411
- const segment = turn.segment;
412
- turn.segment = null;
413
- if (segment?.phase === 'note')
414
- this.note(conversation, turn, segment.text);
415
- else if (segment !== null)
416
- this.answer(conversation, turn, segment.text);
417
- this.end(conversation, turn, { text: turn.text });
418
- }
419
- end(conversation, turn, outcome) {
602
+ end(conversation, turn, failed, notice = null) {
420
603
  if (turn.ended)
421
604
  return;
605
+ if (failed !== null)
606
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
607
+ if (conversation.turn === turn) {
608
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
609
+ this.apply(conversation, turn, event);
610
+ }
611
+ if (notice !== null)
612
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
422
613
  turn.ended = true;
423
614
  conversation.open--;
424
615
  if (conversation.turn === turn)
425
616
  conversation.turn = null;
426
- void this.deliver(conversation, turn, 'ended', outcome).catch(logged);
427
- const late = conversation.open === 0 ? conversation.late.shift() : undefined;
428
- if (late !== undefined)
429
- this.failed(conversation, late);
617
+ const body = Promise.allSettled(turn.uploads).then(() => ({
618
+ parts: turn.parts.saved(),
619
+ context: turn.parts.context,
620
+ ...(failed === null ? {} : { failed: failed.failure }),
621
+ }));
622
+ void this.deliver(conversation, turn, 'ended', body).catch(logged);
623
+ this.next(conversation);
430
624
  this.kit.changed();
431
625
  }
432
- failed(conversation, cause) {
433
- if (conversation.open > 0)
434
- conversation.late.push(cause);
435
- else
436
- this.end(conversation, this.report(conversation), { failed: cause });
437
- }
438
- prompt(conversation, prompt) {
626
+ prompt(conversation, prompt, taken) {
439
627
  const running = conversation.running;
440
- const sent = { turn: null };
441
- if (running.queues && (conversation.turn !== null || conversation.queued.length > 0)) {
442
- conversation.queued.push(sent);
443
- }
444
- else {
445
- this.attach(conversation.turn ?? this.begin(conversation), sent);
446
- }
447
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
448
- if (!running.adapter.connection.signal.aborted)
449
- this.answered(conversation, sent, causeOf(error));
450
- else if (sent.turn !== null) {
451
- this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
628
+ const turn = this.begin(conversation);
629
+ turn.prompted = true;
630
+ turn.taken = taken;
631
+ conversation.prompted = running.sessionId;
632
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
633
+ this.took(turn);
634
+ const notice = answeredFailure(answer);
635
+ if (notice !== null) {
636
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
637
+ }
638
+ else {
639
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
640
+ }
641
+ }, async (error) => {
642
+ if (running.adapter.connection.signal.aborted) {
643
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
644
+ }
645
+ else {
646
+ this.took(turn);
647
+ if (conversation.open === 1 && conversation.running === running) {
648
+ running.adapter.retire();
649
+ this.detach(conversation, running);
650
+ }
651
+ this.end(conversation, turn, answeredWith(error));
452
652
  }
453
653
  });
454
654
  }
455
- attach(turn, sent) {
456
- turn.prompt = sent;
457
- sent.turn = turn;
458
- }
459
- answered(conversation, sent, failed) {
460
- const turn = sent.turn;
461
- if (turn === null) {
462
- const queued = conversation.queued.indexOf(sent);
463
- if (queued < 0)
464
- return;
465
- conversation.queued.splice(queued, 1);
466
- if (failed !== null)
467
- this.failed(conversation, failed);
468
- }
469
- else if (failed !== null) {
470
- if (turn.ended)
471
- this.failed(conversation, failed);
472
- else
473
- this.end(conversation, turn, { failed });
474
- }
475
- else if (turn.prompt === sent) {
476
- this.finish(conversation, turn);
477
- }
478
- }
479
655
  started(conversation) {
480
- if (conversation.turn !== null)
481
- return conversation.turn;
482
- const turn = this.begin(conversation);
483
- const queued = conversation.queued.shift();
484
- if (queued !== undefined)
485
- this.attach(turn, queued);
656
+ const turn = conversation.turn ?? this.begin(conversation);
657
+ this.took(turn);
486
658
  return turn;
487
659
  }
488
660
  ended(conversation) {
489
661
  const turn = conversation.turn;
490
662
  if (turn === null)
491
663
  return;
492
- if (turn.prompt === null) {
493
- this.finish(conversation, turn);
664
+ if (!turn.prompted) {
665
+ this.end(conversation, turn, null);
494
666
  }
495
667
  else {
668
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
669
+ this.apply(conversation, turn, event);
496
670
  conversation.turn = null;
497
671
  this.kit.changed();
498
672
  }
@@ -541,101 +715,107 @@ export class Conversations {
541
715
  if (content !== null)
542
716
  this.questions.get(interactionId)?.answer({ action: 'accept', content });
543
717
  }
544
- update(conversation, notification) {
545
- const update = notification.update;
546
- if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
547
- this.chunk(conversation, this.started(conversation), update, update.content.text);
548
- }
549
- else if (update.sessionUpdate === 'plan') {
550
- const turn = this.started(conversation);
551
- this.close(conversation, turn);
552
- turn.entries = update.entries;
553
- this.plan(conversation, turn);
554
- }
555
- else if (update.sessionUpdate === 'tool_call') {
556
- this.close(conversation, this.started(conversation));
557
- }
558
- else if (update.sessionUpdate === 'agent_thought_chunk') {
559
- const turn = this.started(conversation);
560
- if (turn.segment?.messageId !== (update.messageId ?? null))
561
- this.close(conversation, turn);
562
- }
563
- else if (update.sessionUpdate === 'available_commands_update') {
564
- this.commands(conversation, update.availableCommands);
565
- }
566
- else if (update.sessionUpdate === 'config_option_update') {
567
- this.options(conversation, update.configOptions);
718
+ read(conversation, reader, message) {
719
+ for (const event of reader.read(message))
720
+ this.event(conversation, event);
721
+ }
722
+ event(conversation, event) {
723
+ switch (event.kind) {
724
+ case 'started':
725
+ this.started(conversation);
726
+ return;
727
+ case 'ended':
728
+ this.ended(conversation);
729
+ return;
730
+ case 'commands':
731
+ this.commands(conversation, event.commands);
732
+ return;
733
+ case 'options':
734
+ this.options(conversation, event.options);
735
+ return;
736
+ case 'context':
737
+ conversation.context = { used: event.used, window: event.window };
738
+ conversation.turn?.parts.measure(conversation.context);
739
+ return;
740
+ case 'subagent-done':
741
+ this.subagentDone(conversation, event.id, event.result);
742
+ return;
743
+ case 'marker':
744
+ case 'image':
745
+ if (conversation.turn !== null)
746
+ break;
747
+ if (event.kind === 'marker' && event.marker === 'job')
748
+ conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
749
+ return;
568
750
  }
751
+ this.apply(conversation, this.started(conversation), event);
569
752
  }
570
- job(conversation, job) {
571
- if (job.running)
572
- conversation.jobs.add(job.id);
573
- else
574
- conversation.jobs.delete(job.id);
575
- this.kit.changed();
753
+ took(turn) {
754
+ const taken = turn.taken;
755
+ turn.taken = null;
756
+ taken?.();
576
757
  }
577
- chunk(conversation, turn, chunk, text) {
578
- const phase = conversation.running?.phase(chunk) ?? null;
579
- const messageId = chunk.messageId ?? null;
580
- if (turn.segment?.messageId !== messageId || turn.segment.phase !== phase)
581
- this.close(conversation, turn);
582
- if (phase === 'answer') {
583
- this.answer(conversation, turn, text);
584
- return;
758
+ apply(conversation, turn, event) {
759
+ switch (event.kind) {
760
+ case 'text':
761
+ case 'reasoning':
762
+ turn.parts.write(event.kind, event.id, event.text);
763
+ return;
764
+ case 'command':
765
+ turn.parts.command();
766
+ return;
767
+ case 'subagent': {
768
+ const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
769
+ conversation.subagents.set(event.id, { description: event.description, turn, index });
770
+ return;
771
+ }
772
+ case 'message':
773
+ turn.parts.start({ type: 'message', command: event.command, target: event.target });
774
+ return;
775
+ case 'marker':
776
+ turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
777
+ return;
778
+ case 'plan':
779
+ turn.parts.plan(event.entries);
780
+ return;
781
+ case 'image':
782
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
783
+ this.kit.changed();
784
+ return;
585
785
  }
586
- turn.segment ??= { messageId, phase, text: '' };
587
- turn.segment.text += text;
588
786
  }
589
- close(conversation, turn) {
590
- const segment = turn.segment;
591
- if (segment === null)
787
+ subagentDone(conversation, id, result) {
788
+ const subagent = conversation.subagents.get(id);
789
+ if (subagent === undefined)
592
790
  return;
593
- turn.segment = null;
594
- this.note(conversation, turn, segment.text);
595
- }
596
- note(conversation, turn, text) {
597
- if (text.trim() === '')
791
+ conversation.subagents.delete(id);
792
+ if (subagent.turn === conversation.turn) {
793
+ subagent.turn.parts.done(subagent.index, result);
598
794
  return;
599
- const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
600
- if (turn.admitted)
601
- this.kit.send(note);
795
+ }
796
+ const part = { type: 'subagent', description: subagent.description, status: 'done', result };
797
+ if (conversation.turn === null)
798
+ conversation.held.push(part);
602
799
  else
603
- turn.notes.push(note);
604
- }
605
- answer(conversation, turn, text) {
606
- turn.text += text;
607
- this.draft(conversation, turn);
608
- }
609
- draft(conversation, turn) {
610
- const blocks = blocksOf(turn.text);
611
- const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
612
- const sent = this.kit.send({
613
- type: 'draft',
614
- conversation_id: conversation.id,
615
- turn_id: turn.id,
616
- sequence: turn.draftSequence + 1,
617
- from,
618
- blocks: blocks.slice(from),
619
- });
620
- if (sent) {
621
- turn.draftSequence++;
622
- turn.sent = blocks;
623
- turn.unsentFrom = null;
800
+ conversation.turn.parts.start(part);
801
+ }
802
+ async image(conversation, turn, reserved, data, mediaType) {
803
+ const bytes = Buffer.from(data, 'base64');
804
+ const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
805
+ try {
806
+ const declared = (await this.deliver(conversation, turn, 'images', {
807
+ name,
808
+ media_type: mediaType,
809
+ bytes: bytes.length,
810
+ sha256: createHash('sha256').update(bytes).digest('hex'),
811
+ }));
812
+ const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
813
+ if (saved.save.status !== 'saved')
814
+ throw new Error(saved.save.failure ?? saved.save.status);
815
+ turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
624
816
  }
625
- else {
626
- turn.unsentFrom = from;
817
+ catch (error) {
818
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
627
819
  }
628
820
  }
629
- plan(conversation, turn) {
630
- const sent = this.kit.send({
631
- type: 'plan',
632
- conversation_id: conversation.id,
633
- turn_id: turn.id,
634
- sequence: turn.planSequence + 1,
635
- from: 0,
636
- steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
637
- });
638
- if (sent)
639
- turn.planSequence++;
640
- }
641
821
  }