@agentshouse/kit 0.2.1-c7f53ff9eaf98 → 0.2.1-c83d468784cc9

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