@agentshouse/kit 0.2.1-cb6679d048e60 → 0.2.1-cb66cb2fd0696

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 (136) hide show
  1. package/README.md +47 -46
  2. package/dist/acp.js +102 -108
  3. package/dist/agents.js +214 -62
  4. package/dist/api.js +36 -3
  5. package/dist/app-git.js +48 -0
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +40 -59
  8. package/dist/building.js +50 -0
  9. package/dist/calls.js +71 -0
  10. package/dist/chatgpt.js +32 -0
  11. package/dist/clis.js +161 -95
  12. package/dist/codex-main.js +58 -0
  13. package/dist/connection.js +105 -0
  14. package/dist/conversations.js +732 -330
  15. package/dist/copies.js +205 -150
  16. package/dist/end.js +33 -0
  17. package/dist/enrol-main.js +7 -1
  18. package/dist/events.js +546 -0
  19. package/dist/files.js +140 -17
  20. package/dist/git.js +13 -11
  21. package/dist/home.js +89 -7
  22. package/dist/house-cli.js +299 -0
  23. package/dist/house-main.js +7 -159
  24. package/dist/instructions.js +8 -10
  25. package/dist/kit-cli.js +68 -0
  26. package/dist/kit-main.js +8 -19
  27. package/dist/login.js +119 -104
  28. package/dist/logout.js +14 -13
  29. package/dist/mcp.js +8 -3
  30. package/dist/parts.js +82 -0
  31. package/dist/placement.js +31 -0
  32. package/dist/refusals.js +12 -0
  33. package/dist/relay.js +41 -0
  34. package/dist/resident.js +114 -51
  35. package/dist/scope.js +214 -0
  36. package/dist/scripts.js +255 -0
  37. package/dist/secret-input.js +4 -1
  38. package/dist/service-main.js +9 -0
  39. package/dist/service.js +55 -0
  40. package/dist/shell.js +3 -0
  41. package/dist/sign-in.js +40 -32
  42. package/dist/silence.js +28 -0
  43. package/dist/skills.js +49 -35
  44. package/dist/transcription.js +129 -41
  45. package/dist/translate.js +2 -8
  46. package/dist/update.js +91 -104
  47. package/dist/wire.js +22 -0
  48. package/dist/work.js +346 -0
  49. package/package.json +20 -20
  50. package/dist/blocks.js +0 -51
  51. package/dist/bootstrap.json +0 -24
  52. package/dist/clis-main.js +0 -23
  53. package/dist/stream.js +0 -46
  54. package/how-we-work.md +0 -23
  55. package/skills/ask-matt/LICENSE +0 -21
  56. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  57. package/skills/ask-matt/SKILL.md +0 -91
  58. package/skills/ask-matt/agents/openai.yaml +0 -4
  59. package/skills/code-review/LICENSE +0 -21
  60. package/skills/code-review/SKILL.md +0 -89
  61. package/skills/code-review/agents/openai.yaml +0 -3
  62. package/skills/codebase-design/DEEPENING.md +0 -37
  63. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  64. package/skills/codebase-design/LICENSE +0 -21
  65. package/skills/codebase-design/SKILL.md +0 -116
  66. package/skills/codebase-design/agents/openai.yaml +0 -3
  67. package/skills/diagnosing-bugs/LICENSE +0 -21
  68. package/skills/diagnosing-bugs/SKILL.md +0 -140
  69. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  70. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  71. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  72. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  73. package/skills/domain-modeling/LICENSE +0 -21
  74. package/skills/domain-modeling/SKILL.md +0 -76
  75. package/skills/domain-modeling/agents/openai.yaml +0 -3
  76. package/skills/grill-me/LICENSE +0 -21
  77. package/skills/grill-me/SKILL.md +0 -8
  78. package/skills/grill-me/agents/openai.yaml +0 -4
  79. package/skills/grill-with-docs/LICENSE +0 -21
  80. package/skills/grill-with-docs/SKILL.md +0 -8
  81. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  82. package/skills/grilling/LICENSE +0 -21
  83. package/skills/grilling/SKILL.md +0 -30
  84. package/skills/grilling/agents/openai.yaml +0 -3
  85. package/skills/handoff/LICENSE +0 -21
  86. package/skills/handoff/SKILL.md +0 -17
  87. package/skills/handoff/agents/openai.yaml +0 -4
  88. package/skills/implement/LICENSE +0 -21
  89. package/skills/implement/SKILL.md +0 -16
  90. package/skills/implement/agents/openai.yaml +0 -4
  91. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  92. package/skills/improve-codebase-architecture/LICENSE +0 -21
  93. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  94. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  95. package/skills/prototype/LICENSE +0 -21
  96. package/skills/prototype/LOGIC.md +0 -67
  97. package/skills/prototype/SKILL.md +0 -28
  98. package/skills/prototype/UI.md +0 -112
  99. package/skills/prototype/agents/openai.yaml +0 -3
  100. package/skills/research/LICENSE +0 -21
  101. package/skills/research/SKILL.md +0 -14
  102. package/skills/research/agents/openai.yaml +0 -3
  103. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  104. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  105. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  106. package/skills/tdd/LICENSE +0 -21
  107. package/skills/tdd/SKILL.md +0 -40
  108. package/skills/tdd/agents/openai.yaml +0 -3
  109. package/skills/tdd/mocking.md +0 -59
  110. package/skills/tdd/tests.md +0 -77
  111. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  112. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  113. package/skills/teach/LICENSE +0 -21
  114. package/skills/teach/MISSION-FORMAT.md +0 -31
  115. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  116. package/skills/teach/SKILL.md +0 -141
  117. package/skills/teach/agents/openai.yaml +0 -4
  118. package/skills/to-questionnaire/LICENSE +0 -21
  119. package/skills/to-questionnaire/SKILL.md +0 -55
  120. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  121. package/skills/to-spec/LICENSE +0 -21
  122. package/skills/to-spec/SKILL.md +0 -76
  123. package/skills/to-spec/agents/openai.yaml +0 -4
  124. package/skills/to-tickets/LICENSE +0 -21
  125. package/skills/to-tickets/SKILL.md +0 -106
  126. package/skills/to-tickets/agents/openai.yaml +0 -4
  127. package/skills/wait-what/LICENSE +0 -21
  128. package/skills/wait-what/SKILL.md +0 -8
  129. package/skills/wait-what/agents/openai.yaml +0 -4
  130. package/skills/wayfinder/LICENSE +0 -21
  131. package/skills/wayfinder/SKILL.md +0 -129
  132. package/skills/wayfinder/agents/openai.yaml +0 -4
  133. package/skills/writing-for-agents/LICENSE +0 -21
  134. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  135. package/skills/writing-for-agents/SKILL.md +0 -83
  136. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -1,63 +1,156 @@
1
1
  import { client, } from '@agentclientprotocol/sdk';
2
- import { randomUUID } from 'node:crypto';
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 { killMarked, startAdapter, TURN_ENDED, TURN_STARTED } from './acp.js';
7
- import { blocksOf, firstChange } from './blocks.js';
6
+ import { signedOut, startAdapter } from './acp.js';
7
+ import { finalRefusal, HouseRefusal } from './api.js';
8
8
  import { openBridge } from './bridge.js';
9
9
  import { CLIS } from './clis.js';
10
- import { placeFiles } from './files.js';
10
+ import { answeredFailure, readerFor } from './events.js';
11
+ import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
11
12
  import { agentBase } from './home.js';
12
13
  import { instructions } from './instructions.js';
14
+ import { Parts } from './parts.js';
15
+ import { refusalOf } from './refusals.js';
16
+ import { relayed } from './relay.js';
17
+ import { killMarked, Scope } from './scope.js';
13
18
  import { holdSecretInput } from './secret-input.js';
14
- import { createHowWeWork } from './skills.js';
15
19
  import { transcribe } from './transcription.js';
20
+ import { workFor } from './work.js';
16
21
  const KILLED = 'the conversation was killed';
22
+ const INTERRUPTED = 'the conversation was interrupted';
23
+ const REFUSED = 'the CLI refused the prompt';
24
+ const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
25
+ const GONE = 'House no longer holds this conversation; start a new one';
26
+ const RETIRED = "this conversation's Agent is retired, so it cannot start";
27
+ const ENDED = {
28
+ conversation_not_found: GONE,
29
+ conversation_historical: RETIRED,
30
+ };
31
+ const NO_START = "the Agent's CLI did not start";
32
+ const NO_SESSION = "the Agent's CLI did not open a session";
33
+ const NO_RESUME = "the Agent's CLI could not resume this conversation";
34
+ const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
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
+ class NotStarted extends Error {
38
+ }
39
+ class ResumeRefused extends NotStarted {
40
+ }
41
+ async function step(reason, work) {
42
+ try {
43
+ return await work;
44
+ }
45
+ catch (error) {
46
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
47
+ }
48
+ }
49
+ function unopened(reason) {
50
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
51
+ }
52
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
53
+ const STARTING_SHOWN_AFTER_MS = 1_000;
54
+ function unbridged(error) {
55
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
56
+ }
57
+ const EXIT_PLAN_MODE = {
58
+ parse(value) {
59
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
60
+ throw new Error('_x.ai/exit_plan_mode needs an object');
61
+ }
62
+ return value;
63
+ },
64
+ };
65
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
17
66
  function causeOf(error) {
18
67
  return error instanceof Error ? error.message : String(error);
19
68
  }
69
+ function answeredWith(error) {
70
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
71
+ }
72
+ function exitedWith(running, cause) {
73
+ if (running.killed)
74
+ return { failure: 'killed', cause: KILLED };
75
+ return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
76
+ }
20
77
  function answerMessage(request, response) {
21
78
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
22
79
  }
80
+ async function sentPart(url, bytes, headers) {
81
+ const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
82
+ await answer.arrayBuffer();
83
+ if (!answer.ok)
84
+ throw storeRefusal('upload', answer.status);
85
+ }
23
86
  function logged(error) {
24
87
  process.stderr.write(`kit: ${causeOf(error)}\n`);
25
88
  }
26
- function stop(bridge) {
27
- killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
89
+ function refusal(conversation, error) {
90
+ if (conversation.kills > 0)
91
+ return KILLED;
92
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
93
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
94
+ return causeOf(error);
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
+ }
102
+ function marker(bridge) {
103
+ return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
104
+ }
105
+ function steers(adapter) {
106
+ return adapter.initialized._meta?.steering?.supported === true;
28
107
  }
29
108
  class Turn {
30
109
  id = randomUUID();
31
- text = '';
32
- segment = null;
33
- admitted = false;
34
- notes = [];
35
- sent = [];
36
- unsentFrom = null;
37
- draftSequence = 0;
38
- planSequence = 0;
39
- entries = null;
110
+ parts;
40
111
  questions = 0;
41
112
  secrets = 0;
42
- prompt = null;
113
+ prompted = false;
114
+ steered = false;
115
+ taken = null;
43
116
  ended = false;
117
+ images = 0;
118
+ uploads = [];
119
+ constructor(emit) {
120
+ this.parts = new Parts(emit);
121
+ }
44
122
  }
45
123
  class Conversation {
46
124
  id;
47
125
  running = null;
126
+ session = null;
48
127
  turn = null;
49
128
  commands = null;
50
- jobs = new Set();
51
- queued = [];
52
- late = [];
129
+ options = null;
130
+ exitUntold = false;
131
+ context = null;
132
+ leftovers = new Set();
133
+ waiting = [];
134
+ done = false;
135
+ sending = null;
136
+ subagents = new Map();
137
+ held = [];
53
138
  opening = null;
139
+ bridge = null;
140
+ launch = null;
141
+ prompted = null;
54
142
  kills = 0;
55
143
  killed = new AbortController();
56
144
  interrupts = 0;
57
145
  interrupted = new AbortController();
58
- open = 0;
146
+ turns = new Set();
59
147
  queue = Promise.resolve();
60
148
  reports = Promise.resolve();
149
+ starting = null;
150
+ preparing = null;
151
+ told = null;
152
+ untold = false;
153
+ shown = null;
61
154
  constructor(id) {
62
155
  this.id = id;
63
156
  }
@@ -73,55 +166,82 @@ function asksSecret(request) {
73
166
  export class Conversations {
74
167
  kit;
75
168
  conversations = new Map();
76
- acks = new Map();
77
169
  questions = new Map();
78
- carrying = 0;
79
170
  reporting = 0;
171
+ watched = false;
80
172
  constructor(kit) {
81
173
  this.kit = kit;
174
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
175
+ setInterval(() => this.look(), 2_000);
82
176
  }
83
- input(input) {
84
- this.carrying++;
85
- const carried = this.acks.get(input.input_id) ??
86
- (() => {
87
- const conversation = this.conversation(input.conversation_id);
88
- if (input.kind === 'kill') {
89
- conversation.kills++;
90
- conversation.killed.abort(new Error(KILLED));
91
- this.halt(conversation);
92
- }
93
- if (input.kind === 'interrupt') {
94
- conversation.interrupts++;
95
- conversation.interrupted.abort();
96
- }
97
- const work = conversation.queue.then(() => this.carry(conversation, input));
98
- conversation.queue = work.catch(() => undefined);
99
- this.acks.set(input.input_id, work);
100
- return work;
101
- })();
102
- carried
103
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
104
- .catch(logged)
105
- .finally(() => {
106
- this.carrying--;
107
- this.kit.changed();
177
+ call(call) {
178
+ void this.kit.calls.carry(call.input_id, () => {
179
+ const conversation = this.conversation(call.conversation_id);
180
+ if (call.kind === 'kill') {
181
+ conversation.kills++;
182
+ conversation.killed.abort(new Error(KILLED));
183
+ for (const waiting of conversation.waiting.splice(0))
184
+ waiting.settle({ refused: KILLED });
185
+ this.halt(conversation);
186
+ }
187
+ if (call.kind === 'interrupt') {
188
+ conversation.interrupts++;
189
+ conversation.interrupted.abort();
190
+ }
191
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
192
+ conversation.queue = handled.catch(() => undefined);
193
+ return handled.then(async ({ result }) => ({ result: await result }));
108
194
  });
109
195
  }
110
196
  idle() {
111
- if (this.carrying > 0 || this.reporting > 0)
197
+ if (this.reporting > 0)
112
198
  return false;
113
199
  for (const conversation of this.conversations.values()) {
114
200
  const turn = conversation.turn;
115
201
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
116
- if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
202
+ if (conversation.turns.size > (asking ? 1 : 0))
117
203
  return false;
118
204
  }
119
- return true;
205
+ const conversations = [...this.conversations.values()];
206
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
207
+ return false;
208
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
209
+ }
210
+ quiet() {
211
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
212
+ }
213
+ look() {
214
+ for (const conversation of this.conversations.values()) {
215
+ for (const scope of conversation.leftovers) {
216
+ if (!scope.empty())
217
+ continue;
218
+ scope.close();
219
+ conversation.leftovers.delete(scope);
220
+ }
221
+ }
222
+ this.kit.changed();
120
223
  }
121
224
  opened() {
225
+ this.watched = false;
226
+ for (const conversation of this.conversations.values()) {
227
+ if (conversation.running !== null) {
228
+ this.facts(conversation);
229
+ this.subagentsChanged(conversation);
230
+ }
231
+ else if (conversation.exitUntold)
232
+ this.process(conversation, false);
233
+ if (conversation.untold || conversation.told !== null)
234
+ this.retell(conversation);
235
+ }
236
+ }
237
+ watching(watched) {
238
+ const resend = watched && !this.watched;
239
+ this.watched = watched;
240
+ if (!resend)
241
+ return;
122
242
  for (const conversation of this.conversations.values()) {
123
243
  if (conversation.turn !== null)
124
- conversation.turn.unsentFrom = 0;
244
+ this.replay(conversation, conversation.turn);
125
245
  }
126
246
  }
127
247
  conversation(id) {
@@ -132,96 +252,320 @@ export class Conversations {
132
252
  }
133
253
  return conversation;
134
254
  }
135
- async carry(conversation, input) {
136
- if (input.kind === 'open')
137
- return this.open(conversation, input);
138
- if (input.kind === 'message')
139
- return this.message(conversation, input);
140
- if (input.kind === 'interrupt') {
255
+ async carry(conversation, call) {
256
+ if (call.kind === 'open')
257
+ return this.open(conversation, call);
258
+ if (call.kind === 'message')
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
+ }
265
+ await this.launched(conversation);
266
+ if (call.kind === 'interrupt') {
141
267
  if (--conversation.interrupts === 0)
142
268
  conversation.interrupted = new AbortController();
143
- await this.interrupt(conversation, String(input.turn_id));
269
+ await this.interrupt(conversation, String(call.turn_id));
144
270
  }
145
- if (input.kind === 'kill') {
271
+ if (call.kind === 'kill') {
146
272
  await this.kill(conversation);
273
+ await conversation.sending;
147
274
  if (--conversation.kills === 0)
148
275
  conversation.killed = new AbortController();
149
276
  }
150
- if (input.kind === 'option')
151
- await this.option(conversation, String(input.option), input.value);
152
- if (input.kind === 'answer') {
153
- const question = this.questions.get(String(input.interaction_id));
277
+ if (call.kind === 'option')
278
+ await this.option(conversation, String(call.option), call.value);
279
+ if (call.kind === 'answer') {
280
+ const question = this.questions.get(String(call.interaction_id));
154
281
  if (question === undefined) {
155
- return this.message(conversation, { ...input, text: answerMessage(input.request, input.response), files: [], first: false });
282
+ const text = answerMessage(call.request, call.response);
283
+ return this.message(conversation, { ...call, text, first: false, message_id: null });
156
284
  }
157
- question.answer(input.response);
285
+ question.answer(call.response);
158
286
  }
159
- return {};
287
+ return { result: Promise.resolve({}) };
288
+ }
289
+ async open(conversation, call) {
290
+ await this.stop(conversation);
291
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
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;
300
+ }
301
+ launch(conversation, route, sessionId, empty) {
302
+ let opened;
303
+ let failed;
304
+ const bridge = new Promise((resolve, reject) => {
305
+ opened = resolve;
306
+ failed = reject;
307
+ });
308
+ bridge.catch(() => undefined);
309
+ const session = this.start(conversation, route, sessionId, empty, opened);
310
+ session.catch(failed);
311
+ const launch = { bridge, session };
312
+ conversation.launch = launch;
313
+ void session
314
+ .catch(() => undefined)
315
+ .then(() => {
316
+ if (conversation.launch !== launch)
317
+ return;
318
+ conversation.launch = null;
319
+ this.closeIdleBridge(conversation);
320
+ this.next(conversation);
321
+ });
322
+ this.next(conversation);
323
+ return launch;
324
+ }
325
+ async launched(conversation) {
326
+ while (conversation.launch !== null)
327
+ await conversation.launch.session.catch(() => undefined);
328
+ }
329
+ instructed(conversation, route) {
330
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
331
+ if (bridge === undefined)
332
+ return null;
333
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
334
+ read.catch(() => undefined);
335
+ return read;
160
336
  }
161
- async open(conversation, input) {
162
- if (conversation.running !== null)
163
- return {};
337
+ async message(conversation, call) {
338
+ const content = [
339
+ ...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
340
+ { type: 'text', text: String(call.text) },
341
+ ];
342
+ let route;
343
+ let instructing = null;
164
344
  try {
165
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
345
+ route = await this.route(conversation, call.agent_id);
346
+ if (call.first === true)
347
+ instructing = this.instructed(conversation, route);
348
+ if (call.message_id !== null)
349
+ content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
350
+ if (conversation.kills > 0)
351
+ throw new Error(KILLED);
166
352
  }
167
353
  catch (error) {
168
- return { refused: causeOf(error) };
354
+ this.closeIdleBridge(conversation);
355
+ this.prepares(conversation, null);
356
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
357
+ }
358
+ conversation.preparing = null;
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
+ }
413
+ }
414
+ next(conversation) {
415
+ if (conversation.sending !== null || conversation.turns.size > 0)
416
+ return;
417
+ const waiting = conversation.waiting.shift();
418
+ if (waiting === undefined) {
419
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
420
+ if (conversation.starting !== step)
421
+ this.starts(conversation, step);
422
+ if (conversation.done && conversation.launch === null)
423
+ void this.stop(conversation).catch(logged);
424
+ return;
169
425
  }
426
+ conversation.sending = this.send(conversation, waiting).finally(() => {
427
+ conversation.sending = null;
428
+ this.next(conversation);
429
+ });
170
430
  }
171
- async message(conversation, input) {
172
- let ack = {};
431
+ async send(conversation, waiting) {
432
+ const { call } = waiting;
433
+ let result = {};
434
+ let started = false;
435
+ let instructing = waiting.instructing;
173
436
  const prompt = [];
174
437
  try {
175
- const route = await this.route(conversation, input.agent_id);
176
- const files = input.files;
177
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
178
- const lines = await this.transcribed(conversation, files, paths);
179
438
  if (conversation.running === null) {
180
- const opened = await this.start(conversation, route, input.provider_session_id);
181
- if (input.provider_session_id === null)
182
- ack = { provider_session_id: opened };
439
+ this.starts(conversation, 'session');
440
+ await this.launched(conversation);
441
+ }
442
+ if (conversation.running === null) {
443
+ const stored = conversation.session ?? call.provider_session_id;
444
+ const empty = call.first === true && conversation.prompted !== stored;
445
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
446
+ if (call.first === true)
447
+ instructing = this.instructed(conversation, waiting.route);
448
+ started = (await launch.session) !== stored;
449
+ }
450
+ const session = conversation.running.sessionId;
451
+ if (started || (call.first === true && session !== call.provider_session_id)) {
452
+ result = { provider_session_id: session };
183
453
  }
184
- if (input.first === true) {
185
- prompt.push({
186
- type: 'text',
187
- text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
188
- });
454
+ if (call.first === true) {
455
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
189
456
  }
190
- prompt.push({ type: 'text', text: String(input.text) });
191
- if (lines.length > 0)
192
- prompt.push({ type: 'text', text: lines.join('\n') });
193
457
  if (conversation.kills > 0)
194
458
  throw new Error(KILLED);
195
459
  }
196
460
  catch (error) {
197
- if ('provider_session_id' in ack)
461
+ this.starts(conversation, null);
462
+ if (started) {
463
+ conversation.session = null;
198
464
  await this.kill(conversation);
199
- return { refused: conversation.kills > 0 ? KILLED : causeOf(error) };
465
+ }
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 });
469
+ return;
200
470
  }
201
- this.prompt(conversation, prompt);
202
- return ack;
471
+ this.starts(conversation, null);
472
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
473
+ waiting.settle(result);
203
474
  }
204
- async transcribed(conversation, files, paths) {
205
- const lines = [];
206
- for (const [index, path] of paths.entries()) {
207
- lines.push(path);
208
- if (files[index].media_type?.startsWith('audio/') !== true)
209
- continue;
210
- try {
211
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
212
- if (text !== '')
213
- lines.push(`Transcript: ${text}`);
214
- }
215
- catch (error) {
216
- if (conversation.kills > 0)
217
- throw error;
218
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
475
+ starts(conversation, step) {
476
+ conversation.starting = step;
477
+ if (conversation.shown !== null)
478
+ clearTimeout(conversation.shown);
479
+ conversation.shown = null;
480
+ if (step === null || conversation.told !== null) {
481
+ this.tell(conversation, step);
482
+ return;
483
+ }
484
+ conversation.shown = setTimeout(() => {
485
+ conversation.shown = null;
486
+ if (conversation.starting === step)
487
+ this.tell(conversation, step);
488
+ }, STARTING_SHOWN_AFTER_MS);
489
+ }
490
+ prepares(conversation, step) {
491
+ conversation.preparing = step;
492
+ this.next(conversation);
493
+ }
494
+ tell(conversation, step) {
495
+ if (conversation.told === step && !conversation.untold)
496
+ return;
497
+ conversation.told = step;
498
+ this.retell(conversation);
499
+ }
500
+ retell(conversation) {
501
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
502
+ }
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}` });
219
531
  }
220
532
  }
221
- 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);
222
562
  }
223
563
  async interrupt(conversation, turnId) {
224
- 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)
225
569
  return;
226
570
  await conversation.running.adapter.connection.agent.notify('session/cancel', {
227
571
  sessionId: conversation.running.sessionId,
@@ -229,11 +573,13 @@ export class Conversations {
229
573
  }
230
574
  halt(conversation) {
231
575
  if (conversation.opening !== null)
232
- stop(conversation.opening);
576
+ killMarked(marker(conversation.opening));
577
+ for (const scope of conversation.leftovers)
578
+ scope.kill();
233
579
  if (conversation.running === null)
234
580
  return;
235
581
  conversation.running.killed = true;
236
- stop(conversation.running.bridge);
582
+ conversation.running.scope.kill();
237
583
  }
238
584
  async kill(conversation) {
239
585
  const running = conversation.running;
@@ -245,6 +591,7 @@ export class Conversations {
245
591
  if (running === null)
246
592
  return;
247
593
  try {
594
+ running.reader.requested(option, value);
248
595
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
249
596
  sessionId: running.sessionId,
250
597
  configId: option,
@@ -253,6 +600,7 @@ export class Conversations {
253
600
  this.options(conversation, answered.configOptions);
254
601
  }
255
602
  catch (error) {
603
+ running.reader.refused(option);
256
604
  logged(error);
257
605
  }
258
606
  }
@@ -269,58 +617,82 @@ export class Conversations {
269
617
  }
270
618
  return route;
271
619
  }
272
- async start(conversation, route, sessionId) {
620
+ async start(conversation, routed, sessionId, empty, bridged) {
621
+ const route = await routed;
622
+ const reader = readerFor(route.kind);
623
+ reader.session = sessionId;
273
624
  const app = client({ name: '@agentshouse/kit' })
274
- .onNotification('session/update', ({ params }) => this.update(conversation, params))
275
- .onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
276
- .onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
277
625
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
278
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
626
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
627
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
279
628
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
280
- const bridge = await 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);
281
631
  if (conversation.kills > 0) {
282
- bridge.close();
632
+ this.closeBridge(conversation, bridging, bridge);
283
633
  throw new Error(KILLED);
284
634
  }
285
635
  conversation.opening = bridge;
636
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
637
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
638
+ let adapter = null;
286
639
  try {
287
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
288
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
289
- void adapter.exited.then(() => bridge.close());
640
+ bridged(bridge);
641
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
642
+ const streamed = await step(NO_CONNECTION, work.open(cli));
643
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
644
+ work.read(message);
645
+ this.read(conversation, reader, message);
646
+ }, { ...bridge.env, ...streamed }, undefined, scope));
647
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
290
648
  const agent = adapter.connection.agent;
291
649
  const cwd = route.working_directory;
292
- const opened = sessionId === null
293
- ? await agent.request('session/new', { cwd, mcpServers: [] })
294
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [] })), sessionId };
295
- const running = {
296
- adapter,
297
- bridge,
298
- sessionId: opened.sessionId,
299
- killed: false,
300
- queues: CLIS[route.kind].queues,
301
- phase: CLIS[route.kind].phase,
302
- };
303
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
650
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
651
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
652
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
653
+ reader.session = opened.sessionId;
654
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
655
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
656
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
657
+ }
658
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
659
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
660
+ void work.settled().then(() => scope.record());
304
661
  conversation.running = running;
305
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
306
- if (conversation.commands !== null)
307
- this.commands(conversation, conversation.commands);
308
- this.options(conversation, options);
309
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
662
+ adapter.turn(conversation.turns.size > 0);
663
+ conversation.session = running.sessionId;
664
+ conversation.options = options;
665
+ this.facts(conversation);
666
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
310
667
  return running.sessionId;
311
668
  }
312
669
  catch (error) {
313
- stop(bridge);
314
- bridge.close();
670
+ adapter?.retire();
671
+ work.close();
672
+ conversation.leftovers.add(scope);
673
+ this.closeBridge(conversation, bridging, bridge);
315
674
  throw conversation.kills > 0 ? new Error(KILLED) : error;
316
675
  }
317
676
  finally {
318
677
  conversation.opening = null;
319
678
  }
320
679
  }
680
+ async resume(conversation, kind, adapter, request, empty) {
681
+ try {
682
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
683
+ }
684
+ catch (error) {
685
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
686
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
687
+ }
688
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
689
+ return null;
690
+ }
691
+ }
321
692
  async launchSettings(running, route, offered) {
322
693
  let options = offered;
323
694
  const settings = [
695
+ ['mode', route.mode ?? CLIS[route.kind].fullAccess],
324
696
  ['model', route.model],
325
697
  ['thought_level', route.effort],
326
698
  ];
@@ -328,6 +700,7 @@ export class Conversations {
328
700
  if (value === null)
329
701
  continue;
330
702
  const option = options.find((candidate) => candidate.category === category);
703
+ running.reader.requested(option?.id ?? category, value);
331
704
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
332
705
  sessionId: running.sessionId,
333
706
  configId: option?.id ?? category,
@@ -337,23 +710,45 @@ export class Conversations {
337
710
  }
338
711
  return options;
339
712
  }
340
- exited(conversation, adapter, cause) {
341
- const running = conversation.running;
342
- if (running?.adapter !== adapter)
343
- return;
713
+ exited(conversation, running, cause) {
714
+ this.detach(conversation, running);
715
+ if (conversation.turn !== null) {
716
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
717
+ }
718
+ this.next(conversation);
719
+ this.kit.changed();
720
+ }
721
+ detach(conversation, running) {
722
+ if (conversation.bridge === running.bridging)
723
+ conversation.bridge = null;
724
+ running.work.close();
725
+ conversation.leftovers.add(running.scope);
344
726
  conversation.running = null;
345
727
  conversation.commands = null;
346
- conversation.jobs.clear();
347
- conversation.queued.length = 0;
728
+ conversation.options = null;
729
+ if (conversation.subagents.size > 0) {
730
+ conversation.subagents.clear();
731
+ this.subagentsChanged(conversation);
732
+ }
348
733
  for (const [id, question] of this.questions) {
349
734
  if (question.conversation === conversation)
350
735
  this.questions.delete(id);
351
736
  }
352
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
353
- if (conversation.turn !== null) {
354
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
355
- }
356
- this.kit.changed();
737
+ this.process(conversation, false);
738
+ }
739
+ process(conversation, running) {
740
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
741
+ }
742
+ facts(conversation) {
743
+ this.process(conversation, true);
744
+ if (conversation.commands !== null)
745
+ this.commands(conversation, conversation.commands);
746
+ if (conversation.options !== null)
747
+ this.options(conversation, conversation.options);
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 });
357
752
  }
358
753
  commands(conversation, commands) {
359
754
  conversation.commands = commands;
@@ -364,11 +759,12 @@ export class Conversations {
364
759
  options(conversation, options) {
365
760
  if (conversation.running === null)
366
761
  return;
762
+ conversation.options = options;
367
763
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
368
764
  }
369
765
  deliver(conversation, turn, event, body) {
370
766
  this.reporting++;
371
- const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
767
+ const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
372
768
  conversation.reports = delivered
373
769
  .catch(() => undefined)
374
770
  .finally(() => {
@@ -377,122 +773,103 @@ export class Conversations {
377
773
  });
378
774
  return delivered;
379
775
  }
380
- report(conversation) {
381
- const turn = new Turn();
382
- conversation.open++;
383
- void this.deliver(conversation, turn, 'started', {})
384
- .then(() => this.admitted(conversation, turn))
385
- .catch(logged);
386
- return turn;
776
+ write(conversation, turn, operation) {
777
+ if (turn.ended || !this.watched)
778
+ return;
779
+ this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
387
780
  }
388
- admitted(conversation, turn) {
389
- turn.admitted = true;
390
- for (const note of turn.notes.splice(0))
391
- this.kit.send(note);
392
- if (turn.text !== '') {
393
- turn.unsentFrom = 0;
394
- this.draft(conversation, turn);
395
- }
396
- if (turn.entries !== null)
397
- this.plan(conversation, turn);
781
+ replay(conversation, turn) {
782
+ for (const operation of turn.parts.operations())
783
+ this.write(conversation, turn, operation);
398
784
  }
399
785
  begin(conversation) {
400
- const turn = this.report(conversation);
786
+ const turn = new Turn((operation) => this.write(conversation, turn, operation));
787
+ conversation.turns.add(turn);
788
+ conversation.running?.adapter.turn(true);
789
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
401
790
  conversation.turn = turn;
791
+ if (conversation.context !== null)
792
+ turn.parts.measure(conversation.context);
793
+ for (const part of conversation.held.splice(0))
794
+ turn.parts.start(part);
402
795
  this.kit.changed();
403
796
  return turn;
404
797
  }
405
- finish(conversation, turn) {
406
- if (turn.ended)
407
- return;
408
- const segment = turn.segment;
409
- turn.segment = null;
410
- if (segment?.phase === 'note')
411
- this.note(conversation, turn, segment.text);
412
- else if (segment !== null)
413
- this.answer(conversation, turn, segment.text);
414
- this.end(conversation, turn, { text: turn.text });
415
- }
416
- end(conversation, turn, outcome) {
798
+ end(conversation, turn, failed, notice = null) {
417
799
  if (turn.ended)
418
800
  return;
801
+ if (failed !== null)
802
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
803
+ if (conversation.turn === turn) {
804
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
805
+ this.apply(conversation, turn, event);
806
+ }
807
+ if (notice !== null)
808
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
419
809
  turn.ended = true;
420
- conversation.open--;
810
+ conversation.turns.delete(turn);
811
+ conversation.running?.adapter.turn(conversation.turns.size > 0);
421
812
  if (conversation.turn === turn)
422
813
  conversation.turn = null;
423
- void this.deliver(conversation, turn, 'ended', outcome).catch(logged);
424
- const late = conversation.open === 0 ? conversation.late.shift() : undefined;
425
- if (late !== undefined)
426
- this.failed(conversation, late);
814
+ const body = Promise.allSettled(turn.uploads).then(() => ({
815
+ parts: turn.parts.saved(),
816
+ context: turn.parts.context,
817
+ ...(failed === null ? {} : { failed: failed.failure }),
818
+ }));
819
+ void this.deliver(conversation, turn, 'ended', body).catch(logged);
820
+ this.next(conversation);
427
821
  this.kit.changed();
428
822
  }
429
- failed(conversation, cause) {
430
- if (conversation.open > 0)
431
- conversation.late.push(cause);
432
- else
433
- this.end(conversation, this.report(conversation), { failed: cause });
434
- }
435
- prompt(conversation, prompt) {
823
+ prompt(conversation, prompt, taken) {
436
824
  const running = conversation.running;
437
- const sent = { turn: null };
438
- if (running.queues && (conversation.turn !== null || conversation.queued.length > 0)) {
439
- conversation.queued.push(sent);
440
- }
441
- else {
442
- this.attach(conversation.turn ?? this.begin(conversation), sent);
443
- }
444
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
445
- if (!running.adapter.connection.signal.aborted)
446
- this.answered(conversation, sent, causeOf(error));
447
- else if (sent.turn !== null) {
448
- this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
825
+ const turn = this.begin(conversation);
826
+ turn.prompted = true;
827
+ turn.taken = taken;
828
+ conversation.prompted = running.sessionId;
829
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
830
+ this.took(turn);
831
+ const notice = answeredFailure(answer);
832
+ if (notice !== null) {
833
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
834
+ }
835
+ else {
836
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
837
+ }
838
+ }, async (error) => {
839
+ if (running.adapter.connection.signal.aborted) {
840
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
841
+ }
842
+ else {
843
+ this.took(turn);
844
+ if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
845
+ running.adapter.retire();
846
+ this.detach(conversation, running);
847
+ }
848
+ this.end(conversation, turn, answeredWith(error));
449
849
  }
450
850
  });
451
851
  }
452
- attach(turn, sent) {
453
- turn.prompt = sent;
454
- sent.turn = turn;
455
- }
456
- answered(conversation, sent, failed) {
457
- const turn = sent.turn;
458
- if (turn === null) {
459
- const queued = conversation.queued.indexOf(sent);
460
- if (queued < 0)
461
- return;
462
- conversation.queued.splice(queued, 1);
463
- if (failed !== null)
464
- this.failed(conversation, failed);
465
- }
466
- else if (failed !== null) {
467
- if (turn.ended)
468
- this.failed(conversation, failed);
469
- else
470
- this.end(conversation, turn, { failed });
471
- }
472
- else if (turn.prompt === sent) {
473
- this.finish(conversation, turn);
474
- }
475
- }
476
852
  started(conversation) {
477
- if (conversation.turn !== null)
478
- return conversation.turn;
479
- const turn = this.begin(conversation);
480
- const queued = conversation.queued.shift();
481
- if (queued !== undefined)
482
- this.attach(turn, queued);
853
+ const turn = conversation.turn ?? this.begin(conversation);
854
+ this.took(turn);
483
855
  return turn;
484
856
  }
485
- ended(conversation) {
857
+ ended(conversation, stop) {
486
858
  const turn = conversation.turn;
487
859
  if (turn === null)
488
860
  return;
489
- if (turn.prompt === null) {
490
- this.finish(conversation, turn);
491
- }
492
- else {
493
- conversation.turn = null;
494
- 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;
495
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();
496
873
  }
497
874
  async ask(conversation, method, params, secret, signal) {
498
875
  const turn = this.started(conversation);
@@ -538,101 +915,126 @@ export class Conversations {
538
915
  if (content !== null)
539
916
  this.questions.get(interactionId)?.answer({ action: 'accept', content });
540
917
  }
541
- update(conversation, notification) {
542
- const update = notification.update;
543
- if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
544
- this.chunk(conversation, this.started(conversation), update, update.content.text);
545
- }
546
- else if (update.sessionUpdate === 'plan') {
547
- const turn = this.started(conversation);
548
- this.close(conversation, turn);
549
- turn.entries = update.entries;
550
- this.plan(conversation, turn);
551
- }
552
- else if (update.sessionUpdate === 'tool_call') {
553
- this.close(conversation, this.started(conversation));
554
- }
555
- else if (update.sessionUpdate === 'agent_thought_chunk') {
556
- const turn = this.started(conversation);
557
- if (turn.segment?.messageId !== (update.messageId ?? null))
558
- this.close(conversation, turn);
559
- }
560
- else if (update.sessionUpdate === 'available_commands_update') {
561
- this.commands(conversation, update.availableCommands);
562
- }
563
- else if (update.sessionUpdate === 'config_option_update') {
564
- this.options(conversation, update.configOptions);
918
+ read(conversation, reader, message) {
919
+ for (const event of reader.read(message))
920
+ this.event(conversation, event);
921
+ }
922
+ event(conversation, event) {
923
+ switch (event.kind) {
924
+ case 'started':
925
+ this.started(conversation);
926
+ return;
927
+ case 'ended':
928
+ this.ended(conversation, event.stop);
929
+ return;
930
+ case 'commands':
931
+ this.commands(conversation, event.commands);
932
+ return;
933
+ case 'options':
934
+ this.options(conversation, event.options);
935
+ return;
936
+ case 'context':
937
+ conversation.context = { used: event.used, window: event.window };
938
+ conversation.turn?.parts.measure(conversation.context);
939
+ return;
940
+ case 'subagent-done':
941
+ this.subagentDone(conversation, event.id, event.result);
942
+ return;
943
+ case 'subagent-activity':
944
+ this.subagentActivity(conversation, event.id, event.activity);
945
+ return;
946
+ case 'marker':
947
+ case 'image':
948
+ if (conversation.turn !== null)
949
+ break;
950
+ if (event.kind === 'marker' && event.marker === 'job')
951
+ conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
952
+ return;
565
953
  }
954
+ this.apply(conversation, this.started(conversation), event);
566
955
  }
567
- job(conversation, job) {
568
- if (job.running)
569
- conversation.jobs.add(job.id);
570
- else
571
- conversation.jobs.delete(job.id);
572
- this.kit.changed();
956
+ took(turn) {
957
+ const taken = turn.taken;
958
+ turn.taken = null;
959
+ taken?.();
573
960
  }
574
- chunk(conversation, turn, chunk, text) {
575
- const phase = conversation.running?.phase(chunk) ?? null;
576
- const messageId = chunk.messageId ?? null;
577
- if (turn.segment?.messageId !== messageId || turn.segment.phase !== phase)
578
- this.close(conversation, turn);
579
- if (phase === 'answer') {
580
- this.answer(conversation, turn, text);
581
- return;
961
+ apply(conversation, turn, event) {
962
+ switch (event.kind) {
963
+ case 'text':
964
+ case 'reasoning':
965
+ turn.parts.write(event.kind, event.id, event.text);
966
+ return;
967
+ case 'command':
968
+ turn.parts.command();
969
+ return;
970
+ case 'subagent': {
971
+ const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
972
+ conversation.subagents.set(event.id, { description: event.description, turn, index, activity: null });
973
+ this.subagentsChanged(conversation);
974
+ return;
975
+ }
976
+ case 'message':
977
+ turn.parts.start({ type: 'message', command: event.command, target: event.target });
978
+ return;
979
+ case 'marker':
980
+ turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
981
+ return;
982
+ case 'plan':
983
+ turn.parts.plan(event.entries);
984
+ return;
985
+ case 'image':
986
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
987
+ this.kit.changed();
988
+ return;
582
989
  }
583
- turn.segment ??= { messageId, phase, text: '' };
584
- turn.segment.text += text;
585
990
  }
586
- close(conversation, turn) {
587
- const segment = turn.segment;
588
- if (segment === null)
991
+ subagentDone(conversation, id, result) {
992
+ const subagent = conversation.subagents.get(id);
993
+ if (subagent === undefined)
589
994
  return;
590
- turn.segment = null;
591
- this.note(conversation, turn, segment.text);
592
- }
593
- note(conversation, turn, text) {
594
- if (text.trim() === '')
995
+ conversation.subagents.delete(id);
996
+ this.subagentsChanged(conversation);
997
+ if (subagent.turn === conversation.turn) {
998
+ subagent.turn.parts.done(subagent.index, result);
595
999
  return;
596
- const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
597
- if (turn.admitted)
598
- this.kit.send(note);
1000
+ }
1001
+ const part = { type: 'subagent', description: subagent.description, status: 'done', result };
1002
+ if (conversation.turn === null)
1003
+ conversation.held.push(part);
599
1004
  else
600
- turn.notes.push(note);
601
- }
602
- answer(conversation, turn, text) {
603
- turn.text += text;
604
- this.draft(conversation, turn);
605
- }
606
- draft(conversation, turn) {
607
- const blocks = blocksOf(turn.text);
608
- const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
609
- const sent = this.kit.send({
610
- type: 'draft',
611
- conversation_id: conversation.id,
612
- turn_id: turn.id,
613
- sequence: turn.draftSequence + 1,
614
- from,
615
- blocks: blocks.slice(from),
616
- });
617
- if (sent) {
618
- turn.draftSequence++;
619
- turn.sent = blocks;
620
- turn.unsentFrom = null;
1005
+ conversation.turn.parts.start(part);
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
+ }
1014
+ async image(conversation, turn, reserved, data, mediaType) {
1015
+ const bytes = Buffer.from(data, 'base64');
1016
+ const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
1017
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
1018
+ try {
1019
+ const declared = (await this.deliver(conversation, turn, 'images', {
1020
+ name,
1021
+ media_type: mediaType,
1022
+ bytes: bytes.length,
1023
+ sha256,
1024
+ }));
1025
+ const saved = await uploaded(declared.upload, {
1026
+ name,
1027
+ bytes: bytes.length,
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 }),
1032
+ });
1033
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
621
1034
  }
622
- else {
623
- turn.unsentFrom = from;
1035
+ catch (error) {
1036
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
1037
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
624
1038
  }
625
1039
  }
626
- plan(conversation, turn) {
627
- const sent = this.kit.send({
628
- type: 'plan',
629
- conversation_id: conversation.id,
630
- turn_id: turn.id,
631
- sequence: turn.planSequence + 1,
632
- from: 0,
633
- steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
634
- });
635
- if (sent)
636
- turn.planSequence++;
637
- }
638
1040
  }