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