@agentshouse/kit 0.2.1-c681dfca497a8 → 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 +26 -25
- package/dist/acp.js +36 -16
- package/dist/agents.js +87 -19
- package/dist/api.js +5 -5
- package/dist/app-git.js +10 -26
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +3 -6
- package/dist/calls.js +63 -0
- package/dist/chatgpt.js +7 -1
- package/dist/clis.js +12 -2
- package/dist/connection.js +125 -0
- package/dist/conversations.js +395 -151
- package/dist/copies.js +77 -50
- package/dist/end.js +9 -4
- package/dist/events.js +6 -2
- package/dist/files.js +123 -37
- package/dist/git.js +3 -4
- package/dist/home.js +31 -13
- package/dist/house-cli.js +327 -0
- package/dist/house-main.js +7 -289
- package/dist/instructions.js +4 -28
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -67
- package/dist/login.js +33 -8
- package/dist/logout.js +0 -2
- package/dist/mcp.js +4 -2
- package/dist/placement.js +9 -18
- package/dist/resident.js +82 -37
- package/dist/scope.js +3 -0
- package/dist/service.js +23 -1
- package/dist/sign-in.js +19 -37
- package/dist/skills.js +74 -15
- package/dist/transcription.js +53 -22
- package/dist/translate.js +2 -8
- package/dist/update.js +42 -45
- package/dist/wire.js +19 -3
- package/dist/work.js +3 -5
- package/how-we-work.md +2 -0
- package/package.json +3 -3
- package/dist/stream.js +0 -77
package/dist/conversations.js
CHANGED
|
@@ -1,42 +1,114 @@
|
|
|
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 {
|
|
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
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';
|
|
20
|
-
|
|
24
|
+
const INTERRUPTED = 'the conversation was interrupted';
|
|
25
|
+
const REFUSED = 'the CLI refused the prompt';
|
|
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
|
+
};
|
|
33
|
+
const NO_START = "the Agent's CLI did not start";
|
|
34
|
+
const NO_SESSION = "the Agent's CLI did not open a session";
|
|
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";
|
|
37
|
+
const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
|
|
38
|
+
class NotStarted extends Error {
|
|
39
|
+
}
|
|
40
|
+
async function step(reason, work) {
|
|
41
|
+
try {
|
|
42
|
+
return await work;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
|
|
46
|
+
}
|
|
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
|
+
}
|
|
59
|
+
const EXIT_PLAN_MODE = {
|
|
60
|
+
parse(value) {
|
|
61
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
62
|
+
throw new Error('_x.ai/exit_plan_mode needs an object');
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
|
|
21
68
|
function causeOf(error) {
|
|
22
69
|
return error instanceof Error ? error.message : String(error);
|
|
23
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
|
+
}
|
|
24
77
|
function answerMessage(request, response) {
|
|
25
78
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
26
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
|
+
}
|
|
27
86
|
function logged(error) {
|
|
28
87
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
29
88
|
}
|
|
89
|
+
function refusal(conversation, error) {
|
|
90
|
+
if (conversation.kills > 0)
|
|
91
|
+
return KILLED;
|
|
92
|
+
const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
93
|
+
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
94
|
+
return causeOf(error);
|
|
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
|
+
}
|
|
30
102
|
function marker(bridge) {
|
|
31
103
|
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
32
104
|
}
|
|
33
105
|
class Turn {
|
|
34
106
|
id = randomUUID();
|
|
35
107
|
parts;
|
|
36
|
-
admitted = false;
|
|
37
108
|
questions = 0;
|
|
38
109
|
secrets = 0;
|
|
39
110
|
prompted = false;
|
|
111
|
+
taken = null;
|
|
40
112
|
ended = false;
|
|
41
113
|
images = 0;
|
|
42
114
|
uploads = [];
|
|
@@ -50,6 +122,8 @@ class Conversation {
|
|
|
50
122
|
session = null;
|
|
51
123
|
turn = null;
|
|
52
124
|
commands = null;
|
|
125
|
+
options = null;
|
|
126
|
+
exitUntold = false;
|
|
53
127
|
context = null;
|
|
54
128
|
leftovers = new Set();
|
|
55
129
|
waiting = [];
|
|
@@ -57,6 +131,9 @@ class Conversation {
|
|
|
57
131
|
subagents = new Map();
|
|
58
132
|
held = [];
|
|
59
133
|
opening = null;
|
|
134
|
+
bridge = null;
|
|
135
|
+
launch = null;
|
|
136
|
+
prompted = null;
|
|
60
137
|
kills = 0;
|
|
61
138
|
killed = new AbortController();
|
|
62
139
|
interrupts = 0;
|
|
@@ -64,6 +141,11 @@ class Conversation {
|
|
|
64
141
|
open = 0;
|
|
65
142
|
queue = Promise.resolve();
|
|
66
143
|
reports = Promise.resolve();
|
|
144
|
+
starting = null;
|
|
145
|
+
preparing = null;
|
|
146
|
+
told = null;
|
|
147
|
+
untold = false;
|
|
148
|
+
shown = null;
|
|
67
149
|
constructor(id) {
|
|
68
150
|
this.id = id;
|
|
69
151
|
}
|
|
@@ -79,9 +161,7 @@ function asksSecret(request) {
|
|
|
79
161
|
export class Conversations {
|
|
80
162
|
kit;
|
|
81
163
|
conversations = new Map();
|
|
82
|
-
acks = new Map();
|
|
83
164
|
questions = new Map();
|
|
84
|
-
carrying = 0;
|
|
85
165
|
reporting = 0;
|
|
86
166
|
watched = false;
|
|
87
167
|
constructor(kit) {
|
|
@@ -89,38 +169,27 @@ export class Conversations {
|
|
|
89
169
|
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
90
170
|
setInterval(() => this.look(), 2_000);
|
|
91
171
|
}
|
|
92
|
-
|
|
93
|
-
this.
|
|
94
|
-
|
|
95
|
-
(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
conversation.queue = handled.catch(() => undefined);
|
|
110
|
-
const work = handled.then(({ ack }) => ack);
|
|
111
|
-
this.acks.set(input.input_id, work);
|
|
112
|
-
return work;
|
|
113
|
-
})();
|
|
114
|
-
carried
|
|
115
|
-
.then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
|
|
116
|
-
.catch(logged)
|
|
117
|
-
.finally(() => {
|
|
118
|
-
this.carrying--;
|
|
119
|
-
this.kit.changed();
|
|
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 }));
|
|
120
189
|
});
|
|
121
190
|
}
|
|
122
191
|
idle() {
|
|
123
|
-
if (this.
|
|
192
|
+
if (this.reporting > 0)
|
|
124
193
|
return false;
|
|
125
194
|
for (const conversation of this.conversations.values()) {
|
|
126
195
|
const turn = conversation.turn;
|
|
@@ -149,6 +218,14 @@ export class Conversations {
|
|
|
149
218
|
}
|
|
150
219
|
opened() {
|
|
151
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
|
+
}
|
|
152
229
|
}
|
|
153
230
|
watching(watched) {
|
|
154
231
|
const resend = watched && !this.watched;
|
|
@@ -168,63 +245,99 @@ export class Conversations {
|
|
|
168
245
|
}
|
|
169
246
|
return conversation;
|
|
170
247
|
}
|
|
171
|
-
async carry(conversation,
|
|
172
|
-
if (
|
|
173
|
-
return {
|
|
174
|
-
if (
|
|
175
|
-
return this.message(conversation,
|
|
176
|
-
|
|
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') {
|
|
177
255
|
if (--conversation.interrupts === 0)
|
|
178
256
|
conversation.interrupted = new AbortController();
|
|
179
|
-
await this.interrupt(conversation, String(
|
|
257
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
180
258
|
}
|
|
181
|
-
if (
|
|
259
|
+
if (call.kind === 'kill') {
|
|
182
260
|
await this.kill(conversation);
|
|
183
261
|
await conversation.sending;
|
|
184
262
|
if (--conversation.kills === 0)
|
|
185
263
|
conversation.killed = new AbortController();
|
|
186
264
|
}
|
|
187
|
-
if (
|
|
188
|
-
await this.option(conversation, String(
|
|
189
|
-
if (
|
|
190
|
-
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));
|
|
191
269
|
if (question === undefined) {
|
|
192
|
-
const text = answerMessage(
|
|
193
|
-
return this.message(conversation, { ...
|
|
270
|
+
const text = answerMessage(call.request, call.response);
|
|
271
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
194
272
|
}
|
|
195
|
-
question.answer(
|
|
273
|
+
question.answer(call.response);
|
|
196
274
|
}
|
|
197
|
-
return {
|
|
275
|
+
return { result: Promise.resolve({}) };
|
|
198
276
|
}
|
|
199
|
-
|
|
277
|
+
open(conversation, call) {
|
|
200
278
|
if (conversation.running !== null)
|
|
201
|
-
return {};
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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) }];
|
|
211
321
|
let route;
|
|
322
|
+
let instructing = null;
|
|
212
323
|
try {
|
|
213
|
-
route = await this.route(conversation,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
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))));
|
|
219
329
|
if (conversation.kills > 0)
|
|
220
330
|
throw new Error(KILLED);
|
|
221
331
|
}
|
|
222
332
|
catch (error) {
|
|
223
|
-
|
|
333
|
+
this.closeIdleBridge(conversation);
|
|
334
|
+
this.prepares(conversation, null);
|
|
335
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
224
336
|
}
|
|
337
|
+
conversation.preparing = null;
|
|
225
338
|
return {
|
|
226
|
-
|
|
227
|
-
conversation.waiting.push({
|
|
339
|
+
result: new Promise((settle) => {
|
|
340
|
+
conversation.waiting.push({ call, route, content, instructing, settle });
|
|
228
341
|
this.next(conversation);
|
|
229
342
|
}),
|
|
230
343
|
};
|
|
@@ -233,62 +346,146 @@ export class Conversations {
|
|
|
233
346
|
if (conversation.sending !== null || conversation.open > 0)
|
|
234
347
|
return;
|
|
235
348
|
const waiting = conversation.waiting.shift();
|
|
236
|
-
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);
|
|
237
353
|
return;
|
|
354
|
+
}
|
|
238
355
|
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
239
356
|
conversation.sending = null;
|
|
240
357
|
this.next(conversation);
|
|
241
358
|
});
|
|
242
359
|
}
|
|
243
360
|
async send(conversation, waiting) {
|
|
244
|
-
const {
|
|
245
|
-
let
|
|
361
|
+
const { call } = waiting;
|
|
362
|
+
let result = {};
|
|
363
|
+
let started = false;
|
|
364
|
+
let instructing = waiting.instructing;
|
|
246
365
|
const prompt = [];
|
|
247
366
|
try {
|
|
248
367
|
if (conversation.running === null) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
368
|
+
this.starts(conversation, 'session');
|
|
369
|
+
await this.launched(conversation);
|
|
370
|
+
}
|
|
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;
|
|
253
378
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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)) });
|
|
259
385
|
}
|
|
260
386
|
if (conversation.kills > 0)
|
|
261
387
|
throw new Error(KILLED);
|
|
262
388
|
}
|
|
263
389
|
catch (error) {
|
|
264
|
-
|
|
390
|
+
this.starts(conversation, null);
|
|
391
|
+
if (started) {
|
|
265
392
|
conversation.session = null;
|
|
266
393
|
await this.kill(conversation);
|
|
267
394
|
}
|
|
268
|
-
waiting.settle({ refused: conversation
|
|
395
|
+
waiting.settle({ refused: refusal(conversation, error) });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
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);
|
|
269
409
|
return;
|
|
270
410
|
}
|
|
271
|
-
|
|
272
|
-
|
|
411
|
+
conversation.shown = setTimeout(() => {
|
|
412
|
+
conversation.shown = null;
|
|
413
|
+
if (conversation.starting === step)
|
|
414
|
+
this.tell(conversation, step);
|
|
415
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
273
416
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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}` });
|
|
289
458
|
}
|
|
290
459
|
}
|
|
291
|
-
|
|
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);
|
|
292
489
|
}
|
|
293
490
|
async interrupt(conversation, turnId) {
|
|
294
491
|
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
@@ -343,63 +540,78 @@ export class Conversations {
|
|
|
343
540
|
}
|
|
344
541
|
return route;
|
|
345
542
|
}
|
|
346
|
-
async start(conversation,
|
|
543
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
544
|
+
const route = await routed;
|
|
347
545
|
const reader = readerFor(route.kind);
|
|
348
546
|
reader.session = sessionId;
|
|
349
547
|
const app = client({ name: '@agentshouse/kit' })
|
|
350
548
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
351
|
-
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
549
|
+
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
550
|
+
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
|
|
352
551
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
353
|
-
const
|
|
552
|
+
const bridging = this.bridged(conversation);
|
|
553
|
+
const bridge = await step(unbridged, bridging);
|
|
354
554
|
if (conversation.kills > 0) {
|
|
355
|
-
|
|
555
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
356
556
|
throw new Error(KILLED);
|
|
357
557
|
}
|
|
358
558
|
conversation.opening = bridge;
|
|
359
559
|
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
360
560
|
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
561
|
+
let adapter = null;
|
|
361
562
|
try {
|
|
362
563
|
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
564
|
+
bridged(bridge);
|
|
363
565
|
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
364
|
-
const
|
|
566
|
+
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
567
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
365
568
|
work.read(message);
|
|
366
569
|
this.read(conversation, reader, message);
|
|
367
|
-
}, { ...bridge.env, ...
|
|
368
|
-
void adapter.exited.then(() =>
|
|
570
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
571
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
369
572
|
const agent = adapter.connection.agent;
|
|
370
573
|
const cwd = route.working_directory;
|
|
371
574
|
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
: { ...(await 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 })));
|
|
375
577
|
reader.session = opened.sessionId;
|
|
376
|
-
if (sessionId
|
|
578
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
377
579
|
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
378
|
-
await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
|
|
580
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
379
581
|
}
|
|
380
|
-
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
381
|
-
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
582
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
583
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
382
584
|
void work.settled().then(() => scope.record());
|
|
383
585
|
conversation.running = running;
|
|
384
586
|
conversation.session = running.sessionId;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
this.options(conversation, options);
|
|
389
|
-
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));
|
|
390
590
|
return running.sessionId;
|
|
391
591
|
}
|
|
392
592
|
catch (error) {
|
|
393
|
-
|
|
593
|
+
adapter?.retire();
|
|
394
594
|
work.close();
|
|
395
595
|
conversation.leftovers.add(scope);
|
|
396
|
-
|
|
596
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
397
597
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
398
598
|
}
|
|
399
599
|
finally {
|
|
400
600
|
conversation.opening = null;
|
|
401
601
|
}
|
|
402
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
|
+
}
|
|
403
615
|
async launchSettings(running, route, offered) {
|
|
404
616
|
let options = offered;
|
|
405
617
|
const settings = [
|
|
@@ -421,27 +633,37 @@ export class Conversations {
|
|
|
421
633
|
}
|
|
422
634
|
return options;
|
|
423
635
|
}
|
|
424
|
-
exited(conversation,
|
|
425
|
-
const running = conversation.running;
|
|
426
|
-
if (running?.adapter !== adapter)
|
|
427
|
-
return;
|
|
636
|
+
exited(conversation, running, cause) {
|
|
428
637
|
this.detach(conversation, running);
|
|
429
638
|
if (conversation.turn !== null) {
|
|
430
|
-
this.end(conversation, conversation.turn,
|
|
639
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
431
640
|
}
|
|
432
641
|
this.next(conversation);
|
|
433
642
|
this.kit.changed();
|
|
434
643
|
}
|
|
435
644
|
detach(conversation, running) {
|
|
645
|
+
if (conversation.bridge === running.bridging)
|
|
646
|
+
conversation.bridge = null;
|
|
436
647
|
running.work.close();
|
|
437
648
|
conversation.leftovers.add(running.scope);
|
|
438
649
|
conversation.running = null;
|
|
439
650
|
conversation.commands = null;
|
|
651
|
+
conversation.options = null;
|
|
440
652
|
for (const [id, question] of this.questions) {
|
|
441
653
|
if (question.conversation === conversation)
|
|
442
654
|
this.questions.delete(id);
|
|
443
655
|
}
|
|
444
|
-
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);
|
|
445
667
|
}
|
|
446
668
|
commands(conversation, commands) {
|
|
447
669
|
conversation.commands = commands;
|
|
@@ -452,6 +674,7 @@ export class Conversations {
|
|
|
452
674
|
options(conversation, options) {
|
|
453
675
|
if (conversation.running === null)
|
|
454
676
|
return;
|
|
677
|
+
conversation.options = options;
|
|
455
678
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
456
679
|
}
|
|
457
680
|
deliver(conversation, turn, event, body) {
|
|
@@ -466,7 +689,7 @@ export class Conversations {
|
|
|
466
689
|
return delivered;
|
|
467
690
|
}
|
|
468
691
|
write(conversation, turn, operation) {
|
|
469
|
-
if (
|
|
692
|
+
if (turn.ended || !this.watched)
|
|
470
693
|
return;
|
|
471
694
|
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
472
695
|
}
|
|
@@ -477,12 +700,7 @@ export class Conversations {
|
|
|
477
700
|
begin(conversation) {
|
|
478
701
|
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
479
702
|
conversation.open++;
|
|
480
|
-
void this.deliver(conversation, turn, 'started', {})
|
|
481
|
-
.then(() => {
|
|
482
|
-
turn.admitted = true;
|
|
483
|
-
this.replay(conversation, turn);
|
|
484
|
-
})
|
|
485
|
-
.catch(logged);
|
|
703
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
486
704
|
conversation.turn = turn;
|
|
487
705
|
if (conversation.context !== null)
|
|
488
706
|
turn.parts.measure(conversation.context);
|
|
@@ -491,13 +709,17 @@ export class Conversations {
|
|
|
491
709
|
this.kit.changed();
|
|
492
710
|
return turn;
|
|
493
711
|
}
|
|
494
|
-
end(conversation, turn, failed) {
|
|
712
|
+
end(conversation, turn, failed, notice = null) {
|
|
495
713
|
if (turn.ended)
|
|
496
714
|
return;
|
|
715
|
+
if (failed !== null)
|
|
716
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
497
717
|
if (conversation.turn === turn) {
|
|
498
718
|
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
499
719
|
this.apply(conversation, turn, event);
|
|
500
720
|
}
|
|
721
|
+
if (notice !== null)
|
|
722
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
501
723
|
turn.ended = true;
|
|
502
724
|
conversation.open--;
|
|
503
725
|
if (conversation.turn === turn)
|
|
@@ -505,31 +727,45 @@ export class Conversations {
|
|
|
505
727
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
506
728
|
parts: turn.parts.saved(),
|
|
507
729
|
context: turn.parts.context,
|
|
508
|
-
...failed,
|
|
730
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
509
731
|
}));
|
|
510
732
|
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
511
733
|
this.next(conversation);
|
|
512
734
|
this.kit.changed();
|
|
513
735
|
}
|
|
514
|
-
prompt(conversation, prompt) {
|
|
736
|
+
prompt(conversation, prompt, taken) {
|
|
515
737
|
const running = conversation.running;
|
|
516
738
|
const turn = this.begin(conversation);
|
|
517
739
|
turn.prompted = true;
|
|
518
|
-
|
|
740
|
+
turn.taken = taken;
|
|
741
|
+
conversation.prompted = running.sessionId;
|
|
742
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
743
|
+
this.took(turn);
|
|
744
|
+
const notice = answeredFailure(answer);
|
|
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
|
+
}
|
|
751
|
+
}, async (error) => {
|
|
519
752
|
if (running.adapter.connection.signal.aborted) {
|
|
520
|
-
this.end(conversation, turn,
|
|
753
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
521
754
|
}
|
|
522
755
|
else {
|
|
756
|
+
this.took(turn);
|
|
523
757
|
if (conversation.open === 1 && conversation.running === running) {
|
|
524
|
-
running.
|
|
758
|
+
running.adapter.retire();
|
|
525
759
|
this.detach(conversation, running);
|
|
526
760
|
}
|
|
527
|
-
this.end(conversation, turn,
|
|
761
|
+
this.end(conversation, turn, answeredWith(error));
|
|
528
762
|
}
|
|
529
763
|
});
|
|
530
764
|
}
|
|
531
765
|
started(conversation) {
|
|
532
|
-
|
|
766
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
767
|
+
this.took(turn);
|
|
768
|
+
return turn;
|
|
533
769
|
}
|
|
534
770
|
ended(conversation) {
|
|
535
771
|
const turn = conversation.turn;
|
|
@@ -624,6 +860,11 @@ export class Conversations {
|
|
|
624
860
|
}
|
|
625
861
|
this.apply(conversation, this.started(conversation), event);
|
|
626
862
|
}
|
|
863
|
+
took(turn) {
|
|
864
|
+
const taken = turn.taken;
|
|
865
|
+
turn.taken = null;
|
|
866
|
+
taken?.();
|
|
867
|
+
}
|
|
627
868
|
apply(conversation, turn, event) {
|
|
628
869
|
switch (event.kind) {
|
|
629
870
|
case 'text':
|
|
@@ -678,13 +919,16 @@ export class Conversations {
|
|
|
678
919
|
bytes: bytes.length,
|
|
679
920
|
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
680
921
|
}));
|
|
681
|
-
const saved =
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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);
|
|
685
928
|
}
|
|
686
929
|
catch (error) {
|
|
687
|
-
|
|
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);
|
|
688
932
|
}
|
|
689
933
|
}
|
|
690
934
|
}
|