@agentshouse/kit 0.2.1-c2997cbe74597 → 0.2.1-c2eef9f27f3ab

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