@agentshouse/kit 0.2.1-ce145e9aadbf6 → 0.2.1-cf3a50c90b895

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 +540 -305
  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 +22 -2
  19. package/dist/git.js +11 -9
  20. package/dist/home.js +96 -7
  21. package/dist/house-cli.js +291 -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 +118 -103
  27. package/dist/logout.js +14 -13
  28. package/dist/mcp.js +2 -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 +57 -9
  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,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_METHODS = 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;
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();
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);
156
+ }
157
+ call(call) {
158
+ void this.kit.calls.carry(call.input_id, () => {
159
+ const conversation = this.conversation(call.conversation_id);
160
+ if (call.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 (call.kind === 'interrupt') {
168
+ conversation.interrupts++;
169
+ conversation.interrupted.abort();
170
+ }
171
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
172
+ conversation.queue = handled.catch(() => undefined);
173
+ return handled.then(async ({ result }) => ({ result: await result }));
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) {
@@ -132,74 +230,190 @@ export class Conversations {
132
230
  }
133
231
  return conversation;
134
232
  }
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') {
233
+ async carry(conversation, call) {
234
+ if (call.kind === 'open')
235
+ return { result: this.open(conversation, call) };
236
+ if (call.kind === 'message')
237
+ return this.message(conversation, call);
238
+ await this.launched(conversation);
239
+ if (call.kind === 'interrupt') {
141
240
  if (--conversation.interrupts === 0)
142
241
  conversation.interrupted = new AbortController();
143
- await this.interrupt(conversation, String(input.turn_id));
242
+ await this.interrupt(conversation, String(call.turn_id));
144
243
  }
145
- if (input.kind === 'kill') {
244
+ if (call.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
  }
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));
250
+ if (call.kind === 'option')
251
+ await this.option(conversation, String(call.option), call.value);
252
+ if (call.kind === 'answer') {
253
+ const question = this.questions.get(String(call.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(call.request, call.response);
256
+ return this.message(conversation, { ...call, text, files: [], first: false });
156
257
  }
157
- question.answer(input.response);
258
+ question.answer(call.response);
158
259
  }
159
- return {};
260
+ return { result: Promise.resolve({}) };
160
261
  }
161
- async open(conversation, input) {
262
+ open(conversation, call) {
162
263
  if (conversation.running !== null)
163
- return {};
264
+ return Promise.resolve({});
265
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.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, call) {
304
+ const content = [{ type: 'text', text: String(call.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, call.agent_id);
309
+ if (call.first === true)
310
+ instructing = this.instructed(conversation, route);
311
+ const files = call.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 { result: Promise.resolve({ refused: refusal(conversation, error) }) };
324
+ }
325
+ conversation.preparing = null;
326
+ return {
327
+ result: new Promise((settle) => {
328
+ conversation.waiting.push({ call, 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) {
172
- let ack = {};
348
+ async send(conversation, waiting) {
349
+ const { call } = waiting;
350
+ let result = {};
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 ?? call.provider_session_id;
361
+ const empty = call.first === true && conversation.prompted !== stored;
362
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
363
+ if (call.first === true)
364
+ instructing = this.instructed(conversation, waiting.route);
365
+ started = (await launch.session) !== stored;
183
366
  }
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
- });
367
+ const session = conversation.running.sessionId;
368
+ if (started || (call.first === true && session !== call.provider_session_id)) {
369
+ result = { provider_session_id: session };
370
+ }
371
+ if (call.first === true) {
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.calls.keepResult(String(call.input_id), result));
388
+ waiting.settle(result);
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,50 +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
- await CLIS[route.kind].allowHouse();
289
- 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));
290
518
  void adapter.exited.then(() => bridge.close());
291
519
  const agent = adapter.connection.agent;
292
520
  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 ?? []);
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());
306
532
  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));
533
+ conversation.session = running.sessionId;
534
+ conversation.options = options;
535
+ this.facts(conversation);
536
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
312
537
  return running.sessionId;
313
538
  }
314
539
  catch (error) {
315
- stop(bridge);
540
+ adapter?.retire();
541
+ work.close();
542
+ conversation.leftovers.add(scope);
316
543
  bridge.close();
317
544
  throw conversation.kills > 0 ? new Error(KILLED) : error;
318
545
  }
@@ -320,6 +547,18 @@ export class Conversations {
320
547
  conversation.opening = null;
321
548
  }
322
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
+ }
323
562
  async launchSettings(running, route, offered) {
324
563
  let options = offered;
325
564
  const settings = [
@@ -331,6 +570,7 @@ export class Conversations {
331
570
  if (value === null)
332
571
  continue;
333
572
  const option = options.find((candidate) => candidate.category === category);
573
+ running.reader.requested(option?.id ?? category, value);
334
574
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
335
575
  sessionId: running.sessionId,
336
576
  configId: option?.id ?? category,
@@ -340,23 +580,35 @@ export class Conversations {
340
580
  }
341
581
  return options;
342
582
  }
343
- exited(conversation, adapter, cause) {
344
- const running = conversation.running;
345
- if (running?.adapter !== adapter)
346
- 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);
347
594
  conversation.running = null;
348
595
  conversation.commands = null;
349
- conversation.jobs.clear();
350
- conversation.queued.length = 0;
596
+ conversation.options = null;
351
597
  for (const [id, question] of this.questions) {
352
598
  if (question.conversation === conversation)
353
599
  this.questions.delete(id);
354
600
  }
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();
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);
360
612
  }
361
613
  commands(conversation, commands) {
362
614
  conversation.commands = commands;
@@ -367,11 +619,12 @@ export class Conversations {
367
619
  options(conversation, options) {
368
620
  if (conversation.running === null)
369
621
  return;
622
+ conversation.options = options;
370
623
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
371
624
  }
372
625
  deliver(conversation, turn, event, body) {
373
626
  this.reporting++;
374
- 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));
375
628
  conversation.reports = delivered
376
629
  .catch(() => undefined)
377
630
  .finally(() => {
@@ -380,119 +633,95 @@ export class Conversations {
380
633
  });
381
634
  return delivered;
382
635
  }
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;
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 });
390
640
  }
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);
641
+ replay(conversation, turn) {
642
+ for (const operation of turn.parts.operations())
643
+ this.write(conversation, turn, operation);
401
644
  }
402
645
  begin(conversation) {
403
- 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);
404
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);
405
654
  this.kit.changed();
406
655
  return turn;
407
656
  }
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) {
657
+ end(conversation, turn, failed, notice = null) {
420
658
  if (turn.ended)
421
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 });
422
668
  turn.ended = true;
423
669
  conversation.open--;
424
670
  if (conversation.turn === turn)
425
671
  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);
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);
430
679
  this.kit.changed();
431
680
  }
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) {
681
+ prompt(conversation, prompt, taken) {
439
682
  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 });
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));
452
707
  }
453
708
  });
454
709
  }
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
710
  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);
711
+ const turn = conversation.turn ?? this.begin(conversation);
712
+ this.took(turn);
486
713
  return turn;
487
714
  }
488
715
  ended(conversation) {
489
716
  const turn = conversation.turn;
490
717
  if (turn === null)
491
718
  return;
492
- if (turn.prompt === null) {
493
- this.finish(conversation, turn);
719
+ if (!turn.prompted) {
720
+ this.end(conversation, turn, null);
494
721
  }
495
722
  else {
723
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
724
+ this.apply(conversation, turn, event);
496
725
  conversation.turn = null;
497
726
  this.kit.changed();
498
727
  }
@@ -541,101 +770,107 @@ export class Conversations {
541
770
  if (content !== null)
542
771
  this.questions.get(interactionId)?.answer({ action: 'accept', content });
543
772
  }
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);
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;
568
805
  }
806
+ this.apply(conversation, this.started(conversation), event);
569
807
  }
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();
808
+ took(turn) {
809
+ const taken = turn.taken;
810
+ turn.taken = null;
811
+ taken?.();
576
812
  }
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;
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;
585
840
  }
586
- turn.segment ??= { messageId, phase, text: '' };
587
- turn.segment.text += text;
588
841
  }
589
- close(conversation, turn) {
590
- const segment = turn.segment;
591
- if (segment === null)
842
+ subagentDone(conversation, id, result) {
843
+ const subagent = conversation.subagents.get(id);
844
+ if (subagent === undefined)
592
845
  return;
593
- turn.segment = null;
594
- this.note(conversation, turn, segment.text);
595
- }
596
- note(conversation, turn, text) {
597
- if (text.trim() === '')
846
+ conversation.subagents.delete(id);
847
+ if (subagent.turn === conversation.turn) {
848
+ subagent.turn.parts.done(subagent.index, result);
598
849
  return;
599
- const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
600
- if (turn.admitted)
601
- 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);
602
854
  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;
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);
624
871
  }
625
- else {
626
- turn.unsentFrom = from;
872
+ catch (error) {
873
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
627
874
  }
628
875
  }
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
876
  }