@agentshouse/kit 0.2.1-c7252abc8a583 → 0.2.1-c7a397cb0cce7

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 (94) hide show
  1. package/README.md +20 -19
  2. package/dist/acp.js +34 -14
  3. package/dist/agents.js +104 -21
  4. package/dist/api.js +1 -1
  5. package/dist/app-git.js +10 -26
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +3 -6
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +19 -3
  10. package/dist/conversations.js +280 -94
  11. package/dist/copies.js +98 -47
  12. package/dist/end.js +3 -2
  13. package/dist/events.js +4 -0
  14. package/dist/files.js +8 -2
  15. package/dist/git.js +10 -8
  16. package/dist/home.js +56 -5
  17. package/dist/house-cli.js +291 -0
  18. package/dist/house-main.js +7 -228
  19. package/dist/inputs.js +63 -0
  20. package/dist/instructions.js +4 -28
  21. package/dist/kit-cli.js +68 -0
  22. package/dist/kit-main.js +8 -64
  23. package/dist/login.js +44 -27
  24. package/dist/logout.js +0 -2
  25. package/dist/placement.js +2 -20
  26. package/dist/resident.js +60 -15
  27. package/dist/scope.js +3 -0
  28. package/dist/service.js +22 -0
  29. package/dist/sign-in.js +5 -23
  30. package/dist/skills.js +81 -12
  31. package/dist/stream.js +62 -16
  32. package/dist/transcription.js +3 -3
  33. package/dist/translate.js +2 -8
  34. package/dist/update.js +21 -23
  35. package/dist/wire.js +1 -1
  36. package/dist/work.js +3 -5
  37. package/how-we-work.md +13 -3
  38. package/package.json +3 -2
  39. package/skills/ask-matt/SKILL.md +17 -11
  40. package/skills/ask-matt/agents/openai.yaml +1 -0
  41. package/skills/code-review/SKILL.md +5 -5
  42. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  43. package/skills/codebase-design/SKILL.md +0 -2
  44. package/skills/diagnosing-bugs/SKILL.md +2 -4
  45. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  46. package/skills/domain-modeling/SKILL.md +12 -14
  47. package/skills/grill-me/SKILL.md +1 -2
  48. package/skills/grill-me/agents/openai.yaml +1 -0
  49. package/skills/grill-with-docs/SKILL.md +1 -2
  50. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  51. package/skills/grilling/SKILL.md +2 -2
  52. package/skills/handoff/SKILL.md +2 -3
  53. package/skills/handoff/agents/openai.yaml +1 -0
  54. package/skills/implement/SKILL.md +5 -4
  55. package/skills/implement/agents/openai.yaml +1 -0
  56. package/skills/implement-spec/SKILL.md +40 -0
  57. package/skills/implement-spec/agents/openai.yaml +5 -0
  58. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  59. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  60. package/skills/pr/CREDITS.md +3 -0
  61. package/skills/pr/LICENSE +21 -0
  62. package/skills/pr/SKILL.md +170 -0
  63. package/skills/pr/agents/openai.yaml +3 -0
  64. package/skills/prototype/SKILL.md +0 -2
  65. package/skills/research/SKILL.md +0 -2
  66. package/skills/retro/LICENSE +21 -0
  67. package/skills/retro/SKILL.md +44 -0
  68. package/skills/retro/agents/openai.yaml +5 -0
  69. package/skills/tdd/SKILL.md +2 -4
  70. package/skills/teach/SKILL.md +3 -4
  71. package/skills/teach/agents/openai.yaml +1 -0
  72. package/skills/to-questionnaire/SKILL.md +1 -2
  73. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  74. package/skills/to-spec/SKILL.md +1 -2
  75. package/skills/to-spec/agents/openai.yaml +1 -0
  76. package/skills/to-tickets/SKILL.md +3 -4
  77. package/skills/to-tickets/agents/openai.yaml +1 -0
  78. package/skills/triage/AGENT-BRIEF.md +207 -0
  79. package/skills/triage/LICENSE +21 -0
  80. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  81. package/skills/triage/SKILL.md +112 -0
  82. package/skills/triage/agents/openai.yaml +5 -0
  83. package/skills/wait-what/SKILL.md +2 -3
  84. package/skills/wait-what/agents/openai.yaml +1 -0
  85. package/skills/wayfinder/SKILL.md +5 -6
  86. package/skills/wayfinder/agents/openai.yaml +1 -0
  87. package/skills/wizard/LICENSE +21 -0
  88. package/skills/wizard/SKILL.md +44 -0
  89. package/skills/wizard/agents/openai.yaml +3 -0
  90. package/skills/wizard/template.sh +223 -0
  91. package/skills/writing-for-agents/SKILL.md +0 -2
  92. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  93. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  94. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
@@ -1,42 +1,100 @@
1
- import { client, } from '@agentclientprotocol/sdk';
1
+ import { client, RequestError, } from '@agentclientprotocol/sdk';
2
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
6
  import { startAdapter } from './acp.js';
7
+ import { finalRefusal } from './api.js';
7
8
  import { openBridge } from './bridge.js';
8
9
  import { CLIS } from './clis.js';
9
- import { readerFor } from './events.js';
10
+ import { answeredFailure, readerFor } from './events.js';
10
11
  import { placeFiles, uploadBytes } from './files.js';
11
12
  import { agentBase } from './home.js';
12
13
  import { instructions } from './instructions.js';
13
14
  import { Parts } from './parts.js';
15
+ import { refusalOf } from './refusals.js';
14
16
  import { killMarked, Scope } from './scope.js';
15
17
  import { holdSecretInput } from './secret-input.js';
16
18
  import { createHowWeWork } from './skills.js';
17
19
  import { transcribe } from './transcription.js';
18
20
  import { workFor } from './work.js';
21
+ const SIGNED_OUT = RequestError.authRequired().code;
19
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
+ };
20
65
  export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
21
66
  function causeOf(error) {
22
67
  return error instanceof Error ? error.message : String(error);
23
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
+ }
24
75
  function answerMessage(request, response) {
25
76
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
26
77
  }
27
78
  function logged(error) {
28
79
  process.stderr.write(`kit: ${causeOf(error)}\n`);
29
80
  }
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
+ }
30
88
  function marker(bridge) {
31
89
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
32
90
  }
33
91
  class Turn {
34
92
  id = randomUUID();
35
93
  parts;
36
- admitted = false;
37
94
  questions = 0;
38
95
  secrets = 0;
39
96
  prompted = false;
97
+ taken = null;
40
98
  ended = false;
41
99
  images = 0;
42
100
  uploads = [];
@@ -50,6 +108,8 @@ class Conversation {
50
108
  session = null;
51
109
  turn = null;
52
110
  commands = null;
111
+ options = null;
112
+ exitUntold = false;
53
113
  context = null;
54
114
  leftovers = new Set();
55
115
  waiting = [];
@@ -57,6 +117,8 @@ class Conversation {
57
117
  subagents = new Map();
58
118
  held = [];
59
119
  opening = null;
120
+ launch = null;
121
+ prompted = null;
60
122
  kills = 0;
61
123
  killed = new AbortController();
62
124
  interrupts = 0;
@@ -64,6 +126,11 @@ class Conversation {
64
126
  open = 0;
65
127
  queue = Promise.resolve();
66
128
  reports = Promise.resolve();
129
+ starting = null;
130
+ preparing = null;
131
+ told = null;
132
+ untold = false;
133
+ shown = null;
67
134
  constructor(id) {
68
135
  this.id = id;
69
136
  }
@@ -79,9 +146,7 @@ function asksSecret(request) {
79
146
  export class Conversations {
80
147
  kit;
81
148
  conversations = new Map();
82
- acks = new Map();
83
149
  questions = new Map();
84
- carrying = 0;
85
150
  reporting = 0;
86
151
  watched = false;
87
152
  constructor(kit) {
@@ -90,37 +155,26 @@ export class Conversations {
90
155
  setInterval(() => this.look(), 2_000);
91
156
  }
92
157
  input(input) {
93
- this.carrying++;
94
- const carried = this.acks.get(input.input_id) ??
95
- (() => {
96
- const conversation = this.conversation(input.conversation_id);
97
- if (input.kind === 'kill') {
98
- conversation.kills++;
99
- conversation.killed.abort(new Error(KILLED));
100
- for (const waiting of conversation.waiting.splice(0))
101
- waiting.settle({ refused: KILLED });
102
- this.halt(conversation);
103
- }
104
- if (input.kind === 'interrupt') {
105
- conversation.interrupts++;
106
- conversation.interrupted.abort();
107
- }
108
- const handled = conversation.queue.then(() => this.carry(conversation, input));
109
- conversation.queue = handled.catch(() => undefined);
110
- const work = handled.then(({ ack }) => ack);
111
- this.acks.set(input.input_id, work);
112
- return work;
113
- })();
114
- carried
115
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
116
- .catch(logged)
117
- .finally(() => {
118
- this.carrying--;
119
- this.kit.changed();
158
+ void this.kit.inputs.carry(input.input_id, () => {
159
+ const conversation = this.conversation(input.conversation_id);
160
+ if (input.kind === 'kill') {
161
+ conversation.kills++;
162
+ conversation.killed.abort(new Error(KILLED));
163
+ for (const waiting of conversation.waiting.splice(0))
164
+ waiting.settle({ refused: KILLED });
165
+ this.halt(conversation);
166
+ }
167
+ if (input.kind === 'interrupt') {
168
+ conversation.interrupts++;
169
+ conversation.interrupted.abort();
170
+ }
171
+ const handled = conversation.queue.then(() => this.carry(conversation, input));
172
+ conversation.queue = handled.catch(() => undefined);
173
+ return handled.then(async ({ ack }) => ({ ack: await ack }));
120
174
  });
121
175
  }
122
176
  idle() {
123
- if (this.carrying > 0 || this.reporting > 0)
177
+ if (this.reporting > 0)
124
178
  return false;
125
179
  for (const conversation of this.conversations.values()) {
126
180
  const turn = conversation.turn;
@@ -149,6 +203,14 @@ export class Conversations {
149
203
  }
150
204
  opened() {
151
205
  this.watched = false;
206
+ for (const conversation of this.conversations.values()) {
207
+ if (conversation.running !== null)
208
+ this.facts(conversation);
209
+ else if (conversation.exitUntold)
210
+ this.process(conversation, false);
211
+ if (conversation.untold || conversation.told !== null)
212
+ this.retell(conversation);
213
+ }
152
214
  }
153
215
  watching(watched) {
154
216
  const resend = watched && !this.watched;
@@ -170,9 +232,10 @@ export class Conversations {
170
232
  }
171
233
  async carry(conversation, input) {
172
234
  if (input.kind === 'open')
173
- return { ack: Promise.resolve(await this.open(conversation, input)) };
235
+ return { ack: this.open(conversation, input) };
174
236
  if (input.kind === 'message')
175
237
  return this.message(conversation, input);
238
+ await this.launched(conversation);
176
239
  if (input.kind === 'interrupt') {
177
240
  if (--conversation.interrupts === 0)
178
241
  conversation.interrupted = new AbortController();
@@ -196,22 +259,58 @@ export class Conversations {
196
259
  }
197
260
  return { ack: Promise.resolve({}) };
198
261
  }
199
- async open(conversation, input) {
262
+ open(conversation, input) {
200
263
  if (conversation.running !== null)
201
- return {};
202
- try {
203
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
204
- }
205
- catch (error) {
206
- return { refused: causeOf(error) };
207
- }
264
+ return Promise.resolve({});
265
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, input.agent_id), null, true);
266
+ return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
267
+ }
268
+ launch(conversation, route, sessionId, empty) {
269
+ let opened;
270
+ let failed;
271
+ const bridge = new Promise((resolve, reject) => {
272
+ opened = resolve;
273
+ failed = reject;
274
+ });
275
+ bridge.catch(() => undefined);
276
+ const session = this.start(conversation, route, sessionId, empty, opened);
277
+ session.catch(failed);
278
+ const launch = { bridge, session };
279
+ conversation.launch = launch;
280
+ void session
281
+ .catch(() => undefined)
282
+ .then(() => {
283
+ if (conversation.launch !== launch)
284
+ return;
285
+ conversation.launch = null;
286
+ this.next(conversation);
287
+ });
288
+ this.next(conversation);
289
+ return launch;
290
+ }
291
+ async launched(conversation) {
292
+ while (conversation.launch !== null)
293
+ await conversation.launch.session.catch(() => undefined);
294
+ }
295
+ instructed(conversation, route) {
296
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
297
+ if (bridge === undefined)
298
+ return null;
299
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
300
+ read.catch(() => undefined);
301
+ return read;
208
302
  }
209
303
  async message(conversation, input) {
210
304
  const content = [{ type: 'text', text: String(input.text) }];
211
305
  let route;
306
+ let instructing = null;
212
307
  try {
213
308
  route = await this.route(conversation, input.agent_id);
309
+ if (input.first === true)
310
+ instructing = this.instructed(conversation, route);
214
311
  const files = input.files;
312
+ if (files.length > 0)
313
+ this.prepares(conversation, 'files');
215
314
  const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
216
315
  const lines = await this.transcribed(conversation, files, paths);
217
316
  if (lines.length > 0)
@@ -220,11 +319,13 @@ export class Conversations {
220
319
  throw new Error(KILLED);
221
320
  }
222
321
  catch (error) {
223
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
322
+ this.prepares(conversation, null);
323
+ return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
224
324
  }
325
+ conversation.preparing = null;
225
326
  return {
226
327
  ack: new Promise((settle) => {
227
- conversation.waiting.push({ input, route, content, settle });
328
+ conversation.waiting.push({ input, route, content, instructing, settle });
228
329
  this.next(conversation);
229
330
  }),
230
331
  };
@@ -233,8 +334,12 @@ export class Conversations {
233
334
  if (conversation.sending !== null || conversation.open > 0)
234
335
  return;
235
336
  const waiting = conversation.waiting.shift();
236
- if (waiting === undefined)
337
+ if (waiting === undefined) {
338
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
339
+ if (conversation.starting !== step)
340
+ this.starts(conversation, step);
237
341
  return;
342
+ }
238
343
  conversation.sending = this.send(conversation, waiting).finally(() => {
239
344
  conversation.sending = null;
240
345
  this.next(conversation);
@@ -243,40 +348,80 @@ export class Conversations {
243
348
  async send(conversation, waiting) {
244
349
  const { input } = waiting;
245
350
  let ack = {};
351
+ let started = false;
352
+ let instructing = waiting.instructing;
246
353
  const prompt = [];
247
354
  try {
248
355
  if (conversation.running === null) {
249
- const resumed = input.provider_session_id ?? conversation.session;
250
- const opened = await this.start(conversation, waiting.route, resumed);
251
- if (resumed === null)
252
- ack = { provider_session_id: opened };
356
+ this.starts(conversation, 'session');
357
+ await this.launched(conversation);
358
+ }
359
+ if (conversation.running === null) {
360
+ const stored = conversation.session ?? input.provider_session_id;
361
+ const empty = input.first === true && conversation.prompted !== stored;
362
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
363
+ if (input.first === true)
364
+ instructing = this.instructed(conversation, waiting.route);
365
+ started = (await launch.session) !== stored;
366
+ }
367
+ const session = conversation.running.sessionId;
368
+ if (started || (input.first === true && session !== input.provider_session_id)) {
369
+ ack = { provider_session_id: session };
253
370
  }
254
371
  if (input.first === true) {
255
- prompt.push({
256
- type: 'text',
257
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
258
- });
372
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
259
373
  }
260
374
  if (conversation.kills > 0)
261
375
  throw new Error(KILLED);
262
376
  }
263
377
  catch (error) {
264
- if ('provider_session_id' in ack) {
378
+ this.starts(conversation, null);
379
+ if (started) {
265
380
  conversation.session = null;
266
381
  await this.kill(conversation);
267
382
  }
268
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
383
+ waiting.settle({ refused: refusal(conversation, error) });
269
384
  return;
270
385
  }
271
- this.prompt(conversation, [...prompt, ...waiting.content]);
386
+ this.starts(conversation, null);
387
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.inputs.hands(String(input.input_id), ack));
272
388
  waiting.settle(ack);
273
389
  }
390
+ starts(conversation, step) {
391
+ conversation.starting = step;
392
+ if (conversation.shown !== null)
393
+ clearTimeout(conversation.shown);
394
+ conversation.shown = null;
395
+ if (step === null || conversation.told !== null) {
396
+ this.tell(conversation, step);
397
+ return;
398
+ }
399
+ conversation.shown = setTimeout(() => {
400
+ conversation.shown = null;
401
+ if (conversation.starting === step)
402
+ this.tell(conversation, step);
403
+ }, STARTING_SHOWN_AFTER_MS);
404
+ }
405
+ prepares(conversation, step) {
406
+ conversation.preparing = step;
407
+ this.next(conversation);
408
+ }
409
+ tell(conversation, step) {
410
+ if (conversation.told === step && !conversation.untold)
411
+ return;
412
+ conversation.told = step;
413
+ this.retell(conversation);
414
+ }
415
+ retell(conversation) {
416
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
417
+ }
274
418
  async transcribed(conversation, files, paths) {
275
419
  const lines = [];
276
420
  for (const [index, path] of paths.entries()) {
277
421
  lines.push(path);
278
422
  if (files[index].media_type?.startsWith('audio/') !== true)
279
423
  continue;
424
+ this.prepares(conversation, 'transcription');
280
425
  try {
281
426
  const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
282
427
  if (text !== null && text !== '')
@@ -343,14 +488,16 @@ export class Conversations {
343
488
  }
344
489
  return route;
345
490
  }
346
- async start(conversation, route, sessionId) {
491
+ async start(conversation, routed, sessionId, empty, bridged) {
492
+ const route = await routed;
347
493
  const reader = readerFor(route.kind);
348
494
  reader.session = sessionId;
349
495
  const app = client({ name: '@agentshouse/kit' })
350
496
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
351
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
497
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
498
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
352
499
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
353
- const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
500
+ const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
354
501
  if (conversation.kills > 0) {
355
502
  bridge.close();
356
503
  throw new Error(KILLED);
@@ -358,39 +505,39 @@ export class Conversations {
358
505
  conversation.opening = bridge;
359
506
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
360
507
  const scope = new Scope(marker(bridge), randomUUID(), cli);
508
+ let adapter = null;
361
509
  try {
362
510
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
511
+ bridged(bridge);
363
512
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
364
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
513
+ const streamed = await step(NO_CONNECTION, work.open(cli));
514
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
365
515
  work.read(message);
366
516
  this.read(conversation, reader, message);
367
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
517
+ }, { ...bridge.env, ...streamed }, undefined, scope));
368
518
  void adapter.exited.then(() => bridge.close());
369
519
  const agent = adapter.connection.agent;
370
520
  const cwd = route.working_directory;
371
521
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
372
- const opened = sessionId === null
373
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
374
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
522
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
523
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
375
524
  reader.session = opened.sessionId;
376
- if (sessionId === null && CLIS[route.kind].renames) {
525
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
377
526
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
378
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
527
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
379
528
  }
380
529
  const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
381
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
530
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
382
531
  void work.settled().then(() => scope.record());
383
532
  conversation.running = running;
384
533
  conversation.session = running.sessionId;
385
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
386
- if (conversation.commands !== null)
387
- this.commands(conversation, conversation.commands);
388
- this.options(conversation, options);
389
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
534
+ conversation.options = options;
535
+ this.facts(conversation);
536
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
390
537
  return running.sessionId;
391
538
  }
392
539
  catch (error) {
393
- scope.kill();
540
+ adapter?.retire();
394
541
  work.close();
395
542
  conversation.leftovers.add(scope);
396
543
  bridge.close();
@@ -400,6 +547,18 @@ export class Conversations {
400
547
  conversation.opening = null;
401
548
  }
402
549
  }
550
+ async resume(conversation, kind, adapter, request, empty) {
551
+ try {
552
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
553
+ }
554
+ catch (error) {
555
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
556
+ throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
557
+ }
558
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
559
+ return null;
560
+ }
561
+ }
403
562
  async launchSettings(running, route, offered) {
404
563
  let options = offered;
405
564
  const settings = [
@@ -421,13 +580,10 @@ export class Conversations {
421
580
  }
422
581
  return options;
423
582
  }
424
- exited(conversation, adapter, cause) {
425
- const running = conversation.running;
426
- if (running?.adapter !== adapter)
427
- return;
583
+ exited(conversation, running, cause) {
428
584
  this.detach(conversation, running);
429
585
  if (conversation.turn !== null) {
430
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
586
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
431
587
  }
432
588
  this.next(conversation);
433
589
  this.kit.changed();
@@ -437,11 +593,22 @@ export class Conversations {
437
593
  conversation.leftovers.add(running.scope);
438
594
  conversation.running = null;
439
595
  conversation.commands = null;
596
+ conversation.options = null;
440
597
  for (const [id, question] of this.questions) {
441
598
  if (question.conversation === conversation)
442
599
  this.questions.delete(id);
443
600
  }
444
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
601
+ this.process(conversation, false);
602
+ }
603
+ process(conversation, running) {
604
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
605
+ }
606
+ facts(conversation) {
607
+ this.process(conversation, true);
608
+ if (conversation.commands !== null)
609
+ this.commands(conversation, conversation.commands);
610
+ if (conversation.options !== null)
611
+ this.options(conversation, conversation.options);
445
612
  }
446
613
  commands(conversation, commands) {
447
614
  conversation.commands = commands;
@@ -452,6 +619,7 @@ export class Conversations {
452
619
  options(conversation, options) {
453
620
  if (conversation.running === null)
454
621
  return;
622
+ conversation.options = options;
455
623
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
456
624
  }
457
625
  deliver(conversation, turn, event, body) {
@@ -466,7 +634,7 @@ export class Conversations {
466
634
  return delivered;
467
635
  }
468
636
  write(conversation, turn, operation) {
469
- if (!turn.admitted || turn.ended || !this.watched)
637
+ if (turn.ended || !this.watched)
470
638
  return;
471
639
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
472
640
  }
@@ -477,12 +645,7 @@ export class Conversations {
477
645
  begin(conversation) {
478
646
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
479
647
  conversation.open++;
480
- void this.deliver(conversation, turn, 'started', {})
481
- .then(() => {
482
- turn.admitted = true;
483
- this.replay(conversation, turn);
484
- })
485
- .catch(logged);
648
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
486
649
  conversation.turn = turn;
487
650
  if (conversation.context !== null)
488
651
  turn.parts.measure(conversation.context);
@@ -491,13 +654,17 @@ export class Conversations {
491
654
  this.kit.changed();
492
655
  return turn;
493
656
  }
494
- end(conversation, turn, failed) {
657
+ end(conversation, turn, failed, notice = null) {
495
658
  if (turn.ended)
496
659
  return;
660
+ if (failed !== null)
661
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
497
662
  if (conversation.turn === turn) {
498
663
  for (const event of conversation.running?.reader.closeTurn() ?? [])
499
664
  this.apply(conversation, turn, event);
500
665
  }
666
+ if (notice !== null)
667
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
501
668
  turn.ended = true;
502
669
  conversation.open--;
503
670
  if (conversation.turn === turn)
@@ -505,31 +672,45 @@ export class Conversations {
505
672
  const body = Promise.allSettled(turn.uploads).then(() => ({
506
673
  parts: turn.parts.saved(),
507
674
  context: turn.parts.context,
508
- ...failed,
675
+ ...(failed === null ? {} : { failed: failed.failure }),
509
676
  }));
510
677
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
511
678
  this.next(conversation);
512
679
  this.kit.changed();
513
680
  }
514
- prompt(conversation, prompt) {
681
+ prompt(conversation, prompt, taken) {
515
682
  const running = conversation.running;
516
683
  const turn = this.begin(conversation);
517
684
  turn.prompted = true;
518
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
685
+ turn.taken = taken;
686
+ conversation.prompted = running.sessionId;
687
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
688
+ this.took(turn);
689
+ const notice = answeredFailure(answer);
690
+ if (notice !== null) {
691
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
692
+ }
693
+ else {
694
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
695
+ }
696
+ }, async (error) => {
519
697
  if (running.adapter.connection.signal.aborted) {
520
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
698
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
521
699
  }
522
700
  else {
701
+ this.took(turn);
523
702
  if (conversation.open === 1 && conversation.running === running) {
524
- running.scope.kill();
703
+ running.adapter.retire();
525
704
  this.detach(conversation, running);
526
705
  }
527
- this.end(conversation, turn, { failed: causeOf(error) });
706
+ this.end(conversation, turn, answeredWith(error));
528
707
  }
529
708
  });
530
709
  }
531
710
  started(conversation) {
532
- return conversation.turn ?? this.begin(conversation);
711
+ const turn = conversation.turn ?? this.begin(conversation);
712
+ this.took(turn);
713
+ return turn;
533
714
  }
534
715
  ended(conversation) {
535
716
  const turn = conversation.turn;
@@ -624,6 +805,11 @@ export class Conversations {
624
805
  }
625
806
  this.apply(conversation, this.started(conversation), event);
626
807
  }
808
+ took(turn) {
809
+ const taken = turn.taken;
810
+ turn.taken = null;
811
+ taken?.();
812
+ }
627
813
  apply(conversation, turn, event) {
628
814
  switch (event.kind) {
629
815
  case 'text':