@agentshouse/kit 0.2.1-cbabe8ad337ff → 0.2.1-cc6246211e00c

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