@agentshouse/kit 0.2.1-c64c31420c9bf → 0.2.1-c6ec4d4d781e7
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.
- package/README.md +20 -20
- package/dist/acp.js +36 -16
- package/dist/agents.js +36 -16
- package/dist/api.js +5 -5
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +1 -1
- package/dist/calls.js +63 -0
- package/dist/clis.js +10 -1
- package/dist/{stream.js → connection.js} +32 -30
- package/dist/conversations.js +354 -149
- package/dist/copies.js +62 -43
- package/dist/end.js +6 -2
- package/dist/events.js +4 -3
- package/dist/files.js +123 -37
- package/dist/git.js +1 -1
- package/dist/house-cli.js +95 -59
- package/dist/instructions.js +2 -1
- package/dist/kit-cli.js +2 -2
- package/dist/mcp.js +4 -2
- package/dist/placement.js +11 -2
- package/dist/resident.js +32 -28
- package/dist/scope.js +3 -0
- package/dist/service.js +23 -1
- package/dist/sign-in.js +19 -37
- package/dist/skills.js +3 -3
- package/dist/transcription.js +51 -20
- package/dist/translate.js +2 -8
- package/dist/update.js +42 -45
- package/dist/wire.js +19 -3
- package/package.json +3 -3
package/dist/conversations.js
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
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 {
|
|
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
19
|
import { createHowWeWork } from './skills.js';
|
|
17
20
|
import { transcribe } from './transcription.js';
|
|
18
21
|
import { workFor } from './work.js';
|
|
22
|
+
const SIGNED_OUT = RequestError.authRequired().code;
|
|
19
23
|
const KILLED = 'the conversation was killed';
|
|
24
|
+
const INTERRUPTED = 'the conversation was interrupted';
|
|
20
25
|
const REFUSED = 'the CLI refused the prompt';
|
|
21
|
-
const NO_CONNECTION = "the
|
|
26
|
+
const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
|
|
27
|
+
const GONE = 'House no longer holds this conversation; start a new one';
|
|
28
|
+
const RETIRED = "this conversation's Agent is retired, so it cannot start";
|
|
29
|
+
const ENDED = {
|
|
30
|
+
conversation_not_found: GONE,
|
|
31
|
+
conversation_historical: RETIRED,
|
|
32
|
+
};
|
|
22
33
|
const NO_START = "the Agent's CLI did not start";
|
|
23
34
|
const NO_SESSION = "the Agent's CLI did not open a session";
|
|
24
35
|
const NO_RESUME = "the Agent's CLI could not resume this conversation";
|
|
36
|
+
const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
|
|
25
37
|
const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
|
|
26
38
|
class NotStarted extends Error {
|
|
27
39
|
}
|
|
@@ -30,9 +42,20 @@ async function step(reason, work) {
|
|
|
30
42
|
return await work;
|
|
31
43
|
}
|
|
32
44
|
catch (error) {
|
|
33
|
-
throw new NotStarted(reason, { cause: error });
|
|
45
|
+
throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
|
|
34
46
|
}
|
|
35
47
|
}
|
|
48
|
+
function signedOut(error) {
|
|
49
|
+
return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
|
|
50
|
+
}
|
|
51
|
+
function unopened(reason) {
|
|
52
|
+
return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
|
|
53
|
+
}
|
|
54
|
+
// A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
|
|
55
|
+
const STARTING_SHOWN_AFTER_MS = 1_000;
|
|
56
|
+
function unbridged(error) {
|
|
57
|
+
return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
|
|
58
|
+
}
|
|
36
59
|
const EXIT_PLAN_MODE = {
|
|
37
60
|
parse(value) {
|
|
38
61
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
@@ -41,33 +64,51 @@ const EXIT_PLAN_MODE = {
|
|
|
41
64
|
return value;
|
|
42
65
|
},
|
|
43
66
|
};
|
|
44
|
-
export const
|
|
67
|
+
export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
|
|
45
68
|
function causeOf(error) {
|
|
46
69
|
return error instanceof Error ? error.message : String(error);
|
|
47
70
|
}
|
|
71
|
+
function answeredWith(error) {
|
|
72
|
+
return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
73
|
+
}
|
|
74
|
+
function exitedWith(running, cause) {
|
|
75
|
+
return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
|
|
76
|
+
}
|
|
48
77
|
function answerMessage(request, response) {
|
|
49
78
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
50
79
|
}
|
|
80
|
+
async function sentPart(url, bytes) {
|
|
81
|
+
const answer = await fetch(relayed(url), { method: 'PUT', body: bytes });
|
|
82
|
+
await answer.arrayBuffer();
|
|
83
|
+
if (!answer.ok)
|
|
84
|
+
throw storeRefusal('upload', answer.status);
|
|
85
|
+
}
|
|
51
86
|
function logged(error) {
|
|
52
87
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
53
88
|
}
|
|
54
89
|
function refusal(conversation, error) {
|
|
55
90
|
if (conversation.kills > 0)
|
|
56
91
|
return KILLED;
|
|
57
|
-
const cause = error instanceof
|
|
92
|
+
const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
58
93
|
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
59
94
|
return causeOf(error);
|
|
60
95
|
}
|
|
96
|
+
function audioOf(result) {
|
|
97
|
+
if (result.isError === true)
|
|
98
|
+
return null;
|
|
99
|
+
const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
|
|
100
|
+
return file?.media_type?.startsWith('audio/') === true ? file : null;
|
|
101
|
+
}
|
|
61
102
|
function marker(bridge) {
|
|
62
103
|
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
63
104
|
}
|
|
64
105
|
class Turn {
|
|
65
106
|
id = randomUUID();
|
|
66
107
|
parts;
|
|
67
|
-
admitted = false;
|
|
68
108
|
questions = 0;
|
|
69
109
|
secrets = 0;
|
|
70
110
|
prompted = false;
|
|
111
|
+
taken = null;
|
|
71
112
|
ended = false;
|
|
72
113
|
images = 0;
|
|
73
114
|
uploads = [];
|
|
@@ -81,6 +122,8 @@ class Conversation {
|
|
|
81
122
|
session = null;
|
|
82
123
|
turn = null;
|
|
83
124
|
commands = null;
|
|
125
|
+
options = null;
|
|
126
|
+
exitUntold = false;
|
|
84
127
|
context = null;
|
|
85
128
|
leftovers = new Set();
|
|
86
129
|
waiting = [];
|
|
@@ -88,6 +131,9 @@ class Conversation {
|
|
|
88
131
|
subagents = new Map();
|
|
89
132
|
held = [];
|
|
90
133
|
opening = null;
|
|
134
|
+
bridge = null;
|
|
135
|
+
launch = null;
|
|
136
|
+
prompted = null;
|
|
91
137
|
kills = 0;
|
|
92
138
|
killed = new AbortController();
|
|
93
139
|
interrupts = 0;
|
|
@@ -95,6 +141,11 @@ class Conversation {
|
|
|
95
141
|
open = 0;
|
|
96
142
|
queue = Promise.resolve();
|
|
97
143
|
reports = Promise.resolve();
|
|
144
|
+
starting = null;
|
|
145
|
+
preparing = null;
|
|
146
|
+
told = null;
|
|
147
|
+
untold = false;
|
|
148
|
+
shown = null;
|
|
98
149
|
constructor(id) {
|
|
99
150
|
this.id = id;
|
|
100
151
|
}
|
|
@@ -110,9 +161,7 @@ function asksSecret(request) {
|
|
|
110
161
|
export class Conversations {
|
|
111
162
|
kit;
|
|
112
163
|
conversations = new Map();
|
|
113
|
-
acks = new Map();
|
|
114
164
|
questions = new Map();
|
|
115
|
-
carrying = 0;
|
|
116
165
|
reporting = 0;
|
|
117
166
|
watched = false;
|
|
118
167
|
constructor(kit) {
|
|
@@ -120,38 +169,27 @@ export class Conversations {
|
|
|
120
169
|
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
121
170
|
setInterval(() => this.look(), 2_000);
|
|
122
171
|
}
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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();
|
|
172
|
+
call(call) {
|
|
173
|
+
void this.kit.calls.carry(call.input_id, () => {
|
|
174
|
+
const conversation = this.conversation(call.conversation_id);
|
|
175
|
+
if (call.kind === 'kill') {
|
|
176
|
+
conversation.kills++;
|
|
177
|
+
conversation.killed.abort(new Error(KILLED));
|
|
178
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
179
|
+
waiting.settle({ refused: KILLED });
|
|
180
|
+
this.halt(conversation);
|
|
181
|
+
}
|
|
182
|
+
if (call.kind === 'interrupt') {
|
|
183
|
+
conversation.interrupts++;
|
|
184
|
+
conversation.interrupted.abort();
|
|
185
|
+
}
|
|
186
|
+
const handled = conversation.queue.then(() => this.carry(conversation, call));
|
|
187
|
+
conversation.queue = handled.catch(() => undefined);
|
|
188
|
+
return handled.then(async ({ result }) => ({ result: await result }));
|
|
151
189
|
});
|
|
152
190
|
}
|
|
153
191
|
idle() {
|
|
154
|
-
if (this.
|
|
192
|
+
if (this.reporting > 0)
|
|
155
193
|
return false;
|
|
156
194
|
for (const conversation of this.conversations.values()) {
|
|
157
195
|
const turn = conversation.turn;
|
|
@@ -180,6 +218,14 @@ export class Conversations {
|
|
|
180
218
|
}
|
|
181
219
|
opened() {
|
|
182
220
|
this.watched = false;
|
|
221
|
+
for (const conversation of this.conversations.values()) {
|
|
222
|
+
if (conversation.running !== null)
|
|
223
|
+
this.facts(conversation);
|
|
224
|
+
else if (conversation.exitUntold)
|
|
225
|
+
this.process(conversation, false);
|
|
226
|
+
if (conversation.untold || conversation.told !== null)
|
|
227
|
+
this.retell(conversation);
|
|
228
|
+
}
|
|
183
229
|
}
|
|
184
230
|
watching(watched) {
|
|
185
231
|
const resend = watched && !this.watched;
|
|
@@ -199,63 +245,99 @@ export class Conversations {
|
|
|
199
245
|
}
|
|
200
246
|
return conversation;
|
|
201
247
|
}
|
|
202
|
-
async carry(conversation,
|
|
203
|
-
if (
|
|
204
|
-
return {
|
|
205
|
-
if (
|
|
206
|
-
return this.message(conversation,
|
|
207
|
-
|
|
248
|
+
async carry(conversation, call) {
|
|
249
|
+
if (call.kind === 'open')
|
|
250
|
+
return { result: this.open(conversation, call) };
|
|
251
|
+
if (call.kind === 'message')
|
|
252
|
+
return this.message(conversation, call);
|
|
253
|
+
await this.launched(conversation);
|
|
254
|
+
if (call.kind === 'interrupt') {
|
|
208
255
|
if (--conversation.interrupts === 0)
|
|
209
256
|
conversation.interrupted = new AbortController();
|
|
210
|
-
await this.interrupt(conversation, String(
|
|
257
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
211
258
|
}
|
|
212
|
-
if (
|
|
259
|
+
if (call.kind === 'kill') {
|
|
213
260
|
await this.kill(conversation);
|
|
214
261
|
await conversation.sending;
|
|
215
262
|
if (--conversation.kills === 0)
|
|
216
263
|
conversation.killed = new AbortController();
|
|
217
264
|
}
|
|
218
|
-
if (
|
|
219
|
-
await this.option(conversation, String(
|
|
220
|
-
if (
|
|
221
|
-
const question = this.questions.get(String(
|
|
265
|
+
if (call.kind === 'option')
|
|
266
|
+
await this.option(conversation, String(call.option), call.value);
|
|
267
|
+
if (call.kind === 'answer') {
|
|
268
|
+
const question = this.questions.get(String(call.interaction_id));
|
|
222
269
|
if (question === undefined) {
|
|
223
|
-
const text = answerMessage(
|
|
224
|
-
return this.message(conversation, { ...
|
|
270
|
+
const text = answerMessage(call.request, call.response);
|
|
271
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
225
272
|
}
|
|
226
|
-
question.answer(
|
|
273
|
+
question.answer(call.response);
|
|
227
274
|
}
|
|
228
|
-
return {
|
|
275
|
+
return { result: Promise.resolve({}) };
|
|
229
276
|
}
|
|
230
|
-
|
|
277
|
+
open(conversation, call) {
|
|
231
278
|
if (conversation.running !== null)
|
|
232
|
-
return {};
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
279
|
+
return Promise.resolve({});
|
|
280
|
+
const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
|
|
281
|
+
return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
|
|
282
|
+
}
|
|
283
|
+
launch(conversation, route, sessionId, empty) {
|
|
284
|
+
let opened;
|
|
285
|
+
let failed;
|
|
286
|
+
const bridge = new Promise((resolve, reject) => {
|
|
287
|
+
opened = resolve;
|
|
288
|
+
failed = reject;
|
|
289
|
+
});
|
|
290
|
+
bridge.catch(() => undefined);
|
|
291
|
+
const session = this.start(conversation, route, sessionId, empty, opened);
|
|
292
|
+
session.catch(failed);
|
|
293
|
+
const launch = { bridge, session };
|
|
294
|
+
conversation.launch = launch;
|
|
295
|
+
void session
|
|
296
|
+
.catch(() => undefined)
|
|
297
|
+
.then(() => {
|
|
298
|
+
if (conversation.launch !== launch)
|
|
299
|
+
return;
|
|
300
|
+
conversation.launch = null;
|
|
301
|
+
this.closeIdleBridge(conversation);
|
|
302
|
+
this.next(conversation);
|
|
303
|
+
});
|
|
304
|
+
this.next(conversation);
|
|
305
|
+
return launch;
|
|
306
|
+
}
|
|
307
|
+
async launched(conversation) {
|
|
308
|
+
while (conversation.launch !== null)
|
|
309
|
+
await conversation.launch.session.catch(() => undefined);
|
|
310
|
+
}
|
|
311
|
+
instructed(conversation, route) {
|
|
312
|
+
const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
|
|
313
|
+
if (bridge === undefined)
|
|
314
|
+
return null;
|
|
315
|
+
const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
|
|
316
|
+
read.catch(() => undefined);
|
|
317
|
+
return read;
|
|
318
|
+
}
|
|
319
|
+
async message(conversation, call) {
|
|
320
|
+
const content = [{ type: 'text', text: String(call.text) }];
|
|
242
321
|
let route;
|
|
322
|
+
let instructing = null;
|
|
243
323
|
try {
|
|
244
|
-
route = await this.route(conversation,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
content.push({ type: 'text', text: lines.join('\n') });
|
|
324
|
+
route = await this.route(conversation, call.agent_id);
|
|
325
|
+
if (call.first === true)
|
|
326
|
+
instructing = this.instructed(conversation, route);
|
|
327
|
+
if (call.message_id !== null)
|
|
328
|
+
content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
|
|
250
329
|
if (conversation.kills > 0)
|
|
251
330
|
throw new Error(KILLED);
|
|
252
331
|
}
|
|
253
332
|
catch (error) {
|
|
254
|
-
|
|
333
|
+
this.closeIdleBridge(conversation);
|
|
334
|
+
this.prepares(conversation, null);
|
|
335
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
255
336
|
}
|
|
337
|
+
conversation.preparing = null;
|
|
256
338
|
return {
|
|
257
|
-
|
|
258
|
-
conversation.waiting.push({
|
|
339
|
+
result: new Promise((settle) => {
|
|
340
|
+
conversation.waiting.push({ call, route, content, instructing, settle });
|
|
259
341
|
this.next(conversation);
|
|
260
342
|
}),
|
|
261
343
|
};
|
|
@@ -264,62 +346,146 @@ export class Conversations {
|
|
|
264
346
|
if (conversation.sending !== null || conversation.open > 0)
|
|
265
347
|
return;
|
|
266
348
|
const waiting = conversation.waiting.shift();
|
|
267
|
-
if (waiting === undefined)
|
|
349
|
+
if (waiting === undefined) {
|
|
350
|
+
const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
|
|
351
|
+
if (conversation.starting !== step)
|
|
352
|
+
this.starts(conversation, step);
|
|
268
353
|
return;
|
|
354
|
+
}
|
|
269
355
|
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
270
356
|
conversation.sending = null;
|
|
271
357
|
this.next(conversation);
|
|
272
358
|
});
|
|
273
359
|
}
|
|
274
360
|
async send(conversation, waiting) {
|
|
275
|
-
const {
|
|
276
|
-
let
|
|
361
|
+
const { call } = waiting;
|
|
362
|
+
let result = {};
|
|
363
|
+
let started = false;
|
|
364
|
+
let instructing = waiting.instructing;
|
|
277
365
|
const prompt = [];
|
|
278
366
|
try {
|
|
279
367
|
if (conversation.running === null) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (resumed === null)
|
|
283
|
-
ack = { provider_session_id: opened };
|
|
368
|
+
this.starts(conversation, 'session');
|
|
369
|
+
await this.launched(conversation);
|
|
284
370
|
}
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
371
|
+
if (conversation.running === null) {
|
|
372
|
+
const stored = conversation.session ?? call.provider_session_id;
|
|
373
|
+
const empty = call.first === true && conversation.prompted !== stored;
|
|
374
|
+
const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
|
|
375
|
+
if (call.first === true)
|
|
376
|
+
instructing = this.instructed(conversation, waiting.route);
|
|
377
|
+
started = (await launch.session) !== stored;
|
|
378
|
+
}
|
|
379
|
+
const session = conversation.running.sessionId;
|
|
380
|
+
if (started || (call.first === true && session !== call.provider_session_id)) {
|
|
381
|
+
result = { provider_session_id: session };
|
|
382
|
+
}
|
|
383
|
+
if (call.first === true) {
|
|
384
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
290
385
|
}
|
|
291
386
|
if (conversation.kills > 0)
|
|
292
387
|
throw new Error(KILLED);
|
|
293
388
|
}
|
|
294
389
|
catch (error) {
|
|
295
|
-
|
|
390
|
+
this.starts(conversation, null);
|
|
391
|
+
if (started) {
|
|
296
392
|
conversation.session = null;
|
|
297
393
|
await this.kill(conversation);
|
|
298
394
|
}
|
|
299
395
|
waiting.settle({ refused: refusal(conversation, error) });
|
|
300
396
|
return;
|
|
301
397
|
}
|
|
302
|
-
this.
|
|
303
|
-
waiting.
|
|
398
|
+
this.starts(conversation, null);
|
|
399
|
+
this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
|
|
400
|
+
waiting.settle(result);
|
|
401
|
+
}
|
|
402
|
+
starts(conversation, step) {
|
|
403
|
+
conversation.starting = step;
|
|
404
|
+
if (conversation.shown !== null)
|
|
405
|
+
clearTimeout(conversation.shown);
|
|
406
|
+
conversation.shown = null;
|
|
407
|
+
if (step === null || conversation.told !== null) {
|
|
408
|
+
this.tell(conversation, step);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
conversation.shown = setTimeout(() => {
|
|
412
|
+
conversation.shown = null;
|
|
413
|
+
if (conversation.starting === step)
|
|
414
|
+
this.tell(conversation, step);
|
|
415
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
304
416
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
417
|
+
prepares(conversation, step) {
|
|
418
|
+
conversation.preparing = step;
|
|
419
|
+
this.next(conversation);
|
|
420
|
+
}
|
|
421
|
+
tell(conversation, step) {
|
|
422
|
+
if (conversation.told === step && !conversation.untold)
|
|
423
|
+
return;
|
|
424
|
+
conversation.told = step;
|
|
425
|
+
this.retell(conversation);
|
|
426
|
+
}
|
|
427
|
+
retell(conversation) {
|
|
428
|
+
conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
|
|
429
|
+
}
|
|
430
|
+
async transcribed(conversation, messageId, text) {
|
|
431
|
+
const paths = linkedPaths(text);
|
|
432
|
+
if (paths.length === 0)
|
|
433
|
+
return [];
|
|
434
|
+
const killed = conversation.killed.signal;
|
|
435
|
+
const interrupted = conversation.interrupted.signal;
|
|
436
|
+
const transcribing = AbortSignal.any([killed, interrupted]);
|
|
437
|
+
let signal = killed;
|
|
438
|
+
const blocks = [];
|
|
439
|
+
try {
|
|
440
|
+
const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
|
|
441
|
+
const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
|
|
442
|
+
for (const path of paths) {
|
|
443
|
+
let answered = await read(path);
|
|
444
|
+
if (answered === null)
|
|
445
|
+
continue;
|
|
446
|
+
signal = transcribing;
|
|
447
|
+
this.prepares(conversation, 'transcription');
|
|
448
|
+
const said = await transcribe(async () => {
|
|
449
|
+
const address = answered ?? (await read(path));
|
|
450
|
+
answered = null;
|
|
451
|
+
if (address === null)
|
|
452
|
+
throw new Error(`${path} is no longer an audio file`);
|
|
453
|
+
return address;
|
|
454
|
+
}, signal);
|
|
455
|
+
const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
|
|
456
|
+
await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
|
|
457
|
+
blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
|
|
320
458
|
}
|
|
321
459
|
}
|
|
322
|
-
|
|
460
|
+
catch (error) {
|
|
461
|
+
if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
|
|
462
|
+
throw new NotStarted(INTERRUPTED, { cause: error });
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
return blocks;
|
|
466
|
+
}
|
|
467
|
+
bridged(conversation) {
|
|
468
|
+
if (conversation.bridge === null) {
|
|
469
|
+
const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
470
|
+
conversation.bridge = bridging;
|
|
471
|
+
bridging.catch(() => {
|
|
472
|
+
if (conversation.bridge === bridging)
|
|
473
|
+
conversation.bridge = null;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return conversation.bridge;
|
|
477
|
+
}
|
|
478
|
+
closeBridge(conversation, bridging, bridge) {
|
|
479
|
+
bridge.close();
|
|
480
|
+
if (conversation.bridge === bridging)
|
|
481
|
+
conversation.bridge = null;
|
|
482
|
+
}
|
|
483
|
+
closeIdleBridge(conversation) {
|
|
484
|
+
const bridging = conversation.bridge;
|
|
485
|
+
if (bridging === null || conversation.running !== null || conversation.launch !== null)
|
|
486
|
+
return;
|
|
487
|
+
conversation.bridge = null;
|
|
488
|
+
void bridging.then((bridge) => bridge.close(), () => undefined);
|
|
323
489
|
}
|
|
324
490
|
async interrupt(conversation, turnId) {
|
|
325
491
|
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
@@ -374,7 +540,8 @@ export class Conversations {
|
|
|
374
540
|
}
|
|
375
541
|
return route;
|
|
376
542
|
}
|
|
377
|
-
async start(conversation,
|
|
543
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
544
|
+
const route = await routed;
|
|
378
545
|
const reader = readerFor(route.kind);
|
|
379
546
|
reader.session = sessionId;
|
|
380
547
|
const app = client({ name: '@agentshouse/kit' })
|
|
@@ -382,57 +549,69 @@ export class Conversations {
|
|
|
382
549
|
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
383
550
|
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
|
|
384
551
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
385
|
-
const
|
|
552
|
+
const bridging = this.bridged(conversation);
|
|
553
|
+
const bridge = await step(unbridged, bridging);
|
|
386
554
|
if (conversation.kills > 0) {
|
|
387
|
-
|
|
555
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
388
556
|
throw new Error(KILLED);
|
|
389
557
|
}
|
|
390
558
|
conversation.opening = bridge;
|
|
391
559
|
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
392
560
|
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
561
|
+
let adapter = null;
|
|
393
562
|
try {
|
|
394
563
|
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
564
|
+
bridged(bridge);
|
|
395
565
|
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
396
566
|
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
397
|
-
|
|
567
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
398
568
|
work.read(message);
|
|
399
569
|
this.read(conversation, reader, message);
|
|
400
570
|
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
401
|
-
void adapter.exited.then(() =>
|
|
571
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
402
572
|
const agent = adapter.connection.agent;
|
|
403
573
|
const cwd = route.working_directory;
|
|
404
574
|
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
: { ...(await step(NO_RESUME, agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta }))), sessionId };
|
|
575
|
+
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
576
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
408
577
|
reader.session = opened.sessionId;
|
|
409
|
-
if (sessionId
|
|
578
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
410
579
|
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
411
|
-
await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
580
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
412
581
|
}
|
|
413
|
-
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
582
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
414
583
|
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
415
584
|
void work.settled().then(() => scope.record());
|
|
416
585
|
conversation.running = running;
|
|
417
586
|
conversation.session = running.sessionId;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
this.options(conversation, options);
|
|
422
|
-
void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
|
|
587
|
+
conversation.options = options;
|
|
588
|
+
this.facts(conversation);
|
|
589
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
423
590
|
return running.sessionId;
|
|
424
591
|
}
|
|
425
592
|
catch (error) {
|
|
426
|
-
|
|
593
|
+
adapter?.retire();
|
|
427
594
|
work.close();
|
|
428
595
|
conversation.leftovers.add(scope);
|
|
429
|
-
|
|
596
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
430
597
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
431
598
|
}
|
|
432
599
|
finally {
|
|
433
600
|
conversation.opening = null;
|
|
434
601
|
}
|
|
435
602
|
}
|
|
603
|
+
async resume(conversation, kind, adapter, request, empty) {
|
|
604
|
+
try {
|
|
605
|
+
return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
609
|
+
throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
|
|
610
|
+
}
|
|
611
|
+
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
436
615
|
async launchSettings(running, route, offered) {
|
|
437
616
|
let options = offered;
|
|
438
617
|
const settings = [
|
|
@@ -454,27 +633,37 @@ export class Conversations {
|
|
|
454
633
|
}
|
|
455
634
|
return options;
|
|
456
635
|
}
|
|
457
|
-
exited(conversation,
|
|
458
|
-
const running = conversation.running;
|
|
459
|
-
if (running?.adapter !== adapter)
|
|
460
|
-
return;
|
|
636
|
+
exited(conversation, running, cause) {
|
|
461
637
|
this.detach(conversation, running);
|
|
462
638
|
if (conversation.turn !== null) {
|
|
463
|
-
this.end(conversation, conversation.turn,
|
|
639
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
464
640
|
}
|
|
465
641
|
this.next(conversation);
|
|
466
642
|
this.kit.changed();
|
|
467
643
|
}
|
|
468
644
|
detach(conversation, running) {
|
|
645
|
+
if (conversation.bridge === running.bridging)
|
|
646
|
+
conversation.bridge = null;
|
|
469
647
|
running.work.close();
|
|
470
648
|
conversation.leftovers.add(running.scope);
|
|
471
649
|
conversation.running = null;
|
|
472
650
|
conversation.commands = null;
|
|
651
|
+
conversation.options = null;
|
|
473
652
|
for (const [id, question] of this.questions) {
|
|
474
653
|
if (question.conversation === conversation)
|
|
475
654
|
this.questions.delete(id);
|
|
476
655
|
}
|
|
477
|
-
this.
|
|
656
|
+
this.process(conversation, false);
|
|
657
|
+
}
|
|
658
|
+
process(conversation, running) {
|
|
659
|
+
conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
|
|
660
|
+
}
|
|
661
|
+
facts(conversation) {
|
|
662
|
+
this.process(conversation, true);
|
|
663
|
+
if (conversation.commands !== null)
|
|
664
|
+
this.commands(conversation, conversation.commands);
|
|
665
|
+
if (conversation.options !== null)
|
|
666
|
+
this.options(conversation, conversation.options);
|
|
478
667
|
}
|
|
479
668
|
commands(conversation, commands) {
|
|
480
669
|
conversation.commands = commands;
|
|
@@ -485,6 +674,7 @@ export class Conversations {
|
|
|
485
674
|
options(conversation, options) {
|
|
486
675
|
if (conversation.running === null)
|
|
487
676
|
return;
|
|
677
|
+
conversation.options = options;
|
|
488
678
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
489
679
|
}
|
|
490
680
|
deliver(conversation, turn, event, body) {
|
|
@@ -499,7 +689,7 @@ export class Conversations {
|
|
|
499
689
|
return delivered;
|
|
500
690
|
}
|
|
501
691
|
write(conversation, turn, operation) {
|
|
502
|
-
if (
|
|
692
|
+
if (turn.ended || !this.watched)
|
|
503
693
|
return;
|
|
504
694
|
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
505
695
|
}
|
|
@@ -510,12 +700,7 @@ export class Conversations {
|
|
|
510
700
|
begin(conversation) {
|
|
511
701
|
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
512
702
|
conversation.open++;
|
|
513
|
-
void this.deliver(conversation, turn, 'started', {})
|
|
514
|
-
.then(() => {
|
|
515
|
-
turn.admitted = true;
|
|
516
|
-
this.replay(conversation, turn);
|
|
517
|
-
})
|
|
518
|
-
.catch(logged);
|
|
703
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
519
704
|
conversation.turn = turn;
|
|
520
705
|
if (conversation.context !== null)
|
|
521
706
|
turn.parts.measure(conversation.context);
|
|
@@ -527,6 +712,8 @@ export class Conversations {
|
|
|
527
712
|
end(conversation, turn, failed, notice = null) {
|
|
528
713
|
if (turn.ended)
|
|
529
714
|
return;
|
|
715
|
+
if (failed !== null)
|
|
716
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
530
717
|
if (conversation.turn === turn) {
|
|
531
718
|
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
532
719
|
this.apply(conversation, turn, event);
|
|
@@ -540,35 +727,45 @@ export class Conversations {
|
|
|
540
727
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
541
728
|
parts: turn.parts.saved(),
|
|
542
729
|
context: turn.parts.context,
|
|
543
|
-
...failed,
|
|
730
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
544
731
|
}));
|
|
545
732
|
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
546
733
|
this.next(conversation);
|
|
547
734
|
this.kit.changed();
|
|
548
735
|
}
|
|
549
|
-
prompt(conversation, prompt) {
|
|
736
|
+
prompt(conversation, prompt, taken) {
|
|
550
737
|
const running = conversation.running;
|
|
551
738
|
const turn = this.begin(conversation);
|
|
552
739
|
turn.prompted = true;
|
|
740
|
+
turn.taken = taken;
|
|
741
|
+
conversation.prompted = running.sessionId;
|
|
553
742
|
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
743
|
+
this.took(turn);
|
|
554
744
|
const notice = answeredFailure(answer);
|
|
555
|
-
|
|
556
|
-
|
|
745
|
+
if (notice !== null) {
|
|
746
|
+
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
750
|
+
}
|
|
557
751
|
}, async (error) => {
|
|
558
752
|
if (running.adapter.connection.signal.aborted) {
|
|
559
|
-
this.end(conversation, turn,
|
|
753
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
560
754
|
}
|
|
561
755
|
else {
|
|
756
|
+
this.took(turn);
|
|
562
757
|
if (conversation.open === 1 && conversation.running === running) {
|
|
563
|
-
running.
|
|
758
|
+
running.adapter.retire();
|
|
564
759
|
this.detach(conversation, running);
|
|
565
760
|
}
|
|
566
|
-
this.end(conversation, turn,
|
|
761
|
+
this.end(conversation, turn, answeredWith(error));
|
|
567
762
|
}
|
|
568
763
|
});
|
|
569
764
|
}
|
|
570
765
|
started(conversation) {
|
|
571
|
-
|
|
766
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
767
|
+
this.took(turn);
|
|
768
|
+
return turn;
|
|
572
769
|
}
|
|
573
770
|
ended(conversation) {
|
|
574
771
|
const turn = conversation.turn;
|
|
@@ -663,6 +860,11 @@ export class Conversations {
|
|
|
663
860
|
}
|
|
664
861
|
this.apply(conversation, this.started(conversation), event);
|
|
665
862
|
}
|
|
863
|
+
took(turn) {
|
|
864
|
+
const taken = turn.taken;
|
|
865
|
+
turn.taken = null;
|
|
866
|
+
taken?.();
|
|
867
|
+
}
|
|
666
868
|
apply(conversation, turn, event) {
|
|
667
869
|
switch (event.kind) {
|
|
668
870
|
case 'text':
|
|
@@ -717,13 +919,16 @@ export class Conversations {
|
|
|
717
919
|
bytes: bytes.length,
|
|
718
920
|
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
719
921
|
}));
|
|
720
|
-
const saved =
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
922
|
+
const saved = await uploaded(declared.upload, {
|
|
923
|
+
bytes: bytes.length,
|
|
924
|
+
send: (url, start, end) => sentPart(url, bytes.subarray(start, end)),
|
|
925
|
+
finish: (upload) => this.kit.house.deliver('/kit/uploads/finish', { upload }),
|
|
926
|
+
});
|
|
927
|
+
turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
|
|
724
928
|
}
|
|
725
929
|
catch (error) {
|
|
726
|
-
|
|
930
|
+
const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
931
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
|
|
727
932
|
}
|
|
728
933
|
}
|
|
729
934
|
}
|