@agentshouse/kit 0.2.1-c96543a4c8b15 → 0.2.1-c96855aa27328

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