@agentshouse/kit 0.2.1-c5d36e97b0fea → 0.2.1-c5e51b4e66131

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 (131) hide show
  1. package/README.md +42 -41
  2. package/dist/acp.js +85 -18
  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 +4 -7
  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 +116 -0
  14. package/dist/conversations.js +415 -156
  15. package/dist/copies.js +102 -65
  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 -188
  24. package/dist/instructions.js +7 -10
  25. package/dist/kit-cli.js +68 -0
  26. package/dist/kit-main.js +8 -22
  27. package/dist/login.js +113 -23
  28. package/dist/logout.js +0 -2
  29. package/dist/mcp.js +6 -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 +98 -35
  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/silence.js +28 -0
  39. package/dist/skills.js +49 -35
  40. package/dist/transcription.js +85 -45
  41. package/dist/translate.js +2 -8
  42. package/dist/update.js +115 -53
  43. package/dist/web-wizard.js +101 -0
  44. package/dist/wire.js +22 -0
  45. package/dist/work.js +3 -5
  46. package/package.json +20 -20
  47. package/dist/bootstrap.json +0 -24
  48. package/dist/stream.js +0 -72
  49. package/how-we-work.md +0 -23
  50. package/skills/ask-matt/LICENSE +0 -21
  51. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  52. package/skills/ask-matt/SKILL.md +0 -91
  53. package/skills/ask-matt/agents/openai.yaml +0 -4
  54. package/skills/code-review/LICENSE +0 -21
  55. package/skills/code-review/SKILL.md +0 -89
  56. package/skills/code-review/agents/openai.yaml +0 -3
  57. package/skills/codebase-design/DEEPENING.md +0 -37
  58. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  59. package/skills/codebase-design/LICENSE +0 -21
  60. package/skills/codebase-design/SKILL.md +0 -116
  61. package/skills/codebase-design/agents/openai.yaml +0 -3
  62. package/skills/diagnosing-bugs/LICENSE +0 -21
  63. package/skills/diagnosing-bugs/SKILL.md +0 -140
  64. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  65. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  66. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  67. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  68. package/skills/domain-modeling/LICENSE +0 -21
  69. package/skills/domain-modeling/SKILL.md +0 -76
  70. package/skills/domain-modeling/agents/openai.yaml +0 -3
  71. package/skills/grill-me/LICENSE +0 -21
  72. package/skills/grill-me/SKILL.md +0 -8
  73. package/skills/grill-me/agents/openai.yaml +0 -4
  74. package/skills/grill-with-docs/LICENSE +0 -21
  75. package/skills/grill-with-docs/SKILL.md +0 -8
  76. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  77. package/skills/grilling/LICENSE +0 -21
  78. package/skills/grilling/SKILL.md +0 -30
  79. package/skills/grilling/agents/openai.yaml +0 -3
  80. package/skills/handoff/LICENSE +0 -21
  81. package/skills/handoff/SKILL.md +0 -17
  82. package/skills/handoff/agents/openai.yaml +0 -4
  83. package/skills/implement/LICENSE +0 -21
  84. package/skills/implement/SKILL.md +0 -16
  85. package/skills/implement/agents/openai.yaml +0 -4
  86. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  87. package/skills/improve-codebase-architecture/LICENSE +0 -21
  88. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  89. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  90. package/skills/prototype/LICENSE +0 -21
  91. package/skills/prototype/LOGIC.md +0 -67
  92. package/skills/prototype/SKILL.md +0 -28
  93. package/skills/prototype/UI.md +0 -112
  94. package/skills/prototype/agents/openai.yaml +0 -3
  95. package/skills/research/LICENSE +0 -21
  96. package/skills/research/SKILL.md +0 -14
  97. package/skills/research/agents/openai.yaml +0 -3
  98. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  99. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  100. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  101. package/skills/tdd/LICENSE +0 -21
  102. package/skills/tdd/SKILL.md +0 -40
  103. package/skills/tdd/agents/openai.yaml +0 -3
  104. package/skills/tdd/mocking.md +0 -59
  105. package/skills/tdd/tests.md +0 -77
  106. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  107. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  108. package/skills/teach/LICENSE +0 -21
  109. package/skills/teach/MISSION-FORMAT.md +0 -31
  110. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  111. package/skills/teach/SKILL.md +0 -141
  112. package/skills/teach/agents/openai.yaml +0 -4
  113. package/skills/to-questionnaire/LICENSE +0 -21
  114. package/skills/to-questionnaire/SKILL.md +0 -55
  115. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  116. package/skills/to-spec/LICENSE +0 -21
  117. package/skills/to-spec/SKILL.md +0 -76
  118. package/skills/to-spec/agents/openai.yaml +0 -4
  119. package/skills/to-tickets/LICENSE +0 -21
  120. package/skills/to-tickets/SKILL.md +0 -106
  121. package/skills/to-tickets/agents/openai.yaml +0 -4
  122. package/skills/wait-what/LICENSE +0 -21
  123. package/skills/wait-what/SKILL.md +0 -8
  124. package/skills/wait-what/agents/openai.yaml +0 -4
  125. package/skills/wayfinder/LICENSE +0 -21
  126. package/skills/wayfinder/SKILL.md +0 -129
  127. package/skills/wayfinder/agents/openai.yaml +0 -4
  128. package/skills/writing-for-agents/LICENSE +0 -21
  129. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  130. package/skills/writing-for-agents/SKILL.md +0 -83
  131. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -1,41 +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';
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
+ if (running.killed)
77
+ return { failure: 'killed', cause: KILLED };
78
+ return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
79
+ }
23
80
  function answerMessage(request, response) {
24
81
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
25
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
+ }
26
89
  function logged(error) {
27
90
  process.stderr.write(`kit: ${causeOf(error)}\n`);
28
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
+ }
29
105
  function marker(bridge) {
30
106
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
107
  }
32
108
  class Turn {
33
109
  id = randomUUID();
34
110
  parts;
35
- admitted = false;
36
111
  questions = 0;
37
112
  secrets = 0;
38
113
  prompted = false;
114
+ taken = null;
39
115
  ended = false;
40
116
  images = 0;
41
117
  uploads = [];
@@ -49,6 +125,8 @@ class Conversation {
49
125
  session = null;
50
126
  turn = null;
51
127
  commands = null;
128
+ options = null;
129
+ exitUntold = false;
52
130
  context = null;
53
131
  leftovers = new Set();
54
132
  waiting = [];
@@ -56,6 +134,9 @@ class Conversation {
56
134
  subagents = new Map();
57
135
  held = [];
58
136
  opening = null;
137
+ bridge = null;
138
+ launch = null;
139
+ prompted = null;
59
140
  kills = 0;
60
141
  killed = new AbortController();
61
142
  interrupts = 0;
@@ -63,6 +144,11 @@ class Conversation {
63
144
  open = 0;
64
145
  queue = Promise.resolve();
65
146
  reports = Promise.resolve();
147
+ starting = null;
148
+ preparing = null;
149
+ told = null;
150
+ untold = false;
151
+ shown = null;
66
152
  constructor(id) {
67
153
  this.id = id;
68
154
  }
@@ -78,9 +164,7 @@ function asksSecret(request) {
78
164
  export class Conversations {
79
165
  kit;
80
166
  conversations = new Map();
81
- acks = new Map();
82
167
  questions = new Map();
83
- carrying = 0;
84
168
  reporting = 0;
85
169
  watched = false;
86
170
  constructor(kit) {
@@ -88,38 +172,27 @@ export class Conversations {
88
172
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
89
173
  setInterval(() => this.look(), 2_000);
90
174
  }
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();
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 }));
119
192
  });
120
193
  }
121
194
  idle() {
122
- if (this.carrying > 0 || this.reporting > 0)
195
+ if (this.reporting > 0)
123
196
  return false;
124
197
  for (const conversation of this.conversations.values()) {
125
198
  const turn = conversation.turn;
@@ -132,6 +205,9 @@ export class Conversations {
132
205
  return false;
133
206
  return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
134
207
  }
208
+ quiet() {
209
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
210
+ }
135
211
  look() {
136
212
  for (const conversation of this.conversations.values()) {
137
213
  for (const scope of conversation.leftovers) {
@@ -145,6 +221,14 @@ export class Conversations {
145
221
  }
146
222
  opened() {
147
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
+ }
148
232
  }
149
233
  watching(watched) {
150
234
  const resend = watched && !this.watched;
@@ -164,63 +248,99 @@ export class Conversations {
164
248
  }
165
249
  return conversation;
166
250
  }
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') {
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') {
173
258
  if (--conversation.interrupts === 0)
174
259
  conversation.interrupted = new AbortController();
175
- await this.interrupt(conversation, String(input.turn_id));
260
+ await this.interrupt(conversation, String(call.turn_id));
176
261
  }
177
- if (input.kind === 'kill') {
262
+ if (call.kind === 'kill') {
178
263
  await this.kill(conversation);
179
264
  await conversation.sending;
180
265
  if (--conversation.kills === 0)
181
266
  conversation.killed = new AbortController();
182
267
  }
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));
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));
187
272
  if (question === undefined) {
188
- const text = answerMessage(input.request, input.response);
189
- 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 });
190
275
  }
191
- question.answer(input.response);
276
+ question.answer(call.response);
192
277
  }
193
- return { ack: Promise.resolve({}) };
278
+ return { result: Promise.resolve({}) };
194
279
  }
195
- async open(conversation, input) {
280
+ open(conversation, call) {
196
281
  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) }];
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) }];
207
324
  let route;
325
+ let instructing = null;
208
326
  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') });
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))));
215
332
  if (conversation.kills > 0)
216
333
  throw new Error(KILLED);
217
334
  }
218
335
  catch (error) {
219
- 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) }) };
220
339
  }
340
+ conversation.preparing = null;
221
341
  return {
222
- ack: new Promise((settle) => {
223
- conversation.waiting.push({ input, route, content, settle });
342
+ result: new Promise((settle) => {
343
+ conversation.waiting.push({ call, route, content, instructing, settle });
224
344
  this.next(conversation);
225
345
  }),
226
346
  };
@@ -229,62 +349,148 @@ export class Conversations {
229
349
  if (conversation.sending !== null || conversation.open > 0)
230
350
  return;
231
351
  const waiting = conversation.waiting.shift();
232
- 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);
233
356
  return;
357
+ }
234
358
  conversation.sending = this.send(conversation, waiting).finally(() => {
235
359
  conversation.sending = null;
236
360
  this.next(conversation);
237
361
  });
238
362
  }
239
363
  async send(conversation, waiting) {
240
- const { input } = waiting;
241
- let ack = {};
364
+ const { call } = waiting;
365
+ let result = {};
366
+ let started = false;
367
+ let instructing = waiting.instructing;
242
368
  const prompt = [];
243
369
  try {
244
370
  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 };
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;
249
381
  }
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
- });
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)) });
255
388
  }
256
389
  if (conversation.kills > 0)
257
390
  throw new Error(KILLED);
258
391
  }
259
392
  catch (error) {
260
- if ('provider_session_id' in ack) {
393
+ this.starts(conversation, null);
394
+ if (started) {
261
395
  conversation.session = null;
262
396
  await this.kill(conversation);
263
397
  }
264
- 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);
265
414
  return;
266
415
  }
267
- this.prompt(conversation, [...prompt, ...waiting.content]);
268
- 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);
269
421
  }
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)}`));
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}` });
285
463
  }
286
464
  }
287
- 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);
288
494
  }
289
495
  async interrupt(conversation, turnId) {
290
496
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -339,63 +545,78 @@ export class Conversations {
339
545
  }
340
546
  return route;
341
547
  }
342
- async start(conversation, route, sessionId) {
548
+ async start(conversation, routed, sessionId, empty, bridged) {
549
+ const route = await routed;
343
550
  const reader = readerFor(route.kind);
344
551
  reader.session = sessionId;
345
552
  const app = client({ name: '@agentshouse/kit' })
346
553
  .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));
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));
348
556
  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);
557
+ const bridging = this.bridged(conversation);
558
+ const bridge = await step(unbridged, bridging);
350
559
  if (conversation.kills > 0) {
351
- bridge.close();
560
+ this.closeBridge(conversation, bridging, bridge);
352
561
  throw new Error(KILLED);
353
562
  }
354
563
  conversation.opening = bridge;
355
564
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
356
565
  const scope = new Scope(marker(bridge), randomUUID(), cli);
566
+ let adapter = null;
357
567
  try {
358
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
568
+ bridged(bridge);
359
569
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
360
- 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) => {
361
572
  work.read(message);
362
573
  this.read(conversation, reader, message);
363
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
364
- void adapter.exited.then(() => bridge.close());
574
+ }, { ...bridge.env, ...streamed }, undefined, scope));
575
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
365
576
  const agent = adapter.connection.agent;
366
577
  const cwd = route.working_directory;
367
578
  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 };
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 })));
371
581
  reader.session = opened.sessionId;
372
- if (sessionId === null && CLIS[route.kind].renames) {
582
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
373
583
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
374
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
584
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
375
585
  }
376
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
377
- 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 ?? []));
378
588
  void work.settled().then(() => scope.record());
379
589
  conversation.running = running;
590
+ adapter.turn(conversation.open > 0);
380
591
  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));
592
+ conversation.options = options;
593
+ this.facts(conversation);
594
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
386
595
  return running.sessionId;
387
596
  }
388
597
  catch (error) {
389
- scope.kill();
598
+ adapter?.retire();
390
599
  work.close();
391
600
  conversation.leftovers.add(scope);
392
- bridge.close();
601
+ this.closeBridge(conversation, bridging, bridge);
393
602
  throw conversation.kills > 0 ? new Error(KILLED) : error;
394
603
  }
395
604
  finally {
396
605
  conversation.opening = null;
397
606
  }
398
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
+ }
399
620
  async launchSettings(running, route, offered) {
400
621
  let options = offered;
401
622
  const settings = [
@@ -417,27 +638,37 @@ export class Conversations {
417
638
  }
418
639
  return options;
419
640
  }
420
- exited(conversation, adapter, cause) {
421
- const running = conversation.running;
422
- if (running?.adapter !== adapter)
423
- return;
641
+ exited(conversation, running, cause) {
424
642
  this.detach(conversation, running);
425
643
  if (conversation.turn !== null) {
426
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
644
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
427
645
  }
428
646
  this.next(conversation);
429
647
  this.kit.changed();
430
648
  }
431
649
  detach(conversation, running) {
650
+ if (conversation.bridge === running.bridging)
651
+ conversation.bridge = null;
432
652
  running.work.close();
433
653
  conversation.leftovers.add(running.scope);
434
654
  conversation.running = null;
435
655
  conversation.commands = null;
656
+ conversation.options = null;
436
657
  for (const [id, question] of this.questions) {
437
658
  if (question.conversation === conversation)
438
659
  this.questions.delete(id);
439
660
  }
440
- 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);
441
672
  }
442
673
  commands(conversation, commands) {
443
674
  conversation.commands = commands;
@@ -448,6 +679,7 @@ export class Conversations {
448
679
  options(conversation, options) {
449
680
  if (conversation.running === null)
450
681
  return;
682
+ conversation.options = options;
451
683
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
452
684
  }
453
685
  deliver(conversation, turn, event, body) {
@@ -462,7 +694,7 @@ export class Conversations {
462
694
  return delivered;
463
695
  }
464
696
  write(conversation, turn, operation) {
465
- if (!turn.admitted || turn.ended || !this.watched)
697
+ if (turn.ended || !this.watched)
466
698
  return;
467
699
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
468
700
  }
@@ -473,12 +705,8 @@ export class Conversations {
473
705
  begin(conversation) {
474
706
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
475
707
  conversation.open++;
476
- void this.deliver(conversation, turn, 'started', {})
477
- .then(() => {
478
- turn.admitted = true;
479
- this.replay(conversation, turn);
480
- })
481
- .catch(logged);
708
+ conversation.running?.adapter.turn(true);
709
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
482
710
  conversation.turn = turn;
483
711
  if (conversation.context !== null)
484
712
  turn.parts.measure(conversation.context);
@@ -487,45 +715,64 @@ export class Conversations {
487
715
  this.kit.changed();
488
716
  return turn;
489
717
  }
490
- end(conversation, turn, failed) {
718
+ end(conversation, turn, failed, notice = null) {
491
719
  if (turn.ended)
492
720
  return;
721
+ if (failed !== null)
722
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
493
723
  if (conversation.turn === turn) {
494
724
  for (const event of conversation.running?.reader.closeTurn() ?? [])
495
725
  this.apply(conversation, turn, event);
496
726
  }
727
+ if (notice !== null)
728
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
497
729
  turn.ended = true;
498
730
  conversation.open--;
731
+ conversation.running?.adapter.turn(conversation.open > 0);
499
732
  if (conversation.turn === turn)
500
733
  conversation.turn = null;
501
734
  const body = Promise.allSettled(turn.uploads).then(() => ({
502
- parts: turn.parts.parts,
735
+ parts: turn.parts.saved(),
503
736
  context: turn.parts.context,
504
- ...failed,
737
+ ...(failed === null ? {} : { failed: failed.failure }),
505
738
  }));
506
739
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
507
740
  this.next(conversation);
508
741
  this.kit.changed();
509
742
  }
510
- prompt(conversation, prompt) {
743
+ prompt(conversation, prompt, taken) {
511
744
  const running = conversation.running;
512
745
  const turn = this.begin(conversation);
513
746
  turn.prompted = true;
514
- 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) => {
515
759
  if (running.adapter.connection.signal.aborted) {
516
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
760
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
517
761
  }
518
762
  else {
763
+ this.took(turn);
519
764
  if (conversation.open === 1 && conversation.running === running) {
520
- running.scope.kill();
765
+ running.adapter.retire();
521
766
  this.detach(conversation, running);
522
767
  }
523
- this.end(conversation, turn, { failed: causeOf(error) });
768
+ this.end(conversation, turn, answeredWith(error));
524
769
  }
525
770
  });
526
771
  }
527
772
  started(conversation) {
528
- return conversation.turn ?? this.begin(conversation);
773
+ const turn = conversation.turn ?? this.begin(conversation);
774
+ this.took(turn);
775
+ return turn;
529
776
  }
530
777
  ended(conversation) {
531
778
  const turn = conversation.turn;
@@ -620,6 +867,11 @@ export class Conversations {
620
867
  }
621
868
  this.apply(conversation, this.started(conversation), event);
622
869
  }
870
+ took(turn) {
871
+ const taken = turn.taken;
872
+ turn.taken = null;
873
+ taken?.();
874
+ }
623
875
  apply(conversation, turn, event) {
624
876
  switch (event.kind) {
625
877
  case 'text':
@@ -644,7 +896,7 @@ export class Conversations {
644
896
  turn.parts.plan(event.entries);
645
897
  return;
646
898
  case 'image':
647
- turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
899
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
648
900
  this.kit.changed();
649
901
  return;
650
902
  }
@@ -664,23 +916,30 @@ export class Conversations {
664
916
  else
665
917
  conversation.turn.parts.start(part);
666
918
  }
667
- async image(conversation, turn, data, mediaType) {
919
+ async image(conversation, turn, reserved, data, mediaType) {
668
920
  const bytes = Buffer.from(data, 'base64');
669
921
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
922
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
670
923
  try {
671
924
  const declared = (await this.deliver(conversation, turn, 'images', {
672
925
  name,
673
926
  media_type: mediaType,
674
927
  bytes: bytes.length,
675
- sha256: createHash('sha256').update(bytes).digest('hex'),
928
+ sha256,
676
929
  }));
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 });
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);
681
939
  }
682
940
  catch (error) {
683
- turn.parts.start({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` });
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);
684
943
  }
685
944
  }
686
945
  }