@agentshouse/kit 0.2.1-alpha.8 → 0.2.1-c02dacffca87c
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 +49 -24
- package/dist/abort.js +9 -0
- package/dist/acp.js +102 -88
- package/dist/agents.js +356 -52
- package/dist/api.js +40 -4
- package/dist/app-git.js +48 -0
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +93 -78
- package/dist/building.js +50 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +32 -0
- package/dist/chunks.js +49 -0
- package/dist/clis.js +224 -88
- package/dist/codex-main.js +58 -0
- package/dist/configure-main.js +6 -0
- package/dist/connection.js +105 -0
- package/dist/conversations.js +749 -268
- package/dist/copies.js +475 -260
- package/dist/end.js +33 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +546 -0
- package/dist/files.js +140 -17
- package/dist/git-main.js +2 -2
- package/dist/git.js +62 -22
- package/dist/home.js +134 -10
- package/dist/house-cli.js +299 -0
- package/dist/house-main.js +5 -102
- package/dist/instructions.js +17 -18
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -16
- package/dist/login.js +130 -95
- package/dist/logout.js +19 -0
- package/dist/mcp.js +43 -0
- package/dist/operation.js +7 -4
- package/dist/parts.js +82 -0
- package/dist/placement.js +31 -0
- package/dist/refusals.js +53 -0
- package/dist/relay.js +41 -0
- package/dist/resident.js +145 -51
- package/dist/scope.js +214 -0
- package/dist/scripts.js +255 -0
- package/dist/secret-input.js +7 -2
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +52 -32
- package/dist/silence.js +28 -0
- package/dist/skills.js +92 -0
- package/dist/transcription.js +210 -0
- package/dist/translate.js +2 -8
- package/dist/update.js +127 -0
- package/dist/wire.js +40 -0
- package/dist/work.js +346 -0
- package/package.json +20 -21
- package/dist/blocks.js +0 -51
- package/dist/bootstrap.json +0 -28
- package/dist/stream.js +0 -47
package/dist/conversations.js
CHANGED
|
@@ -1,64 +1,156 @@
|
|
|
1
1
|
import { client, } from '@agentclientprotocol/sdk';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { unlessAborted } from './abort.js';
|
|
6
|
+
import { signedOut, 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 {
|
|
10
|
+
import { answeredFailure, readerFor } from './events.js';
|
|
11
|
+
import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
|
|
12
|
+
import { agentBase } from './home.js';
|
|
10
13
|
import { instructions } from './instructions.js';
|
|
14
|
+
import { Parts } from './parts.js';
|
|
15
|
+
import { refusalOf } from './refusals.js';
|
|
16
|
+
import { relayed } from './relay.js';
|
|
17
|
+
import { killMarked, Scope } from './scope.js';
|
|
11
18
|
import { holdSecretInput } from './secret-input.js';
|
|
12
|
-
|
|
19
|
+
import { transcribe } from './transcription.js';
|
|
20
|
+
import { workFor } from './work.js';
|
|
13
21
|
const KILLED = 'the conversation was killed';
|
|
22
|
+
const INTERRUPTED = 'the conversation was interrupted';
|
|
23
|
+
const REFUSED = 'the CLI refused the prompt';
|
|
24
|
+
const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
|
|
25
|
+
const GONE = 'House no longer holds this conversation; start a new one';
|
|
26
|
+
const RETIRED = "this conversation's Agent is retired, so it cannot start";
|
|
27
|
+
const ENDED = {
|
|
28
|
+
conversation_not_found: GONE,
|
|
29
|
+
conversation_historical: RETIRED,
|
|
30
|
+
};
|
|
31
|
+
const NO_START = "the Agent's CLI did not start";
|
|
32
|
+
const NO_SESSION = "the Agent's CLI did not open a session";
|
|
33
|
+
const NO_RESUME = "the Agent's CLI could not resume this conversation";
|
|
34
|
+
const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
|
|
35
|
+
const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
|
|
36
|
+
const UNSTEERED = "the Agent's CLI did not take the message into its running turn";
|
|
37
|
+
class NotStarted extends Error {
|
|
38
|
+
}
|
|
39
|
+
class ResumeRefused extends NotStarted {
|
|
40
|
+
}
|
|
41
|
+
async function step(reason, work) {
|
|
42
|
+
try {
|
|
43
|
+
return await work;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function unopened(reason) {
|
|
50
|
+
return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
|
|
51
|
+
}
|
|
52
|
+
// A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
|
|
53
|
+
const STARTING_SHOWN_AFTER_MS = 1_000;
|
|
54
|
+
function unbridged(error) {
|
|
55
|
+
return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
|
|
56
|
+
}
|
|
57
|
+
const EXIT_PLAN_MODE = {
|
|
58
|
+
parse(value) {
|
|
59
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
60
|
+
throw new Error('_x.ai/exit_plan_mode needs an object');
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
|
|
14
66
|
function causeOf(error) {
|
|
15
67
|
return error instanceof Error ? error.message : String(error);
|
|
16
68
|
}
|
|
69
|
+
function answeredWith(error) {
|
|
70
|
+
return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
71
|
+
}
|
|
72
|
+
function exitedWith(running, cause) {
|
|
73
|
+
if (running.killed)
|
|
74
|
+
return { failure: 'killed', cause: KILLED };
|
|
75
|
+
return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
|
|
76
|
+
}
|
|
17
77
|
function answerMessage(request, response) {
|
|
18
78
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
19
79
|
}
|
|
80
|
+
async function sentPart(url, bytes, headers) {
|
|
81
|
+
const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
|
|
82
|
+
await answer.arrayBuffer();
|
|
83
|
+
if (!answer.ok)
|
|
84
|
+
throw storeRefusal('upload', answer.status);
|
|
85
|
+
}
|
|
20
86
|
function logged(error) {
|
|
21
87
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
22
88
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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;
|
|
31
101
|
}
|
|
32
|
-
function
|
|
33
|
-
|
|
102
|
+
function marker(bridge) {
|
|
103
|
+
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
104
|
+
}
|
|
105
|
+
function steers(adapter) {
|
|
106
|
+
return adapter.initialized._meta?.steering?.supported === true;
|
|
34
107
|
}
|
|
35
108
|
class Turn {
|
|
36
109
|
id = randomUUID();
|
|
37
|
-
|
|
38
|
-
sent = [];
|
|
39
|
-
unsentFrom = null;
|
|
40
|
-
draftSequence = 0;
|
|
41
|
-
planSequence = 0;
|
|
42
|
-
entries = null;
|
|
110
|
+
parts;
|
|
43
111
|
questions = 0;
|
|
44
112
|
secrets = 0;
|
|
45
|
-
|
|
113
|
+
prompted = false;
|
|
114
|
+
steered = false;
|
|
115
|
+
taken = null;
|
|
46
116
|
ended = false;
|
|
117
|
+
images = 0;
|
|
118
|
+
uploads = [];
|
|
119
|
+
constructor(emit) {
|
|
120
|
+
this.parts = new Parts(emit);
|
|
121
|
+
}
|
|
47
122
|
}
|
|
48
123
|
class Conversation {
|
|
49
124
|
id;
|
|
50
125
|
running = null;
|
|
126
|
+
session = null;
|
|
51
127
|
turn = null;
|
|
52
128
|
commands = null;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
129
|
+
options = null;
|
|
130
|
+
exitUntold = false;
|
|
131
|
+
context = null;
|
|
132
|
+
leftovers = new Set();
|
|
133
|
+
waiting = [];
|
|
134
|
+
done = false;
|
|
135
|
+
sending = null;
|
|
136
|
+
subagents = new Map();
|
|
137
|
+
held = [];
|
|
56
138
|
opening = null;
|
|
139
|
+
bridge = null;
|
|
140
|
+
launch = null;
|
|
141
|
+
prompted = null;
|
|
57
142
|
kills = 0;
|
|
58
143
|
killed = new AbortController();
|
|
59
|
-
|
|
144
|
+
interrupts = 0;
|
|
145
|
+
interrupted = new AbortController();
|
|
146
|
+
turns = new Set();
|
|
60
147
|
queue = Promise.resolve();
|
|
61
148
|
reports = Promise.resolve();
|
|
149
|
+
starting = null;
|
|
150
|
+
preparing = null;
|
|
151
|
+
told = null;
|
|
152
|
+
untold = false;
|
|
153
|
+
shown = null;
|
|
62
154
|
constructor(id) {
|
|
63
155
|
this.id = id;
|
|
64
156
|
}
|
|
@@ -74,51 +166,82 @@ function asksSecret(request) {
|
|
|
74
166
|
export class Conversations {
|
|
75
167
|
kit;
|
|
76
168
|
conversations = new Map();
|
|
77
|
-
acks = new Map();
|
|
78
169
|
questions = new Map();
|
|
79
|
-
carrying = 0;
|
|
80
170
|
reporting = 0;
|
|
171
|
+
watched = false;
|
|
81
172
|
constructor(kit) {
|
|
82
173
|
this.kit = kit;
|
|
174
|
+
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
175
|
+
setInterval(() => this.look(), 2_000);
|
|
83
176
|
}
|
|
84
|
-
|
|
85
|
-
this.
|
|
86
|
-
|
|
87
|
-
(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
conversation.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
.then((
|
|
101
|
-
.catch(logged)
|
|
102
|
-
.finally(() => {
|
|
103
|
-
this.carrying--;
|
|
104
|
-
this.kit.changed();
|
|
177
|
+
call(call) {
|
|
178
|
+
void this.kit.calls.carry(call.input_id, () => {
|
|
179
|
+
const conversation = this.conversation(call.conversation_id);
|
|
180
|
+
if (call.kind === 'kill') {
|
|
181
|
+
conversation.kills++;
|
|
182
|
+
conversation.killed.abort(new Error(KILLED));
|
|
183
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
184
|
+
waiting.settle({ refused: KILLED });
|
|
185
|
+
this.halt(conversation);
|
|
186
|
+
}
|
|
187
|
+
if (call.kind === 'interrupt') {
|
|
188
|
+
conversation.interrupts++;
|
|
189
|
+
conversation.interrupted.abort();
|
|
190
|
+
}
|
|
191
|
+
const handled = conversation.queue.then(() => this.carry(conversation, call));
|
|
192
|
+
conversation.queue = handled.catch(() => undefined);
|
|
193
|
+
return handled.then(async ({ result }) => ({ result: await result }));
|
|
105
194
|
});
|
|
106
195
|
}
|
|
107
196
|
idle() {
|
|
108
|
-
if (this.
|
|
197
|
+
if (this.reporting > 0)
|
|
109
198
|
return false;
|
|
110
199
|
for (const conversation of this.conversations.values()) {
|
|
111
200
|
const turn = conversation.turn;
|
|
112
201
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
113
|
-
if (conversation.
|
|
202
|
+
if (conversation.turns.size > (asking ? 1 : 0))
|
|
114
203
|
return false;
|
|
115
204
|
}
|
|
116
|
-
|
|
205
|
+
const conversations = [...this.conversations.values()];
|
|
206
|
+
if (conversations.some((conversation) => conversation.running?.work.busy()))
|
|
207
|
+
return false;
|
|
208
|
+
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
209
|
+
}
|
|
210
|
+
quiet() {
|
|
211
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
|
|
212
|
+
}
|
|
213
|
+
look() {
|
|
214
|
+
for (const conversation of this.conversations.values()) {
|
|
215
|
+
for (const scope of conversation.leftovers) {
|
|
216
|
+
if (!scope.empty())
|
|
217
|
+
continue;
|
|
218
|
+
scope.close();
|
|
219
|
+
conversation.leftovers.delete(scope);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
this.kit.changed();
|
|
117
223
|
}
|
|
118
224
|
opened() {
|
|
225
|
+
this.watched = false;
|
|
226
|
+
for (const conversation of this.conversations.values()) {
|
|
227
|
+
if (conversation.running !== null) {
|
|
228
|
+
this.facts(conversation);
|
|
229
|
+
this.subagentsChanged(conversation);
|
|
230
|
+
}
|
|
231
|
+
else if (conversation.exitUntold)
|
|
232
|
+
this.process(conversation, false);
|
|
233
|
+
if (conversation.untold || conversation.told !== null)
|
|
234
|
+
this.retell(conversation);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
watching(watched) {
|
|
238
|
+
const resend = watched && !this.watched;
|
|
239
|
+
this.watched = watched;
|
|
240
|
+
if (!resend)
|
|
241
|
+
return;
|
|
119
242
|
for (const conversation of this.conversations.values()) {
|
|
120
243
|
if (conversation.turn !== null)
|
|
121
|
-
conversation.turn
|
|
244
|
+
this.replay(conversation, conversation.turn);
|
|
122
245
|
}
|
|
123
246
|
}
|
|
124
247
|
conversation(id) {
|
|
@@ -129,72 +252,320 @@ export class Conversations {
|
|
|
129
252
|
}
|
|
130
253
|
return conversation;
|
|
131
254
|
}
|
|
132
|
-
async carry(conversation,
|
|
133
|
-
if (
|
|
134
|
-
return this.open(conversation,
|
|
135
|
-
if (
|
|
136
|
-
return this.message(conversation,
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
255
|
+
async carry(conversation, call) {
|
|
256
|
+
if (call.kind === 'open')
|
|
257
|
+
return this.open(conversation, call);
|
|
258
|
+
if (call.kind === 'message')
|
|
259
|
+
return this.message(conversation, call);
|
|
260
|
+
if (call.kind === 'done') {
|
|
261
|
+
conversation.done = true;
|
|
262
|
+
this.next(conversation);
|
|
263
|
+
return { result: Promise.resolve({}) };
|
|
264
|
+
}
|
|
265
|
+
await this.launched(conversation);
|
|
266
|
+
if (call.kind === 'interrupt') {
|
|
267
|
+
if (--conversation.interrupts === 0)
|
|
268
|
+
conversation.interrupted = new AbortController();
|
|
269
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
270
|
+
}
|
|
271
|
+
if (call.kind === 'kill') {
|
|
140
272
|
await this.kill(conversation);
|
|
273
|
+
await conversation.sending;
|
|
141
274
|
if (--conversation.kills === 0)
|
|
142
275
|
conversation.killed = new AbortController();
|
|
143
276
|
}
|
|
144
|
-
if (
|
|
145
|
-
await this.option(conversation, String(
|
|
146
|
-
if (
|
|
147
|
-
const question = this.questions.get(String(
|
|
277
|
+
if (call.kind === 'option')
|
|
278
|
+
await this.option(conversation, String(call.option), call.value);
|
|
279
|
+
if (call.kind === 'answer') {
|
|
280
|
+
const question = this.questions.get(String(call.interaction_id));
|
|
148
281
|
if (question === undefined) {
|
|
149
|
-
|
|
282
|
+
const text = answerMessage(call.request, call.response);
|
|
283
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
150
284
|
}
|
|
151
|
-
question.answer(
|
|
285
|
+
question.answer(call.response);
|
|
152
286
|
}
|
|
153
|
-
return {};
|
|
287
|
+
return { result: Promise.resolve({}) };
|
|
288
|
+
}
|
|
289
|
+
async open(conversation, call) {
|
|
290
|
+
await this.stop(conversation);
|
|
291
|
+
const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
|
|
292
|
+
return {
|
|
293
|
+
result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
async stop(conversation) {
|
|
297
|
+
const running = conversation.running;
|
|
298
|
+
running?.scope.kill();
|
|
299
|
+
await running?.adapter.exited;
|
|
300
|
+
}
|
|
301
|
+
launch(conversation, route, sessionId, empty) {
|
|
302
|
+
let opened;
|
|
303
|
+
let failed;
|
|
304
|
+
const bridge = new Promise((resolve, reject) => {
|
|
305
|
+
opened = resolve;
|
|
306
|
+
failed = reject;
|
|
307
|
+
});
|
|
308
|
+
bridge.catch(() => undefined);
|
|
309
|
+
const session = this.start(conversation, route, sessionId, empty, opened);
|
|
310
|
+
session.catch(failed);
|
|
311
|
+
const launch = { bridge, session };
|
|
312
|
+
conversation.launch = launch;
|
|
313
|
+
void session
|
|
314
|
+
.catch(() => undefined)
|
|
315
|
+
.then(() => {
|
|
316
|
+
if (conversation.launch !== launch)
|
|
317
|
+
return;
|
|
318
|
+
conversation.launch = null;
|
|
319
|
+
this.closeIdleBridge(conversation);
|
|
320
|
+
this.next(conversation);
|
|
321
|
+
});
|
|
322
|
+
this.next(conversation);
|
|
323
|
+
return launch;
|
|
324
|
+
}
|
|
325
|
+
async launched(conversation) {
|
|
326
|
+
while (conversation.launch !== null)
|
|
327
|
+
await conversation.launch.session.catch(() => undefined);
|
|
328
|
+
}
|
|
329
|
+
instructed(conversation, route) {
|
|
330
|
+
const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
|
|
331
|
+
if (bridge === undefined)
|
|
332
|
+
return null;
|
|
333
|
+
const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
|
|
334
|
+
read.catch(() => undefined);
|
|
335
|
+
return read;
|
|
154
336
|
}
|
|
155
|
-
async
|
|
156
|
-
|
|
157
|
-
|
|
337
|
+
async message(conversation, call) {
|
|
338
|
+
const content = [
|
|
339
|
+
...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
|
|
340
|
+
{ type: 'text', text: String(call.text) },
|
|
341
|
+
];
|
|
342
|
+
let route;
|
|
343
|
+
let instructing = null;
|
|
158
344
|
try {
|
|
159
|
-
|
|
345
|
+
route = await this.route(conversation, call.agent_id);
|
|
346
|
+
if (call.first === true)
|
|
347
|
+
instructing = this.instructed(conversation, route);
|
|
348
|
+
if (call.message_id !== null)
|
|
349
|
+
content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
|
|
350
|
+
if (conversation.kills > 0)
|
|
351
|
+
throw new Error(KILLED);
|
|
160
352
|
}
|
|
161
353
|
catch (error) {
|
|
162
|
-
|
|
354
|
+
this.closeIdleBridge(conversation);
|
|
355
|
+
this.prepares(conversation, null);
|
|
356
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
357
|
+
}
|
|
358
|
+
conversation.preparing = null;
|
|
359
|
+
const { promise, resolve: settle } = Promise.withResolvers();
|
|
360
|
+
const waiting = { call, route, content, instructing, settle };
|
|
361
|
+
if (call.at_once === true && conversation.turns.size > 0)
|
|
362
|
+
await this.atOnce(conversation, waiting);
|
|
363
|
+
else
|
|
364
|
+
conversation.waiting.push(waiting);
|
|
365
|
+
this.next(conversation);
|
|
366
|
+
return { result: promise };
|
|
367
|
+
}
|
|
368
|
+
async atOnce(conversation, waiting) {
|
|
369
|
+
while (conversation.sending !== null)
|
|
370
|
+
await conversation.sending;
|
|
371
|
+
const running = conversation.running;
|
|
372
|
+
if (running !== null && conversation.turns.size > 0 && steers(running.adapter)) {
|
|
373
|
+
conversation.sending = this.steer(conversation, running, waiting);
|
|
374
|
+
await conversation.sending;
|
|
375
|
+
conversation.sending = null;
|
|
376
|
+
return;
|
|
163
377
|
}
|
|
378
|
+
this.ahead(conversation, waiting);
|
|
379
|
+
if (conversation.turns.size > 0)
|
|
380
|
+
await this.cancel(conversation);
|
|
381
|
+
}
|
|
382
|
+
ahead(conversation, waiting) {
|
|
383
|
+
const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
|
|
384
|
+
conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
|
|
164
385
|
}
|
|
165
|
-
async
|
|
166
|
-
let
|
|
386
|
+
async steer(conversation, running, waiting) {
|
|
387
|
+
let outcome;
|
|
388
|
+
try {
|
|
389
|
+
({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
|
|
390
|
+
sessionId: running.sessionId,
|
|
391
|
+
prompt: waiting.content,
|
|
392
|
+
_meta: { steering: { idleBehavior: 'promptRequired' } },
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
outcome = error;
|
|
397
|
+
}
|
|
398
|
+
if (outcome === 'promptRequired') {
|
|
399
|
+
this.ahead(conversation, waiting);
|
|
400
|
+
}
|
|
401
|
+
else if (outcome === 'injected' || outcome === 'startedNewTurn') {
|
|
402
|
+
if (outcome === 'injected') {
|
|
403
|
+
for (const turn of conversation.turns)
|
|
404
|
+
turn.steered ||= turn.prompted;
|
|
405
|
+
}
|
|
406
|
+
this.kit.calls.keepResult(String(waiting.call.input_id), {});
|
|
407
|
+
waiting.settle({});
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
|
|
411
|
+
waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
next(conversation) {
|
|
415
|
+
if (conversation.sending !== null || conversation.turns.size > 0)
|
|
416
|
+
return;
|
|
417
|
+
const waiting = conversation.waiting.shift();
|
|
418
|
+
if (waiting === undefined) {
|
|
419
|
+
const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
|
|
420
|
+
if (conversation.starting !== step)
|
|
421
|
+
this.starts(conversation, step);
|
|
422
|
+
if (conversation.done && conversation.launch === null)
|
|
423
|
+
void this.stop(conversation).catch(logged);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
427
|
+
conversation.sending = null;
|
|
428
|
+
this.next(conversation);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async send(conversation, waiting) {
|
|
432
|
+
const { call } = waiting;
|
|
433
|
+
let result = {};
|
|
434
|
+
let started = false;
|
|
435
|
+
let instructing = waiting.instructing;
|
|
167
436
|
const prompt = [];
|
|
168
437
|
try {
|
|
169
|
-
const route = await this.route(conversation, input.agent_id);
|
|
170
|
-
const paths = await placeFiles(this.kit.house, route.working_directory, input.files, conversation.killed.signal);
|
|
171
438
|
if (conversation.running === null) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
439
|
+
this.starts(conversation, 'session');
|
|
440
|
+
await this.launched(conversation);
|
|
441
|
+
}
|
|
442
|
+
if (conversation.running === null) {
|
|
443
|
+
const stored = conversation.session ?? call.provider_session_id;
|
|
444
|
+
const empty = call.first === true && conversation.prompted !== stored;
|
|
445
|
+
const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
|
|
446
|
+
if (call.first === true)
|
|
447
|
+
instructing = this.instructed(conversation, waiting.route);
|
|
448
|
+
started = (await launch.session) !== stored;
|
|
449
|
+
}
|
|
450
|
+
const session = conversation.running.sessionId;
|
|
451
|
+
if (started || (call.first === true && session !== call.provider_session_id)) {
|
|
452
|
+
result = { provider_session_id: session };
|
|
175
453
|
}
|
|
176
|
-
if (
|
|
177
|
-
prompt.push({
|
|
178
|
-
type: 'text',
|
|
179
|
-
text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
|
|
180
|
-
});
|
|
454
|
+
if (call.first === true) {
|
|
455
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
181
456
|
}
|
|
182
|
-
prompt.push({ type: 'text', text: String(input.text) });
|
|
183
|
-
if (paths.length > 0)
|
|
184
|
-
prompt.push({ type: 'text', text: paths.join('\n') });
|
|
185
457
|
if (conversation.kills > 0)
|
|
186
458
|
throw new Error(KILLED);
|
|
187
459
|
}
|
|
188
460
|
catch (error) {
|
|
189
|
-
|
|
461
|
+
this.starts(conversation, null);
|
|
462
|
+
if (started) {
|
|
463
|
+
conversation.session = null;
|
|
190
464
|
await this.kill(conversation);
|
|
191
|
-
|
|
465
|
+
}
|
|
466
|
+
const refused = refusal(conversation, error);
|
|
467
|
+
const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
|
|
468
|
+
waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
this.starts(conversation, null);
|
|
472
|
+
this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
|
|
473
|
+
waiting.settle(result);
|
|
474
|
+
}
|
|
475
|
+
starts(conversation, step) {
|
|
476
|
+
conversation.starting = step;
|
|
477
|
+
if (conversation.shown !== null)
|
|
478
|
+
clearTimeout(conversation.shown);
|
|
479
|
+
conversation.shown = null;
|
|
480
|
+
if (step === null || conversation.told !== null) {
|
|
481
|
+
this.tell(conversation, step);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
conversation.shown = setTimeout(() => {
|
|
485
|
+
conversation.shown = null;
|
|
486
|
+
if (conversation.starting === step)
|
|
487
|
+
this.tell(conversation, step);
|
|
488
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
489
|
+
}
|
|
490
|
+
prepares(conversation, step) {
|
|
491
|
+
conversation.preparing = step;
|
|
492
|
+
this.next(conversation);
|
|
493
|
+
}
|
|
494
|
+
tell(conversation, step) {
|
|
495
|
+
if (conversation.told === step && !conversation.untold)
|
|
496
|
+
return;
|
|
497
|
+
conversation.told = step;
|
|
498
|
+
this.retell(conversation);
|
|
499
|
+
}
|
|
500
|
+
retell(conversation) {
|
|
501
|
+
conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
|
|
502
|
+
}
|
|
503
|
+
async transcribed(conversation, messageId, text) {
|
|
504
|
+
const paths = linkedPaths(text);
|
|
505
|
+
if (paths.length === 0)
|
|
506
|
+
return [];
|
|
507
|
+
const killed = conversation.killed.signal;
|
|
508
|
+
const interrupted = conversation.interrupted.signal;
|
|
509
|
+
const transcribing = AbortSignal.any([killed, interrupted]);
|
|
510
|
+
let signal = killed;
|
|
511
|
+
const blocks = [];
|
|
512
|
+
try {
|
|
513
|
+
const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
|
|
514
|
+
const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
|
|
515
|
+
for (const path of paths) {
|
|
516
|
+
let answered = await read(path);
|
|
517
|
+
if (answered === null)
|
|
518
|
+
continue;
|
|
519
|
+
signal = transcribing;
|
|
520
|
+
this.prepares(conversation, 'transcription');
|
|
521
|
+
const said = await transcribe(async () => {
|
|
522
|
+
const address = answered ?? (await read(path));
|
|
523
|
+
answered = null;
|
|
524
|
+
if (address === null)
|
|
525
|
+
throw new Error(`${path} is no longer an audio file`);
|
|
526
|
+
return address;
|
|
527
|
+
}, signal);
|
|
528
|
+
const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
|
|
529
|
+
await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
|
|
530
|
+
blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
|
|
535
|
+
throw new NotStarted(INTERRUPTED, { cause: error });
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
return blocks;
|
|
539
|
+
}
|
|
540
|
+
bridged(conversation) {
|
|
541
|
+
if (conversation.bridge === null) {
|
|
542
|
+
const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
543
|
+
conversation.bridge = bridging;
|
|
544
|
+
bridging.catch(() => {
|
|
545
|
+
if (conversation.bridge === bridging)
|
|
546
|
+
conversation.bridge = null;
|
|
547
|
+
});
|
|
192
548
|
}
|
|
193
|
-
|
|
194
|
-
|
|
549
|
+
return conversation.bridge;
|
|
550
|
+
}
|
|
551
|
+
closeBridge(conversation, bridging, bridge) {
|
|
552
|
+
bridge.close();
|
|
553
|
+
if (conversation.bridge === bridging)
|
|
554
|
+
conversation.bridge = null;
|
|
555
|
+
}
|
|
556
|
+
closeIdleBridge(conversation) {
|
|
557
|
+
const bridging = conversation.bridge;
|
|
558
|
+
if (bridging === null || conversation.running !== null || conversation.launch !== null)
|
|
559
|
+
return;
|
|
560
|
+
conversation.bridge = null;
|
|
561
|
+
void bridging.then((bridge) => bridge.close(), () => undefined);
|
|
195
562
|
}
|
|
196
563
|
async interrupt(conversation, turnId) {
|
|
197
|
-
if (conversation.
|
|
564
|
+
if ([...conversation.turns].some((turn) => turn.id === turnId))
|
|
565
|
+
await this.cancel(conversation);
|
|
566
|
+
}
|
|
567
|
+
async cancel(conversation) {
|
|
568
|
+
if (conversation.running === null)
|
|
198
569
|
return;
|
|
199
570
|
await conversation.running.adapter.connection.agent.notify('session/cancel', {
|
|
200
571
|
sessionId: conversation.running.sessionId,
|
|
@@ -202,11 +573,13 @@ export class Conversations {
|
|
|
202
573
|
}
|
|
203
574
|
halt(conversation) {
|
|
204
575
|
if (conversation.opening !== null)
|
|
205
|
-
|
|
576
|
+
killMarked(marker(conversation.opening));
|
|
577
|
+
for (const scope of conversation.leftovers)
|
|
578
|
+
scope.kill();
|
|
206
579
|
if (conversation.running === null)
|
|
207
580
|
return;
|
|
208
581
|
conversation.running.killed = true;
|
|
209
|
-
|
|
582
|
+
conversation.running.scope.kill();
|
|
210
583
|
}
|
|
211
584
|
async kill(conversation) {
|
|
212
585
|
const running = conversation.running;
|
|
@@ -218,6 +591,7 @@ export class Conversations {
|
|
|
218
591
|
if (running === null)
|
|
219
592
|
return;
|
|
220
593
|
try {
|
|
594
|
+
running.reader.requested(option, value);
|
|
221
595
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
222
596
|
sessionId: running.sessionId,
|
|
223
597
|
configId: option,
|
|
@@ -226,72 +600,99 @@ export class Conversations {
|
|
|
226
600
|
this.options(conversation, answered.configOptions);
|
|
227
601
|
}
|
|
228
602
|
catch (error) {
|
|
603
|
+
running.reader.refused(option);
|
|
229
604
|
logged(error);
|
|
230
605
|
}
|
|
231
606
|
}
|
|
232
607
|
async route(conversation, agentId) {
|
|
233
608
|
const signal = conversation.killed.signal;
|
|
234
|
-
await
|
|
609
|
+
await unlessAborted(this.kit.agents.read(), signal);
|
|
235
610
|
if (this.kit.agents.route(agentId) === undefined)
|
|
236
|
-
await
|
|
611
|
+
await unlessAborted(this.kit.agents.refresh(), signal);
|
|
237
612
|
const route = this.kit.agents.route(agentId);
|
|
238
613
|
if (route === undefined)
|
|
239
614
|
throw new Error(`this Environment hosts no Agent ${agentId}`);
|
|
240
|
-
if (route.working_directory === join(
|
|
615
|
+
if (route.working_directory === join(agentBase(), route.agent_id)) {
|
|
241
616
|
await mkdir(route.working_directory, { recursive: true });
|
|
242
617
|
}
|
|
243
618
|
return route;
|
|
244
619
|
}
|
|
245
|
-
async start(conversation,
|
|
620
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
621
|
+
const route = await routed;
|
|
622
|
+
const reader = readerFor(route.kind);
|
|
623
|
+
reader.session = sessionId;
|
|
246
624
|
const app = client({ name: '@agentshouse/kit' })
|
|
247
|
-
.onNotification('session/update', ({ params }) => this.update(conversation, params))
|
|
248
|
-
.onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
|
|
249
|
-
.onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
|
|
250
625
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
251
|
-
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
252
|
-
|
|
253
|
-
const
|
|
626
|
+
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
627
|
+
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
|
|
628
|
+
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
629
|
+
const bridging = this.bridged(conversation);
|
|
630
|
+
const bridge = await step(unbridged, bridging);
|
|
254
631
|
if (conversation.kills > 0) {
|
|
255
|
-
|
|
632
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
256
633
|
throw new Error(KILLED);
|
|
257
634
|
}
|
|
258
635
|
conversation.opening = bridge;
|
|
636
|
+
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
637
|
+
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
638
|
+
let adapter = null;
|
|
259
639
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
640
|
+
bridged(bridge);
|
|
641
|
+
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
642
|
+
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
643
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
644
|
+
work.read(message);
|
|
645
|
+
this.read(conversation, reader, message);
|
|
646
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
647
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
262
648
|
const agent = adapter.connection.agent;
|
|
263
649
|
const cwd = route.working_directory;
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
sessionId: opened.sessionId,
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
650
|
+
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
651
|
+
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
652
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
653
|
+
reader.session = opened.sessionId;
|
|
654
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
655
|
+
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
656
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
657
|
+
}
|
|
658
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
659
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
660
|
+
void work.settled().then(() => scope.record());
|
|
275
661
|
conversation.running = running;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
this.
|
|
280
|
-
void adapter.
|
|
662
|
+
adapter.turn(conversation.turns.size > 0);
|
|
663
|
+
conversation.session = running.sessionId;
|
|
664
|
+
conversation.options = options;
|
|
665
|
+
this.facts(conversation);
|
|
666
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
281
667
|
return running.sessionId;
|
|
282
668
|
}
|
|
283
669
|
catch (error) {
|
|
284
|
-
|
|
285
|
-
|
|
670
|
+
adapter?.retire();
|
|
671
|
+
work.close();
|
|
672
|
+
conversation.leftovers.add(scope);
|
|
673
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
286
674
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
287
675
|
}
|
|
288
676
|
finally {
|
|
289
677
|
conversation.opening = null;
|
|
290
678
|
}
|
|
291
679
|
}
|
|
680
|
+
async resume(conversation, kind, adapter, request, empty) {
|
|
681
|
+
try {
|
|
682
|
+
return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
686
|
+
throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
|
|
687
|
+
}
|
|
688
|
+
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
292
692
|
async launchSettings(running, route, offered) {
|
|
293
693
|
let options = offered;
|
|
294
694
|
const settings = [
|
|
695
|
+
['mode', route.mode ?? CLIS[route.kind].fullAccess],
|
|
295
696
|
['model', route.model],
|
|
296
697
|
['thought_level', route.effort],
|
|
297
698
|
];
|
|
@@ -299,6 +700,7 @@ export class Conversations {
|
|
|
299
700
|
if (value === null)
|
|
300
701
|
continue;
|
|
301
702
|
const option = options.find((candidate) => candidate.category === category);
|
|
703
|
+
running.reader.requested(option?.id ?? category, value);
|
|
302
704
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
303
705
|
sessionId: running.sessionId,
|
|
304
706
|
configId: option?.id ?? category,
|
|
@@ -308,23 +710,45 @@ export class Conversations {
|
|
|
308
710
|
}
|
|
309
711
|
return options;
|
|
310
712
|
}
|
|
311
|
-
exited(conversation,
|
|
312
|
-
|
|
313
|
-
if (
|
|
314
|
-
|
|
713
|
+
exited(conversation, running, cause) {
|
|
714
|
+
this.detach(conversation, running);
|
|
715
|
+
if (conversation.turn !== null) {
|
|
716
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
717
|
+
}
|
|
718
|
+
this.next(conversation);
|
|
719
|
+
this.kit.changed();
|
|
720
|
+
}
|
|
721
|
+
detach(conversation, running) {
|
|
722
|
+
if (conversation.bridge === running.bridging)
|
|
723
|
+
conversation.bridge = null;
|
|
724
|
+
running.work.close();
|
|
725
|
+
conversation.leftovers.add(running.scope);
|
|
315
726
|
conversation.running = null;
|
|
316
727
|
conversation.commands = null;
|
|
317
|
-
conversation.
|
|
318
|
-
conversation.
|
|
728
|
+
conversation.options = null;
|
|
729
|
+
if (conversation.subagents.size > 0) {
|
|
730
|
+
conversation.subagents.clear();
|
|
731
|
+
this.subagentsChanged(conversation);
|
|
732
|
+
}
|
|
319
733
|
for (const [id, question] of this.questions) {
|
|
320
734
|
if (question.conversation === conversation)
|
|
321
735
|
this.questions.delete(id);
|
|
322
736
|
}
|
|
323
|
-
this.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
737
|
+
this.process(conversation, false);
|
|
738
|
+
}
|
|
739
|
+
process(conversation, running) {
|
|
740
|
+
conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
|
|
741
|
+
}
|
|
742
|
+
facts(conversation) {
|
|
743
|
+
this.process(conversation, true);
|
|
744
|
+
if (conversation.commands !== null)
|
|
745
|
+
this.commands(conversation, conversation.commands);
|
|
746
|
+
if (conversation.options !== null)
|
|
747
|
+
this.options(conversation, conversation.options);
|
|
748
|
+
}
|
|
749
|
+
subagentsChanged(conversation) {
|
|
750
|
+
const subagents = [...conversation.subagents.values()].map(({ description, activity }) => ({ description, activity }));
|
|
751
|
+
this.kit.send({ type: 'subagents', conversation_id: conversation.id, subagents });
|
|
328
752
|
}
|
|
329
753
|
commands(conversation, commands) {
|
|
330
754
|
conversation.commands = commands;
|
|
@@ -335,11 +759,12 @@ export class Conversations {
|
|
|
335
759
|
options(conversation, options) {
|
|
336
760
|
if (conversation.running === null)
|
|
337
761
|
return;
|
|
762
|
+
conversation.options = options;
|
|
338
763
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
339
764
|
}
|
|
340
765
|
deliver(conversation, turn, event, body) {
|
|
341
766
|
this.reporting++;
|
|
342
|
-
const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
|
|
767
|
+
const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
|
|
343
768
|
conversation.reports = delivered
|
|
344
769
|
.catch(() => undefined)
|
|
345
770
|
.finally(() => {
|
|
@@ -348,108 +773,103 @@ export class Conversations {
|
|
|
348
773
|
});
|
|
349
774
|
return delivered;
|
|
350
775
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
.then(() => this.admitted(conversation, turn))
|
|
356
|
-
.catch(logged);
|
|
357
|
-
return turn;
|
|
776
|
+
write(conversation, turn, operation) {
|
|
777
|
+
if (turn.ended || !this.watched)
|
|
778
|
+
return;
|
|
779
|
+
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
358
780
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
this.draft(conversation, turn);
|
|
363
|
-
}
|
|
364
|
-
if (turn.entries !== null)
|
|
365
|
-
this.plan(conversation, turn);
|
|
781
|
+
replay(conversation, turn) {
|
|
782
|
+
for (const operation of turn.parts.operations())
|
|
783
|
+
this.write(conversation, turn, operation);
|
|
366
784
|
}
|
|
367
785
|
begin(conversation) {
|
|
368
|
-
const turn = this.
|
|
786
|
+
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
787
|
+
conversation.turns.add(turn);
|
|
788
|
+
conversation.running?.adapter.turn(true);
|
|
789
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
369
790
|
conversation.turn = turn;
|
|
791
|
+
if (conversation.context !== null)
|
|
792
|
+
turn.parts.measure(conversation.context);
|
|
793
|
+
for (const part of conversation.held.splice(0))
|
|
794
|
+
turn.parts.start(part);
|
|
370
795
|
this.kit.changed();
|
|
371
796
|
return turn;
|
|
372
797
|
}
|
|
373
|
-
end(conversation, turn,
|
|
798
|
+
end(conversation, turn, failed, notice = null) {
|
|
374
799
|
if (turn.ended)
|
|
375
800
|
return;
|
|
801
|
+
if (failed !== null)
|
|
802
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
803
|
+
if (conversation.turn === turn) {
|
|
804
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
805
|
+
this.apply(conversation, turn, event);
|
|
806
|
+
}
|
|
807
|
+
if (notice !== null)
|
|
808
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
376
809
|
turn.ended = true;
|
|
377
|
-
conversation.
|
|
810
|
+
conversation.turns.delete(turn);
|
|
811
|
+
conversation.running?.adapter.turn(conversation.turns.size > 0);
|
|
378
812
|
if (conversation.turn === turn)
|
|
379
813
|
conversation.turn = null;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
814
|
+
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
815
|
+
parts: turn.parts.saved(),
|
|
816
|
+
context: turn.parts.context,
|
|
817
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
818
|
+
}));
|
|
819
|
+
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
820
|
+
this.next(conversation);
|
|
384
821
|
this.kit.changed();
|
|
385
822
|
}
|
|
386
|
-
|
|
387
|
-
if (conversation.open > 0)
|
|
388
|
-
conversation.late.push(cause);
|
|
389
|
-
else
|
|
390
|
-
this.end(conversation, this.report(conversation), { failed: cause });
|
|
391
|
-
}
|
|
392
|
-
prompt(conversation, prompt) {
|
|
823
|
+
prompt(conversation, prompt, taken) {
|
|
393
824
|
const running = conversation.running;
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
this.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
else
|
|
405
|
-
this.end(conversation,
|
|
825
|
+
const turn = this.begin(conversation);
|
|
826
|
+
turn.prompted = true;
|
|
827
|
+
turn.taken = taken;
|
|
828
|
+
conversation.prompted = running.sessionId;
|
|
829
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
830
|
+
this.took(turn);
|
|
831
|
+
const notice = answeredFailure(answer);
|
|
832
|
+
if (notice !== null) {
|
|
833
|
+
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
837
|
+
}
|
|
838
|
+
}, async (error) => {
|
|
839
|
+
if (running.adapter.connection.signal.aborted) {
|
|
840
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
this.took(turn);
|
|
844
|
+
if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
|
|
845
|
+
running.adapter.retire();
|
|
846
|
+
this.detach(conversation, running);
|
|
847
|
+
}
|
|
848
|
+
this.end(conversation, turn, answeredWith(error));
|
|
406
849
|
}
|
|
407
850
|
});
|
|
408
851
|
}
|
|
409
|
-
attach(turn, sent) {
|
|
410
|
-
turn.prompt = sent;
|
|
411
|
-
sent.turn = turn;
|
|
412
|
-
}
|
|
413
|
-
answered(conversation, sent, failed) {
|
|
414
|
-
const turn = sent.turn;
|
|
415
|
-
if (turn === null) {
|
|
416
|
-
const queued = conversation.queued.indexOf(sent);
|
|
417
|
-
if (queued < 0)
|
|
418
|
-
return;
|
|
419
|
-
conversation.queued.splice(queued, 1);
|
|
420
|
-
if (failed !== null)
|
|
421
|
-
this.failed(conversation, failed);
|
|
422
|
-
}
|
|
423
|
-
else if (failed !== null) {
|
|
424
|
-
if (turn.ended)
|
|
425
|
-
this.failed(conversation, failed);
|
|
426
|
-
else
|
|
427
|
-
this.end(conversation, turn, { failed });
|
|
428
|
-
}
|
|
429
|
-
else if (turn.prompt === sent) {
|
|
430
|
-
this.end(conversation, turn, { text: turn.text });
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
852
|
started(conversation) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const turn = this.begin(conversation);
|
|
437
|
-
const queued = conversation.queued.shift();
|
|
438
|
-
if (queued !== undefined)
|
|
439
|
-
this.attach(turn, queued);
|
|
853
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
854
|
+
this.took(turn);
|
|
440
855
|
return turn;
|
|
441
856
|
}
|
|
442
|
-
ended(conversation) {
|
|
857
|
+
ended(conversation, stop) {
|
|
443
858
|
const turn = conversation.turn;
|
|
444
859
|
if (turn === null)
|
|
445
860
|
return;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
this.kit.changed();
|
|
861
|
+
const held = stop !== null && !turn.steered && [...conversation.subagents.values()].some((subagent) => subagent.turn === turn);
|
|
862
|
+
if (!turn.prompted || held) {
|
|
863
|
+
this.took(turn);
|
|
864
|
+
this.end(conversation, turn, stop === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
865
|
+
return;
|
|
452
866
|
}
|
|
867
|
+
if (stop !== null)
|
|
868
|
+
return;
|
|
869
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
870
|
+
this.apply(conversation, turn, event);
|
|
871
|
+
conversation.turn = null;
|
|
872
|
+
this.kit.changed();
|
|
453
873
|
}
|
|
454
874
|
async ask(conversation, method, params, secret, signal) {
|
|
455
875
|
const turn = this.started(conversation);
|
|
@@ -495,65 +915,126 @@ export class Conversations {
|
|
|
495
915
|
if (content !== null)
|
|
496
916
|
this.questions.get(interactionId)?.answer({ action: 'accept', content });
|
|
497
917
|
}
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
918
|
+
read(conversation, reader, message) {
|
|
919
|
+
for (const event of reader.read(message))
|
|
920
|
+
this.event(conversation, event);
|
|
921
|
+
}
|
|
922
|
+
event(conversation, event) {
|
|
923
|
+
switch (event.kind) {
|
|
924
|
+
case 'started':
|
|
925
|
+
this.started(conversation);
|
|
926
|
+
return;
|
|
927
|
+
case 'ended':
|
|
928
|
+
this.ended(conversation, event.stop);
|
|
929
|
+
return;
|
|
930
|
+
case 'commands':
|
|
931
|
+
this.commands(conversation, event.commands);
|
|
932
|
+
return;
|
|
933
|
+
case 'options':
|
|
934
|
+
this.options(conversation, event.options);
|
|
935
|
+
return;
|
|
936
|
+
case 'context':
|
|
937
|
+
conversation.context = { used: event.used, window: event.window };
|
|
938
|
+
conversation.turn?.parts.measure(conversation.context);
|
|
939
|
+
return;
|
|
940
|
+
case 'subagent-done':
|
|
941
|
+
this.subagentDone(conversation, event.id, event.result);
|
|
942
|
+
return;
|
|
943
|
+
case 'subagent-activity':
|
|
944
|
+
this.subagentActivity(conversation, event.id, event.activity);
|
|
945
|
+
return;
|
|
946
|
+
case 'marker':
|
|
947
|
+
case 'image':
|
|
948
|
+
if (conversation.turn !== null)
|
|
949
|
+
break;
|
|
950
|
+
if (event.kind === 'marker' && event.marker === 'job')
|
|
951
|
+
conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
|
|
952
|
+
return;
|
|
515
953
|
}
|
|
516
|
-
|
|
517
|
-
|
|
954
|
+
this.apply(conversation, this.started(conversation), event);
|
|
955
|
+
}
|
|
956
|
+
took(turn) {
|
|
957
|
+
const taken = turn.taken;
|
|
958
|
+
turn.taken = null;
|
|
959
|
+
taken?.();
|
|
960
|
+
}
|
|
961
|
+
apply(conversation, turn, event) {
|
|
962
|
+
switch (event.kind) {
|
|
963
|
+
case 'text':
|
|
964
|
+
case 'reasoning':
|
|
965
|
+
turn.parts.write(event.kind, event.id, event.text);
|
|
966
|
+
return;
|
|
967
|
+
case 'command':
|
|
968
|
+
turn.parts.command();
|
|
969
|
+
return;
|
|
970
|
+
case 'subagent': {
|
|
971
|
+
const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
|
|
972
|
+
conversation.subagents.set(event.id, { description: event.description, turn, index, activity: null });
|
|
973
|
+
this.subagentsChanged(conversation);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
case 'message':
|
|
977
|
+
turn.parts.start({ type: 'message', command: event.command, target: event.target });
|
|
978
|
+
return;
|
|
979
|
+
case 'marker':
|
|
980
|
+
turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
|
|
981
|
+
return;
|
|
982
|
+
case 'plan':
|
|
983
|
+
turn.parts.plan(event.entries);
|
|
984
|
+
return;
|
|
985
|
+
case 'image':
|
|
986
|
+
turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
|
|
987
|
+
this.kit.changed();
|
|
988
|
+
return;
|
|
518
989
|
}
|
|
519
990
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
991
|
+
subagentDone(conversation, id, result) {
|
|
992
|
+
const subagent = conversation.subagents.get(id);
|
|
993
|
+
if (subagent === undefined)
|
|
994
|
+
return;
|
|
995
|
+
conversation.subagents.delete(id);
|
|
996
|
+
this.subagentsChanged(conversation);
|
|
997
|
+
if (subagent.turn === conversation.turn) {
|
|
998
|
+
subagent.turn.parts.done(subagent.index, result);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const part = { type: 'subagent', description: subagent.description, status: 'done', result };
|
|
1002
|
+
if (conversation.turn === null)
|
|
1003
|
+
conversation.held.push(part);
|
|
523
1004
|
else
|
|
524
|
-
conversation.
|
|
525
|
-
this.kit.changed();
|
|
1005
|
+
conversation.turn.parts.start(part);
|
|
526
1006
|
}
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
turn
|
|
540
|
-
|
|
541
|
-
|
|
1007
|
+
subagentActivity(conversation, id, activity) {
|
|
1008
|
+
const subagent = conversation.subagents.get(id);
|
|
1009
|
+
if (subagent === undefined || subagent.activity === activity)
|
|
1010
|
+
return;
|
|
1011
|
+
subagent.activity = activity;
|
|
1012
|
+
this.subagentsChanged(conversation);
|
|
1013
|
+
}
|
|
1014
|
+
async image(conversation, turn, reserved, data, mediaType) {
|
|
1015
|
+
const bytes = Buffer.from(data, 'base64');
|
|
1016
|
+
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
1017
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
1018
|
+
try {
|
|
1019
|
+
const declared = (await this.deliver(conversation, turn, 'images', {
|
|
1020
|
+
name,
|
|
1021
|
+
media_type: mediaType,
|
|
1022
|
+
bytes: bytes.length,
|
|
1023
|
+
sha256,
|
|
1024
|
+
}));
|
|
1025
|
+
const saved = await uploaded(declared.upload, {
|
|
1026
|
+
name,
|
|
1027
|
+
bytes: bytes.length,
|
|
1028
|
+
sha256,
|
|
1029
|
+
read: (start, end) => [bytes.subarray(start, end)],
|
|
1030
|
+
send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
|
|
1031
|
+
finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
|
|
1032
|
+
});
|
|
1033
|
+
turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
|
|
542
1034
|
}
|
|
543
|
-
|
|
544
|
-
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
1037
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
|
|
545
1038
|
}
|
|
546
1039
|
}
|
|
547
|
-
plan(conversation, turn) {
|
|
548
|
-
const sent = this.kit.send({
|
|
549
|
-
type: 'plan',
|
|
550
|
-
conversation_id: conversation.id,
|
|
551
|
-
turn_id: turn.id,
|
|
552
|
-
sequence: turn.planSequence + 1,
|
|
553
|
-
from: 0,
|
|
554
|
-
steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
|
|
555
|
-
});
|
|
556
|
-
if (sent)
|
|
557
|
-
turn.planSequence++;
|
|
558
|
-
}
|
|
559
1040
|
}
|