@agentshouse/kit 0.2.1-cd9cbd8b6428c → 0.2.1-cf110d15f69c8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -26
- package/dist/acp.js +16 -93
- package/dist/agents.js +82 -40
- package/dist/api.js +30 -2
- package/dist/app-git.js +64 -0
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +35 -52
- package/dist/clis.js +121 -65
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +287 -236
- package/dist/copies.js +147 -130
- package/dist/end.js +18 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +521 -0
- package/dist/files.js +14 -0
- package/dist/home.js +41 -3
- package/dist/house-main.js +79 -32
- package/dist/instructions.js +7 -5
- package/dist/kit-main.js +3 -0
- package/dist/login.js +99 -102
- package/dist/logout.js +16 -13
- package/dist/parts.js +82 -0
- package/dist/refusals.js +12 -0
- package/dist/relay.js +12 -2
- package/dist/resident.js +30 -23
- package/dist/scope.js +203 -0
- package/dist/secret-input.js +4 -1
- package/dist/service-main.js +9 -0
- package/dist/service.js +33 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +34 -9
- package/dist/skills.js +1 -1
- package/dist/stream.js +31 -4
- package/dist/transcription.js +54 -6
- package/dist/update.js +2 -0
- package/dist/work.js +348 -0
- package/package.json +2 -2
- package/dist/blocks.js +0 -51
package/dist/conversations.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
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 {
|
|
7
|
-
import { blocksOf, firstChange } from './blocks.js';
|
|
6
|
+
import { 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';
|
|
14
|
+
import { killMarked, Scope } from './scope.js';
|
|
13
15
|
import { holdSecretInput } from './secret-input.js';
|
|
14
16
|
import { createHowWeWork } from './skills.js';
|
|
15
17
|
import { transcribe } from './transcription.js';
|
|
18
|
+
import { workFor } from './work.js';
|
|
16
19
|
const KILLED = 'the conversation was killed';
|
|
17
20
|
function causeOf(error) {
|
|
18
21
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -23,33 +26,35 @@ function answerMessage(request, response) {
|
|
|
23
26
|
function logged(error) {
|
|
24
27
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
25
28
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
29
|
+
function marker(bridge) {
|
|
30
|
+
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
28
31
|
}
|
|
29
32
|
class Turn {
|
|
30
33
|
id = randomUUID();
|
|
31
|
-
|
|
32
|
-
segment = null;
|
|
34
|
+
parts;
|
|
33
35
|
admitted = false;
|
|
34
|
-
notes = [];
|
|
35
|
-
sent = [];
|
|
36
|
-
unsentFrom = null;
|
|
37
|
-
draftSequence = 0;
|
|
38
|
-
planSequence = 0;
|
|
39
|
-
entries = null;
|
|
40
36
|
questions = 0;
|
|
41
37
|
secrets = 0;
|
|
42
|
-
|
|
38
|
+
prompted = false;
|
|
43
39
|
ended = false;
|
|
40
|
+
images = 0;
|
|
41
|
+
uploads = [];
|
|
42
|
+
constructor(emit) {
|
|
43
|
+
this.parts = new Parts(emit);
|
|
44
|
+
}
|
|
44
45
|
}
|
|
45
46
|
class Conversation {
|
|
46
47
|
id;
|
|
47
48
|
running = null;
|
|
49
|
+
session = null;
|
|
48
50
|
turn = null;
|
|
49
51
|
commands = null;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
context = null;
|
|
53
|
+
leftovers = new Set();
|
|
54
|
+
waiting = [];
|
|
55
|
+
sending = null;
|
|
56
|
+
subagents = new Map();
|
|
57
|
+
held = [];
|
|
53
58
|
opening = null;
|
|
54
59
|
kills = 0;
|
|
55
60
|
killed = new AbortController();
|
|
@@ -77,8 +82,11 @@ export class Conversations {
|
|
|
77
82
|
questions = new Map();
|
|
78
83
|
carrying = 0;
|
|
79
84
|
reporting = 0;
|
|
85
|
+
watched = false;
|
|
80
86
|
constructor(kit) {
|
|
81
87
|
this.kit = kit;
|
|
88
|
+
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
89
|
+
setInterval(() => this.look(), 2_000);
|
|
82
90
|
}
|
|
83
91
|
input(input) {
|
|
84
92
|
this.carrying++;
|
|
@@ -88,14 +96,17 @@ export class Conversations {
|
|
|
88
96
|
if (input.kind === 'kill') {
|
|
89
97
|
conversation.kills++;
|
|
90
98
|
conversation.killed.abort(new Error(KILLED));
|
|
99
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
100
|
+
waiting.settle({ refused: KILLED });
|
|
91
101
|
this.halt(conversation);
|
|
92
102
|
}
|
|
93
103
|
if (input.kind === 'interrupt') {
|
|
94
104
|
conversation.interrupts++;
|
|
95
105
|
conversation.interrupted.abort();
|
|
96
106
|
}
|
|
97
|
-
const
|
|
98
|
-
conversation.queue =
|
|
107
|
+
const handled = conversation.queue.then(() => this.carry(conversation, input));
|
|
108
|
+
conversation.queue = handled.catch(() => undefined);
|
|
109
|
+
const work = handled.then(({ ack }) => ack);
|
|
99
110
|
this.acks.set(input.input_id, work);
|
|
100
111
|
return work;
|
|
101
112
|
})();
|
|
@@ -113,15 +124,39 @@ export class Conversations {
|
|
|
113
124
|
for (const conversation of this.conversations.values()) {
|
|
114
125
|
const turn = conversation.turn;
|
|
115
126
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
116
|
-
if (conversation.
|
|
127
|
+
if (conversation.open > (asking ? 1 : 0))
|
|
117
128
|
return false;
|
|
118
129
|
}
|
|
119
|
-
|
|
130
|
+
const conversations = [...this.conversations.values()];
|
|
131
|
+
if (conversations.some((conversation) => conversation.running?.work.busy()))
|
|
132
|
+
return false;
|
|
133
|
+
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
134
|
+
}
|
|
135
|
+
quiet() {
|
|
136
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
|
|
137
|
+
}
|
|
138
|
+
look() {
|
|
139
|
+
for (const conversation of this.conversations.values()) {
|
|
140
|
+
for (const scope of conversation.leftovers) {
|
|
141
|
+
if (!scope.empty())
|
|
142
|
+
continue;
|
|
143
|
+
scope.close();
|
|
144
|
+
conversation.leftovers.delete(scope);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this.kit.changed();
|
|
120
148
|
}
|
|
121
149
|
opened() {
|
|
150
|
+
this.watched = false;
|
|
151
|
+
}
|
|
152
|
+
watching(watched) {
|
|
153
|
+
const resend = watched && !this.watched;
|
|
154
|
+
this.watched = watched;
|
|
155
|
+
if (!resend)
|
|
156
|
+
return;
|
|
122
157
|
for (const conversation of this.conversations.values()) {
|
|
123
158
|
if (conversation.turn !== null)
|
|
124
|
-
conversation.turn
|
|
159
|
+
this.replay(conversation, conversation.turn);
|
|
125
160
|
}
|
|
126
161
|
}
|
|
127
162
|
conversation(id) {
|
|
@@ -134,7 +169,7 @@ export class Conversations {
|
|
|
134
169
|
}
|
|
135
170
|
async carry(conversation, input) {
|
|
136
171
|
if (input.kind === 'open')
|
|
137
|
-
return this.open(conversation, input);
|
|
172
|
+
return { ack: Promise.resolve(await this.open(conversation, input)) };
|
|
138
173
|
if (input.kind === 'message')
|
|
139
174
|
return this.message(conversation, input);
|
|
140
175
|
if (input.kind === 'interrupt') {
|
|
@@ -144,6 +179,7 @@ export class Conversations {
|
|
|
144
179
|
}
|
|
145
180
|
if (input.kind === 'kill') {
|
|
146
181
|
await this.kill(conversation);
|
|
182
|
+
await conversation.sending;
|
|
147
183
|
if (--conversation.kills === 0)
|
|
148
184
|
conversation.killed = new AbortController();
|
|
149
185
|
}
|
|
@@ -152,11 +188,12 @@ export class Conversations {
|
|
|
152
188
|
if (input.kind === 'answer') {
|
|
153
189
|
const question = this.questions.get(String(input.interaction_id));
|
|
154
190
|
if (question === undefined) {
|
|
155
|
-
|
|
191
|
+
const text = answerMessage(input.request, input.response);
|
|
192
|
+
return this.message(conversation, { ...input, text, files: [], first: false });
|
|
156
193
|
}
|
|
157
194
|
question.answer(input.response);
|
|
158
195
|
}
|
|
159
|
-
return {};
|
|
196
|
+
return { ack: Promise.resolve({}) };
|
|
160
197
|
}
|
|
161
198
|
async open(conversation, input) {
|
|
162
199
|
if (conversation.running !== null)
|
|
@@ -169,37 +206,69 @@ export class Conversations {
|
|
|
169
206
|
}
|
|
170
207
|
}
|
|
171
208
|
async message(conversation, input) {
|
|
172
|
-
|
|
173
|
-
|
|
209
|
+
const content = [{ type: 'text', text: String(input.text) }];
|
|
210
|
+
let route;
|
|
174
211
|
try {
|
|
175
|
-
|
|
212
|
+
route = await this.route(conversation, input.agent_id);
|
|
176
213
|
const files = input.files;
|
|
177
214
|
const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
|
|
178
215
|
const lines = await this.transcribed(conversation, files, paths);
|
|
216
|
+
if (lines.length > 0)
|
|
217
|
+
content.push({ type: 'text', text: lines.join('\n') });
|
|
218
|
+
if (conversation.kills > 0)
|
|
219
|
+
throw new Error(KILLED);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
ack: new Promise((settle) => {
|
|
226
|
+
conversation.waiting.push({ input, route, content, settle });
|
|
227
|
+
this.next(conversation);
|
|
228
|
+
}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
next(conversation) {
|
|
232
|
+
if (conversation.sending !== null || conversation.open > 0)
|
|
233
|
+
return;
|
|
234
|
+
const waiting = conversation.waiting.shift();
|
|
235
|
+
if (waiting === undefined)
|
|
236
|
+
return;
|
|
237
|
+
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
238
|
+
conversation.sending = null;
|
|
239
|
+
this.next(conversation);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
async send(conversation, waiting) {
|
|
243
|
+
const { input } = waiting;
|
|
244
|
+
let ack = {};
|
|
245
|
+
const prompt = [];
|
|
246
|
+
try {
|
|
179
247
|
if (conversation.running === null) {
|
|
180
|
-
const
|
|
181
|
-
|
|
248
|
+
const resumed = input.provider_session_id ?? conversation.session;
|
|
249
|
+
const opened = await this.start(conversation, waiting.route, resumed);
|
|
250
|
+
if (resumed === null)
|
|
182
251
|
ack = { provider_session_id: opened };
|
|
183
252
|
}
|
|
184
253
|
if (input.first === true) {
|
|
185
254
|
prompt.push({
|
|
186
255
|
type: 'text',
|
|
187
|
-
text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
|
|
256
|
+
text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
|
|
188
257
|
});
|
|
189
258
|
}
|
|
190
|
-
prompt.push({ type: 'text', text: String(input.text) });
|
|
191
|
-
if (lines.length > 0)
|
|
192
|
-
prompt.push({ type: 'text', text: lines.join('\n') });
|
|
193
259
|
if (conversation.kills > 0)
|
|
194
260
|
throw new Error(KILLED);
|
|
195
261
|
}
|
|
196
262
|
catch (error) {
|
|
197
|
-
if ('provider_session_id' in ack)
|
|
263
|
+
if ('provider_session_id' in ack) {
|
|
264
|
+
conversation.session = null;
|
|
198
265
|
await this.kill(conversation);
|
|
199
|
-
|
|
266
|
+
}
|
|
267
|
+
waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
|
|
268
|
+
return;
|
|
200
269
|
}
|
|
201
|
-
this.prompt(conversation, prompt);
|
|
202
|
-
|
|
270
|
+
this.prompt(conversation, [...prompt, ...waiting.content]);
|
|
271
|
+
waiting.settle(ack);
|
|
203
272
|
}
|
|
204
273
|
async transcribed(conversation, files, paths) {
|
|
205
274
|
const lines = [];
|
|
@@ -209,7 +278,7 @@ export class Conversations {
|
|
|
209
278
|
continue;
|
|
210
279
|
try {
|
|
211
280
|
const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
|
|
212
|
-
if (text !== '')
|
|
281
|
+
if (text !== null && text !== '')
|
|
213
282
|
lines.push(`Transcript: ${text}`);
|
|
214
283
|
}
|
|
215
284
|
catch (error) {
|
|
@@ -229,11 +298,13 @@ export class Conversations {
|
|
|
229
298
|
}
|
|
230
299
|
halt(conversation) {
|
|
231
300
|
if (conversation.opening !== null)
|
|
232
|
-
|
|
301
|
+
killMarked(marker(conversation.opening));
|
|
302
|
+
for (const scope of conversation.leftovers)
|
|
303
|
+
scope.kill();
|
|
233
304
|
if (conversation.running === null)
|
|
234
305
|
return;
|
|
235
306
|
conversation.running.killed = true;
|
|
236
|
-
|
|
307
|
+
conversation.running.scope.kill();
|
|
237
308
|
}
|
|
238
309
|
async kill(conversation) {
|
|
239
310
|
const running = conversation.running;
|
|
@@ -245,6 +316,7 @@ export class Conversations {
|
|
|
245
316
|
if (running === null)
|
|
246
317
|
return;
|
|
247
318
|
try {
|
|
319
|
+
running.reader.requested(option, value);
|
|
248
320
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
249
321
|
sessionId: running.sessionId,
|
|
250
322
|
configId: option,
|
|
@@ -253,6 +325,7 @@ export class Conversations {
|
|
|
253
325
|
this.options(conversation, answered.configOptions);
|
|
254
326
|
}
|
|
255
327
|
catch (error) {
|
|
328
|
+
running.reader.refused(option);
|
|
256
329
|
logged(error);
|
|
257
330
|
}
|
|
258
331
|
}
|
|
@@ -270,10 +343,9 @@ export class Conversations {
|
|
|
270
343
|
return route;
|
|
271
344
|
}
|
|
272
345
|
async start(conversation, route, sessionId) {
|
|
346
|
+
const reader = readerFor(route.kind);
|
|
347
|
+
reader.session = sessionId;
|
|
273
348
|
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
349
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
278
350
|
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
|
|
279
351
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
@@ -283,25 +355,32 @@ export class Conversations {
|
|
|
283
355
|
throw new Error(KILLED);
|
|
284
356
|
}
|
|
285
357
|
conversation.opening = bridge;
|
|
358
|
+
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
359
|
+
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
286
360
|
try {
|
|
287
361
|
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
288
|
-
|
|
362
|
+
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
363
|
+
const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
364
|
+
work.read(message);
|
|
365
|
+
this.read(conversation, reader, message);
|
|
366
|
+
}, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
|
|
289
367
|
void adapter.exited.then(() => bridge.close());
|
|
290
368
|
const agent = adapter.connection.agent;
|
|
291
369
|
const cwd = route.working_directory;
|
|
370
|
+
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
292
371
|
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
|
-
|
|
298
|
-
sessionId: opened.sessionId,
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
phase: CLIS[route.kind].phase,
|
|
302
|
-
};
|
|
372
|
+
? await agent.request('session/new', { cwd, mcpServers: [], _meta })
|
|
373
|
+
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
|
|
374
|
+
reader.session = opened.sessionId;
|
|
375
|
+
if (sessionId === null && CLIS[route.kind].renames) {
|
|
376
|
+
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
377
|
+
await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
|
|
378
|
+
}
|
|
379
|
+
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
303
380
|
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
381
|
+
void work.settled().then(() => scope.record());
|
|
304
382
|
conversation.running = running;
|
|
383
|
+
conversation.session = running.sessionId;
|
|
305
384
|
this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
|
|
306
385
|
if (conversation.commands !== null)
|
|
307
386
|
this.commands(conversation, conversation.commands);
|
|
@@ -310,7 +389,9 @@ export class Conversations {
|
|
|
310
389
|
return running.sessionId;
|
|
311
390
|
}
|
|
312
391
|
catch (error) {
|
|
313
|
-
|
|
392
|
+
scope.kill();
|
|
393
|
+
work.close();
|
|
394
|
+
conversation.leftovers.add(scope);
|
|
314
395
|
bridge.close();
|
|
315
396
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
316
397
|
}
|
|
@@ -321,6 +402,7 @@ export class Conversations {
|
|
|
321
402
|
async launchSettings(running, route, offered) {
|
|
322
403
|
let options = offered;
|
|
323
404
|
const settings = [
|
|
405
|
+
['mode', route.mode ?? CLIS[route.kind].fullAccess],
|
|
324
406
|
['model', route.model],
|
|
325
407
|
['thought_level', route.effort],
|
|
326
408
|
];
|
|
@@ -328,6 +410,7 @@ export class Conversations {
|
|
|
328
410
|
if (value === null)
|
|
329
411
|
continue;
|
|
330
412
|
const option = options.find((candidate) => candidate.category === category);
|
|
413
|
+
running.reader.requested(option?.id ?? category, value);
|
|
331
414
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
332
415
|
sessionId: running.sessionId,
|
|
333
416
|
configId: option?.id ?? category,
|
|
@@ -341,19 +424,23 @@ export class Conversations {
|
|
|
341
424
|
const running = conversation.running;
|
|
342
425
|
if (running?.adapter !== adapter)
|
|
343
426
|
return;
|
|
427
|
+
this.detach(conversation, running);
|
|
428
|
+
if (conversation.turn !== null) {
|
|
429
|
+
this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
|
|
430
|
+
}
|
|
431
|
+
this.next(conversation);
|
|
432
|
+
this.kit.changed();
|
|
433
|
+
}
|
|
434
|
+
detach(conversation, running) {
|
|
435
|
+
running.work.close();
|
|
436
|
+
conversation.leftovers.add(running.scope);
|
|
344
437
|
conversation.running = null;
|
|
345
438
|
conversation.commands = null;
|
|
346
|
-
conversation.jobs.clear();
|
|
347
|
-
conversation.queued.length = 0;
|
|
348
439
|
for (const [id, question] of this.questions) {
|
|
349
440
|
if (question.conversation === conversation)
|
|
350
441
|
this.questions.delete(id);
|
|
351
442
|
}
|
|
352
443
|
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
444
|
}
|
|
358
445
|
commands(conversation, commands) {
|
|
359
446
|
conversation.commands = commands;
|
|
@@ -368,7 +455,7 @@ export class Conversations {
|
|
|
368
455
|
}
|
|
369
456
|
deliver(conversation, turn, event, body) {
|
|
370
457
|
this.reporting++;
|
|
371
|
-
const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
|
|
458
|
+
const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
|
|
372
459
|
conversation.reports = delivered
|
|
373
460
|
.catch(() => undefined)
|
|
374
461
|
.finally(() => {
|
|
@@ -377,119 +464,82 @@ export class Conversations {
|
|
|
377
464
|
});
|
|
378
465
|
return delivered;
|
|
379
466
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
.then(() => this.admitted(conversation, turn))
|
|
385
|
-
.catch(logged);
|
|
386
|
-
return turn;
|
|
467
|
+
write(conversation, turn, operation) {
|
|
468
|
+
if (!turn.admitted || turn.ended || !this.watched)
|
|
469
|
+
return;
|
|
470
|
+
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
387
471
|
}
|
|
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);
|
|
472
|
+
replay(conversation, turn) {
|
|
473
|
+
for (const operation of turn.parts.operations())
|
|
474
|
+
this.write(conversation, turn, operation);
|
|
398
475
|
}
|
|
399
476
|
begin(conversation) {
|
|
400
|
-
const turn = this.
|
|
477
|
+
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
478
|
+
conversation.open++;
|
|
479
|
+
void this.deliver(conversation, turn, 'started', {})
|
|
480
|
+
.then(() => {
|
|
481
|
+
turn.admitted = true;
|
|
482
|
+
this.replay(conversation, turn);
|
|
483
|
+
})
|
|
484
|
+
.catch(logged);
|
|
401
485
|
conversation.turn = turn;
|
|
486
|
+
if (conversation.context !== null)
|
|
487
|
+
turn.parts.measure(conversation.context);
|
|
488
|
+
for (const part of conversation.held.splice(0))
|
|
489
|
+
turn.parts.start(part);
|
|
402
490
|
this.kit.changed();
|
|
403
491
|
return turn;
|
|
404
492
|
}
|
|
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) {
|
|
493
|
+
end(conversation, turn, failed) {
|
|
417
494
|
if (turn.ended)
|
|
418
495
|
return;
|
|
496
|
+
if (conversation.turn === turn) {
|
|
497
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
498
|
+
this.apply(conversation, turn, event);
|
|
499
|
+
}
|
|
419
500
|
turn.ended = true;
|
|
420
501
|
conversation.open--;
|
|
421
502
|
if (conversation.turn === turn)
|
|
422
503
|
conversation.turn = null;
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
504
|
+
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
505
|
+
parts: turn.parts.saved(),
|
|
506
|
+
context: turn.parts.context,
|
|
507
|
+
...failed,
|
|
508
|
+
}));
|
|
509
|
+
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
510
|
+
this.next(conversation);
|
|
427
511
|
this.kit.changed();
|
|
428
512
|
}
|
|
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
513
|
prompt(conversation, prompt) {
|
|
436
514
|
const running = conversation.running;
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
this.end(conversation,
|
|
515
|
+
const turn = this.begin(conversation);
|
|
516
|
+
turn.prompted = true;
|
|
517
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
|
|
518
|
+
if (running.adapter.connection.signal.aborted) {
|
|
519
|
+
this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
if (conversation.open === 1 && conversation.running === running) {
|
|
523
|
+
running.scope.kill();
|
|
524
|
+
this.detach(conversation, running);
|
|
525
|
+
}
|
|
526
|
+
this.end(conversation, turn, { failed: causeOf(error) });
|
|
449
527
|
}
|
|
450
528
|
});
|
|
451
529
|
}
|
|
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
530
|
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;
|
|
531
|
+
return conversation.turn ?? this.begin(conversation);
|
|
484
532
|
}
|
|
485
533
|
ended(conversation) {
|
|
486
534
|
const turn = conversation.turn;
|
|
487
535
|
if (turn === null)
|
|
488
536
|
return;
|
|
489
|
-
if (turn.
|
|
490
|
-
this.
|
|
537
|
+
if (!turn.prompted) {
|
|
538
|
+
this.end(conversation, turn, null);
|
|
491
539
|
}
|
|
492
540
|
else {
|
|
541
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
542
|
+
this.apply(conversation, turn, event);
|
|
493
543
|
conversation.turn = null;
|
|
494
544
|
this.kit.changed();
|
|
495
545
|
}
|
|
@@ -538,101 +588,102 @@ export class Conversations {
|
|
|
538
588
|
if (content !== null)
|
|
539
589
|
this.questions.get(interactionId)?.answer({ action: 'accept', content });
|
|
540
590
|
}
|
|
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
|
-
}
|
|
591
|
+
read(conversation, reader, message) {
|
|
592
|
+
for (const event of reader.read(message))
|
|
593
|
+
this.event(conversation, event);
|
|
566
594
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
595
|
+
event(conversation, event) {
|
|
596
|
+
switch (event.kind) {
|
|
597
|
+
case 'started':
|
|
598
|
+
this.started(conversation);
|
|
599
|
+
return;
|
|
600
|
+
case 'ended':
|
|
601
|
+
this.ended(conversation);
|
|
602
|
+
return;
|
|
603
|
+
case 'commands':
|
|
604
|
+
this.commands(conversation, event.commands);
|
|
605
|
+
return;
|
|
606
|
+
case 'options':
|
|
607
|
+
this.options(conversation, event.options);
|
|
608
|
+
return;
|
|
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, turn.parts.reserve(), 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, reserved, 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.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
|
|
621
684
|
}
|
|
622
|
-
|
|
623
|
-
turn.
|
|
685
|
+
catch (error) {
|
|
686
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
|
|
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
|
}
|