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

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 (139) hide show
  1. package/README.md +48 -29
  2. package/dist/abort.js +9 -0
  3. package/dist/acp.js +102 -88
  4. package/dist/agents.js +356 -52
  5. package/dist/api.js +40 -4
  6. package/dist/app-git.js +48 -0
  7. package/dist/authority-main.js +2 -2
  8. package/dist/bridge.js +93 -78
  9. package/dist/building.js +50 -0
  10. package/dist/calls.js +71 -0
  11. package/dist/chatgpt.js +32 -0
  12. package/dist/chunks.js +49 -0
  13. package/dist/clis.js +224 -88
  14. package/dist/codex-main.js +58 -0
  15. package/dist/connection.js +105 -0
  16. package/dist/conversations.js +749 -270
  17. package/dist/copies.js +475 -260
  18. package/dist/end.js +33 -0
  19. package/dist/enrol-main.js +7 -1
  20. package/dist/events.js +546 -0
  21. package/dist/files.js +140 -17
  22. package/dist/git-main.js +2 -2
  23. package/dist/git.js +62 -22
  24. package/dist/home.js +120 -9
  25. package/dist/house-cli.js +299 -0
  26. package/dist/house-main.js +5 -102
  27. package/dist/instructions.js +17 -18
  28. package/dist/kit-cli.js +68 -0
  29. package/dist/kit-main.js +8 -16
  30. package/dist/login.js +130 -95
  31. package/dist/logout.js +19 -0
  32. package/dist/mcp.js +43 -0
  33. package/dist/operation.js +7 -4
  34. package/dist/parts.js +82 -0
  35. package/dist/placement.js +31 -0
  36. package/dist/refusals.js +53 -0
  37. package/dist/relay.js +41 -0
  38. package/dist/resident.js +144 -52
  39. package/dist/scope.js +214 -0
  40. package/dist/scripts.js +255 -0
  41. package/dist/secret-input.js +7 -2
  42. package/dist/service-main.js +9 -0
  43. package/dist/service.js +55 -0
  44. package/dist/shell.js +3 -0
  45. package/dist/sign-in.js +52 -32
  46. package/dist/silence.js +28 -0
  47. package/dist/skills.js +48 -39
  48. package/dist/transcription.js +210 -0
  49. package/dist/translate.js +2 -8
  50. package/dist/update.js +127 -0
  51. package/dist/wire.js +40 -0
  52. package/dist/work.js +346 -0
  53. package/package.json +21 -24
  54. package/dist/blocks.js +0 -51
  55. package/dist/bootstrap.json +0 -28
  56. package/dist/stream.js +0 -47
  57. package/how-we-work.md +0 -25
  58. package/skills/ask-matt/LICENSE +0 -21
  59. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  60. package/skills/ask-matt/SKILL.md +0 -91
  61. package/skills/ask-matt/agents/openai.yaml +0 -4
  62. package/skills/code-review/LICENSE +0 -21
  63. package/skills/code-review/SKILL.md +0 -89
  64. package/skills/code-review/agents/openai.yaml +0 -3
  65. package/skills/codebase-design/DEEPENING.md +0 -37
  66. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  67. package/skills/codebase-design/LICENSE +0 -21
  68. package/skills/codebase-design/SKILL.md +0 -116
  69. package/skills/codebase-design/agents/openai.yaml +0 -3
  70. package/skills/diagnosing-bugs/LICENSE +0 -21
  71. package/skills/diagnosing-bugs/SKILL.md +0 -140
  72. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  73. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  74. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  75. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  76. package/skills/domain-modeling/LICENSE +0 -21
  77. package/skills/domain-modeling/SKILL.md +0 -76
  78. package/skills/domain-modeling/agents/openai.yaml +0 -3
  79. package/skills/grill-me/LICENSE +0 -21
  80. package/skills/grill-me/SKILL.md +0 -8
  81. package/skills/grill-me/agents/openai.yaml +0 -4
  82. package/skills/grill-with-docs/LICENSE +0 -21
  83. package/skills/grill-with-docs/SKILL.md +0 -8
  84. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  85. package/skills/grilling/LICENSE +0 -21
  86. package/skills/grilling/SKILL.md +0 -30
  87. package/skills/grilling/agents/openai.yaml +0 -3
  88. package/skills/handoff/LICENSE +0 -21
  89. package/skills/handoff/SKILL.md +0 -17
  90. package/skills/handoff/agents/openai.yaml +0 -4
  91. package/skills/implement/LICENSE +0 -21
  92. package/skills/implement/SKILL.md +0 -16
  93. package/skills/implement/agents/openai.yaml +0 -4
  94. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  95. package/skills/improve-codebase-architecture/LICENSE +0 -21
  96. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  97. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  98. package/skills/prototype/LICENSE +0 -21
  99. package/skills/prototype/LOGIC.md +0 -67
  100. package/skills/prototype/SKILL.md +0 -28
  101. package/skills/prototype/UI.md +0 -112
  102. package/skills/prototype/agents/openai.yaml +0 -3
  103. package/skills/research/LICENSE +0 -21
  104. package/skills/research/SKILL.md +0 -14
  105. package/skills/research/agents/openai.yaml +0 -3
  106. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  107. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  108. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  109. package/skills/tdd/LICENSE +0 -21
  110. package/skills/tdd/SKILL.md +0 -40
  111. package/skills/tdd/agents/openai.yaml +0 -3
  112. package/skills/tdd/mocking.md +0 -59
  113. package/skills/tdd/tests.md +0 -77
  114. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  115. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  116. package/skills/teach/LICENSE +0 -21
  117. package/skills/teach/MISSION-FORMAT.md +0 -31
  118. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  119. package/skills/teach/SKILL.md +0 -141
  120. package/skills/teach/agents/openai.yaml +0 -4
  121. package/skills/to-questionnaire/LICENSE +0 -21
  122. package/skills/to-questionnaire/SKILL.md +0 -55
  123. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  124. package/skills/to-spec/LICENSE +0 -21
  125. package/skills/to-spec/SKILL.md +0 -76
  126. package/skills/to-spec/agents/openai.yaml +0 -4
  127. package/skills/to-tickets/LICENSE +0 -21
  128. package/skills/to-tickets/SKILL.md +0 -106
  129. package/skills/to-tickets/agents/openai.yaml +0 -4
  130. package/skills/wait-what/LICENSE +0 -21
  131. package/skills/wait-what/SKILL.md +0 -8
  132. package/skills/wait-what/agents/openai.yaml +0 -4
  133. package/skills/wayfinder/LICENSE +0 -21
  134. package/skills/wayfinder/SKILL.md +0 -129
  135. package/skills/wayfinder/agents/openai.yaml +0 -4
  136. package/skills/writing-for-agents/LICENSE +0 -21
  137. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  138. package/skills/writing-for-agents/SKILL.md +0 -83
  139. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -1,65 +1,156 @@
1
1
  import { client, } from '@agentclientprotocol/sdk';
2
- import { randomUUID } from 'node:crypto';
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 { signedOut, 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';
14
21
  const KILLED = 'the conversation was killed';
22
+ const INTERRUPTED = 'the conversation was interrupted';
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
+ const UNSTEERED = "the Agent's CLI did not take the message into its running turn";
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 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', 'done', 'option', 'answer']);
15
66
  function causeOf(error) {
16
67
  return error instanceof Error ? error.message : String(error);
17
68
  }
69
+ function answeredWith(error) {
70
+ return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
71
+ }
72
+ function exitedWith(running, cause) {
73
+ if (running.killed)
74
+ return { failure: 'killed', cause: KILLED };
75
+ return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
76
+ }
18
77
  function answerMessage(request, response) {
19
78
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
20
79
  }
80
+ async function sentPart(url, bytes, headers) {
81
+ const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
82
+ await answer.arrayBuffer();
83
+ if (!answer.ok)
84
+ throw storeRefusal('upload', answer.status);
85
+ }
21
86
  function logged(error) {
22
87
  process.stderr.write(`kit: ${causeOf(error)}\n`);
23
88
  }
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
- });
89
+ function refusal(conversation, error) {
90
+ if (conversation.kills > 0)
91
+ return KILLED;
92
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
93
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
94
+ return causeOf(error);
95
+ }
96
+ function audioOf(result) {
97
+ if (result.isError === true)
98
+ return null;
99
+ const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
100
+ return file?.media_type?.startsWith('audio/') === true ? file : null;
32
101
  }
33
- function stop(bridge) {
34
- killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
102
+ function marker(bridge) {
103
+ return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
104
+ }
105
+ function steers(adapter) {
106
+ return adapter.initialized._meta?.steering?.supported === true;
35
107
  }
36
108
  class Turn {
37
109
  id = randomUUID();
38
- text = '';
39
- sent = [];
40
- unsentFrom = null;
41
- draftSequence = 0;
42
- planSequence = 0;
43
- entries = null;
110
+ parts;
44
111
  questions = 0;
45
112
  secrets = 0;
46
- prompt = null;
113
+ prompted = false;
114
+ steered = false;
115
+ taken = null;
47
116
  ended = false;
117
+ images = 0;
118
+ uploads = [];
119
+ constructor(emit) {
120
+ this.parts = new Parts(emit);
121
+ }
48
122
  }
49
123
  class Conversation {
50
124
  id;
51
125
  running = null;
126
+ session = null;
52
127
  turn = null;
53
128
  commands = null;
54
- jobs = new Set();
55
- queued = [];
56
- late = [];
129
+ options = null;
130
+ exitUntold = false;
131
+ context = null;
132
+ leftovers = new Set();
133
+ waiting = [];
134
+ done = false;
135
+ sending = null;
136
+ subagents = new Map();
137
+ held = [];
57
138
  opening = null;
139
+ bridge = null;
140
+ launch = null;
141
+ prompted = null;
58
142
  kills = 0;
59
143
  killed = new AbortController();
60
- open = 0;
144
+ interrupts = 0;
145
+ interrupted = new AbortController();
146
+ turns = new Set();
61
147
  queue = Promise.resolve();
62
148
  reports = Promise.resolve();
149
+ starting = null;
150
+ preparing = null;
151
+ told = null;
152
+ untold = false;
153
+ shown = null;
63
154
  constructor(id) {
64
155
  this.id = id;
65
156
  }
@@ -75,51 +166,82 @@ function asksSecret(request) {
75
166
  export class Conversations {
76
167
  kit;
77
168
  conversations = new Map();
78
- acks = new Map();
79
169
  questions = new Map();
80
- carrying = 0;
81
170
  reporting = 0;
171
+ watched = false;
82
172
  constructor(kit) {
83
173
  this.kit = kit;
174
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
175
+ setInterval(() => this.look(), 2_000);
84
176
  }
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();
177
+ call(call) {
178
+ void this.kit.calls.carry(call.input_id, () => {
179
+ const conversation = this.conversation(call.conversation_id);
180
+ if (call.kind === 'kill') {
181
+ conversation.kills++;
182
+ conversation.killed.abort(new Error(KILLED));
183
+ for (const waiting of conversation.waiting.splice(0))
184
+ waiting.settle({ refused: KILLED });
185
+ this.halt(conversation);
186
+ }
187
+ if (call.kind === 'interrupt') {
188
+ conversation.interrupts++;
189
+ conversation.interrupted.abort();
190
+ }
191
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
192
+ conversation.queue = handled.catch(() => undefined);
193
+ return handled.then(async ({ result }) => ({ result: await result }));
106
194
  });
107
195
  }
108
196
  idle() {
109
- if (this.carrying > 0 || this.reporting > 0)
197
+ if (this.reporting > 0)
110
198
  return false;
111
199
  for (const conversation of this.conversations.values()) {
112
200
  const turn = conversation.turn;
113
201
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
114
- if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
202
+ if (conversation.turns.size > (asking ? 1 : 0))
115
203
  return false;
116
204
  }
117
- return true;
205
+ const conversations = [...this.conversations.values()];
206
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
207
+ return false;
208
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
209
+ }
210
+ quiet() {
211
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
212
+ }
213
+ look() {
214
+ for (const conversation of this.conversations.values()) {
215
+ for (const scope of conversation.leftovers) {
216
+ if (!scope.empty())
217
+ continue;
218
+ scope.close();
219
+ conversation.leftovers.delete(scope);
220
+ }
221
+ }
222
+ this.kit.changed();
118
223
  }
119
224
  opened() {
225
+ this.watched = false;
226
+ for (const conversation of this.conversations.values()) {
227
+ if (conversation.running !== null) {
228
+ this.facts(conversation);
229
+ this.subagentsChanged(conversation);
230
+ }
231
+ else if (conversation.exitUntold)
232
+ this.process(conversation, false);
233
+ if (conversation.untold || conversation.told !== null)
234
+ this.retell(conversation);
235
+ }
236
+ }
237
+ watching(watched) {
238
+ const resend = watched && !this.watched;
239
+ this.watched = watched;
240
+ if (!resend)
241
+ return;
120
242
  for (const conversation of this.conversations.values()) {
121
243
  if (conversation.turn !== null)
122
- conversation.turn.unsentFrom = 0;
244
+ this.replay(conversation, conversation.turn);
123
245
  }
124
246
  }
125
247
  conversation(id) {
@@ -130,72 +252,320 @@ export class Conversations {
130
252
  }
131
253
  return conversation;
132
254
  }
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') {
255
+ async carry(conversation, call) {
256
+ if (call.kind === 'open')
257
+ return this.open(conversation, call);
258
+ if (call.kind === 'message')
259
+ return this.message(conversation, call);
260
+ if (call.kind === 'done') {
261
+ conversation.done = true;
262
+ this.next(conversation);
263
+ return { result: Promise.resolve({}) };
264
+ }
265
+ await this.launched(conversation);
266
+ if (call.kind === 'interrupt') {
267
+ if (--conversation.interrupts === 0)
268
+ conversation.interrupted = new AbortController();
269
+ await this.interrupt(conversation, String(call.turn_id));
270
+ }
271
+ if (call.kind === 'kill') {
141
272
  await this.kill(conversation);
273
+ await conversation.sending;
142
274
  if (--conversation.kills === 0)
143
275
  conversation.killed = new AbortController();
144
276
  }
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));
277
+ if (call.kind === 'option')
278
+ await this.option(conversation, String(call.option), call.value);
279
+ if (call.kind === 'answer') {
280
+ const question = this.questions.get(String(call.interaction_id));
149
281
  if (question === undefined) {
150
- return this.message(conversation, { ...input, text: answerMessage(input.request, input.response), files: [], first: false });
282
+ const text = answerMessage(call.request, call.response);
283
+ return this.message(conversation, { ...call, text, first: false, message_id: null });
151
284
  }
152
- question.answer(input.response);
285
+ question.answer(call.response);
153
286
  }
154
- return {};
287
+ return { result: Promise.resolve({}) };
288
+ }
289
+ async open(conversation, call) {
290
+ await this.stop(conversation);
291
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
292
+ return {
293
+ result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
294
+ };
295
+ }
296
+ async stop(conversation) {
297
+ const running = conversation.running;
298
+ running?.scope.kill();
299
+ await running?.adapter.exited;
300
+ }
301
+ launch(conversation, route, sessionId, empty) {
302
+ let opened;
303
+ let failed;
304
+ const bridge = new Promise((resolve, reject) => {
305
+ opened = resolve;
306
+ failed = reject;
307
+ });
308
+ bridge.catch(() => undefined);
309
+ const session = this.start(conversation, route, sessionId, empty, opened);
310
+ session.catch(failed);
311
+ const launch = { bridge, session };
312
+ conversation.launch = launch;
313
+ void session
314
+ .catch(() => undefined)
315
+ .then(() => {
316
+ if (conversation.launch !== launch)
317
+ return;
318
+ conversation.launch = null;
319
+ this.closeIdleBridge(conversation);
320
+ this.next(conversation);
321
+ });
322
+ this.next(conversation);
323
+ return launch;
324
+ }
325
+ async launched(conversation) {
326
+ while (conversation.launch !== null)
327
+ await conversation.launch.session.catch(() => undefined);
328
+ }
329
+ instructed(conversation, route) {
330
+ const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
331
+ if (bridge === undefined)
332
+ return null;
333
+ const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
334
+ read.catch(() => undefined);
335
+ return read;
155
336
  }
156
- async open(conversation, input) {
157
- if (conversation.running !== null)
158
- return {};
337
+ async message(conversation, call) {
338
+ const content = [
339
+ ...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
340
+ { type: 'text', text: String(call.text) },
341
+ ];
342
+ let route;
343
+ let instructing = null;
159
344
  try {
160
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
345
+ route = await this.route(conversation, call.agent_id);
346
+ if (call.first === true)
347
+ instructing = this.instructed(conversation, route);
348
+ if (call.message_id !== null)
349
+ content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
350
+ if (conversation.kills > 0)
351
+ throw new Error(KILLED);
161
352
  }
162
353
  catch (error) {
163
- return { refused: causeOf(error) };
354
+ this.closeIdleBridge(conversation);
355
+ this.prepares(conversation, null);
356
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
357
+ }
358
+ conversation.preparing = null;
359
+ const { promise, resolve: settle } = Promise.withResolvers();
360
+ const waiting = { call, route, content, instructing, settle };
361
+ if (call.at_once === true && conversation.turns.size > 0)
362
+ await this.atOnce(conversation, waiting);
363
+ else
364
+ conversation.waiting.push(waiting);
365
+ this.next(conversation);
366
+ return { result: promise };
367
+ }
368
+ async atOnce(conversation, waiting) {
369
+ while (conversation.sending !== null)
370
+ await conversation.sending;
371
+ const running = conversation.running;
372
+ if (running !== null && conversation.turns.size > 0 && steers(running.adapter)) {
373
+ conversation.sending = this.steer(conversation, running, waiting);
374
+ await conversation.sending;
375
+ conversation.sending = null;
376
+ return;
164
377
  }
378
+ this.ahead(conversation, waiting);
379
+ if (conversation.turns.size > 0)
380
+ await this.cancel(conversation);
381
+ }
382
+ ahead(conversation, waiting) {
383
+ const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
384
+ conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
165
385
  }
166
- async message(conversation, input) {
167
- let ack = {};
386
+ async steer(conversation, running, waiting) {
387
+ let outcome;
388
+ try {
389
+ ({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
390
+ sessionId: running.sessionId,
391
+ prompt: waiting.content,
392
+ _meta: { steering: { idleBehavior: 'promptRequired' } },
393
+ }));
394
+ }
395
+ catch (error) {
396
+ outcome = error;
397
+ }
398
+ if (outcome === 'promptRequired') {
399
+ this.ahead(conversation, waiting);
400
+ }
401
+ else if (outcome === 'injected' || outcome === 'startedNewTurn') {
402
+ if (outcome === 'injected') {
403
+ for (const turn of conversation.turns)
404
+ turn.steered ||= turn.prompted;
405
+ }
406
+ this.kit.calls.keepResult(String(waiting.call.input_id), {});
407
+ waiting.settle({});
408
+ }
409
+ else {
410
+ const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
411
+ waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
412
+ }
413
+ }
414
+ next(conversation) {
415
+ if (conversation.sending !== null || conversation.turns.size > 0)
416
+ return;
417
+ const waiting = conversation.waiting.shift();
418
+ if (waiting === undefined) {
419
+ const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
420
+ if (conversation.starting !== step)
421
+ this.starts(conversation, step);
422
+ if (conversation.done && conversation.launch === null)
423
+ void this.stop(conversation).catch(logged);
424
+ return;
425
+ }
426
+ conversation.sending = this.send(conversation, waiting).finally(() => {
427
+ conversation.sending = null;
428
+ this.next(conversation);
429
+ });
430
+ }
431
+ async send(conversation, waiting) {
432
+ const { call } = waiting;
433
+ let result = {};
434
+ let started = false;
435
+ let instructing = waiting.instructing;
168
436
  const prompt = [];
169
437
  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
438
  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 };
439
+ this.starts(conversation, 'session');
440
+ await this.launched(conversation);
441
+ }
442
+ if (conversation.running === null) {
443
+ const stored = conversation.session ?? call.provider_session_id;
444
+ const empty = call.first === true && conversation.prompted !== stored;
445
+ const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
446
+ if (call.first === true)
447
+ instructing = this.instructed(conversation, waiting.route);
448
+ started = (await launch.session) !== stored;
449
+ }
450
+ const session = conversation.running.sessionId;
451
+ if (started || (call.first === true && session !== call.provider_session_id)) {
452
+ result = { provider_session_id: session };
176
453
  }
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
- });
454
+ if (call.first === true) {
455
+ prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
182
456
  }
183
- prompt.push({ type: 'text', text: String(input.text) });
184
- if (paths.length > 0)
185
- prompt.push({ type: 'text', text: paths.join('\n') });
186
457
  if (conversation.kills > 0)
187
458
  throw new Error(KILLED);
188
459
  }
189
460
  catch (error) {
190
- if ('provider_session_id' in ack)
461
+ this.starts(conversation, null);
462
+ if (started) {
463
+ conversation.session = null;
191
464
  await this.kill(conversation);
192
- return { refused: conversation.kills > 0 ? KILLED : causeOf(error) };
465
+ }
466
+ const refused = refusal(conversation, error);
467
+ const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
468
+ waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
469
+ return;
470
+ }
471
+ this.starts(conversation, null);
472
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
473
+ waiting.settle(result);
474
+ }
475
+ starts(conversation, step) {
476
+ conversation.starting = step;
477
+ if (conversation.shown !== null)
478
+ clearTimeout(conversation.shown);
479
+ conversation.shown = null;
480
+ if (step === null || conversation.told !== null) {
481
+ this.tell(conversation, step);
482
+ return;
483
+ }
484
+ conversation.shown = setTimeout(() => {
485
+ conversation.shown = null;
486
+ if (conversation.starting === step)
487
+ this.tell(conversation, step);
488
+ }, STARTING_SHOWN_AFTER_MS);
489
+ }
490
+ prepares(conversation, step) {
491
+ conversation.preparing = step;
492
+ this.next(conversation);
493
+ }
494
+ tell(conversation, step) {
495
+ if (conversation.told === step && !conversation.untold)
496
+ return;
497
+ conversation.told = step;
498
+ this.retell(conversation);
499
+ }
500
+ retell(conversation) {
501
+ conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
502
+ }
503
+ async transcribed(conversation, messageId, text) {
504
+ const paths = linkedPaths(text);
505
+ if (paths.length === 0)
506
+ return [];
507
+ const killed = conversation.killed.signal;
508
+ const interrupted = conversation.interrupted.signal;
509
+ const transcribing = AbortSignal.any([killed, interrupted]);
510
+ let signal = killed;
511
+ const blocks = [];
512
+ try {
513
+ const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
514
+ const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
515
+ for (const path of paths) {
516
+ let answered = await read(path);
517
+ if (answered === null)
518
+ continue;
519
+ signal = transcribing;
520
+ this.prepares(conversation, 'transcription');
521
+ const said = await transcribe(async () => {
522
+ const address = answered ?? (await read(path));
523
+ answered = null;
524
+ if (address === null)
525
+ throw new Error(`${path} is no longer an audio file`);
526
+ return address;
527
+ }, signal);
528
+ const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
529
+ await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
530
+ blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
531
+ }
532
+ }
533
+ catch (error) {
534
+ if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
535
+ throw new NotStarted(INTERRUPTED, { cause: error });
536
+ throw error;
537
+ }
538
+ return blocks;
539
+ }
540
+ bridged(conversation) {
541
+ if (conversation.bridge === null) {
542
+ const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
543
+ conversation.bridge = bridging;
544
+ bridging.catch(() => {
545
+ if (conversation.bridge === bridging)
546
+ conversation.bridge = null;
547
+ });
193
548
  }
194
- this.prompt(conversation, prompt);
195
- return ack;
549
+ return conversation.bridge;
550
+ }
551
+ closeBridge(conversation, bridging, bridge) {
552
+ bridge.close();
553
+ if (conversation.bridge === bridging)
554
+ conversation.bridge = null;
555
+ }
556
+ closeIdleBridge(conversation) {
557
+ const bridging = conversation.bridge;
558
+ if (bridging === null || conversation.running !== null || conversation.launch !== null)
559
+ return;
560
+ conversation.bridge = null;
561
+ void bridging.then((bridge) => bridge.close(), () => undefined);
196
562
  }
197
563
  async interrupt(conversation, turnId) {
198
- if (conversation.running === null || conversation.turn?.id !== turnId)
564
+ if ([...conversation.turns].some((turn) => turn.id === turnId))
565
+ await this.cancel(conversation);
566
+ }
567
+ async cancel(conversation) {
568
+ if (conversation.running === null)
199
569
  return;
200
570
  await conversation.running.adapter.connection.agent.notify('session/cancel', {
201
571
  sessionId: conversation.running.sessionId,
@@ -203,11 +573,13 @@ export class Conversations {
203
573
  }
204
574
  halt(conversation) {
205
575
  if (conversation.opening !== null)
206
- stop(conversation.opening);
576
+ killMarked(marker(conversation.opening));
577
+ for (const scope of conversation.leftovers)
578
+ scope.kill();
207
579
  if (conversation.running === null)
208
580
  return;
209
581
  conversation.running.killed = true;
210
- stop(conversation.running.bridge);
582
+ conversation.running.scope.kill();
211
583
  }
212
584
  async kill(conversation) {
213
585
  const running = conversation.running;
@@ -219,6 +591,7 @@ export class Conversations {
219
591
  if (running === null)
220
592
  return;
221
593
  try {
594
+ running.reader.requested(option, value);
222
595
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
223
596
  sessionId: running.sessionId,
224
597
  configId: option,
@@ -227,73 +600,99 @@ export class Conversations {
227
600
  this.options(conversation, answered.configOptions);
228
601
  }
229
602
  catch (error) {
603
+ running.reader.refused(option);
230
604
  logged(error);
231
605
  }
232
606
  }
233
607
  async route(conversation, agentId) {
234
608
  const signal = conversation.killed.signal;
235
- await unlessKilled(this.kit.agents.read(), signal);
609
+ await unlessAborted(this.kit.agents.read(), signal);
236
610
  if (this.kit.agents.route(agentId) === undefined)
237
- await unlessKilled(this.kit.agents.refresh(), signal);
611
+ await unlessAborted(this.kit.agents.refresh(), signal);
238
612
  const route = this.kit.agents.route(agentId);
239
613
  if (route === undefined)
240
614
  throw new Error(`this Environment hosts no Agent ${agentId}`);
241
- if (route.working_directory === join(LAUNCH_BASE, route.agent_id)) {
615
+ if (route.working_directory === join(agentBase(), route.agent_id)) {
242
616
  await mkdir(route.working_directory, { recursive: true });
243
617
  }
244
618
  return route;
245
619
  }
246
- async start(conversation, route, sessionId) {
620
+ async start(conversation, routed, sessionId, empty, bridged) {
621
+ const route = await routed;
622
+ const reader = readerFor(route.kind);
623
+ reader.session = sessionId;
247
624
  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
625
  .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);
626
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
627
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
628
+ const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
629
+ const bridging = this.bridged(conversation);
630
+ const bridge = await step(unbridged, bridging);
255
631
  if (conversation.kills > 0) {
256
- bridge.close();
632
+ this.closeBridge(conversation, bridging, bridge);
257
633
  throw new Error(KILLED);
258
634
  }
259
635
  conversation.opening = bridge;
636
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
637
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
638
+ let adapter = null;
260
639
  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());
640
+ bridged(bridge);
641
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
642
+ const streamed = await step(NO_CONNECTION, work.open(cli));
643
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
644
+ work.read(message);
645
+ this.read(conversation, reader, message);
646
+ }, { ...bridge.env, ...streamed }, undefined, scope));
647
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
264
648
  const agent = adapter.connection.agent;
265
649
  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 ?? []);
650
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
651
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
652
+ const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
653
+ reader.session = opened.sessionId;
654
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
655
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
656
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
657
+ }
658
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
659
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
660
+ void work.settled().then(() => scope.record());
277
661
  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));
662
+ adapter.turn(conversation.turns.size > 0);
663
+ conversation.session = running.sessionId;
664
+ conversation.options = options;
665
+ this.facts(conversation);
666
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
283
667
  return running.sessionId;
284
668
  }
285
669
  catch (error) {
286
- stop(bridge);
287
- bridge.close();
670
+ adapter?.retire();
671
+ work.close();
672
+ conversation.leftovers.add(scope);
673
+ this.closeBridge(conversation, bridging, bridge);
288
674
  throw conversation.kills > 0 ? new Error(KILLED) : error;
289
675
  }
290
676
  finally {
291
677
  conversation.opening = null;
292
678
  }
293
679
  }
680
+ async resume(conversation, kind, adapter, request, empty) {
681
+ try {
682
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
683
+ }
684
+ catch (error) {
685
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
686
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
687
+ }
688
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
689
+ return null;
690
+ }
691
+ }
294
692
  async launchSettings(running, route, offered) {
295
693
  let options = offered;
296
694
  const settings = [
695
+ ['mode', route.mode ?? CLIS[route.kind].fullAccess],
297
696
  ['model', route.model],
298
697
  ['thought_level', route.effort],
299
698
  ];
@@ -301,6 +700,7 @@ export class Conversations {
301
700
  if (value === null)
302
701
  continue;
303
702
  const option = options.find((candidate) => candidate.category === category);
703
+ running.reader.requested(option?.id ?? category, value);
304
704
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
305
705
  sessionId: running.sessionId,
306
706
  configId: option?.id ?? category,
@@ -310,23 +710,45 @@ export class Conversations {
310
710
  }
311
711
  return options;
312
712
  }
313
- exited(conversation, adapter, cause) {
314
- const running = conversation.running;
315
- if (running?.adapter !== adapter)
316
- return;
713
+ exited(conversation, running, cause) {
714
+ this.detach(conversation, running);
715
+ if (conversation.turn !== null) {
716
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
717
+ }
718
+ this.next(conversation);
719
+ this.kit.changed();
720
+ }
721
+ detach(conversation, running) {
722
+ if (conversation.bridge === running.bridging)
723
+ conversation.bridge = null;
724
+ running.work.close();
725
+ conversation.leftovers.add(running.scope);
317
726
  conversation.running = null;
318
727
  conversation.commands = null;
319
- conversation.jobs.clear();
320
- conversation.queued.length = 0;
728
+ conversation.options = null;
729
+ if (conversation.subagents.size > 0) {
730
+ conversation.subagents.clear();
731
+ this.subagentsChanged(conversation);
732
+ }
321
733
  for (const [id, question] of this.questions) {
322
734
  if (question.conversation === conversation)
323
735
  this.questions.delete(id);
324
736
  }
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();
737
+ this.process(conversation, false);
738
+ }
739
+ process(conversation, running) {
740
+ conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
741
+ }
742
+ facts(conversation) {
743
+ this.process(conversation, true);
744
+ if (conversation.commands !== null)
745
+ this.commands(conversation, conversation.commands);
746
+ if (conversation.options !== null)
747
+ this.options(conversation, conversation.options);
748
+ }
749
+ subagentsChanged(conversation) {
750
+ const subagents = [...conversation.subagents.values()].map(({ description, activity }) => ({ description, activity }));
751
+ this.kit.send({ type: 'subagents', conversation_id: conversation.id, subagents });
330
752
  }
331
753
  commands(conversation, commands) {
332
754
  conversation.commands = commands;
@@ -337,11 +759,12 @@ export class Conversations {
337
759
  options(conversation, options) {
338
760
  if (conversation.running === null)
339
761
  return;
762
+ conversation.options = options;
340
763
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
341
764
  }
342
765
  deliver(conversation, turn, event, body) {
343
766
  this.reporting++;
344
- const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
767
+ const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
345
768
  conversation.reports = delivered
346
769
  .catch(() => undefined)
347
770
  .finally(() => {
@@ -350,108 +773,103 @@ export class Conversations {
350
773
  });
351
774
  return delivered;
352
775
  }
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;
776
+ write(conversation, turn, operation) {
777
+ if (turn.ended || !this.watched)
778
+ return;
779
+ this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
360
780
  }
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);
781
+ replay(conversation, turn) {
782
+ for (const operation of turn.parts.operations())
783
+ this.write(conversation, turn, operation);
368
784
  }
369
785
  begin(conversation) {
370
- const turn = this.report(conversation);
786
+ const turn = new Turn((operation) => this.write(conversation, turn, operation));
787
+ conversation.turns.add(turn);
788
+ conversation.running?.adapter.turn(true);
789
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
371
790
  conversation.turn = turn;
791
+ if (conversation.context !== null)
792
+ turn.parts.measure(conversation.context);
793
+ for (const part of conversation.held.splice(0))
794
+ turn.parts.start(part);
372
795
  this.kit.changed();
373
796
  return turn;
374
797
  }
375
- end(conversation, turn, outcome) {
798
+ end(conversation, turn, failed, notice = null) {
376
799
  if (turn.ended)
377
800
  return;
801
+ if (failed !== null)
802
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
803
+ if (conversation.turn === turn) {
804
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
805
+ this.apply(conversation, turn, event);
806
+ }
807
+ if (notice !== null)
808
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
378
809
  turn.ended = true;
379
- conversation.open--;
810
+ conversation.turns.delete(turn);
811
+ conversation.running?.adapter.turn(conversation.turns.size > 0);
380
812
  if (conversation.turn === turn)
381
813
  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);
814
+ const body = Promise.allSettled(turn.uploads).then(() => ({
815
+ parts: turn.parts.saved(),
816
+ context: turn.parts.context,
817
+ ...(failed === null ? {} : { failed: failed.failure }),
818
+ }));
819
+ void this.deliver(conversation, turn, 'ended', body).catch(logged);
820
+ this.next(conversation);
386
821
  this.kit.changed();
387
822
  }
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) {
823
+ prompt(conversation, prompt, taken) {
395
824
  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 });
825
+ const turn = this.begin(conversation);
826
+ turn.prompted = true;
827
+ turn.taken = taken;
828
+ conversation.prompted = running.sessionId;
829
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
830
+ this.took(turn);
831
+ const notice = answeredFailure(answer);
832
+ if (notice !== null) {
833
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
834
+ }
835
+ else {
836
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
837
+ }
838
+ }, async (error) => {
839
+ if (running.adapter.connection.signal.aborted) {
840
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
841
+ }
842
+ else {
843
+ this.took(turn);
844
+ if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
845
+ running.adapter.retire();
846
+ this.detach(conversation, running);
847
+ }
848
+ this.end(conversation, turn, answeredWith(error));
408
849
  }
409
850
  });
410
851
  }
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
852
  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);
853
+ const turn = conversation.turn ?? this.begin(conversation);
854
+ this.took(turn);
442
855
  return turn;
443
856
  }
444
- ended(conversation) {
857
+ ended(conversation, stop) {
445
858
  const turn = conversation.turn;
446
859
  if (turn === null)
447
860
  return;
448
- if (turn.prompt === null) {
449
- this.end(conversation, turn, { text: turn.text });
450
- }
451
- else {
452
- conversation.turn = null;
453
- this.kit.changed();
861
+ const held = stop !== null && !turn.steered && [...conversation.subagents.values()].some((subagent) => subagent.turn === turn);
862
+ if (!turn.prompted || held) {
863
+ this.took(turn);
864
+ this.end(conversation, turn, stop === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
865
+ return;
454
866
  }
867
+ if (stop !== null)
868
+ return;
869
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
870
+ this.apply(conversation, turn, event);
871
+ conversation.turn = null;
872
+ this.kit.changed();
455
873
  }
456
874
  async ask(conversation, method, params, secret, signal) {
457
875
  const turn = this.started(conversation);
@@ -497,65 +915,126 @@ export class Conversations {
497
915
  if (content !== null)
498
916
  this.questions.get(interactionId)?.answer({ action: 'accept', content });
499
917
  }
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);
918
+ read(conversation, reader, message) {
919
+ for (const event of reader.read(message))
920
+ this.event(conversation, event);
921
+ }
922
+ event(conversation, event) {
923
+ switch (event.kind) {
924
+ case 'started':
925
+ this.started(conversation);
926
+ return;
927
+ case 'ended':
928
+ this.ended(conversation, event.stop);
929
+ return;
930
+ case 'commands':
931
+ this.commands(conversation, event.commands);
932
+ return;
933
+ case 'options':
934
+ this.options(conversation, event.options);
935
+ return;
936
+ case 'context':
937
+ conversation.context = { used: event.used, window: event.window };
938
+ conversation.turn?.parts.measure(conversation.context);
939
+ return;
940
+ case 'subagent-done':
941
+ this.subagentDone(conversation, event.id, event.result);
942
+ return;
943
+ case 'subagent-activity':
944
+ this.subagentActivity(conversation, event.id, event.activity);
945
+ return;
946
+ case 'marker':
947
+ case 'image':
948
+ if (conversation.turn !== null)
949
+ break;
950
+ if (event.kind === 'marker' && event.marker === 'job')
951
+ conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
952
+ return;
517
953
  }
518
- else if (update.sessionUpdate === 'config_option_update') {
519
- this.options(conversation, update.configOptions);
954
+ this.apply(conversation, this.started(conversation), event);
955
+ }
956
+ took(turn) {
957
+ const taken = turn.taken;
958
+ turn.taken = null;
959
+ taken?.();
960
+ }
961
+ apply(conversation, turn, event) {
962
+ switch (event.kind) {
963
+ case 'text':
964
+ case 'reasoning':
965
+ turn.parts.write(event.kind, event.id, event.text);
966
+ return;
967
+ case 'command':
968
+ turn.parts.command();
969
+ return;
970
+ case 'subagent': {
971
+ const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
972
+ conversation.subagents.set(event.id, { description: event.description, turn, index, activity: null });
973
+ this.subagentsChanged(conversation);
974
+ return;
975
+ }
976
+ case 'message':
977
+ turn.parts.start({ type: 'message', command: event.command, target: event.target });
978
+ return;
979
+ case 'marker':
980
+ turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
981
+ return;
982
+ case 'plan':
983
+ turn.parts.plan(event.entries);
984
+ return;
985
+ case 'image':
986
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
987
+ this.kit.changed();
988
+ return;
520
989
  }
521
990
  }
522
- job(conversation, job) {
523
- if (job.running)
524
- conversation.jobs.add(job.id);
991
+ subagentDone(conversation, id, result) {
992
+ const subagent = conversation.subagents.get(id);
993
+ if (subagent === undefined)
994
+ return;
995
+ conversation.subagents.delete(id);
996
+ this.subagentsChanged(conversation);
997
+ if (subagent.turn === conversation.turn) {
998
+ subagent.turn.parts.done(subagent.index, result);
999
+ return;
1000
+ }
1001
+ const part = { type: 'subagent', description: subagent.description, status: 'done', result };
1002
+ if (conversation.turn === null)
1003
+ conversation.held.push(part);
525
1004
  else
526
- conversation.jobs.delete(job.id);
527
- this.kit.changed();
1005
+ conversation.turn.parts.start(part);
528
1006
  }
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;
1007
+ subagentActivity(conversation, id, activity) {
1008
+ const subagent = conversation.subagents.get(id);
1009
+ if (subagent === undefined || subagent.activity === activity)
1010
+ return;
1011
+ subagent.activity = activity;
1012
+ this.subagentsChanged(conversation);
1013
+ }
1014
+ async image(conversation, turn, reserved, data, mediaType) {
1015
+ const bytes = Buffer.from(data, 'base64');
1016
+ const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
1017
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
1018
+ try {
1019
+ const declared = (await this.deliver(conversation, turn, 'images', {
1020
+ name,
1021
+ media_type: mediaType,
1022
+ bytes: bytes.length,
1023
+ sha256,
1024
+ }));
1025
+ const saved = await uploaded(declared.upload, {
1026
+ name,
1027
+ bytes: bytes.length,
1028
+ sha256,
1029
+ read: (start, end) => [bytes.subarray(start, end)],
1030
+ send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
1031
+ finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
1032
+ });
1033
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
544
1034
  }
545
- else {
546
- turn.unsentFrom = from;
1035
+ catch (error) {
1036
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
1037
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
547
1038
  }
548
1039
  }
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
1040
  }