@agentshouse/kit 0.2.1-c5262a8cc063d → 0.2.1-c585225098c15

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