@agentshouse/kit 0.2.1-cf110d15f69c8 → 0.2.1-cf3a50c90b895

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 (97) hide show
  1. package/README.md +31 -30
  2. package/dist/acp.js +36 -16
  3. package/dist/agents.js +110 -23
  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/calls.js +63 -0
  9. package/dist/chatgpt.js +32 -0
  10. package/dist/clis.js +20 -4
  11. package/dist/connection.js +125 -0
  12. package/dist/conversations.js +307 -120
  13. package/dist/copies.js +89 -52
  14. package/dist/end.js +11 -2
  15. package/dist/events.js +6 -2
  16. package/dist/files.js +8 -2
  17. package/dist/git.js +11 -9
  18. package/dist/home.js +56 -5
  19. package/dist/house-cli.js +291 -0
  20. package/dist/house-main.js +7 -206
  21. package/dist/instructions.js +4 -28
  22. package/dist/kit-cli.js +68 -0
  23. package/dist/kit-main.js +8 -22
  24. package/dist/login.js +46 -28
  25. package/dist/logout.js +0 -2
  26. package/dist/mcp.js +2 -2
  27. package/dist/placement.js +39 -0
  28. package/dist/relay.js +1 -1
  29. package/dist/resident.js +82 -33
  30. package/dist/scope.js +3 -0
  31. package/dist/service.js +23 -1
  32. package/dist/sign-in.js +19 -37
  33. package/dist/skills.js +84 -15
  34. package/dist/transcription.js +3 -3
  35. package/dist/translate.js +2 -8
  36. package/dist/update.js +69 -50
  37. package/dist/wire.js +22 -0
  38. package/dist/work.js +3 -5
  39. package/how-we-work.md +13 -3
  40. package/package.json +4 -3
  41. package/skills/ask-matt/SKILL.md +17 -11
  42. package/skills/ask-matt/agents/openai.yaml +1 -0
  43. package/skills/code-review/SKILL.md +5 -5
  44. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  45. package/skills/codebase-design/SKILL.md +0 -2
  46. package/skills/diagnosing-bugs/SKILL.md +2 -4
  47. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  48. package/skills/domain-modeling/SKILL.md +12 -14
  49. package/skills/grill-me/SKILL.md +1 -2
  50. package/skills/grill-me/agents/openai.yaml +1 -0
  51. package/skills/grill-with-docs/SKILL.md +1 -2
  52. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  53. package/skills/grilling/SKILL.md +2 -2
  54. package/skills/handoff/SKILL.md +2 -3
  55. package/skills/handoff/agents/openai.yaml +1 -0
  56. package/skills/implement/SKILL.md +5 -4
  57. package/skills/implement/agents/openai.yaml +1 -0
  58. package/skills/implement-spec/SKILL.md +40 -0
  59. package/skills/implement-spec/agents/openai.yaml +5 -0
  60. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  61. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  62. package/skills/pr/CREDITS.md +3 -0
  63. package/skills/pr/LICENSE +21 -0
  64. package/skills/pr/SKILL.md +170 -0
  65. package/skills/pr/agents/openai.yaml +3 -0
  66. package/skills/prototype/SKILL.md +0 -2
  67. package/skills/research/SKILL.md +0 -2
  68. package/skills/retro/LICENSE +21 -0
  69. package/skills/retro/SKILL.md +44 -0
  70. package/skills/retro/agents/openai.yaml +5 -0
  71. package/skills/tdd/SKILL.md +2 -4
  72. package/skills/teach/SKILL.md +3 -4
  73. package/skills/teach/agents/openai.yaml +1 -0
  74. package/skills/to-questionnaire/SKILL.md +1 -2
  75. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  76. package/skills/to-spec/SKILL.md +1 -2
  77. package/skills/to-spec/agents/openai.yaml +1 -0
  78. package/skills/to-tickets/SKILL.md +3 -4
  79. package/skills/to-tickets/agents/openai.yaml +1 -0
  80. package/skills/triage/AGENT-BRIEF.md +207 -0
  81. package/skills/triage/LICENSE +21 -0
  82. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  83. package/skills/triage/SKILL.md +112 -0
  84. package/skills/triage/agents/openai.yaml +5 -0
  85. package/skills/wait-what/SKILL.md +2 -3
  86. package/skills/wait-what/agents/openai.yaml +1 -0
  87. package/skills/wayfinder/SKILL.md +5 -6
  88. package/skills/wayfinder/agents/openai.yaml +1 -0
  89. package/skills/wizard/LICENSE +21 -0
  90. package/skills/wizard/SKILL.md +44 -0
  91. package/skills/wizard/agents/openai.yaml +3 -0
  92. package/skills/wizard/template.sh +223 -0
  93. package/skills/writing-for-agents/SKILL.md +0 -2
  94. package/dist/stream.js +0 -74
  95. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  96. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  97. /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_METHODS = 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) {
@@ -88,38 +154,27 @@ export class Conversations {
88
154
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
89
155
  setInterval(() => this.look(), 2_000);
90
156
  }
91
- 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();
157
+ call(call) {
158
+ void this.kit.calls.carry(call.input_id, () => {
159
+ const conversation = this.conversation(call.conversation_id);
160
+ if (call.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 (call.kind === 'interrupt') {
168
+ conversation.interrupts++;
169
+ conversation.interrupted.abort();
170
+ }
171
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
172
+ conversation.queue = handled.catch(() => undefined);
173
+ return handled.then(async ({ result }) => ({ result: await result }));
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;
@@ -148,6 +203,14 @@ export class Conversations {
148
203
  }
149
204
  opened() {
150
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
+ }
151
214
  }
152
215
  watching(watched) {
153
216
  const resend = watched && !this.watched;
@@ -167,50 +230,87 @@ export class Conversations {
167
230
  }
168
231
  return conversation;
169
232
  }
170
- async carry(conversation, input) {
171
- if (input.kind === 'open')
172
- return { ack: Promise.resolve(await this.open(conversation, input)) };
173
- if (input.kind === 'message')
174
- return this.message(conversation, input);
175
- if (input.kind === 'interrupt') {
233
+ async carry(conversation, call) {
234
+ if (call.kind === 'open')
235
+ return { result: this.open(conversation, call) };
236
+ if (call.kind === 'message')
237
+ return this.message(conversation, call);
238
+ await this.launched(conversation);
239
+ if (call.kind === 'interrupt') {
176
240
  if (--conversation.interrupts === 0)
177
241
  conversation.interrupted = new AbortController();
178
- await this.interrupt(conversation, String(input.turn_id));
242
+ await this.interrupt(conversation, String(call.turn_id));
179
243
  }
180
- if (input.kind === 'kill') {
244
+ if (call.kind === 'kill') {
181
245
  await this.kill(conversation);
182
246
  await conversation.sending;
183
247
  if (--conversation.kills === 0)
184
248
  conversation.killed = new AbortController();
185
249
  }
186
- if (input.kind === 'option')
187
- await this.option(conversation, String(input.option), input.value);
188
- if (input.kind === 'answer') {
189
- const question = this.questions.get(String(input.interaction_id));
250
+ if (call.kind === 'option')
251
+ await this.option(conversation, String(call.option), call.value);
252
+ if (call.kind === 'answer') {
253
+ const question = this.questions.get(String(call.interaction_id));
190
254
  if (question === undefined) {
191
- const text = answerMessage(input.request, input.response);
192
- return this.message(conversation, { ...input, text, files: [], first: false });
255
+ const text = answerMessage(call.request, call.response);
256
+ return this.message(conversation, { ...call, text, files: [], first: false });
193
257
  }
194
- question.answer(input.response);
258
+ question.answer(call.response);
195
259
  }
196
- return { ack: Promise.resolve({}) };
260
+ return { result: Promise.resolve({}) };
197
261
  }
198
- async open(conversation, input) {
262
+ open(conversation, call) {
199
263
  if (conversation.running !== null)
200
- return {};
201
- try {
202
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
203
- }
204
- catch (error) {
205
- return { refused: causeOf(error) };
206
- }
207
- }
208
- async message(conversation, input) {
209
- const content = [{ type: 'text', text: String(input.text) }];
264
+ return Promise.resolve({});
265
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.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;
302
+ }
303
+ async message(conversation, call) {
304
+ const content = [{ type: 'text', text: String(call.text) }];
210
305
  let route;
306
+ let instructing = null;
211
307
  try {
212
- route = await this.route(conversation, input.agent_id);
213
- const files = input.files;
308
+ route = await this.route(conversation, call.agent_id);
309
+ if (call.first === true)
310
+ instructing = this.instructed(conversation, route);
311
+ const files = call.files;
312
+ if (files.length > 0)
313
+ this.prepares(conversation, 'files');
214
314
  const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
215
315
  const lines = await this.transcribed(conversation, files, paths);
216
316
  if (lines.length > 0)
@@ -219,11 +319,13 @@ export class Conversations {
219
319
  throw new Error(KILLED);
220
320
  }
221
321
  catch (error) {
222
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
322
+ this.prepares(conversation, null);
323
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
223
324
  }
325
+ conversation.preparing = null;
224
326
  return {
225
- ack: new Promise((settle) => {
226
- conversation.waiting.push({ input, route, content, settle });
327
+ result: new Promise((settle) => {
328
+ conversation.waiting.push({ call, route, content, instructing, settle });
227
329
  this.next(conversation);
228
330
  }),
229
331
  };
@@ -232,43 +334,86 @@ export class Conversations {
232
334
  if (conversation.sending !== null || conversation.open > 0)
233
335
  return;
234
336
  const waiting = conversation.waiting.shift();
235
- 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);
236
341
  return;
342
+ }
237
343
  conversation.sending = this.send(conversation, waiting).finally(() => {
238
344
  conversation.sending = null;
239
345
  this.next(conversation);
240
346
  });
241
347
  }
242
348
  async send(conversation, waiting) {
243
- const { input } = waiting;
244
- let ack = {};
349
+ const { call } = waiting;
350
+ let result = {};
351
+ let started = false;
352
+ let instructing = waiting.instructing;
245
353
  const prompt = [];
246
354
  try {
247
355
  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 };
356
+ this.starts(conversation, 'session');
357
+ await this.launched(conversation);
358
+ }
359
+ if (conversation.running === null) {
360
+ const stored = conversation.session ?? call.provider_session_id;
361
+ const empty = call.first === true && conversation.prompted !== stored;
362
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
363
+ if (call.first === true)
364
+ instructing = this.instructed(conversation, waiting.route);
365
+ started = (await launch.session) !== stored;
252
366
  }
253
- if (input.first === true) {
254
- prompt.push({
255
- type: 'text',
256
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
257
- });
367
+ const session = conversation.running.sessionId;
368
+ if (started || (call.first === true && session !== call.provider_session_id)) {
369
+ result = { provider_session_id: session };
370
+ }
371
+ if (call.first === true) {
372
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
258
373
  }
259
374
  if (conversation.kills > 0)
260
375
  throw new Error(KILLED);
261
376
  }
262
377
  catch (error) {
263
- if ('provider_session_id' in ack) {
378
+ this.starts(conversation, null);
379
+ if (started) {
264
380
  conversation.session = null;
265
381
  await this.kill(conversation);
266
382
  }
267
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
383
+ waiting.settle({ refused: refusal(conversation, error) });
384
+ return;
385
+ }
386
+ this.starts(conversation, null);
387
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
388
+ waiting.settle(result);
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);
268
397
  return;
269
398
  }
270
- this.prompt(conversation, [...prompt, ...waiting.content]);
271
- waiting.settle(ack);
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 });
272
417
  }
273
418
  async transcribed(conversation, files, paths) {
274
419
  const lines = [];
@@ -276,6 +421,7 @@ export class Conversations {
276
421
  lines.push(path);
277
422
  if (files[index].media_type?.startsWith('audio/') !== true)
278
423
  continue;
424
+ this.prepares(conversation, 'transcription');
279
425
  try {
280
426
  const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
281
427
  if (text !== null && text !== '')
@@ -342,14 +488,16 @@ export class Conversations {
342
488
  }
343
489
  return route;
344
490
  }
345
- async start(conversation, route, sessionId) {
491
+ async start(conversation, routed, sessionId, empty, bridged) {
492
+ const route = await routed;
346
493
  const reader = readerFor(route.kind);
347
494
  reader.session = sessionId;
348
495
  const app = client({ name: '@agentshouse/kit' })
349
496
  .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));
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));
351
499
  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);
500
+ const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
353
501
  if (conversation.kills > 0) {
354
502
  bridge.close();
355
503
  throw new Error(KILLED);
@@ -357,39 +505,39 @@ export class Conversations {
357
505
  conversation.opening = bridge;
358
506
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
359
507
  const scope = new Scope(marker(bridge), randomUUID(), cli);
508
+ let adapter = null;
360
509
  try {
361
510
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
511
+ bridged(bridge);
362
512
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
363
- 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) => {
364
515
  work.read(message);
365
516
  this.read(conversation, reader, message);
366
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
517
+ }, { ...bridge.env, ...streamed }, undefined, scope));
367
518
  void adapter.exited.then(() => bridge.close());
368
519
  const agent = adapter.connection.agent;
369
520
  const cwd = route.working_directory;
370
521
  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 };
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 })));
374
524
  reader.session = opened.sessionId;
375
- if (sessionId === null && CLIS[route.kind].renames) {
525
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
376
526
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
377
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
527
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
378
528
  }
379
529
  const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
380
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
530
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
381
531
  void work.settled().then(() => scope.record());
382
532
  conversation.running = running;
383
533
  conversation.session = running.sessionId;
384
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
385
- if (conversation.commands !== null)
386
- this.commands(conversation, conversation.commands);
387
- this.options(conversation, options);
388
- 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));
389
537
  return running.sessionId;
390
538
  }
391
539
  catch (error) {
392
- scope.kill();
540
+ adapter?.retire();
393
541
  work.close();
394
542
  conversation.leftovers.add(scope);
395
543
  bridge.close();
@@ -399,6 +547,18 @@ export class Conversations {
399
547
  conversation.opening = null;
400
548
  }
401
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
+ }
402
562
  async launchSettings(running, route, offered) {
403
563
  let options = offered;
404
564
  const settings = [
@@ -420,13 +580,10 @@ export class Conversations {
420
580
  }
421
581
  return options;
422
582
  }
423
- exited(conversation, adapter, cause) {
424
- const running = conversation.running;
425
- if (running?.adapter !== adapter)
426
- return;
583
+ exited(conversation, running, cause) {
427
584
  this.detach(conversation, running);
428
585
  if (conversation.turn !== null) {
429
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
586
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
430
587
  }
431
588
  this.next(conversation);
432
589
  this.kit.changed();
@@ -436,11 +593,22 @@ export class Conversations {
436
593
  conversation.leftovers.add(running.scope);
437
594
  conversation.running = null;
438
595
  conversation.commands = null;
596
+ conversation.options = null;
439
597
  for (const [id, question] of this.questions) {
440
598
  if (question.conversation === conversation)
441
599
  this.questions.delete(id);
442
600
  }
443
- 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);
444
612
  }
445
613
  commands(conversation, commands) {
446
614
  conversation.commands = commands;
@@ -451,6 +619,7 @@ export class Conversations {
451
619
  options(conversation, options) {
452
620
  if (conversation.running === null)
453
621
  return;
622
+ conversation.options = options;
454
623
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
455
624
  }
456
625
  deliver(conversation, turn, event, body) {
@@ -465,7 +634,7 @@ export class Conversations {
465
634
  return delivered;
466
635
  }
467
636
  write(conversation, turn, operation) {
468
- if (!turn.admitted || turn.ended || !this.watched)
637
+ if (turn.ended || !this.watched)
469
638
  return;
470
639
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
471
640
  }
@@ -476,12 +645,7 @@ export class Conversations {
476
645
  begin(conversation) {
477
646
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
478
647
  conversation.open++;
479
- void this.deliver(conversation, turn, 'started', {})
480
- .then(() => {
481
- turn.admitted = true;
482
- this.replay(conversation, turn);
483
- })
484
- .catch(logged);
648
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
485
649
  conversation.turn = turn;
486
650
  if (conversation.context !== null)
487
651
  turn.parts.measure(conversation.context);
@@ -490,13 +654,17 @@ export class Conversations {
490
654
  this.kit.changed();
491
655
  return turn;
492
656
  }
493
- end(conversation, turn, failed) {
657
+ end(conversation, turn, failed, notice = null) {
494
658
  if (turn.ended)
495
659
  return;
660
+ if (failed !== null)
661
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
496
662
  if (conversation.turn === turn) {
497
663
  for (const event of conversation.running?.reader.closeTurn() ?? [])
498
664
  this.apply(conversation, turn, event);
499
665
  }
666
+ if (notice !== null)
667
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
500
668
  turn.ended = true;
501
669
  conversation.open--;
502
670
  if (conversation.turn === turn)
@@ -504,31 +672,45 @@ export class Conversations {
504
672
  const body = Promise.allSettled(turn.uploads).then(() => ({
505
673
  parts: turn.parts.saved(),
506
674
  context: turn.parts.context,
507
- ...failed,
675
+ ...(failed === null ? {} : { failed: failed.failure }),
508
676
  }));
509
677
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
510
678
  this.next(conversation);
511
679
  this.kit.changed();
512
680
  }
513
- prompt(conversation, prompt) {
681
+ prompt(conversation, prompt, taken) {
514
682
  const running = conversation.running;
515
683
  const turn = this.begin(conversation);
516
684
  turn.prompted = true;
517
- 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) => {
518
697
  if (running.adapter.connection.signal.aborted) {
519
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
698
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
520
699
  }
521
700
  else {
701
+ this.took(turn);
522
702
  if (conversation.open === 1 && conversation.running === running) {
523
- running.scope.kill();
703
+ running.adapter.retire();
524
704
  this.detach(conversation, running);
525
705
  }
526
- this.end(conversation, turn, { failed: causeOf(error) });
706
+ this.end(conversation, turn, answeredWith(error));
527
707
  }
528
708
  });
529
709
  }
530
710
  started(conversation) {
531
- return conversation.turn ?? this.begin(conversation);
711
+ const turn = conversation.turn ?? this.begin(conversation);
712
+ this.took(turn);
713
+ return turn;
532
714
  }
533
715
  ended(conversation) {
534
716
  const turn = conversation.turn;
@@ -623,6 +805,11 @@ export class Conversations {
623
805
  }
624
806
  this.apply(conversation, this.started(conversation), event);
625
807
  }
808
+ took(turn) {
809
+ const taken = turn.taken;
810
+ turn.taken = null;
811
+ taken?.();
812
+ }
626
813
  apply(conversation, turn, event) {
627
814
  switch (event.kind) {
628
815
  case 'text':