@agentshouse/kit 0.2.1-ce145e9aadbf6 → 0.2.1-ce3d2bb67177c

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