@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c073f4e9391b2
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 +44 -26
- package/dist/abort.js +9 -0
- package/dist/acp.js +18 -75
- package/dist/agents.js +266 -53
- package/dist/api.js +40 -4
- package/dist/app-git.js +64 -0
- package/dist/bootstrap.json +10 -14
- package/dist/bridge.js +89 -71
- package/dist/chatgpt.js +25 -0
- package/dist/clis-main.js +23 -0
- package/dist/clis.js +203 -71
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +328 -199
- package/dist/copies.js +188 -161
- package/dist/end.js +22 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +521 -0
- package/dist/files.js +16 -1
- package/dist/git-main.js +2 -2
- package/dist/git.js +6 -2
- package/dist/home.js +72 -5
- package/dist/house-main.js +181 -57
- package/dist/instructions.js +16 -13
- package/dist/kit-main.js +55 -4
- package/dist/login.js +108 -97
- package/dist/logout.js +21 -0
- package/dist/mcp.js +39 -0
- package/dist/operation.js +7 -4
- package/dist/parts.js +82 -0
- package/dist/placement.js +48 -0
- package/dist/refusals.js +53 -0
- package/dist/relay.js +20 -0
- package/dist/resident.js +70 -34
- package/dist/scope.js +203 -0
- package/dist/secret-input.js +7 -2
- package/dist/service-main.js +9 -0
- package/dist/service.js +33 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +48 -11
- package/dist/skills.js +5 -10
- package/dist/stream.js +37 -7
- package/dist/transcription.js +170 -0
- package/dist/update.js +140 -0
- package/dist/wire.js +6 -0
- package/dist/work.js +348 -0
- package/how-we-work.md +10 -12
- package/package.json +4 -7
- package/dist/blocks.js +0 -51
package/dist/conversations.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
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 { startAdapter } from './acp.js';
|
|
7
7
|
import { openBridge } from './bridge.js';
|
|
8
8
|
import { CLIS } from './clis.js';
|
|
9
|
-
import {
|
|
9
|
+
import { readerFor } from './events.js';
|
|
10
|
+
import { placeFiles, uploadBytes } from './files.js';
|
|
11
|
+
import { agentBase } from './home.js';
|
|
10
12
|
import { instructions } from './instructions.js';
|
|
13
|
+
import { Parts } from './parts.js';
|
|
14
|
+
import { killMarked, Scope } from './scope.js';
|
|
11
15
|
import { holdSecretInput } from './secret-input.js';
|
|
12
16
|
import { createHowWeWork } from './skills.js';
|
|
13
|
-
|
|
17
|
+
import { transcribe } from './transcription.js';
|
|
18
|
+
import { workFor } from './work.js';
|
|
14
19
|
const KILLED = 'the conversation was killed';
|
|
20
|
+
export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
|
|
15
21
|
function causeOf(error) {
|
|
16
22
|
return error instanceof Error ? error.message : String(error);
|
|
17
23
|
}
|
|
@@ -21,42 +27,40 @@ function answerMessage(request, response) {
|
|
|
21
27
|
function logged(error) {
|
|
22
28
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
23
29
|
}
|
|
24
|
-
function
|
|
25
|
-
return
|
|
26
|
-
const killed = () => reject(signal.reason);
|
|
27
|
-
signal.addEventListener('abort', killed, { once: true });
|
|
28
|
-
if (signal.aborted)
|
|
29
|
-
killed();
|
|
30
|
-
work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
function stop(bridge) {
|
|
34
|
-
killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
|
|
30
|
+
function marker(bridge) {
|
|
31
|
+
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
35
32
|
}
|
|
36
33
|
class Turn {
|
|
37
34
|
id = randomUUID();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
unsentFrom = null;
|
|
41
|
-
draftSequence = 0;
|
|
42
|
-
planSequence = 0;
|
|
43
|
-
entries = null;
|
|
35
|
+
parts;
|
|
36
|
+
admitted = false;
|
|
44
37
|
questions = 0;
|
|
45
38
|
secrets = 0;
|
|
46
|
-
|
|
39
|
+
prompted = false;
|
|
47
40
|
ended = false;
|
|
41
|
+
images = 0;
|
|
42
|
+
uploads = [];
|
|
43
|
+
constructor(emit) {
|
|
44
|
+
this.parts = new Parts(emit);
|
|
45
|
+
}
|
|
48
46
|
}
|
|
49
47
|
class Conversation {
|
|
50
48
|
id;
|
|
51
49
|
running = null;
|
|
50
|
+
session = null;
|
|
52
51
|
turn = null;
|
|
53
52
|
commands = null;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
context = null;
|
|
54
|
+
leftovers = new Set();
|
|
55
|
+
waiting = [];
|
|
56
|
+
sending = null;
|
|
57
|
+
subagents = new Map();
|
|
58
|
+
held = [];
|
|
57
59
|
opening = null;
|
|
58
60
|
kills = 0;
|
|
59
61
|
killed = new AbortController();
|
|
62
|
+
interrupts = 0;
|
|
63
|
+
interrupted = new AbortController();
|
|
60
64
|
open = 0;
|
|
61
65
|
queue = Promise.resolve();
|
|
62
66
|
reports = Promise.resolve();
|
|
@@ -79,8 +83,11 @@ export class Conversations {
|
|
|
79
83
|
questions = new Map();
|
|
80
84
|
carrying = 0;
|
|
81
85
|
reporting = 0;
|
|
86
|
+
watched = false;
|
|
82
87
|
constructor(kit) {
|
|
83
88
|
this.kit = kit;
|
|
89
|
+
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
90
|
+
setInterval(() => this.look(), 2_000);
|
|
84
91
|
}
|
|
85
92
|
input(input) {
|
|
86
93
|
this.carrying++;
|
|
@@ -90,10 +97,17 @@ export class Conversations {
|
|
|
90
97
|
if (input.kind === 'kill') {
|
|
91
98
|
conversation.kills++;
|
|
92
99
|
conversation.killed.abort(new Error(KILLED));
|
|
100
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
101
|
+
waiting.settle({ refused: KILLED });
|
|
93
102
|
this.halt(conversation);
|
|
94
103
|
}
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
if (input.kind === 'interrupt') {
|
|
105
|
+
conversation.interrupts++;
|
|
106
|
+
conversation.interrupted.abort();
|
|
107
|
+
}
|
|
108
|
+
const handled = conversation.queue.then(() => this.carry(conversation, input));
|
|
109
|
+
conversation.queue = handled.catch(() => undefined);
|
|
110
|
+
const work = handled.then(({ ack }) => ack);
|
|
97
111
|
this.acks.set(input.input_id, work);
|
|
98
112
|
return work;
|
|
99
113
|
})();
|
|
@@ -111,15 +125,39 @@ export class Conversations {
|
|
|
111
125
|
for (const conversation of this.conversations.values()) {
|
|
112
126
|
const turn = conversation.turn;
|
|
113
127
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
114
|
-
if (conversation.
|
|
128
|
+
if (conversation.open > (asking ? 1 : 0))
|
|
115
129
|
return false;
|
|
116
130
|
}
|
|
117
|
-
|
|
131
|
+
const conversations = [...this.conversations.values()];
|
|
132
|
+
if (conversations.some((conversation) => conversation.running?.work.busy()))
|
|
133
|
+
return false;
|
|
134
|
+
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
135
|
+
}
|
|
136
|
+
quiet() {
|
|
137
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
|
|
138
|
+
}
|
|
139
|
+
look() {
|
|
140
|
+
for (const conversation of this.conversations.values()) {
|
|
141
|
+
for (const scope of conversation.leftovers) {
|
|
142
|
+
if (!scope.empty())
|
|
143
|
+
continue;
|
|
144
|
+
scope.close();
|
|
145
|
+
conversation.leftovers.delete(scope);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
this.kit.changed();
|
|
118
149
|
}
|
|
119
150
|
opened() {
|
|
151
|
+
this.watched = false;
|
|
152
|
+
}
|
|
153
|
+
watching(watched) {
|
|
154
|
+
const resend = watched && !this.watched;
|
|
155
|
+
this.watched = watched;
|
|
156
|
+
if (!resend)
|
|
157
|
+
return;
|
|
120
158
|
for (const conversation of this.conversations.values()) {
|
|
121
159
|
if (conversation.turn !== null)
|
|
122
|
-
conversation.turn
|
|
160
|
+
this.replay(conversation, conversation.turn);
|
|
123
161
|
}
|
|
124
162
|
}
|
|
125
163
|
conversation(id) {
|
|
@@ -132,13 +170,17 @@ export class Conversations {
|
|
|
132
170
|
}
|
|
133
171
|
async carry(conversation, input) {
|
|
134
172
|
if (input.kind === 'open')
|
|
135
|
-
return this.open(conversation, input);
|
|
173
|
+
return { ack: Promise.resolve(await this.open(conversation, input)) };
|
|
136
174
|
if (input.kind === 'message')
|
|
137
175
|
return this.message(conversation, input);
|
|
138
|
-
if (input.kind === 'interrupt')
|
|
176
|
+
if (input.kind === 'interrupt') {
|
|
177
|
+
if (--conversation.interrupts === 0)
|
|
178
|
+
conversation.interrupted = new AbortController();
|
|
139
179
|
await this.interrupt(conversation, String(input.turn_id));
|
|
180
|
+
}
|
|
140
181
|
if (input.kind === 'kill') {
|
|
141
182
|
await this.kill(conversation);
|
|
183
|
+
await conversation.sending;
|
|
142
184
|
if (--conversation.kills === 0)
|
|
143
185
|
conversation.killed = new AbortController();
|
|
144
186
|
}
|
|
@@ -147,11 +189,12 @@ export class Conversations {
|
|
|
147
189
|
if (input.kind === 'answer') {
|
|
148
190
|
const question = this.questions.get(String(input.interaction_id));
|
|
149
191
|
if (question === undefined) {
|
|
150
|
-
|
|
192
|
+
const text = answerMessage(input.request, input.response);
|
|
193
|
+
return this.message(conversation, { ...input, text, files: [], first: false });
|
|
151
194
|
}
|
|
152
195
|
question.answer(input.response);
|
|
153
196
|
}
|
|
154
|
-
return {};
|
|
197
|
+
return { ack: Promise.resolve({}) };
|
|
155
198
|
}
|
|
156
199
|
async open(conversation, input) {
|
|
157
200
|
if (conversation.running !== null)
|
|
@@ -164,35 +207,88 @@ export class Conversations {
|
|
|
164
207
|
}
|
|
165
208
|
}
|
|
166
209
|
async message(conversation, input) {
|
|
210
|
+
const content = [{ type: 'text', text: String(input.text) }];
|
|
211
|
+
let route;
|
|
212
|
+
try {
|
|
213
|
+
route = await this.route(conversation, input.agent_id);
|
|
214
|
+
const files = input.files;
|
|
215
|
+
const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
|
|
216
|
+
const lines = await this.transcribed(conversation, files, paths);
|
|
217
|
+
if (lines.length > 0)
|
|
218
|
+
content.push({ type: 'text', text: lines.join('\n') });
|
|
219
|
+
if (conversation.kills > 0)
|
|
220
|
+
throw new Error(KILLED);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
ack: new Promise((settle) => {
|
|
227
|
+
conversation.waiting.push({ input, route, content, settle });
|
|
228
|
+
this.next(conversation);
|
|
229
|
+
}),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
next(conversation) {
|
|
233
|
+
if (conversation.sending !== null || conversation.open > 0)
|
|
234
|
+
return;
|
|
235
|
+
const waiting = conversation.waiting.shift();
|
|
236
|
+
if (waiting === undefined)
|
|
237
|
+
return;
|
|
238
|
+
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
239
|
+
conversation.sending = null;
|
|
240
|
+
this.next(conversation);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
async send(conversation, waiting) {
|
|
244
|
+
const { input } = waiting;
|
|
167
245
|
let ack = {};
|
|
168
246
|
const prompt = [];
|
|
169
247
|
try {
|
|
170
|
-
const route = await this.route(conversation, input.agent_id);
|
|
171
|
-
const paths = await placeFiles(this.kit.house, route.working_directory, input.files, conversation.killed.signal);
|
|
172
248
|
if (conversation.running === null) {
|
|
173
|
-
const
|
|
174
|
-
|
|
249
|
+
const resumed = input.provider_session_id ?? conversation.session;
|
|
250
|
+
const opened = await this.start(conversation, waiting.route, resumed);
|
|
251
|
+
if (resumed === null)
|
|
175
252
|
ack = { provider_session_id: opened };
|
|
176
253
|
}
|
|
177
254
|
if (input.first === true) {
|
|
178
255
|
prompt.push({
|
|
179
256
|
type: 'text',
|
|
180
|
-
text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
|
|
257
|
+
text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
|
|
181
258
|
});
|
|
182
259
|
}
|
|
183
|
-
prompt.push({ type: 'text', text: String(input.text) });
|
|
184
|
-
if (paths.length > 0)
|
|
185
|
-
prompt.push({ type: 'text', text: paths.join('\n') });
|
|
186
260
|
if (conversation.kills > 0)
|
|
187
261
|
throw new Error(KILLED);
|
|
188
262
|
}
|
|
189
263
|
catch (error) {
|
|
190
|
-
if ('provider_session_id' in ack)
|
|
264
|
+
if ('provider_session_id' in ack) {
|
|
265
|
+
conversation.session = null;
|
|
191
266
|
await this.kill(conversation);
|
|
192
|
-
|
|
267
|
+
}
|
|
268
|
+
waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.prompt(conversation, [...prompt, ...waiting.content]);
|
|
272
|
+
waiting.settle(ack);
|
|
273
|
+
}
|
|
274
|
+
async transcribed(conversation, files, paths) {
|
|
275
|
+
const lines = [];
|
|
276
|
+
for (const [index, path] of paths.entries()) {
|
|
277
|
+
lines.push(path);
|
|
278
|
+
if (files[index].media_type?.startsWith('audio/') !== true)
|
|
279
|
+
continue;
|
|
280
|
+
try {
|
|
281
|
+
const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
|
|
282
|
+
if (text !== null && text !== '')
|
|
283
|
+
lines.push(`Transcript: ${text}`);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (conversation.kills > 0)
|
|
287
|
+
throw error;
|
|
288
|
+
logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
|
|
289
|
+
}
|
|
193
290
|
}
|
|
194
|
-
|
|
195
|
-
return ack;
|
|
291
|
+
return lines;
|
|
196
292
|
}
|
|
197
293
|
async interrupt(conversation, turnId) {
|
|
198
294
|
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
@@ -203,11 +299,13 @@ export class Conversations {
|
|
|
203
299
|
}
|
|
204
300
|
halt(conversation) {
|
|
205
301
|
if (conversation.opening !== null)
|
|
206
|
-
|
|
302
|
+
killMarked(marker(conversation.opening));
|
|
303
|
+
for (const scope of conversation.leftovers)
|
|
304
|
+
scope.kill();
|
|
207
305
|
if (conversation.running === null)
|
|
208
306
|
return;
|
|
209
307
|
conversation.running.killed = true;
|
|
210
|
-
|
|
308
|
+
conversation.running.scope.kill();
|
|
211
309
|
}
|
|
212
310
|
async kill(conversation) {
|
|
213
311
|
const running = conversation.running;
|
|
@@ -219,6 +317,7 @@ export class Conversations {
|
|
|
219
317
|
if (running === null)
|
|
220
318
|
return;
|
|
221
319
|
try {
|
|
320
|
+
running.reader.requested(option, value);
|
|
222
321
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
223
322
|
sessionId: running.sessionId,
|
|
224
323
|
configId: option,
|
|
@@ -227,54 +326,62 @@ export class Conversations {
|
|
|
227
326
|
this.options(conversation, answered.configOptions);
|
|
228
327
|
}
|
|
229
328
|
catch (error) {
|
|
329
|
+
running.reader.refused(option);
|
|
230
330
|
logged(error);
|
|
231
331
|
}
|
|
232
332
|
}
|
|
233
333
|
async route(conversation, agentId) {
|
|
234
334
|
const signal = conversation.killed.signal;
|
|
235
|
-
await
|
|
335
|
+
await unlessAborted(this.kit.agents.read(), signal);
|
|
236
336
|
if (this.kit.agents.route(agentId) === undefined)
|
|
237
|
-
await
|
|
337
|
+
await unlessAborted(this.kit.agents.refresh(), signal);
|
|
238
338
|
const route = this.kit.agents.route(agentId);
|
|
239
339
|
if (route === undefined)
|
|
240
340
|
throw new Error(`this Environment hosts no Agent ${agentId}`);
|
|
241
|
-
if (route.working_directory === join(
|
|
341
|
+
if (route.working_directory === join(agentBase(), route.agent_id)) {
|
|
242
342
|
await mkdir(route.working_directory, { recursive: true });
|
|
243
343
|
}
|
|
244
344
|
return route;
|
|
245
345
|
}
|
|
246
346
|
async start(conversation, route, sessionId) {
|
|
347
|
+
const reader = readerFor(route.kind);
|
|
348
|
+
reader.session = sessionId;
|
|
247
349
|
const app = client({ name: '@agentshouse/kit' })
|
|
248
|
-
.onNotification('session/update', ({ params }) => this.update(conversation, params))
|
|
249
|
-
.onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
|
|
250
|
-
.onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
|
|
251
350
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
252
351
|
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
|
|
253
|
-
await
|
|
352
|
+
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
254
353
|
const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
255
354
|
if (conversation.kills > 0) {
|
|
256
355
|
bridge.close();
|
|
257
356
|
throw new Error(KILLED);
|
|
258
357
|
}
|
|
259
358
|
conversation.opening = bridge;
|
|
359
|
+
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
360
|
+
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
260
361
|
try {
|
|
261
|
-
await
|
|
262
|
-
|
|
362
|
+
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
363
|
+
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
364
|
+
const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
365
|
+
work.read(message);
|
|
366
|
+
this.read(conversation, reader, message);
|
|
367
|
+
}, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
|
|
263
368
|
void adapter.exited.then(() => bridge.close());
|
|
264
369
|
const agent = adapter.connection.agent;
|
|
265
370
|
const cwd = route.working_directory;
|
|
371
|
+
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
266
372
|
const opened = sessionId === null
|
|
267
|
-
? await agent.request('session/new', { cwd, mcpServers: [] })
|
|
268
|
-
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [] })), sessionId };
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
sessionId: opened.sessionId,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
};
|
|
373
|
+
? await agent.request('session/new', { cwd, mcpServers: [], _meta })
|
|
374
|
+
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
|
|
375
|
+
reader.session = opened.sessionId;
|
|
376
|
+
if (sessionId === null && CLIS[route.kind].renames) {
|
|
377
|
+
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
378
|
+
await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
|
|
379
|
+
}
|
|
380
|
+
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
276
381
|
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
382
|
+
void work.settled().then(() => scope.record());
|
|
277
383
|
conversation.running = running;
|
|
384
|
+
conversation.session = running.sessionId;
|
|
278
385
|
this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
|
|
279
386
|
if (conversation.commands !== null)
|
|
280
387
|
this.commands(conversation, conversation.commands);
|
|
@@ -283,7 +390,9 @@ export class Conversations {
|
|
|
283
390
|
return running.sessionId;
|
|
284
391
|
}
|
|
285
392
|
catch (error) {
|
|
286
|
-
|
|
393
|
+
scope.kill();
|
|
394
|
+
work.close();
|
|
395
|
+
conversation.leftovers.add(scope);
|
|
287
396
|
bridge.close();
|
|
288
397
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
289
398
|
}
|
|
@@ -294,6 +403,7 @@ export class Conversations {
|
|
|
294
403
|
async launchSettings(running, route, offered) {
|
|
295
404
|
let options = offered;
|
|
296
405
|
const settings = [
|
|
406
|
+
['mode', route.mode ?? CLIS[route.kind].fullAccess],
|
|
297
407
|
['model', route.model],
|
|
298
408
|
['thought_level', route.effort],
|
|
299
409
|
];
|
|
@@ -301,6 +411,7 @@ export class Conversations {
|
|
|
301
411
|
if (value === null)
|
|
302
412
|
continue;
|
|
303
413
|
const option = options.find((candidate) => candidate.category === category);
|
|
414
|
+
running.reader.requested(option?.id ?? category, value);
|
|
304
415
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
305
416
|
sessionId: running.sessionId,
|
|
306
417
|
configId: option?.id ?? category,
|
|
@@ -314,19 +425,23 @@ export class Conversations {
|
|
|
314
425
|
const running = conversation.running;
|
|
315
426
|
if (running?.adapter !== adapter)
|
|
316
427
|
return;
|
|
428
|
+
this.detach(conversation, running);
|
|
429
|
+
if (conversation.turn !== null) {
|
|
430
|
+
this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
|
|
431
|
+
}
|
|
432
|
+
this.next(conversation);
|
|
433
|
+
this.kit.changed();
|
|
434
|
+
}
|
|
435
|
+
detach(conversation, running) {
|
|
436
|
+
running.work.close();
|
|
437
|
+
conversation.leftovers.add(running.scope);
|
|
317
438
|
conversation.running = null;
|
|
318
439
|
conversation.commands = null;
|
|
319
|
-
conversation.jobs.clear();
|
|
320
|
-
conversation.queued.length = 0;
|
|
321
440
|
for (const [id, question] of this.questions) {
|
|
322
441
|
if (question.conversation === conversation)
|
|
323
442
|
this.questions.delete(id);
|
|
324
443
|
}
|
|
325
444
|
this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
|
|
326
|
-
if (conversation.turn !== null) {
|
|
327
|
-
this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
|
|
328
|
-
}
|
|
329
|
-
this.kit.changed();
|
|
330
445
|
}
|
|
331
446
|
commands(conversation, commands) {
|
|
332
447
|
conversation.commands = commands;
|
|
@@ -341,7 +456,7 @@ export class Conversations {
|
|
|
341
456
|
}
|
|
342
457
|
deliver(conversation, turn, event, body) {
|
|
343
458
|
this.reporting++;
|
|
344
|
-
const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
|
|
459
|
+
const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
|
|
345
460
|
conversation.reports = delivered
|
|
346
461
|
.catch(() => undefined)
|
|
347
462
|
.finally(() => {
|
|
@@ -350,105 +465,82 @@ export class Conversations {
|
|
|
350
465
|
});
|
|
351
466
|
return delivered;
|
|
352
467
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
.then(() => this.admitted(conversation, turn))
|
|
358
|
-
.catch(logged);
|
|
359
|
-
return turn;
|
|
468
|
+
write(conversation, turn, operation) {
|
|
469
|
+
if (!turn.admitted || turn.ended || !this.watched)
|
|
470
|
+
return;
|
|
471
|
+
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
360
472
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
this.draft(conversation, turn);
|
|
365
|
-
}
|
|
366
|
-
if (turn.entries !== null)
|
|
367
|
-
this.plan(conversation, turn);
|
|
473
|
+
replay(conversation, turn) {
|
|
474
|
+
for (const operation of turn.parts.operations())
|
|
475
|
+
this.write(conversation, turn, operation);
|
|
368
476
|
}
|
|
369
477
|
begin(conversation) {
|
|
370
|
-
const turn = this.
|
|
478
|
+
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
479
|
+
conversation.open++;
|
|
480
|
+
void this.deliver(conversation, turn, 'started', {})
|
|
481
|
+
.then(() => {
|
|
482
|
+
turn.admitted = true;
|
|
483
|
+
this.replay(conversation, turn);
|
|
484
|
+
})
|
|
485
|
+
.catch(logged);
|
|
371
486
|
conversation.turn = turn;
|
|
487
|
+
if (conversation.context !== null)
|
|
488
|
+
turn.parts.measure(conversation.context);
|
|
489
|
+
for (const part of conversation.held.splice(0))
|
|
490
|
+
turn.parts.start(part);
|
|
372
491
|
this.kit.changed();
|
|
373
492
|
return turn;
|
|
374
493
|
}
|
|
375
|
-
end(conversation, turn,
|
|
494
|
+
end(conversation, turn, failed) {
|
|
376
495
|
if (turn.ended)
|
|
377
496
|
return;
|
|
497
|
+
if (conversation.turn === turn) {
|
|
498
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
499
|
+
this.apply(conversation, turn, event);
|
|
500
|
+
}
|
|
378
501
|
turn.ended = true;
|
|
379
502
|
conversation.open--;
|
|
380
503
|
if (conversation.turn === turn)
|
|
381
504
|
conversation.turn = null;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
505
|
+
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
506
|
+
parts: turn.parts.saved(),
|
|
507
|
+
context: turn.parts.context,
|
|
508
|
+
...failed,
|
|
509
|
+
}));
|
|
510
|
+
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
511
|
+
this.next(conversation);
|
|
386
512
|
this.kit.changed();
|
|
387
513
|
}
|
|
388
|
-
failed(conversation, cause) {
|
|
389
|
-
if (conversation.open > 0)
|
|
390
|
-
conversation.late.push(cause);
|
|
391
|
-
else
|
|
392
|
-
this.end(conversation, this.report(conversation), { failed: cause });
|
|
393
|
-
}
|
|
394
514
|
prompt(conversation, prompt) {
|
|
395
515
|
const running = conversation.running;
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
this.end(conversation,
|
|
516
|
+
const turn = this.begin(conversation);
|
|
517
|
+
turn.prompted = true;
|
|
518
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
|
|
519
|
+
if (running.adapter.connection.signal.aborted) {
|
|
520
|
+
this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
if (conversation.open === 1 && conversation.running === running) {
|
|
524
|
+
running.scope.kill();
|
|
525
|
+
this.detach(conversation, running);
|
|
526
|
+
}
|
|
527
|
+
this.end(conversation, turn, { failed: causeOf(error) });
|
|
408
528
|
}
|
|
409
529
|
});
|
|
410
530
|
}
|
|
411
|
-
attach(turn, sent) {
|
|
412
|
-
turn.prompt = sent;
|
|
413
|
-
sent.turn = turn;
|
|
414
|
-
}
|
|
415
|
-
answered(conversation, sent, failed) {
|
|
416
|
-
const turn = sent.turn;
|
|
417
|
-
if (turn === null) {
|
|
418
|
-
const queued = conversation.queued.indexOf(sent);
|
|
419
|
-
if (queued < 0)
|
|
420
|
-
return;
|
|
421
|
-
conversation.queued.splice(queued, 1);
|
|
422
|
-
if (failed !== null)
|
|
423
|
-
this.failed(conversation, failed);
|
|
424
|
-
}
|
|
425
|
-
else if (failed !== null) {
|
|
426
|
-
if (turn.ended)
|
|
427
|
-
this.failed(conversation, failed);
|
|
428
|
-
else
|
|
429
|
-
this.end(conversation, turn, { failed });
|
|
430
|
-
}
|
|
431
|
-
else if (turn.prompt === sent) {
|
|
432
|
-
this.end(conversation, turn, { text: turn.text });
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
531
|
started(conversation) {
|
|
436
|
-
|
|
437
|
-
return conversation.turn;
|
|
438
|
-
const turn = this.begin(conversation);
|
|
439
|
-
const queued = conversation.queued.shift();
|
|
440
|
-
if (queued !== undefined)
|
|
441
|
-
this.attach(turn, queued);
|
|
442
|
-
return turn;
|
|
532
|
+
return conversation.turn ?? this.begin(conversation);
|
|
443
533
|
}
|
|
444
534
|
ended(conversation) {
|
|
445
535
|
const turn = conversation.turn;
|
|
446
536
|
if (turn === null)
|
|
447
537
|
return;
|
|
448
|
-
if (turn.
|
|
449
|
-
this.end(conversation, turn,
|
|
538
|
+
if (!turn.prompted) {
|
|
539
|
+
this.end(conversation, turn, null);
|
|
450
540
|
}
|
|
451
541
|
else {
|
|
542
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
543
|
+
this.apply(conversation, turn, event);
|
|
452
544
|
conversation.turn = null;
|
|
453
545
|
this.kit.changed();
|
|
454
546
|
}
|
|
@@ -497,65 +589,102 @@ export class Conversations {
|
|
|
497
589
|
if (content !== null)
|
|
498
590
|
this.questions.get(interactionId)?.answer({ action: 'accept', content });
|
|
499
591
|
}
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
592
|
+
read(conversation, reader, message) {
|
|
593
|
+
for (const event of reader.read(message))
|
|
594
|
+
this.event(conversation, event);
|
|
595
|
+
}
|
|
596
|
+
event(conversation, event) {
|
|
597
|
+
switch (event.kind) {
|
|
598
|
+
case 'started':
|
|
599
|
+
this.started(conversation);
|
|
600
|
+
return;
|
|
601
|
+
case 'ended':
|
|
602
|
+
this.ended(conversation);
|
|
603
|
+
return;
|
|
604
|
+
case 'commands':
|
|
605
|
+
this.commands(conversation, event.commands);
|
|
606
|
+
return;
|
|
607
|
+
case 'options':
|
|
608
|
+
this.options(conversation, event.options);
|
|
609
|
+
return;
|
|
610
|
+
case 'context':
|
|
611
|
+
conversation.context = { used: event.used, window: event.window };
|
|
612
|
+
conversation.turn?.parts.measure(conversation.context);
|
|
613
|
+
return;
|
|
614
|
+
case 'subagent-done':
|
|
615
|
+
this.subagentDone(conversation, event.id, event.result);
|
|
616
|
+
return;
|
|
617
|
+
case 'marker':
|
|
618
|
+
case 'image':
|
|
619
|
+
if (conversation.turn !== null)
|
|
620
|
+
break;
|
|
621
|
+
if (event.kind === 'marker' && event.marker === 'job')
|
|
622
|
+
conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
|
|
623
|
+
return;
|
|
517
624
|
}
|
|
518
|
-
|
|
519
|
-
|
|
625
|
+
this.apply(conversation, this.started(conversation), event);
|
|
626
|
+
}
|
|
627
|
+
apply(conversation, turn, event) {
|
|
628
|
+
switch (event.kind) {
|
|
629
|
+
case 'text':
|
|
630
|
+
case 'reasoning':
|
|
631
|
+
turn.parts.write(event.kind, event.id, event.text);
|
|
632
|
+
return;
|
|
633
|
+
case 'command':
|
|
634
|
+
turn.parts.command();
|
|
635
|
+
return;
|
|
636
|
+
case 'subagent': {
|
|
637
|
+
const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
|
|
638
|
+
conversation.subagents.set(event.id, { description: event.description, turn, index });
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
case 'message':
|
|
642
|
+
turn.parts.start({ type: 'message', command: event.command, target: event.target });
|
|
643
|
+
return;
|
|
644
|
+
case 'marker':
|
|
645
|
+
turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
|
|
646
|
+
return;
|
|
647
|
+
case 'plan':
|
|
648
|
+
turn.parts.plan(event.entries);
|
|
649
|
+
return;
|
|
650
|
+
case 'image':
|
|
651
|
+
turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
|
|
652
|
+
this.kit.changed();
|
|
653
|
+
return;
|
|
520
654
|
}
|
|
521
655
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
656
|
+
subagentDone(conversation, id, result) {
|
|
657
|
+
const subagent = conversation.subagents.get(id);
|
|
658
|
+
if (subagent === undefined)
|
|
659
|
+
return;
|
|
660
|
+
conversation.subagents.delete(id);
|
|
661
|
+
if (subagent.turn === conversation.turn) {
|
|
662
|
+
subagent.turn.parts.done(subagent.index, result);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const part = { type: 'subagent', description: subagent.description, status: 'done', result };
|
|
666
|
+
if (conversation.turn === null)
|
|
667
|
+
conversation.held.push(part);
|
|
525
668
|
else
|
|
526
|
-
conversation.
|
|
527
|
-
this.kit.changed();
|
|
669
|
+
conversation.turn.parts.start(part);
|
|
528
670
|
}
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
turn.
|
|
543
|
-
turn.unsentFrom = null;
|
|
671
|
+
async image(conversation, turn, reserved, data, mediaType) {
|
|
672
|
+
const bytes = Buffer.from(data, 'base64');
|
|
673
|
+
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
674
|
+
try {
|
|
675
|
+
const declared = (await this.deliver(conversation, turn, 'images', {
|
|
676
|
+
name,
|
|
677
|
+
media_type: mediaType,
|
|
678
|
+
bytes: bytes.length,
|
|
679
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
680
|
+
}));
|
|
681
|
+
const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
|
|
682
|
+
if (saved.save.status !== 'saved')
|
|
683
|
+
throw new Error(saved.save.failure ?? saved.save.status);
|
|
684
|
+
turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
|
|
544
685
|
}
|
|
545
|
-
|
|
546
|
-
turn.
|
|
686
|
+
catch (error) {
|
|
687
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
|
|
547
688
|
}
|
|
548
689
|
}
|
|
549
|
-
plan(conversation, turn) {
|
|
550
|
-
const sent = this.kit.send({
|
|
551
|
-
type: 'plan',
|
|
552
|
-
conversation_id: conversation.id,
|
|
553
|
-
turn_id: turn.id,
|
|
554
|
-
sequence: turn.planSequence + 1,
|
|
555
|
-
from: 0,
|
|
556
|
-
steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
|
|
557
|
-
});
|
|
558
|
-
if (sent)
|
|
559
|
-
turn.planSequence++;
|
|
560
|
-
}
|
|
561
690
|
}
|