@agentshouse/kit 0.2.1-alpha.8 → 0.2.1-c05cd91c26b1d

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 (150) hide show
  1. package/README.md +46 -21
  2. package/dist/abort.js +9 -0
  3. package/dist/acp.js +48 -85
  4. package/dist/agents.js +312 -53
  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 +92 -77
  9. package/dist/chatgpt.js +32 -0
  10. package/dist/clis-main.js +23 -0
  11. package/dist/clis.js +213 -71
  12. package/dist/codex-main.js +58 -0
  13. package/dist/configure-main.js +6 -0
  14. package/dist/conversations.js +429 -232
  15. package/dist/copies.js +233 -171
  16. package/dist/end.js +23 -0
  17. package/dist/enrol-main.js +7 -1
  18. package/dist/events.js +525 -0
  19. package/dist/files.js +24 -3
  20. package/dist/git-main.js +2 -2
  21. package/dist/git.js +15 -9
  22. package/dist/home.js +141 -10
  23. package/dist/house-cli.js +291 -0
  24. package/dist/house-main.js +5 -102
  25. package/dist/inputs.js +37 -0
  26. package/dist/instructions.js +10 -31
  27. package/dist/kit-cli.js +68 -0
  28. package/dist/kit-main.js +8 -16
  29. package/dist/login.js +129 -94
  30. package/dist/logout.js +19 -0
  31. package/dist/mcp.js +39 -0
  32. package/dist/operation.js +7 -4
  33. package/dist/parts.js +82 -0
  34. package/dist/placement.js +30 -0
  35. package/dist/refusals.js +53 -0
  36. package/dist/relay.js +20 -0
  37. package/dist/resident.js +123 -40
  38. package/dist/scope.js +203 -0
  39. package/dist/secret-input.js +7 -2
  40. package/dist/service-main.js +9 -0
  41. package/dist/service.js +33 -0
  42. package/dist/shell.js +3 -0
  43. package/dist/sign-in.js +47 -28
  44. package/dist/skills.js +147 -0
  45. package/dist/stream.js +89 -13
  46. package/dist/transcription.js +170 -0
  47. package/dist/update.js +138 -0
  48. package/dist/wire.js +6 -0
  49. package/dist/work.js +346 -0
  50. package/how-we-work.md +33 -0
  51. package/package.json +8 -8
  52. package/skills/ask-matt/LICENSE +21 -0
  53. package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
  54. package/skills/ask-matt/SKILL.md +97 -0
  55. package/skills/ask-matt/agents/openai.yaml +5 -0
  56. package/skills/code-review/LICENSE +21 -0
  57. package/skills/code-review/SKILL.md +89 -0
  58. package/skills/code-review/agents/openai.yaml +3 -0
  59. package/skills/codebase-design/DEEPENING.md +37 -0
  60. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  61. package/skills/codebase-design/LICENSE +21 -0
  62. package/skills/codebase-design/SKILL.md +114 -0
  63. package/skills/codebase-design/agents/openai.yaml +3 -0
  64. package/skills/diagnosing-bugs/LICENSE +21 -0
  65. package/skills/diagnosing-bugs/SKILL.md +138 -0
  66. package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  67. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  68. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  69. package/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
  70. package/skills/domain-modeling/LICENSE +21 -0
  71. package/skills/domain-modeling/SKILL.md +74 -0
  72. package/skills/domain-modeling/agents/openai.yaml +3 -0
  73. package/skills/grill-me/LICENSE +21 -0
  74. package/skills/grill-me/SKILL.md +7 -0
  75. package/skills/grill-me/agents/openai.yaml +5 -0
  76. package/skills/grill-with-docs/LICENSE +21 -0
  77. package/skills/grill-with-docs/SKILL.md +7 -0
  78. package/skills/grill-with-docs/agents/openai.yaml +5 -0
  79. package/skills/grilling/LICENSE +21 -0
  80. package/skills/grilling/SKILL.md +30 -0
  81. package/skills/grilling/agents/openai.yaml +3 -0
  82. package/skills/handoff/LICENSE +21 -0
  83. package/skills/handoff/SKILL.md +16 -0
  84. package/skills/handoff/agents/openai.yaml +5 -0
  85. package/skills/implement/LICENSE +21 -0
  86. package/skills/implement/SKILL.md +17 -0
  87. package/skills/implement/agents/openai.yaml +5 -0
  88. package/skills/implement-spec/LICENSE +21 -0
  89. package/skills/implement-spec/SKILL.md +40 -0
  90. package/skills/implement-spec/agents/openai.yaml +5 -0
  91. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  92. package/skills/improve-codebase-architecture/LICENSE +21 -0
  93. package/skills/improve-codebase-architecture/SKILL.md +71 -0
  94. package/skills/improve-codebase-architecture/agents/openai.yaml +5 -0
  95. package/skills/pr/CREDITS.md +3 -0
  96. package/skills/pr/LICENSE +21 -0
  97. package/skills/pr/SKILL.md +170 -0
  98. package/skills/pr/agents/openai.yaml +3 -0
  99. package/skills/prototype/LICENSE +21 -0
  100. package/skills/prototype/LOGIC.md +67 -0
  101. package/skills/prototype/SKILL.md +26 -0
  102. package/skills/prototype/UI.md +112 -0
  103. package/skills/prototype/agents/openai.yaml +3 -0
  104. package/skills/research/LICENSE +21 -0
  105. package/skills/research/SKILL.md +12 -0
  106. package/skills/research/agents/openai.yaml +3 -0
  107. package/skills/retro/LICENSE +21 -0
  108. package/skills/retro/SKILL.md +44 -0
  109. package/skills/retro/agents/openai.yaml +5 -0
  110. package/skills/tdd/LICENSE +21 -0
  111. package/skills/tdd/SKILL.md +38 -0
  112. package/skills/tdd/agents/openai.yaml +3 -0
  113. package/skills/tdd/mocking.md +59 -0
  114. package/skills/tdd/tests.md +77 -0
  115. package/skills/teach/GLOSSARY-FORMAT.md +35 -0
  116. package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  117. package/skills/teach/LICENSE +21 -0
  118. package/skills/teach/MISSION-FORMAT.md +31 -0
  119. package/skills/teach/RESOURCES-FORMAT.md +32 -0
  120. package/skills/teach/SKILL.md +140 -0
  121. package/skills/teach/agents/openai.yaml +5 -0
  122. package/skills/to-questionnaire/LICENSE +21 -0
  123. package/skills/to-questionnaire/SKILL.md +54 -0
  124. package/skills/to-questionnaire/agents/openai.yaml +5 -0
  125. package/skills/to-spec/LICENSE +21 -0
  126. package/skills/to-spec/SKILL.md +75 -0
  127. package/skills/to-spec/agents/openai.yaml +5 -0
  128. package/skills/to-tickets/LICENSE +21 -0
  129. package/skills/to-tickets/SKILL.md +105 -0
  130. package/skills/to-tickets/agents/openai.yaml +5 -0
  131. package/skills/triage/AGENT-BRIEF.md +207 -0
  132. package/skills/triage/LICENSE +21 -0
  133. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  134. package/skills/triage/SKILL.md +112 -0
  135. package/skills/triage/agents/openai.yaml +5 -0
  136. package/skills/wait-what/LICENSE +21 -0
  137. package/skills/wait-what/SKILL.md +7 -0
  138. package/skills/wait-what/agents/openai.yaml +5 -0
  139. package/skills/wayfinder/LICENSE +21 -0
  140. package/skills/wayfinder/SKILL.md +128 -0
  141. package/skills/wayfinder/agents/openai.yaml +5 -0
  142. package/skills/wizard/LICENSE +21 -0
  143. package/skills/wizard/SKILL.md +44 -0
  144. package/skills/wizard/agents/openai.yaml +3 -0
  145. package/skills/wizard/template.sh +223 -0
  146. package/skills/writing-for-agents/LICENSE +21 -0
  147. package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  148. package/skills/writing-for-agents/SKILL.md +81 -0
  149. package/skills/writing-for-agents/agents/openai.yaml +3 -0
  150. package/dist/blocks.js +0 -51
@@ -1,61 +1,117 @@
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 } 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 { placeFiles, uploadBytes } 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 { killMarked, Scope } from './scope.js';
11
17
  import { holdSecretInput } from './secret-input.js';
12
- const LAUNCH_BASE = '/agents/house';
18
+ import { createHowWeWork } from './skills.js';
19
+ import { transcribe } from './transcription.js';
20
+ import { workFor } from './work.js';
21
+ const SIGNED_OUT = RequestError.authRequired().code;
13
22
  const KILLED = 'the conversation was killed';
23
+ const REFUSED = 'the CLI refused the prompt';
24
+ const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
25
+ const GONE = 'House no longer holds this conversation; start a new one';
26
+ const RETIRED = "this conversation's Agent is retired, so it cannot start";
27
+ const ENDED = {
28
+ conversation_not_found: GONE,
29
+ conversation_historical: RETIRED,
30
+ };
31
+ const NO_START = "the Agent's CLI did not start";
32
+ const NO_SESSION = "the Agent's CLI did not open a session";
33
+ const NO_RESUME = "the Agent's CLI could not resume this conversation";
34
+ const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
35
+ class NotStarted extends Error {
36
+ }
37
+ async function step(reason, work) {
38
+ try {
39
+ return await work;
40
+ }
41
+ catch (error) {
42
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
43
+ }
44
+ }
45
+ function unbridged(error) {
46
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
47
+ }
48
+ const EXIT_PLAN_MODE = {
49
+ parse(value) {
50
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
51
+ throw new Error('_x.ai/exit_plan_mode needs an object');
52
+ }
53
+ return value;
54
+ },
55
+ };
56
+ export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
14
57
  function causeOf(error) {
15
58
  return error instanceof Error ? error.message : String(error);
16
59
  }
60
+ function answeredWith(error) {
61
+ const signedOut = typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
62
+ return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
63
+ }
64
+ function exitedWith(running, cause) {
65
+ return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
66
+ }
17
67
  function answerMessage(request, response) {
18
68
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
19
69
  }
20
70
  function logged(error) {
21
71
  process.stderr.write(`kit: ${causeOf(error)}\n`);
22
72
  }
23
- function unlessKilled(work, signal) {
24
- return new Promise((resolve, reject) => {
25
- const killed = () => reject(signal.reason);
26
- signal.addEventListener('abort', killed, { once: true });
27
- if (signal.aborted)
28
- killed();
29
- work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
30
- });
73
+ function refusal(conversation, error) {
74
+ if (conversation.kills > 0)
75
+ return KILLED;
76
+ const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
77
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
78
+ return causeOf(error);
31
79
  }
32
- function stop(bridge) {
33
- killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
80
+ function marker(bridge) {
81
+ return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
34
82
  }
35
83
  class Turn {
36
84
  id = randomUUID();
37
- text = '';
38
- sent = [];
39
- unsentFrom = null;
40
- draftSequence = 0;
41
- planSequence = 0;
42
- entries = null;
85
+ parts;
86
+ admitted = false;
43
87
  questions = 0;
44
88
  secrets = 0;
45
- prompt = null;
89
+ prompted = false;
46
90
  ended = false;
91
+ images = 0;
92
+ uploads = [];
93
+ constructor(emit) {
94
+ this.parts = new Parts(emit);
95
+ }
47
96
  }
48
97
  class Conversation {
49
98
  id;
50
99
  running = null;
100
+ session = null;
51
101
  turn = null;
52
102
  commands = null;
53
- jobs = new Set();
54
- queued = [];
55
- late = [];
103
+ context = null;
104
+ leftovers = new Set();
105
+ waiting = [];
106
+ sending = null;
107
+ subagents = new Map();
108
+ held = [];
56
109
  opening = null;
110
+ prompted = null;
57
111
  kills = 0;
58
112
  killed = new AbortController();
113
+ interrupts = 0;
114
+ interrupted = new AbortController();
59
115
  open = 0;
60
116
  queue = Promise.resolve();
61
117
  reports = Promise.resolve();
@@ -74,51 +130,72 @@ function asksSecret(request) {
74
130
  export class Conversations {
75
131
  kit;
76
132
  conversations = new Map();
77
- acks = new Map();
78
133
  questions = new Map();
79
- carrying = 0;
80
134
  reporting = 0;
135
+ watched = false;
81
136
  constructor(kit) {
82
137
  this.kit = kit;
138
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
139
+ setInterval(() => this.look(), 2_000);
83
140
  }
84
141
  input(input) {
85
- this.carrying++;
86
- const carried = this.acks.get(input.input_id) ??
87
- (() => {
88
- const conversation = this.conversation(input.conversation_id);
89
- if (input.kind === 'kill') {
90
- conversation.kills++;
91
- conversation.killed.abort(new Error(KILLED));
92
- this.halt(conversation);
93
- }
94
- const work = conversation.queue.then(() => this.carry(conversation, input));
95
- conversation.queue = work.catch(() => undefined);
96
- this.acks.set(input.input_id, work);
97
- return work;
98
- })();
99
- carried
100
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
101
- .catch(logged)
102
- .finally(() => {
103
- this.carrying--;
104
- this.kit.changed();
142
+ void this.kit.inputs.carry(input.input_id, () => {
143
+ const conversation = this.conversation(input.conversation_id);
144
+ if (input.kind === 'kill') {
145
+ conversation.kills++;
146
+ conversation.killed.abort(new Error(KILLED));
147
+ for (const waiting of conversation.waiting.splice(0))
148
+ waiting.settle({ refused: KILLED });
149
+ this.halt(conversation);
150
+ }
151
+ if (input.kind === 'interrupt') {
152
+ conversation.interrupts++;
153
+ conversation.interrupted.abort();
154
+ }
155
+ const handled = conversation.queue.then(() => this.carry(conversation, input));
156
+ conversation.queue = handled.catch(() => undefined);
157
+ return handled.then(async ({ ack }) => ({ ack: await ack }));
105
158
  });
106
159
  }
107
160
  idle() {
108
- if (this.carrying > 0 || this.reporting > 0)
161
+ if (this.reporting > 0)
109
162
  return false;
110
163
  for (const conversation of this.conversations.values()) {
111
164
  const turn = conversation.turn;
112
165
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
113
- if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
166
+ if (conversation.open > (asking ? 1 : 0))
114
167
  return false;
115
168
  }
116
- return true;
169
+ const conversations = [...this.conversations.values()];
170
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
171
+ return false;
172
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
173
+ }
174
+ quiet() {
175
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
176
+ }
177
+ look() {
178
+ for (const conversation of this.conversations.values()) {
179
+ for (const scope of conversation.leftovers) {
180
+ if (!scope.empty())
181
+ continue;
182
+ scope.close();
183
+ conversation.leftovers.delete(scope);
184
+ }
185
+ }
186
+ this.kit.changed();
117
187
  }
118
188
  opened() {
189
+ this.watched = false;
190
+ }
191
+ watching(watched) {
192
+ const resend = watched && !this.watched;
193
+ this.watched = watched;
194
+ if (!resend)
195
+ return;
119
196
  for (const conversation of this.conversations.values()) {
120
197
  if (conversation.turn !== null)
121
- conversation.turn.unsentFrom = 0;
198
+ this.replay(conversation, conversation.turn);
122
199
  }
123
200
  }
124
201
  conversation(id) {
@@ -131,13 +208,17 @@ export class Conversations {
131
208
  }
132
209
  async carry(conversation, input) {
133
210
  if (input.kind === 'open')
134
- return this.open(conversation, input);
211
+ return { ack: Promise.resolve(await this.open(conversation, input)) };
135
212
  if (input.kind === 'message')
136
213
  return this.message(conversation, input);
137
- if (input.kind === 'interrupt')
214
+ if (input.kind === 'interrupt') {
215
+ if (--conversation.interrupts === 0)
216
+ conversation.interrupted = new AbortController();
138
217
  await this.interrupt(conversation, String(input.turn_id));
218
+ }
139
219
  if (input.kind === 'kill') {
140
220
  await this.kill(conversation);
221
+ await conversation.sending;
141
222
  if (--conversation.kills === 0)
142
223
  conversation.killed = new AbortController();
143
224
  }
@@ -146,52 +227,110 @@ export class Conversations {
146
227
  if (input.kind === 'answer') {
147
228
  const question = this.questions.get(String(input.interaction_id));
148
229
  if (question === undefined) {
149
- return this.message(conversation, { ...input, text: answerMessage(input.request, input.response), files: [], first: false });
230
+ const text = answerMessage(input.request, input.response);
231
+ return this.message(conversation, { ...input, text, files: [], first: false });
150
232
  }
151
233
  question.answer(input.response);
152
234
  }
153
- return {};
235
+ return { ack: Promise.resolve({}) };
154
236
  }
155
237
  async open(conversation, input) {
156
238
  if (conversation.running !== null)
157
239
  return {};
158
240
  try {
159
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
241
+ return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
160
242
  }
161
243
  catch (error) {
162
- return { refused: causeOf(error) };
244
+ return { refused: refusal(conversation, error) };
163
245
  }
164
246
  }
165
247
  async message(conversation, input) {
248
+ const content = [{ type: 'text', text: String(input.text) }];
249
+ let route;
250
+ try {
251
+ route = await this.route(conversation, input.agent_id);
252
+ const files = input.files;
253
+ const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
254
+ const lines = await this.transcribed(conversation, files, paths);
255
+ if (lines.length > 0)
256
+ content.push({ type: 'text', text: lines.join('\n') });
257
+ if (conversation.kills > 0)
258
+ throw new Error(KILLED);
259
+ }
260
+ catch (error) {
261
+ return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
262
+ }
263
+ return {
264
+ ack: new Promise((settle) => {
265
+ conversation.waiting.push({ input, route, content, settle });
266
+ this.next(conversation);
267
+ }),
268
+ };
269
+ }
270
+ next(conversation) {
271
+ if (conversation.sending !== null || conversation.open > 0)
272
+ return;
273
+ const waiting = conversation.waiting.shift();
274
+ if (waiting === undefined)
275
+ return;
276
+ conversation.sending = this.send(conversation, waiting).finally(() => {
277
+ conversation.sending = null;
278
+ this.next(conversation);
279
+ });
280
+ }
281
+ async send(conversation, waiting) {
282
+ const { input } = waiting;
166
283
  let ack = {};
284
+ let started = false;
167
285
  const prompt = [];
168
286
  try {
169
- const route = await this.route(conversation, input.agent_id);
170
- const paths = await placeFiles(this.kit.house, route.working_directory, input.files, conversation.killed.signal);
171
287
  if (conversation.running === null) {
172
- const opened = await this.start(conversation, route, input.provider_session_id);
173
- if (input.provider_session_id === null)
174
- ack = { provider_session_id: opened };
288
+ const stored = conversation.session ?? input.provider_session_id;
289
+ const empty = input.first === true && conversation.prompted !== stored;
290
+ started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
291
+ }
292
+ const session = conversation.running.sessionId;
293
+ if (started || (input.first === true && session !== input.provider_session_id)) {
294
+ ack = { provider_session_id: session };
175
295
  }
176
296
  if (input.first === true) {
177
297
  prompt.push({
178
298
  type: 'text',
179
- text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
299
+ text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
180
300
  });
181
301
  }
182
- prompt.push({ type: 'text', text: String(input.text) });
183
- if (paths.length > 0)
184
- prompt.push({ type: 'text', text: paths.join('\n') });
185
302
  if (conversation.kills > 0)
186
303
  throw new Error(KILLED);
187
304
  }
188
305
  catch (error) {
189
- if ('provider_session_id' in ack)
306
+ if (started) {
307
+ conversation.session = null;
190
308
  await this.kill(conversation);
191
- return { refused: conversation.kills > 0 ? KILLED : causeOf(error) };
309
+ }
310
+ waiting.settle({ refused: refusal(conversation, error) });
311
+ return;
192
312
  }
193
- this.prompt(conversation, prompt);
194
- return ack;
313
+ this.prompt(conversation, [...prompt, ...waiting.content]);
314
+ waiting.settle(ack);
315
+ }
316
+ async transcribed(conversation, files, paths) {
317
+ const lines = [];
318
+ for (const [index, path] of paths.entries()) {
319
+ lines.push(path);
320
+ if (files[index].media_type?.startsWith('audio/') !== true)
321
+ continue;
322
+ try {
323
+ const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
324
+ if (text !== null && text !== '')
325
+ lines.push(`Transcript: ${text}`);
326
+ }
327
+ catch (error) {
328
+ if (conversation.kills > 0)
329
+ throw error;
330
+ logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
331
+ }
332
+ }
333
+ return lines;
195
334
  }
196
335
  async interrupt(conversation, turnId) {
197
336
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -202,11 +341,13 @@ export class Conversations {
202
341
  }
203
342
  halt(conversation) {
204
343
  if (conversation.opening !== null)
205
- stop(conversation.opening);
344
+ killMarked(marker(conversation.opening));
345
+ for (const scope of conversation.leftovers)
346
+ scope.kill();
206
347
  if (conversation.running === null)
207
348
  return;
208
349
  conversation.running.killed = true;
209
- stop(conversation.running.bridge);
350
+ conversation.running.scope.kill();
210
351
  }
211
352
  async kill(conversation) {
212
353
  const running = conversation.running;
@@ -218,6 +359,7 @@ export class Conversations {
218
359
  if (running === null)
219
360
  return;
220
361
  try {
362
+ running.reader.requested(option, value);
221
363
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
222
364
  sessionId: running.sessionId,
223
365
  configId: option,
@@ -226,62 +368,75 @@ export class Conversations {
226
368
  this.options(conversation, answered.configOptions);
227
369
  }
228
370
  catch (error) {
371
+ running.reader.refused(option);
229
372
  logged(error);
230
373
  }
231
374
  }
232
375
  async route(conversation, agentId) {
233
376
  const signal = conversation.killed.signal;
234
- await unlessKilled(this.kit.agents.read(), signal);
377
+ await unlessAborted(this.kit.agents.read(), signal);
235
378
  if (this.kit.agents.route(agentId) === undefined)
236
- await unlessKilled(this.kit.agents.refresh(), signal);
379
+ await unlessAborted(this.kit.agents.refresh(), signal);
237
380
  const route = this.kit.agents.route(agentId);
238
381
  if (route === undefined)
239
382
  throw new Error(`this Environment hosts no Agent ${agentId}`);
240
- if (route.working_directory === join(LAUNCH_BASE, route.agent_id)) {
383
+ if (route.working_directory === join(agentBase(), route.agent_id)) {
241
384
  await mkdir(route.working_directory, { recursive: true });
242
385
  }
243
386
  return route;
244
387
  }
245
- async start(conversation, route, sessionId) {
388
+ async start(conversation, route, sessionId, empty) {
389
+ const reader = readerFor(route.kind);
390
+ reader.session = sessionId;
246
391
  const app = client({ name: '@agentshouse/kit' })
247
- .onNotification('session/update', ({ params }) => this.update(conversation, params))
248
- .onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
249
- .onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
250
392
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
251
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
252
- await unlessKilled(this.kit.agents.installed(), conversation.killed.signal);
253
- const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
393
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
394
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
395
+ const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
396
+ const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
254
397
  if (conversation.kills > 0) {
255
398
  bridge.close();
256
399
  throw new Error(KILLED);
257
400
  }
258
401
  conversation.opening = bridge;
402
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
403
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
404
+ let adapter = null;
259
405
  try {
260
- const adapter = await startAdapter(route.kind, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
406
+ await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
407
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
408
+ const streamed = await step(NO_CONNECTION, work.open(cli));
409
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
410
+ work.read(message);
411
+ this.read(conversation, reader, message);
412
+ }, { ...bridge.env, ...streamed }, undefined, scope));
261
413
  void adapter.exited.then(() => bridge.close());
262
414
  const agent = adapter.connection.agent;
263
415
  const cwd = route.working_directory;
264
- const opened = sessionId === null
265
- ? await agent.request('session/new', { cwd, mcpServers: [] })
266
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [] })), sessionId };
267
- const running = {
268
- adapter,
269
- bridge,
270
- sessionId: opened.sessionId,
271
- killed: false,
272
- queues: CLIS[route.kind].queues,
273
- };
274
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
416
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
417
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
418
+ const opened = resumed ?? (await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta })));
419
+ reader.session = opened.sessionId;
420
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
421
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
422
+ await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
423
+ }
424
+ const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
425
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
426
+ void work.settled().then(() => scope.record());
275
427
  conversation.running = running;
428
+ conversation.session = running.sessionId;
276
429
  this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
277
430
  if (conversation.commands !== null)
278
431
  this.commands(conversation, conversation.commands);
279
432
  this.options(conversation, options);
280
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
433
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
281
434
  return running.sessionId;
282
435
  }
283
436
  catch (error) {
284
- stop(bridge);
437
+ adapter?.retire();
438
+ work.close();
439
+ conversation.leftovers.add(scope);
285
440
  bridge.close();
286
441
  throw conversation.kills > 0 ? new Error(KILLED) : error;
287
442
  }
@@ -289,9 +444,22 @@ export class Conversations {
289
444
  conversation.opening = null;
290
445
  }
291
446
  }
447
+ async resume(conversation, kind, adapter, request, empty) {
448
+ try {
449
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
450
+ }
451
+ catch (error) {
452
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
453
+ throw new NotStarted(NO_RESUME, { cause: error });
454
+ }
455
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
456
+ return null;
457
+ }
458
+ }
292
459
  async launchSettings(running, route, offered) {
293
460
  let options = offered;
294
461
  const settings = [
462
+ ['mode', route.mode ?? CLIS[route.kind].fullAccess],
295
463
  ['model', route.model],
296
464
  ['thought_level', route.effort],
297
465
  ];
@@ -299,6 +467,7 @@ export class Conversations {
299
467
  if (value === null)
300
468
  continue;
301
469
  const option = options.find((candidate) => candidate.category === category);
470
+ running.reader.requested(option?.id ?? category, value);
302
471
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
303
472
  sessionId: running.sessionId,
304
473
  configId: option?.id ?? category,
@@ -308,23 +477,24 @@ export class Conversations {
308
477
  }
309
478
  return options;
310
479
  }
311
- exited(conversation, adapter, cause) {
312
- const running = conversation.running;
313
- if (running?.adapter !== adapter)
314
- return;
480
+ exited(conversation, running, cause) {
481
+ this.detach(conversation, running);
482
+ if (conversation.turn !== null) {
483
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
484
+ }
485
+ this.next(conversation);
486
+ this.kit.changed();
487
+ }
488
+ detach(conversation, running) {
489
+ running.work.close();
490
+ conversation.leftovers.add(running.scope);
315
491
  conversation.running = null;
316
492
  conversation.commands = null;
317
- conversation.jobs.clear();
318
- conversation.queued.length = 0;
319
493
  for (const [id, question] of this.questions) {
320
494
  if (question.conversation === conversation)
321
495
  this.questions.delete(id);
322
496
  }
323
497
  this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
324
- if (conversation.turn !== null) {
325
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
326
- }
327
- this.kit.changed();
328
498
  }
329
499
  commands(conversation, commands) {
330
500
  conversation.commands = commands;
@@ -339,7 +509,7 @@ export class Conversations {
339
509
  }
340
510
  deliver(conversation, turn, event, body) {
341
511
  this.reporting++;
342
- const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
512
+ const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
343
513
  conversation.reports = delivered
344
514
  .catch(() => undefined)
345
515
  .finally(() => {
@@ -348,105 +518,95 @@ export class Conversations {
348
518
  });
349
519
  return delivered;
350
520
  }
351
- report(conversation) {
352
- const turn = new Turn();
353
- conversation.open++;
354
- void this.deliver(conversation, turn, 'started', {})
355
- .then(() => this.admitted(conversation, turn))
356
- .catch(logged);
357
- return turn;
521
+ write(conversation, turn, operation) {
522
+ if (!turn.admitted || turn.ended || !this.watched)
523
+ return;
524
+ this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
358
525
  }
359
- admitted(conversation, turn) {
360
- if (turn.text !== '') {
361
- turn.unsentFrom = 0;
362
- this.draft(conversation, turn);
363
- }
364
- if (turn.entries !== null)
365
- this.plan(conversation, turn);
526
+ replay(conversation, turn) {
527
+ for (const operation of turn.parts.operations())
528
+ this.write(conversation, turn, operation);
366
529
  }
367
530
  begin(conversation) {
368
- const turn = this.report(conversation);
531
+ const turn = new Turn((operation) => this.write(conversation, turn, operation));
532
+ conversation.open++;
533
+ void this.deliver(conversation, turn, 'started', {})
534
+ .then(() => {
535
+ turn.admitted = true;
536
+ this.replay(conversation, turn);
537
+ })
538
+ .catch(logged);
369
539
  conversation.turn = turn;
540
+ if (conversation.context !== null)
541
+ turn.parts.measure(conversation.context);
542
+ for (const part of conversation.held.splice(0))
543
+ turn.parts.start(part);
370
544
  this.kit.changed();
371
545
  return turn;
372
546
  }
373
- end(conversation, turn, outcome) {
547
+ end(conversation, turn, failed, notice = null) {
374
548
  if (turn.ended)
375
549
  return;
550
+ if (failed !== null)
551
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
552
+ if (conversation.turn === turn) {
553
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
554
+ this.apply(conversation, turn, event);
555
+ }
556
+ if (notice !== null)
557
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
376
558
  turn.ended = true;
377
559
  conversation.open--;
378
560
  if (conversation.turn === turn)
379
561
  conversation.turn = null;
380
- void this.deliver(conversation, turn, 'ended', outcome).catch(logged);
381
- const late = conversation.open === 0 ? conversation.late.shift() : undefined;
382
- if (late !== undefined)
383
- this.failed(conversation, late);
562
+ const body = Promise.allSettled(turn.uploads).then(() => ({
563
+ parts: turn.parts.saved(),
564
+ context: turn.parts.context,
565
+ ...(failed === null ? {} : { failed: failed.failure }),
566
+ }));
567
+ void this.deliver(conversation, turn, 'ended', body).catch(logged);
568
+ this.next(conversation);
384
569
  this.kit.changed();
385
570
  }
386
- failed(conversation, cause) {
387
- if (conversation.open > 0)
388
- conversation.late.push(cause);
389
- else
390
- this.end(conversation, this.report(conversation), { failed: cause });
391
- }
392
571
  prompt(conversation, prompt) {
393
572
  const running = conversation.running;
394
- const sent = { turn: null };
395
- if (running.queues && (conversation.turn !== null || conversation.queued.length > 0)) {
396
- conversation.queued.push(sent);
397
- }
398
- else {
399
- this.attach(conversation.turn ?? this.begin(conversation), sent);
400
- }
401
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
402
- if (!running.adapter.connection.signal.aborted)
403
- this.answered(conversation, sent, causeOf(error));
404
- else if (sent.turn !== null) {
405
- this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
573
+ const turn = this.begin(conversation);
574
+ turn.prompted = true;
575
+ conversation.prompted = running.sessionId;
576
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
577
+ const notice = answeredFailure(answer);
578
+ if (notice !== null) {
579
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
580
+ }
581
+ else {
582
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
583
+ }
584
+ }, async (error) => {
585
+ if (running.adapter.connection.signal.aborted) {
586
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
587
+ }
588
+ else {
589
+ if (conversation.open === 1 && conversation.running === running) {
590
+ running.adapter.retire();
591
+ this.detach(conversation, running);
592
+ }
593
+ this.end(conversation, turn, answeredWith(error));
406
594
  }
407
595
  });
408
596
  }
409
- attach(turn, sent) {
410
- turn.prompt = sent;
411
- sent.turn = turn;
412
- }
413
- answered(conversation, sent, failed) {
414
- const turn = sent.turn;
415
- if (turn === null) {
416
- const queued = conversation.queued.indexOf(sent);
417
- if (queued < 0)
418
- return;
419
- conversation.queued.splice(queued, 1);
420
- if (failed !== null)
421
- this.failed(conversation, failed);
422
- }
423
- else if (failed !== null) {
424
- if (turn.ended)
425
- this.failed(conversation, failed);
426
- else
427
- this.end(conversation, turn, { failed });
428
- }
429
- else if (turn.prompt === sent) {
430
- this.end(conversation, turn, { text: turn.text });
431
- }
432
- }
433
597
  started(conversation) {
434
- if (conversation.turn !== null)
435
- return conversation.turn;
436
- const turn = this.begin(conversation);
437
- const queued = conversation.queued.shift();
438
- if (queued !== undefined)
439
- this.attach(turn, queued);
440
- return turn;
598
+ return conversation.turn ?? this.begin(conversation);
441
599
  }
442
600
  ended(conversation) {
443
601
  const turn = conversation.turn;
444
602
  if (turn === null)
445
603
  return;
446
- if (turn.prompt === null) {
447
- this.end(conversation, turn, { text: turn.text });
604
+ if (!turn.prompted) {
605
+ this.end(conversation, turn, null);
448
606
  }
449
607
  else {
608
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
609
+ this.apply(conversation, turn, event);
450
610
  conversation.turn = null;
451
611
  this.kit.changed();
452
612
  }
@@ -495,65 +655,102 @@ export class Conversations {
495
655
  if (content !== null)
496
656
  this.questions.get(interactionId)?.answer({ action: 'accept', content });
497
657
  }
498
- update(conversation, notification) {
499
- const update = notification.update;
500
- if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
501
- const turn = this.started(conversation);
502
- turn.text += update.content.text;
503
- this.draft(conversation, turn);
504
- }
505
- else if (update.sessionUpdate === 'plan') {
506
- const turn = this.started(conversation);
507
- turn.entries = update.entries;
508
- this.plan(conversation, turn);
509
- }
510
- else if (update.sessionUpdate === 'agent_thought_chunk' || update.sessionUpdate === 'tool_call') {
511
- this.started(conversation);
512
- }
513
- else if (update.sessionUpdate === 'available_commands_update') {
514
- this.commands(conversation, update.availableCommands);
658
+ read(conversation, reader, message) {
659
+ for (const event of reader.read(message))
660
+ this.event(conversation, event);
661
+ }
662
+ event(conversation, event) {
663
+ switch (event.kind) {
664
+ case 'started':
665
+ this.started(conversation);
666
+ return;
667
+ case 'ended':
668
+ this.ended(conversation);
669
+ return;
670
+ case 'commands':
671
+ this.commands(conversation, event.commands);
672
+ return;
673
+ case 'options':
674
+ this.options(conversation, event.options);
675
+ return;
676
+ case 'context':
677
+ conversation.context = { used: event.used, window: event.window };
678
+ conversation.turn?.parts.measure(conversation.context);
679
+ return;
680
+ case 'subagent-done':
681
+ this.subagentDone(conversation, event.id, event.result);
682
+ return;
683
+ case 'marker':
684
+ case 'image':
685
+ if (conversation.turn !== null)
686
+ break;
687
+ if (event.kind === 'marker' && event.marker === 'job')
688
+ conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
689
+ return;
515
690
  }
516
- else if (update.sessionUpdate === 'config_option_update') {
517
- this.options(conversation, update.configOptions);
691
+ this.apply(conversation, this.started(conversation), event);
692
+ }
693
+ apply(conversation, turn, event) {
694
+ switch (event.kind) {
695
+ case 'text':
696
+ case 'reasoning':
697
+ turn.parts.write(event.kind, event.id, event.text);
698
+ return;
699
+ case 'command':
700
+ turn.parts.command();
701
+ return;
702
+ case 'subagent': {
703
+ const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
704
+ conversation.subagents.set(event.id, { description: event.description, turn, index });
705
+ return;
706
+ }
707
+ case 'message':
708
+ turn.parts.start({ type: 'message', command: event.command, target: event.target });
709
+ return;
710
+ case 'marker':
711
+ turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
712
+ return;
713
+ case 'plan':
714
+ turn.parts.plan(event.entries);
715
+ return;
716
+ case 'image':
717
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
718
+ this.kit.changed();
719
+ return;
518
720
  }
519
721
  }
520
- job(conversation, job) {
521
- if (job.running)
522
- conversation.jobs.add(job.id);
722
+ subagentDone(conversation, id, result) {
723
+ const subagent = conversation.subagents.get(id);
724
+ if (subagent === undefined)
725
+ return;
726
+ conversation.subagents.delete(id);
727
+ if (subagent.turn === conversation.turn) {
728
+ subagent.turn.parts.done(subagent.index, result);
729
+ return;
730
+ }
731
+ const part = { type: 'subagent', description: subagent.description, status: 'done', result };
732
+ if (conversation.turn === null)
733
+ conversation.held.push(part);
523
734
  else
524
- conversation.jobs.delete(job.id);
525
- this.kit.changed();
735
+ conversation.turn.parts.start(part);
526
736
  }
527
- draft(conversation, turn) {
528
- const blocks = blocksOf(turn.text);
529
- const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
530
- const sent = this.kit.send({
531
- type: 'draft',
532
- conversation_id: conversation.id,
533
- turn_id: turn.id,
534
- sequence: turn.draftSequence + 1,
535
- from,
536
- blocks: blocks.slice(from),
537
- });
538
- if (sent) {
539
- turn.draftSequence++;
540
- turn.sent = blocks;
541
- turn.unsentFrom = null;
737
+ async image(conversation, turn, reserved, data, mediaType) {
738
+ const bytes = Buffer.from(data, 'base64');
739
+ const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
740
+ try {
741
+ const declared = (await this.deliver(conversation, turn, 'images', {
742
+ name,
743
+ media_type: mediaType,
744
+ bytes: bytes.length,
745
+ sha256: createHash('sha256').update(bytes).digest('hex'),
746
+ }));
747
+ const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
748
+ if (saved.save.status !== 'saved')
749
+ throw new Error(saved.save.failure ?? saved.save.status);
750
+ turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
542
751
  }
543
- else {
544
- turn.unsentFrom = from;
752
+ catch (error) {
753
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
545
754
  }
546
755
  }
547
- plan(conversation, turn) {
548
- const sent = this.kit.send({
549
- type: 'plan',
550
- conversation_id: conversation.id,
551
- turn_id: turn.id,
552
- sequence: turn.planSequence + 1,
553
- from: 0,
554
- steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
555
- });
556
- if (sent)
557
- turn.planSequence++;
558
- }
559
756
  }