@agentshouse/kit 0.2.1-c0eea939ba749 → 0.2.1-c1236ac1fbfbb

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 +21 -21
  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/calls.js +63 -0
  7. package/dist/clis.js +1 -1
  8. package/dist/{stream.js → connection.js} +29 -27
  9. package/dist/conversations.js +289 -103
  10. package/dist/copies.js +15 -29
  11. package/dist/end.js +6 -2
  12. package/dist/events.js +2 -2
  13. package/dist/files.js +123 -37
  14. package/dist/git.js +1 -1
  15. package/dist/house-cli.js +95 -59
  16. package/dist/instructions.js +28 -8
  17. package/dist/kit-cli.js +2 -2
  18. package/dist/login.js +1 -3
  19. package/dist/mcp.js +4 -2
  20. package/dist/placement.js +11 -2
  21. package/dist/resident.js +32 -32
  22. package/dist/scope.js +3 -0
  23. package/dist/service.js +1 -1
  24. package/dist/sign-in.js +19 -19
  25. package/dist/skills.js +12 -93
  26. package/dist/transcription.js +51 -20
  27. package/dist/translate.js +2 -8
  28. package/dist/update.js +32 -32
  29. package/dist/wire.js +19 -3
  30. package/package.json +6 -7
  31. package/dist/inputs.js +0 -37
  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';
@@ -31,9 +32,12 @@ const ENDED = {
31
32
  const NO_START = "the Agent's CLI did not start";
32
33
  const NO_SESSION = "the Agent's CLI did not open a session";
33
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";
34
36
  const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
35
37
  class NotStarted extends Error {
36
38
  }
39
+ class ResumeRefused extends NotStarted {
40
+ }
37
41
  async function step(reason, work) {
38
42
  try {
39
43
  return await work;
@@ -42,6 +46,14 @@ async function step(reason, work) {
42
46
  throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
43
47
  }
44
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;
45
57
  function unbridged(error) {
46
58
  return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
47
59
  }
@@ -53,13 +65,12 @@ const EXIT_PLAN_MODE = {
53
65
  return value;
54
66
  },
55
67
  };
56
- 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']);
57
69
  function causeOf(error) {
58
70
  return error instanceof Error ? error.message : String(error);
59
71
  }
60
72
  function answeredWith(error) {
61
- const signedOut = typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
62
- return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
73
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
63
74
  }
64
75
  function exitedWith(running, cause) {
65
76
  return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
@@ -67,16 +78,28 @@ function exitedWith(running, cause) {
67
78
  function answerMessage(request, response) {
68
79
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
69
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
+ }
70
87
  function logged(error) {
71
88
  process.stderr.write(`kit: ${causeOf(error)}\n`);
72
89
  }
73
90
  function refusal(conversation, error) {
74
91
  if (conversation.kills > 0)
75
92
  return KILLED;
76
- 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);
77
94
  logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
78
95
  return causeOf(error);
79
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
+ }
80
103
  function marker(bridge) {
81
104
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
82
105
  }
@@ -86,6 +109,7 @@ class Turn {
86
109
  questions = 0;
87
110
  secrets = 0;
88
111
  prompted = false;
112
+ taken = null;
89
113
  ended = false;
90
114
  images = 0;
91
115
  uploads = [];
@@ -99,6 +123,8 @@ class Conversation {
99
123
  session = null;
100
124
  turn = null;
101
125
  commands = null;
126
+ options = null;
127
+ exitUntold = false;
102
128
  context = null;
103
129
  leftovers = new Set();
104
130
  waiting = [];
@@ -106,6 +132,8 @@ class Conversation {
106
132
  subagents = new Map();
107
133
  held = [];
108
134
  opening = null;
135
+ bridge = null;
136
+ launch = null;
109
137
  prompted = null;
110
138
  kills = 0;
111
139
  killed = new AbortController();
@@ -114,6 +142,11 @@ class Conversation {
114
142
  open = 0;
115
143
  queue = Promise.resolve();
116
144
  reports = Promise.resolve();
145
+ starting = null;
146
+ preparing = null;
147
+ told = null;
148
+ untold = false;
149
+ shown = null;
117
150
  constructor(id) {
118
151
  this.id = id;
119
152
  }
@@ -137,23 +170,23 @@ export class Conversations {
137
170
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
138
171
  setInterval(() => this.look(), 2_000);
139
172
  }
140
- input(input) {
141
- void this.kit.inputs.carry(input.input_id, () => {
142
- const conversation = this.conversation(input.conversation_id);
143
- 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') {
144
177
  conversation.kills++;
145
178
  conversation.killed.abort(new Error(KILLED));
146
179
  for (const waiting of conversation.waiting.splice(0))
147
180
  waiting.settle({ refused: KILLED });
148
181
  this.halt(conversation);
149
182
  }
150
- if (input.kind === 'interrupt') {
183
+ if (call.kind === 'interrupt') {
151
184
  conversation.interrupts++;
152
185
  conversation.interrupted.abort();
153
186
  }
154
- const handled = conversation.queue.then(() => this.carry(conversation, input));
187
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
155
188
  conversation.queue = handled.catch(() => undefined);
156
- return handled.then(async ({ ack }) => ({ ack: await ack }));
189
+ return handled.then(async ({ result }) => ({ result: await result }));
157
190
  });
158
191
  }
159
192
  idle() {
@@ -186,6 +219,14 @@ export class Conversations {
186
219
  }
187
220
  opened() {
188
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
+ }
189
230
  }
190
231
  watching(watched) {
191
232
  const resend = watched && !this.watched;
@@ -205,63 +246,99 @@ export class Conversations {
205
246
  }
206
247
  return conversation;
207
248
  }
208
- async carry(conversation, input) {
209
- if (input.kind === 'open')
210
- return { ack: Promise.resolve(await this.open(conversation, input)) };
211
- if (input.kind === 'message')
212
- return this.message(conversation, input);
213
- 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') {
214
256
  if (--conversation.interrupts === 0)
215
257
  conversation.interrupted = new AbortController();
216
- await this.interrupt(conversation, String(input.turn_id));
258
+ await this.interrupt(conversation, String(call.turn_id));
217
259
  }
218
- if (input.kind === 'kill') {
260
+ if (call.kind === 'kill') {
219
261
  await this.kill(conversation);
220
262
  await conversation.sending;
221
263
  if (--conversation.kills === 0)
222
264
  conversation.killed = new AbortController();
223
265
  }
224
- if (input.kind === 'option')
225
- await this.option(conversation, String(input.option), input.value);
226
- if (input.kind === 'answer') {
227
- 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));
228
270
  if (question === undefined) {
229
- const text = answerMessage(input.request, input.response);
230
- 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 });
231
273
  }
232
- question.answer(input.response);
274
+ question.answer(call.response);
233
275
  }
234
- return { ack: Promise.resolve({}) };
276
+ return { result: Promise.resolve({}) };
235
277
  }
236
- async open(conversation, input) {
278
+ open(conversation, call) {
237
279
  if (conversation.running !== null)
238
- return {};
239
- try {
240
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
241
- }
242
- catch (error) {
243
- return { refused: refusal(conversation, error) };
244
- }
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;
245
307
  }
246
- async message(conversation, input) {
247
- 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) }];
248
322
  let route;
323
+ let instructing = null;
249
324
  try {
250
- route = await this.route(conversation, input.agent_id);
251
- const files = input.files;
252
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
253
- const lines = await this.transcribed(conversation, files, paths);
254
- if (lines.length > 0)
255
- 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))));
256
330
  if (conversation.kills > 0)
257
331
  throw new Error(KILLED);
258
332
  }
259
333
  catch (error) {
260
- return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
334
+ this.closeIdleBridge(conversation);
335
+ this.prepares(conversation, null);
336
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
261
337
  }
338
+ conversation.preparing = null;
262
339
  return {
263
- ack: new Promise((settle) => {
264
- conversation.waiting.push({ input, route, content, settle });
340
+ result: new Promise((settle) => {
341
+ conversation.waiting.push({ call, route, content, instructing, settle });
265
342
  this.next(conversation);
266
343
  }),
267
344
  };
@@ -270,66 +347,148 @@ export class Conversations {
270
347
  if (conversation.sending !== null || conversation.open > 0)
271
348
  return;
272
349
  const waiting = conversation.waiting.shift();
273
- 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);
274
354
  return;
355
+ }
275
356
  conversation.sending = this.send(conversation, waiting).finally(() => {
276
357
  conversation.sending = null;
277
358
  this.next(conversation);
278
359
  });
279
360
  }
280
361
  async send(conversation, waiting) {
281
- const { input } = waiting;
282
- let ack = {};
362
+ const { call } = waiting;
363
+ let result = {};
283
364
  let started = false;
365
+ let instructing = waiting.instructing;
284
366
  const prompt = [];
285
367
  try {
286
368
  if (conversation.running === null) {
287
- const stored = conversation.session ?? input.provider_session_id;
288
- const empty = input.first === true && conversation.prompted !== stored;
289
- started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
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;
290
379
  }
291
380
  const session = conversation.running.sessionId;
292
- if (started || (input.first === true && session !== input.provider_session_id)) {
293
- ack = { provider_session_id: session };
381
+ if (started || (call.first === true && session !== call.provider_session_id)) {
382
+ result = { provider_session_id: session };
294
383
  }
295
- if (input.first === true) {
296
- prompt.push({
297
- type: 'text',
298
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
299
- });
384
+ if (call.first === true) {
385
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
300
386
  }
301
387
  if (conversation.kills > 0)
302
388
  throw new Error(KILLED);
303
389
  }
304
390
  catch (error) {
391
+ this.starts(conversation, null);
305
392
  if (started) {
306
393
  conversation.session = null;
307
394
  await this.kill(conversation);
308
395
  }
309
- 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 });
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);
310
412
  return;
311
413
  }
312
- this.prompt(conversation, [...prompt, ...waiting.content]);
313
- 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);
314
419
  }
315
- async transcribed(conversation, files, paths) {
316
- const lines = [];
317
- for (const [index, path] of paths.entries()) {
318
- lines.push(path);
319
- if (files[index].media_type?.startsWith('audio/') !== true)
320
- continue;
321
- try {
322
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
323
- if (text !== null && text !== '')
324
- lines.push(`Transcript: ${text}`);
325
- }
326
- catch (error) {
327
- if (conversation.kills > 0)
328
- throw error;
329
- 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}` });
330
461
  }
331
462
  }
332
- 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);
333
492
  }
334
493
  async interrupt(conversation, turnId) {
335
494
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -384,7 +543,8 @@ export class Conversations {
384
543
  }
385
544
  return route;
386
545
  }
387
- async start(conversation, route, sessionId, empty) {
546
+ async start(conversation, routed, sessionId, empty, bridged) {
547
+ const route = await routed;
388
548
  const reader = readerFor(route.kind);
389
549
  reader.session = sessionId;
390
550
  const app = client({ name: '@agentshouse/kit' })
@@ -392,9 +552,10 @@ export class Conversations {
392
552
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
393
553
  .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
394
554
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
395
- 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);
396
557
  if (conversation.kills > 0) {
397
- bridge.close();
558
+ this.closeBridge(conversation, bridging, bridge);
398
559
  throw new Error(KILLED);
399
560
  }
400
561
  conversation.opening = bridge;
@@ -402,33 +563,31 @@ export class Conversations {
402
563
  const scope = new Scope(marker(bridge), randomUUID(), cli);
403
564
  let adapter = null;
404
565
  try {
405
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
566
+ bridged(bridge);
406
567
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
407
568
  const streamed = await step(NO_CONNECTION, work.open(cli));
408
569
  adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
409
570
  work.read(message);
410
571
  this.read(conversation, reader, message);
411
572
  }, { ...bridge.env, ...streamed }, undefined, scope));
412
- void adapter.exited.then(() => bridge.close());
573
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
413
574
  const agent = adapter.connection.agent;
414
575
  const cwd = route.working_directory;
415
576
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
416
577
  const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
417
- const opened = resumed ?? (await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta })));
578
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
418
579
  reader.session = opened.sessionId;
419
580
  if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
420
581
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
421
- await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
582
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
422
583
  }
423
- 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 };
424
585
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
425
586
  void work.settled().then(() => scope.record());
426
587
  conversation.running = running;
427
588
  conversation.session = running.sessionId;
428
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
429
- if (conversation.commands !== null)
430
- this.commands(conversation, conversation.commands);
431
- this.options(conversation, options);
589
+ conversation.options = options;
590
+ this.facts(conversation);
432
591
  void adapter.lost.then((cause) => this.exited(conversation, running, cause));
433
592
  return running.sessionId;
434
593
  }
@@ -436,7 +595,7 @@ export class Conversations {
436
595
  adapter?.retire();
437
596
  work.close();
438
597
  conversation.leftovers.add(scope);
439
- bridge.close();
598
+ this.closeBridge(conversation, bridging, bridge);
440
599
  throw conversation.kills > 0 ? new Error(KILLED) : error;
441
600
  }
442
601
  finally {
@@ -449,7 +608,7 @@ export class Conversations {
449
608
  }
450
609
  catch (error) {
451
610
  if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
452
- throw new NotStarted(NO_RESUME, { cause: error });
611
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
453
612
  }
454
613
  logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
455
614
  return null;
@@ -485,15 +644,28 @@ export class Conversations {
485
644
  this.kit.changed();
486
645
  }
487
646
  detach(conversation, running) {
647
+ if (conversation.bridge === running.bridging)
648
+ conversation.bridge = null;
488
649
  running.work.close();
489
650
  conversation.leftovers.add(running.scope);
490
651
  conversation.running = null;
491
652
  conversation.commands = null;
653
+ conversation.options = null;
492
654
  for (const [id, question] of this.questions) {
493
655
  if (question.conversation === conversation)
494
656
  this.questions.delete(id);
495
657
  }
496
- 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);
497
669
  }
498
670
  commands(conversation, commands) {
499
671
  conversation.commands = commands;
@@ -504,6 +676,7 @@ export class Conversations {
504
676
  options(conversation, options) {
505
677
  if (conversation.running === null)
506
678
  return;
679
+ conversation.options = options;
507
680
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
508
681
  }
509
682
  deliver(conversation, turn, event, body) {
@@ -562,12 +735,14 @@ export class Conversations {
562
735
  this.next(conversation);
563
736
  this.kit.changed();
564
737
  }
565
- prompt(conversation, prompt) {
738
+ prompt(conversation, prompt, taken) {
566
739
  const running = conversation.running;
567
740
  const turn = this.begin(conversation);
568
741
  turn.prompted = true;
742
+ turn.taken = taken;
569
743
  conversation.prompted = running.sessionId;
570
744
  running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
745
+ this.took(turn);
571
746
  const notice = answeredFailure(answer);
572
747
  if (notice !== null) {
573
748
  this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
@@ -580,6 +755,7 @@ export class Conversations {
580
755
  this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
581
756
  }
582
757
  else {
758
+ this.took(turn);
583
759
  if (conversation.open === 1 && conversation.running === running) {
584
760
  running.adapter.retire();
585
761
  this.detach(conversation, running);
@@ -589,7 +765,9 @@ export class Conversations {
589
765
  });
590
766
  }
591
767
  started(conversation) {
592
- return conversation.turn ?? this.begin(conversation);
768
+ const turn = conversation.turn ?? this.begin(conversation);
769
+ this.took(turn);
770
+ return turn;
593
771
  }
594
772
  ended(conversation) {
595
773
  const turn = conversation.turn;
@@ -684,6 +862,11 @@ export class Conversations {
684
862
  }
685
863
  this.apply(conversation, this.started(conversation), event);
686
864
  }
865
+ took(turn) {
866
+ const taken = turn.taken;
867
+ turn.taken = null;
868
+ taken?.();
869
+ }
687
870
  apply(conversation, turn, event) {
688
871
  switch (event.kind) {
689
872
  case 'text':
@@ -738,13 +921,16 @@ export class Conversations {
738
921
  bytes: bytes.length,
739
922
  sha256: createHash('sha256').update(bytes).digest('hex'),
740
923
  }));
741
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
742
- if (saved.save.status !== 'saved')
743
- throw new Error(saved.save.failure ?? saved.save.status);
744
- 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);
745
930
  }
746
931
  catch (error) {
747
- 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);
748
934
  }
749
935
  }
750
936
  }