@agentshouse/kit 0.2.1-c5262a8cc063d → 0.2.1-c5883630417e1
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 +27 -26
- package/dist/acp.js +16 -93
- package/dist/agents.js +104 -42
- package/dist/api.js +36 -3
- package/dist/app-git.js +64 -0
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +36 -52
- package/dist/chatgpt.js +26 -0
- package/dist/clis.js +128 -65
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +288 -236
- package/dist/copies.js +167 -139
- package/dist/end.js +22 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +521 -0
- package/dist/files.js +23 -2
- package/dist/git.js +11 -8
- package/dist/home.js +81 -6
- package/dist/house-main.js +186 -56
- package/dist/instructions.js +9 -32
- package/dist/kit-main.js +54 -6
- package/dist/login.js +109 -103
- package/dist/logout.js +16 -13
- package/dist/mcp.js +2 -1
- package/dist/parts.js +82 -0
- package/dist/placement.js +48 -0
- package/dist/refusals.js +12 -0
- package/dist/relay.js +20 -0
- package/dist/resident.js +38 -27
- 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 +81 -12
- package/dist/stream.js +37 -6
- package/dist/transcription.js +57 -9
- package/dist/update.js +30 -6
- package/dist/wire.js +6 -0
- package/dist/work.js +348 -0
- package/how-we-work.md +13 -3
- package/package.json +3 -2
- package/skills/ask-matt/SKILL.md +17 -11
- package/skills/ask-matt/agents/openai.yaml +1 -0
- package/skills/code-review/SKILL.md +5 -5
- package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
- package/skills/codebase-design/SKILL.md +0 -2
- package/skills/diagnosing-bugs/SKILL.md +2 -4
- package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
- package/skills/domain-modeling/SKILL.md +12 -14
- package/skills/grill-me/SKILL.md +1 -2
- package/skills/grill-me/agents/openai.yaml +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -2
- package/skills/grill-with-docs/agents/openai.yaml +1 -0
- package/skills/grilling/SKILL.md +2 -2
- package/skills/handoff/SKILL.md +2 -3
- package/skills/handoff/agents/openai.yaml +1 -0
- package/skills/implement/SKILL.md +5 -4
- package/skills/implement/agents/openai.yaml +1 -0
- package/skills/implement-spec/SKILL.md +40 -0
- package/skills/implement-spec/agents/openai.yaml +5 -0
- package/skills/improve-codebase-architecture/SKILL.md +6 -7
- package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
- package/skills/pr/CREDITS.md +3 -0
- package/skills/pr/LICENSE +21 -0
- package/skills/pr/SKILL.md +170 -0
- package/skills/pr/agents/openai.yaml +3 -0
- package/skills/prototype/SKILL.md +0 -2
- package/skills/research/SKILL.md +0 -2
- package/skills/retro/LICENSE +21 -0
- package/skills/retro/SKILL.md +44 -0
- package/skills/retro/agents/openai.yaml +5 -0
- package/skills/tdd/SKILL.md +2 -4
- package/skills/teach/SKILL.md +3 -4
- package/skills/teach/agents/openai.yaml +1 -0
- package/skills/to-questionnaire/SKILL.md +1 -2
- package/skills/to-questionnaire/agents/openai.yaml +1 -0
- package/skills/to-spec/SKILL.md +1 -2
- package/skills/to-spec/agents/openai.yaml +1 -0
- package/skills/to-tickets/SKILL.md +3 -4
- package/skills/to-tickets/agents/openai.yaml +1 -0
- package/skills/triage/AGENT-BRIEF.md +207 -0
- package/skills/triage/LICENSE +21 -0
- package/skills/triage/OUT-OF-SCOPE.md +105 -0
- package/skills/triage/SKILL.md +112 -0
- package/skills/triage/agents/openai.yaml +5 -0
- package/skills/wait-what/SKILL.md +2 -3
- package/skills/wait-what/agents/openai.yaml +1 -0
- package/skills/wayfinder/SKILL.md +5 -6
- package/skills/wayfinder/agents/openai.yaml +1 -0
- package/skills/wizard/LICENSE +21 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/wizard/agents/openai.yaml +3 -0
- package/skills/wizard/template.sh +223 -0
- package/skills/writing-for-agents/SKILL.md +0 -2
- package/dist/blocks.js +0 -51
- package/skills/resolving-merge-conflicts/SKILL.md +0 -16
- package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
- /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/conversations.js
CHANGED
|
@@ -1,19 +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
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';
|
|
20
|
+
export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
|
|
17
21
|
function causeOf(error) {
|
|
18
22
|
return error instanceof Error ? error.message : String(error);
|
|
19
23
|
}
|
|
@@ -23,33 +27,35 @@ function answerMessage(request, response) {
|
|
|
23
27
|
function logged(error) {
|
|
24
28
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
25
29
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
30
|
+
function marker(bridge) {
|
|
31
|
+
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
28
32
|
}
|
|
29
33
|
class Turn {
|
|
30
34
|
id = randomUUID();
|
|
31
|
-
|
|
32
|
-
segment = null;
|
|
35
|
+
parts;
|
|
33
36
|
admitted = false;
|
|
34
|
-
notes = [];
|
|
35
|
-
sent = [];
|
|
36
|
-
unsentFrom = null;
|
|
37
|
-
draftSequence = 0;
|
|
38
|
-
planSequence = 0;
|
|
39
|
-
entries = null;
|
|
40
37
|
questions = 0;
|
|
41
38
|
secrets = 0;
|
|
42
|
-
|
|
39
|
+
prompted = false;
|
|
43
40
|
ended = false;
|
|
41
|
+
images = 0;
|
|
42
|
+
uploads = [];
|
|
43
|
+
constructor(emit) {
|
|
44
|
+
this.parts = new Parts(emit);
|
|
45
|
+
}
|
|
44
46
|
}
|
|
45
47
|
class Conversation {
|
|
46
48
|
id;
|
|
47
49
|
running = null;
|
|
50
|
+
session = null;
|
|
48
51
|
turn = null;
|
|
49
52
|
commands = null;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
context = null;
|
|
54
|
+
leftovers = new Set();
|
|
55
|
+
waiting = [];
|
|
56
|
+
sending = null;
|
|
57
|
+
subagents = new Map();
|
|
58
|
+
held = [];
|
|
53
59
|
opening = null;
|
|
54
60
|
kills = 0;
|
|
55
61
|
killed = new AbortController();
|
|
@@ -77,8 +83,11 @@ export class Conversations {
|
|
|
77
83
|
questions = new Map();
|
|
78
84
|
carrying = 0;
|
|
79
85
|
reporting = 0;
|
|
86
|
+
watched = false;
|
|
80
87
|
constructor(kit) {
|
|
81
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);
|
|
82
91
|
}
|
|
83
92
|
input(input) {
|
|
84
93
|
this.carrying++;
|
|
@@ -88,14 +97,17 @@ export class Conversations {
|
|
|
88
97
|
if (input.kind === 'kill') {
|
|
89
98
|
conversation.kills++;
|
|
90
99
|
conversation.killed.abort(new Error(KILLED));
|
|
100
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
101
|
+
waiting.settle({ refused: KILLED });
|
|
91
102
|
this.halt(conversation);
|
|
92
103
|
}
|
|
93
104
|
if (input.kind === 'interrupt') {
|
|
94
105
|
conversation.interrupts++;
|
|
95
106
|
conversation.interrupted.abort();
|
|
96
107
|
}
|
|
97
|
-
const
|
|
98
|
-
conversation.queue =
|
|
108
|
+
const handled = conversation.queue.then(() => this.carry(conversation, input));
|
|
109
|
+
conversation.queue = handled.catch(() => undefined);
|
|
110
|
+
const work = handled.then(({ ack }) => ack);
|
|
99
111
|
this.acks.set(input.input_id, work);
|
|
100
112
|
return work;
|
|
101
113
|
})();
|
|
@@ -113,15 +125,39 @@ export class Conversations {
|
|
|
113
125
|
for (const conversation of this.conversations.values()) {
|
|
114
126
|
const turn = conversation.turn;
|
|
115
127
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
116
|
-
if (conversation.
|
|
128
|
+
if (conversation.open > (asking ? 1 : 0))
|
|
117
129
|
return false;
|
|
118
130
|
}
|
|
119
|
-
|
|
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();
|
|
120
149
|
}
|
|
121
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;
|
|
122
158
|
for (const conversation of this.conversations.values()) {
|
|
123
159
|
if (conversation.turn !== null)
|
|
124
|
-
conversation.turn
|
|
160
|
+
this.replay(conversation, conversation.turn);
|
|
125
161
|
}
|
|
126
162
|
}
|
|
127
163
|
conversation(id) {
|
|
@@ -134,7 +170,7 @@ export class Conversations {
|
|
|
134
170
|
}
|
|
135
171
|
async carry(conversation, input) {
|
|
136
172
|
if (input.kind === 'open')
|
|
137
|
-
return this.open(conversation, input);
|
|
173
|
+
return { ack: Promise.resolve(await this.open(conversation, input)) };
|
|
138
174
|
if (input.kind === 'message')
|
|
139
175
|
return this.message(conversation, input);
|
|
140
176
|
if (input.kind === 'interrupt') {
|
|
@@ -144,6 +180,7 @@ export class Conversations {
|
|
|
144
180
|
}
|
|
145
181
|
if (input.kind === 'kill') {
|
|
146
182
|
await this.kill(conversation);
|
|
183
|
+
await conversation.sending;
|
|
147
184
|
if (--conversation.kills === 0)
|
|
148
185
|
conversation.killed = new AbortController();
|
|
149
186
|
}
|
|
@@ -152,11 +189,12 @@ export class Conversations {
|
|
|
152
189
|
if (input.kind === 'answer') {
|
|
153
190
|
const question = this.questions.get(String(input.interaction_id));
|
|
154
191
|
if (question === undefined) {
|
|
155
|
-
|
|
192
|
+
const text = answerMessage(input.request, input.response);
|
|
193
|
+
return this.message(conversation, { ...input, text, files: [], first: false });
|
|
156
194
|
}
|
|
157
195
|
question.answer(input.response);
|
|
158
196
|
}
|
|
159
|
-
return {};
|
|
197
|
+
return { ack: Promise.resolve({}) };
|
|
160
198
|
}
|
|
161
199
|
async open(conversation, input) {
|
|
162
200
|
if (conversation.running !== null)
|
|
@@ -169,37 +207,69 @@ export class Conversations {
|
|
|
169
207
|
}
|
|
170
208
|
}
|
|
171
209
|
async message(conversation, input) {
|
|
172
|
-
|
|
173
|
-
|
|
210
|
+
const content = [{ type: 'text', text: String(input.text) }];
|
|
211
|
+
let route;
|
|
174
212
|
try {
|
|
175
|
-
|
|
213
|
+
route = await this.route(conversation, input.agent_id);
|
|
176
214
|
const files = input.files;
|
|
177
215
|
const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
|
|
178
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;
|
|
245
|
+
let ack = {};
|
|
246
|
+
const prompt = [];
|
|
247
|
+
try {
|
|
179
248
|
if (conversation.running === null) {
|
|
180
|
-
const
|
|
181
|
-
|
|
249
|
+
const resumed = input.provider_session_id ?? conversation.session;
|
|
250
|
+
const opened = await this.start(conversation, waiting.route, resumed);
|
|
251
|
+
if (resumed === null)
|
|
182
252
|
ack = { provider_session_id: opened };
|
|
183
253
|
}
|
|
184
254
|
if (input.first === true) {
|
|
185
255
|
prompt.push({
|
|
186
256
|
type: 'text',
|
|
187
|
-
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()),
|
|
188
258
|
});
|
|
189
259
|
}
|
|
190
|
-
prompt.push({ type: 'text', text: String(input.text) });
|
|
191
|
-
if (lines.length > 0)
|
|
192
|
-
prompt.push({ type: 'text', text: lines.join('\n') });
|
|
193
260
|
if (conversation.kills > 0)
|
|
194
261
|
throw new Error(KILLED);
|
|
195
262
|
}
|
|
196
263
|
catch (error) {
|
|
197
|
-
if ('provider_session_id' in ack)
|
|
264
|
+
if ('provider_session_id' in ack) {
|
|
265
|
+
conversation.session = null;
|
|
198
266
|
await this.kill(conversation);
|
|
199
|
-
|
|
267
|
+
}
|
|
268
|
+
waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
|
|
269
|
+
return;
|
|
200
270
|
}
|
|
201
|
-
this.prompt(conversation, prompt);
|
|
202
|
-
|
|
271
|
+
this.prompt(conversation, [...prompt, ...waiting.content]);
|
|
272
|
+
waiting.settle(ack);
|
|
203
273
|
}
|
|
204
274
|
async transcribed(conversation, files, paths) {
|
|
205
275
|
const lines = [];
|
|
@@ -209,7 +279,7 @@ export class Conversations {
|
|
|
209
279
|
continue;
|
|
210
280
|
try {
|
|
211
281
|
const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
|
|
212
|
-
if (text !== '')
|
|
282
|
+
if (text !== null && text !== '')
|
|
213
283
|
lines.push(`Transcript: ${text}`);
|
|
214
284
|
}
|
|
215
285
|
catch (error) {
|
|
@@ -229,11 +299,13 @@ export class Conversations {
|
|
|
229
299
|
}
|
|
230
300
|
halt(conversation) {
|
|
231
301
|
if (conversation.opening !== null)
|
|
232
|
-
|
|
302
|
+
killMarked(marker(conversation.opening));
|
|
303
|
+
for (const scope of conversation.leftovers)
|
|
304
|
+
scope.kill();
|
|
233
305
|
if (conversation.running === null)
|
|
234
306
|
return;
|
|
235
307
|
conversation.running.killed = true;
|
|
236
|
-
|
|
308
|
+
conversation.running.scope.kill();
|
|
237
309
|
}
|
|
238
310
|
async kill(conversation) {
|
|
239
311
|
const running = conversation.running;
|
|
@@ -245,6 +317,7 @@ export class Conversations {
|
|
|
245
317
|
if (running === null)
|
|
246
318
|
return;
|
|
247
319
|
try {
|
|
320
|
+
running.reader.requested(option, value);
|
|
248
321
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
249
322
|
sessionId: running.sessionId,
|
|
250
323
|
configId: option,
|
|
@@ -253,6 +326,7 @@ export class Conversations {
|
|
|
253
326
|
this.options(conversation, answered.configOptions);
|
|
254
327
|
}
|
|
255
328
|
catch (error) {
|
|
329
|
+
running.reader.refused(option);
|
|
256
330
|
logged(error);
|
|
257
331
|
}
|
|
258
332
|
}
|
|
@@ -270,10 +344,9 @@ export class Conversations {
|
|
|
270
344
|
return route;
|
|
271
345
|
}
|
|
272
346
|
async start(conversation, route, sessionId) {
|
|
347
|
+
const reader = readerFor(route.kind);
|
|
348
|
+
reader.session = sessionId;
|
|
273
349
|
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
350
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
278
351
|
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
|
|
279
352
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
@@ -283,25 +356,32 @@ export class Conversations {
|
|
|
283
356
|
throw new Error(KILLED);
|
|
284
357
|
}
|
|
285
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);
|
|
286
361
|
try {
|
|
287
362
|
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
288
|
-
|
|
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);
|
|
289
368
|
void adapter.exited.then(() => bridge.close());
|
|
290
369
|
const agent = adapter.connection.agent;
|
|
291
370
|
const cwd = route.working_directory;
|
|
371
|
+
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
292
372
|
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
|
-
};
|
|
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 };
|
|
303
381
|
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
382
|
+
void work.settled().then(() => scope.record());
|
|
304
383
|
conversation.running = running;
|
|
384
|
+
conversation.session = running.sessionId;
|
|
305
385
|
this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
|
|
306
386
|
if (conversation.commands !== null)
|
|
307
387
|
this.commands(conversation, conversation.commands);
|
|
@@ -310,7 +390,9 @@ export class Conversations {
|
|
|
310
390
|
return running.sessionId;
|
|
311
391
|
}
|
|
312
392
|
catch (error) {
|
|
313
|
-
|
|
393
|
+
scope.kill();
|
|
394
|
+
work.close();
|
|
395
|
+
conversation.leftovers.add(scope);
|
|
314
396
|
bridge.close();
|
|
315
397
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
316
398
|
}
|
|
@@ -321,6 +403,7 @@ export class Conversations {
|
|
|
321
403
|
async launchSettings(running, route, offered) {
|
|
322
404
|
let options = offered;
|
|
323
405
|
const settings = [
|
|
406
|
+
['mode', route.mode ?? CLIS[route.kind].fullAccess],
|
|
324
407
|
['model', route.model],
|
|
325
408
|
['thought_level', route.effort],
|
|
326
409
|
];
|
|
@@ -328,6 +411,7 @@ export class Conversations {
|
|
|
328
411
|
if (value === null)
|
|
329
412
|
continue;
|
|
330
413
|
const option = options.find((candidate) => candidate.category === category);
|
|
414
|
+
running.reader.requested(option?.id ?? category, value);
|
|
331
415
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
332
416
|
sessionId: running.sessionId,
|
|
333
417
|
configId: option?.id ?? category,
|
|
@@ -341,19 +425,23 @@ export class Conversations {
|
|
|
341
425
|
const running = conversation.running;
|
|
342
426
|
if (running?.adapter !== adapter)
|
|
343
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);
|
|
344
438
|
conversation.running = null;
|
|
345
439
|
conversation.commands = null;
|
|
346
|
-
conversation.jobs.clear();
|
|
347
|
-
conversation.queued.length = 0;
|
|
348
440
|
for (const [id, question] of this.questions) {
|
|
349
441
|
if (question.conversation === conversation)
|
|
350
442
|
this.questions.delete(id);
|
|
351
443
|
}
|
|
352
444
|
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
445
|
}
|
|
358
446
|
commands(conversation, commands) {
|
|
359
447
|
conversation.commands = commands;
|
|
@@ -368,7 +456,7 @@ export class Conversations {
|
|
|
368
456
|
}
|
|
369
457
|
deliver(conversation, turn, event, body) {
|
|
370
458
|
this.reporting++;
|
|
371
|
-
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));
|
|
372
460
|
conversation.reports = delivered
|
|
373
461
|
.catch(() => undefined)
|
|
374
462
|
.finally(() => {
|
|
@@ -377,119 +465,82 @@ export class Conversations {
|
|
|
377
465
|
});
|
|
378
466
|
return delivered;
|
|
379
467
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
.then(() => this.admitted(conversation, turn))
|
|
385
|
-
.catch(logged);
|
|
386
|
-
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 });
|
|
387
472
|
}
|
|
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);
|
|
473
|
+
replay(conversation, turn) {
|
|
474
|
+
for (const operation of turn.parts.operations())
|
|
475
|
+
this.write(conversation, turn, operation);
|
|
398
476
|
}
|
|
399
477
|
begin(conversation) {
|
|
400
|
-
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);
|
|
401
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);
|
|
402
491
|
this.kit.changed();
|
|
403
492
|
return turn;
|
|
404
493
|
}
|
|
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) {
|
|
494
|
+
end(conversation, turn, failed) {
|
|
417
495
|
if (turn.ended)
|
|
418
496
|
return;
|
|
497
|
+
if (conversation.turn === turn) {
|
|
498
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
499
|
+
this.apply(conversation, turn, event);
|
|
500
|
+
}
|
|
419
501
|
turn.ended = true;
|
|
420
502
|
conversation.open--;
|
|
421
503
|
if (conversation.turn === turn)
|
|
422
504
|
conversation.turn = null;
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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);
|
|
427
512
|
this.kit.changed();
|
|
428
513
|
}
|
|
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
514
|
prompt(conversation, prompt) {
|
|
436
515
|
const running = conversation.running;
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
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) });
|
|
449
528
|
}
|
|
450
529
|
});
|
|
451
530
|
}
|
|
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
531
|
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;
|
|
532
|
+
return conversation.turn ?? this.begin(conversation);
|
|
484
533
|
}
|
|
485
534
|
ended(conversation) {
|
|
486
535
|
const turn = conversation.turn;
|
|
487
536
|
if (turn === null)
|
|
488
537
|
return;
|
|
489
|
-
if (turn.
|
|
490
|
-
this.
|
|
538
|
+
if (!turn.prompted) {
|
|
539
|
+
this.end(conversation, turn, null);
|
|
491
540
|
}
|
|
492
541
|
else {
|
|
542
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
543
|
+
this.apply(conversation, turn, event);
|
|
493
544
|
conversation.turn = null;
|
|
494
545
|
this.kit.changed();
|
|
495
546
|
}
|
|
@@ -538,101 +589,102 @@ export class Conversations {
|
|
|
538
589
|
if (content !== null)
|
|
539
590
|
this.questions.get(interactionId)?.answer({ action: 'accept', content });
|
|
540
591
|
}
|
|
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
|
-
}
|
|
592
|
+
read(conversation, reader, message) {
|
|
593
|
+
for (const event of reader.read(message))
|
|
594
|
+
this.event(conversation, event);
|
|
566
595
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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;
|
|
624
|
+
}
|
|
625
|
+
this.apply(conversation, this.started(conversation), event);
|
|
573
626
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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;
|
|
582
654
|
}
|
|
583
|
-
turn.segment ??= { messageId, phase, text: '' };
|
|
584
|
-
turn.segment.text += text;
|
|
585
655
|
}
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
if (
|
|
656
|
+
subagentDone(conversation, id, result) {
|
|
657
|
+
const subagent = conversation.subagents.get(id);
|
|
658
|
+
if (subagent === undefined)
|
|
589
659
|
return;
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
note(conversation, turn, text) {
|
|
594
|
-
if (text.trim() === '')
|
|
660
|
+
conversation.subagents.delete(id);
|
|
661
|
+
if (subagent.turn === conversation.turn) {
|
|
662
|
+
subagent.turn.parts.done(subagent.index, result);
|
|
595
663
|
return;
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
664
|
+
}
|
|
665
|
+
const part = { type: 'subagent', description: subagent.description, status: 'done', result };
|
|
666
|
+
if (conversation.turn === null)
|
|
667
|
+
conversation.held.push(part);
|
|
599
668
|
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;
|
|
669
|
+
conversation.turn.parts.start(part);
|
|
670
|
+
}
|
|
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);
|
|
621
685
|
}
|
|
622
|
-
|
|
623
|
-
turn.
|
|
686
|
+
catch (error) {
|
|
687
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
|
|
624
688
|
}
|
|
625
689
|
}
|
|
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
690
|
}
|