@agentshouse/kit 0.2.1-cf6326dfa5c3e → 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 (130) hide show
  1. package/README.md +22 -22
  2. package/dist/acp.js +2 -2
  3. package/dist/agents.js +2 -2
  4. package/dist/api.js +5 -5
  5. package/dist/bootstrap.json +4 -4
  6. package/dist/bridge.js +1 -1
  7. package/dist/{inputs.js → calls.js} +20 -20
  8. package/dist/clis.js +6 -3
  9. package/dist/{stream.js → connection.js} +30 -27
  10. package/dist/conversations.js +220 -105
  11. package/dist/copies.js +15 -29
  12. package/dist/end.js +6 -2
  13. package/dist/events.js +2 -2
  14. package/dist/files.js +123 -37
  15. package/dist/git.js +1 -1
  16. package/dist/house-cli.js +81 -86
  17. package/dist/instructions.js +28 -8
  18. package/dist/kit-cli.js +2 -2
  19. package/dist/login.js +1 -3
  20. package/dist/mcp.js +4 -2
  21. package/dist/placement.js +11 -2
  22. package/dist/resident.js +38 -32
  23. package/dist/scope.js +3 -0
  24. package/dist/service.js +1 -1
  25. package/dist/sign-in.js +19 -19
  26. package/dist/skills.js +12 -93
  27. package/dist/transcription.js +51 -20
  28. package/dist/update.js +32 -32
  29. package/dist/web-wizard.js +100 -0
  30. package/dist/wire.js +19 -3
  31. package/package.json +6 -6
  32. package/how-we-work.md +0 -33
  33. package/skills/ask-matt/LICENSE +0 -21
  34. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  35. package/skills/ask-matt/SKILL.md +0 -97
  36. package/skills/ask-matt/agents/openai.yaml +0 -5
  37. package/skills/code-review/LICENSE +0 -21
  38. package/skills/code-review/SKILL.md +0 -89
  39. package/skills/code-review/agents/openai.yaml +0 -3
  40. package/skills/codebase-design/DEEPENING.md +0 -37
  41. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  42. package/skills/codebase-design/LICENSE +0 -21
  43. package/skills/codebase-design/SKILL.md +0 -114
  44. package/skills/codebase-design/agents/openai.yaml +0 -3
  45. package/skills/diagnosing-bugs/LICENSE +0 -21
  46. package/skills/diagnosing-bugs/SKILL.md +0 -138
  47. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  48. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  49. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  50. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  51. package/skills/domain-modeling/LICENSE +0 -21
  52. package/skills/domain-modeling/SKILL.md +0 -74
  53. package/skills/domain-modeling/agents/openai.yaml +0 -3
  54. package/skills/grill-me/LICENSE +0 -21
  55. package/skills/grill-me/SKILL.md +0 -7
  56. package/skills/grill-me/agents/openai.yaml +0 -5
  57. package/skills/grill-with-docs/LICENSE +0 -21
  58. package/skills/grill-with-docs/SKILL.md +0 -7
  59. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  60. package/skills/grilling/LICENSE +0 -21
  61. package/skills/grilling/SKILL.md +0 -30
  62. package/skills/grilling/agents/openai.yaml +0 -3
  63. package/skills/handoff/LICENSE +0 -21
  64. package/skills/handoff/SKILL.md +0 -16
  65. package/skills/handoff/agents/openai.yaml +0 -5
  66. package/skills/implement/LICENSE +0 -21
  67. package/skills/implement/SKILL.md +0 -17
  68. package/skills/implement/agents/openai.yaml +0 -5
  69. package/skills/implement-spec/LICENSE +0 -21
  70. package/skills/implement-spec/SKILL.md +0 -40
  71. package/skills/implement-spec/agents/openai.yaml +0 -5
  72. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  73. package/skills/improve-codebase-architecture/LICENSE +0 -21
  74. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  75. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  76. package/skills/pr/CREDITS.md +0 -3
  77. package/skills/pr/LICENSE +0 -21
  78. package/skills/pr/SKILL.md +0 -170
  79. package/skills/pr/agents/openai.yaml +0 -3
  80. package/skills/prototype/LICENSE +0 -21
  81. package/skills/prototype/LOGIC.md +0 -67
  82. package/skills/prototype/SKILL.md +0 -26
  83. package/skills/prototype/UI.md +0 -112
  84. package/skills/prototype/agents/openai.yaml +0 -3
  85. package/skills/research/LICENSE +0 -21
  86. package/skills/research/SKILL.md +0 -12
  87. package/skills/research/agents/openai.yaml +0 -3
  88. package/skills/retro/LICENSE +0 -21
  89. package/skills/retro/SKILL.md +0 -44
  90. package/skills/retro/agents/openai.yaml +0 -5
  91. package/skills/tdd/LICENSE +0 -21
  92. package/skills/tdd/SKILL.md +0 -38
  93. package/skills/tdd/agents/openai.yaml +0 -3
  94. package/skills/tdd/mocking.md +0 -59
  95. package/skills/tdd/tests.md +0 -77
  96. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  97. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  98. package/skills/teach/LICENSE +0 -21
  99. package/skills/teach/MISSION-FORMAT.md +0 -31
  100. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  101. package/skills/teach/SKILL.md +0 -140
  102. package/skills/teach/agents/openai.yaml +0 -5
  103. package/skills/to-questionnaire/LICENSE +0 -21
  104. package/skills/to-questionnaire/SKILL.md +0 -54
  105. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  106. package/skills/to-spec/LICENSE +0 -21
  107. package/skills/to-spec/SKILL.md +0 -75
  108. package/skills/to-spec/agents/openai.yaml +0 -5
  109. package/skills/to-tickets/LICENSE +0 -21
  110. package/skills/to-tickets/SKILL.md +0 -105
  111. package/skills/to-tickets/agents/openai.yaml +0 -5
  112. package/skills/triage/AGENT-BRIEF.md +0 -207
  113. package/skills/triage/LICENSE +0 -21
  114. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  115. package/skills/triage/SKILL.md +0 -112
  116. package/skills/triage/agents/openai.yaml +0 -5
  117. package/skills/wait-what/LICENSE +0 -21
  118. package/skills/wait-what/SKILL.md +0 -7
  119. package/skills/wait-what/agents/openai.yaml +0 -5
  120. package/skills/wayfinder/LICENSE +0 -21
  121. package/skills/wayfinder/SKILL.md +0 -128
  122. package/skills/wayfinder/agents/openai.yaml +0 -5
  123. package/skills/wizard/LICENSE +0 -21
  124. package/skills/wizard/SKILL.md +0 -44
  125. package/skills/wizard/agents/openai.yaml +0 -3
  126. package/skills/wizard/template.sh +0 -223
  127. package/skills/writing-for-agents/LICENSE +0 -21
  128. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  129. package/skills/writing-for-agents/SKILL.md +0 -81
  130. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -4,22 +4,23 @@ 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
+ import { finalRefusal, HouseRefusal } from './api.js';
8
8
  import { openBridge } from './bridge.js';
9
9
  import { CLIS } from './clis.js';
10
10
  import { answeredFailure, readerFor } from './events.js';
11
- import { placeFiles, uploadBytes } from './files.js';
11
+ import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
12
12
  import { agentBase } from './home.js';
13
13
  import { instructions } from './instructions.js';
14
14
  import { Parts } from './parts.js';
15
15
  import { refusalOf } from './refusals.js';
16
+ import { relayed } from './relay.js';
16
17
  import { killMarked, Scope } from './scope.js';
17
18
  import { holdSecretInput } from './secret-input.js';
18
- import { createHowWeWork } from './skills.js';
19
19
  import { transcribe } from './transcription.js';
20
20
  import { workFor } from './work.js';
21
21
  const SIGNED_OUT = RequestError.authRequired().code;
22
22
  const KILLED = 'the conversation was killed';
23
+ const INTERRUPTED = 'the conversation was interrupted';
23
24
  const REFUSED = 'the CLI refused the prompt';
24
25
  const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
25
26
  const GONE = 'House no longer holds this conversation; start a new one';
@@ -35,6 +36,8 @@ const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent
35
36
  const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
36
37
  class NotStarted extends Error {
37
38
  }
39
+ class ResumeRefused extends NotStarted {
40
+ }
38
41
  async function step(reason, work) {
39
42
  try {
40
43
  return await work;
@@ -62,7 +65,7 @@ const EXIT_PLAN_MODE = {
62
65
  return value;
63
66
  },
64
67
  };
65
- export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
68
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
66
69
  function causeOf(error) {
67
70
  return error instanceof Error ? error.message : String(error);
68
71
  }
@@ -75,16 +78,28 @@ function exitedWith(running, cause) {
75
78
  function answerMessage(request, response) {
76
79
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
77
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
+ }
78
87
  function logged(error) {
79
88
  process.stderr.write(`kit: ${causeOf(error)}\n`);
80
89
  }
81
90
  function refusal(conversation, error) {
82
91
  if (conversation.kills > 0)
83
92
  return KILLED;
84
- const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
93
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
85
94
  logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
86
95
  return causeOf(error);
87
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
+ }
88
103
  function marker(bridge) {
89
104
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
90
105
  }
@@ -108,6 +123,8 @@ class Conversation {
108
123
  session = null;
109
124
  turn = null;
110
125
  commands = null;
126
+ options = null;
127
+ exitUntold = false;
111
128
  context = null;
112
129
  leftovers = new Set();
113
130
  waiting = [];
@@ -115,6 +132,8 @@ class Conversation {
115
132
  subagents = new Map();
116
133
  held = [];
117
134
  opening = null;
135
+ bridge = null;
136
+ launch = null;
118
137
  prompted = null;
119
138
  kills = 0;
120
139
  killed = new AbortController();
@@ -151,23 +170,23 @@ export class Conversations {
151
170
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
152
171
  setInterval(() => this.look(), 2_000);
153
172
  }
154
- input(input) {
155
- void this.kit.inputs.carry(input.input_id, () => {
156
- const conversation = this.conversation(input.conversation_id);
157
- if (input.kind === 'kill') {
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') {
158
177
  conversation.kills++;
159
178
  conversation.killed.abort(new Error(KILLED));
160
179
  for (const waiting of conversation.waiting.splice(0))
161
180
  waiting.settle({ refused: KILLED });
162
181
  this.halt(conversation);
163
182
  }
164
- if (input.kind === 'interrupt') {
183
+ if (call.kind === 'interrupt') {
165
184
  conversation.interrupts++;
166
185
  conversation.interrupted.abort();
167
186
  }
168
- const handled = conversation.queue.then(() => this.carry(conversation, input));
187
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
169
188
  conversation.queue = handled.catch(() => undefined);
170
- return handled.then(async ({ ack }) => ({ ack: await ack }));
189
+ return handled.then(async ({ result }) => ({ result: await result }));
171
190
  });
172
191
  }
173
192
  idle() {
@@ -201,6 +220,10 @@ export class Conversations {
201
220
  opened() {
202
221
  this.watched = false;
203
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);
204
227
  if (conversation.untold || conversation.told !== null)
205
228
  this.retell(conversation);
206
229
  }
@@ -223,71 +246,99 @@ export class Conversations {
223
246
  }
224
247
  return conversation;
225
248
  }
226
- async carry(conversation, input) {
227
- if (input.kind === 'open')
228
- return { ack: Promise.resolve(await this.open(conversation, input)) };
229
- if (input.kind === 'message')
230
- return this.message(conversation, input);
231
- 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') {
232
256
  if (--conversation.interrupts === 0)
233
257
  conversation.interrupted = new AbortController();
234
- await this.interrupt(conversation, String(input.turn_id));
258
+ await this.interrupt(conversation, String(call.turn_id));
235
259
  }
236
- if (input.kind === 'kill') {
260
+ if (call.kind === 'kill') {
237
261
  await this.kill(conversation);
238
262
  await conversation.sending;
239
263
  if (--conversation.kills === 0)
240
264
  conversation.killed = new AbortController();
241
265
  }
242
- if (input.kind === 'option')
243
- await this.option(conversation, String(input.option), input.value);
244
- if (input.kind === 'answer') {
245
- 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));
246
270
  if (question === undefined) {
247
- const text = answerMessage(input.request, input.response);
248
- 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 });
249
273
  }
250
- question.answer(input.response);
274
+ question.answer(call.response);
251
275
  }
252
- return { ack: Promise.resolve({}) };
276
+ return { result: Promise.resolve({}) };
253
277
  }
254
- async open(conversation, input) {
278
+ open(conversation, call) {
255
279
  if (conversation.running !== null)
256
- return {};
257
- this.prepares(conversation, 'session');
258
- try {
259
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
260
- }
261
- catch (error) {
262
- return { refused: refusal(conversation, error) };
263
- }
264
- finally {
265
- this.prepares(conversation, null);
266
- }
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;
267
307
  }
268
- async message(conversation, input) {
269
- const content = [{ type: 'text', text: String(input.text) }];
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) }];
270
322
  let route;
323
+ let instructing = null;
271
324
  try {
272
- route = await this.route(conversation, input.agent_id);
273
- const files = input.files;
274
- if (files.length > 0)
275
- this.prepares(conversation, 'files');
276
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
277
- const lines = await this.transcribed(conversation, files, paths);
278
- if (lines.length > 0)
279
- 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))));
280
330
  if (conversation.kills > 0)
281
331
  throw new Error(KILLED);
282
332
  }
283
333
  catch (error) {
334
+ this.closeIdleBridge(conversation);
284
335
  this.prepares(conversation, null);
285
- return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
336
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
286
337
  }
287
338
  conversation.preparing = null;
288
339
  return {
289
- ack: new Promise((settle) => {
290
- conversation.waiting.push({ input, route, content, settle });
340
+ result: new Promise((settle) => {
341
+ conversation.waiting.push({ call, route, content, instructing, settle });
291
342
  this.next(conversation);
292
343
  }),
293
344
  };
@@ -297,8 +348,9 @@ export class Conversations {
297
348
  return;
298
349
  const waiting = conversation.waiting.shift();
299
350
  if (waiting === undefined) {
300
- if (conversation.starting !== conversation.preparing)
301
- this.starts(conversation, conversation.preparing);
351
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
352
+ if (conversation.starting !== step)
353
+ this.starts(conversation, step);
302
354
  return;
303
355
  }
304
356
  conversation.sending = this.send(conversation, waiting).finally(() => {
@@ -307,26 +359,30 @@ export class Conversations {
307
359
  });
308
360
  }
309
361
  async send(conversation, waiting) {
310
- const { input } = waiting;
311
- let ack = {};
362
+ const { call } = waiting;
363
+ let result = {};
312
364
  let started = false;
365
+ let instructing = waiting.instructing;
313
366
  const prompt = [];
314
367
  try {
315
368
  if (conversation.running === null) {
316
369
  this.starts(conversation, 'session');
317
- const stored = conversation.session ?? input.provider_session_id;
318
- const empty = input.first === true && conversation.prompted !== stored;
319
- started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
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;
320
379
  }
321
380
  const session = conversation.running.sessionId;
322
- if (started || (input.first === true && session !== input.provider_session_id)) {
323
- ack = { provider_session_id: session };
381
+ if (started || (call.first === true && session !== call.provider_session_id)) {
382
+ result = { provider_session_id: session };
324
383
  }
325
- if (input.first === true) {
326
- prompt.push({
327
- type: 'text',
328
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
329
- });
384
+ if (call.first === true) {
385
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
330
386
  }
331
387
  if (conversation.kills > 0)
332
388
  throw new Error(KILLED);
@@ -337,12 +393,14 @@ export class Conversations {
337
393
  conversation.session = null;
338
394
  await this.kill(conversation);
339
395
  }
340
- waiting.settle({ refused: refusal(conversation, 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 });
341
399
  return;
342
400
  }
343
401
  this.starts(conversation, null);
344
- this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.inputs.hands(String(input.input_id), ack));
345
- waiting.settle(ack);
402
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
403
+ waiting.settle(result);
346
404
  }
347
405
  starts(conversation, step) {
348
406
  conversation.starting = step;
@@ -372,25 +430,65 @@ export class Conversations {
372
430
  retell(conversation) {
373
431
  conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
374
432
  }
375
- async transcribed(conversation, files, paths) {
376
- const lines = [];
377
- for (const [index, path] of paths.entries()) {
378
- lines.push(path);
379
- if (files[index].media_type?.startsWith('audio/') !== true)
380
- continue;
381
- this.prepares(conversation, 'transcription');
382
- try {
383
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
384
- if (text !== null && text !== '')
385
- lines.push(`Transcript: ${text}`);
386
- }
387
- catch (error) {
388
- if (conversation.kills > 0)
389
- throw error;
390
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
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}` });
391
461
  }
392
462
  }
393
- 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);
394
492
  }
395
493
  async interrupt(conversation, turnId) {
396
494
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -445,7 +543,8 @@ export class Conversations {
445
543
  }
446
544
  return route;
447
545
  }
448
- async start(conversation, route, sessionId, empty) {
546
+ async start(conversation, routed, sessionId, empty, bridged) {
547
+ const route = await routed;
449
548
  const reader = readerFor(route.kind);
450
549
  reader.session = sessionId;
451
550
  const app = client({ name: '@agentshouse/kit' })
@@ -453,9 +552,10 @@ export class Conversations {
453
552
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
454
553
  .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
455
554
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
456
- const bridge = await step(unbridged, 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);
457
557
  if (conversation.kills > 0) {
458
- bridge.close();
558
+ this.closeBridge(conversation, bridging, bridge);
459
559
  throw new Error(KILLED);
460
560
  }
461
561
  conversation.opening = bridge;
@@ -463,14 +563,14 @@ export class Conversations {
463
563
  const scope = new Scope(marker(bridge), randomUUID(), cli);
464
564
  let adapter = null;
465
565
  try {
466
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
566
+ bridged(bridge);
467
567
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
468
568
  const streamed = await step(NO_CONNECTION, work.open(cli));
469
569
  adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
470
570
  work.read(message);
471
571
  this.read(conversation, reader, message);
472
572
  }, { ...bridge.env, ...streamed }, undefined, scope));
473
- void adapter.exited.then(() => bridge.close());
573
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
474
574
  const agent = adapter.connection.agent;
475
575
  const cwd = route.working_directory;
476
576
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
@@ -481,15 +581,13 @@ export class Conversations {
481
581
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
482
582
  await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
483
583
  }
484
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
584
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
485
585
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
486
586
  void work.settled().then(() => scope.record());
487
587
  conversation.running = running;
488
588
  conversation.session = running.sessionId;
489
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
490
- if (conversation.commands !== null)
491
- this.commands(conversation, conversation.commands);
492
- this.options(conversation, options);
589
+ conversation.options = options;
590
+ this.facts(conversation);
493
591
  void adapter.lost.then((cause) => this.exited(conversation, running, cause));
494
592
  return running.sessionId;
495
593
  }
@@ -497,7 +595,7 @@ export class Conversations {
497
595
  adapter?.retire();
498
596
  work.close();
499
597
  conversation.leftovers.add(scope);
500
- bridge.close();
598
+ this.closeBridge(conversation, bridging, bridge);
501
599
  throw conversation.kills > 0 ? new Error(KILLED) : error;
502
600
  }
503
601
  finally {
@@ -510,7 +608,7 @@ export class Conversations {
510
608
  }
511
609
  catch (error) {
512
610
  if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
513
- throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
611
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
514
612
  }
515
613
  logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
516
614
  return null;
@@ -546,15 +644,28 @@ export class Conversations {
546
644
  this.kit.changed();
547
645
  }
548
646
  detach(conversation, running) {
647
+ if (conversation.bridge === running.bridging)
648
+ conversation.bridge = null;
549
649
  running.work.close();
550
650
  conversation.leftovers.add(running.scope);
551
651
  conversation.running = null;
552
652
  conversation.commands = null;
653
+ conversation.options = null;
553
654
  for (const [id, question] of this.questions) {
554
655
  if (question.conversation === conversation)
555
656
  this.questions.delete(id);
556
657
  }
557
- 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);
558
669
  }
559
670
  commands(conversation, commands) {
560
671
  conversation.commands = commands;
@@ -565,6 +676,7 @@ export class Conversations {
565
676
  options(conversation, options) {
566
677
  if (conversation.running === null)
567
678
  return;
679
+ conversation.options = options;
568
680
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
569
681
  }
570
682
  deliver(conversation, turn, event, body) {
@@ -809,13 +921,16 @@ export class Conversations {
809
921
  bytes: bytes.length,
810
922
  sha256: createHash('sha256').update(bytes).digest('hex'),
811
923
  }));
812
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
813
- if (saved.save.status !== 'saved')
814
- throw new Error(saved.save.failure ?? saved.save.status);
815
- 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);
816
930
  }
817
931
  catch (error) {
818
- 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);
819
934
  }
820
935
  }
821
936
  }