@agentshouse/kit 0.2.1-alpha.9 → 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 (107) hide show
  1. package/README.md +45 -26
  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/conversations.js +428 -233
  14. package/dist/copies.js +233 -171
  15. package/dist/end.js +23 -0
  16. package/dist/enrol-main.js +7 -1
  17. package/dist/events.js +525 -0
  18. package/dist/files.js +24 -3
  19. package/dist/git-main.js +2 -2
  20. package/dist/git.js +15 -9
  21. package/dist/home.js +127 -9
  22. package/dist/house-cli.js +291 -0
  23. package/dist/house-main.js +5 -102
  24. package/dist/inputs.js +37 -0
  25. package/dist/instructions.js +10 -31
  26. package/dist/kit-cli.js +68 -0
  27. package/dist/kit-main.js +8 -16
  28. package/dist/login.js +129 -94
  29. package/dist/logout.js +19 -0
  30. package/dist/mcp.js +39 -0
  31. package/dist/operation.js +7 -4
  32. package/dist/parts.js +82 -0
  33. package/dist/placement.js +30 -0
  34. package/dist/refusals.js +53 -0
  35. package/dist/relay.js +20 -0
  36. package/dist/resident.js +122 -41
  37. package/dist/scope.js +203 -0
  38. package/dist/secret-input.js +7 -2
  39. package/dist/service-main.js +9 -0
  40. package/dist/service.js +33 -0
  41. package/dist/shell.js +3 -0
  42. package/dist/sign-in.js +47 -28
  43. package/dist/skills.js +80 -16
  44. package/dist/stream.js +89 -13
  45. package/dist/transcription.js +170 -0
  46. package/dist/update.js +138 -0
  47. package/dist/wire.js +6 -0
  48. package/dist/work.js +346 -0
  49. package/how-we-work.md +22 -14
  50. package/package.json +5 -7
  51. package/skills/ask-matt/SKILL.md +17 -11
  52. package/skills/ask-matt/agents/openai.yaml +1 -0
  53. package/skills/code-review/SKILL.md +5 -5
  54. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  55. package/skills/codebase-design/SKILL.md +0 -2
  56. package/skills/diagnosing-bugs/SKILL.md +2 -4
  57. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  58. package/skills/domain-modeling/SKILL.md +12 -14
  59. package/skills/grill-me/SKILL.md +1 -2
  60. package/skills/grill-me/agents/openai.yaml +1 -0
  61. package/skills/grill-with-docs/SKILL.md +1 -2
  62. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  63. package/skills/grilling/SKILL.md +2 -2
  64. package/skills/handoff/SKILL.md +2 -3
  65. package/skills/handoff/agents/openai.yaml +1 -0
  66. package/skills/implement/SKILL.md +5 -4
  67. package/skills/implement/agents/openai.yaml +1 -0
  68. package/skills/implement-spec/SKILL.md +40 -0
  69. package/skills/implement-spec/agents/openai.yaml +5 -0
  70. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  71. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  72. package/skills/pr/CREDITS.md +3 -0
  73. package/skills/pr/LICENSE +21 -0
  74. package/skills/pr/SKILL.md +170 -0
  75. package/skills/pr/agents/openai.yaml +3 -0
  76. package/skills/prototype/SKILL.md +0 -2
  77. package/skills/research/SKILL.md +0 -2
  78. package/skills/retro/LICENSE +21 -0
  79. package/skills/retro/SKILL.md +44 -0
  80. package/skills/retro/agents/openai.yaml +5 -0
  81. package/skills/tdd/SKILL.md +2 -4
  82. package/skills/teach/SKILL.md +3 -4
  83. package/skills/teach/agents/openai.yaml +1 -0
  84. package/skills/to-questionnaire/SKILL.md +1 -2
  85. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  86. package/skills/to-spec/SKILL.md +1 -2
  87. package/skills/to-spec/agents/openai.yaml +1 -0
  88. package/skills/to-tickets/SKILL.md +3 -4
  89. package/skills/to-tickets/agents/openai.yaml +1 -0
  90. package/skills/triage/AGENT-BRIEF.md +207 -0
  91. package/skills/triage/LICENSE +21 -0
  92. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  93. package/skills/triage/SKILL.md +112 -0
  94. package/skills/triage/agents/openai.yaml +5 -0
  95. package/skills/wait-what/SKILL.md +2 -3
  96. package/skills/wait-what/agents/openai.yaml +1 -0
  97. package/skills/wayfinder/SKILL.md +5 -6
  98. package/skills/wayfinder/agents/openai.yaml +1 -0
  99. package/skills/wizard/LICENSE +21 -0
  100. package/skills/wizard/SKILL.md +44 -0
  101. package/skills/wizard/agents/openai.yaml +3 -0
  102. package/skills/wizard/template.sh +223 -0
  103. package/skills/writing-for-agents/SKILL.md +0 -2
  104. package/dist/blocks.js +0 -51
  105. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  106. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  107. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
@@ -1,62 +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
18
  import { createHowWeWork } from './skills.js';
13
- const LAUNCH_BASE = '/agents/house';
19
+ import { transcribe } from './transcription.js';
20
+ import { workFor } from './work.js';
21
+ const SIGNED_OUT = RequestError.authRequired().code;
14
22
  const KILLED = 'the conversation was killed';
23
+ const 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']);
15
57
  function causeOf(error) {
16
58
  return error instanceof Error ? error.message : String(error);
17
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
+ }
18
67
  function answerMessage(request, response) {
19
68
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
20
69
  }
21
70
  function logged(error) {
22
71
  process.stderr.write(`kit: ${causeOf(error)}\n`);
23
72
  }
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
- });
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);
32
79
  }
33
- function stop(bridge) {
34
- killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
80
+ function marker(bridge) {
81
+ return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
35
82
  }
36
83
  class Turn {
37
84
  id = randomUUID();
38
- text = '';
39
- sent = [];
40
- unsentFrom = null;
41
- draftSequence = 0;
42
- planSequence = 0;
43
- entries = null;
85
+ parts;
86
+ admitted = false;
44
87
  questions = 0;
45
88
  secrets = 0;
46
- prompt = null;
89
+ prompted = false;
47
90
  ended = false;
91
+ images = 0;
92
+ uploads = [];
93
+ constructor(emit) {
94
+ this.parts = new Parts(emit);
95
+ }
48
96
  }
49
97
  class Conversation {
50
98
  id;
51
99
  running = null;
100
+ session = null;
52
101
  turn = null;
53
102
  commands = null;
54
- jobs = new Set();
55
- queued = [];
56
- late = [];
103
+ context = null;
104
+ leftovers = new Set();
105
+ waiting = [];
106
+ sending = null;
107
+ subagents = new Map();
108
+ held = [];
57
109
  opening = null;
110
+ prompted = null;
58
111
  kills = 0;
59
112
  killed = new AbortController();
113
+ interrupts = 0;
114
+ interrupted = new AbortController();
60
115
  open = 0;
61
116
  queue = Promise.resolve();
62
117
  reports = Promise.resolve();
@@ -75,51 +130,72 @@ function asksSecret(request) {
75
130
  export class Conversations {
76
131
  kit;
77
132
  conversations = new Map();
78
- acks = new Map();
79
133
  questions = new Map();
80
- carrying = 0;
81
134
  reporting = 0;
135
+ watched = false;
82
136
  constructor(kit) {
83
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);
84
140
  }
85
141
  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();
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 }));
106
158
  });
107
159
  }
108
160
  idle() {
109
- if (this.carrying > 0 || this.reporting > 0)
161
+ if (this.reporting > 0)
110
162
  return false;
111
163
  for (const conversation of this.conversations.values()) {
112
164
  const turn = conversation.turn;
113
165
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
114
- if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
166
+ if (conversation.open > (asking ? 1 : 0))
115
167
  return false;
116
168
  }
117
- 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();
118
187
  }
119
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;
120
196
  for (const conversation of this.conversations.values()) {
121
197
  if (conversation.turn !== null)
122
- conversation.turn.unsentFrom = 0;
198
+ this.replay(conversation, conversation.turn);
123
199
  }
124
200
  }
125
201
  conversation(id) {
@@ -132,13 +208,17 @@ export class Conversations {
132
208
  }
133
209
  async carry(conversation, input) {
134
210
  if (input.kind === 'open')
135
- return this.open(conversation, input);
211
+ return { ack: Promise.resolve(await this.open(conversation, input)) };
136
212
  if (input.kind === 'message')
137
213
  return this.message(conversation, input);
138
- if (input.kind === 'interrupt')
214
+ if (input.kind === 'interrupt') {
215
+ if (--conversation.interrupts === 0)
216
+ conversation.interrupted = new AbortController();
139
217
  await this.interrupt(conversation, String(input.turn_id));
218
+ }
140
219
  if (input.kind === 'kill') {
141
220
  await this.kill(conversation);
221
+ await conversation.sending;
142
222
  if (--conversation.kills === 0)
143
223
  conversation.killed = new AbortController();
144
224
  }
@@ -147,52 +227,110 @@ export class Conversations {
147
227
  if (input.kind === 'answer') {
148
228
  const question = this.questions.get(String(input.interaction_id));
149
229
  if (question === undefined) {
150
- 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 });
151
232
  }
152
233
  question.answer(input.response);
153
234
  }
154
- return {};
235
+ return { ack: Promise.resolve({}) };
155
236
  }
156
237
  async open(conversation, input) {
157
238
  if (conversation.running !== null)
158
239
  return {};
159
240
  try {
160
- 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) };
161
242
  }
162
243
  catch (error) {
163
- return { refused: causeOf(error) };
244
+ return { refused: refusal(conversation, error) };
164
245
  }
165
246
  }
166
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;
167
283
  let ack = {};
284
+ let started = false;
168
285
  const prompt = [];
169
286
  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
287
  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 };
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 };
176
295
  }
177
296
  if (input.first === true) {
178
297
  prompt.push({
179
298
  type: 'text',
180
- 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()),
181
300
  });
182
301
  }
183
- prompt.push({ type: 'text', text: String(input.text) });
184
- if (paths.length > 0)
185
- prompt.push({ type: 'text', text: paths.join('\n') });
186
302
  if (conversation.kills > 0)
187
303
  throw new Error(KILLED);
188
304
  }
189
305
  catch (error) {
190
- if ('provider_session_id' in ack)
306
+ if (started) {
307
+ conversation.session = null;
191
308
  await this.kill(conversation);
192
- return { refused: conversation.kills > 0 ? KILLED : causeOf(error) };
309
+ }
310
+ waiting.settle({ refused: refusal(conversation, error) });
311
+ return;
193
312
  }
194
- this.prompt(conversation, prompt);
195
- 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;
196
334
  }
197
335
  async interrupt(conversation, turnId) {
198
336
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -203,11 +341,13 @@ export class Conversations {
203
341
  }
204
342
  halt(conversation) {
205
343
  if (conversation.opening !== null)
206
- stop(conversation.opening);
344
+ killMarked(marker(conversation.opening));
345
+ for (const scope of conversation.leftovers)
346
+ scope.kill();
207
347
  if (conversation.running === null)
208
348
  return;
209
349
  conversation.running.killed = true;
210
- stop(conversation.running.bridge);
350
+ conversation.running.scope.kill();
211
351
  }
212
352
  async kill(conversation) {
213
353
  const running = conversation.running;
@@ -219,6 +359,7 @@ export class Conversations {
219
359
  if (running === null)
220
360
  return;
221
361
  try {
362
+ running.reader.requested(option, value);
222
363
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
223
364
  sessionId: running.sessionId,
224
365
  configId: option,
@@ -227,63 +368,75 @@ export class Conversations {
227
368
  this.options(conversation, answered.configOptions);
228
369
  }
229
370
  catch (error) {
371
+ running.reader.refused(option);
230
372
  logged(error);
231
373
  }
232
374
  }
233
375
  async route(conversation, agentId) {
234
376
  const signal = conversation.killed.signal;
235
- await unlessKilled(this.kit.agents.read(), signal);
377
+ await unlessAborted(this.kit.agents.read(), signal);
236
378
  if (this.kit.agents.route(agentId) === undefined)
237
- await unlessKilled(this.kit.agents.refresh(), signal);
379
+ await unlessAborted(this.kit.agents.refresh(), signal);
238
380
  const route = this.kit.agents.route(agentId);
239
381
  if (route === undefined)
240
382
  throw new Error(`this Environment hosts no Agent ${agentId}`);
241
- if (route.working_directory === join(LAUNCH_BASE, route.agent_id)) {
383
+ if (route.working_directory === join(agentBase(), route.agent_id)) {
242
384
  await mkdir(route.working_directory, { recursive: true });
243
385
  }
244
386
  return route;
245
387
  }
246
- async start(conversation, route, sessionId) {
388
+ async start(conversation, route, sessionId, empty) {
389
+ const reader = readerFor(route.kind);
390
+ reader.session = sessionId;
247
391
  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
392
  .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);
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));
255
397
  if (conversation.kills > 0) {
256
398
  bridge.close();
257
399
  throw new Error(KILLED);
258
400
  }
259
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;
260
405
  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);
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));
263
413
  void adapter.exited.then(() => bridge.close());
264
414
  const agent = adapter.connection.agent;
265
415
  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 ?? []);
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());
277
427
  conversation.running = running;
428
+ conversation.session = running.sessionId;
278
429
  this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
279
430
  if (conversation.commands !== null)
280
431
  this.commands(conversation, conversation.commands);
281
432
  this.options(conversation, options);
282
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
433
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
283
434
  return running.sessionId;
284
435
  }
285
436
  catch (error) {
286
- stop(bridge);
437
+ adapter?.retire();
438
+ work.close();
439
+ conversation.leftovers.add(scope);
287
440
  bridge.close();
288
441
  throw conversation.kills > 0 ? new Error(KILLED) : error;
289
442
  }
@@ -291,9 +444,22 @@ export class Conversations {
291
444
  conversation.opening = null;
292
445
  }
293
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
+ }
294
459
  async launchSettings(running, route, offered) {
295
460
  let options = offered;
296
461
  const settings = [
462
+ ['mode', route.mode ?? CLIS[route.kind].fullAccess],
297
463
  ['model', route.model],
298
464
  ['thought_level', route.effort],
299
465
  ];
@@ -301,6 +467,7 @@ export class Conversations {
301
467
  if (value === null)
302
468
  continue;
303
469
  const option = options.find((candidate) => candidate.category === category);
470
+ running.reader.requested(option?.id ?? category, value);
304
471
  const answered = await running.adapter.connection.agent.request('session/set_config_option', {
305
472
  sessionId: running.sessionId,
306
473
  configId: option?.id ?? category,
@@ -310,23 +477,24 @@ export class Conversations {
310
477
  }
311
478
  return options;
312
479
  }
313
- exited(conversation, adapter, cause) {
314
- const running = conversation.running;
315
- if (running?.adapter !== adapter)
316
- 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);
317
491
  conversation.running = null;
318
492
  conversation.commands = null;
319
- conversation.jobs.clear();
320
- conversation.queued.length = 0;
321
493
  for (const [id, question] of this.questions) {
322
494
  if (question.conversation === conversation)
323
495
  this.questions.delete(id);
324
496
  }
325
497
  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();
330
498
  }
331
499
  commands(conversation, commands) {
332
500
  conversation.commands = commands;
@@ -341,7 +509,7 @@ export class Conversations {
341
509
  }
342
510
  deliver(conversation, turn, event, body) {
343
511
  this.reporting++;
344
- 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));
345
513
  conversation.reports = delivered
346
514
  .catch(() => undefined)
347
515
  .finally(() => {
@@ -350,105 +518,95 @@ export class Conversations {
350
518
  });
351
519
  return delivered;
352
520
  }
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;
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 });
360
525
  }
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);
526
+ replay(conversation, turn) {
527
+ for (const operation of turn.parts.operations())
528
+ this.write(conversation, turn, operation);
368
529
  }
369
530
  begin(conversation) {
370
- 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);
371
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);
372
544
  this.kit.changed();
373
545
  return turn;
374
546
  }
375
- end(conversation, turn, outcome) {
547
+ end(conversation, turn, failed, notice = null) {
376
548
  if (turn.ended)
377
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 });
378
558
  turn.ended = true;
379
559
  conversation.open--;
380
560
  if (conversation.turn === turn)
381
561
  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);
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);
386
569
  this.kit.changed();
387
570
  }
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
571
  prompt(conversation, prompt) {
395
572
  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 });
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));
408
594
  }
409
595
  });
410
596
  }
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
597
  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);
442
- return turn;
598
+ return conversation.turn ?? this.begin(conversation);
443
599
  }
444
600
  ended(conversation) {
445
601
  const turn = conversation.turn;
446
602
  if (turn === null)
447
603
  return;
448
- if (turn.prompt === null) {
449
- this.end(conversation, turn, { text: turn.text });
604
+ if (!turn.prompted) {
605
+ this.end(conversation, turn, null);
450
606
  }
451
607
  else {
608
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
609
+ this.apply(conversation, turn, event);
452
610
  conversation.turn = null;
453
611
  this.kit.changed();
454
612
  }
@@ -497,65 +655,102 @@ export class Conversations {
497
655
  if (content !== null)
498
656
  this.questions.get(interactionId)?.answer({ action: 'accept', content });
499
657
  }
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);
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;
517
690
  }
518
- else if (update.sessionUpdate === 'config_option_update') {
519
- 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;
520
720
  }
521
721
  }
522
- job(conversation, job) {
523
- if (job.running)
524
- 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);
525
734
  else
526
- conversation.jobs.delete(job.id);
527
- this.kit.changed();
735
+ conversation.turn.parts.start(part);
528
736
  }
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;
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);
544
751
  }
545
- else {
546
- turn.unsentFrom = from;
752
+ catch (error) {
753
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
547
754
  }
548
755
  }
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
756
  }