@agentshouse/kit 0.2.1-c53a84236c3d9 → 0.2.1-c57b73ff93a76

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