@agentshouse/kit 0.2.1-cf110d15f69c8 → 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 (94) hide show
  1. package/README.md +20 -19
  2. package/dist/acp.js +34 -14
  3. package/dist/agents.js +108 -21
  4. package/dist/api.js +5 -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 +209 -77
  11. package/dist/copies.js +98 -47
  12. package/dist/end.js +5 -0
  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 -206
  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 -22
  23. package/dist/login.js +46 -28
  24. package/dist/logout.js +0 -2
  25. package/dist/placement.js +30 -0
  26. package/dist/relay.js +1 -1
  27. package/dist/resident.js +65 -16
  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 +66 -17
  32. package/dist/transcription.js +3 -3
  33. package/dist/translate.js +2 -8
  34. package/dist/update.js +48 -29
  35. package/dist/wire.js +6 -0
  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
package/dist/clis.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { constants, readFileSync } from 'node:fs';
3
4
  import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
5
  import { homedir, userInfo } from 'node:os';
5
- import { delimiter, isAbsolute, join } from 'node:path';
6
+ import { delimiter, dirname, isAbsolute, join } from 'node:path';
6
7
  import { kitHome } from './home.js';
7
8
  const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
8
9
  function houseRule(houses) {
@@ -54,11 +55,14 @@ export async function allowHouseInCodex(path) {
54
55
  houses.push(house);
55
56
  }
56
57
  await mkdir(rules, { recursive: true });
57
- const temporary = `${file}.${process.pid}`;
58
+ const temporary = `${file}.${randomUUID()}`;
58
59
  await writeFile(temporary, houseRule(houses));
59
60
  await rename(temporary, file);
60
61
  }
61
62
  async function nothing() { }
63
+ function answered(error) {
64
+ return typeof error === 'object' && error !== null ? error : {};
65
+ }
62
66
  function bridgeInClaudeSandbox(bridge) {
63
67
  return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
64
68
  }
@@ -76,6 +80,10 @@ export const CLIS = {
76
80
  args: [],
77
81
  fullAccess: 'agent-full-access',
78
82
  sessionMeta: () => ({}),
83
+ unknownSession: (error) => {
84
+ const { code, data } = answered(error);
85
+ return code === -32603 && typeof data?.details === 'string' && data.details.startsWith('no rollout found');
86
+ },
79
87
  renames: true,
80
88
  allowHouse: allowHouseInCodex,
81
89
  signedIn: { command: ['login', 'status'] },
@@ -105,6 +113,7 @@ export const CLIS = {
105
113
  emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
106
114
  },
107
115
  }),
116
+ unknownSession: (error) => answered(error).code === -32002,
108
117
  renames: false,
109
118
  allowHouse: nothing,
110
119
  signedIn: { command: ['auth', 'status'] },
@@ -119,6 +128,7 @@ export const CLIS = {
119
128
  args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
120
129
  fullAccess: null,
121
130
  sessionMeta: () => ({}),
131
+ unknownSession: (error) => answered(error).data?.code === 'FS_NOT_FOUND',
122
132
  renames: false,
123
133
  allowHouse: nothing,
124
134
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
@@ -199,7 +209,7 @@ export async function installAdapter(kind, signal) {
199
209
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
200
210
  if ((await installedAdapter(kind)) === release)
201
211
  return null;
202
- const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
212
+ const ran = await run('npm', ['install', '--prefix', prefix(kind), '--cache', join(kitHome(), 'npm-cache'), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
203
213
  return ran.status === 0 ? null : failureOf(ran, 'npm');
204
214
  }
205
215
  export async function installCli(kind, signal) {
@@ -224,6 +234,12 @@ export async function loginPath() {
224
234
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
225
235
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
226
236
  }
237
+ export async function writeCodexRule() {
238
+ const shell = await loginPath().catch(() => '');
239
+ await allowHouseInCodex([dirname(process.execPath), process.env.PATH ?? '', shell].join(delimiter)).catch((error) => {
240
+ throw new Error(`Codex's rule for house could not be written: ${error.message}`);
241
+ });
242
+ }
227
243
  export async function versionOf(path, signal) {
228
244
  return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
229
245
  }
@@ -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 = [];
@@ -56,6 +115,7 @@ class Conversation {
56
115
  subagents = new Map();
57
116
  held = [];
58
117
  opening = null;
118
+ prompted = null;
59
119
  kills = 0;
60
120
  killed = new AbortController();
61
121
  interrupts = 0;
@@ -63,6 +123,11 @@ class Conversation {
63
123
  open = 0;
64
124
  queue = Promise.resolve();
65
125
  reports = Promise.resolve();
126
+ starting = null;
127
+ preparing = null;
128
+ told = null;
129
+ untold = false;
130
+ shown = null;
66
131
  constructor(id) {
67
132
  this.id = id;
68
133
  }
@@ -78,9 +143,7 @@ function asksSecret(request) {
78
143
  export class Conversations {
79
144
  kit;
80
145
  conversations = new Map();
81
- acks = new Map();
82
146
  questions = new Map();
83
- carrying = 0;
84
147
  reporting = 0;
85
148
  watched = false;
86
149
  constructor(kit) {
@@ -89,37 +152,26 @@ export class Conversations {
89
152
  setInterval(() => this.look(), 2_000);
90
153
  }
91
154
  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();
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 }));
119
171
  });
120
172
  }
121
173
  idle() {
122
- if (this.carrying > 0 || this.reporting > 0)
174
+ if (this.reporting > 0)
123
175
  return false;
124
176
  for (const conversation of this.conversations.values()) {
125
177
  const turn = conversation.turn;
@@ -148,6 +200,10 @@ export class Conversations {
148
200
  }
149
201
  opened() {
150
202
  this.watched = false;
203
+ for (const conversation of this.conversations.values()) {
204
+ if (conversation.untold || conversation.told !== null)
205
+ this.retell(conversation);
206
+ }
151
207
  }
152
208
  watching(watched) {
153
209
  const resend = watched && !this.watched;
@@ -198,11 +254,15 @@ export class Conversations {
198
254
  async open(conversation, input) {
199
255
  if (conversation.running !== null)
200
256
  return {};
257
+ this.prepares(conversation, 'session');
201
258
  try {
202
- 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) };
203
260
  }
204
261
  catch (error) {
205
- return { refused: causeOf(error) };
262
+ return { refused: refusal(conversation, error) };
263
+ }
264
+ finally {
265
+ this.prepares(conversation, null);
206
266
  }
207
267
  }
208
268
  async message(conversation, input) {
@@ -211,6 +271,8 @@ export class Conversations {
211
271
  try {
212
272
  route = await this.route(conversation, input.agent_id);
213
273
  const files = input.files;
274
+ if (files.length > 0)
275
+ this.prepares(conversation, 'files');
214
276
  const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
215
277
  const lines = await this.transcribed(conversation, files, paths);
216
278
  if (lines.length > 0)
@@ -219,8 +281,10 @@ export class Conversations {
219
281
  throw new Error(KILLED);
220
282
  }
221
283
  catch (error) {
222
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
284
+ this.prepares(conversation, null);
285
+ return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
223
286
  }
287
+ conversation.preparing = null;
224
288
  return {
225
289
  ack: new Promise((settle) => {
226
290
  conversation.waiting.push({ input, route, content, settle });
@@ -232,8 +296,11 @@ export class Conversations {
232
296
  if (conversation.sending !== null || conversation.open > 0)
233
297
  return;
234
298
  const waiting = conversation.waiting.shift();
235
- if (waiting === undefined)
299
+ if (waiting === undefined) {
300
+ if (conversation.starting !== conversation.preparing)
301
+ this.starts(conversation, conversation.preparing);
236
302
  return;
303
+ }
237
304
  conversation.sending = this.send(conversation, waiting).finally(() => {
238
305
  conversation.sending = null;
239
306
  this.next(conversation);
@@ -242,13 +309,18 @@ export class Conversations {
242
309
  async send(conversation, waiting) {
243
310
  const { input } = waiting;
244
311
  let ack = {};
312
+ let started = false;
245
313
  const prompt = [];
246
314
  try {
247
315
  if (conversation.running === null) {
248
- const resumed = input.provider_session_id ?? conversation.session;
249
- const opened = await this.start(conversation, waiting.route, resumed);
250
- if (resumed === null)
251
- 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 };
252
324
  }
253
325
  if (input.first === true) {
254
326
  prompt.push({
@@ -260,22 +332,53 @@ export class Conversations {
260
332
  throw new Error(KILLED);
261
333
  }
262
334
  catch (error) {
263
- if ('provider_session_id' in ack) {
335
+ this.starts(conversation, null);
336
+ if (started) {
264
337
  conversation.session = null;
265
338
  await this.kill(conversation);
266
339
  }
267
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
340
+ waiting.settle({ refused: refusal(conversation, error) });
268
341
  return;
269
342
  }
270
- this.prompt(conversation, [...prompt, ...waiting.content]);
343
+ this.starts(conversation, null);
344
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.inputs.hands(String(input.input_id), ack));
271
345
  waiting.settle(ack);
272
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;
355
+ }
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 });
374
+ }
273
375
  async transcribed(conversation, files, paths) {
274
376
  const lines = [];
275
377
  for (const [index, path] of paths.entries()) {
276
378
  lines.push(path);
277
379
  if (files[index].media_type?.startsWith('audio/') !== true)
278
380
  continue;
381
+ this.prepares(conversation, 'transcription');
279
382
  try {
280
383
  const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
281
384
  if (text !== null && text !== '')
@@ -342,14 +445,15 @@ export class Conversations {
342
445
  }
343
446
  return route;
344
447
  }
345
- async start(conversation, route, sessionId) {
448
+ async start(conversation, route, sessionId, empty) {
346
449
  const reader = readerFor(route.kind);
347
450
  reader.session = sessionId;
348
451
  const app = client({ name: '@agentshouse/kit' })
349
452
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
350
- .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));
351
455
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
352
- 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));
353
457
  if (conversation.kills > 0) {
354
458
  bridge.close();
355
459
  throw new Error(KILLED);
@@ -357,27 +461,28 @@ export class Conversations {
357
461
  conversation.opening = bridge;
358
462
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
359
463
  const scope = new Scope(marker(bridge), randomUUID(), cli);
464
+ let adapter = null;
360
465
  try {
361
466
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
362
467
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
363
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
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) => {
364
470
  work.read(message);
365
471
  this.read(conversation, reader, message);
366
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
472
+ }, { ...bridge.env, ...streamed }, undefined, scope));
367
473
  void adapter.exited.then(() => bridge.close());
368
474
  const agent = adapter.connection.agent;
369
475
  const cwd = route.working_directory;
370
476
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
371
- const opened = sessionId === null
372
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
373
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
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 })));
374
479
  reader.session = opened.sessionId;
375
- if (sessionId === null && CLIS[route.kind].renames) {
480
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
376
481
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
377
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
482
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
378
483
  }
379
484
  const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
380
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
485
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
381
486
  void work.settled().then(() => scope.record());
382
487
  conversation.running = running;
383
488
  conversation.session = running.sessionId;
@@ -385,11 +490,11 @@ export class Conversations {
385
490
  if (conversation.commands !== null)
386
491
  this.commands(conversation, conversation.commands);
387
492
  this.options(conversation, options);
388
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
493
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
389
494
  return running.sessionId;
390
495
  }
391
496
  catch (error) {
392
- scope.kill();
497
+ adapter?.retire();
393
498
  work.close();
394
499
  conversation.leftovers.add(scope);
395
500
  bridge.close();
@@ -399,6 +504,18 @@ export class Conversations {
399
504
  conversation.opening = null;
400
505
  }
401
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
+ }
402
519
  async launchSettings(running, route, offered) {
403
520
  let options = offered;
404
521
  const settings = [
@@ -420,13 +537,10 @@ export class Conversations {
420
537
  }
421
538
  return options;
422
539
  }
423
- exited(conversation, adapter, cause) {
424
- const running = conversation.running;
425
- if (running?.adapter !== adapter)
426
- return;
540
+ exited(conversation, running, cause) {
427
541
  this.detach(conversation, running);
428
542
  if (conversation.turn !== null) {
429
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
543
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
430
544
  }
431
545
  this.next(conversation);
432
546
  this.kit.changed();
@@ -465,7 +579,7 @@ export class Conversations {
465
579
  return delivered;
466
580
  }
467
581
  write(conversation, turn, operation) {
468
- if (!turn.admitted || turn.ended || !this.watched)
582
+ if (turn.ended || !this.watched)
469
583
  return;
470
584
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
471
585
  }
@@ -476,12 +590,7 @@ export class Conversations {
476
590
  begin(conversation) {
477
591
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
478
592
  conversation.open++;
479
- void this.deliver(conversation, turn, 'started', {})
480
- .then(() => {
481
- turn.admitted = true;
482
- this.replay(conversation, turn);
483
- })
484
- .catch(logged);
593
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
485
594
  conversation.turn = turn;
486
595
  if (conversation.context !== null)
487
596
  turn.parts.measure(conversation.context);
@@ -490,13 +599,17 @@ export class Conversations {
490
599
  this.kit.changed();
491
600
  return turn;
492
601
  }
493
- end(conversation, turn, failed) {
602
+ end(conversation, turn, failed, notice = null) {
494
603
  if (turn.ended)
495
604
  return;
605
+ if (failed !== null)
606
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
496
607
  if (conversation.turn === turn) {
497
608
  for (const event of conversation.running?.reader.closeTurn() ?? [])
498
609
  this.apply(conversation, turn, event);
499
610
  }
611
+ if (notice !== null)
612
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
500
613
  turn.ended = true;
501
614
  conversation.open--;
502
615
  if (conversation.turn === turn)
@@ -504,31 +617,45 @@ export class Conversations {
504
617
  const body = Promise.allSettled(turn.uploads).then(() => ({
505
618
  parts: turn.parts.saved(),
506
619
  context: turn.parts.context,
507
- ...failed,
620
+ ...(failed === null ? {} : { failed: failed.failure }),
508
621
  }));
509
622
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
510
623
  this.next(conversation);
511
624
  this.kit.changed();
512
625
  }
513
- prompt(conversation, prompt) {
626
+ prompt(conversation, prompt, taken) {
514
627
  const running = conversation.running;
515
628
  const turn = this.begin(conversation);
516
629
  turn.prompted = true;
517
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
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) => {
518
642
  if (running.adapter.connection.signal.aborted) {
519
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
643
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
520
644
  }
521
645
  else {
646
+ this.took(turn);
522
647
  if (conversation.open === 1 && conversation.running === running) {
523
- running.scope.kill();
648
+ running.adapter.retire();
524
649
  this.detach(conversation, running);
525
650
  }
526
- this.end(conversation, turn, { failed: causeOf(error) });
651
+ this.end(conversation, turn, answeredWith(error));
527
652
  }
528
653
  });
529
654
  }
530
655
  started(conversation) {
531
- return conversation.turn ?? this.begin(conversation);
656
+ const turn = conversation.turn ?? this.begin(conversation);
657
+ this.took(turn);
658
+ return turn;
532
659
  }
533
660
  ended(conversation) {
534
661
  const turn = conversation.turn;
@@ -623,6 +750,11 @@ export class Conversations {
623
750
  }
624
751
  this.apply(conversation, this.started(conversation), event);
625
752
  }
753
+ took(turn) {
754
+ const taken = turn.taken;
755
+ turn.taken = null;
756
+ taken?.();
757
+ }
626
758
  apply(conversation, turn, event) {
627
759
  switch (event.kind) {
628
760
  case 'text':