@agentshouse/kit 0.2.1-c5d51f1e4ce19 → 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 (143) hide show
  1. package/README.md +28 -28
  2. package/dist/acp.js +85 -18
  3. package/dist/agents.js +38 -17
  4. package/dist/api.js +5 -5
  5. package/dist/app-git.js +10 -26
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +4 -7
  8. package/dist/building.js +46 -0
  9. package/dist/calls.js +63 -0
  10. package/dist/chatgpt.js +1 -1
  11. package/dist/clis-main.js +2 -2
  12. package/dist/clis.js +47 -15
  13. package/dist/{stream.js → connection.js} +45 -52
  14. package/dist/conversations.js +391 -152
  15. package/dist/copies.js +77 -50
  16. package/dist/end.js +9 -4
  17. package/dist/events.js +4 -3
  18. package/dist/files.js +139 -37
  19. package/dist/git.js +5 -6
  20. package/dist/home.js +27 -13
  21. package/dist/house-cli.js +96 -88
  22. package/dist/house-main.js +3 -3
  23. package/dist/instructions.js +29 -8
  24. package/dist/kit-cli.js +4 -4
  25. package/dist/kit-main.js +3 -3
  26. package/dist/login.js +18 -9
  27. package/dist/logout.js +0 -2
  28. package/dist/mcp.js +6 -2
  29. package/dist/placement.js +9 -18
  30. package/dist/relay.js +21 -0
  31. package/dist/resident.js +92 -38
  32. package/dist/scope.js +3 -0
  33. package/dist/service.js +23 -1
  34. package/dist/sign-in.js +22 -39
  35. package/dist/silence.js +28 -0
  36. package/dist/skills.js +42 -97
  37. package/dist/transcription.js +83 -43
  38. package/dist/translate.js +2 -8
  39. package/dist/update.js +89 -49
  40. package/dist/web-wizard.js +101 -0
  41. package/dist/wire.js +19 -3
  42. package/dist/work.js +3 -5
  43. package/package.json +20 -21
  44. package/dist/bootstrap.json +0 -24
  45. package/how-we-work.md +0 -33
  46. package/skills/ask-matt/LICENSE +0 -21
  47. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  48. package/skills/ask-matt/SKILL.md +0 -97
  49. package/skills/ask-matt/agents/openai.yaml +0 -5
  50. package/skills/code-review/LICENSE +0 -21
  51. package/skills/code-review/SKILL.md +0 -89
  52. package/skills/code-review/agents/openai.yaml +0 -3
  53. package/skills/codebase-design/DEEPENING.md +0 -37
  54. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  55. package/skills/codebase-design/LICENSE +0 -21
  56. package/skills/codebase-design/SKILL.md +0 -114
  57. package/skills/codebase-design/agents/openai.yaml +0 -3
  58. package/skills/diagnosing-bugs/LICENSE +0 -21
  59. package/skills/diagnosing-bugs/SKILL.md +0 -138
  60. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  61. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  62. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  63. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  64. package/skills/domain-modeling/LICENSE +0 -21
  65. package/skills/domain-modeling/SKILL.md +0 -74
  66. package/skills/domain-modeling/agents/openai.yaml +0 -3
  67. package/skills/grill-me/LICENSE +0 -21
  68. package/skills/grill-me/SKILL.md +0 -7
  69. package/skills/grill-me/agents/openai.yaml +0 -5
  70. package/skills/grill-with-docs/LICENSE +0 -21
  71. package/skills/grill-with-docs/SKILL.md +0 -7
  72. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  73. package/skills/grilling/LICENSE +0 -21
  74. package/skills/grilling/SKILL.md +0 -30
  75. package/skills/grilling/agents/openai.yaml +0 -3
  76. package/skills/handoff/LICENSE +0 -21
  77. package/skills/handoff/SKILL.md +0 -16
  78. package/skills/handoff/agents/openai.yaml +0 -5
  79. package/skills/implement/LICENSE +0 -21
  80. package/skills/implement/SKILL.md +0 -17
  81. package/skills/implement/agents/openai.yaml +0 -5
  82. package/skills/implement-spec/LICENSE +0 -21
  83. package/skills/implement-spec/SKILL.md +0 -40
  84. package/skills/implement-spec/agents/openai.yaml +0 -5
  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 -71
  88. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  89. package/skills/pr/CREDITS.md +0 -3
  90. package/skills/pr/LICENSE +0 -21
  91. package/skills/pr/SKILL.md +0 -170
  92. package/skills/pr/agents/openai.yaml +0 -3
  93. package/skills/prototype/LICENSE +0 -21
  94. package/skills/prototype/LOGIC.md +0 -67
  95. package/skills/prototype/SKILL.md +0 -26
  96. package/skills/prototype/UI.md +0 -112
  97. package/skills/prototype/agents/openai.yaml +0 -3
  98. package/skills/research/LICENSE +0 -21
  99. package/skills/research/SKILL.md +0 -12
  100. package/skills/research/agents/openai.yaml +0 -3
  101. package/skills/retro/LICENSE +0 -21
  102. package/skills/retro/SKILL.md +0 -44
  103. package/skills/retro/agents/openai.yaml +0 -5
  104. package/skills/tdd/LICENSE +0 -21
  105. package/skills/tdd/SKILL.md +0 -38
  106. package/skills/tdd/agents/openai.yaml +0 -3
  107. package/skills/tdd/mocking.md +0 -59
  108. package/skills/tdd/tests.md +0 -77
  109. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  110. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  111. package/skills/teach/LICENSE +0 -21
  112. package/skills/teach/MISSION-FORMAT.md +0 -31
  113. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  114. package/skills/teach/SKILL.md +0 -140
  115. package/skills/teach/agents/openai.yaml +0 -5
  116. package/skills/to-questionnaire/LICENSE +0 -21
  117. package/skills/to-questionnaire/SKILL.md +0 -54
  118. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  119. package/skills/to-spec/LICENSE +0 -21
  120. package/skills/to-spec/SKILL.md +0 -75
  121. package/skills/to-spec/agents/openai.yaml +0 -5
  122. package/skills/to-tickets/LICENSE +0 -21
  123. package/skills/to-tickets/SKILL.md +0 -105
  124. package/skills/to-tickets/agents/openai.yaml +0 -5
  125. package/skills/triage/AGENT-BRIEF.md +0 -207
  126. package/skills/triage/LICENSE +0 -21
  127. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  128. package/skills/triage/SKILL.md +0 -112
  129. package/skills/triage/agents/openai.yaml +0 -5
  130. package/skills/wait-what/LICENSE +0 -21
  131. package/skills/wait-what/SKILL.md +0 -7
  132. package/skills/wait-what/agents/openai.yaml +0 -5
  133. package/skills/wayfinder/LICENSE +0 -21
  134. package/skills/wayfinder/SKILL.md +0 -128
  135. package/skills/wayfinder/agents/openai.yaml +0 -5
  136. package/skills/wizard/LICENSE +0 -21
  137. package/skills/wizard/SKILL.md +0 -44
  138. package/skills/wizard/agents/openai.yaml +0 -3
  139. package/skills/wizard/template.sh +0 -223
  140. package/skills/writing-for-agents/LICENSE +0 -21
  141. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  142. package/skills/writing-for-agents/SKILL.md +0 -81
  143. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -1,23 +1,62 @@
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
10
  import { answeredFailure, readerFor } from './events.js';
10
- import { placeFiles, uploadBytes } from './files.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';
20
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
+ }
21
60
  const EXIT_PLAN_MODE = {
22
61
  parse(value) {
23
62
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
@@ -26,26 +65,53 @@ const EXIT_PLAN_MODE = {
26
65
  return value;
27
66
  },
28
67
  };
29
- export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
68
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
30
69
  function causeOf(error) {
31
70
  return error instanceof Error ? error.message : String(error);
32
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
+ }
33
80
  function answerMessage(request, response) {
34
81
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
35
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
+ }
36
89
  function logged(error) {
37
90
  process.stderr.write(`kit: ${causeOf(error)}\n`);
38
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
+ }
39
105
  function marker(bridge) {
40
106
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
41
107
  }
42
108
  class Turn {
43
109
  id = randomUUID();
44
110
  parts;
45
- admitted = false;
46
111
  questions = 0;
47
112
  secrets = 0;
48
113
  prompted = false;
114
+ taken = null;
49
115
  ended = false;
50
116
  images = 0;
51
117
  uploads = [];
@@ -59,6 +125,8 @@ class Conversation {
59
125
  session = null;
60
126
  turn = null;
61
127
  commands = null;
128
+ options = null;
129
+ exitUntold = false;
62
130
  context = null;
63
131
  leftovers = new Set();
64
132
  waiting = [];
@@ -66,6 +134,9 @@ class Conversation {
66
134
  subagents = new Map();
67
135
  held = [];
68
136
  opening = null;
137
+ bridge = null;
138
+ launch = null;
139
+ prompted = null;
69
140
  kills = 0;
70
141
  killed = new AbortController();
71
142
  interrupts = 0;
@@ -73,6 +144,11 @@ class Conversation {
73
144
  open = 0;
74
145
  queue = Promise.resolve();
75
146
  reports = Promise.resolve();
147
+ starting = null;
148
+ preparing = null;
149
+ told = null;
150
+ untold = false;
151
+ shown = null;
76
152
  constructor(id) {
77
153
  this.id = id;
78
154
  }
@@ -88,9 +164,7 @@ function asksSecret(request) {
88
164
  export class Conversations {
89
165
  kit;
90
166
  conversations = new Map();
91
- acks = new Map();
92
167
  questions = new Map();
93
- carrying = 0;
94
168
  reporting = 0;
95
169
  watched = false;
96
170
  constructor(kit) {
@@ -98,38 +172,27 @@ export class Conversations {
98
172
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
99
173
  setInterval(() => this.look(), 2_000);
100
174
  }
101
- input(input) {
102
- this.carrying++;
103
- const carried = this.acks.get(input.input_id) ??
104
- (() => {
105
- const conversation = this.conversation(input.conversation_id);
106
- if (input.kind === 'kill') {
107
- conversation.kills++;
108
- conversation.killed.abort(new Error(KILLED));
109
- for (const waiting of conversation.waiting.splice(0))
110
- waiting.settle({ refused: KILLED });
111
- this.halt(conversation);
112
- }
113
- if (input.kind === 'interrupt') {
114
- conversation.interrupts++;
115
- conversation.interrupted.abort();
116
- }
117
- const handled = conversation.queue.then(() => this.carry(conversation, input));
118
- conversation.queue = handled.catch(() => undefined);
119
- const work = handled.then(({ ack }) => ack);
120
- this.acks.set(input.input_id, work);
121
- return work;
122
- })();
123
- carried
124
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
125
- .catch(logged)
126
- .finally(() => {
127
- this.carrying--;
128
- 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 }));
129
192
  });
130
193
  }
131
194
  idle() {
132
- if (this.carrying > 0 || this.reporting > 0)
195
+ if (this.reporting > 0)
133
196
  return false;
134
197
  for (const conversation of this.conversations.values()) {
135
198
  const turn = conversation.turn;
@@ -158,6 +221,14 @@ export class Conversations {
158
221
  }
159
222
  opened() {
160
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
+ }
161
232
  }
162
233
  watching(watched) {
163
234
  const resend = watched && !this.watched;
@@ -177,63 +248,99 @@ export class Conversations {
177
248
  }
178
249
  return conversation;
179
250
  }
180
- async carry(conversation, input) {
181
- if (input.kind === 'open')
182
- return { ack: Promise.resolve(await this.open(conversation, input)) };
183
- if (input.kind === 'message')
184
- return this.message(conversation, input);
185
- 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') {
186
258
  if (--conversation.interrupts === 0)
187
259
  conversation.interrupted = new AbortController();
188
- await this.interrupt(conversation, String(input.turn_id));
260
+ await this.interrupt(conversation, String(call.turn_id));
189
261
  }
190
- if (input.kind === 'kill') {
262
+ if (call.kind === 'kill') {
191
263
  await this.kill(conversation);
192
264
  await conversation.sending;
193
265
  if (--conversation.kills === 0)
194
266
  conversation.killed = new AbortController();
195
267
  }
196
- if (input.kind === 'option')
197
- await this.option(conversation, String(input.option), input.value);
198
- if (input.kind === 'answer') {
199
- 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));
200
272
  if (question === undefined) {
201
- const text = answerMessage(input.request, input.response);
202
- 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 });
203
275
  }
204
- question.answer(input.response);
276
+ question.answer(call.response);
205
277
  }
206
- return { ack: Promise.resolve({}) };
278
+ return { result: Promise.resolve({}) };
207
279
  }
208
- async open(conversation, input) {
280
+ open(conversation, call) {
209
281
  if (conversation.running !== null)
210
- return {};
211
- try {
212
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
213
- }
214
- catch (error) {
215
- return { refused: causeOf(error) };
216
- }
217
- }
218
- async message(conversation, input) {
219
- 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) }];
220
324
  let route;
325
+ let instructing = null;
221
326
  try {
222
- route = await this.route(conversation, input.agent_id);
223
- const files = input.files;
224
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
225
- const lines = await this.transcribed(conversation, files, paths);
226
- if (lines.length > 0)
227
- 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))));
228
332
  if (conversation.kills > 0)
229
333
  throw new Error(KILLED);
230
334
  }
231
335
  catch (error) {
232
- 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) }) };
233
339
  }
340
+ conversation.preparing = null;
234
341
  return {
235
- ack: new Promise((settle) => {
236
- conversation.waiting.push({ input, route, content, settle });
342
+ result: new Promise((settle) => {
343
+ conversation.waiting.push({ call, route, content, instructing, settle });
237
344
  this.next(conversation);
238
345
  }),
239
346
  };
@@ -242,62 +349,148 @@ export class Conversations {
242
349
  if (conversation.sending !== null || conversation.open > 0)
243
350
  return;
244
351
  const waiting = conversation.waiting.shift();
245
- 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);
246
356
  return;
357
+ }
247
358
  conversation.sending = this.send(conversation, waiting).finally(() => {
248
359
  conversation.sending = null;
249
360
  this.next(conversation);
250
361
  });
251
362
  }
252
363
  async send(conversation, waiting) {
253
- const { input } = waiting;
254
- let ack = {};
364
+ const { call } = waiting;
365
+ let result = {};
366
+ let started = false;
367
+ let instructing = waiting.instructing;
255
368
  const prompt = [];
256
369
  try {
257
370
  if (conversation.running === null) {
258
- const resumed = input.provider_session_id ?? conversation.session;
259
- const opened = await this.start(conversation, waiting.route, resumed);
260
- if (resumed === null)
261
- 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;
262
381
  }
263
- if (input.first === true) {
264
- prompt.push({
265
- type: 'text',
266
- text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
267
- });
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)) });
268
388
  }
269
389
  if (conversation.kills > 0)
270
390
  throw new Error(KILLED);
271
391
  }
272
392
  catch (error) {
273
- if ('provider_session_id' in ack) {
393
+ this.starts(conversation, null);
394
+ if (started) {
274
395
  conversation.session = null;
275
396
  await this.kill(conversation);
276
397
  }
277
- 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);
278
414
  return;
279
415
  }
280
- this.prompt(conversation, [...prompt, ...waiting.content]);
281
- 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);
282
421
  }
283
- async transcribed(conversation, files, paths) {
284
- const lines = [];
285
- for (const [index, path] of paths.entries()) {
286
- lines.push(path);
287
- if (files[index].media_type?.startsWith('audio/') !== true)
288
- continue;
289
- try {
290
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
291
- if (text !== null && text !== '')
292
- lines.push(`Transcript: ${text}`);
293
- }
294
- catch (error) {
295
- if (conversation.kills > 0)
296
- throw error;
297
- 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}` });
298
463
  }
299
464
  }
300
- 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);
301
494
  }
302
495
  async interrupt(conversation, turnId) {
303
496
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -352,7 +545,8 @@ export class Conversations {
352
545
  }
353
546
  return route;
354
547
  }
355
- async start(conversation, route, sessionId) {
548
+ async start(conversation, routed, sessionId, empty, bridged) {
549
+ const route = await routed;
356
550
  const reader = readerFor(route.kind);
357
551
  reader.session = sessionId;
358
552
  const app = client({ name: '@agentshouse/kit' })
@@ -360,56 +554,69 @@ export class Conversations {
360
554
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
361
555
  .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
362
556
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
363
- 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);
364
559
  if (conversation.kills > 0) {
365
- bridge.close();
560
+ this.closeBridge(conversation, bridging, bridge);
366
561
  throw new Error(KILLED);
367
562
  }
368
563
  conversation.opening = bridge;
369
564
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
370
565
  const scope = new Scope(marker(bridge), randomUUID(), cli);
566
+ let adapter = null;
371
567
  try {
372
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
568
+ bridged(bridge);
373
569
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
374
- 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) => {
375
572
  work.read(message);
376
573
  this.read(conversation, reader, message);
377
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
378
- void adapter.exited.then(() => bridge.close());
574
+ }, { ...bridge.env, ...streamed }, undefined, scope));
575
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
379
576
  const agent = adapter.connection.agent;
380
577
  const cwd = route.working_directory;
381
578
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
382
- const opened = sessionId === null
383
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
384
- : { ...(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 })));
385
581
  reader.session = opened.sessionId;
386
- if (sessionId === null && CLIS[route.kind].renames) {
582
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
387
583
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
388
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
584
+ await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
389
585
  }
390
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
391
- 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 ?? []));
392
588
  void work.settled().then(() => scope.record());
393
589
  conversation.running = running;
590
+ adapter.turn(conversation.open > 0);
394
591
  conversation.session = running.sessionId;
395
- this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
396
- if (conversation.commands !== null)
397
- this.commands(conversation, conversation.commands);
398
- this.options(conversation, options);
399
- 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));
400
595
  return running.sessionId;
401
596
  }
402
597
  catch (error) {
403
- scope.kill();
598
+ adapter?.retire();
404
599
  work.close();
405
600
  conversation.leftovers.add(scope);
406
- bridge.close();
601
+ this.closeBridge(conversation, bridging, bridge);
407
602
  throw conversation.kills > 0 ? new Error(KILLED) : error;
408
603
  }
409
604
  finally {
410
605
  conversation.opening = null;
411
606
  }
412
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
+ }
413
620
  async launchSettings(running, route, offered) {
414
621
  let options = offered;
415
622
  const settings = [
@@ -431,27 +638,37 @@ export class Conversations {
431
638
  }
432
639
  return options;
433
640
  }
434
- exited(conversation, adapter, cause) {
435
- const running = conversation.running;
436
- if (running?.adapter !== adapter)
437
- return;
641
+ exited(conversation, running, cause) {
438
642
  this.detach(conversation, running);
439
643
  if (conversation.turn !== null) {
440
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
644
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
441
645
  }
442
646
  this.next(conversation);
443
647
  this.kit.changed();
444
648
  }
445
649
  detach(conversation, running) {
650
+ if (conversation.bridge === running.bridging)
651
+ conversation.bridge = null;
446
652
  running.work.close();
447
653
  conversation.leftovers.add(running.scope);
448
654
  conversation.running = null;
449
655
  conversation.commands = null;
656
+ conversation.options = null;
450
657
  for (const [id, question] of this.questions) {
451
658
  if (question.conversation === conversation)
452
659
  this.questions.delete(id);
453
660
  }
454
- 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);
455
672
  }
456
673
  commands(conversation, commands) {
457
674
  conversation.commands = commands;
@@ -462,6 +679,7 @@ export class Conversations {
462
679
  options(conversation, options) {
463
680
  if (conversation.running === null)
464
681
  return;
682
+ conversation.options = options;
465
683
  this.kit.send({ type: 'options', conversation_id: conversation.id, options });
466
684
  }
467
685
  deliver(conversation, turn, event, body) {
@@ -476,7 +694,7 @@ export class Conversations {
476
694
  return delivered;
477
695
  }
478
696
  write(conversation, turn, operation) {
479
- if (!turn.admitted || turn.ended || !this.watched)
697
+ if (turn.ended || !this.watched)
480
698
  return;
481
699
  this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
482
700
  }
@@ -487,12 +705,8 @@ export class Conversations {
487
705
  begin(conversation) {
488
706
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
489
707
  conversation.open++;
490
- void this.deliver(conversation, turn, 'started', {})
491
- .then(() => {
492
- turn.admitted = true;
493
- this.replay(conversation, turn);
494
- })
495
- .catch(logged);
708
+ conversation.running?.adapter.turn(true);
709
+ void this.deliver(conversation, turn, 'started', {}).catch(logged);
496
710
  conversation.turn = turn;
497
711
  if (conversation.context !== null)
498
712
  turn.parts.measure(conversation.context);
@@ -504,6 +718,8 @@ export class Conversations {
504
718
  end(conversation, turn, failed, notice = null) {
505
719
  if (turn.ended)
506
720
  return;
721
+ if (failed !== null)
722
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
507
723
  if (conversation.turn === turn) {
508
724
  for (const event of conversation.running?.reader.closeTurn() ?? [])
509
725
  this.apply(conversation, turn, event);
@@ -512,40 +728,51 @@ export class Conversations {
512
728
  turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
513
729
  turn.ended = true;
514
730
  conversation.open--;
731
+ conversation.running?.adapter.turn(conversation.open > 0);
515
732
  if (conversation.turn === turn)
516
733
  conversation.turn = null;
517
734
  const body = Promise.allSettled(turn.uploads).then(() => ({
518
735
  parts: turn.parts.saved(),
519
736
  context: turn.parts.context,
520
- ...failed,
737
+ ...(failed === null ? {} : { failed: failed.failure }),
521
738
  }));
522
739
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
523
740
  this.next(conversation);
524
741
  this.kit.changed();
525
742
  }
526
- prompt(conversation, prompt) {
743
+ prompt(conversation, prompt, taken) {
527
744
  const running = conversation.running;
528
745
  const turn = this.begin(conversation);
529
746
  turn.prompted = true;
747
+ turn.taken = taken;
748
+ conversation.prompted = running.sessionId;
530
749
  running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
750
+ this.took(turn);
531
751
  const notice = answeredFailure(answer);
532
- const failed = notice ?? (answer.stopReason === 'refusal' ? REFUSED : null);
533
- this.end(conversation, turn, failed === null ? null : { failed }, notice);
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
+ }
534
758
  }, async (error) => {
535
759
  if (running.adapter.connection.signal.aborted) {
536
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
760
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
537
761
  }
538
762
  else {
763
+ this.took(turn);
539
764
  if (conversation.open === 1 && conversation.running === running) {
540
- running.scope.kill();
765
+ running.adapter.retire();
541
766
  this.detach(conversation, running);
542
767
  }
543
- this.end(conversation, turn, { failed: causeOf(error) });
768
+ this.end(conversation, turn, answeredWith(error));
544
769
  }
545
770
  });
546
771
  }
547
772
  started(conversation) {
548
- return conversation.turn ?? this.begin(conversation);
773
+ const turn = conversation.turn ?? this.begin(conversation);
774
+ this.took(turn);
775
+ return turn;
549
776
  }
550
777
  ended(conversation) {
551
778
  const turn = conversation.turn;
@@ -640,6 +867,11 @@ export class Conversations {
640
867
  }
641
868
  this.apply(conversation, this.started(conversation), event);
642
869
  }
870
+ took(turn) {
871
+ const taken = turn.taken;
872
+ turn.taken = null;
873
+ taken?.();
874
+ }
643
875
  apply(conversation, turn, event) {
644
876
  switch (event.kind) {
645
877
  case 'text':
@@ -687,20 +919,27 @@ export class Conversations {
687
919
  async image(conversation, turn, reserved, data, mediaType) {
688
920
  const bytes = Buffer.from(data, 'base64');
689
921
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
922
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
690
923
  try {
691
924
  const declared = (await this.deliver(conversation, turn, 'images', {
692
925
  name,
693
926
  media_type: mediaType,
694
927
  bytes: bytes.length,
695
- sha256: createHash('sha256').update(bytes).digest('hex'),
928
+ sha256,
696
929
  }));
697
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
698
- if (saved.save.status !== 'saved')
699
- throw new Error(saved.save.failure ?? saved.save.status);
700
- turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
930
+ const saved = await uploaded(declared.upload, {
931
+ name,
932
+ bytes: bytes.length,
933
+ sha256,
934
+ read: (start, end) => [bytes.subarray(start, end)],
935
+ send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
936
+ finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
937
+ });
938
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
701
939
  }
702
940
  catch (error) {
703
- turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
941
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
942
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
704
943
  }
705
944
  }
706
945
  }