@agentshouse/kit 0.2.1-cf669c6d1ecbb → 0.2.1-cf6a92e13f031

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 (124) hide show
  1. package/README.md +36 -35
  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 +4 -7
  8. package/dist/calls.js +63 -0
  9. package/dist/chatgpt.js +32 -0
  10. package/dist/clis.js +57 -12
  11. package/dist/connection.js +126 -0
  12. package/dist/conversations.js +399 -152
  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 +123 -31
  17. package/dist/git.js +11 -9
  18. package/dist/home.js +56 -5
  19. package/dist/house-cli.js +286 -0
  20. package/dist/house-main.js +7 -189
  21. package/dist/instructions.js +6 -10
  22. package/dist/kit-cli.js +68 -0
  23. package/dist/kit-main.js +8 -22
  24. package/dist/login.js +48 -16
  25. package/dist/logout.js +0 -2
  26. package/dist/mcp.js +4 -2
  27. package/dist/placement.js +39 -0
  28. package/dist/relay.js +1 -1
  29. package/dist/resident.js +88 -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 +20 -32
  34. package/dist/transcription.js +53 -22
  35. package/dist/translate.js +2 -8
  36. package/dist/update.js +69 -50
  37. package/dist/web-wizard.js +100 -0
  38. package/dist/wire.js +22 -0
  39. package/dist/work.js +3 -5
  40. package/package.json +7 -6
  41. package/dist/stream.js +0 -74
  42. package/how-we-work.md +0 -23
  43. package/skills/ask-matt/LICENSE +0 -21
  44. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  45. package/skills/ask-matt/SKILL.md +0 -91
  46. package/skills/ask-matt/agents/openai.yaml +0 -4
  47. package/skills/code-review/LICENSE +0 -21
  48. package/skills/code-review/SKILL.md +0 -89
  49. package/skills/code-review/agents/openai.yaml +0 -3
  50. package/skills/codebase-design/DEEPENING.md +0 -37
  51. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  52. package/skills/codebase-design/LICENSE +0 -21
  53. package/skills/codebase-design/SKILL.md +0 -116
  54. package/skills/codebase-design/agents/openai.yaml +0 -3
  55. package/skills/diagnosing-bugs/LICENSE +0 -21
  56. package/skills/diagnosing-bugs/SKILL.md +0 -140
  57. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  58. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  59. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  60. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  61. package/skills/domain-modeling/LICENSE +0 -21
  62. package/skills/domain-modeling/SKILL.md +0 -76
  63. package/skills/domain-modeling/agents/openai.yaml +0 -3
  64. package/skills/grill-me/LICENSE +0 -21
  65. package/skills/grill-me/SKILL.md +0 -8
  66. package/skills/grill-me/agents/openai.yaml +0 -4
  67. package/skills/grill-with-docs/LICENSE +0 -21
  68. package/skills/grill-with-docs/SKILL.md +0 -8
  69. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  70. package/skills/grilling/LICENSE +0 -21
  71. package/skills/grilling/SKILL.md +0 -30
  72. package/skills/grilling/agents/openai.yaml +0 -3
  73. package/skills/handoff/LICENSE +0 -21
  74. package/skills/handoff/SKILL.md +0 -17
  75. package/skills/handoff/agents/openai.yaml +0 -4
  76. package/skills/implement/LICENSE +0 -21
  77. package/skills/implement/SKILL.md +0 -16
  78. package/skills/implement/agents/openai.yaml +0 -4
  79. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  80. package/skills/improve-codebase-architecture/LICENSE +0 -21
  81. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  82. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  83. package/skills/prototype/LICENSE +0 -21
  84. package/skills/prototype/LOGIC.md +0 -67
  85. package/skills/prototype/SKILL.md +0 -28
  86. package/skills/prototype/UI.md +0 -112
  87. package/skills/prototype/agents/openai.yaml +0 -3
  88. package/skills/research/LICENSE +0 -21
  89. package/skills/research/SKILL.md +0 -14
  90. package/skills/research/agents/openai.yaml +0 -3
  91. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  92. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  93. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  94. package/skills/tdd/LICENSE +0 -21
  95. package/skills/tdd/SKILL.md +0 -40
  96. package/skills/tdd/agents/openai.yaml +0 -3
  97. package/skills/tdd/mocking.md +0 -59
  98. package/skills/tdd/tests.md +0 -77
  99. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  100. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  101. package/skills/teach/LICENSE +0 -21
  102. package/skills/teach/MISSION-FORMAT.md +0 -31
  103. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  104. package/skills/teach/SKILL.md +0 -141
  105. package/skills/teach/agents/openai.yaml +0 -4
  106. package/skills/to-questionnaire/LICENSE +0 -21
  107. package/skills/to-questionnaire/SKILL.md +0 -55
  108. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  109. package/skills/to-spec/LICENSE +0 -21
  110. package/skills/to-spec/SKILL.md +0 -76
  111. package/skills/to-spec/agents/openai.yaml +0 -4
  112. package/skills/to-tickets/LICENSE +0 -21
  113. package/skills/to-tickets/SKILL.md +0 -106
  114. package/skills/to-tickets/agents/openai.yaml +0 -4
  115. package/skills/wait-what/LICENSE +0 -21
  116. package/skills/wait-what/SKILL.md +0 -8
  117. package/skills/wait-what/agents/openai.yaml +0 -4
  118. package/skills/wayfinder/LICENSE +0 -21
  119. package/skills/wayfinder/SKILL.md +0 -129
  120. package/skills/wayfinder/agents/openai.yaml +0 -4
  121. package/skills/writing-for-agents/LICENSE +0 -21
  122. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  123. package/skills/writing-for-agents/SKILL.md +0 -83
  124. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -1,41 +1,115 @@
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 { inspectedFile, linkedPaths, 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
- 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 INTERRUPTED = 'the conversation was interrupted';
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
+ class ResumeRefused extends NotStarted {
40
+ }
41
+ async function step(reason, work) {
42
+ try {
43
+ return await work;
44
+ }
45
+ catch (error) {
46
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
47
+ }
48
+ }
49
+ function signedOut(error) {
50
+ return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
51
+ }
52
+ function unopened(reason) {
53
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
54
+ }
55
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
56
+ const STARTING_SHOWN_AFTER_MS = 1_000;
57
+ function unbridged(error) {
58
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
59
+ }
60
+ const EXIT_PLAN_MODE = {
61
+ parse(value) {
62
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
63
+ throw new Error('_x.ai/exit_plan_mode needs an object');
64
+ }
65
+ return value;
66
+ },
67
+ };
68
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
20
69
  function causeOf(error) {
21
70
  return error instanceof Error ? error.message : String(error);
22
71
  }
72
+ function answeredWith(error) {
73
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
74
+ }
75
+ function exitedWith(running, cause) {
76
+ return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
77
+ }
23
78
  function answerMessage(request, response) {
24
79
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
25
80
  }
81
+ async function sentPart(url, bytes) {
82
+ const answer = await fetch(relayed(url), { method: 'PUT', body: bytes });
83
+ await answer.arrayBuffer();
84
+ if (!answer.ok)
85
+ throw storeRefusal('upload', answer.status);
86
+ }
26
87
  function logged(error) {
27
88
  process.stderr.write(`kit: ${causeOf(error)}\n`);
28
89
  }
90
+ function refusal(conversation, error) {
91
+ if (conversation.kills > 0)
92
+ return KILLED;
93
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
94
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
95
+ return causeOf(error);
96
+ }
97
+ function audioOf(result) {
98
+ if (result.isError === true)
99
+ return null;
100
+ const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
101
+ return file?.media_type?.startsWith('audio/') === true ? file : null;
102
+ }
29
103
  function marker(bridge) {
30
104
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
105
  }
32
106
  class Turn {
33
107
  id = randomUUID();
34
108
  parts;
35
- admitted = false;
36
109
  questions = 0;
37
110
  secrets = 0;
38
111
  prompted = false;
112
+ taken = null;
39
113
  ended = false;
40
114
  images = 0;
41
115
  uploads = [];
@@ -49,6 +123,8 @@ class Conversation {
49
123
  session = null;
50
124
  turn = null;
51
125
  commands = null;
126
+ options = null;
127
+ exitUntold = false;
52
128
  context = null;
53
129
  leftovers = new Set();
54
130
  waiting = [];
@@ -56,6 +132,9 @@ class Conversation {
56
132
  subagents = new Map();
57
133
  held = [];
58
134
  opening = null;
135
+ bridge = null;
136
+ launch = null;
137
+ prompted = null;
59
138
  kills = 0;
60
139
  killed = new AbortController();
61
140
  interrupts = 0;
@@ -63,6 +142,11 @@ class Conversation {
63
142
  open = 0;
64
143
  queue = Promise.resolve();
65
144
  reports = Promise.resolve();
145
+ starting = null;
146
+ preparing = null;
147
+ told = null;
148
+ untold = false;
149
+ shown = null;
66
150
  constructor(id) {
67
151
  this.id = id;
68
152
  }
@@ -78,9 +162,7 @@ function asksSecret(request) {
78
162
  export class Conversations {
79
163
  kit;
80
164
  conversations = new Map();
81
- acks = new Map();
82
165
  questions = new Map();
83
- carrying = 0;
84
166
  reporting = 0;
85
167
  watched = false;
86
168
  constructor(kit) {
@@ -88,38 +170,27 @@ export class Conversations {
88
170
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
89
171
  setInterval(() => this.look(), 2_000);
90
172
  }
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();
173
+ call(call) {
174
+ void this.kit.calls.carry(call.input_id, () => {
175
+ const conversation = this.conversation(call.conversation_id);
176
+ if (call.kind === 'kill') {
177
+ conversation.kills++;
178
+ conversation.killed.abort(new Error(KILLED));
179
+ for (const waiting of conversation.waiting.splice(0))
180
+ waiting.settle({ refused: KILLED });
181
+ this.halt(conversation);
182
+ }
183
+ if (call.kind === 'interrupt') {
184
+ conversation.interrupts++;
185
+ conversation.interrupted.abort();
186
+ }
187
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
188
+ conversation.queue = handled.catch(() => undefined);
189
+ return handled.then(async ({ result }) => ({ result: await result }));
119
190
  });
120
191
  }
121
192
  idle() {
122
- if (this.carrying > 0 || this.reporting > 0)
193
+ if (this.reporting > 0)
123
194
  return false;
124
195
  for (const conversation of this.conversations.values()) {
125
196
  const turn = conversation.turn;
@@ -148,6 +219,14 @@ export class Conversations {
148
219
  }
149
220
  opened() {
150
221
  this.watched = false;
222
+ for (const conversation of this.conversations.values()) {
223
+ if (conversation.running !== null)
224
+ this.facts(conversation);
225
+ else if (conversation.exitUntold)
226
+ this.process(conversation, false);
227
+ if (conversation.untold || conversation.told !== null)
228
+ this.retell(conversation);
229
+ }
151
230
  }
152
231
  watching(watched) {
153
232
  const resend = watched && !this.watched;
@@ -167,63 +246,99 @@ export class Conversations {
167
246
  }
168
247
  return conversation;
169
248
  }
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') {
249
+ async carry(conversation, call) {
250
+ if (call.kind === 'open')
251
+ return { result: this.open(conversation, call) };
252
+ if (call.kind === 'message')
253
+ return this.message(conversation, call);
254
+ await this.launched(conversation);
255
+ if (call.kind === 'interrupt') {
176
256
  if (--conversation.interrupts === 0)
177
257
  conversation.interrupted = new AbortController();
178
- await this.interrupt(conversation, String(input.turn_id));
258
+ await this.interrupt(conversation, String(call.turn_id));
179
259
  }
180
- if (input.kind === 'kill') {
260
+ if (call.kind === 'kill') {
181
261
  await this.kill(conversation);
182
262
  await conversation.sending;
183
263
  if (--conversation.kills === 0)
184
264
  conversation.killed = new AbortController();
185
265
  }
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));
266
+ if (call.kind === 'option')
267
+ await this.option(conversation, String(call.option), call.value);
268
+ if (call.kind === 'answer') {
269
+ const question = this.questions.get(String(call.interaction_id));
190
270
  if (question === undefined) {
191
- const text = answerMessage(input.request, input.response);
192
- return this.message(conversation, { ...input, text, files: [], first: false });
271
+ const text = answerMessage(call.request, call.response);
272
+ return this.message(conversation, { ...call, text, first: false, message_id: null });
193
273
  }
194
- question.answer(input.response);
274
+ question.answer(call.response);
195
275
  }
196
- return { ack: Promise.resolve({}) };
276
+ return { result: Promise.resolve({}) };
197
277
  }
198
- async open(conversation, input) {
278
+ open(conversation, call) {
199
279
  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) }];
280
+ return Promise.resolve({});
281
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
282
+ return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
283
+ }
284
+ launch(conversation, route, sessionId, empty) {
285
+ let opened;
286
+ let failed;
287
+ const bridge = new Promise((resolve, reject) => {
288
+ opened = resolve;
289
+ failed = reject;
290
+ });
291
+ bridge.catch(() => undefined);
292
+ const session = this.start(conversation, route, sessionId, empty, opened);
293
+ session.catch(failed);
294
+ const launch = { bridge, session };
295
+ conversation.launch = launch;
296
+ void session
297
+ .catch(() => undefined)
298
+ .then(() => {
299
+ if (conversation.launch !== launch)
300
+ return;
301
+ conversation.launch = null;
302
+ this.closeIdleBridge(conversation);
303
+ this.next(conversation);
304
+ });
305
+ this.next(conversation);
306
+ return launch;
307
+ }
308
+ async launched(conversation) {
309
+ while (conversation.launch !== null)
310
+ await conversation.launch.session.catch(() => undefined);
311
+ }
312
+ instructed(conversation, route) {
313
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
314
+ if (bridge === undefined)
315
+ return null;
316
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
317
+ read.catch(() => undefined);
318
+ return read;
319
+ }
320
+ async message(conversation, call) {
321
+ const content = [{ type: 'text', text: String(call.text) }];
210
322
  let route;
323
+ let instructing = null;
211
324
  try {
212
- route = await this.route(conversation, input.agent_id);
213
- const files = input.files;
214
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
215
- const lines = await this.transcribed(conversation, files, paths);
216
- if (lines.length > 0)
217
- content.push({ type: 'text', text: lines.join('\n') });
325
+ route = await this.route(conversation, call.agent_id);
326
+ if (call.first === true)
327
+ instructing = this.instructed(conversation, route);
328
+ if (call.message_id !== null)
329
+ content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
218
330
  if (conversation.kills > 0)
219
331
  throw new Error(KILLED);
220
332
  }
221
333
  catch (error) {
222
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
334
+ this.closeIdleBridge(conversation);
335
+ this.prepares(conversation, null);
336
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
223
337
  }
338
+ conversation.preparing = null;
224
339
  return {
225
- ack: new Promise((settle) => {
226
- conversation.waiting.push({ input, route, content, settle });
340
+ result: new Promise((settle) => {
341
+ conversation.waiting.push({ call, route, content, instructing, settle });
227
342
  this.next(conversation);
228
343
  }),
229
344
  };
@@ -232,62 +347,148 @@ export class Conversations {
232
347
  if (conversation.sending !== null || conversation.open > 0)
233
348
  return;
234
349
  const waiting = conversation.waiting.shift();
235
- if (waiting === undefined)
350
+ if (waiting === undefined) {
351
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
352
+ if (conversation.starting !== step)
353
+ this.starts(conversation, step);
236
354
  return;
355
+ }
237
356
  conversation.sending = this.send(conversation, waiting).finally(() => {
238
357
  conversation.sending = null;
239
358
  this.next(conversation);
240
359
  });
241
360
  }
242
361
  async send(conversation, waiting) {
243
- const { input } = waiting;
244
- let ack = {};
362
+ const { call } = waiting;
363
+ let result = {};
364
+ let started = false;
365
+ let instructing = waiting.instructing;
245
366
  const prompt = [];
246
367
  try {
247
368
  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 };
369
+ this.starts(conversation, 'session');
370
+ await this.launched(conversation);
371
+ }
372
+ if (conversation.running === null) {
373
+ const stored = conversation.session ?? call.provider_session_id;
374
+ const empty = call.first === true && conversation.prompted !== stored;
375
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
376
+ if (call.first === true)
377
+ instructing = this.instructed(conversation, waiting.route);
378
+ started = (await launch.session) !== stored;
252
379
  }
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
- });
380
+ const session = conversation.running.sessionId;
381
+ if (started || (call.first === true && session !== call.provider_session_id)) {
382
+ result = { provider_session_id: session };
383
+ }
384
+ if (call.first === true) {
385
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
258
386
  }
259
387
  if (conversation.kills > 0)
260
388
  throw new Error(KILLED);
261
389
  }
262
390
  catch (error) {
263
- if ('provider_session_id' in ack) {
391
+ this.starts(conversation, null);
392
+ if (started) {
264
393
  conversation.session = null;
265
394
  await this.kill(conversation);
266
395
  }
267
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
396
+ const refused = refusal(conversation, error);
397
+ const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
398
+ waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
399
+ return;
400
+ }
401
+ this.starts(conversation, null);
402
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
403
+ waiting.settle(result);
404
+ }
405
+ starts(conversation, step) {
406
+ conversation.starting = step;
407
+ if (conversation.shown !== null)
408
+ clearTimeout(conversation.shown);
409
+ conversation.shown = null;
410
+ if (step === null || conversation.told !== null) {
411
+ this.tell(conversation, step);
268
412
  return;
269
413
  }
270
- this.prompt(conversation, [...prompt, ...waiting.content]);
271
- waiting.settle(ack);
414
+ conversation.shown = setTimeout(() => {
415
+ conversation.shown = null;
416
+ if (conversation.starting === step)
417
+ this.tell(conversation, step);
418
+ }, STARTING_SHOWN_AFTER_MS);
272
419
  }
273
- async transcribed(conversation, files, paths) {
274
- const lines = [];
275
- for (const [index, path] of paths.entries()) {
276
- lines.push(path);
277
- if (files[index].media_type?.startsWith('audio/') !== true)
278
- continue;
279
- try {
280
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
281
- if (text !== null && text !== '')
282
- lines.push(`Transcript: ${text}`);
283
- }
284
- catch (error) {
285
- if (conversation.kills > 0)
286
- throw error;
287
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
420
+ prepares(conversation, step) {
421
+ conversation.preparing = step;
422
+ this.next(conversation);
423
+ }
424
+ tell(conversation, step) {
425
+ if (conversation.told === step && !conversation.untold)
426
+ return;
427
+ conversation.told = step;
428
+ this.retell(conversation);
429
+ }
430
+ retell(conversation) {
431
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
432
+ }
433
+ async transcribed(conversation, messageId, text) {
434
+ const paths = linkedPaths(text);
435
+ if (paths.length === 0)
436
+ return [];
437
+ const killed = conversation.killed.signal;
438
+ const interrupted = conversation.interrupted.signal;
439
+ const transcribing = AbortSignal.any([killed, interrupted]);
440
+ let signal = killed;
441
+ const blocks = [];
442
+ try {
443
+ const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
444
+ const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
445
+ for (const path of paths) {
446
+ let answered = await read(path);
447
+ if (answered === null)
448
+ continue;
449
+ signal = transcribing;
450
+ this.prepares(conversation, 'transcription');
451
+ const said = await transcribe(async () => {
452
+ const address = answered ?? (await read(path));
453
+ answered = null;
454
+ if (address === null)
455
+ throw new Error(`${path} is no longer an audio file`);
456
+ return address;
457
+ }, signal);
458
+ const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
459
+ await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
460
+ blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
288
461
  }
289
462
  }
290
- return lines;
463
+ catch (error) {
464
+ if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
465
+ throw new NotStarted(INTERRUPTED, { cause: error });
466
+ throw error;
467
+ }
468
+ return blocks;
469
+ }
470
+ bridged(conversation) {
471
+ if (conversation.bridge === null) {
472
+ const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
473
+ conversation.bridge = bridging;
474
+ bridging.catch(() => {
475
+ if (conversation.bridge === bridging)
476
+ conversation.bridge = null;
477
+ });
478
+ }
479
+ return conversation.bridge;
480
+ }
481
+ closeBridge(conversation, bridging, bridge) {
482
+ bridge.close();
483
+ if (conversation.bridge === bridging)
484
+ conversation.bridge = null;
485
+ }
486
+ closeIdleBridge(conversation) {
487
+ const bridging = conversation.bridge;
488
+ if (bridging === null || conversation.running !== null || conversation.launch !== null)
489
+ return;
490
+ conversation.bridge = null;
491
+ void bridging.then((bridge) => bridge.close(), () => undefined);
291
492
  }
292
493
  async interrupt(conversation, turnId) {
293
494
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -342,63 +543,77 @@ export class Conversations {
342
543
  }
343
544
  return route;
344
545
  }
345
- async start(conversation, route, sessionId) {
546
+ async start(conversation, routed, sessionId, empty, bridged) {
547
+ const route = await routed;
346
548
  const reader = readerFor(route.kind);
347
549
  reader.session = sessionId;
348
550
  const app = client({ name: '@agentshouse/kit' })
349
551
  .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));
552
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
553
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
351
554
  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);
555
+ const bridging = this.bridged(conversation);
556
+ const bridge = await step(unbridged, bridging);
353
557
  if (conversation.kills > 0) {
354
- bridge.close();
558
+ this.closeBridge(conversation, bridging, bridge);
355
559
  throw new Error(KILLED);
356
560
  }
357
561
  conversation.opening = bridge;
358
562
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
359
563
  const scope = new Scope(marker(bridge), randomUUID(), cli);
564
+ let adapter = null;
360
565
  try {
361
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
566
+ bridged(bridge);
362
567
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
363
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
568
+ const streamed = await step(NO_CONNECTION, work.open(cli));
569
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
364
570
  work.read(message);
365
571
  this.read(conversation, reader, message);
366
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
367
- void adapter.exited.then(() => bridge.close());
572
+ }, { ...bridge.env, ...streamed }, undefined, scope));
573
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
368
574
  const agent = adapter.connection.agent;
369
575
  const cwd = route.working_directory;
370
576
  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 };
577
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
578
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
374
579
  reader.session = opened.sessionId;
375
- if (sessionId === null && CLIS[route.kind].renames) {
580
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
376
581
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
377
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
582
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
378
583
  }
379
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
380
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
584
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
585
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
381
586
  void work.settled().then(() => scope.record());
382
587
  conversation.running = running;
383
588
  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));
589
+ conversation.options = options;
590
+ this.facts(conversation);
591
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
389
592
  return running.sessionId;
390
593
  }
391
594
  catch (error) {
392
- scope.kill();
595
+ adapter?.retire();
393
596
  work.close();
394
597
  conversation.leftovers.add(scope);
395
- bridge.close();
598
+ this.closeBridge(conversation, bridging, bridge);
396
599
  throw conversation.kills > 0 ? new Error(KILLED) : error;
397
600
  }
398
601
  finally {
399
602
  conversation.opening = null;
400
603
  }
401
604
  }
605
+ async resume(conversation, kind, adapter, request, empty) {
606
+ try {
607
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
608
+ }
609
+ catch (error) {
610
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
611
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
612
+ }
613
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
614
+ return null;
615
+ }
616
+ }
402
617
  async launchSettings(running, route, offered) {
403
618
  let options = offered;
404
619
  const settings = [
@@ -420,27 +635,37 @@ export class Conversations {
420
635
  }
421
636
  return options;
422
637
  }
423
- exited(conversation, adapter, cause) {
424
- const running = conversation.running;
425
- if (running?.adapter !== adapter)
426
- return;
638
+ exited(conversation, running, cause) {
427
639
  this.detach(conversation, running);
428
640
  if (conversation.turn !== null) {
429
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
641
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
430
642
  }
431
643
  this.next(conversation);
432
644
  this.kit.changed();
433
645
  }
434
646
  detach(conversation, running) {
647
+ if (conversation.bridge === running.bridging)
648
+ conversation.bridge = null;
435
649
  running.work.close();
436
650
  conversation.leftovers.add(running.scope);
437
651
  conversation.running = null;
438
652
  conversation.commands = null;
653
+ conversation.options = null;
439
654
  for (const [id, question] of this.questions) {
440
655
  if (question.conversation === conversation)
441
656
  this.questions.delete(id);
442
657
  }
443
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
658
+ this.process(conversation, false);
659
+ }
660
+ process(conversation, running) {
661
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
662
+ }
663
+ facts(conversation) {
664
+ this.process(conversation, true);
665
+ if (conversation.commands !== null)
666
+ this.commands(conversation, conversation.commands);
667
+ if (conversation.options !== null)
668
+ this.options(conversation, conversation.options);
444
669
  }
445
670
  commands(conversation, commands) {
446
671
  conversation.commands = commands;
@@ -451,6 +676,7 @@ export class Conversations {
451
676
  options(conversation, options) {
452
677
  if (conversation.running === null)
453
678
  return;
679
+ conversation.options = options;
454
680
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
455
681
  }
456
682
  deliver(conversation, turn, event, body) {
@@ -465,7 +691,7 @@ export class Conversations {
465
691
  return delivered;
466
692
  }
467
693
  write(conversation, turn, operation) {
468
- if (!turn.admitted || turn.ended || !this.watched)
694
+ if (turn.ended || !this.watched)
469
695
  return;
470
696
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
471
697
  }
@@ -476,12 +702,7 @@ export class Conversations {
476
702
  begin(conversation) {
477
703
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
478
704
  conversation.open++;
479
- void this.deliver(conversation, turn, 'started', {})
480
- .then(() => {
481
- turn.admitted = true;
482
- this.replay(conversation, turn);
483
- })
484
- .catch(logged);
705
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
485
706
  conversation.turn = turn;
486
707
  if (conversation.context !== null)
487
708
  turn.parts.measure(conversation.context);
@@ -490,13 +711,17 @@ export class Conversations {
490
711
  this.kit.changed();
491
712
  return turn;
492
713
  }
493
- end(conversation, turn, failed) {
714
+ end(conversation, turn, failed, notice = null) {
494
715
  if (turn.ended)
495
716
  return;
717
+ if (failed !== null)
718
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
496
719
  if (conversation.turn === turn) {
497
720
  for (const event of conversation.running?.reader.closeTurn() ?? [])
498
721
  this.apply(conversation, turn, event);
499
722
  }
723
+ if (notice !== null)
724
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
500
725
  turn.ended = true;
501
726
  conversation.open--;
502
727
  if (conversation.turn === turn)
@@ -504,31 +729,45 @@ export class Conversations {
504
729
  const body = Promise.allSettled(turn.uploads).then(() => ({
505
730
  parts: turn.parts.saved(),
506
731
  context: turn.parts.context,
507
- ...failed,
732
+ ...(failed === null ? {} : { failed: failed.failure }),
508
733
  }));
509
734
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
510
735
  this.next(conversation);
511
736
  this.kit.changed();
512
737
  }
513
- prompt(conversation, prompt) {
738
+ prompt(conversation, prompt, taken) {
514
739
  const running = conversation.running;
515
740
  const turn = this.begin(conversation);
516
741
  turn.prompted = true;
517
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
742
+ turn.taken = taken;
743
+ conversation.prompted = running.sessionId;
744
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
745
+ this.took(turn);
746
+ const notice = answeredFailure(answer);
747
+ if (notice !== null) {
748
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
749
+ }
750
+ else {
751
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
752
+ }
753
+ }, async (error) => {
518
754
  if (running.adapter.connection.signal.aborted) {
519
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
755
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
520
756
  }
521
757
  else {
758
+ this.took(turn);
522
759
  if (conversation.open === 1 && conversation.running === running) {
523
- running.scope.kill();
760
+ running.adapter.retire();
524
761
  this.detach(conversation, running);
525
762
  }
526
- this.end(conversation, turn, { failed: causeOf(error) });
763
+ this.end(conversation, turn, answeredWith(error));
527
764
  }
528
765
  });
529
766
  }
530
767
  started(conversation) {
531
- return conversation.turn ?? this.begin(conversation);
768
+ const turn = conversation.turn ?? this.begin(conversation);
769
+ this.took(turn);
770
+ return turn;
532
771
  }
533
772
  ended(conversation) {
534
773
  const turn = conversation.turn;
@@ -623,6 +862,11 @@ export class Conversations {
623
862
  }
624
863
  this.apply(conversation, this.started(conversation), event);
625
864
  }
865
+ took(turn) {
866
+ const taken = turn.taken;
867
+ turn.taken = null;
868
+ taken?.();
869
+ }
626
870
  apply(conversation, turn, event) {
627
871
  switch (event.kind) {
628
872
  case 'text':
@@ -677,13 +921,16 @@ export class Conversations {
677
921
  bytes: bytes.length,
678
922
  sha256: createHash('sha256').update(bytes).digest('hex'),
679
923
  }));
680
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
681
- if (saved.save.status !== 'saved')
682
- throw new Error(saved.save.failure ?? saved.save.status);
683
- turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
924
+ const saved = await uploaded(declared.upload, {
925
+ bytes: bytes.length,
926
+ send: (url, start, end) => sentPart(url, bytes.subarray(start, end)),
927
+ finish: (upload) => this.kit.house.deliver('/kit/uploads/finish', { upload }),
928
+ });
929
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
684
930
  }
685
931
  catch (error) {
686
- turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
932
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
933
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
687
934
  }
688
935
  }
689
936
  }