@agentshouse/kit 0.2.1-cf110d15f69c8 → 0.2.1-cf268dff1869b

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 (128) hide show
  1. package/README.md +38 -37
  2. package/dist/acp.js +90 -19
  3. package/dist/agents.js +155 -45
  4. package/dist/api.js +5 -1
  5. package/dist/app-git.js +10 -26
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +4 -7
  8. package/dist/building.js +50 -0
  9. package/dist/calls.js +71 -0
  10. package/dist/chatgpt.js +32 -0
  11. package/dist/clis.js +52 -42
  12. package/dist/connection.js +105 -0
  13. package/dist/conversations.js +526 -175
  14. package/dist/copies.js +89 -52
  15. package/dist/end.js +17 -2
  16. package/dist/events.js +33 -8
  17. package/dist/files.js +139 -31
  18. package/dist/git.js +13 -11
  19. package/dist/home.js +56 -12
  20. package/dist/house-cli.js +299 -0
  21. package/dist/house-main.js +7 -206
  22. package/dist/instructions.js +7 -11
  23. package/dist/kit-cli.js +68 -0
  24. package/dist/kit-main.js +8 -22
  25. package/dist/login.js +49 -31
  26. package/dist/logout.js +0 -2
  27. package/dist/mcp.js +6 -2
  28. package/dist/placement.js +31 -0
  29. package/dist/relay.js +22 -1
  30. package/dist/resident.js +97 -38
  31. package/dist/scope.js +15 -4
  32. package/dist/scripts.js +255 -0
  33. package/dist/service.js +23 -1
  34. package/dist/sign-in.js +22 -39
  35. package/dist/silence.js +28 -0
  36. package/dist/skills.js +49 -35
  37. package/dist/transcription.js +85 -45
  38. package/dist/translate.js +2 -8
  39. package/dist/update.js +113 -105
  40. package/dist/wire.js +40 -0
  41. package/dist/work.js +3 -5
  42. package/package.json +20 -20
  43. package/dist/bootstrap.json +0 -24
  44. package/dist/clis-main.js +0 -23
  45. package/dist/stream.js +0 -74
  46. package/how-we-work.md +0 -23
  47. package/skills/ask-matt/LICENSE +0 -21
  48. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  49. package/skills/ask-matt/SKILL.md +0 -91
  50. package/skills/ask-matt/agents/openai.yaml +0 -4
  51. package/skills/code-review/LICENSE +0 -21
  52. package/skills/code-review/SKILL.md +0 -89
  53. package/skills/code-review/agents/openai.yaml +0 -3
  54. package/skills/codebase-design/DEEPENING.md +0 -37
  55. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  56. package/skills/codebase-design/LICENSE +0 -21
  57. package/skills/codebase-design/SKILL.md +0 -116
  58. package/skills/codebase-design/agents/openai.yaml +0 -3
  59. package/skills/diagnosing-bugs/LICENSE +0 -21
  60. package/skills/diagnosing-bugs/SKILL.md +0 -140
  61. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  62. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  63. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  64. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  65. package/skills/domain-modeling/LICENSE +0 -21
  66. package/skills/domain-modeling/SKILL.md +0 -76
  67. package/skills/domain-modeling/agents/openai.yaml +0 -3
  68. package/skills/grill-me/LICENSE +0 -21
  69. package/skills/grill-me/SKILL.md +0 -8
  70. package/skills/grill-me/agents/openai.yaml +0 -4
  71. package/skills/grill-with-docs/LICENSE +0 -21
  72. package/skills/grill-with-docs/SKILL.md +0 -8
  73. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  74. package/skills/grilling/LICENSE +0 -21
  75. package/skills/grilling/SKILL.md +0 -30
  76. package/skills/grilling/agents/openai.yaml +0 -3
  77. package/skills/handoff/LICENSE +0 -21
  78. package/skills/handoff/SKILL.md +0 -17
  79. package/skills/handoff/agents/openai.yaml +0 -4
  80. package/skills/implement/LICENSE +0 -21
  81. package/skills/implement/SKILL.md +0 -16
  82. package/skills/implement/agents/openai.yaml +0 -4
  83. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  84. package/skills/improve-codebase-architecture/LICENSE +0 -21
  85. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  86. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  87. package/skills/prototype/LICENSE +0 -21
  88. package/skills/prototype/LOGIC.md +0 -67
  89. package/skills/prototype/SKILL.md +0 -28
  90. package/skills/prototype/UI.md +0 -112
  91. package/skills/prototype/agents/openai.yaml +0 -3
  92. package/skills/research/LICENSE +0 -21
  93. package/skills/research/SKILL.md +0 -14
  94. package/skills/research/agents/openai.yaml +0 -3
  95. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  96. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  97. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  98. package/skills/tdd/LICENSE +0 -21
  99. package/skills/tdd/SKILL.md +0 -40
  100. package/skills/tdd/agents/openai.yaml +0 -3
  101. package/skills/tdd/mocking.md +0 -59
  102. package/skills/tdd/tests.md +0 -77
  103. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  104. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  105. package/skills/teach/LICENSE +0 -21
  106. package/skills/teach/MISSION-FORMAT.md +0 -31
  107. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  108. package/skills/teach/SKILL.md +0 -141
  109. package/skills/teach/agents/openai.yaml +0 -4
  110. package/skills/to-questionnaire/LICENSE +0 -21
  111. package/skills/to-questionnaire/SKILL.md +0 -55
  112. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  113. package/skills/to-spec/LICENSE +0 -21
  114. package/skills/to-spec/SKILL.md +0 -76
  115. package/skills/to-spec/agents/openai.yaml +0 -4
  116. package/skills/to-tickets/LICENSE +0 -21
  117. package/skills/to-tickets/SKILL.md +0 -106
  118. package/skills/to-tickets/agents/openai.yaml +0 -4
  119. package/skills/wait-what/LICENSE +0 -21
  120. package/skills/wait-what/SKILL.md +0 -8
  121. package/skills/wait-what/agents/openai.yaml +0 -4
  122. package/skills/wayfinder/LICENSE +0 -21
  123. package/skills/wayfinder/SKILL.md +0 -129
  124. package/skills/wayfinder/agents/openai.yaml +0 -4
  125. package/skills/writing-for-agents/LICENSE +0 -21
  126. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  127. package/skills/writing-for-agents/SKILL.md +0 -83
  128. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -3,39 +3,116 @@ 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';
6
+ import { signedOut, startAdapter } from './acp.js';
7
+ import { finalRefusal, HouseRefusal } from './api.js';
7
8
  import { openBridge } from './bridge.js';
8
9
  import { CLIS } from './clis.js';
9
- import { readerFor } from './events.js';
10
- import { placeFiles, uploadBytes } from './files.js';
10
+ import { answeredFailure, readerFor } from './events.js';
11
+ import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
11
12
  import { agentBase } from './home.js';
12
13
  import { instructions } from './instructions.js';
13
14
  import { Parts } from './parts.js';
15
+ import { refusalOf } from './refusals.js';
16
+ import { relayed } from './relay.js';
14
17
  import { killMarked, Scope } from './scope.js';
15
18
  import { holdSecretInput } from './secret-input.js';
16
- import { createHowWeWork } from './skills.js';
17
19
  import { transcribe } from './transcription.js';
18
20
  import { workFor } from './work.js';
19
21
  const KILLED = 'the conversation was killed';
22
+ const INTERRUPTED = 'the conversation was interrupted';
23
+ const REFUSED = 'the CLI refused the prompt';
24
+ const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
25
+ const GONE = 'House no longer holds this conversation; start a new one';
26
+ const RETIRED = "this conversation's Agent is retired, so it cannot start";
27
+ const ENDED = {
28
+ conversation_not_found: GONE,
29
+ conversation_historical: RETIRED,
30
+ };
31
+ const NO_START = "the Agent's CLI did not start";
32
+ const NO_SESSION = "the Agent's CLI did not open a session";
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";
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";
37
+ class NotStarted extends Error {
38
+ }
39
+ class ResumeRefused extends NotStarted {
40
+ }
41
+ async function step(reason, work) {
42
+ try {
43
+ return await work;
44
+ }
45
+ catch (error) {
46
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
47
+ }
48
+ }
49
+ function 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;
54
+ function unbridged(error) {
55
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
56
+ }
57
+ const EXIT_PLAN_MODE = {
58
+ parse(value) {
59
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
60
+ throw new Error('_x.ai/exit_plan_mode needs an object');
61
+ }
62
+ return value;
63
+ },
64
+ };
65
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
20
66
  function causeOf(error) {
21
67
  return error instanceof Error ? error.message : String(error);
22
68
  }
69
+ function answeredWith(error) {
70
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
71
+ }
72
+ function exitedWith(running, cause) {
73
+ if (running.killed)
74
+ return { failure: 'killed', cause: KILLED };
75
+ return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
76
+ }
23
77
  function answerMessage(request, response) {
24
78
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
25
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
+ }
26
86
  function logged(error) {
27
87
  process.stderr.write(`kit: ${causeOf(error)}\n`);
28
88
  }
89
+ function refusal(conversation, error) {
90
+ if (conversation.kills > 0)
91
+ return KILLED;
92
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
93
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
94
+ return causeOf(error);
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
+ }
29
102
  function marker(bridge) {
30
103
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
104
  }
105
+ function steers(adapter) {
106
+ return adapter.initialized._meta?.steering?.supported === true;
107
+ }
32
108
  class Turn {
33
109
  id = randomUUID();
34
110
  parts;
35
- admitted = false;
36
111
  questions = 0;
37
112
  secrets = 0;
38
113
  prompted = false;
114
+ steered = false;
115
+ taken = null;
39
116
  ended = false;
40
117
  images = 0;
41
118
  uploads = [];
@@ -49,20 +126,31 @@ class Conversation {
49
126
  session = null;
50
127
  turn = null;
51
128
  commands = null;
129
+ options = null;
130
+ exitUntold = false;
52
131
  context = null;
53
132
  leftovers = new Set();
54
133
  waiting = [];
134
+ done = false;
55
135
  sending = null;
56
136
  subagents = new Map();
57
137
  held = [];
58
138
  opening = null;
139
+ bridge = null;
140
+ launch = null;
141
+ prompted = null;
59
142
  kills = 0;
60
143
  killed = new AbortController();
61
144
  interrupts = 0;
62
145
  interrupted = new AbortController();
63
- open = 0;
146
+ turns = new Set();
64
147
  queue = Promise.resolve();
65
148
  reports = Promise.resolve();
149
+ starting = null;
150
+ preparing = null;
151
+ told = null;
152
+ untold = false;
153
+ shown = null;
66
154
  constructor(id) {
67
155
  this.id = id;
68
156
  }
@@ -78,9 +166,7 @@ function asksSecret(request) {
78
166
  export class Conversations {
79
167
  kit;
80
168
  conversations = new Map();
81
- acks = new Map();
82
169
  questions = new Map();
83
- carrying = 0;
84
170
  reporting = 0;
85
171
  watched = false;
86
172
  constructor(kit) {
@@ -88,43 +174,32 @@ export class Conversations {
88
174
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
89
175
  setInterval(() => this.look(), 2_000);
90
176
  }
91
- input(input) {
92
- this.carrying++;
93
- const carried = this.acks.get(input.input_id) ??
94
- (() => {
95
- const conversation = this.conversation(input.conversation_id);
96
- if (input.kind === 'kill') {
97
- conversation.kills++;
98
- conversation.killed.abort(new Error(KILLED));
99
- for (const waiting of conversation.waiting.splice(0))
100
- waiting.settle({ refused: KILLED });
101
- this.halt(conversation);
102
- }
103
- if (input.kind === 'interrupt') {
104
- conversation.interrupts++;
105
- conversation.interrupted.abort();
106
- }
107
- const handled = conversation.queue.then(() => this.carry(conversation, input));
108
- conversation.queue = handled.catch(() => undefined);
109
- const work = handled.then(({ ack }) => ack);
110
- this.acks.set(input.input_id, work);
111
- return work;
112
- })();
113
- carried
114
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
115
- .catch(logged)
116
- .finally(() => {
117
- this.carrying--;
118
- this.kit.changed();
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') {
181
+ conversation.kills++;
182
+ conversation.killed.abort(new Error(KILLED));
183
+ for (const waiting of conversation.waiting.splice(0))
184
+ waiting.settle({ refused: KILLED });
185
+ this.halt(conversation);
186
+ }
187
+ if (call.kind === 'interrupt') {
188
+ conversation.interrupts++;
189
+ conversation.interrupted.abort();
190
+ }
191
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
192
+ conversation.queue = handled.catch(() => undefined);
193
+ return handled.then(async ({ result }) => ({ result: await result }));
119
194
  });
120
195
  }
121
196
  idle() {
122
- if (this.carrying > 0 || this.reporting > 0)
197
+ if (this.reporting > 0)
123
198
  return false;
124
199
  for (const conversation of this.conversations.values()) {
125
200
  const turn = conversation.turn;
126
201
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
127
- if (conversation.open > (asking ? 1 : 0))
202
+ if (conversation.turns.size > (asking ? 1 : 0))
128
203
  return false;
129
204
  }
130
205
  const conversations = [...this.conversations.values()];
@@ -133,7 +208,7 @@ export class Conversations {
133
208
  return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
134
209
  }
135
210
  quiet() {
136
- return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
211
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
137
212
  }
138
213
  look() {
139
214
  for (const conversation of this.conversations.values()) {
@@ -148,6 +223,16 @@ export class Conversations {
148
223
  }
149
224
  opened() {
150
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
+ }
151
236
  }
152
237
  watching(watched) {
153
238
  const resend = watched && !this.watched;
@@ -167,130 +252,320 @@ export class Conversations {
167
252
  }
168
253
  return conversation;
169
254
  }
170
- async carry(conversation, input) {
171
- if (input.kind === 'open')
172
- return { ack: Promise.resolve(await this.open(conversation, input)) };
173
- if (input.kind === 'message')
174
- return this.message(conversation, input);
175
- if (input.kind === 'interrupt') {
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') {
176
267
  if (--conversation.interrupts === 0)
177
268
  conversation.interrupted = new AbortController();
178
- await this.interrupt(conversation, String(input.turn_id));
269
+ await this.interrupt(conversation, String(call.turn_id));
179
270
  }
180
- if (input.kind === 'kill') {
271
+ if (call.kind === 'kill') {
181
272
  await this.kill(conversation);
182
273
  await conversation.sending;
183
274
  if (--conversation.kills === 0)
184
275
  conversation.killed = new AbortController();
185
276
  }
186
- if (input.kind === 'option')
187
- await this.option(conversation, String(input.option), input.value);
188
- if (input.kind === 'answer') {
189
- const question = this.questions.get(String(input.interaction_id));
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));
190
281
  if (question === undefined) {
191
- const text = answerMessage(input.request, input.response);
192
- 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 });
193
284
  }
194
- question.answer(input.response);
285
+ question.answer(call.response);
195
286
  }
196
- 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);
197
328
  }
198
- async open(conversation, input) {
199
- if (conversation.running !== null)
200
- return {};
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;
336
+ }
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;
201
344
  try {
202
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
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);
203
352
  }
204
353
  catch (error) {
205
- return { refused: causeOf(error) };
354
+ this.closeIdleBridge(conversation);
355
+ this.prepares(conversation, null);
356
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
206
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 };
207
367
  }
208
- async message(conversation, input) {
209
- const content = [{ type: 'text', text: String(input.text) }];
210
- 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;
211
388
  try {
212
- route = await this.route(conversation, input.agent_id);
213
- const files = input.files;
214
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
215
- const lines = await this.transcribed(conversation, files, paths);
216
- if (lines.length > 0)
217
- content.push({ type: 'text', text: lines.join('\n') });
218
- if (conversation.kills > 0)
219
- 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
+ }));
220
394
  }
221
395
  catch (error) {
222
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(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 })) });
223
412
  }
224
- return {
225
- ack: new Promise((settle) => {
226
- conversation.waiting.push({ input, route, content, settle });
227
- this.next(conversation);
228
- }),
229
- };
230
413
  }
231
414
  next(conversation) {
232
- if (conversation.sending !== null || conversation.open > 0)
415
+ if (conversation.sending !== null || conversation.turns.size > 0)
233
416
  return;
234
417
  const waiting = conversation.waiting.shift();
235
- 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);
236
424
  return;
425
+ }
237
426
  conversation.sending = this.send(conversation, waiting).finally(() => {
238
427
  conversation.sending = null;
239
428
  this.next(conversation);
240
429
  });
241
430
  }
242
431
  async send(conversation, waiting) {
243
- const { input } = waiting;
244
- let ack = {};
432
+ const { call } = waiting;
433
+ let result = {};
434
+ let started = false;
435
+ let instructing = waiting.instructing;
245
436
  const prompt = [];
246
437
  try {
247
438
  if (conversation.running === null) {
248
- const resumed = input.provider_session_id ?? conversation.session;
249
- const opened = await this.start(conversation, waiting.route, resumed);
250
- if (resumed === null)
251
- ack = { provider_session_id: opened };
439
+ this.starts(conversation, 'session');
440
+ await this.launched(conversation);
252
441
  }
253
- if (input.first === true) {
254
- prompt.push({
255
- type: 'text',
256
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
257
- });
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;
449
+ }
450
+ const session = conversation.running.sessionId;
451
+ if (started || (call.first === true && session !== call.provider_session_id)) {
452
+ result = { provider_session_id: session };
453
+ }
454
+ if (call.first === true) {
455
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
258
456
  }
259
457
  if (conversation.kills > 0)
260
458
  throw new Error(KILLED);
261
459
  }
262
460
  catch (error) {
263
- if ('provider_session_id' in ack) {
461
+ this.starts(conversation, null);
462
+ if (started) {
264
463
  conversation.session = null;
265
464
  await this.kill(conversation);
266
465
  }
267
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(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 });
268
469
  return;
269
470
  }
270
- this.prompt(conversation, [...prompt, ...waiting.content]);
271
- 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);
272
474
  }
273
- async transcribed(conversation, files, paths) {
274
- const lines = [];
275
- for (const [index, path] of paths.entries()) {
276
- lines.push(path);
277
- if (files[index].media_type?.startsWith('audio/') !== true)
278
- continue;
279
- try {
280
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
281
- if (text !== null && text !== '')
282
- lines.push(`Transcript: ${text}`);
283
- }
284
- catch (error) {
285
- if (conversation.kills > 0)
286
- throw error;
287
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
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}` });
288
531
  }
289
532
  }
290
- 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);
291
562
  }
292
563
  async interrupt(conversation, turnId) {
293
- 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)
294
569
  return;
295
570
  await conversation.running.adapter.connection.agent.notify('session/cancel', {
296
571
  sessionId: conversation.running.sessionId,
@@ -342,63 +617,78 @@ export class Conversations {
342
617
  }
343
618
  return route;
344
619
  }
345
- async start(conversation, route, sessionId) {
620
+ async start(conversation, routed, sessionId, empty, bridged) {
621
+ const route = await routed;
346
622
  const reader = readerFor(route.kind);
347
623
  reader.session = sessionId;
348
624
  const app = client({ name: '@agentshouse/kit' })
349
625
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
350
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
626
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
627
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
351
628
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
352
- const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
629
+ const bridging = this.bridged(conversation);
630
+ const bridge = await step(unbridged, bridging);
353
631
  if (conversation.kills > 0) {
354
- bridge.close();
632
+ this.closeBridge(conversation, bridging, bridge);
355
633
  throw new Error(KILLED);
356
634
  }
357
635
  conversation.opening = bridge;
358
636
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
359
637
  const scope = new Scope(marker(bridge), randomUUID(), cli);
638
+ let adapter = null;
360
639
  try {
361
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
640
+ bridged(bridge);
362
641
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
363
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
642
+ const streamed = await step(NO_CONNECTION, work.open(cli));
643
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
364
644
  work.read(message);
365
645
  this.read(conversation, reader, message);
366
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
367
- void adapter.exited.then(() => bridge.close());
646
+ }, { ...bridge.env, ...streamed }, undefined, scope));
647
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
368
648
  const agent = adapter.connection.agent;
369
649
  const cwd = route.working_directory;
370
650
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
371
- const opened = sessionId === null
372
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
373
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
651
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
652
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
374
653
  reader.session = opened.sessionId;
375
- if (sessionId === null && CLIS[route.kind].renames) {
654
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
376
655
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
377
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
656
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
378
657
  }
379
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
380
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
658
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
659
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
381
660
  void work.settled().then(() => scope.record());
382
661
  conversation.running = running;
662
+ adapter.turn(conversation.turns.size > 0);
383
663
  conversation.session = running.sessionId;
384
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
385
- if (conversation.commands !== null)
386
- this.commands(conversation, conversation.commands);
387
- this.options(conversation, options);
388
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
664
+ conversation.options = options;
665
+ this.facts(conversation);
666
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
389
667
  return running.sessionId;
390
668
  }
391
669
  catch (error) {
392
- scope.kill();
670
+ adapter?.retire();
393
671
  work.close();
394
672
  conversation.leftovers.add(scope);
395
- bridge.close();
673
+ this.closeBridge(conversation, bridging, bridge);
396
674
  throw conversation.kills > 0 ? new Error(KILLED) : error;
397
675
  }
398
676
  finally {
399
677
  conversation.opening = null;
400
678
  }
401
679
  }
680
+ async resume(conversation, kind, adapter, request, empty) {
681
+ try {
682
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
683
+ }
684
+ catch (error) {
685
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
686
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
687
+ }
688
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
689
+ return null;
690
+ }
691
+ }
402
692
  async launchSettings(running, route, offered) {
403
693
  let options = offered;
404
694
  const settings = [
@@ -420,27 +710,45 @@ export class Conversations {
420
710
  }
421
711
  return options;
422
712
  }
423
- exited(conversation, adapter, cause) {
424
- const running = conversation.running;
425
- if (running?.adapter !== adapter)
426
- return;
713
+ exited(conversation, running, cause) {
427
714
  this.detach(conversation, running);
428
715
  if (conversation.turn !== null) {
429
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
716
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
430
717
  }
431
718
  this.next(conversation);
432
719
  this.kit.changed();
433
720
  }
434
721
  detach(conversation, running) {
722
+ if (conversation.bridge === running.bridging)
723
+ conversation.bridge = null;
435
724
  running.work.close();
436
725
  conversation.leftovers.add(running.scope);
437
726
  conversation.running = null;
438
727
  conversation.commands = null;
728
+ conversation.options = null;
729
+ if (conversation.subagents.size > 0) {
730
+ conversation.subagents.clear();
731
+ this.subagentsChanged(conversation);
732
+ }
439
733
  for (const [id, question] of this.questions) {
440
734
  if (question.conversation === conversation)
441
735
  this.questions.delete(id);
442
736
  }
443
- 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 });
444
752
  }
445
753
  commands(conversation, commands) {
446
754
  conversation.commands = commands;
@@ -451,6 +759,7 @@ export class Conversations {
451
759
  options(conversation, options) {
452
760
  if (conversation.running === null)
453
761
  return;
762
+ conversation.options = options;
454
763
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
455
764
  }
456
765
  deliver(conversation, turn, event, body) {
@@ -465,7 +774,7 @@ export class Conversations {
465
774
  return delivered;
466
775
  }
467
776
  write(conversation, turn, operation) {
468
- if (!turn.admitted || turn.ended || !this.watched)
777
+ if (turn.ended || !this.watched)
469
778
  return;
470
779
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
471
780
  }
@@ -475,13 +784,9 @@ export class Conversations {
475
784
  }
476
785
  begin(conversation) {
477
786
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
478
- conversation.open++;
479
- void this.deliver(conversation, turn, 'started', {})
480
- .then(() => {
481
- turn.admitted = true;
482
- this.replay(conversation, turn);
483
- })
484
- .catch(logged);
787
+ conversation.turns.add(turn);
788
+ conversation.running?.adapter.turn(true);
789
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
485
790
  conversation.turn = turn;
486
791
  if (conversation.context !== null)
487
792
  turn.parts.measure(conversation.context);
@@ -490,59 +795,81 @@ export class Conversations {
490
795
  this.kit.changed();
491
796
  return turn;
492
797
  }
493
- end(conversation, turn, failed) {
798
+ end(conversation, turn, failed, notice = null) {
494
799
  if (turn.ended)
495
800
  return;
801
+ if (failed !== null)
802
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
496
803
  if (conversation.turn === turn) {
497
804
  for (const event of conversation.running?.reader.closeTurn() ?? [])
498
805
  this.apply(conversation, turn, event);
499
806
  }
807
+ if (notice !== null)
808
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
500
809
  turn.ended = true;
501
- conversation.open--;
810
+ conversation.turns.delete(turn);
811
+ conversation.running?.adapter.turn(conversation.turns.size > 0);
502
812
  if (conversation.turn === turn)
503
813
  conversation.turn = null;
504
814
  const body = Promise.allSettled(turn.uploads).then(() => ({
505
815
  parts: turn.parts.saved(),
506
816
  context: turn.parts.context,
507
- ...failed,
817
+ ...(failed === null ? {} : { failed: failed.failure }),
508
818
  }));
509
819
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
510
820
  this.next(conversation);
511
821
  this.kit.changed();
512
822
  }
513
- prompt(conversation, prompt) {
823
+ prompt(conversation, prompt, taken) {
514
824
  const running = conversation.running;
515
825
  const turn = this.begin(conversation);
516
826
  turn.prompted = true;
517
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
827
+ turn.taken = taken;
828
+ conversation.prompted = running.sessionId;
829
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
830
+ this.took(turn);
831
+ const notice = answeredFailure(answer);
832
+ if (notice !== null) {
833
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
834
+ }
835
+ else {
836
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
837
+ }
838
+ }, async (error) => {
518
839
  if (running.adapter.connection.signal.aborted) {
519
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
840
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
520
841
  }
521
842
  else {
522
- if (conversation.open === 1 && conversation.running === running) {
523
- running.scope.kill();
843
+ this.took(turn);
844
+ if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
845
+ running.adapter.retire();
524
846
  this.detach(conversation, running);
525
847
  }
526
- this.end(conversation, turn, { failed: causeOf(error) });
848
+ this.end(conversation, turn, answeredWith(error));
527
849
  }
528
850
  });
529
851
  }
530
852
  started(conversation) {
531
- return conversation.turn ?? this.begin(conversation);
853
+ const turn = conversation.turn ?? this.begin(conversation);
854
+ this.took(turn);
855
+ return turn;
532
856
  }
533
- ended(conversation) {
857
+ ended(conversation, stop) {
534
858
  const turn = conversation.turn;
535
859
  if (turn === null)
536
860
  return;
537
- if (!turn.prompted) {
538
- this.end(conversation, turn, null);
539
- }
540
- else {
541
- for (const event of conversation.running?.reader.closeTurn() ?? [])
542
- this.apply(conversation, turn, event);
543
- conversation.turn = null;
544
- 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;
545
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();
546
873
  }
547
874
  async ask(conversation, method, params, secret, signal) {
548
875
  const turn = this.started(conversation);
@@ -598,7 +925,7 @@ export class Conversations {
598
925
  this.started(conversation);
599
926
  return;
600
927
  case 'ended':
601
- this.ended(conversation);
928
+ this.ended(conversation, event.stop);
602
929
  return;
603
930
  case 'commands':
604
931
  this.commands(conversation, event.commands);
@@ -613,6 +940,9 @@ export class Conversations {
613
940
  case 'subagent-done':
614
941
  this.subagentDone(conversation, event.id, event.result);
615
942
  return;
943
+ case 'subagent-activity':
944
+ this.subagentActivity(conversation, event.id, event.activity);
945
+ return;
616
946
  case 'marker':
617
947
  case 'image':
618
948
  if (conversation.turn !== null)
@@ -623,6 +953,11 @@ export class Conversations {
623
953
  }
624
954
  this.apply(conversation, this.started(conversation), event);
625
955
  }
956
+ took(turn) {
957
+ const taken = turn.taken;
958
+ turn.taken = null;
959
+ taken?.();
960
+ }
626
961
  apply(conversation, turn, event) {
627
962
  switch (event.kind) {
628
963
  case 'text':
@@ -634,7 +969,8 @@ export class Conversations {
634
969
  return;
635
970
  case 'subagent': {
636
971
  const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
637
- 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);
638
974
  return;
639
975
  }
640
976
  case 'message':
@@ -657,6 +993,7 @@ export class Conversations {
657
993
  if (subagent === undefined)
658
994
  return;
659
995
  conversation.subagents.delete(id);
996
+ this.subagentsChanged(conversation);
660
997
  if (subagent.turn === conversation.turn) {
661
998
  subagent.turn.parts.done(subagent.index, result);
662
999
  return;
@@ -667,23 +1004,37 @@ export class Conversations {
667
1004
  else
668
1005
  conversation.turn.parts.start(part);
669
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
+ }
670
1014
  async image(conversation, turn, reserved, data, mediaType) {
671
1015
  const bytes = Buffer.from(data, 'base64');
672
1016
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
1017
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
673
1018
  try {
674
1019
  const declared = (await this.deliver(conversation, turn, 'images', {
675
1020
  name,
676
1021
  media_type: mediaType,
677
1022
  bytes: bytes.length,
678
- sha256: createHash('sha256').update(bytes).digest('hex'),
1023
+ sha256,
679
1024
  }));
680
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
681
- if (saved.save.status !== 'saved')
682
- throw new Error(saved.save.failure ?? saved.save.status);
683
- turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
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);
684
1034
  }
685
1035
  catch (error) {
686
- 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);
687
1038
  }
688
1039
  }
689
1040
  }