@agentshouse/kit 0.2.1-cb6679d048e60 → 0.2.1-cbdf76b9a90a3

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 (104) hide show
  1. package/README.md +32 -31
  2. package/dist/acp.js +46 -103
  3. package/dist/agents.js +178 -49
  4. package/dist/api.js +36 -3
  5. package/dist/app-git.js +48 -0
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +39 -58
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +137 -65
  10. package/dist/codex-main.js +58 -0
  11. package/dist/conversations.js +525 -287
  12. package/dist/copies.js +215 -146
  13. package/dist/end.js +27 -0
  14. package/dist/enrol-main.js +7 -1
  15. package/dist/events.js +525 -0
  16. package/dist/files.js +24 -3
  17. package/dist/git.js +10 -8
  18. package/dist/home.js +96 -7
  19. package/dist/house-cli.js +291 -0
  20. package/dist/house-main.js +7 -159
  21. package/dist/inputs.js +63 -0
  22. package/dist/instructions.js +10 -32
  23. package/dist/kit-cli.js +68 -0
  24. package/dist/kit-main.js +8 -19
  25. package/dist/login.js +118 -103
  26. package/dist/logout.js +14 -13
  27. package/dist/mcp.js +2 -1
  28. package/dist/parts.js +82 -0
  29. package/dist/placement.js +39 -0
  30. package/dist/refusals.js +12 -0
  31. package/dist/relay.js +20 -0
  32. package/dist/resident.js +91 -35
  33. package/dist/scope.js +206 -0
  34. package/dist/secret-input.js +4 -1
  35. package/dist/service-main.js +9 -0
  36. package/dist/service.js +55 -0
  37. package/dist/shell.js +3 -0
  38. package/dist/sign-in.js +34 -27
  39. package/dist/skills.js +81 -12
  40. package/dist/stream.js +89 -12
  41. package/dist/transcription.js +57 -9
  42. package/dist/translate.js +2 -8
  43. package/dist/update.js +50 -29
  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 +3 -2
  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/skills/resolving-merge-conflicts/SKILL.md +0 -16
  103. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  104. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
@@ -1,56 +1,124 @@
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 } 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 { placeFiles, uploadBytes } 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 { killMarked, Scope } from './scope.js';
13
17
  import { holdSecretInput } from './secret-input.js';
14
18
  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 REFUSED = 'the CLI refused the prompt';
24
+ const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
25
+ const GONE = 'House no longer holds this conversation; start a new one';
26
+ const RETIRED = "this conversation's Agent is retired, so it cannot start";
27
+ const ENDED = {
28
+ conversation_not_found: GONE,
29
+ conversation_historical: RETIRED,
30
+ };
31
+ const NO_START = "the Agent's CLI did not start";
32
+ const NO_SESSION = "the Agent's CLI did not open a session";
33
+ const NO_RESUME = "the Agent's CLI could not resume this conversation";
34
+ const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
35
+ const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
36
+ class NotStarted extends Error {
37
+ }
38
+ async function step(reason, work) {
39
+ try {
40
+ return await work;
41
+ }
42
+ catch (error) {
43
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
44
+ }
45
+ }
46
+ function signedOut(error) {
47
+ return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
48
+ }
49
+ function unopened(reason) {
50
+ return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
51
+ }
52
+ // A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
53
+ const STARTING_SHOWN_AFTER_MS = 1_000;
54
+ function unbridged(error) {
55
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
56
+ }
57
+ const EXIT_PLAN_MODE = {
58
+ parse(value) {
59
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
60
+ throw new Error('_x.ai/exit_plan_mode needs an object');
61
+ }
62
+ return value;
63
+ },
64
+ };
65
+ export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
17
66
  function causeOf(error) {
18
67
  return error instanceof Error ? error.message : String(error);
19
68
  }
69
+ function answeredWith(error) {
70
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
71
+ }
72
+ function exitedWith(running, cause) {
73
+ return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
74
+ }
20
75
  function answerMessage(request, response) {
21
76
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
22
77
  }
23
78
  function logged(error) {
24
79
  process.stderr.write(`kit: ${causeOf(error)}\n`);
25
80
  }
26
- function stop(bridge) {
27
- killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
81
+ function refusal(conversation, error) {
82
+ if (conversation.kills > 0)
83
+ return KILLED;
84
+ const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
85
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
86
+ return causeOf(error);
87
+ }
88
+ function marker(bridge) {
89
+ return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
28
90
  }
29
91
  class Turn {
30
92
  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;
93
+ parts;
40
94
  questions = 0;
41
95
  secrets = 0;
42
- prompt = null;
96
+ prompted = false;
97
+ taken = null;
43
98
  ended = false;
99
+ images = 0;
100
+ uploads = [];
101
+ constructor(emit) {
102
+ this.parts = new Parts(emit);
103
+ }
44
104
  }
45
105
  class Conversation {
46
106
  id;
47
107
  running = null;
108
+ session = null;
48
109
  turn = null;
49
110
  commands = null;
50
- jobs = new Set();
51
- queued = [];
52
- late = [];
111
+ options = null;
112
+ exitUntold = false;
113
+ context = null;
114
+ leftovers = new Set();
115
+ waiting = [];
116
+ sending = null;
117
+ subagents = new Map();
118
+ held = [];
53
119
  opening = null;
120
+ launch = null;
121
+ prompted = null;
54
122
  kills = 0;
55
123
  killed = new AbortController();
56
124
  interrupts = 0;
@@ -58,6 +126,11 @@ class Conversation {
58
126
  open = 0;
59
127
  queue = Promise.resolve();
60
128
  reports = Promise.resolve();
129
+ starting = null;
130
+ preparing = null;
131
+ told = null;
132
+ untold = false;
133
+ shown = null;
61
134
  constructor(id) {
62
135
  this.id = id;
63
136
  }
@@ -73,55 +146,80 @@ function asksSecret(request) {
73
146
  export class Conversations {
74
147
  kit;
75
148
  conversations = new Map();
76
- acks = new Map();
77
149
  questions = new Map();
78
- carrying = 0;
79
150
  reporting = 0;
151
+ watched = false;
80
152
  constructor(kit) {
81
153
  this.kit = kit;
154
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
155
+ setInterval(() => this.look(), 2_000);
82
156
  }
83
157
  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();
158
+ void this.kit.inputs.carry(input.input_id, () => {
159
+ const conversation = this.conversation(input.conversation_id);
160
+ if (input.kind === 'kill') {
161
+ conversation.kills++;
162
+ conversation.killed.abort(new Error(KILLED));
163
+ for (const waiting of conversation.waiting.splice(0))
164
+ waiting.settle({ refused: KILLED });
165
+ this.halt(conversation);
166
+ }
167
+ if (input.kind === 'interrupt') {
168
+ conversation.interrupts++;
169
+ conversation.interrupted.abort();
170
+ }
171
+ const handled = conversation.queue.then(() => this.carry(conversation, input));
172
+ conversation.queue = handled.catch(() => undefined);
173
+ return handled.then(async ({ ack }) => ({ ack: await ack }));
108
174
  });
109
175
  }
110
176
  idle() {
111
- if (this.carrying > 0 || this.reporting > 0)
177
+ if (this.reporting > 0)
112
178
  return false;
113
179
  for (const conversation of this.conversations.values()) {
114
180
  const turn = conversation.turn;
115
181
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
116
- if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
182
+ if (conversation.open > (asking ? 1 : 0))
117
183
  return false;
118
184
  }
119
- return true;
185
+ const conversations = [...this.conversations.values()];
186
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
187
+ return false;
188
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
189
+ }
190
+ quiet() {
191
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
192
+ }
193
+ look() {
194
+ for (const conversation of this.conversations.values()) {
195
+ for (const scope of conversation.leftovers) {
196
+ if (!scope.empty())
197
+ continue;
198
+ scope.close();
199
+ conversation.leftovers.delete(scope);
200
+ }
201
+ }
202
+ this.kit.changed();
120
203
  }
121
204
  opened() {
205
+ this.watched = false;
206
+ for (const conversation of this.conversations.values()) {
207
+ if (conversation.running !== null)
208
+ this.facts(conversation);
209
+ else if (conversation.exitUntold)
210
+ this.process(conversation, false);
211
+ if (conversation.untold || conversation.told !== null)
212
+ this.retell(conversation);
213
+ }
214
+ }
215
+ watching(watched) {
216
+ const resend = watched && !this.watched;
217
+ this.watched = watched;
218
+ if (!resend)
219
+ return;
122
220
  for (const conversation of this.conversations.values()) {
123
221
  if (conversation.turn !== null)
124
- conversation.turn.unsentFrom = 0;
222
+ this.replay(conversation, conversation.turn);
125
223
  }
126
224
  }
127
225
  conversation(id) {
@@ -134,9 +232,10 @@ export class Conversations {
134
232
  }
135
233
  async carry(conversation, input) {
136
234
  if (input.kind === 'open')
137
- return this.open(conversation, input);
235
+ return { ack: this.open(conversation, input) };
138
236
  if (input.kind === 'message')
139
237
  return this.message(conversation, input);
238
+ await this.launched(conversation);
140
239
  if (input.kind === 'interrupt') {
141
240
  if (--conversation.interrupts === 0)
142
241
  conversation.interrupted = new AbortController();
@@ -144,6 +243,7 @@ export class Conversations {
144
243
  }
145
244
  if (input.kind === 'kill') {
146
245
  await this.kill(conversation);
246
+ await conversation.sending;
147
247
  if (--conversation.kills === 0)
148
248
  conversation.killed = new AbortController();
149
249
  }
@@ -152,54 +252,168 @@ export class Conversations {
152
252
  if (input.kind === 'answer') {
153
253
  const question = this.questions.get(String(input.interaction_id));
154
254
  if (question === undefined) {
155
- return this.message(conversation, { ...input, text: answerMessage(input.request, input.response), files: [], first: false });
255
+ const text = answerMessage(input.request, input.response);
256
+ return this.message(conversation, { ...input, text, files: [], first: false });
156
257
  }
157
258
  question.answer(input.response);
158
259
  }
159
- return {};
260
+ return { ack: Promise.resolve({}) };
160
261
  }
161
- async open(conversation, input) {
262
+ open(conversation, input) {
162
263
  if (conversation.running !== null)
163
- return {};
264
+ return Promise.resolve({});
265
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, input.agent_id), null, true);
266
+ return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
267
+ }
268
+ launch(conversation, route, sessionId, empty) {
269
+ let opened;
270
+ let failed;
271
+ const bridge = new Promise((resolve, reject) => {
272
+ opened = resolve;
273
+ failed = reject;
274
+ });
275
+ bridge.catch(() => undefined);
276
+ const session = this.start(conversation, route, sessionId, empty, opened);
277
+ session.catch(failed);
278
+ const launch = { bridge, session };
279
+ conversation.launch = launch;
280
+ void session
281
+ .catch(() => undefined)
282
+ .then(() => {
283
+ if (conversation.launch !== launch)
284
+ return;
285
+ conversation.launch = null;
286
+ this.next(conversation);
287
+ });
288
+ this.next(conversation);
289
+ return launch;
290
+ }
291
+ async launched(conversation) {
292
+ while (conversation.launch !== null)
293
+ await conversation.launch.session.catch(() => undefined);
294
+ }
295
+ instructed(conversation, route) {
296
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
297
+ if (bridge === undefined)
298
+ return null;
299
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
300
+ read.catch(() => undefined);
301
+ return read;
302
+ }
303
+ async message(conversation, input) {
304
+ const content = [{ type: 'text', text: String(input.text) }];
305
+ let route;
306
+ let instructing = null;
164
307
  try {
165
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
308
+ route = await this.route(conversation, input.agent_id);
309
+ if (input.first === true)
310
+ instructing = this.instructed(conversation, route);
311
+ const files = input.files;
312
+ if (files.length > 0)
313
+ this.prepares(conversation, 'files');
314
+ const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
315
+ const lines = await this.transcribed(conversation, files, paths);
316
+ if (lines.length > 0)
317
+ content.push({ type: 'text', text: lines.join('\n') });
318
+ if (conversation.kills > 0)
319
+ throw new Error(KILLED);
166
320
  }
167
321
  catch (error) {
168
- return { refused: causeOf(error) };
322
+ this.prepares(conversation, null);
323
+ return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
324
+ }
325
+ conversation.preparing = null;
326
+ return {
327
+ ack: new Promise((settle) => {
328
+ conversation.waiting.push({ input, route, content, instructing, settle });
329
+ this.next(conversation);
330
+ }),
331
+ };
332
+ }
333
+ next(conversation) {
334
+ if (conversation.sending !== null || conversation.open > 0)
335
+ return;
336
+ const waiting = conversation.waiting.shift();
337
+ if (waiting === undefined) {
338
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
339
+ if (conversation.starting !== step)
340
+ this.starts(conversation, step);
341
+ return;
169
342
  }
343
+ conversation.sending = this.send(conversation, waiting).finally(() => {
344
+ conversation.sending = null;
345
+ this.next(conversation);
346
+ });
170
347
  }
171
- async message(conversation, input) {
348
+ async send(conversation, waiting) {
349
+ const { input } = waiting;
172
350
  let ack = {};
351
+ let started = false;
352
+ let instructing = waiting.instructing;
173
353
  const prompt = [];
174
354
  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
355
  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 };
356
+ this.starts(conversation, 'session');
357
+ await this.launched(conversation);
358
+ }
359
+ if (conversation.running === null) {
360
+ const stored = conversation.session ?? input.provider_session_id;
361
+ const empty = input.first === true && conversation.prompted !== stored;
362
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
363
+ if (input.first === true)
364
+ instructing = this.instructed(conversation, waiting.route);
365
+ started = (await launch.session) !== stored;
366
+ }
367
+ const session = conversation.running.sessionId;
368
+ if (started || (input.first === true && session !== input.provider_session_id)) {
369
+ ack = { provider_session_id: session };
183
370
  }
184
371
  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
- });
372
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
189
373
  }
190
- prompt.push({ type: 'text', text: String(input.text) });
191
- if (lines.length > 0)
192
- prompt.push({ type: 'text', text: lines.join('\n') });
193
374
  if (conversation.kills > 0)
194
375
  throw new Error(KILLED);
195
376
  }
196
377
  catch (error) {
197
- if ('provider_session_id' in ack)
378
+ this.starts(conversation, null);
379
+ if (started) {
380
+ conversation.session = null;
198
381
  await this.kill(conversation);
199
- return { refused: conversation.kills > 0 ? KILLED : causeOf(error) };
382
+ }
383
+ waiting.settle({ refused: refusal(conversation, error) });
384
+ return;
385
+ }
386
+ this.starts(conversation, null);
387
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.inputs.hands(String(input.input_id), ack));
388
+ waiting.settle(ack);
389
+ }
390
+ starts(conversation, step) {
391
+ conversation.starting = step;
392
+ if (conversation.shown !== null)
393
+ clearTimeout(conversation.shown);
394
+ conversation.shown = null;
395
+ if (step === null || conversation.told !== null) {
396
+ this.tell(conversation, step);
397
+ return;
200
398
  }
201
- this.prompt(conversation, prompt);
202
- return ack;
399
+ conversation.shown = setTimeout(() => {
400
+ conversation.shown = null;
401
+ if (conversation.starting === step)
402
+ this.tell(conversation, step);
403
+ }, STARTING_SHOWN_AFTER_MS);
404
+ }
405
+ prepares(conversation, step) {
406
+ conversation.preparing = step;
407
+ this.next(conversation);
408
+ }
409
+ tell(conversation, step) {
410
+ if (conversation.told === step && !conversation.untold)
411
+ return;
412
+ conversation.told = step;
413
+ this.retell(conversation);
414
+ }
415
+ retell(conversation) {
416
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
203
417
  }
204
418
  async transcribed(conversation, files, paths) {
205
419
  const lines = [];
@@ -207,9 +421,10 @@ export class Conversations {
207
421
  lines.push(path);
208
422
  if (files[index].media_type?.startsWith('audio/') !== true)
209
423
  continue;
424
+ this.prepares(conversation, 'transcription');
210
425
  try {
211
426
  const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
212
- if (text !== '')
427
+ if (text !== null && text !== '')
213
428
  lines.push(`Transcript: ${text}`);
214
429
  }
215
430
  catch (error) {
@@ -229,11 +444,13 @@ export class Conversations {
229
444
  }
230
445
  halt(conversation) {
231
446
  if (conversation.opening !== null)
232
- stop(conversation.opening);
447
+ killMarked(marker(conversation.opening));
448
+ for (const scope of conversation.leftovers)
449
+ scope.kill();
233
450
  if (conversation.running === null)
234
451
  return;
235
452
  conversation.running.killed = true;
236
- stop(conversation.running.bridge);
453
+ conversation.running.scope.kill();
237
454
  }
238
455
  async kill(conversation) {
239
456
  const running = conversation.running;
@@ -245,6 +462,7 @@ export class Conversations {
245
462
  if (running === null)
246
463
  return;
247
464
  try {
465
+ running.reader.requested(option, value);
248
466
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
249
467
  sessionId: running.sessionId,
250
468
  configId: option,
@@ -253,6 +471,7 @@ export class Conversations {
253
471
  this.options(conversation, answered.configOptions);
254
472
  }
255
473
  catch (error) {
474
+ running.reader.refused(option);
256
475
  logged(error);
257
476
  }
258
477
  }
@@ -269,48 +488,58 @@ export class Conversations {
269
488
  }
270
489
  return route;
271
490
  }
272
- async start(conversation, route, sessionId) {
491
+ async start(conversation, routed, sessionId, empty, bridged) {
492
+ const route = await routed;
493
+ const reader = readerFor(route.kind);
494
+ reader.session = sessionId;
273
495
  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
496
  .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));
497
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
498
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
279
499
  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);
500
+ const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
281
501
  if (conversation.kills > 0) {
282
502
  bridge.close();
283
503
  throw new Error(KILLED);
284
504
  }
285
505
  conversation.opening = bridge;
506
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
507
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
508
+ let adapter = null;
286
509
  try {
287
510
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
288
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
511
+ bridged(bridge);
512
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
513
+ const streamed = await step(NO_CONNECTION, work.open(cli));
514
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
515
+ work.read(message);
516
+ this.read(conversation, reader, message);
517
+ }, { ...bridge.env, ...streamed }, undefined, scope));
289
518
  void adapter.exited.then(() => bridge.close());
290
519
  const agent = adapter.connection.agent;
291
520
  const cwd = route.working_directory;
292
- const opened = sessionId === null
293
- ? await agent.request('session/new', { cwd, mcpServers: [] })
294
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [] })), sessionId };
295
- const running = {
296
- adapter,
297
- bridge,
298
- sessionId: opened.sessionId,
299
- killed: false,
300
- queues: CLIS[route.kind].queues,
301
- phase: CLIS[route.kind].phase,
302
- };
303
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
521
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
522
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
523
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
524
+ reader.session = opened.sessionId;
525
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
526
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
527
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
528
+ }
529
+ const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
530
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
531
+ void work.settled().then(() => scope.record());
304
532
  conversation.running = running;
305
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
306
- if (conversation.commands !== null)
307
- this.commands(conversation, conversation.commands);
308
- this.options(conversation, options);
309
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
533
+ conversation.session = running.sessionId;
534
+ conversation.options = options;
535
+ this.facts(conversation);
536
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
310
537
  return running.sessionId;
311
538
  }
312
539
  catch (error) {
313
- stop(bridge);
540
+ adapter?.retire();
541
+ work.close();
542
+ conversation.leftovers.add(scope);
314
543
  bridge.close();
315
544
  throw conversation.kills > 0 ? new Error(KILLED) : error;
316
545
  }
@@ -318,9 +547,22 @@ export class Conversations {
318
547
  conversation.opening = null;
319
548
  }
320
549
  }
550
+ async resume(conversation, kind, adapter, request, empty) {
551
+ try {
552
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
553
+ }
554
+ catch (error) {
555
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
556
+ throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
557
+ }
558
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
559
+ return null;
560
+ }
561
+ }
321
562
  async launchSettings(running, route, offered) {
322
563
  let options = offered;
323
564
  const settings = [
565
+ ['mode', route.mode ?? CLIS[route.kind].fullAccess],
324
566
  ['model', route.model],
325
567
  ['thought_level', route.effort],
326
568
  ];
@@ -328,6 +570,7 @@ export class Conversations {
328
570
  if (value === null)
329
571
  continue;
330
572
  const option = options.find((candidate) => candidate.category === category);
573
+ running.reader.requested(option?.id ?? category, value);
331
574
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
332
575
  sessionId: running.sessionId,
333
576
  configId: option?.id ?? category,
@@ -337,23 +580,35 @@ export class Conversations {
337
580
  }
338
581
  return options;
339
582
  }
340
- exited(conversation, adapter, cause) {
341
- const running = conversation.running;
342
- if (running?.adapter !== adapter)
343
- return;
583
+ exited(conversation, running, cause) {
584
+ this.detach(conversation, running);
585
+ if (conversation.turn !== null) {
586
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
587
+ }
588
+ this.next(conversation);
589
+ this.kit.changed();
590
+ }
591
+ detach(conversation, running) {
592
+ running.work.close();
593
+ conversation.leftovers.add(running.scope);
344
594
  conversation.running = null;
345
595
  conversation.commands = null;
346
- conversation.jobs.clear();
347
- conversation.queued.length = 0;
596
+ conversation.options = null;
348
597
  for (const [id, question] of this.questions) {
349
598
  if (question.conversation === conversation)
350
599
  this.questions.delete(id);
351
600
  }
352
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
353
- if (conversation.turn !== null) {
354
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
355
- }
356
- this.kit.changed();
601
+ this.process(conversation, false);
602
+ }
603
+ process(conversation, running) {
604
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
605
+ }
606
+ facts(conversation) {
607
+ this.process(conversation, true);
608
+ if (conversation.commands !== null)
609
+ this.commands(conversation, conversation.commands);
610
+ if (conversation.options !== null)
611
+ this.options(conversation, conversation.options);
357
612
  }
358
613
  commands(conversation, commands) {
359
614
  conversation.commands = commands;
@@ -364,11 +619,12 @@ export class Conversations {
364
619
  options(conversation, options) {
365
620
  if (conversation.running === null)
366
621
  return;
622
+ conversation.options = options;
367
623
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
368
624
  }
369
625
  deliver(conversation, turn, event, body) {
370
626
  this.reporting++;
371
- const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
627
+ const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
372
628
  conversation.reports = delivered
373
629
  .catch(() => undefined)
374
630
  .finally(() => {
@@ -377,119 +633,95 @@ export class Conversations {
377
633
  });
378
634
  return delivered;
379
635
  }
380
- report(conversation) {
381
- const turn = new Turn();
382
- conversation.open++;
383
- void this.deliver(conversation, turn, 'started', {})
384
- .then(() => this.admitted(conversation, turn))
385
- .catch(logged);
386
- return turn;
636
+ write(conversation, turn, operation) {
637
+ if (turn.ended || !this.watched)
638
+ return;
639
+ this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
387
640
  }
388
- admitted(conversation, turn) {
389
- turn.admitted = true;
390
- for (const note of turn.notes.splice(0))
391
- this.kit.send(note);
392
- if (turn.text !== '') {
393
- turn.unsentFrom = 0;
394
- this.draft(conversation, turn);
395
- }
396
- if (turn.entries !== null)
397
- this.plan(conversation, turn);
641
+ replay(conversation, turn) {
642
+ for (const operation of turn.parts.operations())
643
+ this.write(conversation, turn, operation);
398
644
  }
399
645
  begin(conversation) {
400
- const turn = this.report(conversation);
646
+ const turn = new Turn((operation) => this.write(conversation, turn, operation));
647
+ conversation.open++;
648
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
401
649
  conversation.turn = turn;
650
+ if (conversation.context !== null)
651
+ turn.parts.measure(conversation.context);
652
+ for (const part of conversation.held.splice(0))
653
+ turn.parts.start(part);
402
654
  this.kit.changed();
403
655
  return turn;
404
656
  }
405
- finish(conversation, turn) {
406
- if (turn.ended)
407
- return;
408
- const segment = turn.segment;
409
- turn.segment = null;
410
- if (segment?.phase === 'note')
411
- this.note(conversation, turn, segment.text);
412
- else if (segment !== null)
413
- this.answer(conversation, turn, segment.text);
414
- this.end(conversation, turn, { text: turn.text });
415
- }
416
- end(conversation, turn, outcome) {
657
+ end(conversation, turn, failed, notice = null) {
417
658
  if (turn.ended)
418
659
  return;
660
+ if (failed !== null)
661
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
662
+ if (conversation.turn === turn) {
663
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
664
+ this.apply(conversation, turn, event);
665
+ }
666
+ if (notice !== null)
667
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
419
668
  turn.ended = true;
420
669
  conversation.open--;
421
670
  if (conversation.turn === turn)
422
671
  conversation.turn = null;
423
- void this.deliver(conversation, turn, 'ended', outcome).catch(logged);
424
- const late = conversation.open === 0 ? conversation.late.shift() : undefined;
425
- if (late !== undefined)
426
- this.failed(conversation, late);
672
+ const body = Promise.allSettled(turn.uploads).then(() => ({
673
+ parts: turn.parts.saved(),
674
+ context: turn.parts.context,
675
+ ...(failed === null ? {} : { failed: failed.failure }),
676
+ }));
677
+ void this.deliver(conversation, turn, 'ended', body).catch(logged);
678
+ this.next(conversation);
427
679
  this.kit.changed();
428
680
  }
429
- failed(conversation, cause) {
430
- if (conversation.open > 0)
431
- conversation.late.push(cause);
432
- else
433
- this.end(conversation, this.report(conversation), { failed: cause });
434
- }
435
- prompt(conversation, prompt) {
681
+ prompt(conversation, prompt, taken) {
436
682
  const running = conversation.running;
437
- const sent = { turn: null };
438
- if (running.queues && (conversation.turn !== null || conversation.queued.length > 0)) {
439
- conversation.queued.push(sent);
440
- }
441
- else {
442
- this.attach(conversation.turn ?? this.begin(conversation), sent);
443
- }
444
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
445
- if (!running.adapter.connection.signal.aborted)
446
- this.answered(conversation, sent, causeOf(error));
447
- else if (sent.turn !== null) {
448
- this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
683
+ const turn = this.begin(conversation);
684
+ turn.prompted = true;
685
+ turn.taken = taken;
686
+ conversation.prompted = running.sessionId;
687
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
688
+ this.took(turn);
689
+ const notice = answeredFailure(answer);
690
+ if (notice !== null) {
691
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
692
+ }
693
+ else {
694
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
695
+ }
696
+ }, async (error) => {
697
+ if (running.adapter.connection.signal.aborted) {
698
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
699
+ }
700
+ else {
701
+ this.took(turn);
702
+ if (conversation.open === 1 && conversation.running === running) {
703
+ running.adapter.retire();
704
+ this.detach(conversation, running);
705
+ }
706
+ this.end(conversation, turn, answeredWith(error));
449
707
  }
450
708
  });
451
709
  }
452
- attach(turn, sent) {
453
- turn.prompt = sent;
454
- sent.turn = turn;
455
- }
456
- answered(conversation, sent, failed) {
457
- const turn = sent.turn;
458
- if (turn === null) {
459
- const queued = conversation.queued.indexOf(sent);
460
- if (queued < 0)
461
- return;
462
- conversation.queued.splice(queued, 1);
463
- if (failed !== null)
464
- this.failed(conversation, failed);
465
- }
466
- else if (failed !== null) {
467
- if (turn.ended)
468
- this.failed(conversation, failed);
469
- else
470
- this.end(conversation, turn, { failed });
471
- }
472
- else if (turn.prompt === sent) {
473
- this.finish(conversation, turn);
474
- }
475
- }
476
710
  started(conversation) {
477
- if (conversation.turn !== null)
478
- return conversation.turn;
479
- const turn = this.begin(conversation);
480
- const queued = conversation.queued.shift();
481
- if (queued !== undefined)
482
- this.attach(turn, queued);
711
+ const turn = conversation.turn ?? this.begin(conversation);
712
+ this.took(turn);
483
713
  return turn;
484
714
  }
485
715
  ended(conversation) {
486
716
  const turn = conversation.turn;
487
717
  if (turn === null)
488
718
  return;
489
- if (turn.prompt === null) {
490
- this.finish(conversation, turn);
719
+ if (!turn.prompted) {
720
+ this.end(conversation, turn, null);
491
721
  }
492
722
  else {
723
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
724
+ this.apply(conversation, turn, event);
493
725
  conversation.turn = null;
494
726
  this.kit.changed();
495
727
  }
@@ -538,101 +770,107 @@ export class Conversations {
538
770
  if (content !== null)
539
771
  this.questions.get(interactionId)?.answer({ action: 'accept', content });
540
772
  }
541
- update(conversation, notification) {
542
- const update = notification.update;
543
- if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
544
- this.chunk(conversation, this.started(conversation), update, update.content.text);
545
- }
546
- else if (update.sessionUpdate === 'plan') {
547
- const turn = this.started(conversation);
548
- this.close(conversation, turn);
549
- turn.entries = update.entries;
550
- this.plan(conversation, turn);
551
- }
552
- else if (update.sessionUpdate === 'tool_call') {
553
- this.close(conversation, this.started(conversation));
554
- }
555
- else if (update.sessionUpdate === 'agent_thought_chunk') {
556
- const turn = this.started(conversation);
557
- if (turn.segment?.messageId !== (update.messageId ?? null))
558
- this.close(conversation, turn);
559
- }
560
- else if (update.sessionUpdate === 'available_commands_update') {
561
- this.commands(conversation, update.availableCommands);
562
- }
563
- else if (update.sessionUpdate === 'config_option_update') {
564
- this.options(conversation, update.configOptions);
773
+ read(conversation, reader, message) {
774
+ for (const event of reader.read(message))
775
+ this.event(conversation, event);
776
+ }
777
+ event(conversation, event) {
778
+ switch (event.kind) {
779
+ case 'started':
780
+ this.started(conversation);
781
+ return;
782
+ case 'ended':
783
+ this.ended(conversation);
784
+ return;
785
+ case 'commands':
786
+ this.commands(conversation, event.commands);
787
+ return;
788
+ case 'options':
789
+ this.options(conversation, event.options);
790
+ return;
791
+ case 'context':
792
+ conversation.context = { used: event.used, window: event.window };
793
+ conversation.turn?.parts.measure(conversation.context);
794
+ return;
795
+ case 'subagent-done':
796
+ this.subagentDone(conversation, event.id, event.result);
797
+ return;
798
+ case 'marker':
799
+ case 'image':
800
+ if (conversation.turn !== null)
801
+ break;
802
+ if (event.kind === 'marker' && event.marker === 'job')
803
+ conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
804
+ return;
565
805
  }
806
+ this.apply(conversation, this.started(conversation), event);
566
807
  }
567
- job(conversation, job) {
568
- if (job.running)
569
- conversation.jobs.add(job.id);
570
- else
571
- conversation.jobs.delete(job.id);
572
- this.kit.changed();
808
+ took(turn) {
809
+ const taken = turn.taken;
810
+ turn.taken = null;
811
+ taken?.();
573
812
  }
574
- chunk(conversation, turn, chunk, text) {
575
- const phase = conversation.running?.phase(chunk) ?? null;
576
- const messageId = chunk.messageId ?? null;
577
- if (turn.segment?.messageId !== messageId || turn.segment.phase !== phase)
578
- this.close(conversation, turn);
579
- if (phase === 'answer') {
580
- this.answer(conversation, turn, text);
581
- return;
813
+ apply(conversation, turn, event) {
814
+ switch (event.kind) {
815
+ case 'text':
816
+ case 'reasoning':
817
+ turn.parts.write(event.kind, event.id, event.text);
818
+ return;
819
+ case 'command':
820
+ turn.parts.command();
821
+ return;
822
+ case 'subagent': {
823
+ const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
824
+ conversation.subagents.set(event.id, { description: event.description, turn, index });
825
+ return;
826
+ }
827
+ case 'message':
828
+ turn.parts.start({ type: 'message', command: event.command, target: event.target });
829
+ return;
830
+ case 'marker':
831
+ turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
832
+ return;
833
+ case 'plan':
834
+ turn.parts.plan(event.entries);
835
+ return;
836
+ case 'image':
837
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
838
+ this.kit.changed();
839
+ return;
582
840
  }
583
- turn.segment ??= { messageId, phase, text: '' };
584
- turn.segment.text += text;
585
841
  }
586
- close(conversation, turn) {
587
- const segment = turn.segment;
588
- if (segment === null)
842
+ subagentDone(conversation, id, result) {
843
+ const subagent = conversation.subagents.get(id);
844
+ if (subagent === undefined)
589
845
  return;
590
- turn.segment = null;
591
- this.note(conversation, turn, segment.text);
592
- }
593
- note(conversation, turn, text) {
594
- if (text.trim() === '')
846
+ conversation.subagents.delete(id);
847
+ if (subagent.turn === conversation.turn) {
848
+ subagent.turn.parts.done(subagent.index, result);
595
849
  return;
596
- const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
597
- if (turn.admitted)
598
- this.kit.send(note);
850
+ }
851
+ const part = { type: 'subagent', description: subagent.description, status: 'done', result };
852
+ if (conversation.turn === null)
853
+ conversation.held.push(part);
599
854
  else
600
- turn.notes.push(note);
601
- }
602
- answer(conversation, turn, text) {
603
- turn.text += text;
604
- this.draft(conversation, turn);
605
- }
606
- draft(conversation, turn) {
607
- const blocks = blocksOf(turn.text);
608
- const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
609
- const sent = this.kit.send({
610
- type: 'draft',
611
- conversation_id: conversation.id,
612
- turn_id: turn.id,
613
- sequence: turn.draftSequence + 1,
614
- from,
615
- blocks: blocks.slice(from),
616
- });
617
- if (sent) {
618
- turn.draftSequence++;
619
- turn.sent = blocks;
620
- turn.unsentFrom = null;
855
+ conversation.turn.parts.start(part);
856
+ }
857
+ async image(conversation, turn, reserved, data, mediaType) {
858
+ const bytes = Buffer.from(data, 'base64');
859
+ const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
860
+ try {
861
+ const declared = (await this.deliver(conversation, turn, 'images', {
862
+ name,
863
+ media_type: mediaType,
864
+ bytes: bytes.length,
865
+ sha256: createHash('sha256').update(bytes).digest('hex'),
866
+ }));
867
+ const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
868
+ if (saved.save.status !== 'saved')
869
+ throw new Error(saved.save.failure ?? saved.save.status);
870
+ turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
621
871
  }
622
- else {
623
- turn.unsentFrom = from;
872
+ catch (error) {
873
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
624
874
  }
625
875
  }
626
- plan(conversation, turn) {
627
- const sent = this.kit.send({
628
- type: 'plan',
629
- conversation_id: conversation.id,
630
- turn_id: turn.id,
631
- sequence: turn.planSequence + 1,
632
- from: 0,
633
- steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
634
- });
635
- if (sent)
636
- turn.planSequence++;
637
- }
638
876
  }