@agentshouse/kit 0.2.1-cca9e1f43de44 → 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 (98) hide show
  1. package/README.md +30 -28
  2. package/dist/acp.js +41 -111
  3. package/dist/agents.js +110 -22
  4. package/dist/api.js +9 -5
  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 +95 -14
  10. package/dist/codex-main.js +58 -0
  11. package/dist/conversations.js +181 -114
  12. package/dist/copies.js +188 -119
  13. package/dist/end.js +19 -4
  14. package/dist/enrol-main.js +7 -1
  15. package/dist/events.js +14 -76
  16. package/dist/files.js +8 -2
  17. package/dist/git.js +10 -8
  18. package/dist/home.js +96 -7
  19. package/dist/house-cli.js +291 -0
  20. package/dist/house-main.js +7 -177
  21. package/dist/inputs.js +37 -0
  22. package/dist/instructions.js +10 -32
  23. package/dist/kit-cli.js +68 -0
  24. package/dist/kit-main.js +8 -22
  25. package/dist/login.js +117 -106
  26. package/dist/logout.js +14 -16
  27. package/dist/parts.js +34 -16
  28. package/dist/placement.js +30 -0
  29. package/dist/relay.js +12 -2
  30. package/dist/resident.js +89 -33
  31. package/dist/scope.js +203 -0
  32. package/dist/service-main.js +9 -0
  33. package/dist/service.js +55 -0
  34. package/dist/sign-in.js +6 -24
  35. package/dist/skills.js +81 -12
  36. package/dist/stream.js +68 -17
  37. package/dist/transcription.js +3 -3
  38. package/dist/update.js +48 -29
  39. package/dist/wire.js +6 -0
  40. package/dist/work.js +346 -0
  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,37 +1,85 @@
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
- import { killMarked, processes, startAdapter } from './acp.js';
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';
16
+ import { killMarked, Scope } from './scope.js';
14
17
  import { holdSecretInput } from './secret-input.js';
15
18
  import { createHowWeWork } from './skills.js';
16
19
  import { transcribe } from './transcription.js';
20
+ import { workFor } from './work.js';
21
+ const SIGNED_OUT = RequestError.authRequired().code;
17
22
  const KILLED = 'the conversation was killed';
18
- // A process a conversation left running reports nothing when it exits, so the Kit looks again every five seconds while one runs.
19
- const PROCESS_RECHECK_MS = 5000;
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
  }
32
- function stop(bridge) {
33
- killMarked(marker(bridge));
34
- }
35
83
  class Turn {
36
84
  id = randomUUID();
37
85
  parts;
@@ -53,12 +101,13 @@ class Conversation {
53
101
  turn = null;
54
102
  commands = null;
55
103
  context = null;
56
- busy = new Set();
104
+ leftovers = new Set();
57
105
  waiting = [];
58
106
  sending = null;
59
107
  subagents = new Map();
60
108
  held = [];
61
109
  opening = null;
110
+ prompted = null;
62
111
  kills = 0;
63
112
  killed = new AbortController();
64
113
  interrupts = 0;
@@ -81,67 +130,60 @@ function asksSecret(request) {
81
130
  export class Conversations {
82
131
  kit;
83
132
  conversations = new Map();
84
- acks = new Map();
85
133
  questions = new Map();
86
- carrying = 0;
87
134
  reporting = 0;
88
135
  watched = false;
89
- recheck = null;
90
136
  constructor(kit) {
91
137
  this.kit = kit;
138
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
139
+ setInterval(() => this.look(), 2_000);
92
140
  }
93
141
  input(input) {
94
- this.carrying++;
95
- const carried = this.acks.get(input.input_id) ??
96
- (() => {
97
- const conversation = this.conversation(input.conversation_id);
98
- if (input.kind === 'kill') {
99
- conversation.kills++;
100
- conversation.killed.abort(new Error(KILLED));
101
- for (const waiting of conversation.waiting.splice(0))
102
- waiting.settle({ refused: KILLED });
103
- this.halt(conversation);
104
- }
105
- if (input.kind === 'interrupt') {
106
- conversation.interrupts++;
107
- conversation.interrupted.abort();
108
- }
109
- const handled = conversation.queue.then(() => this.carry(conversation, input));
110
- conversation.queue = handled.catch(() => undefined);
111
- const work = handled.then(({ ack }) => ack);
112
- this.acks.set(input.input_id, work);
113
- return work;
114
- })();
115
- carried
116
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
117
- .catch(logged)
118
- .finally(() => {
119
- this.carrying--;
120
- 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 }));
121
158
  });
122
159
  }
123
160
  idle() {
124
- if (this.carrying > 0 || this.reporting > 0)
161
+ if (this.reporting > 0)
125
162
  return false;
126
163
  for (const conversation of this.conversations.values()) {
127
164
  const turn = conversation.turn;
128
165
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
129
- if (conversation.busy.size > 0 || conversation.open > (asking ? 1 : 0))
166
+ if (conversation.open > (asking ? 1 : 0))
130
167
  return false;
131
168
  }
132
- for (const conversation of this.conversations.values()) {
133
- const running = conversation.running;
134
- if (running === null || running.baseline === null)
135
- continue;
136
- if ([...processes(marker(running.bridge))].every((found) => running.baseline.has(found)))
137
- continue;
138
- this.recheck ??= setTimeout(() => {
139
- this.recheck = null;
140
- this.kit.changed();
141
- }, PROCESS_RECHECK_MS);
169
+ const conversations = [...this.conversations.values()];
170
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
142
171
  return false;
172
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
173
+ }
174
+ quiet() {
175
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
176
+ }
177
+ look() {
178
+ for (const conversation of this.conversations.values()) {
179
+ for (const scope of conversation.leftovers) {
180
+ if (!scope.empty())
181
+ continue;
182
+ scope.close();
183
+ conversation.leftovers.delete(scope);
184
+ }
143
185
  }
144
- return true;
186
+ this.kit.changed();
145
187
  }
146
188
  opened() {
147
189
  this.watched = false;
@@ -196,10 +238,10 @@ export class Conversations {
196
238
  if (conversation.running !== null)
197
239
  return {};
198
240
  try {
199
- 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) };
200
242
  }
201
243
  catch (error) {
202
- return { refused: causeOf(error) };
244
+ return { refused: refusal(conversation, error) };
203
245
  }
204
246
  }
205
247
  async message(conversation, input) {
@@ -216,7 +258,7 @@ export class Conversations {
216
258
  throw new Error(KILLED);
217
259
  }
218
260
  catch (error) {
219
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
261
+ return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
220
262
  }
221
263
  return {
222
264
  ack: new Promise((settle) => {
@@ -239,13 +281,17 @@ export class Conversations {
239
281
  async send(conversation, waiting) {
240
282
  const { input } = waiting;
241
283
  let ack = {};
284
+ let started = false;
242
285
  const prompt = [];
243
286
  try {
244
287
  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 };
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 };
249
295
  }
250
296
  if (input.first === true) {
251
297
  prompt.push({
@@ -257,11 +303,11 @@ export class Conversations {
257
303
  throw new Error(KILLED);
258
304
  }
259
305
  catch (error) {
260
- if ('provider_session_id' in ack) {
306
+ if (started) {
261
307
  conversation.session = null;
262
308
  await this.kill(conversation);
263
309
  }
264
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
310
+ waiting.settle({ refused: refusal(conversation, error) });
265
311
  return;
266
312
  }
267
313
  this.prompt(conversation, [...prompt, ...waiting.content]);
@@ -295,11 +341,13 @@ export class Conversations {
295
341
  }
296
342
  halt(conversation) {
297
343
  if (conversation.opening !== null)
298
- stop(conversation.opening);
344
+ killMarked(marker(conversation.opening));
345
+ for (const scope of conversation.leftovers)
346
+ scope.kill();
299
347
  if (conversation.running === null)
300
348
  return;
301
349
  conversation.running.killed = true;
302
- stop(conversation.running.bridge);
350
+ conversation.running.scope.kill();
303
351
  }
304
352
  async kill(conversation) {
305
353
  const running = conversation.running;
@@ -337,46 +385,58 @@ export class Conversations {
337
385
  }
338
386
  return route;
339
387
  }
340
- async start(conversation, route, sessionId) {
388
+ async start(conversation, route, sessionId, empty) {
341
389
  const reader = readerFor(route.kind);
342
390
  reader.session = sessionId;
343
391
  const app = client({ name: '@agentshouse/kit' })
344
392
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
345
- .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));
346
395
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
347
- 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));
348
397
  if (conversation.kills > 0) {
349
398
  bridge.close();
350
399
  throw new Error(KILLED);
351
400
  }
352
401
  conversation.opening = bridge;
402
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
403
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
404
+ let adapter = null;
353
405
  try {
354
406
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
355
- await CLIS[route.kind].allowHouse();
356
- conversation.busy.clear();
357
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => this.read(conversation, reader, message), bridge.env);
407
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
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) => {
410
+ work.read(message);
411
+ this.read(conversation, reader, message);
412
+ }, { ...bridge.env, ...streamed }, undefined, scope));
358
413
  void adapter.exited.then(() => bridge.close());
359
414
  const agent = adapter.connection.agent;
360
415
  const cwd = route.working_directory;
361
- const _meta = CLIS[route.kind].sessionMeta;
362
- const opened = sessionId === null
363
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
364
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
416
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
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 })));
365
419
  reader.session = opened.sessionId;
366
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, baseline: null };
367
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
368
- running.baseline = processes(marker(bridge));
420
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
421
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
422
+ await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
423
+ }
424
+ const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
425
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
426
+ void work.settled().then(() => scope.record());
369
427
  conversation.running = running;
370
428
  conversation.session = running.sessionId;
371
429
  this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
372
430
  if (conversation.commands !== null)
373
431
  this.commands(conversation, conversation.commands);
374
432
  this.options(conversation, options);
375
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
433
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
376
434
  return running.sessionId;
377
435
  }
378
436
  catch (error) {
379
- stop(bridge);
437
+ adapter?.retire();
438
+ work.close();
439
+ conversation.leftovers.add(scope);
380
440
  bridge.close();
381
441
  throw conversation.kills > 0 ? new Error(KILLED) : error;
382
442
  }
@@ -384,6 +444,18 @@ export class Conversations {
384
444
  conversation.opening = null;
385
445
  }
386
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
+ }
387
459
  async launchSettings(running, route, offered) {
388
460
  let options = offered;
389
461
  const settings = [
@@ -405,21 +477,19 @@ export class Conversations {
405
477
  }
406
478
  return options;
407
479
  }
408
- exited(conversation, adapter, cause) {
409
- const running = conversation.running;
410
- if (running?.adapter !== adapter)
411
- return;
412
- this.detach(conversation);
480
+ exited(conversation, running, cause) {
481
+ this.detach(conversation, running);
413
482
  if (conversation.turn !== null) {
414
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
483
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
415
484
  }
416
485
  this.next(conversation);
417
486
  this.kit.changed();
418
487
  }
419
- detach(conversation) {
488
+ detach(conversation, running) {
489
+ running.work.close();
490
+ conversation.leftovers.add(running.scope);
420
491
  conversation.running = null;
421
492
  conversation.commands = null;
422
- conversation.busy.clear();
423
493
  for (const [id, question] of this.questions) {
424
494
  if (question.conversation === conversation)
425
495
  this.questions.delete(id);
@@ -471,26 +541,28 @@ export class Conversations {
471
541
  turn.parts.measure(conversation.context);
472
542
  for (const part of conversation.held.splice(0))
473
543
  turn.parts.start(part);
474
- for (const event of conversation.running?.reader.turnStarted() ?? [])
475
- this.event(conversation, event);
476
544
  this.kit.changed();
477
545
  return turn;
478
546
  }
479
- end(conversation, turn, failed) {
547
+ end(conversation, turn, failed, notice = null) {
480
548
  if (turn.ended)
481
549
  return;
550
+ if (failed !== null)
551
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
482
552
  if (conversation.turn === turn) {
483
553
  for (const event of conversation.running?.reader.closeTurn() ?? [])
484
554
  this.apply(conversation, turn, event);
485
555
  }
556
+ if (notice !== null)
557
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
486
558
  turn.ended = true;
487
559
  conversation.open--;
488
560
  if (conversation.turn === turn)
489
561
  conversation.turn = null;
490
562
  const body = Promise.allSettled(turn.uploads).then(() => ({
491
- parts: turn.parts.parts,
563
+ parts: turn.parts.saved(),
492
564
  context: turn.parts.context,
493
- ...failed,
565
+ ...(failed === null ? {} : { failed: failed.failure }),
494
566
  }));
495
567
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
496
568
  this.next(conversation);
@@ -500,16 +572,25 @@ export class Conversations {
500
572
  const running = conversation.running;
501
573
  const turn = this.begin(conversation);
502
574
  turn.prompted = true;
503
- 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) => {
504
585
  if (running.adapter.connection.signal.aborted) {
505
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
586
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
506
587
  }
507
588
  else {
508
589
  if (conversation.open === 1 && conversation.running === running) {
509
- stop(running.bridge);
510
- this.detach(conversation);
590
+ running.adapter.retire();
591
+ this.detach(conversation, running);
511
592
  }
512
- this.end(conversation, turn, { failed: causeOf(error) });
593
+ this.end(conversation, turn, answeredWith(error));
513
594
  }
514
595
  });
515
596
  }
@@ -592,20 +673,6 @@ export class Conversations {
592
673
  case 'options':
593
674
  this.options(conversation, event.options);
594
675
  return;
595
- case 'busy':
596
- if (event.running)
597
- conversation.busy.add(event.key);
598
- else
599
- conversation.busy.delete(event.key);
600
- this.kit.changed();
601
- return;
602
- case 'servers': {
603
- const running = conversation.running;
604
- if (running?.baseline)
605
- for (const found of processes(marker(running.bridge)))
606
- running.baseline.add(found);
607
- return;
608
- }
609
676
  case 'context':
610
677
  conversation.context = { used: event.used, window: event.window };
611
678
  conversation.turn?.parts.measure(conversation.context);
@@ -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
  }