@agentshouse/kit 0.2.1-c39de3ed26c1b → 0.2.1-c3d0b9ac4a82a

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