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