@agentshouse/kit 0.2.1-ca18b01951c3b → 0.2.1-ca1f38641026d

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