@agentshouse/kit 0.2.1-cbdf76b9a90a3 → 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 (142) hide show
  1. package/README.md +28 -28
  2. package/dist/acp.js +58 -7
  3. package/dist/afk.js +17 -0
  4. package/dist/agents.js +55 -32
  5. package/dist/api.js +1 -1
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +1 -1
  8. package/dist/building.js +50 -0
  9. package/dist/calls.js +71 -0
  10. package/dist/chunks.js +49 -0
  11. package/dist/clis.js +45 -40
  12. package/dist/{stream.js → connection.js} +28 -46
  13. package/dist/conversations.js +330 -125
  14. package/dist/copies.js +286 -149
  15. package/dist/end.js +11 -5
  16. package/dist/events.js +66 -8
  17. package/dist/files.js +139 -37
  18. package/dist/git.js +47 -13
  19. package/dist/home.js +0 -7
  20. package/dist/house-cli.js +95 -87
  21. package/dist/house-main.js +3 -3
  22. package/dist/instructions.js +29 -9
  23. package/dist/kit-cli.js +4 -4
  24. package/dist/kit-main.js +3 -3
  25. package/dist/login.js +4 -4
  26. package/dist/mcp.js +6 -2
  27. package/dist/parts.js +10 -0
  28. package/dist/placement.js +10 -18
  29. package/dist/relay.js +21 -0
  30. package/dist/resident.js +49 -38
  31. package/dist/scope.js +12 -4
  32. package/dist/scripts.js +255 -0
  33. package/dist/service.js +1 -1
  34. package/dist/sign-in.js +22 -21
  35. package/dist/silence.js +28 -0
  36. package/dist/skills.js +42 -97
  37. package/dist/transcription.js +83 -43
  38. package/dist/update.js +113 -124
  39. package/dist/wire.js +23 -5
  40. package/package.json +20 -21
  41. package/dist/bootstrap.json +0 -24
  42. package/dist/clis-main.js +0 -23
  43. package/dist/inputs.js +0 -63
  44. package/how-we-work.md +0 -33
  45. package/skills/ask-matt/LICENSE +0 -21
  46. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  47. package/skills/ask-matt/SKILL.md +0 -97
  48. package/skills/ask-matt/agents/openai.yaml +0 -5
  49. package/skills/code-review/LICENSE +0 -21
  50. package/skills/code-review/SKILL.md +0 -89
  51. package/skills/code-review/agents/openai.yaml +0 -3
  52. package/skills/codebase-design/DEEPENING.md +0 -37
  53. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  54. package/skills/codebase-design/LICENSE +0 -21
  55. package/skills/codebase-design/SKILL.md +0 -114
  56. package/skills/codebase-design/agents/openai.yaml +0 -3
  57. package/skills/diagnosing-bugs/LICENSE +0 -21
  58. package/skills/diagnosing-bugs/SKILL.md +0 -138
  59. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  60. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  61. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  62. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  63. package/skills/domain-modeling/LICENSE +0 -21
  64. package/skills/domain-modeling/SKILL.md +0 -74
  65. package/skills/domain-modeling/agents/openai.yaml +0 -3
  66. package/skills/grill-me/LICENSE +0 -21
  67. package/skills/grill-me/SKILL.md +0 -7
  68. package/skills/grill-me/agents/openai.yaml +0 -5
  69. package/skills/grill-with-docs/LICENSE +0 -21
  70. package/skills/grill-with-docs/SKILL.md +0 -7
  71. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  72. package/skills/grilling/LICENSE +0 -21
  73. package/skills/grilling/SKILL.md +0 -30
  74. package/skills/grilling/agents/openai.yaml +0 -3
  75. package/skills/handoff/LICENSE +0 -21
  76. package/skills/handoff/SKILL.md +0 -16
  77. package/skills/handoff/agents/openai.yaml +0 -5
  78. package/skills/implement/LICENSE +0 -21
  79. package/skills/implement/SKILL.md +0 -17
  80. package/skills/implement/agents/openai.yaml +0 -5
  81. package/skills/implement-spec/LICENSE +0 -21
  82. package/skills/implement-spec/SKILL.md +0 -40
  83. package/skills/implement-spec/agents/openai.yaml +0 -5
  84. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  85. package/skills/improve-codebase-architecture/LICENSE +0 -21
  86. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  87. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  88. package/skills/pr/CREDITS.md +0 -3
  89. package/skills/pr/LICENSE +0 -21
  90. package/skills/pr/SKILL.md +0 -170
  91. package/skills/pr/agents/openai.yaml +0 -3
  92. package/skills/prototype/LICENSE +0 -21
  93. package/skills/prototype/LOGIC.md +0 -67
  94. package/skills/prototype/SKILL.md +0 -26
  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 -12
  99. package/skills/research/agents/openai.yaml +0 -3
  100. package/skills/retro/LICENSE +0 -21
  101. package/skills/retro/SKILL.md +0 -44
  102. package/skills/retro/agents/openai.yaml +0 -5
  103. package/skills/tdd/LICENSE +0 -21
  104. package/skills/tdd/SKILL.md +0 -38
  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 -140
  114. package/skills/teach/agents/openai.yaml +0 -5
  115. package/skills/to-questionnaire/LICENSE +0 -21
  116. package/skills/to-questionnaire/SKILL.md +0 -54
  117. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  118. package/skills/to-spec/LICENSE +0 -21
  119. package/skills/to-spec/SKILL.md +0 -75
  120. package/skills/to-spec/agents/openai.yaml +0 -5
  121. package/skills/to-tickets/LICENSE +0 -21
  122. package/skills/to-tickets/SKILL.md +0 -105
  123. package/skills/to-tickets/agents/openai.yaml +0 -5
  124. package/skills/triage/AGENT-BRIEF.md +0 -207
  125. package/skills/triage/LICENSE +0 -21
  126. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  127. package/skills/triage/SKILL.md +0 -112
  128. package/skills/triage/agents/openai.yaml +0 -5
  129. package/skills/wait-what/LICENSE +0 -21
  130. package/skills/wait-what/SKILL.md +0 -7
  131. package/skills/wait-what/agents/openai.yaml +0 -5
  132. package/skills/wayfinder/LICENSE +0 -21
  133. package/skills/wayfinder/SKILL.md +0 -128
  134. package/skills/wayfinder/agents/openai.yaml +0 -5
  135. package/skills/wizard/LICENSE +0 -21
  136. package/skills/wizard/SKILL.md +0 -44
  137. package/skills/wizard/agents/openai.yaml +0 -3
  138. package/skills/wizard/template.sh +0 -223
  139. package/skills/writing-for-agents/LICENSE +0 -21
  140. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  141. package/skills/writing-for-agents/SKILL.md +0 -81
  142. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -3,23 +3,24 @@ 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';
7
- import { finalRefusal } from './api.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';
8
9
  import { openBridge } from './bridge.js';
9
10
  import { CLIS } from './clis.js';
10
11
  import { answeredFailure, readerFor } from './events.js';
11
- import { placeFiles, uploadBytes } from './files.js';
12
+ import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
12
13
  import { agentBase } from './home.js';
13
14
  import { instructions } from './instructions.js';
14
- import { Parts } from './parts.js';
15
+ import { addedTokens, Parts } from './parts.js';
15
16
  import { refusalOf } from './refusals.js';
17
+ import { relayed } from './relay.js';
16
18
  import { killMarked, Scope } from './scope.js';
17
19
  import { holdSecretInput } from './secret-input.js';
18
- import { createHowWeWork } from './skills.js';
19
20
  import { transcribe } from './transcription.js';
20
21
  import { workFor } from './work.js';
21
- const SIGNED_OUT = RequestError.authRequired().code;
22
22
  const KILLED = 'the conversation was killed';
23
+ const INTERRUPTED = 'the conversation was interrupted';
23
24
  const REFUSED = 'the CLI refused the prompt';
24
25
  const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
25
26
  const GONE = 'House no longer holds this conversation; start a new one';
@@ -33,8 +34,13 @@ const NO_SESSION = "the Agent's CLI did not open a session";
33
34
  const NO_RESUME = "the Agent's CLI could not resume this conversation";
34
35
  const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
35
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;
36
40
  class NotStarted extends Error {
37
41
  }
42
+ class ResumeRefused extends NotStarted {
43
+ }
38
44
  async function step(reason, work) {
39
45
  try {
40
46
  return await work;
@@ -43,9 +49,6 @@ async function step(reason, work) {
43
49
  throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
44
50
  }
45
51
  }
46
- function signedOut(error) {
47
- return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
48
- }
49
52
  function unopened(reason) {
50
53
  return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
51
54
  }
@@ -62,7 +65,12 @@ const EXIT_PLAN_MODE = {
62
65
  return value;
63
66
  },
64
67
  };
65
- export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
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']);
66
74
  function causeOf(error) {
67
75
  return error instanceof Error ? error.message : String(error);
68
76
  }
@@ -70,32 +78,51 @@ function answeredWith(error) {
70
78
  return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
71
79
  }
72
80
  function exitedWith(running, cause) {
73
- return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
81
+ if (running.killed)
82
+ return { failure: 'killed', cause: KILLED };
83
+ return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
74
84
  }
75
85
  function answerMessage(request, response) {
76
86
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
77
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
+ }
78
94
  function logged(error) {
79
95
  process.stderr.write(`kit: ${causeOf(error)}\n`);
80
96
  }
81
97
  function refusal(conversation, error) {
82
98
  if (conversation.kills > 0)
83
99
  return KILLED;
84
- const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
100
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
85
101
  logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
86
102
  return causeOf(error);
87
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
+ }
88
110
  function marker(bridge) {
89
111
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
90
112
  }
113
+ function steers(adapter) {
114
+ return adapter.initialized._meta?.steering?.supported === true;
115
+ }
91
116
  class Turn {
92
117
  id = randomUUID();
93
118
  parts;
94
119
  questions = 0;
95
120
  secrets = 0;
96
121
  prompted = false;
122
+ steered = false;
97
123
  taken = null;
98
124
  ended = false;
125
+ quietUntil = Infinity;
99
126
  images = 0;
100
127
  uploads = [];
101
128
  constructor(emit) {
@@ -110,20 +137,24 @@ class Conversation {
110
137
  commands = null;
111
138
  options = null;
112
139
  exitUntold = false;
140
+ afk = false;
113
141
  context = null;
142
+ tokens = null;
114
143
  leftovers = new Set();
115
144
  waiting = [];
145
+ done = false;
116
146
  sending = null;
117
147
  subagents = new Map();
118
148
  held = [];
119
149
  opening = null;
150
+ bridge = null;
120
151
  launch = null;
121
152
  prompted = null;
122
153
  kills = 0;
123
154
  killed = new AbortController();
124
155
  interrupts = 0;
125
156
  interrupted = new AbortController();
126
- open = 0;
157
+ turns = new Set();
127
158
  queue = Promise.resolve();
128
159
  reports = Promise.resolve();
129
160
  starting = null;
@@ -154,23 +185,25 @@ export class Conversations {
154
185
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
155
186
  setInterval(() => this.look(), 2_000);
156
187
  }
157
- input(input) {
158
- void this.kit.inputs.carry(input.input_id, () => {
159
- const conversation = this.conversation(input.conversation_id);
160
- if (input.kind === 'kill') {
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') {
161
194
  conversation.kills++;
162
195
  conversation.killed.abort(new Error(KILLED));
163
196
  for (const waiting of conversation.waiting.splice(0))
164
197
  waiting.settle({ refused: KILLED });
165
198
  this.halt(conversation);
166
199
  }
167
- if (input.kind === 'interrupt') {
200
+ if (call.kind === 'interrupt') {
168
201
  conversation.interrupts++;
169
202
  conversation.interrupted.abort();
170
203
  }
171
- const handled = conversation.queue.then(() => this.carry(conversation, input));
204
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
172
205
  conversation.queue = handled.catch(() => undefined);
173
- return handled.then(async ({ ack }) => ({ ack: await ack }));
206
+ return handled.then(async ({ result }) => ({ result: await result }));
174
207
  });
175
208
  }
176
209
  idle() {
@@ -179,7 +212,7 @@ export class Conversations {
179
212
  for (const conversation of this.conversations.values()) {
180
213
  const turn = conversation.turn;
181
214
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
182
- if (conversation.open > (asking ? 1 : 0))
215
+ if (conversation.turns.size > (asking ? 1 : 0))
183
216
  return false;
184
217
  }
185
218
  const conversations = [...this.conversations.values()];
@@ -188,7 +221,7 @@ export class Conversations {
188
221
  return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
189
222
  }
190
223
  quiet() {
191
- return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
224
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
192
225
  }
193
226
  look() {
194
227
  for (const conversation of this.conversations.values()) {
@@ -204,8 +237,10 @@ export class Conversations {
204
237
  opened() {
205
238
  this.watched = false;
206
239
  for (const conversation of this.conversations.values()) {
207
- if (conversation.running !== null)
240
+ if (conversation.running !== null) {
208
241
  this.facts(conversation);
242
+ this.subagentsChanged(conversation);
243
+ }
209
244
  else if (conversation.exitUntold)
210
245
  this.process(conversation, false);
211
246
  if (conversation.untold || conversation.told !== null)
@@ -230,40 +265,51 @@ export class Conversations {
230
265
  }
231
266
  return conversation;
232
267
  }
233
- async carry(conversation, input) {
234
- if (input.kind === 'open')
235
- return { ack: this.open(conversation, input) };
236
- if (input.kind === 'message')
237
- return this.message(conversation, input);
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
+ }
238
278
  await this.launched(conversation);
239
- if (input.kind === 'interrupt') {
279
+ if (call.kind === 'interrupt') {
240
280
  if (--conversation.interrupts === 0)
241
281
  conversation.interrupted = new AbortController();
242
- await this.interrupt(conversation, String(input.turn_id));
282
+ await this.interrupt(conversation, String(call.turn_id));
243
283
  }
244
- if (input.kind === 'kill') {
284
+ if (call.kind === 'kill') {
245
285
  await this.kill(conversation);
246
286
  await conversation.sending;
247
287
  if (--conversation.kills === 0)
248
288
  conversation.killed = new AbortController();
249
289
  }
250
- if (input.kind === 'option')
251
- await this.option(conversation, String(input.option), input.value);
252
- if (input.kind === 'answer') {
253
- 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));
254
294
  if (question === undefined) {
255
- const text = answerMessage(input.request, input.response);
256
- 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 });
257
297
  }
258
- question.answer(input.response);
298
+ question.answer(call.response);
259
299
  }
260
- return { ack: Promise.resolve({}) };
300
+ return { result: Promise.resolve({}) };
261
301
  }
262
- open(conversation, input) {
263
- if (conversation.running !== null)
264
- return Promise.resolve({});
265
- const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, input.agent_id), null, true);
266
- return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
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;
267
313
  }
268
314
  launch(conversation, route, sessionId, empty) {
269
315
  let opened;
@@ -283,6 +329,7 @@ export class Conversations {
283
329
  if (conversation.launch !== launch)
284
330
  return;
285
331
  conversation.launch = null;
332
+ this.closeIdleBridge(conversation);
286
333
  this.next(conversation);
287
334
  });
288
335
  this.next(conversation);
@@ -300,44 +347,93 @@ export class Conversations {
300
347
  read.catch(() => undefined);
301
348
  return read;
302
349
  }
303
- async message(conversation, input) {
304
- const content = [{ type: 'text', text: String(input.text) }];
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
+ ];
305
355
  let route;
306
356
  let instructing = null;
307
357
  try {
308
- route = await this.route(conversation, input.agent_id);
309
- if (input.first === true)
358
+ route = await this.route(conversation, call.agent_id);
359
+ if (call.first === true)
310
360
  instructing = this.instructed(conversation, route);
311
- const files = input.files;
312
- if (files.length > 0)
313
- this.prepares(conversation, 'files');
314
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
315
- const lines = await this.transcribed(conversation, files, paths);
316
- if (lines.length > 0)
317
- content.push({ type: 'text', text: lines.join('\n') });
361
+ if (call.message_id !== null)
362
+ content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
318
363
  if (conversation.kills > 0)
319
364
  throw new Error(KILLED);
320
365
  }
321
366
  catch (error) {
367
+ this.closeIdleBridge(conversation);
322
368
  this.prepares(conversation, null);
323
- return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
369
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
324
370
  }
325
371
  conversation.preparing = null;
326
- return {
327
- ack: new Promise((settle) => {
328
- conversation.waiting.push({ input, route, content, instructing, settle });
329
- this.next(conversation);
330
- }),
331
- };
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 };
380
+ }
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;
401
+ try {
402
+ ({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
403
+ sessionId: running.sessionId,
404
+ prompt: waiting.content,
405
+ _meta: { steering: { idleBehavior: 'promptRequired' } },
406
+ }));
407
+ }
408
+ catch (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 })) });
425
+ }
332
426
  }
333
427
  next(conversation) {
334
- if (conversation.sending !== null || conversation.open > 0)
428
+ if (conversation.sending !== null || conversation.turns.size > 0)
335
429
  return;
336
430
  const waiting = conversation.waiting.shift();
337
431
  if (waiting === undefined) {
338
432
  const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
339
433
  if (conversation.starting !== step)
340
434
  this.starts(conversation, step);
435
+ if (conversation.done && conversation.launch === null)
436
+ void this.stop(conversation).catch(logged);
341
437
  return;
342
438
  }
343
439
  conversation.sending = this.send(conversation, waiting).finally(() => {
@@ -346,8 +442,8 @@ export class Conversations {
346
442
  });
347
443
  }
348
444
  async send(conversation, waiting) {
349
- const { input } = waiting;
350
- let ack = {};
445
+ const { call } = waiting;
446
+ let result = {};
351
447
  let started = false;
352
448
  let instructing = waiting.instructing;
353
449
  const prompt = [];
@@ -357,18 +453,18 @@ export class Conversations {
357
453
  await this.launched(conversation);
358
454
  }
359
455
  if (conversation.running === null) {
360
- const stored = conversation.session ?? input.provider_session_id;
361
- const empty = input.first === true && conversation.prompted !== stored;
456
+ const stored = conversation.session ?? call.provider_session_id;
457
+ const empty = call.first === true && conversation.prompted !== stored;
362
458
  const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
363
- if (input.first === true)
459
+ if (call.first === true)
364
460
  instructing = this.instructed(conversation, waiting.route);
365
461
  started = (await launch.session) !== stored;
366
462
  }
367
463
  const session = conversation.running.sessionId;
368
- if (started || (input.first === true && session !== input.provider_session_id)) {
369
- ack = { provider_session_id: session };
464
+ if (started || (call.first === true && session !== call.provider_session_id)) {
465
+ result = { provider_session_id: session };
370
466
  }
371
- if (input.first === true) {
467
+ if (call.first === true) {
372
468
  prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
373
469
  }
374
470
  if (conversation.kills > 0)
@@ -380,12 +476,14 @@ export class Conversations {
380
476
  conversation.session = null;
381
477
  await this.kill(conversation);
382
478
  }
383
- waiting.settle({ refused: refusal(conversation, 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 });
384
482
  return;
385
483
  }
386
484
  this.starts(conversation, null);
387
- this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.inputs.hands(String(input.input_id), ack));
388
- waiting.settle(ack);
485
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
486
+ waiting.settle(result);
389
487
  }
390
488
  starts(conversation, step) {
391
489
  conversation.starting = step;
@@ -415,28 +513,72 @@ export class Conversations {
415
513
  retell(conversation) {
416
514
  conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
417
515
  }
418
- async transcribed(conversation, files, paths) {
419
- const lines = [];
420
- for (const [index, path] of paths.entries()) {
421
- lines.push(path);
422
- if (files[index].media_type?.startsWith('audio/') !== true)
423
- continue;
424
- this.prepares(conversation, 'transcription');
425
- try {
426
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
427
- if (text !== null && text !== '')
428
- lines.push(`Transcript: ${text}`);
429
- }
430
- catch (error) {
431
- if (conversation.kills > 0)
432
- throw error;
433
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
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}` });
434
544
  }
435
545
  }
436
- 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);
437
575
  }
438
576
  async interrupt(conversation, turnId) {
439
- 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)
440
582
  return;
441
583
  await conversation.running.adapter.connection.agent.notify('session/cancel', {
442
584
  sessionId: conversation.running.sessionId,
@@ -493,13 +635,19 @@ export class Conversations {
493
635
  const reader = readerFor(route.kind);
494
636
  reader.session = sessionId;
495
637
  const app = client({ name: '@agentshouse/kit' })
496
- .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
497
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
498
- .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, 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
+ });
499
646
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
500
- const bridge = await step(unbridged, 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);
501
649
  if (conversation.kills > 0) {
502
- bridge.close();
650
+ this.closeBridge(conversation, bridging, bridge);
503
651
  throw new Error(KILLED);
504
652
  }
505
653
  conversation.opening = bridge;
@@ -507,7 +655,6 @@ export class Conversations {
507
655
  const scope = new Scope(marker(bridge), randomUUID(), cli);
508
656
  let adapter = null;
509
657
  try {
510
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
511
658
  bridged(bridge);
512
659
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
513
660
  const streamed = await step(NO_CONNECTION, work.open(cli));
@@ -515,7 +662,7 @@ export class Conversations {
515
662
  work.read(message);
516
663
  this.read(conversation, reader, message);
517
664
  }, { ...bridge.env, ...streamed }, undefined, scope));
518
- void adapter.exited.then(() => bridge.close());
665
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
519
666
  const agent = adapter.connection.agent;
520
667
  const cwd = route.working_directory;
521
668
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
@@ -526,10 +673,11 @@ export class Conversations {
526
673
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
527
674
  await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
528
675
  }
529
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
676
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
530
677
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
531
678
  void work.settled().then(() => scope.record());
532
679
  conversation.running = running;
680
+ adapter.turn(conversation.turns.size > 0);
533
681
  conversation.session = running.sessionId;
534
682
  conversation.options = options;
535
683
  this.facts(conversation);
@@ -540,7 +688,7 @@ export class Conversations {
540
688
  adapter?.retire();
541
689
  work.close();
542
690
  conversation.leftovers.add(scope);
543
- bridge.close();
691
+ this.closeBridge(conversation, bridging, bridge);
544
692
  throw conversation.kills > 0 ? new Error(KILLED) : error;
545
693
  }
546
694
  finally {
@@ -553,7 +701,7 @@ export class Conversations {
553
701
  }
554
702
  catch (error) {
555
703
  if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
556
- throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
704
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
557
705
  }
558
706
  logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
559
707
  return null;
@@ -589,11 +737,17 @@ export class Conversations {
589
737
  this.kit.changed();
590
738
  }
591
739
  detach(conversation, running) {
740
+ if (conversation.bridge === running.bridging)
741
+ conversation.bridge = null;
592
742
  running.work.close();
593
743
  conversation.leftovers.add(running.scope);
594
744
  conversation.running = null;
595
745
  conversation.commands = null;
596
746
  conversation.options = null;
747
+ if (conversation.subagents.size > 0) {
748
+ conversation.subagents.clear();
749
+ this.subagentsChanged(conversation);
750
+ }
597
751
  for (const [id, question] of this.questions) {
598
752
  if (question.conversation === conversation)
599
753
  this.questions.delete(id);
@@ -610,6 +764,10 @@ export class Conversations {
610
764
  if (conversation.options !== null)
611
765
  this.options(conversation, conversation.options);
612
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 });
770
+ }
613
771
  commands(conversation, commands) {
614
772
  conversation.commands = commands;
615
773
  if (conversation.running === null)
@@ -622,9 +780,9 @@ export class Conversations {
622
780
  conversation.options = options;
623
781
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
624
782
  }
625
- deliver(conversation, turn, event, body) {
783
+ deliver(conversation, turn, event, body, current) {
626
784
  this.reporting++;
627
- 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));
628
786
  conversation.reports = delivered
629
787
  .catch(() => undefined)
630
788
  .finally(() => {
@@ -644,8 +802,14 @@ export class Conversations {
644
802
  }
645
803
  begin(conversation) {
646
804
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
647
- conversation.open++;
648
- void this.deliver(conversation, turn, 'started', {}).catch(logged);
805
+ conversation.turns.add(turn);
806
+ conversation.running?.adapter.turn(true);
807
+ void this.deliver(conversation, turn, 'started', {})
808
+ .then((answer) => {
809
+ if (answer.run === true)
810
+ turn.quietUntil = Date.now() + ACTIVITY_REPORT_MS;
811
+ })
812
+ .catch(logged);
649
813
  conversation.turn = turn;
650
814
  if (conversation.context !== null)
651
815
  turn.parts.measure(conversation.context);
@@ -666,12 +830,16 @@ export class Conversations {
666
830
  if (notice !== null)
667
831
  turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
668
832
  turn.ended = true;
669
- conversation.open--;
833
+ conversation.turns.delete(turn);
834
+ conversation.running?.adapter.turn(conversation.turns.size > 0);
670
835
  if (conversation.turn === turn)
671
836
  conversation.turn = null;
837
+ const tokens = conversation.tokens;
838
+ conversation.tokens = null;
672
839
  const body = Promise.allSettled(turn.uploads).then(() => ({
673
840
  parts: turn.parts.saved(),
674
841
  context: turn.parts.context,
842
+ ...(tokens === null ? {} : { tokens }),
675
843
  ...(failed === null ? {} : { failed: failed.failure }),
676
844
  }));
677
845
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
@@ -699,7 +867,7 @@ export class Conversations {
699
867
  }
700
868
  else {
701
869
  this.took(turn);
702
- if (conversation.open === 1 && conversation.running === running) {
870
+ if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
703
871
  running.adapter.retire();
704
872
  this.detach(conversation, running);
705
873
  }
@@ -712,19 +880,22 @@ export class Conversations {
712
880
  this.took(turn);
713
881
  return turn;
714
882
  }
715
- ended(conversation) {
883
+ ended(conversation, stop) {
716
884
  const turn = conversation.turn;
717
885
  if (turn === null)
718
886
  return;
719
- if (!turn.prompted) {
720
- this.end(conversation, turn, null);
721
- }
722
- else {
723
- for (const event of conversation.running?.reader.closeTurn() ?? [])
724
- this.apply(conversation, turn, event);
725
- conversation.turn = null;
726
- 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;
727
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();
728
899
  }
729
900
  async ask(conversation, method, params, secret, signal) {
730
901
  const turn = this.started(conversation);
@@ -773,6 +944,18 @@ export class Conversations {
773
944
  read(conversation, reader, message) {
774
945
  for (const event of reader.read(message))
775
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
+ });
776
959
  }
777
960
  event(conversation, event) {
778
961
  switch (event.kind) {
@@ -780,7 +963,7 @@ export class Conversations {
780
963
  this.started(conversation);
781
964
  return;
782
965
  case 'ended':
783
- this.ended(conversation);
966
+ this.ended(conversation, event.stop);
784
967
  return;
785
968
  case 'commands':
786
969
  this.commands(conversation, event.commands);
@@ -792,9 +975,15 @@ export class Conversations {
792
975
  conversation.context = { used: event.used, window: event.window };
793
976
  conversation.turn?.parts.measure(conversation.context);
794
977
  return;
978
+ case 'tokens':
979
+ conversation.tokens = addedTokens(conversation.tokens, event.tokens);
980
+ return;
795
981
  case 'subagent-done':
796
982
  this.subagentDone(conversation, event.id, event.result);
797
983
  return;
984
+ case 'subagent-activity':
985
+ this.subagentActivity(conversation, event.id, event.activity);
986
+ return;
798
987
  case 'marker':
799
988
  case 'image':
800
989
  if (conversation.turn !== null)
@@ -821,7 +1010,8 @@ export class Conversations {
821
1010
  return;
822
1011
  case 'subagent': {
823
1012
  const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
824
- 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);
825
1015
  return;
826
1016
  }
827
1017
  case 'message':
@@ -844,6 +1034,7 @@ export class Conversations {
844
1034
  if (subagent === undefined)
845
1035
  return;
846
1036
  conversation.subagents.delete(id);
1037
+ this.subagentsChanged(conversation);
847
1038
  if (subagent.turn === conversation.turn) {
848
1039
  subagent.turn.parts.done(subagent.index, result);
849
1040
  return;
@@ -854,23 +1045,37 @@ export class Conversations {
854
1045
  else
855
1046
  conversation.turn.parts.start(part);
856
1047
  }
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
+ }
857
1055
  async image(conversation, turn, reserved, data, mediaType) {
858
1056
  const bytes = Buffer.from(data, 'base64');
859
1057
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
1058
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
860
1059
  try {
861
1060
  const declared = (await this.deliver(conversation, turn, 'images', {
862
1061
  name,
863
1062
  media_type: mediaType,
864
1063
  bytes: bytes.length,
865
- sha256: createHash('sha256').update(bytes).digest('hex'),
1064
+ sha256,
866
1065
  }));
867
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
868
- if (saved.save.status !== 'saved')
869
- throw new Error(saved.save.failure ?? saved.save.status);
870
- turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
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);
871
1075
  }
872
1076
  catch (error) {
873
- turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
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);
874
1079
  }
875
1080
  }
876
1081
  }