@agentshouse/kit 0.2.1-c8add15efc1ec → 0.2.1-c943f280303fb

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