@agentshouse/kit 0.2.1-c9a26041e119a → 0.2.1-ca0e1517cce79

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 (138) hide show
  1. package/README.md +28 -28
  2. package/dist/acp.js +36 -16
  3. package/dist/agents.js +52 -19
  4. package/dist/api.js +5 -5
  5. package/dist/app-git.js +10 -26
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +4 -7
  8. package/dist/calls.js +63 -0
  9. package/dist/chatgpt.js +7 -1
  10. package/dist/clis.js +15 -3
  11. package/dist/{stream.js → connection.js} +33 -30
  12. package/dist/conversations.js +404 -154
  13. package/dist/copies.js +77 -50
  14. package/dist/end.js +9 -4
  15. package/dist/events.js +6 -2
  16. package/dist/files.js +139 -37
  17. package/dist/git.js +3 -4
  18. package/dist/home.js +27 -13
  19. package/dist/house-cli.js +300 -0
  20. package/dist/house-main.js +7 -289
  21. package/dist/instructions.js +28 -7
  22. package/dist/kit-cli.js +68 -0
  23. package/dist/kit-main.js +8 -67
  24. package/dist/login.js +15 -8
  25. package/dist/logout.js +0 -2
  26. package/dist/mcp.js +4 -2
  27. package/dist/placement.js +9 -18
  28. package/dist/resident.js +88 -37
  29. package/dist/scope.js +3 -0
  30. package/dist/service.js +23 -1
  31. package/dist/sign-in.js +19 -37
  32. package/dist/skills.js +12 -93
  33. package/dist/transcription.js +51 -20
  34. package/dist/translate.js +2 -8
  35. package/dist/update.js +42 -45
  36. package/dist/web-wizard.js +100 -0
  37. package/dist/wire.js +19 -3
  38. package/dist/work.js +3 -5
  39. package/package.json +12 -12
  40. package/how-we-work.md +0 -33
  41. package/skills/ask-matt/LICENSE +0 -21
  42. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  43. package/skills/ask-matt/SKILL.md +0 -97
  44. package/skills/ask-matt/agents/openai.yaml +0 -5
  45. package/skills/code-review/LICENSE +0 -21
  46. package/skills/code-review/SKILL.md +0 -89
  47. package/skills/code-review/agents/openai.yaml +0 -3
  48. package/skills/codebase-design/DEEPENING.md +0 -37
  49. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  50. package/skills/codebase-design/LICENSE +0 -21
  51. package/skills/codebase-design/SKILL.md +0 -114
  52. package/skills/codebase-design/agents/openai.yaml +0 -3
  53. package/skills/diagnosing-bugs/LICENSE +0 -21
  54. package/skills/diagnosing-bugs/SKILL.md +0 -138
  55. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  56. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  57. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  58. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  59. package/skills/domain-modeling/LICENSE +0 -21
  60. package/skills/domain-modeling/SKILL.md +0 -74
  61. package/skills/domain-modeling/agents/openai.yaml +0 -3
  62. package/skills/grill-me/LICENSE +0 -21
  63. package/skills/grill-me/SKILL.md +0 -7
  64. package/skills/grill-me/agents/openai.yaml +0 -5
  65. package/skills/grill-with-docs/LICENSE +0 -21
  66. package/skills/grill-with-docs/SKILL.md +0 -7
  67. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  68. package/skills/grilling/LICENSE +0 -21
  69. package/skills/grilling/SKILL.md +0 -30
  70. package/skills/grilling/agents/openai.yaml +0 -3
  71. package/skills/handoff/LICENSE +0 -21
  72. package/skills/handoff/SKILL.md +0 -16
  73. package/skills/handoff/agents/openai.yaml +0 -5
  74. package/skills/implement/LICENSE +0 -21
  75. package/skills/implement/SKILL.md +0 -17
  76. package/skills/implement/agents/openai.yaml +0 -5
  77. package/skills/implement-spec/LICENSE +0 -21
  78. package/skills/implement-spec/SKILL.md +0 -40
  79. package/skills/implement-spec/agents/openai.yaml +0 -5
  80. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  81. package/skills/improve-codebase-architecture/LICENSE +0 -21
  82. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  83. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  84. package/skills/pr/CREDITS.md +0 -3
  85. package/skills/pr/LICENSE +0 -21
  86. package/skills/pr/SKILL.md +0 -170
  87. package/skills/pr/agents/openai.yaml +0 -3
  88. package/skills/prototype/LICENSE +0 -21
  89. package/skills/prototype/LOGIC.md +0 -67
  90. package/skills/prototype/SKILL.md +0 -26
  91. package/skills/prototype/UI.md +0 -112
  92. package/skills/prototype/agents/openai.yaml +0 -3
  93. package/skills/research/LICENSE +0 -21
  94. package/skills/research/SKILL.md +0 -12
  95. package/skills/research/agents/openai.yaml +0 -3
  96. package/skills/retro/LICENSE +0 -21
  97. package/skills/retro/SKILL.md +0 -44
  98. package/skills/retro/agents/openai.yaml +0 -5
  99. package/skills/tdd/LICENSE +0 -21
  100. package/skills/tdd/SKILL.md +0 -38
  101. package/skills/tdd/agents/openai.yaml +0 -3
  102. package/skills/tdd/mocking.md +0 -59
  103. package/skills/tdd/tests.md +0 -77
  104. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  105. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  106. package/skills/teach/LICENSE +0 -21
  107. package/skills/teach/MISSION-FORMAT.md +0 -31
  108. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  109. package/skills/teach/SKILL.md +0 -140
  110. package/skills/teach/agents/openai.yaml +0 -5
  111. package/skills/to-questionnaire/LICENSE +0 -21
  112. package/skills/to-questionnaire/SKILL.md +0 -54
  113. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  114. package/skills/to-spec/LICENSE +0 -21
  115. package/skills/to-spec/SKILL.md +0 -75
  116. package/skills/to-spec/agents/openai.yaml +0 -5
  117. package/skills/to-tickets/LICENSE +0 -21
  118. package/skills/to-tickets/SKILL.md +0 -105
  119. package/skills/to-tickets/agents/openai.yaml +0 -5
  120. package/skills/triage/AGENT-BRIEF.md +0 -207
  121. package/skills/triage/LICENSE +0 -21
  122. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  123. package/skills/triage/SKILL.md +0 -112
  124. package/skills/triage/agents/openai.yaml +0 -5
  125. package/skills/wait-what/LICENSE +0 -21
  126. package/skills/wait-what/SKILL.md +0 -7
  127. package/skills/wait-what/agents/openai.yaml +0 -5
  128. package/skills/wayfinder/LICENSE +0 -21
  129. package/skills/wayfinder/SKILL.md +0 -128
  130. package/skills/wayfinder/agents/openai.yaml +0 -5
  131. package/skills/wizard/LICENSE +0 -21
  132. package/skills/wizard/SKILL.md +0 -44
  133. package/skills/wizard/agents/openai.yaml +0 -3
  134. package/skills/wizard/template.sh +0 -223
  135. package/skills/writing-for-agents/LICENSE +0 -21
  136. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  137. package/skills/writing-for-agents/SKILL.md +0 -81
  138. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -1,42 +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';
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
+ return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
77
+ }
24
78
  function answerMessage(request, response) {
25
79
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
26
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
+ }
27
87
  function logged(error) {
28
88
  process.stderr.write(`kit: ${causeOf(error)}\n`);
29
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
+ }
30
103
  function marker(bridge) {
31
104
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
32
105
  }
33
106
  class Turn {
34
107
  id = randomUUID();
35
108
  parts;
36
- admitted = false;
37
109
  questions = 0;
38
110
  secrets = 0;
39
111
  prompted = false;
112
+ taken = null;
40
113
  ended = false;
41
114
  images = 0;
42
115
  uploads = [];
@@ -50,6 +123,8 @@ class Conversation {
50
123
  session = null;
51
124
  turn = null;
52
125
  commands = null;
126
+ options = null;
127
+ exitUntold = false;
53
128
  context = null;
54
129
  leftovers = new Set();
55
130
  waiting = [];
@@ -57,6 +132,9 @@ class Conversation {
57
132
  subagents = new Map();
58
133
  held = [];
59
134
  opening = null;
135
+ bridge = null;
136
+ launch = null;
137
+ prompted = null;
60
138
  kills = 0;
61
139
  killed = new AbortController();
62
140
  interrupts = 0;
@@ -64,6 +142,11 @@ class Conversation {
64
142
  open = 0;
65
143
  queue = Promise.resolve();
66
144
  reports = Promise.resolve();
145
+ starting = null;
146
+ preparing = null;
147
+ told = null;
148
+ untold = false;
149
+ shown = null;
67
150
  constructor(id) {
68
151
  this.id = id;
69
152
  }
@@ -79,9 +162,7 @@ function asksSecret(request) {
79
162
  export class Conversations {
80
163
  kit;
81
164
  conversations = new Map();
82
- acks = new Map();
83
165
  questions = new Map();
84
- carrying = 0;
85
166
  reporting = 0;
86
167
  watched = false;
87
168
  constructor(kit) {
@@ -89,38 +170,27 @@ export class Conversations {
89
170
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
90
171
  setInterval(() => this.look(), 2_000);
91
172
  }
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();
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 }));
120
190
  });
121
191
  }
122
192
  idle() {
123
- if (this.carrying > 0 || this.reporting > 0)
193
+ if (this.reporting > 0)
124
194
  return false;
125
195
  for (const conversation of this.conversations.values()) {
126
196
  const turn = conversation.turn;
@@ -149,6 +219,14 @@ export class Conversations {
149
219
  }
150
220
  opened() {
151
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
+ }
152
230
  }
153
231
  watching(watched) {
154
232
  const resend = watched && !this.watched;
@@ -168,63 +246,99 @@ export class Conversations {
168
246
  }
169
247
  return conversation;
170
248
  }
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') {
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') {
177
256
  if (--conversation.interrupts === 0)
178
257
  conversation.interrupted = new AbortController();
179
- await this.interrupt(conversation, String(input.turn_id));
258
+ await this.interrupt(conversation, String(call.turn_id));
180
259
  }
181
- if (input.kind === 'kill') {
260
+ if (call.kind === 'kill') {
182
261
  await this.kill(conversation);
183
262
  await conversation.sending;
184
263
  if (--conversation.kills === 0)
185
264
  conversation.killed = new AbortController();
186
265
  }
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));
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));
191
270
  if (question === undefined) {
192
- const text = answerMessage(input.request, input.response);
193
- 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 });
194
273
  }
195
- question.answer(input.response);
274
+ question.answer(call.response);
196
275
  }
197
- return { ack: Promise.resolve({}) };
276
+ return { result: Promise.resolve({}) };
198
277
  }
199
- async open(conversation, input) {
278
+ open(conversation, call) {
200
279
  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) }];
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) }];
211
322
  let route;
323
+ let instructing = null;
212
324
  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') });
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))));
219
330
  if (conversation.kills > 0)
220
331
  throw new Error(KILLED);
221
332
  }
222
333
  catch (error) {
223
- 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) }) };
224
337
  }
338
+ conversation.preparing = null;
225
339
  return {
226
- ack: new Promise((settle) => {
227
- conversation.waiting.push({ input, route, content, settle });
340
+ result: new Promise((settle) => {
341
+ conversation.waiting.push({ call, route, content, instructing, settle });
228
342
  this.next(conversation);
229
343
  }),
230
344
  };
@@ -233,62 +347,148 @@ export class Conversations {
233
347
  if (conversation.sending !== null || conversation.open > 0)
234
348
  return;
235
349
  const waiting = conversation.waiting.shift();
236
- 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);
237
354
  return;
355
+ }
238
356
  conversation.sending = this.send(conversation, waiting).finally(() => {
239
357
  conversation.sending = null;
240
358
  this.next(conversation);
241
359
  });
242
360
  }
243
361
  async send(conversation, waiting) {
244
- const { input } = waiting;
245
- let ack = {};
362
+ const { call } = waiting;
363
+ let result = {};
364
+ let started = false;
365
+ let instructing = waiting.instructing;
246
366
  const prompt = [];
247
367
  try {
248
368
  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 };
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;
253
379
  }
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
- });
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)) });
259
386
  }
260
387
  if (conversation.kills > 0)
261
388
  throw new Error(KILLED);
262
389
  }
263
390
  catch (error) {
264
- if ('provider_session_id' in ack) {
391
+ this.starts(conversation, null);
392
+ if (started) {
265
393
  conversation.session = null;
266
394
  await this.kill(conversation);
267
395
  }
268
- 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);
269
412
  return;
270
413
  }
271
- this.prompt(conversation, [...prompt, ...waiting.content]);
272
- 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);
273
419
  }
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)}`));
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}` });
289
461
  }
290
462
  }
291
- 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);
292
492
  }
293
493
  async interrupt(conversation, turnId) {
294
494
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -343,63 +543,77 @@ export class Conversations {
343
543
  }
344
544
  return route;
345
545
  }
346
- async start(conversation, route, sessionId) {
546
+ async start(conversation, routed, sessionId, empty, bridged) {
547
+ const route = await routed;
347
548
  const reader = readerFor(route.kind);
348
549
  reader.session = sessionId;
349
550
  const app = client({ name: '@agentshouse/kit' })
350
551
  .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));
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));
352
554
  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);
555
+ const bridging = this.bridged(conversation);
556
+ const bridge = await step(unbridged, bridging);
354
557
  if (conversation.kills > 0) {
355
- bridge.close();
558
+ this.closeBridge(conversation, bridging, bridge);
356
559
  throw new Error(KILLED);
357
560
  }
358
561
  conversation.opening = bridge;
359
562
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
360
563
  const scope = new Scope(marker(bridge), randomUUID(), cli);
564
+ let adapter = null;
361
565
  try {
362
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
566
+ bridged(bridge);
363
567
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
364
- 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) => {
365
570
  work.read(message);
366
571
  this.read(conversation, reader, message);
367
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
368
- void adapter.exited.then(() => bridge.close());
572
+ }, { ...bridge.env, ...streamed }, undefined, scope));
573
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
369
574
  const agent = adapter.connection.agent;
370
575
  const cwd = route.working_directory;
371
576
  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 };
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 })));
375
579
  reader.session = opened.sessionId;
376
- if (sessionId === null && CLIS[route.kind].renames) {
580
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
377
581
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
378
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
582
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
379
583
  }
380
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
381
- 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 ?? []));
382
586
  void work.settled().then(() => scope.record());
383
587
  conversation.running = running;
384
588
  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));
589
+ conversation.options = options;
590
+ this.facts(conversation);
591
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
390
592
  return running.sessionId;
391
593
  }
392
594
  catch (error) {
393
- scope.kill();
595
+ adapter?.retire();
394
596
  work.close();
395
597
  conversation.leftovers.add(scope);
396
- bridge.close();
598
+ this.closeBridge(conversation, bridging, bridge);
397
599
  throw conversation.kills > 0 ? new Error(KILLED) : error;
398
600
  }
399
601
  finally {
400
602
  conversation.opening = null;
401
603
  }
402
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
+ }
403
617
  async launchSettings(running, route, offered) {
404
618
  let options = offered;
405
619
  const settings = [
@@ -421,27 +635,37 @@ export class Conversations {
421
635
  }
422
636
  return options;
423
637
  }
424
- exited(conversation, adapter, cause) {
425
- const running = conversation.running;
426
- if (running?.adapter !== adapter)
427
- return;
638
+ exited(conversation, running, cause) {
428
639
  this.detach(conversation, running);
429
640
  if (conversation.turn !== null) {
430
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
641
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
431
642
  }
432
643
  this.next(conversation);
433
644
  this.kit.changed();
434
645
  }
435
646
  detach(conversation, running) {
647
+ if (conversation.bridge === running.bridging)
648
+ conversation.bridge = null;
436
649
  running.work.close();
437
650
  conversation.leftovers.add(running.scope);
438
651
  conversation.running = null;
439
652
  conversation.commands = null;
653
+ conversation.options = null;
440
654
  for (const [id, question] of this.questions) {
441
655
  if (question.conversation === conversation)
442
656
  this.questions.delete(id);
443
657
  }
444
- 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);
445
669
  }
446
670
  commands(conversation, commands) {
447
671
  conversation.commands = commands;
@@ -452,6 +676,7 @@ export class Conversations {
452
676
  options(conversation, options) {
453
677
  if (conversation.running === null)
454
678
  return;
679
+ conversation.options = options;
455
680
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
456
681
  }
457
682
  deliver(conversation, turn, event, body) {
@@ -466,7 +691,7 @@ export class Conversations {
466
691
  return delivered;
467
692
  }
468
693
  write(conversation, turn, operation) {
469
- if (!turn.admitted || turn.ended || !this.watched)
694
+ if (turn.ended || !this.watched)
470
695
  return;
471
696
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
472
697
  }
@@ -477,12 +702,7 @@ export class Conversations {
477
702
  begin(conversation) {
478
703
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
479
704
  conversation.open++;
480
- void this.deliver(conversation, turn, 'started', {})
481
- .then(() => {
482
- turn.admitted = true;
483
- this.replay(conversation, turn);
484
- })
485
- .catch(logged);
705
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
486
706
  conversation.turn = turn;
487
707
  if (conversation.context !== null)
488
708
  turn.parts.measure(conversation.context);
@@ -491,13 +711,17 @@ export class Conversations {
491
711
  this.kit.changed();
492
712
  return turn;
493
713
  }
494
- end(conversation, turn, failed) {
714
+ end(conversation, turn, failed, notice = null) {
495
715
  if (turn.ended)
496
716
  return;
717
+ if (failed !== null)
718
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
497
719
  if (conversation.turn === turn) {
498
720
  for (const event of conversation.running?.reader.closeTurn() ?? [])
499
721
  this.apply(conversation, turn, event);
500
722
  }
723
+ if (notice !== null)
724
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
501
725
  turn.ended = true;
502
726
  conversation.open--;
503
727
  if (conversation.turn === turn)
@@ -505,31 +729,45 @@ export class Conversations {
505
729
  const body = Promise.allSettled(turn.uploads).then(() => ({
506
730
  parts: turn.parts.saved(),
507
731
  context: turn.parts.context,
508
- ...failed,
732
+ ...(failed === null ? {} : { failed: failed.failure }),
509
733
  }));
510
734
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
511
735
  this.next(conversation);
512
736
  this.kit.changed();
513
737
  }
514
- prompt(conversation, prompt) {
738
+ prompt(conversation, prompt, taken) {
515
739
  const running = conversation.running;
516
740
  const turn = this.begin(conversation);
517
741
  turn.prompted = true;
518
- 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) => {
519
754
  if (running.adapter.connection.signal.aborted) {
520
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
755
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
521
756
  }
522
757
  else {
758
+ this.took(turn);
523
759
  if (conversation.open === 1 && conversation.running === running) {
524
- running.scope.kill();
760
+ running.adapter.retire();
525
761
  this.detach(conversation, running);
526
762
  }
527
- this.end(conversation, turn, { failed: causeOf(error) });
763
+ this.end(conversation, turn, answeredWith(error));
528
764
  }
529
765
  });
530
766
  }
531
767
  started(conversation) {
532
- return conversation.turn ?? this.begin(conversation);
768
+ const turn = conversation.turn ?? this.begin(conversation);
769
+ this.took(turn);
770
+ return turn;
533
771
  }
534
772
  ended(conversation) {
535
773
  const turn = conversation.turn;
@@ -624,6 +862,11 @@ export class Conversations {
624
862
  }
625
863
  this.apply(conversation, this.started(conversation), event);
626
864
  }
865
+ took(turn) {
866
+ const taken = turn.taken;
867
+ turn.taken = null;
868
+ taken?.();
869
+ }
627
870
  apply(conversation, turn, event) {
628
871
  switch (event.kind) {
629
872
  case 'text':
@@ -671,20 +914,27 @@ export class Conversations {
671
914
  async image(conversation, turn, reserved, data, mediaType) {
672
915
  const bytes = Buffer.from(data, 'base64');
673
916
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
917
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
674
918
  try {
675
919
  const declared = (await this.deliver(conversation, turn, 'images', {
676
920
  name,
677
921
  media_type: mediaType,
678
922
  bytes: bytes.length,
679
- sha256: createHash('sha256').update(bytes).digest('hex'),
923
+ sha256,
680
924
  }));
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);
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);
685
934
  }
686
935
  catch (error) {
687
- turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
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);
688
938
  }
689
939
  }
690
940
  }