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