@agentshouse/kit 0.2.1-c84a4fccf2835 → 0.2.1-c8850697c8230

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 (140) hide show
  1. package/README.md +28 -28
  2. package/dist/acp.js +90 -19
  3. package/dist/agents.js +86 -38
  4. package/dist/api.js +5 -5
  5. package/dist/authority-main.js +2 -2
  6. package/dist/bridge.js +1 -1
  7. package/dist/building.js +46 -0
  8. package/dist/calls.js +71 -0
  9. package/dist/clis.js +58 -22
  10. package/dist/{stream.js → connection.js} +45 -52
  11. package/dist/conversations.js +429 -147
  12. package/dist/copies.js +53 -51
  13. package/dist/end.js +14 -4
  14. package/dist/events.js +29 -8
  15. package/dist/files.js +139 -37
  16. package/dist/git.js +3 -3
  17. package/dist/home.js +0 -7
  18. package/dist/house-cli.js +95 -87
  19. package/dist/house-main.js +3 -3
  20. package/dist/instructions.js +29 -9
  21. package/dist/kit-cli.js +4 -4
  22. package/dist/kit-main.js +3 -3
  23. package/dist/login.js +4 -4
  24. package/dist/mcp.js +6 -2
  25. package/dist/placement.js +11 -2
  26. package/dist/relay.js +21 -0
  27. package/dist/resident.js +46 -38
  28. package/dist/scope.js +15 -4
  29. package/dist/scripts.js +255 -0
  30. package/dist/service.js +23 -1
  31. package/dist/sign-in.js +22 -21
  32. package/dist/silence.js +28 -0
  33. package/dist/skills.js +42 -97
  34. package/dist/transcription.js +83 -43
  35. package/dist/translate.js +2 -8
  36. package/dist/update.js +79 -37
  37. package/dist/wire.js +19 -3
  38. package/package.json +20 -21
  39. package/dist/bootstrap.json +0 -24
  40. package/dist/clis-main.js +0 -23
  41. package/dist/inputs.js +0 -37
  42. package/how-we-work.md +0 -33
  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 -97
  46. package/skills/ask-matt/agents/openai.yaml +0 -5
  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 -114
  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 -138
  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/GLOSSARY-FORMAT.md +0 -60
  61. package/skills/domain-modeling/LICENSE +0 -21
  62. package/skills/domain-modeling/SKILL.md +0 -74
  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 -7
  66. package/skills/grill-me/agents/openai.yaml +0 -5
  67. package/skills/grill-with-docs/LICENSE +0 -21
  68. package/skills/grill-with-docs/SKILL.md +0 -7
  69. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  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 -16
  75. package/skills/handoff/agents/openai.yaml +0 -5
  76. package/skills/implement/LICENSE +0 -21
  77. package/skills/implement/SKILL.md +0 -17
  78. package/skills/implement/agents/openai.yaml +0 -5
  79. package/skills/implement-spec/LICENSE +0 -21
  80. package/skills/implement-spec/SKILL.md +0 -40
  81. package/skills/implement-spec/agents/openai.yaml +0 -5
  82. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  83. package/skills/improve-codebase-architecture/LICENSE +0 -21
  84. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  85. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  86. package/skills/pr/CREDITS.md +0 -3
  87. package/skills/pr/LICENSE +0 -21
  88. package/skills/pr/SKILL.md +0 -170
  89. package/skills/pr/agents/openai.yaml +0 -3
  90. package/skills/prototype/LICENSE +0 -21
  91. package/skills/prototype/LOGIC.md +0 -67
  92. package/skills/prototype/SKILL.md +0 -26
  93. package/skills/prototype/UI.md +0 -112
  94. package/skills/prototype/agents/openai.yaml +0 -3
  95. package/skills/research/LICENSE +0 -21
  96. package/skills/research/SKILL.md +0 -12
  97. package/skills/research/agents/openai.yaml +0 -3
  98. package/skills/retro/LICENSE +0 -21
  99. package/skills/retro/SKILL.md +0 -44
  100. package/skills/retro/agents/openai.yaml +0 -5
  101. package/skills/tdd/LICENSE +0 -21
  102. package/skills/tdd/SKILL.md +0 -38
  103. package/skills/tdd/agents/openai.yaml +0 -3
  104. package/skills/tdd/mocking.md +0 -59
  105. package/skills/tdd/tests.md +0 -77
  106. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  107. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  108. package/skills/teach/LICENSE +0 -21
  109. package/skills/teach/MISSION-FORMAT.md +0 -31
  110. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  111. package/skills/teach/SKILL.md +0 -140
  112. package/skills/teach/agents/openai.yaml +0 -5
  113. package/skills/to-questionnaire/LICENSE +0 -21
  114. package/skills/to-questionnaire/SKILL.md +0 -54
  115. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  116. package/skills/to-spec/LICENSE +0 -21
  117. package/skills/to-spec/SKILL.md +0 -75
  118. package/skills/to-spec/agents/openai.yaml +0 -5
  119. package/skills/to-tickets/LICENSE +0 -21
  120. package/skills/to-tickets/SKILL.md +0 -105
  121. package/skills/to-tickets/agents/openai.yaml +0 -5
  122. package/skills/triage/AGENT-BRIEF.md +0 -207
  123. package/skills/triage/LICENSE +0 -21
  124. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  125. package/skills/triage/SKILL.md +0 -112
  126. package/skills/triage/agents/openai.yaml +0 -5
  127. package/skills/wait-what/LICENSE +0 -21
  128. package/skills/wait-what/SKILL.md +0 -7
  129. package/skills/wait-what/agents/openai.yaml +0 -5
  130. package/skills/wayfinder/LICENSE +0 -21
  131. package/skills/wayfinder/SKILL.md +0 -128
  132. package/skills/wayfinder/agents/openai.yaml +0 -5
  133. package/skills/wizard/LICENSE +0 -21
  134. package/skills/wizard/SKILL.md +0 -44
  135. package/skills/wizard/agents/openai.yaml +0 -3
  136. package/skills/wizard/template.sh +0 -223
  137. package/skills/writing-for-agents/LICENSE +0 -21
  138. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  139. package/skills/writing-for-agents/SKILL.md +0 -81
  140. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -1,25 +1,25 @@
1
- import { client, RequestError, } from '@agentclientprotocol/sdk';
1
+ import { client, } 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
- import { startAdapter } from './acp.js';
7
- import { finalRefusal } from './api.js';
6
+ import { signedOut, startAdapter } from './acp.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
- const SIGNED_OUT = RequestError.authRequired().code;
22
21
  const KILLED = 'the conversation was killed';
22
+ const INTERRUPTED = 'the conversation was interrupted';
23
23
  const REFUSED = 'the CLI refused the prompt';
24
24
  const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
25
25
  const GONE = 'House no longer holds this conversation; start a new one';
@@ -31,9 +31,13 @@ const ENDED = {
31
31
  const NO_START = "the Agent's CLI did not start";
32
32
  const NO_SESSION = "the Agent's CLI did not open a session";
33
33
  const NO_RESUME = "the Agent's CLI could not resume this conversation";
34
+ const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
34
35
  const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
36
+ const UNSTEERED = "the Agent's CLI did not take the message into its running turn";
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,11 @@ async function step(reason, work) {
42
46
  throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
43
47
  }
44
48
  }
49
+ function unopened(reason) {
50
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
51
+ }
52
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
53
+ const STARTING_SHOWN_AFTER_MS = 1_000;
45
54
  function unbridged(error) {
46
55
  return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
47
56
  }
@@ -53,40 +62,57 @@ const EXIT_PLAN_MODE = {
53
62
  return value;
54
63
  },
55
64
  };
56
- export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
65
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
57
66
  function causeOf(error) {
58
67
  return error instanceof Error ? error.message : String(error);
59
68
  }
60
69
  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) };
70
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
63
71
  }
64
72
  function exitedWith(running, cause) {
65
- return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
73
+ if (running.killed)
74
+ return { failure: 'killed', cause: KILLED };
75
+ return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
66
76
  }
67
77
  function answerMessage(request, response) {
68
78
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
69
79
  }
80
+ async function sentPart(url, bytes, headers) {
81
+ const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
82
+ await answer.arrayBuffer();
83
+ if (!answer.ok)
84
+ throw storeRefusal('upload', answer.status);
85
+ }
70
86
  function logged(error) {
71
87
  process.stderr.write(`kit: ${causeOf(error)}\n`);
72
88
  }
73
89
  function refusal(conversation, error) {
74
90
  if (conversation.kills > 0)
75
91
  return KILLED;
76
- const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
92
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
77
93
  logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
78
94
  return causeOf(error);
79
95
  }
96
+ function audioOf(result) {
97
+ if (result.isError === true)
98
+ return null;
99
+ const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
100
+ return file?.media_type?.startsWith('audio/') === true ? file : null;
101
+ }
80
102
  function marker(bridge) {
81
103
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
82
104
  }
105
+ function steers(adapter) {
106
+ return adapter.initialized._meta?.steering?.supported === true;
107
+ }
83
108
  class Turn {
84
109
  id = randomUUID();
85
110
  parts;
86
- admitted = false;
87
111
  questions = 0;
88
112
  secrets = 0;
89
113
  prompted = false;
114
+ steered = false;
115
+ taken = null;
90
116
  ended = false;
91
117
  images = 0;
92
118
  uploads = [];
@@ -100,21 +126,31 @@ class Conversation {
100
126
  session = null;
101
127
  turn = null;
102
128
  commands = null;
129
+ options = null;
130
+ exitUntold = false;
103
131
  context = null;
104
132
  leftovers = new Set();
105
133
  waiting = [];
134
+ done = false;
106
135
  sending = null;
107
136
  subagents = new Map();
108
137
  held = [];
109
138
  opening = null;
139
+ bridge = null;
140
+ launch = null;
110
141
  prompted = null;
111
142
  kills = 0;
112
143
  killed = new AbortController();
113
144
  interrupts = 0;
114
145
  interrupted = new AbortController();
115
- open = 0;
146
+ turns = new Set();
116
147
  queue = Promise.resolve();
117
148
  reports = Promise.resolve();
149
+ starting = null;
150
+ preparing = null;
151
+ told = null;
152
+ untold = false;
153
+ shown = null;
118
154
  constructor(id) {
119
155
  this.id = id;
120
156
  }
@@ -138,23 +174,23 @@ export class Conversations {
138
174
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
139
175
  setInterval(() => this.look(), 2_000);
140
176
  }
141
- input(input) {
142
- void this.kit.inputs.carry(input.input_id, () => {
143
- const conversation = this.conversation(input.conversation_id);
144
- if (input.kind === 'kill') {
177
+ call(call) {
178
+ void this.kit.calls.carry(call.input_id, () => {
179
+ const conversation = this.conversation(call.conversation_id);
180
+ if (call.kind === 'kill') {
145
181
  conversation.kills++;
146
182
  conversation.killed.abort(new Error(KILLED));
147
183
  for (const waiting of conversation.waiting.splice(0))
148
184
  waiting.settle({ refused: KILLED });
149
185
  this.halt(conversation);
150
186
  }
151
- if (input.kind === 'interrupt') {
187
+ if (call.kind === 'interrupt') {
152
188
  conversation.interrupts++;
153
189
  conversation.interrupted.abort();
154
190
  }
155
- const handled = conversation.queue.then(() => this.carry(conversation, input));
191
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
156
192
  conversation.queue = handled.catch(() => undefined);
157
- return handled.then(async ({ ack }) => ({ ack: await ack }));
193
+ return handled.then(async ({ result }) => ({ result: await result }));
158
194
  });
159
195
  }
160
196
  idle() {
@@ -163,7 +199,7 @@ export class Conversations {
163
199
  for (const conversation of this.conversations.values()) {
164
200
  const turn = conversation.turn;
165
201
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
166
- if (conversation.open > (asking ? 1 : 0))
202
+ if (conversation.turns.size > (asking ? 1 : 0))
167
203
  return false;
168
204
  }
169
205
  const conversations = [...this.conversations.values()];
@@ -172,7 +208,7 @@ export class Conversations {
172
208
  return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
173
209
  }
174
210
  quiet() {
175
- return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
211
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
176
212
  }
177
213
  look() {
178
214
  for (const conversation of this.conversations.values()) {
@@ -187,6 +223,16 @@ export class Conversations {
187
223
  }
188
224
  opened() {
189
225
  this.watched = false;
226
+ for (const conversation of this.conversations.values()) {
227
+ if (conversation.running !== null) {
228
+ this.facts(conversation);
229
+ this.subagentsChanged(conversation);
230
+ }
231
+ else if (conversation.exitUntold)
232
+ this.process(conversation, false);
233
+ if (conversation.untold || conversation.told !== null)
234
+ this.retell(conversation);
235
+ }
190
236
  }
191
237
  watching(watched) {
192
238
  const resend = watched && !this.watched;
@@ -206,134 +252,320 @@ export class Conversations {
206
252
  }
207
253
  return conversation;
208
254
  }
209
- async carry(conversation, input) {
210
- if (input.kind === 'open')
211
- return { ack: Promise.resolve(await this.open(conversation, input)) };
212
- if (input.kind === 'message')
213
- return this.message(conversation, input);
214
- if (input.kind === 'interrupt') {
255
+ async carry(conversation, call) {
256
+ if (call.kind === 'open')
257
+ return this.open(conversation, call);
258
+ if (call.kind === 'message')
259
+ return this.message(conversation, call);
260
+ if (call.kind === 'done') {
261
+ conversation.done = true;
262
+ this.next(conversation);
263
+ return { result: Promise.resolve({}) };
264
+ }
265
+ await this.launched(conversation);
266
+ if (call.kind === 'interrupt') {
215
267
  if (--conversation.interrupts === 0)
216
268
  conversation.interrupted = new AbortController();
217
- await this.interrupt(conversation, String(input.turn_id));
269
+ await this.interrupt(conversation, String(call.turn_id));
218
270
  }
219
- if (input.kind === 'kill') {
271
+ if (call.kind === 'kill') {
220
272
  await this.kill(conversation);
221
273
  await conversation.sending;
222
274
  if (--conversation.kills === 0)
223
275
  conversation.killed = new AbortController();
224
276
  }
225
- if (input.kind === 'option')
226
- await this.option(conversation, String(input.option), input.value);
227
- if (input.kind === 'answer') {
228
- const question = this.questions.get(String(input.interaction_id));
277
+ if (call.kind === 'option')
278
+ await this.option(conversation, String(call.option), call.value);
279
+ if (call.kind === 'answer') {
280
+ const question = this.questions.get(String(call.interaction_id));
229
281
  if (question === undefined) {
230
- const text = answerMessage(input.request, input.response);
231
- return this.message(conversation, { ...input, text, files: [], first: false });
282
+ const text = answerMessage(call.request, call.response);
283
+ return this.message(conversation, { ...call, text, first: false, message_id: null });
232
284
  }
233
- question.answer(input.response);
285
+ question.answer(call.response);
234
286
  }
235
- return { ack: Promise.resolve({}) };
287
+ return { result: Promise.resolve({}) };
288
+ }
289
+ async open(conversation, call) {
290
+ await this.stop(conversation);
291
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
292
+ return {
293
+ result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
294
+ };
295
+ }
296
+ async stop(conversation) {
297
+ const running = conversation.running;
298
+ running?.scope.kill();
299
+ await running?.adapter.exited;
300
+ }
301
+ launch(conversation, route, sessionId, empty) {
302
+ let opened;
303
+ let failed;
304
+ const bridge = new Promise((resolve, reject) => {
305
+ opened = resolve;
306
+ failed = reject;
307
+ });
308
+ bridge.catch(() => undefined);
309
+ const session = this.start(conversation, route, sessionId, empty, opened);
310
+ session.catch(failed);
311
+ const launch = { bridge, session };
312
+ conversation.launch = launch;
313
+ void session
314
+ .catch(() => undefined)
315
+ .then(() => {
316
+ if (conversation.launch !== launch)
317
+ return;
318
+ conversation.launch = null;
319
+ this.closeIdleBridge(conversation);
320
+ this.next(conversation);
321
+ });
322
+ this.next(conversation);
323
+ return launch;
324
+ }
325
+ async launched(conversation) {
326
+ while (conversation.launch !== null)
327
+ await conversation.launch.session.catch(() => undefined);
328
+ }
329
+ instructed(conversation, route) {
330
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
331
+ if (bridge === undefined)
332
+ return null;
333
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
334
+ read.catch(() => undefined);
335
+ return read;
236
336
  }
237
- async open(conversation, input) {
238
- if (conversation.running !== null)
239
- return {};
337
+ async message(conversation, call) {
338
+ const content = [
339
+ ...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
340
+ { type: 'text', text: String(call.text) },
341
+ ];
342
+ let route;
343
+ let instructing = null;
240
344
  try {
241
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
345
+ route = await this.route(conversation, call.agent_id);
346
+ if (call.first === true)
347
+ instructing = this.instructed(conversation, route);
348
+ if (call.message_id !== null)
349
+ content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
350
+ if (conversation.kills > 0)
351
+ throw new Error(KILLED);
242
352
  }
243
353
  catch (error) {
244
- return { refused: refusal(conversation, error) };
354
+ this.closeIdleBridge(conversation);
355
+ this.prepares(conversation, null);
356
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
245
357
  }
358
+ conversation.preparing = null;
359
+ const { promise, resolve: settle } = Promise.withResolvers();
360
+ const waiting = { call, route, content, instructing, settle };
361
+ if (call.at_once === true && conversation.turns.size > 0)
362
+ await this.atOnce(conversation, waiting);
363
+ else
364
+ conversation.waiting.push(waiting);
365
+ this.next(conversation);
366
+ return { result: promise };
246
367
  }
247
- async message(conversation, input) {
248
- const content = [{ type: 'text', text: String(input.text) }];
249
- let route;
368
+ async atOnce(conversation, waiting) {
369
+ while (conversation.sending !== null)
370
+ await conversation.sending;
371
+ const running = conversation.running;
372
+ if (running !== null && conversation.turns.size > 0 && steers(running.adapter)) {
373
+ conversation.sending = this.steer(conversation, running, waiting);
374
+ await conversation.sending;
375
+ conversation.sending = null;
376
+ return;
377
+ }
378
+ this.ahead(conversation, waiting);
379
+ if (conversation.turns.size > 0)
380
+ await this.cancel(conversation);
381
+ }
382
+ ahead(conversation, waiting) {
383
+ const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
384
+ conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
385
+ }
386
+ async steer(conversation, running, waiting) {
387
+ let outcome;
250
388
  try {
251
- route = await this.route(conversation, input.agent_id);
252
- const files = input.files;
253
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
254
- const lines = await this.transcribed(conversation, files, paths);
255
- if (lines.length > 0)
256
- content.push({ type: 'text', text: lines.join('\n') });
257
- if (conversation.kills > 0)
258
- throw new Error(KILLED);
389
+ ({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
390
+ sessionId: running.sessionId,
391
+ prompt: waiting.content,
392
+ _meta: { steering: { idleBehavior: 'promptRequired' } },
393
+ }));
259
394
  }
260
395
  catch (error) {
261
- return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
396
+ outcome = error;
397
+ }
398
+ if (outcome === 'promptRequired') {
399
+ this.ahead(conversation, waiting);
400
+ }
401
+ else if (outcome === 'injected' || outcome === 'startedNewTurn') {
402
+ if (outcome === 'injected') {
403
+ for (const turn of conversation.turns)
404
+ turn.steered ||= turn.prompted;
405
+ }
406
+ this.kit.calls.keepResult(String(waiting.call.input_id), {});
407
+ waiting.settle({});
408
+ }
409
+ else {
410
+ const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
411
+ waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
262
412
  }
263
- return {
264
- ack: new Promise((settle) => {
265
- conversation.waiting.push({ input, route, content, settle });
266
- this.next(conversation);
267
- }),
268
- };
269
413
  }
270
414
  next(conversation) {
271
- if (conversation.sending !== null || conversation.open > 0)
415
+ if (conversation.sending !== null || conversation.turns.size > 0)
272
416
  return;
273
417
  const waiting = conversation.waiting.shift();
274
- if (waiting === undefined)
418
+ if (waiting === undefined) {
419
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
420
+ if (conversation.starting !== step)
421
+ this.starts(conversation, step);
422
+ if (conversation.done && conversation.launch === null)
423
+ void this.stop(conversation).catch(logged);
275
424
  return;
425
+ }
276
426
  conversation.sending = this.send(conversation, waiting).finally(() => {
277
427
  conversation.sending = null;
278
428
  this.next(conversation);
279
429
  });
280
430
  }
281
431
  async send(conversation, waiting) {
282
- const { input } = waiting;
283
- let ack = {};
432
+ const { call } = waiting;
433
+ let result = {};
284
434
  let started = false;
435
+ let instructing = waiting.instructing;
285
436
  const prompt = [];
286
437
  try {
287
438
  if (conversation.running === null) {
288
- const stored = conversation.session ?? input.provider_session_id;
289
- const empty = input.first === true && conversation.prompted !== stored;
290
- started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
439
+ this.starts(conversation, 'session');
440
+ await this.launched(conversation);
441
+ }
442
+ if (conversation.running === null) {
443
+ const stored = conversation.session ?? call.provider_session_id;
444
+ const empty = call.first === true && conversation.prompted !== stored;
445
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
446
+ if (call.first === true)
447
+ instructing = this.instructed(conversation, waiting.route);
448
+ started = (await launch.session) !== stored;
291
449
  }
292
450
  const session = conversation.running.sessionId;
293
- if (started || (input.first === true && session !== input.provider_session_id)) {
294
- ack = { provider_session_id: session };
451
+ if (started || (call.first === true && session !== call.provider_session_id)) {
452
+ result = { provider_session_id: session };
295
453
  }
296
- if (input.first === true) {
297
- prompt.push({
298
- type: 'text',
299
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
300
- });
454
+ if (call.first === true) {
455
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
301
456
  }
302
457
  if (conversation.kills > 0)
303
458
  throw new Error(KILLED);
304
459
  }
305
460
  catch (error) {
461
+ this.starts(conversation, null);
306
462
  if (started) {
307
463
  conversation.session = null;
308
464
  await this.kill(conversation);
309
465
  }
310
- waiting.settle({ refused: refusal(conversation, error) });
466
+ const refused = refusal(conversation, error);
467
+ const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
468
+ waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
311
469
  return;
312
470
  }
313
- this.prompt(conversation, [...prompt, ...waiting.content]);
314
- waiting.settle(ack);
471
+ this.starts(conversation, null);
472
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
473
+ waiting.settle(result);
315
474
  }
316
- async transcribed(conversation, files, paths) {
317
- const lines = [];
318
- for (const [index, path] of paths.entries()) {
319
- lines.push(path);
320
- if (files[index].media_type?.startsWith('audio/') !== true)
321
- continue;
322
- try {
323
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
324
- if (text !== null && text !== '')
325
- lines.push(`Transcript: ${text}`);
326
- }
327
- catch (error) {
328
- if (conversation.kills > 0)
329
- throw error;
330
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
475
+ starts(conversation, step) {
476
+ conversation.starting = step;
477
+ if (conversation.shown !== null)
478
+ clearTimeout(conversation.shown);
479
+ conversation.shown = null;
480
+ if (step === null || conversation.told !== null) {
481
+ this.tell(conversation, step);
482
+ return;
483
+ }
484
+ conversation.shown = setTimeout(() => {
485
+ conversation.shown = null;
486
+ if (conversation.starting === step)
487
+ this.tell(conversation, step);
488
+ }, STARTING_SHOWN_AFTER_MS);
489
+ }
490
+ prepares(conversation, step) {
491
+ conversation.preparing = step;
492
+ this.next(conversation);
493
+ }
494
+ tell(conversation, step) {
495
+ if (conversation.told === step && !conversation.untold)
496
+ return;
497
+ conversation.told = step;
498
+ this.retell(conversation);
499
+ }
500
+ retell(conversation) {
501
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
502
+ }
503
+ async transcribed(conversation, messageId, text) {
504
+ const paths = linkedPaths(text);
505
+ if (paths.length === 0)
506
+ return [];
507
+ const killed = conversation.killed.signal;
508
+ const interrupted = conversation.interrupted.signal;
509
+ const transcribing = AbortSignal.any([killed, interrupted]);
510
+ let signal = killed;
511
+ const blocks = [];
512
+ try {
513
+ const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
514
+ const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
515
+ for (const path of paths) {
516
+ let answered = await read(path);
517
+ if (answered === null)
518
+ continue;
519
+ signal = transcribing;
520
+ this.prepares(conversation, 'transcription');
521
+ const said = await transcribe(async () => {
522
+ const address = answered ?? (await read(path));
523
+ answered = null;
524
+ if (address === null)
525
+ throw new Error(`${path} is no longer an audio file`);
526
+ return address;
527
+ }, signal);
528
+ const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
529
+ await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
530
+ blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
331
531
  }
332
532
  }
333
- return lines;
533
+ catch (error) {
534
+ if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
535
+ throw new NotStarted(INTERRUPTED, { cause: error });
536
+ throw error;
537
+ }
538
+ return blocks;
539
+ }
540
+ bridged(conversation) {
541
+ if (conversation.bridge === null) {
542
+ const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
543
+ conversation.bridge = bridging;
544
+ bridging.catch(() => {
545
+ if (conversation.bridge === bridging)
546
+ conversation.bridge = null;
547
+ });
548
+ }
549
+ return conversation.bridge;
550
+ }
551
+ closeBridge(conversation, bridging, bridge) {
552
+ bridge.close();
553
+ if (conversation.bridge === bridging)
554
+ conversation.bridge = null;
555
+ }
556
+ closeIdleBridge(conversation) {
557
+ const bridging = conversation.bridge;
558
+ if (bridging === null || conversation.running !== null || conversation.launch !== null)
559
+ return;
560
+ conversation.bridge = null;
561
+ void bridging.then((bridge) => bridge.close(), () => undefined);
334
562
  }
335
563
  async interrupt(conversation, turnId) {
336
- if (conversation.running === null || conversation.turn?.id !== turnId)
564
+ if ([...conversation.turns].some((turn) => turn.id === turnId))
565
+ await this.cancel(conversation);
566
+ }
567
+ async cancel(conversation) {
568
+ if (conversation.running === null)
337
569
  return;
338
570
  await conversation.running.adapter.connection.agent.notify('session/cancel', {
339
571
  sessionId: conversation.running.sessionId,
@@ -385,7 +617,8 @@ export class Conversations {
385
617
  }
386
618
  return route;
387
619
  }
388
- async start(conversation, route, sessionId, empty) {
620
+ async start(conversation, routed, sessionId, empty, bridged) {
621
+ const route = await routed;
389
622
  const reader = readerFor(route.kind);
390
623
  reader.session = sessionId;
391
624
  const app = client({ name: '@agentshouse/kit' })
@@ -393,50 +626,51 @@ export class Conversations {
393
626
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
394
627
  .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
395
628
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
396
- const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
629
+ const bridging = this.bridged(conversation);
630
+ const bridge = await step(unbridged, bridging);
397
631
  if (conversation.kills > 0) {
398
- bridge.close();
632
+ this.closeBridge(conversation, bridging, bridge);
399
633
  throw new Error(KILLED);
400
634
  }
401
635
  conversation.opening = bridge;
402
636
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
403
637
  const scope = new Scope(marker(bridge), randomUUID(), cli);
638
+ let adapter = null;
404
639
  try {
405
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
640
+ bridged(bridge);
406
641
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
407
642
  const streamed = await step(NO_CONNECTION, work.open(cli));
408
- const adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
643
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
409
644
  work.read(message);
410
645
  this.read(conversation, reader, message);
411
646
  }, { ...bridge.env, ...streamed }, undefined, scope));
412
- void adapter.exited.then(() => bridge.close());
647
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
413
648
  const agent = adapter.connection.agent;
414
649
  const cwd = route.working_directory;
415
650
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
416
651
  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 })));
652
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
418
653
  reader.session = opened.sessionId;
419
654
  if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
420
655
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
421
- await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
656
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
422
657
  }
423
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
658
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
424
659
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
425
660
  void work.settled().then(() => scope.record());
426
661
  conversation.running = running;
662
+ adapter.turn(conversation.turns.size > 0);
427
663
  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);
432
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
664
+ conversation.options = options;
665
+ this.facts(conversation);
666
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
433
667
  return running.sessionId;
434
668
  }
435
669
  catch (error) {
436
- scope.kill();
670
+ adapter?.retire();
437
671
  work.close();
438
672
  conversation.leftovers.add(scope);
439
- bridge.close();
673
+ this.closeBridge(conversation, bridging, bridge);
440
674
  throw conversation.kills > 0 ? new Error(KILLED) : error;
441
675
  }
442
676
  finally {
@@ -449,7 +683,7 @@ export class Conversations {
449
683
  }
450
684
  catch (error) {
451
685
  if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
452
- throw new NotStarted(NO_RESUME, { cause: error });
686
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
453
687
  }
454
688
  logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
455
689
  return null;
@@ -476,10 +710,7 @@ export class Conversations {
476
710
  }
477
711
  return options;
478
712
  }
479
- exited(conversation, adapter, cause) {
480
- const running = conversation.running;
481
- if (running?.adapter !== adapter)
482
- return;
713
+ exited(conversation, running, cause) {
483
714
  this.detach(conversation, running);
484
715
  if (conversation.turn !== null) {
485
716
  this.end(conversation, conversation.turn, exitedWith(running, cause));
@@ -488,15 +719,36 @@ export class Conversations {
488
719
  this.kit.changed();
489
720
  }
490
721
  detach(conversation, running) {
722
+ if (conversation.bridge === running.bridging)
723
+ conversation.bridge = null;
491
724
  running.work.close();
492
725
  conversation.leftovers.add(running.scope);
493
726
  conversation.running = null;
494
727
  conversation.commands = null;
728
+ conversation.options = null;
729
+ if (conversation.subagents.size > 0) {
730
+ conversation.subagents.clear();
731
+ this.subagentsChanged(conversation);
732
+ }
495
733
  for (const [id, question] of this.questions) {
496
734
  if (question.conversation === conversation)
497
735
  this.questions.delete(id);
498
736
  }
499
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
737
+ this.process(conversation, false);
738
+ }
739
+ process(conversation, running) {
740
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
741
+ }
742
+ facts(conversation) {
743
+ this.process(conversation, true);
744
+ if (conversation.commands !== null)
745
+ this.commands(conversation, conversation.commands);
746
+ if (conversation.options !== null)
747
+ this.options(conversation, conversation.options);
748
+ }
749
+ subagentsChanged(conversation) {
750
+ const subagents = [...conversation.subagents.values()].map(({ description, activity }) => ({ description, activity }));
751
+ this.kit.send({ type: 'subagents', conversation_id: conversation.id, subagents });
500
752
  }
501
753
  commands(conversation, commands) {
502
754
  conversation.commands = commands;
@@ -507,6 +759,7 @@ export class Conversations {
507
759
  options(conversation, options) {
508
760
  if (conversation.running === null)
509
761
  return;
762
+ conversation.options = options;
510
763
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
511
764
  }
512
765
  deliver(conversation, turn, event, body) {
@@ -521,7 +774,7 @@ export class Conversations {
521
774
  return delivered;
522
775
  }
523
776
  write(conversation, turn, operation) {
524
- if (!turn.admitted || turn.ended || !this.watched)
777
+ if (turn.ended || !this.watched)
525
778
  return;
526
779
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
527
780
  }
@@ -531,13 +784,9 @@ export class Conversations {
531
784
  }
532
785
  begin(conversation) {
533
786
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
534
- conversation.open++;
535
- void this.deliver(conversation, turn, 'started', {})
536
- .then(() => {
537
- turn.admitted = true;
538
- this.replay(conversation, turn);
539
- })
540
- .catch(logged);
787
+ conversation.turns.add(turn);
788
+ conversation.running?.adapter.turn(true);
789
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
541
790
  conversation.turn = turn;
542
791
  if (conversation.context !== null)
543
792
  turn.parts.measure(conversation.context);
@@ -558,7 +807,8 @@ export class Conversations {
558
807
  if (notice !== null)
559
808
  turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
560
809
  turn.ended = true;
561
- conversation.open--;
810
+ conversation.turns.delete(turn);
811
+ conversation.running?.adapter.turn(conversation.turns.size > 0);
562
812
  if (conversation.turn === turn)
563
813
  conversation.turn = null;
564
814
  const body = Promise.allSettled(turn.uploads).then(() => ({
@@ -570,12 +820,14 @@ export class Conversations {
570
820
  this.next(conversation);
571
821
  this.kit.changed();
572
822
  }
573
- prompt(conversation, prompt) {
823
+ prompt(conversation, prompt, taken) {
574
824
  const running = conversation.running;
575
825
  const turn = this.begin(conversation);
576
826
  turn.prompted = true;
827
+ turn.taken = taken;
577
828
  conversation.prompted = running.sessionId;
578
829
  running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
830
+ this.took(turn);
579
831
  const notice = answeredFailure(answer);
580
832
  if (notice !== null) {
581
833
  this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
@@ -588,8 +840,9 @@ export class Conversations {
588
840
  this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
589
841
  }
590
842
  else {
591
- if (conversation.open === 1 && conversation.running === running) {
592
- running.scope.kill();
843
+ this.took(turn);
844
+ if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
845
+ running.adapter.retire();
593
846
  this.detach(conversation, running);
594
847
  }
595
848
  this.end(conversation, turn, answeredWith(error));
@@ -597,21 +850,26 @@ export class Conversations {
597
850
  });
598
851
  }
599
852
  started(conversation) {
600
- return conversation.turn ?? this.begin(conversation);
853
+ const turn = conversation.turn ?? this.begin(conversation);
854
+ this.took(turn);
855
+ return turn;
601
856
  }
602
- ended(conversation) {
857
+ ended(conversation, stop) {
603
858
  const turn = conversation.turn;
604
859
  if (turn === null)
605
860
  return;
606
- if (!turn.prompted) {
607
- this.end(conversation, turn, null);
608
- }
609
- else {
610
- for (const event of conversation.running?.reader.closeTurn() ?? [])
611
- this.apply(conversation, turn, event);
612
- conversation.turn = null;
613
- this.kit.changed();
861
+ const held = stop !== null && !turn.steered && [...conversation.subagents.values()].some((subagent) => subagent.turn === turn);
862
+ if (!turn.prompted || held) {
863
+ this.took(turn);
864
+ this.end(conversation, turn, stop === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
865
+ return;
614
866
  }
867
+ if (stop !== null)
868
+ return;
869
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
870
+ this.apply(conversation, turn, event);
871
+ conversation.turn = null;
872
+ this.kit.changed();
615
873
  }
616
874
  async ask(conversation, method, params, secret, signal) {
617
875
  const turn = this.started(conversation);
@@ -667,7 +925,7 @@ export class Conversations {
667
925
  this.started(conversation);
668
926
  return;
669
927
  case 'ended':
670
- this.ended(conversation);
928
+ this.ended(conversation, event.stop);
671
929
  return;
672
930
  case 'commands':
673
931
  this.commands(conversation, event.commands);
@@ -682,6 +940,9 @@ export class Conversations {
682
940
  case 'subagent-done':
683
941
  this.subagentDone(conversation, event.id, event.result);
684
942
  return;
943
+ case 'subagent-activity':
944
+ this.subagentActivity(conversation, event.id, event.activity);
945
+ return;
685
946
  case 'marker':
686
947
  case 'image':
687
948
  if (conversation.turn !== null)
@@ -692,6 +953,11 @@ export class Conversations {
692
953
  }
693
954
  this.apply(conversation, this.started(conversation), event);
694
955
  }
956
+ took(turn) {
957
+ const taken = turn.taken;
958
+ turn.taken = null;
959
+ taken?.();
960
+ }
695
961
  apply(conversation, turn, event) {
696
962
  switch (event.kind) {
697
963
  case 'text':
@@ -703,7 +969,8 @@ export class Conversations {
703
969
  return;
704
970
  case 'subagent': {
705
971
  const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
706
- conversation.subagents.set(event.id, { description: event.description, turn, index });
972
+ conversation.subagents.set(event.id, { description: event.description, turn, index, activity: null });
973
+ this.subagentsChanged(conversation);
707
974
  return;
708
975
  }
709
976
  case 'message':
@@ -726,6 +993,7 @@ export class Conversations {
726
993
  if (subagent === undefined)
727
994
  return;
728
995
  conversation.subagents.delete(id);
996
+ this.subagentsChanged(conversation);
729
997
  if (subagent.turn === conversation.turn) {
730
998
  subagent.turn.parts.done(subagent.index, result);
731
999
  return;
@@ -736,23 +1004,37 @@ export class Conversations {
736
1004
  else
737
1005
  conversation.turn.parts.start(part);
738
1006
  }
1007
+ subagentActivity(conversation, id, activity) {
1008
+ const subagent = conversation.subagents.get(id);
1009
+ if (subagent === undefined || subagent.activity === activity)
1010
+ return;
1011
+ subagent.activity = activity;
1012
+ this.subagentsChanged(conversation);
1013
+ }
739
1014
  async image(conversation, turn, reserved, data, mediaType) {
740
1015
  const bytes = Buffer.from(data, 'base64');
741
1016
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
1017
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
742
1018
  try {
743
1019
  const declared = (await this.deliver(conversation, turn, 'images', {
744
1020
  name,
745
1021
  media_type: mediaType,
746
1022
  bytes: bytes.length,
747
- sha256: createHash('sha256').update(bytes).digest('hex'),
1023
+ sha256,
748
1024
  }));
749
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
750
- if (saved.save.status !== 'saved')
751
- throw new Error(saved.save.failure ?? saved.save.status);
752
- turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
1025
+ const saved = await uploaded(declared.upload, {
1026
+ name,
1027
+ bytes: bytes.length,
1028
+ sha256,
1029
+ read: (start, end) => [bytes.subarray(start, end)],
1030
+ send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
1031
+ finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
1032
+ });
1033
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
753
1034
  }
754
1035
  catch (error) {
755
- turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
1036
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
1037
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
756
1038
  }
757
1039
  }
758
1040
  }