@agentshouse/kit 0.2.1-cca496635583e → 0.2.1-ccf188e02985d

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 +23 -22
  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 +51 -9
  10. package/dist/conversations.js +137 -70
  11. package/dist/copies.js +78 -34
  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 -189
  19. package/dist/inputs.js +37 -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 +47 -13
  24. package/dist/logout.js +0 -2
  25. package/dist/parts.js +34 -16
  26. package/dist/placement.js +30 -0
  27. package/dist/relay.js +1 -1
  28. package/dist/resident.js +65 -16
  29. package/dist/service.js +22 -0
  30. package/dist/sign-in.js +5 -23
  31. package/dist/skills.js +81 -12
  32. package/dist/stream.js +66 -17
  33. package/dist/transcription.js +3 -3
  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 +2 -1
  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,9 +1,14 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { constants, readFileSync } from 'node:fs';
3
- import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
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';
8
+ const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
9
+ function houseRule(houses) {
10
+ return `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`;
11
+ }
7
12
  const RAW_SUBTYPES = [
8
13
  'background_tasks_changed',
9
14
  'task_started',
@@ -22,17 +27,42 @@ async function executable(path) {
22
27
  return false;
23
28
  }
24
29
  }
25
- async function allowHouseInCodex(path) {
30
+ async function listedHouses(file) {
31
+ let text;
32
+ try {
33
+ text = await readFile(file, 'utf8');
34
+ }
35
+ catch (error) {
36
+ if (error.code === 'ENOENT')
37
+ return [];
38
+ throw error;
39
+ }
40
+ try {
41
+ const listed = JSON.parse(HOUSE_EXECUTABLES.exec(text)?.[1] ?? '[]');
42
+ return Array.isArray(listed) ? listed.filter((house) => typeof house === 'string') : [];
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ }
48
+ export async function allowHouseInCodex(path) {
26
49
  const candidates = [...new Set(path.split(delimiter).filter((directory) => isAbsolute(directory)))].map((directory) => join(directory, 'house'));
27
- const houses = [];
28
- for (const candidate of candidates)
29
- if (await executable(candidate))
30
- houses.push(candidate);
31
50
  const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
51
+ const file = join(rules, 'house.rules');
52
+ const houses = [];
53
+ for (const house of [...(await listedHouses(file)), ...candidates]) {
54
+ if (!houses.includes(house) && (await executable(house)))
55
+ houses.push(house);
56
+ }
32
57
  await mkdir(rules, { recursive: true });
33
- await writeFile(join(rules, 'house.rules'), `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`);
58
+ const temporary = `${file}.${randomUUID()}`;
59
+ await writeFile(temporary, houseRule(houses));
60
+ await rename(temporary, file);
34
61
  }
35
62
  async function nothing() { }
63
+ function answered(error) {
64
+ return typeof error === 'object' && error !== null ? error : {};
65
+ }
36
66
  function bridgeInClaudeSandbox(bridge) {
37
67
  return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
38
68
  }
@@ -50,6 +80,10 @@ export const CLIS = {
50
80
  args: [],
51
81
  fullAccess: 'agent-full-access',
52
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
+ },
53
87
  renames: true,
54
88
  allowHouse: allowHouseInCodex,
55
89
  signedIn: { command: ['login', 'status'] },
@@ -79,6 +113,7 @@ export const CLIS = {
79
113
  emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
80
114
  },
81
115
  }),
116
+ unknownSession: (error) => answered(error).code === -32002,
82
117
  renames: false,
83
118
  allowHouse: nothing,
84
119
  signedIn: { command: ['auth', 'status'] },
@@ -93,6 +128,7 @@ export const CLIS = {
93
128
  args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
94
129
  fullAccess: null,
95
130
  sessionMeta: () => ({}),
131
+ unknownSession: (error) => answered(error).data?.code === 'FS_NOT_FOUND',
96
132
  renames: false,
97
133
  allowHouse: nothing,
98
134
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
@@ -173,7 +209,7 @@ export async function installAdapter(kind, signal) {
173
209
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
174
210
  if ((await installedAdapter(kind)) === release)
175
211
  return null;
176
- 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);
177
213
  return ran.status === 0 ? null : failureOf(ran, 'npm');
178
214
  }
179
215
  export async function installCli(kind, signal) {
@@ -198,6 +234,12 @@ export async function loginPath() {
198
234
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
199
235
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
200
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
+ }
201
243
  export async function versionOf(path, signal) {
202
244
  return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
203
245
  }
@@ -1,31 +1,82 @@
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 NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
35
+ class NotStarted extends Error {
36
+ }
37
+ async function step(reason, work) {
38
+ try {
39
+ return await work;
40
+ }
41
+ catch (error) {
42
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
43
+ }
44
+ }
45
+ function unbridged(error) {
46
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
47
+ }
48
+ const EXIT_PLAN_MODE = {
49
+ parse(value) {
50
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
51
+ throw new Error('_x.ai/exit_plan_mode needs an object');
52
+ }
53
+ return value;
54
+ },
55
+ };
56
+ export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
20
57
  function causeOf(error) {
21
58
  return error instanceof Error ? error.message : String(error);
22
59
  }
60
+ function answeredWith(error) {
61
+ const signedOut = typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
62
+ return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
63
+ }
64
+ function exitedWith(running, cause) {
65
+ return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
66
+ }
23
67
  function answerMessage(request, response) {
24
68
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
25
69
  }
26
70
  function logged(error) {
27
71
  process.stderr.write(`kit: ${causeOf(error)}\n`);
28
72
  }
73
+ function refusal(conversation, error) {
74
+ if (conversation.kills > 0)
75
+ return KILLED;
76
+ const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
77
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
78
+ return causeOf(error);
79
+ }
29
80
  function marker(bridge) {
30
81
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
82
  }
@@ -56,6 +107,7 @@ class Conversation {
56
107
  subagents = new Map();
57
108
  held = [];
58
109
  opening = null;
110
+ prompted = null;
59
111
  kills = 0;
60
112
  killed = new AbortController();
61
113
  interrupts = 0;
@@ -78,9 +130,7 @@ function asksSecret(request) {
78
130
  export class Conversations {
79
131
  kit;
80
132
  conversations = new Map();
81
- acks = new Map();
82
133
  questions = new Map();
83
- carrying = 0;
84
134
  reporting = 0;
85
135
  watched = false;
86
136
  constructor(kit) {
@@ -89,37 +139,26 @@ export class Conversations {
89
139
  setInterval(() => this.look(), 2_000);
90
140
  }
91
141
  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();
142
+ void this.kit.inputs.carry(input.input_id, () => {
143
+ const conversation = this.conversation(input.conversation_id);
144
+ if (input.kind === 'kill') {
145
+ conversation.kills++;
146
+ conversation.killed.abort(new Error(KILLED));
147
+ for (const waiting of conversation.waiting.splice(0))
148
+ waiting.settle({ refused: KILLED });
149
+ this.halt(conversation);
150
+ }
151
+ if (input.kind === 'interrupt') {
152
+ conversation.interrupts++;
153
+ conversation.interrupted.abort();
154
+ }
155
+ const handled = conversation.queue.then(() => this.carry(conversation, input));
156
+ conversation.queue = handled.catch(() => undefined);
157
+ return handled.then(async ({ ack }) => ({ ack: await ack }));
119
158
  });
120
159
  }
121
160
  idle() {
122
- if (this.carrying > 0 || this.reporting > 0)
161
+ if (this.reporting > 0)
123
162
  return false;
124
163
  for (const conversation of this.conversations.values()) {
125
164
  const turn = conversation.turn;
@@ -199,10 +238,10 @@ export class Conversations {
199
238
  if (conversation.running !== null)
200
239
  return {};
201
240
  try {
202
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
241
+ return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
203
242
  }
204
243
  catch (error) {
205
- return { refused: causeOf(error) };
244
+ return { refused: refusal(conversation, error) };
206
245
  }
207
246
  }
208
247
  async message(conversation, input) {
@@ -219,7 +258,7 @@ export class Conversations {
219
258
  throw new Error(KILLED);
220
259
  }
221
260
  catch (error) {
222
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
261
+ return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
223
262
  }
224
263
  return {
225
264
  ack: new Promise((settle) => {
@@ -242,13 +281,17 @@ export class Conversations {
242
281
  async send(conversation, waiting) {
243
282
  const { input } = waiting;
244
283
  let ack = {};
284
+ let started = false;
245
285
  const prompt = [];
246
286
  try {
247
287
  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 };
288
+ const stored = conversation.session ?? input.provider_session_id;
289
+ const empty = input.first === true && conversation.prompted !== stored;
290
+ started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
291
+ }
292
+ const session = conversation.running.sessionId;
293
+ if (started || (input.first === true && session !== input.provider_session_id)) {
294
+ ack = { provider_session_id: session };
252
295
  }
253
296
  if (input.first === true) {
254
297
  prompt.push({
@@ -260,11 +303,11 @@ export class Conversations {
260
303
  throw new Error(KILLED);
261
304
  }
262
305
  catch (error) {
263
- if ('provider_session_id' in ack) {
306
+ if (started) {
264
307
  conversation.session = null;
265
308
  await this.kill(conversation);
266
309
  }
267
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
310
+ waiting.settle({ refused: refusal(conversation, error) });
268
311
  return;
269
312
  }
270
313
  this.prompt(conversation, [...prompt, ...waiting.content]);
@@ -342,14 +385,15 @@ export class Conversations {
342
385
  }
343
386
  return route;
344
387
  }
345
- async start(conversation, route, sessionId) {
388
+ async start(conversation, route, sessionId, empty) {
346
389
  const reader = readerFor(route.kind);
347
390
  reader.session = sessionId;
348
391
  const app = client({ name: '@agentshouse/kit' })
349
392
  .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));
393
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
394
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
351
395
  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);
396
+ const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
353
397
  if (conversation.kills > 0) {
354
398
  bridge.close();
355
399
  throw new Error(KILLED);
@@ -357,27 +401,28 @@ export class Conversations {
357
401
  conversation.opening = bridge;
358
402
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
359
403
  const scope = new Scope(marker(bridge), randomUUID(), cli);
404
+ let adapter = null;
360
405
  try {
361
406
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
362
407
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
363
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
408
+ const streamed = await step(NO_CONNECTION, work.open(cli));
409
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
364
410
  work.read(message);
365
411
  this.read(conversation, reader, message);
366
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
412
+ }, { ...bridge.env, ...streamed }, undefined, scope));
367
413
  void adapter.exited.then(() => bridge.close());
368
414
  const agent = adapter.connection.agent;
369
415
  const cwd = route.working_directory;
370
416
  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 };
417
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
418
+ const opened = resumed ?? (await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta })));
374
419
  reader.session = opened.sessionId;
375
- if (sessionId === null && CLIS[route.kind].renames) {
420
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
376
421
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
377
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
422
+ await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
378
423
  }
379
424
  const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
380
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
425
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
381
426
  void work.settled().then(() => scope.record());
382
427
  conversation.running = running;
383
428
  conversation.session = running.sessionId;
@@ -385,11 +430,11 @@ export class Conversations {
385
430
  if (conversation.commands !== null)
386
431
  this.commands(conversation, conversation.commands);
387
432
  this.options(conversation, options);
388
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
433
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
389
434
  return running.sessionId;
390
435
  }
391
436
  catch (error) {
392
- scope.kill();
437
+ adapter?.retire();
393
438
  work.close();
394
439
  conversation.leftovers.add(scope);
395
440
  bridge.close();
@@ -399,6 +444,18 @@ export class Conversations {
399
444
  conversation.opening = null;
400
445
  }
401
446
  }
447
+ async resume(conversation, kind, adapter, request, empty) {
448
+ try {
449
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
450
+ }
451
+ catch (error) {
452
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
453
+ throw new NotStarted(NO_RESUME, { cause: error });
454
+ }
455
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
456
+ return null;
457
+ }
458
+ }
402
459
  async launchSettings(running, route, offered) {
403
460
  let options = offered;
404
461
  const settings = [
@@ -420,13 +477,10 @@ export class Conversations {
420
477
  }
421
478
  return options;
422
479
  }
423
- exited(conversation, adapter, cause) {
424
- const running = conversation.running;
425
- if (running?.adapter !== adapter)
426
- return;
480
+ exited(conversation, running, cause) {
427
481
  this.detach(conversation, running);
428
482
  if (conversation.turn !== null) {
429
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
483
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
430
484
  }
431
485
  this.next(conversation);
432
486
  this.kit.changed();
@@ -490,21 +544,25 @@ export class Conversations {
490
544
  this.kit.changed();
491
545
  return turn;
492
546
  }
493
- end(conversation, turn, failed) {
547
+ end(conversation, turn, failed, notice = null) {
494
548
  if (turn.ended)
495
549
  return;
550
+ if (failed !== null)
551
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
496
552
  if (conversation.turn === turn) {
497
553
  for (const event of conversation.running?.reader.closeTurn() ?? [])
498
554
  this.apply(conversation, turn, event);
499
555
  }
556
+ if (notice !== null)
557
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
500
558
  turn.ended = true;
501
559
  conversation.open--;
502
560
  if (conversation.turn === turn)
503
561
  conversation.turn = null;
504
562
  const body = Promise.allSettled(turn.uploads).then(() => ({
505
- parts: turn.parts.parts,
563
+ parts: turn.parts.saved(),
506
564
  context: turn.parts.context,
507
- ...failed,
565
+ ...(failed === null ? {} : { failed: failed.failure }),
508
566
  }));
509
567
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
510
568
  this.next(conversation);
@@ -514,16 +572,25 @@ export class Conversations {
514
572
  const running = conversation.running;
515
573
  const turn = this.begin(conversation);
516
574
  turn.prompted = true;
517
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
575
+ conversation.prompted = running.sessionId;
576
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
577
+ const notice = answeredFailure(answer);
578
+ if (notice !== null) {
579
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
580
+ }
581
+ else {
582
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
583
+ }
584
+ }, async (error) => {
518
585
  if (running.adapter.connection.signal.aborted) {
519
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
586
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
520
587
  }
521
588
  else {
522
589
  if (conversation.open === 1 && conversation.running === running) {
523
- running.scope.kill();
590
+ running.adapter.retire();
524
591
  this.detach(conversation, running);
525
592
  }
526
- this.end(conversation, turn, { failed: causeOf(error) });
593
+ this.end(conversation, turn, answeredWith(error));
527
594
  }
528
595
  });
529
596
  }
@@ -647,7 +714,7 @@ export class Conversations {
647
714
  turn.parts.plan(event.entries);
648
715
  return;
649
716
  case 'image':
650
- turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
717
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
651
718
  this.kit.changed();
652
719
  return;
653
720
  }
@@ -667,7 +734,7 @@ export class Conversations {
667
734
  else
668
735
  conversation.turn.parts.start(part);
669
736
  }
670
- async image(conversation, turn, data, mediaType) {
737
+ async image(conversation, turn, reserved, data, mediaType) {
671
738
  const bytes = Buffer.from(data, 'base64');
672
739
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
673
740
  try {
@@ -680,10 +747,10 @@ export class Conversations {
680
747
  const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
681
748
  if (saved.save.status !== 'saved')
682
749
  throw new Error(saved.save.failure ?? saved.save.status);
683
- turn.parts.start({ type: 'image', file: saved.version, name, media_type: mediaType });
750
+ turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
684
751
  }
685
752
  catch (error) {
686
- turn.parts.start({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` });
753
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
687
754
  }
688
755
  }
689
756
  }