@agentshouse/kit 0.2.1-cbabe8ad337ff → 0.2.1-cbdf76b9a90a3

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 (98) hide show
  1. package/README.md +30 -29
  2. package/dist/acp.js +34 -14
  3. package/dist/agents.js +109 -22
  4. package/dist/api.js +6 -2
  5. package/dist/app-git.js +10 -26
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +10 -24
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +51 -9
  10. package/dist/conversations.js +289 -99
  11. package/dist/copies.js +195 -119
  12. package/dist/end.js +22 -3
  13. package/dist/enrol-main.js +7 -1
  14. package/dist/events.js +4 -0
  15. package/dist/files.js +8 -2
  16. package/dist/git.js +10 -8
  17. package/dist/home.js +96 -7
  18. package/dist/house-cli.js +291 -0
  19. package/dist/house-main.js +7 -177
  20. package/dist/inputs.js +63 -0
  21. package/dist/instructions.js +10 -32
  22. package/dist/kit-cli.js +68 -0
  23. package/dist/kit-main.js +8 -22
  24. package/dist/login.js +112 -22
  25. package/dist/logout.js +0 -2
  26. package/dist/parts.js +34 -16
  27. package/dist/placement.js +39 -0
  28. package/dist/relay.js +12 -2
  29. package/dist/resident.js +82 -26
  30. package/dist/scope.js +3 -0
  31. package/dist/service-main.js +9 -0
  32. package/dist/service.js +55 -0
  33. package/dist/sign-in.js +5 -23
  34. package/dist/skills.js +81 -12
  35. package/dist/stream.js +68 -17
  36. package/dist/transcription.js +3 -3
  37. package/dist/translate.js +2 -8
  38. package/dist/update.js +48 -29
  39. package/dist/wire.js +22 -0
  40. package/dist/work.js +3 -5
  41. package/how-we-work.md +13 -3
  42. package/package.json +3 -2
  43. package/skills/ask-matt/SKILL.md +17 -11
  44. package/skills/ask-matt/agents/openai.yaml +1 -0
  45. package/skills/code-review/SKILL.md +5 -5
  46. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  47. package/skills/codebase-design/SKILL.md +0 -2
  48. package/skills/diagnosing-bugs/SKILL.md +2 -4
  49. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  50. package/skills/domain-modeling/SKILL.md +12 -14
  51. package/skills/grill-me/SKILL.md +1 -2
  52. package/skills/grill-me/agents/openai.yaml +1 -0
  53. package/skills/grill-with-docs/SKILL.md +1 -2
  54. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  55. package/skills/grilling/SKILL.md +2 -2
  56. package/skills/handoff/SKILL.md +2 -3
  57. package/skills/handoff/agents/openai.yaml +1 -0
  58. package/skills/implement/SKILL.md +5 -4
  59. package/skills/implement/agents/openai.yaml +1 -0
  60. package/skills/implement-spec/SKILL.md +40 -0
  61. package/skills/implement-spec/agents/openai.yaml +5 -0
  62. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  63. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  64. package/skills/pr/CREDITS.md +3 -0
  65. package/skills/pr/LICENSE +21 -0
  66. package/skills/pr/SKILL.md +170 -0
  67. package/skills/pr/agents/openai.yaml +3 -0
  68. package/skills/prototype/SKILL.md +0 -2
  69. package/skills/research/SKILL.md +0 -2
  70. package/skills/retro/LICENSE +21 -0
  71. package/skills/retro/SKILL.md +44 -0
  72. package/skills/retro/agents/openai.yaml +5 -0
  73. package/skills/tdd/SKILL.md +2 -4
  74. package/skills/teach/SKILL.md +3 -4
  75. package/skills/teach/agents/openai.yaml +1 -0
  76. package/skills/to-questionnaire/SKILL.md +1 -2
  77. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  78. package/skills/to-spec/SKILL.md +1 -2
  79. package/skills/to-spec/agents/openai.yaml +1 -0
  80. package/skills/to-tickets/SKILL.md +3 -4
  81. package/skills/to-tickets/agents/openai.yaml +1 -0
  82. package/skills/triage/AGENT-BRIEF.md +207 -0
  83. package/skills/triage/LICENSE +21 -0
  84. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  85. package/skills/triage/SKILL.md +112 -0
  86. package/skills/triage/agents/openai.yaml +5 -0
  87. package/skills/wait-what/SKILL.md +2 -3
  88. package/skills/wait-what/agents/openai.yaml +1 -0
  89. package/skills/wayfinder/SKILL.md +5 -6
  90. package/skills/wayfinder/agents/openai.yaml +1 -0
  91. package/skills/wizard/LICENSE +21 -0
  92. package/skills/wizard/SKILL.md +44 -0
  93. package/skills/wizard/agents/openai.yaml +3 -0
  94. package/skills/wizard/template.sh +223 -0
  95. package/skills/writing-for-agents/SKILL.md +0 -2
  96. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  97. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  98. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
@@ -1,41 +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
+ };
65
+ export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
20
66
  function causeOf(error) {
21
67
  return error instanceof Error ? error.message : String(error);
22
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
+ }
23
75
  function answerMessage(request, response) {
24
76
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
25
77
  }
26
78
  function logged(error) {
27
79
  process.stderr.write(`kit: ${causeOf(error)}\n`);
28
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
+ }
29
88
  function marker(bridge) {
30
89
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
90
  }
32
91
  class Turn {
33
92
  id = randomUUID();
34
93
  parts;
35
- admitted = false;
36
94
  questions = 0;
37
95
  secrets = 0;
38
96
  prompted = false;
97
+ taken = null;
39
98
  ended = false;
40
99
  images = 0;
41
100
  uploads = [];
@@ -49,6 +108,8 @@ class Conversation {
49
108
  session = null;
50
109
  turn = null;
51
110
  commands = null;
111
+ options = null;
112
+ exitUntold = false;
52
113
  context = null;
53
114
  leftovers = new Set();
54
115
  waiting = [];
@@ -56,6 +117,8 @@ class Conversation {
56
117
  subagents = new Map();
57
118
  held = [];
58
119
  opening = null;
120
+ launch = null;
121
+ prompted = null;
59
122
  kills = 0;
60
123
  killed = new AbortController();
61
124
  interrupts = 0;
@@ -63,6 +126,11 @@ class Conversation {
63
126
  open = 0;
64
127
  queue = Promise.resolve();
65
128
  reports = Promise.resolve();
129
+ starting = null;
130
+ preparing = null;
131
+ told = null;
132
+ untold = false;
133
+ shown = null;
66
134
  constructor(id) {
67
135
  this.id = id;
68
136
  }
@@ -78,9 +146,7 @@ function asksSecret(request) {
78
146
  export class Conversations {
79
147
  kit;
80
148
  conversations = new Map();
81
- acks = new Map();
82
149
  questions = new Map();
83
- carrying = 0;
84
150
  reporting = 0;
85
151
  watched = false;
86
152
  constructor(kit) {
@@ -89,37 +155,26 @@ export class Conversations {
89
155
  setInterval(() => this.look(), 2_000);
90
156
  }
91
157
  input(input) {
92
- this.carrying++;
93
- const carried = this.acks.get(input.input_id) ??
94
- (() => {
95
- const conversation = this.conversation(input.conversation_id);
96
- if (input.kind === 'kill') {
97
- conversation.kills++;
98
- conversation.killed.abort(new Error(KILLED));
99
- for (const waiting of conversation.waiting.splice(0))
100
- waiting.settle({ refused: KILLED });
101
- this.halt(conversation);
102
- }
103
- if (input.kind === 'interrupt') {
104
- conversation.interrupts++;
105
- conversation.interrupted.abort();
106
- }
107
- const handled = conversation.queue.then(() => this.carry(conversation, input));
108
- conversation.queue = handled.catch(() => undefined);
109
- const work = handled.then(({ ack }) => ack);
110
- this.acks.set(input.input_id, work);
111
- return work;
112
- })();
113
- carried
114
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
115
- .catch(logged)
116
- .finally(() => {
117
- this.carrying--;
118
- 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 }));
119
174
  });
120
175
  }
121
176
  idle() {
122
- if (this.carrying > 0 || this.reporting > 0)
177
+ if (this.reporting > 0)
123
178
  return false;
124
179
  for (const conversation of this.conversations.values()) {
125
180
  const turn = conversation.turn;
@@ -132,6 +187,9 @@ export class Conversations {
132
187
  return false;
133
188
  return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
134
189
  }
190
+ quiet() {
191
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
192
+ }
135
193
  look() {
136
194
  for (const conversation of this.conversations.values()) {
137
195
  for (const scope of conversation.leftovers) {
@@ -145,6 +203,14 @@ export class Conversations {
145
203
  }
146
204
  opened() {
147
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
+ }
148
214
  }
149
215
  watching(watched) {
150
216
  const resend = watched && !this.watched;
@@ -166,9 +232,10 @@ export class Conversations {
166
232
  }
167
233
  async carry(conversation, input) {
168
234
  if (input.kind === 'open')
169
- return { ack: Promise.resolve(await this.open(conversation, input)) };
235
+ return { ack: this.open(conversation, input) };
170
236
  if (input.kind === 'message')
171
237
  return this.message(conversation, input);
238
+ await this.launched(conversation);
172
239
  if (input.kind === 'interrupt') {
173
240
  if (--conversation.interrupts === 0)
174
241
  conversation.interrupted = new AbortController();
@@ -192,22 +259,58 @@ export class Conversations {
192
259
  }
193
260
  return { ack: Promise.resolve({}) };
194
261
  }
195
- async open(conversation, input) {
262
+ open(conversation, input) {
196
263
  if (conversation.running !== null)
197
- return {};
198
- try {
199
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
200
- }
201
- catch (error) {
202
- return { refused: causeOf(error) };
203
- }
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;
204
302
  }
205
303
  async message(conversation, input) {
206
304
  const content = [{ type: 'text', text: String(input.text) }];
207
305
  let route;
306
+ let instructing = null;
208
307
  try {
209
308
  route = await this.route(conversation, input.agent_id);
309
+ if (input.first === true)
310
+ instructing = this.instructed(conversation, route);
210
311
  const files = input.files;
312
+ if (files.length > 0)
313
+ this.prepares(conversation, 'files');
211
314
  const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
212
315
  const lines = await this.transcribed(conversation, files, paths);
213
316
  if (lines.length > 0)
@@ -216,11 +319,13 @@ export class Conversations {
216
319
  throw new Error(KILLED);
217
320
  }
218
321
  catch (error) {
219
- 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) }) };
220
324
  }
325
+ conversation.preparing = null;
221
326
  return {
222
327
  ack: new Promise((settle) => {
223
- conversation.waiting.push({ input, route, content, settle });
328
+ conversation.waiting.push({ input, route, content, instructing, settle });
224
329
  this.next(conversation);
225
330
  }),
226
331
  };
@@ -229,8 +334,12 @@ export class Conversations {
229
334
  if (conversation.sending !== null || conversation.open > 0)
230
335
  return;
231
336
  const waiting = conversation.waiting.shift();
232
- 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);
233
341
  return;
342
+ }
234
343
  conversation.sending = this.send(conversation, waiting).finally(() => {
235
344
  conversation.sending = null;
236
345
  this.next(conversation);
@@ -239,40 +348,80 @@ export class Conversations {
239
348
  async send(conversation, waiting) {
240
349
  const { input } = waiting;
241
350
  let ack = {};
351
+ let started = false;
352
+ let instructing = waiting.instructing;
242
353
  const prompt = [];
243
354
  try {
244
355
  if (conversation.running === null) {
245
- const resumed = input.provider_session_id ?? conversation.session;
246
- const opened = await this.start(conversation, waiting.route, resumed);
247
- if (resumed === null)
248
- 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 };
249
370
  }
250
371
  if (input.first === true) {
251
- prompt.push({
252
- type: 'text',
253
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
254
- });
372
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
255
373
  }
256
374
  if (conversation.kills > 0)
257
375
  throw new Error(KILLED);
258
376
  }
259
377
  catch (error) {
260
- if ('provider_session_id' in ack) {
378
+ this.starts(conversation, null);
379
+ if (started) {
261
380
  conversation.session = null;
262
381
  await this.kill(conversation);
263
382
  }
264
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
383
+ waiting.settle({ refused: refusal(conversation, error) });
265
384
  return;
266
385
  }
267
- 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));
268
388
  waiting.settle(ack);
269
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
+ }
270
418
  async transcribed(conversation, files, paths) {
271
419
  const lines = [];
272
420
  for (const [index, path] of paths.entries()) {
273
421
  lines.push(path);
274
422
  if (files[index].media_type?.startsWith('audio/') !== true)
275
423
  continue;
424
+ this.prepares(conversation, 'transcription');
276
425
  try {
277
426
  const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
278
427
  if (text !== null && text !== '')
@@ -339,14 +488,16 @@ export class Conversations {
339
488
  }
340
489
  return route;
341
490
  }
342
- async start(conversation, route, sessionId) {
491
+ async start(conversation, routed, sessionId, empty, bridged) {
492
+ const route = await routed;
343
493
  const reader = readerFor(route.kind);
344
494
  reader.session = sessionId;
345
495
  const app = client({ name: '@agentshouse/kit' })
346
496
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
347
- .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));
348
499
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
349
- 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));
350
501
  if (conversation.kills > 0) {
351
502
  bridge.close();
352
503
  throw new Error(KILLED);
@@ -354,39 +505,39 @@ export class Conversations {
354
505
  conversation.opening = bridge;
355
506
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
356
507
  const scope = new Scope(marker(bridge), randomUUID(), cli);
508
+ let adapter = null;
357
509
  try {
358
510
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
511
+ bridged(bridge);
359
512
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
360
- 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) => {
361
515
  work.read(message);
362
516
  this.read(conversation, reader, message);
363
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
517
+ }, { ...bridge.env, ...streamed }, undefined, scope));
364
518
  void adapter.exited.then(() => bridge.close());
365
519
  const agent = adapter.connection.agent;
366
520
  const cwd = route.working_directory;
367
521
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
368
- const opened = sessionId === null
369
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
370
- : { ...(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 })));
371
524
  reader.session = opened.sessionId;
372
- if (sessionId === null && CLIS[route.kind].renames) {
525
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
373
526
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
374
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
527
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
375
528
  }
376
529
  const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
377
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
530
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
378
531
  void work.settled().then(() => scope.record());
379
532
  conversation.running = running;
380
533
  conversation.session = running.sessionId;
381
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
382
- if (conversation.commands !== null)
383
- this.commands(conversation, conversation.commands);
384
- this.options(conversation, options);
385
- 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));
386
537
  return running.sessionId;
387
538
  }
388
539
  catch (error) {
389
- scope.kill();
540
+ adapter?.retire();
390
541
  work.close();
391
542
  conversation.leftovers.add(scope);
392
543
  bridge.close();
@@ -396,6 +547,18 @@ export class Conversations {
396
547
  conversation.opening = null;
397
548
  }
398
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
+ }
399
562
  async launchSettings(running, route, offered) {
400
563
  let options = offered;
401
564
  const settings = [
@@ -417,13 +580,10 @@ export class Conversations {
417
580
  }
418
581
  return options;
419
582
  }
420
- exited(conversation, adapter, cause) {
421
- const running = conversation.running;
422
- if (running?.adapter !== adapter)
423
- return;
583
+ exited(conversation, running, cause) {
424
584
  this.detach(conversation, running);
425
585
  if (conversation.turn !== null) {
426
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
586
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
427
587
  }
428
588
  this.next(conversation);
429
589
  this.kit.changed();
@@ -433,11 +593,22 @@ export class Conversations {
433
593
  conversation.leftovers.add(running.scope);
434
594
  conversation.running = null;
435
595
  conversation.commands = null;
596
+ conversation.options = null;
436
597
  for (const [id, question] of this.questions) {
437
598
  if (question.conversation === conversation)
438
599
  this.questions.delete(id);
439
600
  }
440
- 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);
441
612
  }
442
613
  commands(conversation, commands) {
443
614
  conversation.commands = commands;
@@ -448,6 +619,7 @@ export class Conversations {
448
619
  options(conversation, options) {
449
620
  if (conversation.running === null)
450
621
  return;
622
+ conversation.options = options;
451
623
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
452
624
  }
453
625
  deliver(conversation, turn, event, body) {
@@ -462,7 +634,7 @@ export class Conversations {
462
634
  return delivered;
463
635
  }
464
636
  write(conversation, turn, operation) {
465
- if (!turn.admitted || turn.ended || !this.watched)
637
+ if (turn.ended || !this.watched)
466
638
  return;
467
639
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
468
640
  }
@@ -473,12 +645,7 @@ export class Conversations {
473
645
  begin(conversation) {
474
646
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
475
647
  conversation.open++;
476
- void this.deliver(conversation, turn, 'started', {})
477
- .then(() => {
478
- turn.admitted = true;
479
- this.replay(conversation, turn);
480
- })
481
- .catch(logged);
648
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
482
649
  conversation.turn = turn;
483
650
  if (conversation.context !== null)
484
651
  turn.parts.measure(conversation.context);
@@ -487,45 +654,63 @@ export class Conversations {
487
654
  this.kit.changed();
488
655
  return turn;
489
656
  }
490
- end(conversation, turn, failed) {
657
+ end(conversation, turn, failed, notice = null) {
491
658
  if (turn.ended)
492
659
  return;
660
+ if (failed !== null)
661
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
493
662
  if (conversation.turn === turn) {
494
663
  for (const event of conversation.running?.reader.closeTurn() ?? [])
495
664
  this.apply(conversation, turn, event);
496
665
  }
666
+ if (notice !== null)
667
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
497
668
  turn.ended = true;
498
669
  conversation.open--;
499
670
  if (conversation.turn === turn)
500
671
  conversation.turn = null;
501
672
  const body = Promise.allSettled(turn.uploads).then(() => ({
502
- parts: turn.parts.parts,
673
+ parts: turn.parts.saved(),
503
674
  context: turn.parts.context,
504
- ...failed,
675
+ ...(failed === null ? {} : { failed: failed.failure }),
505
676
  }));
506
677
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
507
678
  this.next(conversation);
508
679
  this.kit.changed();
509
680
  }
510
- prompt(conversation, prompt) {
681
+ prompt(conversation, prompt, taken) {
511
682
  const running = conversation.running;
512
683
  const turn = this.begin(conversation);
513
684
  turn.prompted = true;
514
- 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) => {
515
697
  if (running.adapter.connection.signal.aborted) {
516
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
698
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
517
699
  }
518
700
  else {
701
+ this.took(turn);
519
702
  if (conversation.open === 1 && conversation.running === running) {
520
- running.scope.kill();
703
+ running.adapter.retire();
521
704
  this.detach(conversation, running);
522
705
  }
523
- this.end(conversation, turn, { failed: causeOf(error) });
706
+ this.end(conversation, turn, answeredWith(error));
524
707
  }
525
708
  });
526
709
  }
527
710
  started(conversation) {
528
- return conversation.turn ?? this.begin(conversation);
711
+ const turn = conversation.turn ?? this.begin(conversation);
712
+ this.took(turn);
713
+ return turn;
529
714
  }
530
715
  ended(conversation) {
531
716
  const turn = conversation.turn;
@@ -620,6 +805,11 @@ export class Conversations {
620
805
  }
621
806
  this.apply(conversation, this.started(conversation), event);
622
807
  }
808
+ took(turn) {
809
+ const taken = turn.taken;
810
+ turn.taken = null;
811
+ taken?.();
812
+ }
623
813
  apply(conversation, turn, event) {
624
814
  switch (event.kind) {
625
815
  case 'text':
@@ -644,7 +834,7 @@ export class Conversations {
644
834
  turn.parts.plan(event.entries);
645
835
  return;
646
836
  case 'image':
647
- turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
837
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
648
838
  this.kit.changed();
649
839
  return;
650
840
  }
@@ -664,7 +854,7 @@ export class Conversations {
664
854
  else
665
855
  conversation.turn.parts.start(part);
666
856
  }
667
- async image(conversation, turn, data, mediaType) {
857
+ async image(conversation, turn, reserved, data, mediaType) {
668
858
  const bytes = Buffer.from(data, 'base64');
669
859
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
670
860
  try {
@@ -677,10 +867,10 @@ export class Conversations {
677
867
  const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
678
868
  if (saved.save.status !== 'saved')
679
869
  throw new Error(saved.save.failure ?? saved.save.status);
680
- turn.parts.start({ type: 'image', file: saved.version, name, media_type: mediaType });
870
+ turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
681
871
  }
682
872
  catch (error) {
683
- turn.parts.start({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` });
873
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
684
874
  }
685
875
  }
686
876
  }