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