@agentshouse/kit 0.2.1-ce3d2bb67177c → 0.2.1-ce47f621f90e1

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 (135) hide show
  1. package/README.md +17 -17
  2. package/dist/acp.js +57 -6
  3. package/dist/agents.js +54 -31
  4. package/dist/authority-main.js +2 -2
  5. package/dist/bridge.js +1 -1
  6. package/dist/building.js +50 -0
  7. package/dist/calls.js +11 -3
  8. package/dist/clis.js +33 -39
  9. package/dist/connection.js +10 -30
  10. package/dist/conversations.js +149 -47
  11. package/dist/end.js +8 -2
  12. package/dist/events.js +29 -8
  13. package/dist/files.js +33 -17
  14. package/dist/git.js +2 -2
  15. package/dist/home.js +0 -7
  16. package/dist/house-cli.js +27 -55
  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 +3 -1
  22. package/dist/mcp.js +2 -0
  23. package/dist/placement.js +10 -18
  24. package/dist/relay.js +21 -0
  25. package/dist/resident.js +19 -9
  26. package/dist/scope.js +12 -4
  27. package/dist/scripts.js +255 -0
  28. package/dist/sign-in.js +3 -2
  29. package/dist/silence.js +28 -0
  30. package/dist/skills.js +41 -96
  31. package/dist/transcription.js +32 -23
  32. package/dist/update.js +109 -120
  33. package/dist/wire.js +23 -5
  34. package/package.json +19 -20
  35. package/dist/bootstrap.json +0 -24
  36. package/dist/clis-main.js +0 -23
  37. package/how-we-work.md +0 -33
  38. package/skills/ask-matt/LICENSE +0 -21
  39. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  40. package/skills/ask-matt/SKILL.md +0 -97
  41. package/skills/ask-matt/agents/openai.yaml +0 -5
  42. package/skills/code-review/LICENSE +0 -21
  43. package/skills/code-review/SKILL.md +0 -89
  44. package/skills/code-review/agents/openai.yaml +0 -3
  45. package/skills/codebase-design/DEEPENING.md +0 -37
  46. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  47. package/skills/codebase-design/LICENSE +0 -21
  48. package/skills/codebase-design/SKILL.md +0 -114
  49. package/skills/codebase-design/agents/openai.yaml +0 -3
  50. package/skills/diagnosing-bugs/LICENSE +0 -21
  51. package/skills/diagnosing-bugs/SKILL.md +0 -138
  52. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  53. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  54. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  55. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  56. package/skills/domain-modeling/LICENSE +0 -21
  57. package/skills/domain-modeling/SKILL.md +0 -74
  58. package/skills/domain-modeling/agents/openai.yaml +0 -3
  59. package/skills/grill-me/LICENSE +0 -21
  60. package/skills/grill-me/SKILL.md +0 -7
  61. package/skills/grill-me/agents/openai.yaml +0 -5
  62. package/skills/grill-with-docs/LICENSE +0 -21
  63. package/skills/grill-with-docs/SKILL.md +0 -7
  64. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  65. package/skills/grilling/LICENSE +0 -21
  66. package/skills/grilling/SKILL.md +0 -30
  67. package/skills/grilling/agents/openai.yaml +0 -3
  68. package/skills/handoff/LICENSE +0 -21
  69. package/skills/handoff/SKILL.md +0 -16
  70. package/skills/handoff/agents/openai.yaml +0 -5
  71. package/skills/implement/LICENSE +0 -21
  72. package/skills/implement/SKILL.md +0 -17
  73. package/skills/implement/agents/openai.yaml +0 -5
  74. package/skills/implement-spec/LICENSE +0 -21
  75. package/skills/implement-spec/SKILL.md +0 -40
  76. package/skills/implement-spec/agents/openai.yaml +0 -5
  77. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  78. package/skills/improve-codebase-architecture/LICENSE +0 -21
  79. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  80. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  81. package/skills/pr/CREDITS.md +0 -3
  82. package/skills/pr/LICENSE +0 -21
  83. package/skills/pr/SKILL.md +0 -170
  84. package/skills/pr/agents/openai.yaml +0 -3
  85. package/skills/prototype/LICENSE +0 -21
  86. package/skills/prototype/LOGIC.md +0 -67
  87. package/skills/prototype/SKILL.md +0 -26
  88. package/skills/prototype/UI.md +0 -112
  89. package/skills/prototype/agents/openai.yaml +0 -3
  90. package/skills/research/LICENSE +0 -21
  91. package/skills/research/SKILL.md +0 -12
  92. package/skills/research/agents/openai.yaml +0 -3
  93. package/skills/retro/LICENSE +0 -21
  94. package/skills/retro/SKILL.md +0 -44
  95. package/skills/retro/agents/openai.yaml +0 -5
  96. package/skills/tdd/LICENSE +0 -21
  97. package/skills/tdd/SKILL.md +0 -38
  98. package/skills/tdd/agents/openai.yaml +0 -3
  99. package/skills/tdd/mocking.md +0 -59
  100. package/skills/tdd/tests.md +0 -77
  101. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  102. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  103. package/skills/teach/LICENSE +0 -21
  104. package/skills/teach/MISSION-FORMAT.md +0 -31
  105. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  106. package/skills/teach/SKILL.md +0 -140
  107. package/skills/teach/agents/openai.yaml +0 -5
  108. package/skills/to-questionnaire/LICENSE +0 -21
  109. package/skills/to-questionnaire/SKILL.md +0 -54
  110. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  111. package/skills/to-spec/LICENSE +0 -21
  112. package/skills/to-spec/SKILL.md +0 -75
  113. package/skills/to-spec/agents/openai.yaml +0 -5
  114. package/skills/to-tickets/LICENSE +0 -21
  115. package/skills/to-tickets/SKILL.md +0 -105
  116. package/skills/to-tickets/agents/openai.yaml +0 -5
  117. package/skills/triage/AGENT-BRIEF.md +0 -207
  118. package/skills/triage/LICENSE +0 -21
  119. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  120. package/skills/triage/SKILL.md +0 -112
  121. package/skills/triage/agents/openai.yaml +0 -5
  122. package/skills/wait-what/LICENSE +0 -21
  123. package/skills/wait-what/SKILL.md +0 -7
  124. package/skills/wait-what/agents/openai.yaml +0 -5
  125. package/skills/wayfinder/LICENSE +0 -21
  126. package/skills/wayfinder/SKILL.md +0 -128
  127. package/skills/wayfinder/agents/openai.yaml +0 -5
  128. package/skills/wizard/LICENSE +0 -21
  129. package/skills/wizard/SKILL.md +0 -44
  130. package/skills/wizard/agents/openai.yaml +0 -3
  131. package/skills/wizard/template.sh +0 -223
  132. package/skills/writing-for-agents/LICENSE +0 -21
  133. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  134. package/skills/writing-for-agents/SKILL.md +0 -81
  135. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -3,40 +3,13 @@ import { HouseRefusal, retryDelay } from './api.js';
3
3
  import { end, REPLACED, signInAgain } from './end.js';
4
4
  import { readEnrolment } from './home.js';
5
5
  import { relayed } from './relay.js';
6
- import { endUnserved, KIT_WIRE } from './wire.js';
6
+ import { Silence } from './silence.js';
7
+ import { endUnserved, KIT_WIRE, unserved } from './wire.js';
7
8
  const PATH = '/kit/stream';
8
9
  const SUBPROTOCOL = `house.kit.stream.${KIT_WIRE}`;
9
10
  const REFUSAL_HEADER = 'x-house-refusal';
10
11
  // House beats each Kit connection every thirty seconds, so two silent beats mean this Kit process cannot hear House.
11
12
  export const CONNECTION_SILENCE_MS = 60_000;
12
- const SYSTEM_CONNECTION_CLOCK = {
13
- later: (delay, callback) => setTimeout(callback, delay),
14
- clear: (timer) => clearTimeout(timer),
15
- };
16
- export class ConnectionSilence {
17
- timer = null;
18
- silent;
19
- clock;
20
- interval;
21
- constructor(silent, clock = SYSTEM_CONNECTION_CLOCK, interval = CONNECTION_SILENCE_MS) {
22
- this.silent = silent;
23
- this.clock = clock;
24
- this.interval = interval;
25
- }
26
- heard() {
27
- this.stop();
28
- this.timer = this.clock.later(this.interval, () => {
29
- this.timer = null;
30
- this.silent();
31
- });
32
- }
33
- stop() {
34
- if (this.timer === null)
35
- return;
36
- this.clock.clear(this.timer);
37
- this.timer = null;
38
- }
39
- }
40
13
  function connectionUrl(house) {
41
14
  const url = new URL(PATH, house);
42
15
  url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -58,21 +31,28 @@ subscribe('undici:request:headers', (message) => {
58
31
  });
59
32
  export function holdConnection(handlers) {
60
33
  let connection = null;
34
+ let socket = null;
61
35
  let failures = 0;
62
36
  const connect = async () => {
63
37
  const { house, credential } = (await readEnrolment());
38
+ if (socket !== null || unserved())
39
+ return;
64
40
  refused = null;
65
41
  const opening = new WebSocket(relayed(connectionUrl(house)), {
66
42
  protocols: [SUBPROTOCOL],
67
43
  headers: { authorization: `Bearer ${credential}` },
68
44
  });
45
+ socket = opening;
69
46
  let opened = false;
70
47
  let silence = null;
71
48
  const closed = (event) => {
72
49
  silence?.stop();
73
50
  silence = null;
51
+ if (socket === opening)
52
+ socket = null;
74
53
  if (connection === opening)
75
54
  connection = null;
55
+ handlers.closed();
76
56
  process.stderr.write(`kit: connection closed ${event.code} ${event.reason}\n`);
77
57
  if (event.reason === 'replaced')
78
58
  end(REPLACED);
@@ -91,7 +71,7 @@ export function holdConnection(handlers) {
91
71
  opened = true;
92
72
  failures = 0;
93
73
  connection = opening;
94
- silence = new ConnectionSilence(() => {
74
+ silence = new Silence(CONNECTION_SILENCE_MS, () => {
95
75
  if (connection !== opening)
96
76
  return;
97
77
  connection = null;
@@ -1,9 +1,9 @@
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';
@@ -16,10 +16,8 @@ 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';
24
22
  const INTERRUPTED = 'the conversation was interrupted';
25
23
  const REFUSED = 'the CLI refused the prompt';
@@ -35,6 +33,7 @@ const NO_SESSION = "the Agent's CLI did not open a session";
35
33
  const NO_RESUME = "the Agent's CLI could not resume this conversation";
36
34
  const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
37
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";
38
37
  class NotStarted extends Error {
39
38
  }
40
39
  class ResumeRefused extends NotStarted {
@@ -47,9 +46,6 @@ async function step(reason, work) {
47
46
  throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
48
47
  }
49
48
  }
50
- function signedOut(error) {
51
- return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
52
- }
53
49
  function unopened(reason) {
54
50
  return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
55
51
  }
@@ -66,7 +62,7 @@ const EXIT_PLAN_MODE = {
66
62
  return value;
67
63
  },
68
64
  };
69
- 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']);
70
66
  function causeOf(error) {
71
67
  return error instanceof Error ? error.message : String(error);
72
68
  }
@@ -74,13 +70,15 @@ function answeredWith(error) {
74
70
  return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
75
71
  }
76
72
  function exitedWith(running, cause) {
77
- 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 };
78
76
  }
79
77
  function answerMessage(request, response) {
80
78
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
81
79
  }
82
- async function sentPart(url, bytes) {
83
- 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 });
84
82
  await answer.arrayBuffer();
85
83
  if (!answer.ok)
86
84
  throw storeRefusal('upload', answer.status);
@@ -104,12 +102,16 @@ function audioOf(result) {
104
102
  function marker(bridge) {
105
103
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
106
104
  }
105
+ function steers(adapter) {
106
+ return adapter.initialized._meta?.steering?.supported === true;
107
+ }
107
108
  class Turn {
108
109
  id = randomUUID();
109
110
  parts;
110
111
  questions = 0;
111
112
  secrets = 0;
112
113
  prompted = false;
114
+ steered = false;
113
115
  taken = null;
114
116
  ended = false;
115
117
  images = 0;
@@ -129,6 +131,7 @@ class Conversation {
129
131
  context = null;
130
132
  leftovers = new Set();
131
133
  waiting = [];
134
+ done = false;
132
135
  sending = null;
133
136
  subagents = new Map();
134
137
  held = [];
@@ -140,7 +143,7 @@ class Conversation {
140
143
  killed = new AbortController();
141
144
  interrupts = 0;
142
145
  interrupted = new AbortController();
143
- open = 0;
146
+ turns = new Set();
144
147
  queue = Promise.resolve();
145
148
  reports = Promise.resolve();
146
149
  starting = null;
@@ -196,7 +199,7 @@ export class Conversations {
196
199
  for (const conversation of this.conversations.values()) {
197
200
  const turn = conversation.turn;
198
201
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
199
- if (conversation.open > (asking ? 1 : 0))
202
+ if (conversation.turns.size > (asking ? 1 : 0))
200
203
  return false;
201
204
  }
202
205
  const conversations = [...this.conversations.values()];
@@ -205,7 +208,7 @@ export class Conversations {
205
208
  return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
206
209
  }
207
210
  quiet() {
208
- return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
211
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
209
212
  }
210
213
  look() {
211
214
  for (const conversation of this.conversations.values()) {
@@ -221,8 +224,10 @@ export class Conversations {
221
224
  opened() {
222
225
  this.watched = false;
223
226
  for (const conversation of this.conversations.values()) {
224
- if (conversation.running !== null)
227
+ if (conversation.running !== null) {
225
228
  this.facts(conversation);
229
+ this.subagentsChanged(conversation);
230
+ }
226
231
  else if (conversation.exitUntold)
227
232
  this.process(conversation, false);
228
233
  if (conversation.untold || conversation.told !== null)
@@ -249,9 +254,14 @@ export class Conversations {
249
254
  }
250
255
  async carry(conversation, call) {
251
256
  if (call.kind === 'open')
252
- return { result: this.open(conversation, call) };
257
+ return this.open(conversation, call);
253
258
  if (call.kind === 'message')
254
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
+ }
255
265
  await this.launched(conversation);
256
266
  if (call.kind === 'interrupt') {
257
267
  if (--conversation.interrupts === 0)
@@ -276,11 +286,17 @@ export class Conversations {
276
286
  }
277
287
  return { result: Promise.resolve({}) };
278
288
  }
279
- open(conversation, call) {
280
- if (conversation.running !== null)
281
- return Promise.resolve({});
289
+ async open(conversation, call) {
290
+ await this.stop(conversation);
282
291
  const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
283
- 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;
284
300
  }
285
301
  launch(conversation, route, sessionId, empty) {
286
302
  let opened;
@@ -319,7 +335,10 @@ export class Conversations {
319
335
  return read;
320
336
  }
321
337
  async message(conversation, call) {
322
- 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
+ ];
323
342
  let route;
324
343
  let instructing = null;
325
344
  try {
@@ -337,21 +356,71 @@ export class Conversations {
337
356
  return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
338
357
  }
339
358
  conversation.preparing = null;
340
- return {
341
- result: new Promise((settle) => {
342
- conversation.waiting.push({ call, route, content, instructing, settle });
343
- this.next(conversation);
344
- }),
345
- };
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
+ }
346
413
  }
347
414
  next(conversation) {
348
- if (conversation.sending !== null || conversation.open > 0)
415
+ if (conversation.sending !== null || conversation.turns.size > 0)
349
416
  return;
350
417
  const waiting = conversation.waiting.shift();
351
418
  if (waiting === undefined) {
352
419
  const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
353
420
  if (conversation.starting !== step)
354
421
  this.starts(conversation, step);
422
+ if (conversation.done && conversation.launch === null)
423
+ void this.stop(conversation).catch(logged);
355
424
  return;
356
425
  }
357
426
  conversation.sending = this.send(conversation, waiting).finally(() => {
@@ -492,7 +561,11 @@ export class Conversations {
492
561
  void bridging.then((bridge) => bridge.close(), () => undefined);
493
562
  }
494
563
  async interrupt(conversation, turnId) {
495
- 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)
496
569
  return;
497
570
  await conversation.running.adapter.connection.agent.notify('session/cancel', {
498
571
  sessionId: conversation.running.sessionId,
@@ -564,7 +637,6 @@ export class Conversations {
564
637
  const scope = new Scope(marker(bridge), randomUUID(), cli);
565
638
  let adapter = null;
566
639
  try {
567
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
568
640
  bridged(bridge);
569
641
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
570
642
  const streamed = await step(NO_CONNECTION, work.open(cli));
@@ -587,6 +659,7 @@ export class Conversations {
587
659
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
588
660
  void work.settled().then(() => scope.record());
589
661
  conversation.running = running;
662
+ adapter.turn(conversation.turns.size > 0);
590
663
  conversation.session = running.sessionId;
591
664
  conversation.options = options;
592
665
  this.facts(conversation);
@@ -653,6 +726,10 @@ export class Conversations {
653
726
  conversation.running = null;
654
727
  conversation.commands = null;
655
728
  conversation.options = null;
729
+ if (conversation.subagents.size > 0) {
730
+ conversation.subagents.clear();
731
+ this.subagentsChanged(conversation);
732
+ }
656
733
  for (const [id, question] of this.questions) {
657
734
  if (question.conversation === conversation)
658
735
  this.questions.delete(id);
@@ -669,6 +746,10 @@ export class Conversations {
669
746
  if (conversation.options !== null)
670
747
  this.options(conversation, conversation.options);
671
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
+ }
672
753
  commands(conversation, commands) {
673
754
  conversation.commands = commands;
674
755
  if (conversation.running === null)
@@ -703,7 +784,8 @@ export class Conversations {
703
784
  }
704
785
  begin(conversation) {
705
786
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
706
- conversation.open++;
787
+ conversation.turns.add(turn);
788
+ conversation.running?.adapter.turn(true);
707
789
  void this.deliver(conversation, turn, 'started', {}).catch(logged);
708
790
  conversation.turn = turn;
709
791
  if (conversation.context !== null)
@@ -725,7 +807,8 @@ export class Conversations {
725
807
  if (notice !== null)
726
808
  turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
727
809
  turn.ended = true;
728
- conversation.open--;
810
+ conversation.turns.delete(turn);
811
+ conversation.running?.adapter.turn(conversation.turns.size > 0);
729
812
  if (conversation.turn === turn)
730
813
  conversation.turn = null;
731
814
  const body = Promise.allSettled(turn.uploads).then(() => ({
@@ -758,7 +841,7 @@ export class Conversations {
758
841
  }
759
842
  else {
760
843
  this.took(turn);
761
- if (conversation.open === 1 && conversation.running === running) {
844
+ if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
762
845
  running.adapter.retire();
763
846
  this.detach(conversation, running);
764
847
  }
@@ -771,19 +854,22 @@ export class Conversations {
771
854
  this.took(turn);
772
855
  return turn;
773
856
  }
774
- ended(conversation) {
857
+ ended(conversation, stop) {
775
858
  const turn = conversation.turn;
776
859
  if (turn === null)
777
860
  return;
778
- if (!turn.prompted) {
779
- this.end(conversation, turn, null);
780
- }
781
- else {
782
- for (const event of conversation.running?.reader.closeTurn() ?? [])
783
- this.apply(conversation, turn, event);
784
- conversation.turn = null;
785
- 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;
786
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();
787
873
  }
788
874
  async ask(conversation, method, params, secret, signal) {
789
875
  const turn = this.started(conversation);
@@ -839,7 +925,7 @@ export class Conversations {
839
925
  this.started(conversation);
840
926
  return;
841
927
  case 'ended':
842
- this.ended(conversation);
928
+ this.ended(conversation, event.stop);
843
929
  return;
844
930
  case 'commands':
845
931
  this.commands(conversation, event.commands);
@@ -854,6 +940,9 @@ export class Conversations {
854
940
  case 'subagent-done':
855
941
  this.subagentDone(conversation, event.id, event.result);
856
942
  return;
943
+ case 'subagent-activity':
944
+ this.subagentActivity(conversation, event.id, event.activity);
945
+ return;
857
946
  case 'marker':
858
947
  case 'image':
859
948
  if (conversation.turn !== null)
@@ -880,7 +969,8 @@ export class Conversations {
880
969
  return;
881
970
  case 'subagent': {
882
971
  const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
883
- 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);
884
974
  return;
885
975
  }
886
976
  case 'message':
@@ -903,6 +993,7 @@ export class Conversations {
903
993
  if (subagent === undefined)
904
994
  return;
905
995
  conversation.subagents.delete(id);
996
+ this.subagentsChanged(conversation);
906
997
  if (subagent.turn === conversation.turn) {
907
998
  subagent.turn.parts.done(subagent.index, result);
908
999
  return;
@@ -913,20 +1004,31 @@ export class Conversations {
913
1004
  else
914
1005
  conversation.turn.parts.start(part);
915
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
+ }
916
1014
  async image(conversation, turn, reserved, data, mediaType) {
917
1015
  const bytes = Buffer.from(data, 'base64');
918
1016
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
1017
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
919
1018
  try {
920
1019
  const declared = (await this.deliver(conversation, turn, 'images', {
921
1020
  name,
922
1021
  media_type: mediaType,
923
1022
  bytes: bytes.length,
924
- sha256: createHash('sha256').update(bytes).digest('hex'),
1023
+ sha256,
925
1024
  }));
926
1025
  const saved = await uploaded(declared.upload, {
1026
+ name,
927
1027
  bytes: bytes.length,
928
- send: (url, start, end) => sentPart(url, bytes.subarray(start, end)),
929
- 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 }),
930
1032
  });
931
1033
  turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
932
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
+ export 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}`);
package/dist/events.js CHANGED
@@ -1,8 +1,13 @@
1
+ const AUTONOMOUS_ORIGINS = new Set(['task-notification', 'peer', 'coordinator', 'observer', 'observer-activity']);
1
2
  const COMMAND = { shown: 'command' };
2
3
  const HIDDEN = { shown: 'hidden' };
3
4
  const FINISHED = new Set(['completed', 'failed']);
4
5
  export const RUNNING_JOB = new Set(['running', 'paused']);
5
- const MESSAGE_TARGETS = { send_routine_request: 'access', cancel_routine_request: 'request', send_mail: 'request' };
6
+ const MESSAGE_TARGETS = new Map([
7
+ ['send_agent_function_request', ['agent_function', 'agent']],
8
+ ['cancel_agent_function_request', ['request']],
9
+ ['send_mail', ['conversation', 'request']],
10
+ ]);
6
11
  function words(line) {
7
12
  const found = [];
8
13
  let word = null;
@@ -48,11 +53,13 @@ export function houseMessage(line) {
48
53
  catch {
49
54
  return null;
50
55
  }
51
- const argument = MESSAGE_TARGETS[call.command];
52
- if (argument === undefined)
56
+ if (call === null)
53
57
  return null;
54
- const target = call.arguments?.[argument];
55
- return { shown: 'message', command: call.command, target: typeof target === 'string' ? target : null };
58
+ const targets = MESSAGE_TARGETS.get(call.command);
59
+ if (targets === undefined)
60
+ return null;
61
+ const target = targets.map((argument) => call.arguments?.[argument]).find((value) => typeof value === 'string');
62
+ return { shown: 'message', command: call.command, target: target ?? null };
56
63
  }
57
64
  function decidedByLine(command, final) {
58
65
  const line = typeof command === 'string' ? command : Array.isArray(command) ? command.at(-1) : undefined;
@@ -269,6 +276,7 @@ class Reader {
269
276
  class ClaudeReader extends Reader {
270
277
  watchedOptions = { model: 'model' };
271
278
  tasks = new Map();
279
+ stop = null;
272
280
  own(message) {
273
281
  if (message.params?.update?._meta?.claudeCode?.parentToolUseId !== undefined)
274
282
  return [];
@@ -276,7 +284,9 @@ class ClaudeReader extends Reader {
276
284
  }
277
285
  update(update) {
278
286
  if (update.sessionUpdate === 'usage_update' && update._meta?.['_claude/origin'] !== undefined) {
279
- return [...super.update(update), { kind: 'ended' }];
287
+ const stop = this.stop;
288
+ this.stop = null;
289
+ return [...super.update(update), { kind: 'ended', stop }];
280
290
  }
281
291
  if (update.sessionUpdate === 'session_info_update' && update._meta?.jetbrains?.air?.sessionFailure !== undefined) {
282
292
  return [{ kind: 'marker', marker: 'retry', text: update._meta.jetbrains.air.sessionFailure.title }];
@@ -287,6 +297,13 @@ class ClaudeReader extends Reader {
287
297
  if (message.method !== '_claude/sdkMessage')
288
298
  return [];
289
299
  const sdk = message.params.message;
300
+ if (sdk.type === 'result') {
301
+ if (AUTONOMOUS_ORIGINS.has(sdk.origin?.kind))
302
+ this.stop = null;
303
+ else
304
+ this.stop = sdk.stop_reason === 'refusal' ? 'refusal' : sdk.is_error === true ? null : 'end_turn';
305
+ return [];
306
+ }
290
307
  switch (sdk.subtype) {
291
308
  case 'task_started':
292
309
  if (sdk.tool_use_id !== undefined)
@@ -294,6 +311,10 @@ class ClaudeReader extends Reader {
294
311
  return [];
295
312
  case 'task_notification':
296
313
  return sdk.tool_use_id === undefined ? [] : this.finish(sdk.tool_use_id, sdk.summary ?? null);
314
+ case 'task_progress':
315
+ if (!this.subagents.has(sdk.tool_use_id) || this.done.has(sdk.tool_use_id) || typeof sdk.description !== 'string')
316
+ return [];
317
+ return [{ kind: 'subagent-activity', id: sdk.tool_use_id, activity: sdk.description }];
297
318
  default:
298
319
  return [];
299
320
  }
@@ -356,7 +377,7 @@ class CodexReader extends Reader {
356
377
  if (codex?.threadStatus?.type === 'active')
357
378
  return [{ kind: 'started' }];
358
379
  if (codex?.threadStatus?.type === 'idle')
359
- return [{ kind: 'ended' }];
380
+ return [{ kind: 'ended', stop: null }];
360
381
  if (codex?.error !== undefined)
361
382
  return [{ kind: 'marker', marker: 'retry', text: codex.error.message ?? '' }];
362
383
  return [];
@@ -429,7 +450,7 @@ class GrokReader extends Reader {
429
450
  return (message.params.runningPromptId ?? null) !== null && session === this.session ? [{ kind: 'started' }] : [];
430
451
  }
431
452
  if (message.params?.update?.sessionUpdate === 'turn_completed') {
432
- return session === this.session ? [{ kind: 'ended' }] : [];
453
+ return session === this.session ? [{ kind: 'ended', stop: null }] : [];
433
454
  }
434
455
  if (message.method === '_x.ai/models/update') {
435
456
  this.offered(message.params);