@agentshouse/kit 0.2.1-c845e172df1af → 0.2.1-c849d8d4ab62f

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 (131) hide show
  1. package/README.md +6 -6
  2. package/dist/acp.js +57 -6
  3. package/dist/agents.js +6 -9
  4. package/dist/authority-main.js +2 -2
  5. package/dist/bridge.js +1 -1
  6. package/dist/building.js +46 -0
  7. package/dist/calls.js +11 -3
  8. package/dist/clis-main.js +2 -2
  9. package/dist/clis.js +39 -15
  10. package/dist/connection.js +20 -29
  11. package/dist/conversations.js +235 -81
  12. package/dist/end.js +8 -2
  13. package/dist/events.js +29 -8
  14. package/dist/files.js +53 -40
  15. package/dist/git.js +2 -2
  16. package/dist/house-cli.js +31 -59
  17. package/dist/house-main.js +3 -3
  18. package/dist/instructions.js +29 -9
  19. package/dist/kit-cli.js +2 -2
  20. package/dist/kit-main.js +3 -3
  21. package/dist/login.js +4 -4
  22. package/dist/mcp.js +2 -0
  23. package/dist/relay.js +21 -0
  24. package/dist/resident.js +17 -6
  25. package/dist/scripts.js +255 -0
  26. package/dist/sign-in.js +3 -2
  27. package/dist/silence.js +28 -0
  28. package/dist/skills.js +41 -96
  29. package/dist/transcription.js +83 -43
  30. package/dist/update.js +51 -8
  31. package/package.json +19 -20
  32. package/dist/bootstrap.json +0 -24
  33. package/how-we-work.md +0 -33
  34. package/skills/ask-matt/LICENSE +0 -21
  35. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  36. package/skills/ask-matt/SKILL.md +0 -97
  37. package/skills/ask-matt/agents/openai.yaml +0 -5
  38. package/skills/code-review/LICENSE +0 -21
  39. package/skills/code-review/SKILL.md +0 -89
  40. package/skills/code-review/agents/openai.yaml +0 -3
  41. package/skills/codebase-design/DEEPENING.md +0 -37
  42. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  43. package/skills/codebase-design/LICENSE +0 -21
  44. package/skills/codebase-design/SKILL.md +0 -114
  45. package/skills/codebase-design/agents/openai.yaml +0 -3
  46. package/skills/diagnosing-bugs/LICENSE +0 -21
  47. package/skills/diagnosing-bugs/SKILL.md +0 -138
  48. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  49. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  50. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  51. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  52. package/skills/domain-modeling/LICENSE +0 -21
  53. package/skills/domain-modeling/SKILL.md +0 -74
  54. package/skills/domain-modeling/agents/openai.yaml +0 -3
  55. package/skills/grill-me/LICENSE +0 -21
  56. package/skills/grill-me/SKILL.md +0 -7
  57. package/skills/grill-me/agents/openai.yaml +0 -5
  58. package/skills/grill-with-docs/LICENSE +0 -21
  59. package/skills/grill-with-docs/SKILL.md +0 -7
  60. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  61. package/skills/grilling/LICENSE +0 -21
  62. package/skills/grilling/SKILL.md +0 -30
  63. package/skills/grilling/agents/openai.yaml +0 -3
  64. package/skills/handoff/LICENSE +0 -21
  65. package/skills/handoff/SKILL.md +0 -16
  66. package/skills/handoff/agents/openai.yaml +0 -5
  67. package/skills/implement/LICENSE +0 -21
  68. package/skills/implement/SKILL.md +0 -17
  69. package/skills/implement/agents/openai.yaml +0 -5
  70. package/skills/implement-spec/LICENSE +0 -21
  71. package/skills/implement-spec/SKILL.md +0 -40
  72. package/skills/implement-spec/agents/openai.yaml +0 -5
  73. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  74. package/skills/improve-codebase-architecture/LICENSE +0 -21
  75. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  76. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  77. package/skills/pr/CREDITS.md +0 -3
  78. package/skills/pr/LICENSE +0 -21
  79. package/skills/pr/SKILL.md +0 -170
  80. package/skills/pr/agents/openai.yaml +0 -3
  81. package/skills/prototype/LICENSE +0 -21
  82. package/skills/prototype/LOGIC.md +0 -67
  83. package/skills/prototype/SKILL.md +0 -26
  84. package/skills/prototype/UI.md +0 -112
  85. package/skills/prototype/agents/openai.yaml +0 -3
  86. package/skills/research/LICENSE +0 -21
  87. package/skills/research/SKILL.md +0 -12
  88. package/skills/research/agents/openai.yaml +0 -3
  89. package/skills/retro/LICENSE +0 -21
  90. package/skills/retro/SKILL.md +0 -44
  91. package/skills/retro/agents/openai.yaml +0 -5
  92. package/skills/tdd/LICENSE +0 -21
  93. package/skills/tdd/SKILL.md +0 -38
  94. package/skills/tdd/agents/openai.yaml +0 -3
  95. package/skills/tdd/mocking.md +0 -59
  96. package/skills/tdd/tests.md +0 -77
  97. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  98. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  99. package/skills/teach/LICENSE +0 -21
  100. package/skills/teach/MISSION-FORMAT.md +0 -31
  101. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  102. package/skills/teach/SKILL.md +0 -140
  103. package/skills/teach/agents/openai.yaml +0 -5
  104. package/skills/to-questionnaire/LICENSE +0 -21
  105. package/skills/to-questionnaire/SKILL.md +0 -54
  106. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  107. package/skills/to-spec/LICENSE +0 -21
  108. package/skills/to-spec/SKILL.md +0 -75
  109. package/skills/to-spec/agents/openai.yaml +0 -5
  110. package/skills/to-tickets/LICENSE +0 -21
  111. package/skills/to-tickets/SKILL.md +0 -105
  112. package/skills/to-tickets/agents/openai.yaml +0 -5
  113. package/skills/triage/AGENT-BRIEF.md +0 -207
  114. package/skills/triage/LICENSE +0 -21
  115. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  116. package/skills/triage/SKILL.md +0 -112
  117. package/skills/triage/agents/openai.yaml +0 -5
  118. package/skills/wait-what/LICENSE +0 -21
  119. package/skills/wait-what/SKILL.md +0 -7
  120. package/skills/wait-what/agents/openai.yaml +0 -5
  121. package/skills/wayfinder/LICENSE +0 -21
  122. package/skills/wayfinder/SKILL.md +0 -128
  123. package/skills/wayfinder/agents/openai.yaml +0 -5
  124. package/skills/wizard/LICENSE +0 -21
  125. package/skills/wizard/SKILL.md +0 -44
  126. package/skills/wizard/agents/openai.yaml +0 -3
  127. package/skills/wizard/template.sh +0 -223
  128. package/skills/writing-for-agents/LICENSE +0 -21
  129. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  130. package/skills/writing-for-agents/SKILL.md +0 -81
  131. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -1,14 +1,14 @@
1
- import { client, RequestError, } from '@agentclientprotocol/sdk';
1
+ import { client, } 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
7
  import { finalRefusal, HouseRefusal } from './api.js';
8
8
  import { openBridge } from './bridge.js';
9
9
  import { CLIS } from './clis.js';
10
10
  import { answeredFailure, readerFor } from './events.js';
11
- import { placeFiles, storeRefusal, uploaded } from './files.js';
11
+ import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
12
12
  import { agentBase } from './home.js';
13
13
  import { instructions } from './instructions.js';
14
14
  import { Parts } from './parts.js';
@@ -16,11 +16,10 @@ import { refusalOf } from './refusals.js';
16
16
  import { relayed } from './relay.js';
17
17
  import { killMarked, Scope } from './scope.js';
18
18
  import { holdSecretInput } from './secret-input.js';
19
- import { createHowWeWork } from './skills.js';
20
19
  import { transcribe } from './transcription.js';
21
20
  import { workFor } from './work.js';
22
- const SIGNED_OUT = RequestError.authRequired().code;
23
21
  const KILLED = 'the conversation was killed';
22
+ const INTERRUPTED = 'the conversation was interrupted';
24
23
  const REFUSED = 'the CLI refused the prompt';
25
24
  const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
26
25
  const GONE = 'House no longer holds this conversation; start a new one';
@@ -34,8 +33,11 @@ const NO_SESSION = "the Agent's CLI did not open a session";
34
33
  const NO_RESUME = "the Agent's CLI could not resume this conversation";
35
34
  const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
36
35
  const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
36
+ const UNSTEERED = "the Agent's CLI did not take the message into its running turn";
37
37
  class NotStarted extends Error {
38
38
  }
39
+ class ResumeRefused extends NotStarted {
40
+ }
39
41
  async function step(reason, work) {
40
42
  try {
41
43
  return await work;
@@ -44,9 +46,6 @@ async function step(reason, work) {
44
46
  throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
45
47
  }
46
48
  }
47
- function signedOut(error) {
48
- return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
49
- }
50
49
  function unopened(reason) {
51
50
  return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
52
51
  }
@@ -63,7 +62,7 @@ const EXIT_PLAN_MODE = {
63
62
  return value;
64
63
  },
65
64
  };
66
- export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
65
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
67
66
  function causeOf(error) {
68
67
  return error instanceof Error ? error.message : String(error);
69
68
  }
@@ -71,13 +70,15 @@ function answeredWith(error) {
71
70
  return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
72
71
  }
73
72
  function exitedWith(running, cause) {
74
- return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
73
+ if (running.killed)
74
+ return { failure: 'killed', cause: KILLED };
75
+ return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
75
76
  }
76
77
  function answerMessage(request, response) {
77
78
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
78
79
  }
79
- async function sentPart(url, bytes) {
80
- const answer = await fetch(relayed(url), { method: 'PUT', body: bytes });
80
+ async function sentPart(url, bytes, headers) {
81
+ const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
81
82
  await answer.arrayBuffer();
82
83
  if (!answer.ok)
83
84
  throw storeRefusal('upload', answer.status);
@@ -88,19 +89,29 @@ function logged(error) {
88
89
  function refusal(conversation, error) {
89
90
  if (conversation.kills > 0)
90
91
  return KILLED;
91
- const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
92
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
92
93
  logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
93
94
  return causeOf(error);
94
95
  }
96
+ function audioOf(result) {
97
+ if (result.isError === true)
98
+ return null;
99
+ const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
100
+ return file?.media_type?.startsWith('audio/') === true ? file : null;
101
+ }
95
102
  function marker(bridge) {
96
103
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
97
104
  }
105
+ function steers(adapter) {
106
+ return adapter.initialized._meta?.steering?.supported === true;
107
+ }
98
108
  class Turn {
99
109
  id = randomUUID();
100
110
  parts;
101
111
  questions = 0;
102
112
  secrets = 0;
103
113
  prompted = false;
114
+ steered = false;
104
115
  taken = null;
105
116
  ended = false;
106
117
  images = 0;
@@ -120,17 +131,19 @@ class Conversation {
120
131
  context = null;
121
132
  leftovers = new Set();
122
133
  waiting = [];
134
+ done = false;
123
135
  sending = null;
124
136
  subagents = new Map();
125
137
  held = [];
126
138
  opening = null;
139
+ bridge = null;
127
140
  launch = null;
128
141
  prompted = null;
129
142
  kills = 0;
130
143
  killed = new AbortController();
131
144
  interrupts = 0;
132
145
  interrupted = new AbortController();
133
- open = 0;
146
+ turns = new Set();
134
147
  queue = Promise.resolve();
135
148
  reports = Promise.resolve();
136
149
  starting = null;
@@ -186,7 +199,7 @@ export class Conversations {
186
199
  for (const conversation of this.conversations.values()) {
187
200
  const turn = conversation.turn;
188
201
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
189
- if (conversation.open > (asking ? 1 : 0))
202
+ if (conversation.turns.size > (asking ? 1 : 0))
190
203
  return false;
191
204
  }
192
205
  const conversations = [...this.conversations.values()];
@@ -195,7 +208,7 @@ export class Conversations {
195
208
  return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
196
209
  }
197
210
  quiet() {
198
- return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
211
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
199
212
  }
200
213
  look() {
201
214
  for (const conversation of this.conversations.values()) {
@@ -211,8 +224,10 @@ export class Conversations {
211
224
  opened() {
212
225
  this.watched = false;
213
226
  for (const conversation of this.conversations.values()) {
214
- if (conversation.running !== null)
227
+ if (conversation.running !== null) {
215
228
  this.facts(conversation);
229
+ this.subagentsChanged(conversation);
230
+ }
216
231
  else if (conversation.exitUntold)
217
232
  this.process(conversation, false);
218
233
  if (conversation.untold || conversation.told !== null)
@@ -239,9 +254,14 @@ export class Conversations {
239
254
  }
240
255
  async carry(conversation, call) {
241
256
  if (call.kind === 'open')
242
- return { result: this.open(conversation, call) };
257
+ return this.open(conversation, call);
243
258
  if (call.kind === 'message')
244
259
  return this.message(conversation, call);
260
+ if (call.kind === 'done') {
261
+ conversation.done = true;
262
+ this.next(conversation);
263
+ return { result: Promise.resolve({}) };
264
+ }
245
265
  await this.launched(conversation);
246
266
  if (call.kind === 'interrupt') {
247
267
  if (--conversation.interrupts === 0)
@@ -260,17 +280,23 @@ export class Conversations {
260
280
  const question = this.questions.get(String(call.interaction_id));
261
281
  if (question === undefined) {
262
282
  const text = answerMessage(call.request, call.response);
263
- return this.message(conversation, { ...call, text, files: [], first: false });
283
+ return this.message(conversation, { ...call, text, first: false, message_id: null });
264
284
  }
265
285
  question.answer(call.response);
266
286
  }
267
287
  return { result: Promise.resolve({}) };
268
288
  }
269
- open(conversation, call) {
270
- if (conversation.running !== null)
271
- return Promise.resolve({});
289
+ async open(conversation, call) {
290
+ await this.stop(conversation);
272
291
  const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
273
- return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
292
+ return {
293
+ result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
294
+ };
295
+ }
296
+ async stop(conversation) {
297
+ const running = conversation.running;
298
+ running?.scope.kill();
299
+ await running?.adapter.exited;
274
300
  }
275
301
  launch(conversation, route, sessionId, empty) {
276
302
  let opened;
@@ -290,6 +316,7 @@ export class Conversations {
290
316
  if (conversation.launch !== launch)
291
317
  return;
292
318
  conversation.launch = null;
319
+ this.closeIdleBridge(conversation);
293
320
  this.next(conversation);
294
321
  });
295
322
  this.next(conversation);
@@ -308,43 +335,92 @@ export class Conversations {
308
335
  return read;
309
336
  }
310
337
  async message(conversation, call) {
311
- const content = [{ type: 'text', text: String(call.text) }];
338
+ const content = [
339
+ ...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
340
+ { type: 'text', text: String(call.text) },
341
+ ];
312
342
  let route;
313
343
  let instructing = null;
314
344
  try {
315
345
  route = await this.route(conversation, call.agent_id);
316
346
  if (call.first === true)
317
347
  instructing = this.instructed(conversation, route);
318
- const files = call.files;
319
- if (files.length > 0)
320
- this.prepares(conversation, 'files');
321
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
322
- const lines = await this.transcribed(conversation, files, paths);
323
- if (lines.length > 0)
324
- content.push({ type: 'text', text: lines.join('\n') });
348
+ if (call.message_id !== null)
349
+ content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
325
350
  if (conversation.kills > 0)
326
351
  throw new Error(KILLED);
327
352
  }
328
353
  catch (error) {
354
+ this.closeIdleBridge(conversation);
329
355
  this.prepares(conversation, null);
330
356
  return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
331
357
  }
332
358
  conversation.preparing = null;
333
- return {
334
- result: new Promise((settle) => {
335
- conversation.waiting.push({ call, route, content, instructing, settle });
336
- this.next(conversation);
337
- }),
338
- };
359
+ const { promise, resolve: settle } = Promise.withResolvers();
360
+ const waiting = { call, route, content, instructing, settle };
361
+ if (call.at_once === true && conversation.turns.size > 0)
362
+ await this.atOnce(conversation, waiting);
363
+ else
364
+ conversation.waiting.push(waiting);
365
+ this.next(conversation);
366
+ return { result: promise };
367
+ }
368
+ async atOnce(conversation, waiting) {
369
+ while (conversation.sending !== null)
370
+ await conversation.sending;
371
+ const running = conversation.running;
372
+ if (running !== null && conversation.turns.size > 0 && steers(running.adapter)) {
373
+ conversation.sending = this.steer(conversation, running, waiting);
374
+ await conversation.sending;
375
+ conversation.sending = null;
376
+ return;
377
+ }
378
+ this.ahead(conversation, waiting);
379
+ if (conversation.turns.size > 0)
380
+ await this.cancel(conversation);
381
+ }
382
+ ahead(conversation, waiting) {
383
+ const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
384
+ conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
385
+ }
386
+ async steer(conversation, running, waiting) {
387
+ let outcome;
388
+ try {
389
+ ({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
390
+ sessionId: running.sessionId,
391
+ prompt: waiting.content,
392
+ _meta: { steering: { idleBehavior: 'promptRequired' } },
393
+ }));
394
+ }
395
+ catch (error) {
396
+ outcome = error;
397
+ }
398
+ if (outcome === 'promptRequired') {
399
+ this.ahead(conversation, waiting);
400
+ }
401
+ else if (outcome === 'injected' || outcome === 'startedNewTurn') {
402
+ if (outcome === 'injected') {
403
+ for (const turn of conversation.turns)
404
+ turn.steered ||= turn.prompted;
405
+ }
406
+ this.kit.calls.keepResult(String(waiting.call.input_id), {});
407
+ waiting.settle({});
408
+ }
409
+ else {
410
+ const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
411
+ waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
412
+ }
339
413
  }
340
414
  next(conversation) {
341
- if (conversation.sending !== null || conversation.open > 0)
415
+ if (conversation.sending !== null || conversation.turns.size > 0)
342
416
  return;
343
417
  const waiting = conversation.waiting.shift();
344
418
  if (waiting === undefined) {
345
419
  const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
346
420
  if (conversation.starting !== step)
347
421
  this.starts(conversation, step);
422
+ if (conversation.done && conversation.launch === null)
423
+ void this.stop(conversation).catch(logged);
348
424
  return;
349
425
  }
350
426
  conversation.sending = this.send(conversation, waiting).finally(() => {
@@ -387,7 +463,9 @@ export class Conversations {
387
463
  conversation.session = null;
388
464
  await this.kill(conversation);
389
465
  }
390
- waiting.settle({ refused: refusal(conversation, error) });
466
+ const refused = refusal(conversation, error);
467
+ const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
468
+ waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
391
469
  return;
392
470
  }
393
471
  this.starts(conversation, null);
@@ -422,28 +500,72 @@ export class Conversations {
422
500
  retell(conversation) {
423
501
  conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
424
502
  }
425
- async transcribed(conversation, files, paths) {
426
- const lines = [];
427
- for (const [index, path] of paths.entries()) {
428
- lines.push(path);
429
- if (files[index].media_type?.startsWith('audio/') !== true)
430
- continue;
431
- this.prepares(conversation, 'transcription');
432
- try {
433
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
434
- if (text !== null && text !== '')
435
- lines.push(`Transcript: ${text}`);
436
- }
437
- catch (error) {
438
- if (conversation.kills > 0)
439
- throw error;
440
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
503
+ async transcribed(conversation, messageId, text) {
504
+ const paths = linkedPaths(text);
505
+ if (paths.length === 0)
506
+ return [];
507
+ const killed = conversation.killed.signal;
508
+ const interrupted = conversation.interrupted.signal;
509
+ const transcribing = AbortSignal.any([killed, interrupted]);
510
+ let signal = killed;
511
+ const blocks = [];
512
+ try {
513
+ const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
514
+ const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
515
+ for (const path of paths) {
516
+ let answered = await read(path);
517
+ if (answered === null)
518
+ continue;
519
+ signal = transcribing;
520
+ this.prepares(conversation, 'transcription');
521
+ const said = await transcribe(async () => {
522
+ const address = answered ?? (await read(path));
523
+ answered = null;
524
+ if (address === null)
525
+ throw new Error(`${path} is no longer an audio file`);
526
+ return address;
527
+ }, signal);
528
+ const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
529
+ await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
530
+ blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
441
531
  }
442
532
  }
443
- return lines;
533
+ catch (error) {
534
+ if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
535
+ throw new NotStarted(INTERRUPTED, { cause: error });
536
+ throw error;
537
+ }
538
+ return blocks;
539
+ }
540
+ bridged(conversation) {
541
+ if (conversation.bridge === null) {
542
+ const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
543
+ conversation.bridge = bridging;
544
+ bridging.catch(() => {
545
+ if (conversation.bridge === bridging)
546
+ conversation.bridge = null;
547
+ });
548
+ }
549
+ return conversation.bridge;
550
+ }
551
+ closeBridge(conversation, bridging, bridge) {
552
+ bridge.close();
553
+ if (conversation.bridge === bridging)
554
+ conversation.bridge = null;
555
+ }
556
+ closeIdleBridge(conversation) {
557
+ const bridging = conversation.bridge;
558
+ if (bridging === null || conversation.running !== null || conversation.launch !== null)
559
+ return;
560
+ conversation.bridge = null;
561
+ void bridging.then((bridge) => bridge.close(), () => undefined);
444
562
  }
445
563
  async interrupt(conversation, turnId) {
446
- if (conversation.running === null || conversation.turn?.id !== turnId)
564
+ if ([...conversation.turns].some((turn) => turn.id === turnId))
565
+ await this.cancel(conversation);
566
+ }
567
+ async cancel(conversation) {
568
+ if (conversation.running === null)
447
569
  return;
448
570
  await conversation.running.adapter.connection.agent.notify('session/cancel', {
449
571
  sessionId: conversation.running.sessionId,
@@ -504,9 +626,10 @@ export class Conversations {
504
626
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
505
627
  .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
506
628
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
507
- const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
629
+ const bridging = this.bridged(conversation);
630
+ const bridge = await step(unbridged, bridging);
508
631
  if (conversation.kills > 0) {
509
- bridge.close();
632
+ this.closeBridge(conversation, bridging, bridge);
510
633
  throw new Error(KILLED);
511
634
  }
512
635
  conversation.opening = bridge;
@@ -514,7 +637,6 @@ export class Conversations {
514
637
  const scope = new Scope(marker(bridge), randomUUID(), cli);
515
638
  let adapter = null;
516
639
  try {
517
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
518
640
  bridged(bridge);
519
641
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
520
642
  const streamed = await step(NO_CONNECTION, work.open(cli));
@@ -522,7 +644,7 @@ export class Conversations {
522
644
  work.read(message);
523
645
  this.read(conversation, reader, message);
524
646
  }, { ...bridge.env, ...streamed }, undefined, scope));
525
- void adapter.exited.then(() => bridge.close());
647
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
526
648
  const agent = adapter.connection.agent;
527
649
  const cwd = route.working_directory;
528
650
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
@@ -533,10 +655,11 @@ export class Conversations {
533
655
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
534
656
  await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
535
657
  }
536
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
658
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
537
659
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
538
660
  void work.settled().then(() => scope.record());
539
661
  conversation.running = running;
662
+ adapter.turn(conversation.turns.size > 0);
540
663
  conversation.session = running.sessionId;
541
664
  conversation.options = options;
542
665
  this.facts(conversation);
@@ -547,7 +670,7 @@ export class Conversations {
547
670
  adapter?.retire();
548
671
  work.close();
549
672
  conversation.leftovers.add(scope);
550
- bridge.close();
673
+ this.closeBridge(conversation, bridging, bridge);
551
674
  throw conversation.kills > 0 ? new Error(KILLED) : error;
552
675
  }
553
676
  finally {
@@ -560,7 +683,7 @@ export class Conversations {
560
683
  }
561
684
  catch (error) {
562
685
  if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
563
- throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
686
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
564
687
  }
565
688
  logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
566
689
  return null;
@@ -596,11 +719,17 @@ export class Conversations {
596
719
  this.kit.changed();
597
720
  }
598
721
  detach(conversation, running) {
722
+ if (conversation.bridge === running.bridging)
723
+ conversation.bridge = null;
599
724
  running.work.close();
600
725
  conversation.leftovers.add(running.scope);
601
726
  conversation.running = null;
602
727
  conversation.commands = null;
603
728
  conversation.options = null;
729
+ if (conversation.subagents.size > 0) {
730
+ conversation.subagents.clear();
731
+ this.subagentsChanged(conversation);
732
+ }
604
733
  for (const [id, question] of this.questions) {
605
734
  if (question.conversation === conversation)
606
735
  this.questions.delete(id);
@@ -617,6 +746,10 @@ export class Conversations {
617
746
  if (conversation.options !== null)
618
747
  this.options(conversation, conversation.options);
619
748
  }
749
+ subagentsChanged(conversation) {
750
+ const subagents = [...conversation.subagents.values()].map(({ description, activity }) => ({ description, activity }));
751
+ this.kit.send({ type: 'subagents', conversation_id: conversation.id, subagents });
752
+ }
620
753
  commands(conversation, commands) {
621
754
  conversation.commands = commands;
622
755
  if (conversation.running === null)
@@ -651,7 +784,8 @@ export class Conversations {
651
784
  }
652
785
  begin(conversation) {
653
786
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
654
- conversation.open++;
787
+ conversation.turns.add(turn);
788
+ conversation.running?.adapter.turn(true);
655
789
  void this.deliver(conversation, turn, 'started', {}).catch(logged);
656
790
  conversation.turn = turn;
657
791
  if (conversation.context !== null)
@@ -673,7 +807,8 @@ export class Conversations {
673
807
  if (notice !== null)
674
808
  turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
675
809
  turn.ended = true;
676
- conversation.open--;
810
+ conversation.turns.delete(turn);
811
+ conversation.running?.adapter.turn(conversation.turns.size > 0);
677
812
  if (conversation.turn === turn)
678
813
  conversation.turn = null;
679
814
  const body = Promise.allSettled(turn.uploads).then(() => ({
@@ -706,7 +841,7 @@ export class Conversations {
706
841
  }
707
842
  else {
708
843
  this.took(turn);
709
- if (conversation.open === 1 && conversation.running === running) {
844
+ if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
710
845
  running.adapter.retire();
711
846
  this.detach(conversation, running);
712
847
  }
@@ -719,19 +854,22 @@ export class Conversations {
719
854
  this.took(turn);
720
855
  return turn;
721
856
  }
722
- ended(conversation) {
857
+ ended(conversation, stop) {
723
858
  const turn = conversation.turn;
724
859
  if (turn === null)
725
860
  return;
726
- if (!turn.prompted) {
727
- this.end(conversation, turn, null);
728
- }
729
- else {
730
- for (const event of conversation.running?.reader.closeTurn() ?? [])
731
- this.apply(conversation, turn, event);
732
- conversation.turn = null;
733
- this.kit.changed();
861
+ const held = stop !== null && !turn.steered && [...conversation.subagents.values()].some((subagent) => subagent.turn === turn);
862
+ if (!turn.prompted || held) {
863
+ this.took(turn);
864
+ this.end(conversation, turn, stop === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
865
+ return;
734
866
  }
867
+ if (stop !== null)
868
+ return;
869
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
870
+ this.apply(conversation, turn, event);
871
+ conversation.turn = null;
872
+ this.kit.changed();
735
873
  }
736
874
  async ask(conversation, method, params, secret, signal) {
737
875
  const turn = this.started(conversation);
@@ -787,7 +925,7 @@ export class Conversations {
787
925
  this.started(conversation);
788
926
  return;
789
927
  case 'ended':
790
- this.ended(conversation);
928
+ this.ended(conversation, event.stop);
791
929
  return;
792
930
  case 'commands':
793
931
  this.commands(conversation, event.commands);
@@ -802,6 +940,9 @@ export class Conversations {
802
940
  case 'subagent-done':
803
941
  this.subagentDone(conversation, event.id, event.result);
804
942
  return;
943
+ case 'subagent-activity':
944
+ this.subagentActivity(conversation, event.id, event.activity);
945
+ return;
805
946
  case 'marker':
806
947
  case 'image':
807
948
  if (conversation.turn !== null)
@@ -828,7 +969,8 @@ export class Conversations {
828
969
  return;
829
970
  case 'subagent': {
830
971
  const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
831
- conversation.subagents.set(event.id, { description: event.description, turn, index });
972
+ conversation.subagents.set(event.id, { description: event.description, turn, index, activity: null });
973
+ this.subagentsChanged(conversation);
832
974
  return;
833
975
  }
834
976
  case 'message':
@@ -851,6 +993,7 @@ export class Conversations {
851
993
  if (subagent === undefined)
852
994
  return;
853
995
  conversation.subagents.delete(id);
996
+ this.subagentsChanged(conversation);
854
997
  if (subagent.turn === conversation.turn) {
855
998
  subagent.turn.parts.done(subagent.index, result);
856
999
  return;
@@ -861,20 +1004,31 @@ export class Conversations {
861
1004
  else
862
1005
  conversation.turn.parts.start(part);
863
1006
  }
1007
+ subagentActivity(conversation, id, activity) {
1008
+ const subagent = conversation.subagents.get(id);
1009
+ if (subagent === undefined || subagent.activity === activity)
1010
+ return;
1011
+ subagent.activity = activity;
1012
+ this.subagentsChanged(conversation);
1013
+ }
864
1014
  async image(conversation, turn, reserved, data, mediaType) {
865
1015
  const bytes = Buffer.from(data, 'base64');
866
1016
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
1017
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
867
1018
  try {
868
1019
  const declared = (await this.deliver(conversation, turn, 'images', {
869
1020
  name,
870
1021
  media_type: mediaType,
871
1022
  bytes: bytes.length,
872
- sha256: createHash('sha256').update(bytes).digest('hex'),
1023
+ sha256,
873
1024
  }));
874
1025
  const saved = await uploaded(declared.upload, {
1026
+ name,
875
1027
  bytes: bytes.length,
876
- send: (url, start, end) => sentPart(url, bytes.subarray(start, end)),
877
- finish: (upload) => this.kit.house.deliver('/kit/uploads/finish', { upload }),
1028
+ sha256,
1029
+ read: (start, end) => [bytes.subarray(start, end)],
1030
+ send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
1031
+ finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
878
1032
  });
879
1033
  turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
880
1034
  }
package/dist/end.js CHANGED
@@ -14,9 +14,15 @@ export function end(reason) {
14
14
  export function restart(reason) {
15
15
  exit(reason, 1);
16
16
  }
17
- export function updateRequired() {
17
+ function updateLine(lead) {
18
18
  const command = installCommand();
19
- end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
19
+ return command === null ? `${lead}; install it again.` : `${lead}. Update it: ${command}`;
20
+ }
21
+ export function updateRequired() {
22
+ end(updateLine('This Kit is too old for House'));
23
+ }
24
+ export function unknownMethod(kind) {
25
+ return updateLine(`This Kit does not know ${kind}`);
20
26
  }
21
27
  export function newerThanHouse(release, command) {
22
28
  end(`Kit ${KIT_RELEASE} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);