@agentshouse/kit 0.2.1-c8179f06883b6 → 0.2.1-c83d468784cc9

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 +37 -36
  2. package/dist/acp.js +90 -19
  3. package/dist/afk.js +17 -0
  4. package/dist/agents.js +154 -48
  5. package/dist/api.js +5 -5
  6. package/dist/app-git.js +10 -26
  7. package/dist/authority-main.js +3 -3
  8. package/dist/bridge.js +7 -10
  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 +57 -42
  14. package/dist/connection.js +105 -0
  15. package/dist/conversations.js +572 -182
  16. package/dist/copies.js +350 -162
  17. package/dist/end.js +15 -4
  18. package/dist/events.js +70 -8
  19. package/dist/files.js +139 -31
  20. package/dist/git.js +57 -21
  21. package/dist/home.js +56 -12
  22. package/dist/house-cli.js +299 -0
  23. package/dist/house-main.js +7 -228
  24. package/dist/instructions.js +7 -11
  25. package/dist/kit-cli.js +68 -0
  26. package/dist/kit-main.js +8 -67
  27. package/dist/login.js +43 -19
  28. package/dist/logout.js +1 -3
  29. package/dist/mcp.js +6 -2
  30. package/dist/parts.js +10 -0
  31. package/dist/placement.js +19 -36
  32. package/dist/relay.js +21 -0
  33. package/dist/resident.js +100 -44
  34. package/dist/scope.js +15 -4
  35. package/dist/scripts.js +255 -0
  36. package/dist/secret-input.js +1 -1
  37. package/dist/service.js +23 -1
  38. package/dist/sign-in.js +22 -39
  39. package/dist/silence.js +28 -0
  40. package/dist/skills.js +49 -35
  41. package/dist/transcription.js +85 -45
  42. package/dist/translate.js +2 -8
  43. package/dist/update.js +113 -126
  44. package/dist/wire.js +37 -3
  45. package/dist/work.js +3 -5
  46. package/package.json +20 -20
  47. package/dist/bootstrap.json +0 -24
  48. package/dist/clis-main.js +0 -23
  49. package/dist/codex.js +0 -61
  50. package/dist/stream.js +0 -77
  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,43 +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';
20
- export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
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']);
21
74
  function causeOf(error) {
22
75
  return error instanceof Error ? error.message : String(error);
23
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
+ }
24
85
  function answerMessage(request, response) {
25
86
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
26
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
+ }
27
94
  function logged(error) {
28
95
  process.stderr.write(`kit: ${causeOf(error)}\n`);
29
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
+ }
30
110
  function marker(bridge) {
31
111
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
32
112
  }
113
+ function steers(adapter) {
114
+ return adapter.initialized._meta?.steering?.supported === true;
115
+ }
33
116
  class Turn {
34
117
  id = randomUUID();
35
118
  parts;
36
- admitted = false;
37
119
  questions = 0;
38
120
  secrets = 0;
39
121
  prompted = false;
122
+ steered = false;
123
+ taken = null;
40
124
  ended = false;
125
+ quietUntil = Infinity;
41
126
  images = 0;
42
127
  uploads = [];
43
128
  constructor(emit) {
@@ -50,20 +135,33 @@ class Conversation {
50
135
  session = null;
51
136
  turn = null;
52
137
  commands = null;
138
+ options = null;
139
+ exitUntold = false;
140
+ afk = false;
53
141
  context = null;
142
+ tokens = null;
54
143
  leftovers = new Set();
55
144
  waiting = [];
145
+ done = false;
56
146
  sending = null;
57
147
  subagents = new Map();
58
148
  held = [];
59
149
  opening = null;
150
+ bridge = null;
151
+ launch = null;
152
+ prompted = null;
60
153
  kills = 0;
61
154
  killed = new AbortController();
62
155
  interrupts = 0;
63
156
  interrupted = new AbortController();
64
- open = 0;
157
+ turns = new Set();
65
158
  queue = Promise.resolve();
66
159
  reports = Promise.resolve();
160
+ starting = null;
161
+ preparing = null;
162
+ told = null;
163
+ untold = false;
164
+ shown = null;
67
165
  constructor(id) {
68
166
  this.id = id;
69
167
  }
@@ -79,9 +177,7 @@ function asksSecret(request) {
79
177
  export class Conversations {
80
178
  kit;
81
179
  conversations = new Map();
82
- acks = new Map();
83
180
  questions = new Map();
84
- carrying = 0;
85
181
  reporting = 0;
86
182
  watched = false;
87
183
  constructor(kit) {
@@ -89,43 +185,34 @@ export class Conversations {
89
185
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
90
186
  setInterval(() => this.look(), 2_000);
91
187
  }
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();
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 }));
120
207
  });
121
208
  }
122
209
  idle() {
123
- if (this.carrying > 0 || this.reporting > 0)
210
+ if (this.reporting > 0)
124
211
  return false;
125
212
  for (const conversation of this.conversations.values()) {
126
213
  const turn = conversation.turn;
127
214
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
128
- if (conversation.open > (asking ? 1 : 0))
215
+ if (conversation.turns.size > (asking ? 1 : 0))
129
216
  return false;
130
217
  }
131
218
  const conversations = [...this.conversations.values()];
@@ -134,7 +221,7 @@ export class Conversations {
134
221
  return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
135
222
  }
136
223
  quiet() {
137
- return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
224
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
138
225
  }
139
226
  look() {
140
227
  for (const conversation of this.conversations.values()) {
@@ -149,6 +236,16 @@ export class Conversations {
149
236
  }
150
237
  opened() {
151
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
+ }
152
249
  }
153
250
  watching(watched) {
154
251
  const resend = watched && !this.watched;
@@ -168,130 +265,319 @@ export class Conversations {
168
265
  }
169
266
  return conversation;
170
267
  }
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') {
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') {
177
280
  if (--conversation.interrupts === 0)
178
281
  conversation.interrupted = new AbortController();
179
- await this.interrupt(conversation, String(input.turn_id));
282
+ await this.interrupt(conversation, String(call.turn_id));
180
283
  }
181
- if (input.kind === 'kill') {
284
+ if (call.kind === 'kill') {
182
285
  await this.kill(conversation);
183
286
  await conversation.sending;
184
287
  if (--conversation.kills === 0)
185
288
  conversation.killed = new AbortController();
186
289
  }
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));
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));
191
294
  if (question === undefined) {
192
- const text = answerMessage(input.request, input.response);
193
- 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 });
194
297
  }
195
- question.answer(input.response);
298
+ question.answer(call.response);
196
299
  }
197
- return { ack: Promise.resolve({}) };
300
+ return { result: Promise.resolve({}) };
198
301
  }
199
- async open(conversation, input) {
200
- if (conversation.running !== null)
201
- 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;
202
357
  try {
203
- 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);
204
365
  }
205
366
  catch (error) {
206
- return { refused: causeOf(error) };
367
+ this.closeIdleBridge(conversation);
368
+ this.prepares(conversation, null);
369
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
207
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 };
208
380
  }
209
- async message(conversation, input) {
210
- const content = [{ type: 'text', text: String(input.text) }];
211
- 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;
212
401
  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);
402
+ ({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
403
+ sessionId: running.sessionId,
404
+ prompt: waiting.content,
405
+ _meta: { steering: { idleBehavior: 'promptRequired' } },
406
+ }));
221
407
  }
222
408
  catch (error) {
223
- 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 })) });
224
425
  }
225
- return {
226
- ack: new Promise((settle) => {
227
- conversation.waiting.push({ input, route, content, settle });
228
- this.next(conversation);
229
- }),
230
- };
231
426
  }
232
427
  next(conversation) {
233
- if (conversation.sending !== null || conversation.open > 0)
428
+ if (conversation.sending !== null || conversation.turns.size > 0)
234
429
  return;
235
430
  const waiting = conversation.waiting.shift();
236
- 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);
237
437
  return;
438
+ }
238
439
  conversation.sending = this.send(conversation, waiting).finally(() => {
239
440
  conversation.sending = null;
240
441
  this.next(conversation);
241
442
  });
242
443
  }
243
444
  async send(conversation, waiting) {
244
- const { input } = waiting;
245
- let ack = {};
445
+ const { call } = waiting;
446
+ let result = {};
447
+ let started = false;
448
+ let instructing = waiting.instructing;
246
449
  const prompt = [];
247
450
  try {
248
451
  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 };
452
+ this.starts(conversation, 'session');
453
+ await this.launched(conversation);
253
454
  }
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
- });
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;
462
+ }
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)) });
259
469
  }
260
470
  if (conversation.kills > 0)
261
471
  throw new Error(KILLED);
262
472
  }
263
473
  catch (error) {
264
- if ('provider_session_id' in ack) {
474
+ this.starts(conversation, null);
475
+ if (started) {
265
476
  conversation.session = null;
266
477
  await this.kill(conversation);
267
478
  }
268
- 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 });
269
482
  return;
270
483
  }
271
- this.prompt(conversation, [...prompt, ...waiting.content]);
272
- 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);
273
487
  }
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)}`));
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
+ await step(unbridged, unlessAborted(this.kit.house.deliver('/kit/transcripts/keep', { conversation: conversation.id, message: messageId, path, text: said }, () => !signal.aborted), signal));
542
+ blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
289
543
  }
290
544
  }
291
- return lines;
545
+ catch (error) {
546
+ if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
547
+ throw new NotStarted(INTERRUPTED, { cause: error });
548
+ throw error;
549
+ }
550
+ return blocks;
551
+ }
552
+ bridged(conversation) {
553
+ if (conversation.bridge === null) {
554
+ const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
555
+ conversation.bridge = bridging;
556
+ bridging.catch(() => {
557
+ if (conversation.bridge === bridging)
558
+ conversation.bridge = null;
559
+ });
560
+ }
561
+ return conversation.bridge;
562
+ }
563
+ closeBridge(conversation, bridging, bridge) {
564
+ bridge.close();
565
+ if (conversation.bridge === bridging)
566
+ conversation.bridge = null;
567
+ }
568
+ closeIdleBridge(conversation) {
569
+ const bridging = conversation.bridge;
570
+ if (bridging === null || conversation.running !== null || conversation.launch !== null)
571
+ return;
572
+ conversation.bridge = null;
573
+ void bridging.then((bridge) => bridge.close(), () => undefined);
292
574
  }
293
575
  async interrupt(conversation, turnId) {
294
- if (conversation.running === null || conversation.turn?.id !== turnId)
576
+ if ([...conversation.turns].some((turn) => turn.id === turnId))
577
+ await this.cancel(conversation);
578
+ }
579
+ async cancel(conversation) {
580
+ if (conversation.running === null)
295
581
  return;
296
582
  await conversation.running.adapter.connection.agent.notify('session/cancel', {
297
583
  sessionId: conversation.running.sessionId,
@@ -343,63 +629,83 @@ export class Conversations {
343
629
  }
344
630
  return route;
345
631
  }
346
- async start(conversation, route, sessionId) {
632
+ async start(conversation, routed, sessionId, empty, bridged) {
633
+ const route = await routed;
347
634
  const reader = readerFor(route.kind);
348
635
  reader.session = sessionId;
349
636
  const app = client({ name: '@agentshouse/kit' })
350
- .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));
637
+ .onRequest('session/request_permission', ({ params, signal }) => conversation.afk ? permissionAllowed(params) : this.ask(conversation, 'session/request_permission', params, false, signal))
638
+ .onRequest('elicitation/create', ({ params, signal }) => conversation.afk ? formAnswered(route.kind, params) : this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
639
+ .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))
640
+ .onRequest('_x.ai/ask_user_question', GROK_QUESTION, ({ params }) => {
641
+ if (!conversation.afk)
642
+ throw RequestError.methodNotFound('_x.ai/ask_user_question');
643
+ return grokQuestionAnswered(params);
644
+ });
352
645
  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);
646
+ const bridging = this.bridged(conversation);
647
+ const bridge = await step(unbridged, bridging);
354
648
  if (conversation.kills > 0) {
355
- bridge.close();
649
+ this.closeBridge(conversation, bridging, bridge);
356
650
  throw new Error(KILLED);
357
651
  }
358
652
  conversation.opening = bridge;
359
653
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
360
654
  const scope = new Scope(marker(bridge), randomUUID(), cli);
655
+ let adapter = null;
361
656
  try {
362
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
657
+ bridged(bridge);
363
658
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
364
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
659
+ const streamed = await step(NO_CONNECTION, work.open(cli));
660
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
365
661
  work.read(message);
366
662
  this.read(conversation, reader, message);
367
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
368
- void adapter.exited.then(() => bridge.close());
663
+ }, { ...bridge.env, ...streamed }, undefined, scope));
664
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
369
665
  const agent = adapter.connection.agent;
370
666
  const cwd = route.working_directory;
371
667
  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 };
668
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
669
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
375
670
  reader.session = opened.sessionId;
376
- if (sessionId === null && CLIS[route.kind].renames) {
671
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
377
672
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
378
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
673
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
379
674
  }
380
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
381
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
675
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
676
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
382
677
  void work.settled().then(() => scope.record());
383
678
  conversation.running = running;
679
+ adapter.turn(conversation.turns.size > 0);
384
680
  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));
681
+ conversation.options = options;
682
+ this.facts(conversation);
683
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
390
684
  return running.sessionId;
391
685
  }
392
686
  catch (error) {
393
- scope.kill();
687
+ adapter?.retire();
394
688
  work.close();
395
689
  conversation.leftovers.add(scope);
396
- bridge.close();
690
+ this.closeBridge(conversation, bridging, bridge);
397
691
  throw conversation.kills > 0 ? new Error(KILLED) : error;
398
692
  }
399
693
  finally {
400
694
  conversation.opening = null;
401
695
  }
402
696
  }
697
+ async resume(conversation, kind, adapter, request, empty) {
698
+ try {
699
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
700
+ }
701
+ catch (error) {
702
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
703
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
704
+ }
705
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
706
+ return null;
707
+ }
708
+ }
403
709
  async launchSettings(running, route, offered) {
404
710
  let options = offered;
405
711
  const settings = [
@@ -421,27 +727,45 @@ export class Conversations {
421
727
  }
422
728
  return options;
423
729
  }
424
- exited(conversation, adapter, cause) {
425
- const running = conversation.running;
426
- if (running?.adapter !== adapter)
427
- return;
730
+ exited(conversation, running, cause) {
428
731
  this.detach(conversation, running);
429
732
  if (conversation.turn !== null) {
430
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
733
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
431
734
  }
432
735
  this.next(conversation);
433
736
  this.kit.changed();
434
737
  }
435
738
  detach(conversation, running) {
739
+ if (conversation.bridge === running.bridging)
740
+ conversation.bridge = null;
436
741
  running.work.close();
437
742
  conversation.leftovers.add(running.scope);
438
743
  conversation.running = null;
439
744
  conversation.commands = null;
745
+ conversation.options = null;
746
+ if (conversation.subagents.size > 0) {
747
+ conversation.subagents.clear();
748
+ this.subagentsChanged(conversation);
749
+ }
440
750
  for (const [id, question] of this.questions) {
441
751
  if (question.conversation === conversation)
442
752
  this.questions.delete(id);
443
753
  }
444
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
754
+ this.process(conversation, false);
755
+ }
756
+ process(conversation, running) {
757
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
758
+ }
759
+ facts(conversation) {
760
+ this.process(conversation, true);
761
+ if (conversation.commands !== null)
762
+ this.commands(conversation, conversation.commands);
763
+ if (conversation.options !== null)
764
+ this.options(conversation, conversation.options);
765
+ }
766
+ subagentsChanged(conversation) {
767
+ const subagents = [...conversation.subagents.values()].map(({ description, activity }) => ({ description, activity }));
768
+ this.kit.send({ type: 'subagents', conversation_id: conversation.id, subagents });
445
769
  }
446
770
  commands(conversation, commands) {
447
771
  conversation.commands = commands;
@@ -452,11 +776,12 @@ export class Conversations {
452
776
  options(conversation, options) {
453
777
  if (conversation.running === null)
454
778
  return;
779
+ conversation.options = options;
455
780
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
456
781
  }
457
- deliver(conversation, turn, event, body) {
782
+ deliver(conversation, turn, act, body, current) {
458
783
  this.reporting++;
459
- const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
784
+ const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/${act}`, { conversation: conversation.id, turn: turn.id, ...(await body) }, current));
460
785
  conversation.reports = delivered
461
786
  .catch(() => undefined)
462
787
  .finally(() => {
@@ -466,7 +791,7 @@ export class Conversations {
466
791
  return delivered;
467
792
  }
468
793
  write(conversation, turn, operation) {
469
- if (!turn.admitted || turn.ended || !this.watched)
794
+ if (turn.ended || !this.watched)
470
795
  return;
471
796
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
472
797
  }
@@ -476,11 +801,12 @@ export class Conversations {
476
801
  }
477
802
  begin(conversation) {
478
803
  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);
804
+ conversation.turns.add(turn);
805
+ conversation.running?.adapter.turn(true);
806
+ void this.deliver(conversation, turn, 'turns/start', {})
807
+ .then((answer) => {
808
+ if (answer.run === true)
809
+ turn.quietUntil = Date.now() + ACTIVITY_REPORT_MS;
484
810
  })
485
811
  .catch(logged);
486
812
  conversation.turn = turn;
@@ -491,59 +817,84 @@ export class Conversations {
491
817
  this.kit.changed();
492
818
  return turn;
493
819
  }
494
- end(conversation, turn, failed) {
820
+ end(conversation, turn, failed, notice = null) {
495
821
  if (turn.ended)
496
822
  return;
823
+ if (failed !== null)
824
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
497
825
  if (conversation.turn === turn) {
498
826
  for (const event of conversation.running?.reader.closeTurn() ?? [])
499
827
  this.apply(conversation, turn, event);
500
828
  }
829
+ if (notice !== null)
830
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
501
831
  turn.ended = true;
502
- conversation.open--;
832
+ conversation.turns.delete(turn);
833
+ conversation.running?.adapter.turn(conversation.turns.size > 0);
503
834
  if (conversation.turn === turn)
504
835
  conversation.turn = null;
836
+ const tokens = conversation.tokens;
837
+ conversation.tokens = null;
505
838
  const body = Promise.allSettled(turn.uploads).then(() => ({
506
839
  parts: turn.parts.saved(),
507
840
  context: turn.parts.context,
508
- ...failed,
841
+ ...(tokens === null ? {} : { tokens }),
842
+ ...(failed === null ? {} : { failed: failed.failure }),
509
843
  }));
510
- void this.deliver(conversation, turn, 'ended', body).catch(logged);
844
+ void this.deliver(conversation, turn, 'turns/end', body).catch(logged);
511
845
  this.next(conversation);
512
846
  this.kit.changed();
513
847
  }
514
- prompt(conversation, prompt) {
848
+ prompt(conversation, prompt, taken) {
515
849
  const running = conversation.running;
516
850
  const turn = this.begin(conversation);
517
851
  turn.prompted = true;
518
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
852
+ turn.taken = taken;
853
+ conversation.prompted = running.sessionId;
854
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
855
+ this.took(turn);
856
+ const notice = answeredFailure(answer);
857
+ if (notice !== null) {
858
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
859
+ }
860
+ else {
861
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
862
+ }
863
+ }, async (error) => {
519
864
  if (running.adapter.connection.signal.aborted) {
520
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
865
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
521
866
  }
522
867
  else {
523
- if (conversation.open === 1 && conversation.running === running) {
524
- running.scope.kill();
868
+ this.took(turn);
869
+ if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
870
+ running.adapter.retire();
525
871
  this.detach(conversation, running);
526
872
  }
527
- this.end(conversation, turn, { failed: causeOf(error) });
873
+ this.end(conversation, turn, answeredWith(error));
528
874
  }
529
875
  });
530
876
  }
531
877
  started(conversation) {
532
- return conversation.turn ?? this.begin(conversation);
878
+ const turn = conversation.turn ?? this.begin(conversation);
879
+ this.took(turn);
880
+ return turn;
533
881
  }
534
- ended(conversation) {
882
+ ended(conversation, stop) {
535
883
  const turn = conversation.turn;
536
884
  if (turn === null)
537
885
  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();
886
+ const held = stop !== null && !turn.steered && [...conversation.subagents.values()].some((subagent) => subagent.turn === turn);
887
+ if (!turn.prompted || held) {
888
+ this.took(turn);
889
+ this.end(conversation, turn, stop === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
890
+ return;
546
891
  }
892
+ if (stop !== null)
893
+ return;
894
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
895
+ this.apply(conversation, turn, event);
896
+ conversation.turn = null;
897
+ this.kit.changed();
547
898
  }
548
899
  async ask(conversation, method, params, secret, signal) {
549
900
  const turn = this.started(conversation);
@@ -551,7 +902,7 @@ export class Conversations {
551
902
  const answered = new Promise((answer, fail) => {
552
903
  this.questions.set(interactionId, { conversation, answer: (response) => answer(response) });
553
904
  signal.addEventListener('abort', () => answer(null), { once: true });
554
- this.deliver(conversation, turn, 'interactions', {
905
+ this.deliver(conversation, turn, 'conversations/interaction/ask', {
555
906
  interaction_id: interactionId,
556
907
  request: { method, params },
557
908
  secret,
@@ -592,6 +943,18 @@ export class Conversations {
592
943
  read(conversation, reader, message) {
593
944
  for (const event of reader.read(message))
594
945
  this.event(conversation, event);
946
+ this.reportActivity(conversation);
947
+ }
948
+ reportActivity(conversation) {
949
+ const turn = conversation.turn;
950
+ if (turn === null || Date.now() < turn.quietUntil)
951
+ return;
952
+ turn.quietUntil = Infinity;
953
+ void this.deliver(conversation, turn, 'turns/activity/report', {}, () => false)
954
+ .catch(logged)
955
+ .finally(() => {
956
+ turn.quietUntil = Date.now() + ACTIVITY_REPORT_MS;
957
+ });
595
958
  }
596
959
  event(conversation, event) {
597
960
  switch (event.kind) {
@@ -599,7 +962,7 @@ export class Conversations {
599
962
  this.started(conversation);
600
963
  return;
601
964
  case 'ended':
602
- this.ended(conversation);
965
+ this.ended(conversation, event.stop);
603
966
  return;
604
967
  case 'commands':
605
968
  this.commands(conversation, event.commands);
@@ -611,9 +974,15 @@ export class Conversations {
611
974
  conversation.context = { used: event.used, window: event.window };
612
975
  conversation.turn?.parts.measure(conversation.context);
613
976
  return;
977
+ case 'tokens':
978
+ conversation.tokens = addedTokens(conversation.tokens, event.tokens);
979
+ return;
614
980
  case 'subagent-done':
615
981
  this.subagentDone(conversation, event.id, event.result);
616
982
  return;
983
+ case 'subagent-activity':
984
+ this.subagentActivity(conversation, event.id, event.activity);
985
+ return;
617
986
  case 'marker':
618
987
  case 'image':
619
988
  if (conversation.turn !== null)
@@ -624,6 +993,11 @@ export class Conversations {
624
993
  }
625
994
  this.apply(conversation, this.started(conversation), event);
626
995
  }
996
+ took(turn) {
997
+ const taken = turn.taken;
998
+ turn.taken = null;
999
+ taken?.();
1000
+ }
627
1001
  apply(conversation, turn, event) {
628
1002
  switch (event.kind) {
629
1003
  case 'text':
@@ -635,7 +1009,8 @@ export class Conversations {
635
1009
  return;
636
1010
  case 'subagent': {
637
1011
  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 });
1012
+ conversation.subagents.set(event.id, { description: event.description, turn, index, activity: null });
1013
+ this.subagentsChanged(conversation);
639
1014
  return;
640
1015
  }
641
1016
  case 'message':
@@ -658,6 +1033,7 @@ export class Conversations {
658
1033
  if (subagent === undefined)
659
1034
  return;
660
1035
  conversation.subagents.delete(id);
1036
+ this.subagentsChanged(conversation);
661
1037
  if (subagent.turn === conversation.turn) {
662
1038
  subagent.turn.parts.done(subagent.index, result);
663
1039
  return;
@@ -668,23 +1044,37 @@ export class Conversations {
668
1044
  else
669
1045
  conversation.turn.parts.start(part);
670
1046
  }
1047
+ subagentActivity(conversation, id, activity) {
1048
+ const subagent = conversation.subagents.get(id);
1049
+ if (subagent === undefined || subagent.activity === activity)
1050
+ return;
1051
+ subagent.activity = activity;
1052
+ this.subagentsChanged(conversation);
1053
+ }
671
1054
  async image(conversation, turn, reserved, data, mediaType) {
672
1055
  const bytes = Buffer.from(data, 'base64');
673
1056
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
1057
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
674
1058
  try {
675
- const declared = (await this.deliver(conversation, turn, 'images', {
1059
+ const declared = (await this.deliver(conversation, turn, 'turns/images/declare', {
676
1060
  name,
677
1061
  media_type: mediaType,
678
1062
  bytes: bytes.length,
679
- sha256: createHash('sha256').update(bytes).digest('hex'),
1063
+ sha256,
680
1064
  }));
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);
1065
+ const saved = await uploaded(declared.upload, {
1066
+ name,
1067
+ bytes: bytes.length,
1068
+ sha256,
1069
+ read: (start, end) => [bytes.subarray(start, end)],
1070
+ send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
1071
+ finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
1072
+ });
1073
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
685
1074
  }
686
1075
  catch (error) {
687
- turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
1076
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
1077
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
688
1078
  }
689
1079
  }
690
1080
  }