@agentshouse/kit 0.2.1-cbabe8ad337ff → 0.2.1-cbff58027f078

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