@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c031c7457a661

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