@agentshouse/kit 0.2.1-c3895fcff473b → 0.2.1-c3a3042f56d6a

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