@agentshouse/kit 0.2.1-c8179f06883b6 → 0.2.1-c845e172df1af

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 +30 -29
  2. package/dist/acp.js +36 -16
  3. package/dist/agents.js +106 -23
  4. package/dist/api.js +5 -5
  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 +14 -4
  11. package/dist/connection.js +125 -0
  12. package/dist/conversations.js +323 -127
  13. package/dist/copies.js +89 -52
  14. package/dist/end.js +9 -4
  15. package/dist/events.js +6 -2
  16. package/dist/files.js +117 -22
  17. package/dist/git.js +11 -9
  18. package/dist/home.js +56 -5
  19. package/dist/house-cli.js +327 -0
  20. package/dist/house-main.js +7 -228
  21. package/dist/instructions.js +4 -28
  22. package/dist/kit-cli.js +68 -0
  23. package/dist/kit-main.js +8 -67
  24. package/dist/login.js +40 -16
  25. package/dist/logout.js +0 -2
  26. package/dist/mcp.js +4 -2
  27. package/dist/placement.js +9 -18
  28. package/dist/resident.js +82 -37
  29. package/dist/scope.js +3 -0
  30. package/dist/service.js +23 -1
  31. package/dist/sign-in.js +19 -37
  32. package/dist/skills.js +84 -15
  33. package/dist/transcription.js +3 -3
  34. package/dist/translate.js +2 -8
  35. package/dist/update.js +43 -45
  36. package/dist/wire.js +19 -3
  37. package/dist/work.js +3 -5
  38. package/how-we-work.md +13 -3
  39. package/package.json +4 -3
  40. package/skills/ask-matt/SKILL.md +17 -11
  41. package/skills/ask-matt/agents/openai.yaml +1 -0
  42. package/skills/code-review/SKILL.md +5 -5
  43. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  44. package/skills/codebase-design/SKILL.md +0 -2
  45. package/skills/diagnosing-bugs/SKILL.md +2 -4
  46. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  47. package/skills/domain-modeling/SKILL.md +12 -14
  48. package/skills/grill-me/SKILL.md +1 -2
  49. package/skills/grill-me/agents/openai.yaml +1 -0
  50. package/skills/grill-with-docs/SKILL.md +1 -2
  51. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  52. package/skills/grilling/SKILL.md +2 -2
  53. package/skills/handoff/SKILL.md +2 -3
  54. package/skills/handoff/agents/openai.yaml +1 -0
  55. package/skills/implement/SKILL.md +5 -4
  56. package/skills/implement/agents/openai.yaml +1 -0
  57. package/skills/implement-spec/SKILL.md +40 -0
  58. package/skills/implement-spec/agents/openai.yaml +5 -0
  59. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  60. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  61. package/skills/pr/CREDITS.md +3 -0
  62. package/skills/pr/LICENSE +21 -0
  63. package/skills/pr/SKILL.md +170 -0
  64. package/skills/pr/agents/openai.yaml +3 -0
  65. package/skills/prototype/SKILL.md +0 -2
  66. package/skills/research/SKILL.md +0 -2
  67. package/skills/retro/LICENSE +21 -0
  68. package/skills/retro/SKILL.md +44 -0
  69. package/skills/retro/agents/openai.yaml +5 -0
  70. package/skills/tdd/SKILL.md +2 -4
  71. package/skills/teach/SKILL.md +3 -4
  72. package/skills/teach/agents/openai.yaml +1 -0
  73. package/skills/to-questionnaire/SKILL.md +1 -2
  74. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  75. package/skills/to-spec/SKILL.md +1 -2
  76. package/skills/to-spec/agents/openai.yaml +1 -0
  77. package/skills/to-tickets/SKILL.md +3 -4
  78. package/skills/to-tickets/agents/openai.yaml +1 -0
  79. package/skills/triage/AGENT-BRIEF.md +207 -0
  80. package/skills/triage/LICENSE +21 -0
  81. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  82. package/skills/triage/SKILL.md +112 -0
  83. package/skills/triage/agents/openai.yaml +5 -0
  84. package/skills/wait-what/SKILL.md +2 -3
  85. package/skills/wait-what/agents/openai.yaml +1 -0
  86. package/skills/wayfinder/SKILL.md +5 -6
  87. package/skills/wayfinder/agents/openai.yaml +1 -0
  88. package/skills/wizard/LICENSE +21 -0
  89. package/skills/wizard/SKILL.md +44 -0
  90. package/skills/wizard/agents/openai.yaml +3 -0
  91. package/skills/wizard/template.sh +223 -0
  92. package/skills/writing-for-agents/SKILL.md +0 -2
  93. package/dist/codex.js +0 -61
  94. package/dist/stream.js +0 -77
  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,42 +1,107 @@
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, HouseRefusal } 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 { placeFiles, uploadBytes } from './files.js';
10
+ import { answeredFailure, readerFor } from './events.js';
11
+ import { placeFiles, storeRefusal, uploaded } 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 { relayed } from './relay.js';
14
17
  import { killMarked, Scope } from './scope.js';
15
18
  import { holdSecretInput } from './secret-input.js';
16
19
  import { createHowWeWork } from './skills.js';
17
20
  import { transcribe } from './transcription.js';
18
21
  import { workFor } from './work.js';
22
+ const SIGNED_OUT = RequestError.authRequired().code;
19
23
  const KILLED = 'the conversation was killed';
20
- export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
24
+ const REFUSED = 'the CLI refused the prompt';
25
+ const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
26
+ const GONE = 'House no longer holds this conversation; start a new one';
27
+ const RETIRED = "this conversation's Agent is retired, so it cannot start";
28
+ const ENDED = {
29
+ conversation_not_found: GONE,
30
+ conversation_historical: RETIRED,
31
+ };
32
+ const NO_START = "the Agent's CLI did not start";
33
+ const NO_SESSION = "the Agent's CLI did not open a session";
34
+ const NO_RESUME = "the Agent's CLI could not resume this conversation";
35
+ const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
36
+ const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
37
+ class NotStarted extends Error {
38
+ }
39
+ async function step(reason, work) {
40
+ try {
41
+ return await work;
42
+ }
43
+ catch (error) {
44
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
45
+ }
46
+ }
47
+ function signedOut(error) {
48
+ return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
49
+ }
50
+ function unopened(reason) {
51
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
52
+ }
53
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
54
+ const STARTING_SHOWN_AFTER_MS = 1_000;
55
+ function unbridged(error) {
56
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
57
+ }
58
+ const EXIT_PLAN_MODE = {
59
+ parse(value) {
60
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
61
+ throw new Error('_x.ai/exit_plan_mode needs an object');
62
+ }
63
+ return value;
64
+ },
65
+ };
66
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
21
67
  function causeOf(error) {
22
68
  return error instanceof Error ? error.message : String(error);
23
69
  }
70
+ function answeredWith(error) {
71
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
72
+ }
73
+ function exitedWith(running, cause) {
74
+ return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
75
+ }
24
76
  function answerMessage(request, response) {
25
77
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
26
78
  }
79
+ async function sentPart(url, bytes) {
80
+ const answer = await fetch(relayed(url), { method: 'PUT', body: bytes });
81
+ await answer.arrayBuffer();
82
+ if (!answer.ok)
83
+ throw storeRefusal('upload', answer.status);
84
+ }
27
85
  function logged(error) {
28
86
  process.stderr.write(`kit: ${causeOf(error)}\n`);
29
87
  }
88
+ function refusal(conversation, error) {
89
+ if (conversation.kills > 0)
90
+ return KILLED;
91
+ const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
92
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
93
+ return causeOf(error);
94
+ }
30
95
  function marker(bridge) {
31
96
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
32
97
  }
33
98
  class Turn {
34
99
  id = randomUUID();
35
100
  parts;
36
- admitted = false;
37
101
  questions = 0;
38
102
  secrets = 0;
39
103
  prompted = false;
104
+ taken = null;
40
105
  ended = false;
41
106
  images = 0;
42
107
  uploads = [];
@@ -50,6 +115,8 @@ class Conversation {
50
115
  session = null;
51
116
  turn = null;
52
117
  commands = null;
118
+ options = null;
119
+ exitUntold = false;
53
120
  context = null;
54
121
  leftovers = new Set();
55
122
  waiting = [];
@@ -57,6 +124,8 @@ class Conversation {
57
124
  subagents = new Map();
58
125
  held = [];
59
126
  opening = null;
127
+ launch = null;
128
+ prompted = null;
60
129
  kills = 0;
61
130
  killed = new AbortController();
62
131
  interrupts = 0;
@@ -64,6 +133,11 @@ class Conversation {
64
133
  open = 0;
65
134
  queue = Promise.resolve();
66
135
  reports = Promise.resolve();
136
+ starting = null;
137
+ preparing = null;
138
+ told = null;
139
+ untold = false;
140
+ shown = null;
67
141
  constructor(id) {
68
142
  this.id = id;
69
143
  }
@@ -79,9 +153,7 @@ function asksSecret(request) {
79
153
  export class Conversations {
80
154
  kit;
81
155
  conversations = new Map();
82
- acks = new Map();
83
156
  questions = new Map();
84
- carrying = 0;
85
157
  reporting = 0;
86
158
  watched = false;
87
159
  constructor(kit) {
@@ -89,38 +161,27 @@ export class Conversations {
89
161
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
90
162
  setInterval(() => this.look(), 2_000);
91
163
  }
92
- input(input) {
93
- this.carrying++;
94
- const carried = this.acks.get(input.input_id) ??
95
- (() => {
96
- const conversation = this.conversation(input.conversation_id);
97
- if (input.kind === 'kill') {
98
- conversation.kills++;
99
- conversation.killed.abort(new Error(KILLED));
100
- for (const waiting of conversation.waiting.splice(0))
101
- waiting.settle({ refused: KILLED });
102
- this.halt(conversation);
103
- }
104
- if (input.kind === 'interrupt') {
105
- conversation.interrupts++;
106
- conversation.interrupted.abort();
107
- }
108
- const handled = conversation.queue.then(() => this.carry(conversation, input));
109
- conversation.queue = handled.catch(() => undefined);
110
- const work = handled.then(({ ack }) => ack);
111
- this.acks.set(input.input_id, work);
112
- return work;
113
- })();
114
- carried
115
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
116
- .catch(logged)
117
- .finally(() => {
118
- this.carrying--;
119
- this.kit.changed();
164
+ call(call) {
165
+ void this.kit.calls.carry(call.input_id, () => {
166
+ const conversation = this.conversation(call.conversation_id);
167
+ if (call.kind === 'kill') {
168
+ conversation.kills++;
169
+ conversation.killed.abort(new Error(KILLED));
170
+ for (const waiting of conversation.waiting.splice(0))
171
+ waiting.settle({ refused: KILLED });
172
+ this.halt(conversation);
173
+ }
174
+ if (call.kind === 'interrupt') {
175
+ conversation.interrupts++;
176
+ conversation.interrupted.abort();
177
+ }
178
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
179
+ conversation.queue = handled.catch(() => undefined);
180
+ return handled.then(async ({ result }) => ({ result: await result }));
120
181
  });
121
182
  }
122
183
  idle() {
123
- if (this.carrying > 0 || this.reporting > 0)
184
+ if (this.reporting > 0)
124
185
  return false;
125
186
  for (const conversation of this.conversations.values()) {
126
187
  const turn = conversation.turn;
@@ -149,6 +210,14 @@ export class Conversations {
149
210
  }
150
211
  opened() {
151
212
  this.watched = false;
213
+ for (const conversation of this.conversations.values()) {
214
+ if (conversation.running !== null)
215
+ this.facts(conversation);
216
+ else if (conversation.exitUntold)
217
+ this.process(conversation, false);
218
+ if (conversation.untold || conversation.told !== null)
219
+ this.retell(conversation);
220
+ }
152
221
  }
153
222
  watching(watched) {
154
223
  const resend = watched && !this.watched;
@@ -168,50 +237,87 @@ export class Conversations {
168
237
  }
169
238
  return conversation;
170
239
  }
171
- async carry(conversation, input) {
172
- if (input.kind === 'open')
173
- return { ack: Promise.resolve(await this.open(conversation, input)) };
174
- if (input.kind === 'message')
175
- return this.message(conversation, input);
176
- if (input.kind === 'interrupt') {
240
+ async carry(conversation, call) {
241
+ if (call.kind === 'open')
242
+ return { result: this.open(conversation, call) };
243
+ if (call.kind === 'message')
244
+ return this.message(conversation, call);
245
+ await this.launched(conversation);
246
+ if (call.kind === 'interrupt') {
177
247
  if (--conversation.interrupts === 0)
178
248
  conversation.interrupted = new AbortController();
179
- await this.interrupt(conversation, String(input.turn_id));
249
+ await this.interrupt(conversation, String(call.turn_id));
180
250
  }
181
- if (input.kind === 'kill') {
251
+ if (call.kind === 'kill') {
182
252
  await this.kill(conversation);
183
253
  await conversation.sending;
184
254
  if (--conversation.kills === 0)
185
255
  conversation.killed = new AbortController();
186
256
  }
187
- if (input.kind === 'option')
188
- await this.option(conversation, String(input.option), input.value);
189
- if (input.kind === 'answer') {
190
- const question = this.questions.get(String(input.interaction_id));
257
+ if (call.kind === 'option')
258
+ await this.option(conversation, String(call.option), call.value);
259
+ if (call.kind === 'answer') {
260
+ const question = this.questions.get(String(call.interaction_id));
191
261
  if (question === undefined) {
192
- const text = answerMessage(input.request, input.response);
193
- return this.message(conversation, { ...input, text, files: [], first: false });
262
+ const text = answerMessage(call.request, call.response);
263
+ return this.message(conversation, { ...call, text, files: [], first: false });
194
264
  }
195
- question.answer(input.response);
265
+ question.answer(call.response);
196
266
  }
197
- return { ack: Promise.resolve({}) };
267
+ return { result: Promise.resolve({}) };
198
268
  }
199
- async open(conversation, input) {
269
+ open(conversation, call) {
200
270
  if (conversation.running !== null)
201
- return {};
202
- try {
203
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
204
- }
205
- catch (error) {
206
- return { refused: causeOf(error) };
207
- }
208
- }
209
- async message(conversation, input) {
210
- const content = [{ type: 'text', text: String(input.text) }];
271
+ return Promise.resolve({});
272
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
273
+ return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
274
+ }
275
+ launch(conversation, route, sessionId, empty) {
276
+ let opened;
277
+ let failed;
278
+ const bridge = new Promise((resolve, reject) => {
279
+ opened = resolve;
280
+ failed = reject;
281
+ });
282
+ bridge.catch(() => undefined);
283
+ const session = this.start(conversation, route, sessionId, empty, opened);
284
+ session.catch(failed);
285
+ const launch = { bridge, session };
286
+ conversation.launch = launch;
287
+ void session
288
+ .catch(() => undefined)
289
+ .then(() => {
290
+ if (conversation.launch !== launch)
291
+ return;
292
+ conversation.launch = null;
293
+ this.next(conversation);
294
+ });
295
+ this.next(conversation);
296
+ return launch;
297
+ }
298
+ async launched(conversation) {
299
+ while (conversation.launch !== null)
300
+ await conversation.launch.session.catch(() => undefined);
301
+ }
302
+ instructed(conversation, route) {
303
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
304
+ if (bridge === undefined)
305
+ return null;
306
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
307
+ read.catch(() => undefined);
308
+ return read;
309
+ }
310
+ async message(conversation, call) {
311
+ const content = [{ type: 'text', text: String(call.text) }];
211
312
  let route;
313
+ let instructing = null;
212
314
  try {
213
- route = await this.route(conversation, input.agent_id);
214
- const files = input.files;
315
+ route = await this.route(conversation, call.agent_id);
316
+ if (call.first === true)
317
+ instructing = this.instructed(conversation, route);
318
+ const files = call.files;
319
+ if (files.length > 0)
320
+ this.prepares(conversation, 'files');
215
321
  const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
216
322
  const lines = await this.transcribed(conversation, files, paths);
217
323
  if (lines.length > 0)
@@ -220,11 +326,13 @@ export class Conversations {
220
326
  throw new Error(KILLED);
221
327
  }
222
328
  catch (error) {
223
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
329
+ this.prepares(conversation, null);
330
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
224
331
  }
332
+ conversation.preparing = null;
225
333
  return {
226
- ack: new Promise((settle) => {
227
- conversation.waiting.push({ input, route, content, settle });
334
+ result: new Promise((settle) => {
335
+ conversation.waiting.push({ call, route, content, instructing, settle });
228
336
  this.next(conversation);
229
337
  }),
230
338
  };
@@ -233,43 +341,86 @@ export class Conversations {
233
341
  if (conversation.sending !== null || conversation.open > 0)
234
342
  return;
235
343
  const waiting = conversation.waiting.shift();
236
- if (waiting === undefined)
344
+ if (waiting === undefined) {
345
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
346
+ if (conversation.starting !== step)
347
+ this.starts(conversation, step);
237
348
  return;
349
+ }
238
350
  conversation.sending = this.send(conversation, waiting).finally(() => {
239
351
  conversation.sending = null;
240
352
  this.next(conversation);
241
353
  });
242
354
  }
243
355
  async send(conversation, waiting) {
244
- const { input } = waiting;
245
- let ack = {};
356
+ const { call } = waiting;
357
+ let result = {};
358
+ let started = false;
359
+ let instructing = waiting.instructing;
246
360
  const prompt = [];
247
361
  try {
248
362
  if (conversation.running === null) {
249
- const resumed = input.provider_session_id ?? conversation.session;
250
- const opened = await this.start(conversation, waiting.route, resumed);
251
- if (resumed === null)
252
- ack = { provider_session_id: opened };
363
+ this.starts(conversation, 'session');
364
+ await this.launched(conversation);
365
+ }
366
+ if (conversation.running === null) {
367
+ const stored = conversation.session ?? call.provider_session_id;
368
+ const empty = call.first === true && conversation.prompted !== stored;
369
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
370
+ if (call.first === true)
371
+ instructing = this.instructed(conversation, waiting.route);
372
+ started = (await launch.session) !== stored;
373
+ }
374
+ const session = conversation.running.sessionId;
375
+ if (started || (call.first === true && session !== call.provider_session_id)) {
376
+ result = { provider_session_id: session };
253
377
  }
254
- if (input.first === true) {
255
- prompt.push({
256
- type: 'text',
257
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
258
- });
378
+ if (call.first === true) {
379
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
259
380
  }
260
381
  if (conversation.kills > 0)
261
382
  throw new Error(KILLED);
262
383
  }
263
384
  catch (error) {
264
- if ('provider_session_id' in ack) {
385
+ this.starts(conversation, null);
386
+ if (started) {
265
387
  conversation.session = null;
266
388
  await this.kill(conversation);
267
389
  }
268
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
390
+ waiting.settle({ refused: refusal(conversation, error) });
269
391
  return;
270
392
  }
271
- this.prompt(conversation, [...prompt, ...waiting.content]);
272
- waiting.settle(ack);
393
+ this.starts(conversation, null);
394
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
395
+ waiting.settle(result);
396
+ }
397
+ starts(conversation, step) {
398
+ conversation.starting = step;
399
+ if (conversation.shown !== null)
400
+ clearTimeout(conversation.shown);
401
+ conversation.shown = null;
402
+ if (step === null || conversation.told !== null) {
403
+ this.tell(conversation, step);
404
+ return;
405
+ }
406
+ conversation.shown = setTimeout(() => {
407
+ conversation.shown = null;
408
+ if (conversation.starting === step)
409
+ this.tell(conversation, step);
410
+ }, STARTING_SHOWN_AFTER_MS);
411
+ }
412
+ prepares(conversation, step) {
413
+ conversation.preparing = step;
414
+ this.next(conversation);
415
+ }
416
+ tell(conversation, step) {
417
+ if (conversation.told === step && !conversation.untold)
418
+ return;
419
+ conversation.told = step;
420
+ this.retell(conversation);
421
+ }
422
+ retell(conversation) {
423
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
273
424
  }
274
425
  async transcribed(conversation, files, paths) {
275
426
  const lines = [];
@@ -277,6 +428,7 @@ export class Conversations {
277
428
  lines.push(path);
278
429
  if (files[index].media_type?.startsWith('audio/') !== true)
279
430
  continue;
431
+ this.prepares(conversation, 'transcription');
280
432
  try {
281
433
  const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
282
434
  if (text !== null && text !== '')
@@ -343,14 +495,16 @@ export class Conversations {
343
495
  }
344
496
  return route;
345
497
  }
346
- async start(conversation, route, sessionId) {
498
+ async start(conversation, routed, sessionId, empty, bridged) {
499
+ const route = await routed;
347
500
  const reader = readerFor(route.kind);
348
501
  reader.session = sessionId;
349
502
  const app = client({ name: '@agentshouse/kit' })
350
503
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
351
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
504
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
505
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
352
506
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
353
- const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
507
+ const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
354
508
  if (conversation.kills > 0) {
355
509
  bridge.close();
356
510
  throw new Error(KILLED);
@@ -358,39 +512,39 @@ export class Conversations {
358
512
  conversation.opening = bridge;
359
513
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
360
514
  const scope = new Scope(marker(bridge), randomUUID(), cli);
515
+ let adapter = null;
361
516
  try {
362
517
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
518
+ bridged(bridge);
363
519
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
364
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
520
+ const streamed = await step(NO_CONNECTION, work.open(cli));
521
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
365
522
  work.read(message);
366
523
  this.read(conversation, reader, message);
367
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
524
+ }, { ...bridge.env, ...streamed }, undefined, scope));
368
525
  void adapter.exited.then(() => bridge.close());
369
526
  const agent = adapter.connection.agent;
370
527
  const cwd = route.working_directory;
371
528
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
372
- const opened = sessionId === null
373
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
374
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
529
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
530
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
375
531
  reader.session = opened.sessionId;
376
- if (sessionId === null && CLIS[route.kind].renames) {
532
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
377
533
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
378
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
534
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
379
535
  }
380
536
  const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
381
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
537
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
382
538
  void work.settled().then(() => scope.record());
383
539
  conversation.running = running;
384
540
  conversation.session = running.sessionId;
385
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
386
- if (conversation.commands !== null)
387
- this.commands(conversation, conversation.commands);
388
- this.options(conversation, options);
389
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
541
+ conversation.options = options;
542
+ this.facts(conversation);
543
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
390
544
  return running.sessionId;
391
545
  }
392
546
  catch (error) {
393
- scope.kill();
547
+ adapter?.retire();
394
548
  work.close();
395
549
  conversation.leftovers.add(scope);
396
550
  bridge.close();
@@ -400,6 +554,18 @@ export class Conversations {
400
554
  conversation.opening = null;
401
555
  }
402
556
  }
557
+ async resume(conversation, kind, adapter, request, empty) {
558
+ try {
559
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
560
+ }
561
+ catch (error) {
562
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
563
+ throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
564
+ }
565
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
566
+ return null;
567
+ }
568
+ }
403
569
  async launchSettings(running, route, offered) {
404
570
  let options = offered;
405
571
  const settings = [
@@ -421,13 +587,10 @@ export class Conversations {
421
587
  }
422
588
  return options;
423
589
  }
424
- exited(conversation, adapter, cause) {
425
- const running = conversation.running;
426
- if (running?.adapter !== adapter)
427
- return;
590
+ exited(conversation, running, cause) {
428
591
  this.detach(conversation, running);
429
592
  if (conversation.turn !== null) {
430
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
593
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
431
594
  }
432
595
  this.next(conversation);
433
596
  this.kit.changed();
@@ -437,11 +600,22 @@ export class Conversations {
437
600
  conversation.leftovers.add(running.scope);
438
601
  conversation.running = null;
439
602
  conversation.commands = null;
603
+ conversation.options = null;
440
604
  for (const [id, question] of this.questions) {
441
605
  if (question.conversation === conversation)
442
606
  this.questions.delete(id);
443
607
  }
444
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
608
+ this.process(conversation, false);
609
+ }
610
+ process(conversation, running) {
611
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
612
+ }
613
+ facts(conversation) {
614
+ this.process(conversation, true);
615
+ if (conversation.commands !== null)
616
+ this.commands(conversation, conversation.commands);
617
+ if (conversation.options !== null)
618
+ this.options(conversation, conversation.options);
445
619
  }
446
620
  commands(conversation, commands) {
447
621
  conversation.commands = commands;
@@ -452,6 +626,7 @@ export class Conversations {
452
626
  options(conversation, options) {
453
627
  if (conversation.running === null)
454
628
  return;
629
+ conversation.options = options;
455
630
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
456
631
  }
457
632
  deliver(conversation, turn, event, body) {
@@ -466,7 +641,7 @@ export class Conversations {
466
641
  return delivered;
467
642
  }
468
643
  write(conversation, turn, operation) {
469
- if (!turn.admitted || turn.ended || !this.watched)
644
+ if (turn.ended || !this.watched)
470
645
  return;
471
646
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
472
647
  }
@@ -477,12 +652,7 @@ export class Conversations {
477
652
  begin(conversation) {
478
653
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
479
654
  conversation.open++;
480
- void this.deliver(conversation, turn, 'started', {})
481
- .then(() => {
482
- turn.admitted = true;
483
- this.replay(conversation, turn);
484
- })
485
- .catch(logged);
655
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
486
656
  conversation.turn = turn;
487
657
  if (conversation.context !== null)
488
658
  turn.parts.measure(conversation.context);
@@ -491,13 +661,17 @@ export class Conversations {
491
661
  this.kit.changed();
492
662
  return turn;
493
663
  }
494
- end(conversation, turn, failed) {
664
+ end(conversation, turn, failed, notice = null) {
495
665
  if (turn.ended)
496
666
  return;
667
+ if (failed !== null)
668
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
497
669
  if (conversation.turn === turn) {
498
670
  for (const event of conversation.running?.reader.closeTurn() ?? [])
499
671
  this.apply(conversation, turn, event);
500
672
  }
673
+ if (notice !== null)
674
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
501
675
  turn.ended = true;
502
676
  conversation.open--;
503
677
  if (conversation.turn === turn)
@@ -505,31 +679,45 @@ export class Conversations {
505
679
  const body = Promise.allSettled(turn.uploads).then(() => ({
506
680
  parts: turn.parts.saved(),
507
681
  context: turn.parts.context,
508
- ...failed,
682
+ ...(failed === null ? {} : { failed: failed.failure }),
509
683
  }));
510
684
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
511
685
  this.next(conversation);
512
686
  this.kit.changed();
513
687
  }
514
- prompt(conversation, prompt) {
688
+ prompt(conversation, prompt, taken) {
515
689
  const running = conversation.running;
516
690
  const turn = this.begin(conversation);
517
691
  turn.prompted = true;
518
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
692
+ turn.taken = taken;
693
+ conversation.prompted = running.sessionId;
694
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
695
+ this.took(turn);
696
+ const notice = answeredFailure(answer);
697
+ if (notice !== null) {
698
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
699
+ }
700
+ else {
701
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
702
+ }
703
+ }, async (error) => {
519
704
  if (running.adapter.connection.signal.aborted) {
520
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
705
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
521
706
  }
522
707
  else {
708
+ this.took(turn);
523
709
  if (conversation.open === 1 && conversation.running === running) {
524
- running.scope.kill();
710
+ running.adapter.retire();
525
711
  this.detach(conversation, running);
526
712
  }
527
- this.end(conversation, turn, { failed: causeOf(error) });
713
+ this.end(conversation, turn, answeredWith(error));
528
714
  }
529
715
  });
530
716
  }
531
717
  started(conversation) {
532
- return conversation.turn ?? this.begin(conversation);
718
+ const turn = conversation.turn ?? this.begin(conversation);
719
+ this.took(turn);
720
+ return turn;
533
721
  }
534
722
  ended(conversation) {
535
723
  const turn = conversation.turn;
@@ -624,6 +812,11 @@ export class Conversations {
624
812
  }
625
813
  this.apply(conversation, this.started(conversation), event);
626
814
  }
815
+ took(turn) {
816
+ const taken = turn.taken;
817
+ turn.taken = null;
818
+ taken?.();
819
+ }
627
820
  apply(conversation, turn, event) {
628
821
  switch (event.kind) {
629
822
  case 'text':
@@ -678,13 +871,16 @@ export class Conversations {
678
871
  bytes: bytes.length,
679
872
  sha256: createHash('sha256').update(bytes).digest('hex'),
680
873
  }));
681
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
682
- if (saved.save.status !== 'saved')
683
- throw new Error(saved.save.failure ?? saved.save.status);
684
- turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
874
+ const saved = await uploaded(declared.upload, {
875
+ bytes: bytes.length,
876
+ send: (url, start, end) => sentPart(url, bytes.subarray(start, end)),
877
+ finish: (upload) => this.kit.house.deliver('/kit/uploads/finish', { upload }),
878
+ });
879
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
685
880
  }
686
881
  catch (error) {
687
- turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
882
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
883
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
688
884
  }
689
885
  }
690
886
  }