@agentshouse/kit 0.2.1-cbabe8ad337ff → 0.2.1-cbdf76b9a90a3
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 +30 -29
- package/dist/acp.js +34 -14
- package/dist/agents.js +109 -22
- package/dist/api.js +6 -2
- package/dist/app-git.js +10 -26
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +10 -24
- package/dist/chatgpt.js +32 -0
- package/dist/clis.js +51 -9
- package/dist/conversations.js +289 -99
- package/dist/copies.js +195 -119
- package/dist/end.js +22 -3
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +4 -0
- package/dist/files.js +8 -2
- package/dist/git.js +10 -8
- package/dist/home.js +96 -7
- package/dist/house-cli.js +291 -0
- package/dist/house-main.js +7 -177
- package/dist/inputs.js +63 -0
- package/dist/instructions.js +10 -32
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -22
- package/dist/login.js +112 -22
- package/dist/logout.js +0 -2
- package/dist/parts.js +34 -16
- package/dist/placement.js +39 -0
- package/dist/relay.js +12 -2
- package/dist/resident.js +82 -26
- package/dist/scope.js +3 -0
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/sign-in.js +5 -23
- package/dist/skills.js +81 -12
- package/dist/stream.js +68 -17
- package/dist/transcription.js +3 -3
- package/dist/translate.js +2 -8
- package/dist/update.js +48 -29
- package/dist/wire.js +22 -0
- package/dist/work.js +3 -5
- 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/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,41 +1,100 @@
|
|
|
1
|
-
import { client, } from '@agentclientprotocol/sdk';
|
|
1
|
+
import { client, RequestError, } from '@agentclientprotocol/sdk';
|
|
2
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
6
|
import { startAdapter } from './acp.js';
|
|
7
|
+
import { finalRefusal } from './api.js';
|
|
7
8
|
import { openBridge } from './bridge.js';
|
|
8
9
|
import { CLIS } from './clis.js';
|
|
9
|
-
import { readerFor } from './events.js';
|
|
10
|
+
import { answeredFailure, readerFor } from './events.js';
|
|
10
11
|
import { placeFiles, uploadBytes } from './files.js';
|
|
11
12
|
import { agentBase } from './home.js';
|
|
12
13
|
import { instructions } from './instructions.js';
|
|
13
14
|
import { Parts } from './parts.js';
|
|
15
|
+
import { refusalOf } from './refusals.js';
|
|
14
16
|
import { killMarked, Scope } from './scope.js';
|
|
15
17
|
import { holdSecretInput } from './secret-input.js';
|
|
16
18
|
import { createHowWeWork } from './skills.js';
|
|
17
19
|
import { transcribe } from './transcription.js';
|
|
18
20
|
import { workFor } from './work.js';
|
|
21
|
+
const SIGNED_OUT = RequestError.authRequired().code;
|
|
19
22
|
const KILLED = 'the conversation was killed';
|
|
23
|
+
const REFUSED = 'the CLI refused the prompt';
|
|
24
|
+
const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
|
|
25
|
+
const GONE = 'House no longer holds this conversation; start a new one';
|
|
26
|
+
const RETIRED = "this conversation's Agent is retired, so it cannot start";
|
|
27
|
+
const ENDED = {
|
|
28
|
+
conversation_not_found: GONE,
|
|
29
|
+
conversation_historical: RETIRED,
|
|
30
|
+
};
|
|
31
|
+
const NO_START = "the Agent's CLI did not start";
|
|
32
|
+
const NO_SESSION = "the Agent's CLI did not open a session";
|
|
33
|
+
const NO_RESUME = "the Agent's CLI could not resume this conversation";
|
|
34
|
+
const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
|
|
35
|
+
const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
|
|
36
|
+
class NotStarted extends Error {
|
|
37
|
+
}
|
|
38
|
+
async function step(reason, work) {
|
|
39
|
+
try {
|
|
40
|
+
return await work;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function signedOut(error) {
|
|
47
|
+
return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
|
|
48
|
+
}
|
|
49
|
+
function unopened(reason) {
|
|
50
|
+
return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
|
|
51
|
+
}
|
|
52
|
+
// A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
|
|
53
|
+
const STARTING_SHOWN_AFTER_MS = 1_000;
|
|
54
|
+
function unbridged(error) {
|
|
55
|
+
return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
|
|
56
|
+
}
|
|
57
|
+
const EXIT_PLAN_MODE = {
|
|
58
|
+
parse(value) {
|
|
59
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
60
|
+
throw new Error('_x.ai/exit_plan_mode needs an object');
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
|
|
20
66
|
function causeOf(error) {
|
|
21
67
|
return error instanceof Error ? error.message : String(error);
|
|
22
68
|
}
|
|
69
|
+
function answeredWith(error) {
|
|
70
|
+
return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
71
|
+
}
|
|
72
|
+
function exitedWith(running, cause) {
|
|
73
|
+
return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
|
|
74
|
+
}
|
|
23
75
|
function answerMessage(request, response) {
|
|
24
76
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
25
77
|
}
|
|
26
78
|
function logged(error) {
|
|
27
79
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
28
80
|
}
|
|
81
|
+
function refusal(conversation, error) {
|
|
82
|
+
if (conversation.kills > 0)
|
|
83
|
+
return KILLED;
|
|
84
|
+
const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
85
|
+
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
86
|
+
return causeOf(error);
|
|
87
|
+
}
|
|
29
88
|
function marker(bridge) {
|
|
30
89
|
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
31
90
|
}
|
|
32
91
|
class Turn {
|
|
33
92
|
id = randomUUID();
|
|
34
93
|
parts;
|
|
35
|
-
admitted = false;
|
|
36
94
|
questions = 0;
|
|
37
95
|
secrets = 0;
|
|
38
96
|
prompted = false;
|
|
97
|
+
taken = null;
|
|
39
98
|
ended = false;
|
|
40
99
|
images = 0;
|
|
41
100
|
uploads = [];
|
|
@@ -49,6 +108,8 @@ class Conversation {
|
|
|
49
108
|
session = null;
|
|
50
109
|
turn = null;
|
|
51
110
|
commands = null;
|
|
111
|
+
options = null;
|
|
112
|
+
exitUntold = false;
|
|
52
113
|
context = null;
|
|
53
114
|
leftovers = new Set();
|
|
54
115
|
waiting = [];
|
|
@@ -56,6 +117,8 @@ class Conversation {
|
|
|
56
117
|
subagents = new Map();
|
|
57
118
|
held = [];
|
|
58
119
|
opening = null;
|
|
120
|
+
launch = null;
|
|
121
|
+
prompted = null;
|
|
59
122
|
kills = 0;
|
|
60
123
|
killed = new AbortController();
|
|
61
124
|
interrupts = 0;
|
|
@@ -63,6 +126,11 @@ class Conversation {
|
|
|
63
126
|
open = 0;
|
|
64
127
|
queue = Promise.resolve();
|
|
65
128
|
reports = Promise.resolve();
|
|
129
|
+
starting = null;
|
|
130
|
+
preparing = null;
|
|
131
|
+
told = null;
|
|
132
|
+
untold = false;
|
|
133
|
+
shown = null;
|
|
66
134
|
constructor(id) {
|
|
67
135
|
this.id = id;
|
|
68
136
|
}
|
|
@@ -78,9 +146,7 @@ function asksSecret(request) {
|
|
|
78
146
|
export class Conversations {
|
|
79
147
|
kit;
|
|
80
148
|
conversations = new Map();
|
|
81
|
-
acks = new Map();
|
|
82
149
|
questions = new Map();
|
|
83
|
-
carrying = 0;
|
|
84
150
|
reporting = 0;
|
|
85
151
|
watched = false;
|
|
86
152
|
constructor(kit) {
|
|
@@ -89,37 +155,26 @@ export class Conversations {
|
|
|
89
155
|
setInterval(() => this.look(), 2_000);
|
|
90
156
|
}
|
|
91
157
|
input(input) {
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
conversation.queue = handled.catch(() => undefined);
|
|
109
|
-
const work = handled.then(({ ack }) => ack);
|
|
110
|
-
this.acks.set(input.input_id, work);
|
|
111
|
-
return work;
|
|
112
|
-
})();
|
|
113
|
-
carried
|
|
114
|
-
.then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
|
|
115
|
-
.catch(logged)
|
|
116
|
-
.finally(() => {
|
|
117
|
-
this.carrying--;
|
|
118
|
-
this.kit.changed();
|
|
158
|
+
void this.kit.inputs.carry(input.input_id, () => {
|
|
159
|
+
const conversation = this.conversation(input.conversation_id);
|
|
160
|
+
if (input.kind === 'kill') {
|
|
161
|
+
conversation.kills++;
|
|
162
|
+
conversation.killed.abort(new Error(KILLED));
|
|
163
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
164
|
+
waiting.settle({ refused: KILLED });
|
|
165
|
+
this.halt(conversation);
|
|
166
|
+
}
|
|
167
|
+
if (input.kind === 'interrupt') {
|
|
168
|
+
conversation.interrupts++;
|
|
169
|
+
conversation.interrupted.abort();
|
|
170
|
+
}
|
|
171
|
+
const handled = conversation.queue.then(() => this.carry(conversation, input));
|
|
172
|
+
conversation.queue = handled.catch(() => undefined);
|
|
173
|
+
return handled.then(async ({ ack }) => ({ ack: await ack }));
|
|
119
174
|
});
|
|
120
175
|
}
|
|
121
176
|
idle() {
|
|
122
|
-
if (this.
|
|
177
|
+
if (this.reporting > 0)
|
|
123
178
|
return false;
|
|
124
179
|
for (const conversation of this.conversations.values()) {
|
|
125
180
|
const turn = conversation.turn;
|
|
@@ -132,6 +187,9 @@ export class Conversations {
|
|
|
132
187
|
return false;
|
|
133
188
|
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
134
189
|
}
|
|
190
|
+
quiet() {
|
|
191
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
|
|
192
|
+
}
|
|
135
193
|
look() {
|
|
136
194
|
for (const conversation of this.conversations.values()) {
|
|
137
195
|
for (const scope of conversation.leftovers) {
|
|
@@ -145,6 +203,14 @@ export class Conversations {
|
|
|
145
203
|
}
|
|
146
204
|
opened() {
|
|
147
205
|
this.watched = false;
|
|
206
|
+
for (const conversation of this.conversations.values()) {
|
|
207
|
+
if (conversation.running !== null)
|
|
208
|
+
this.facts(conversation);
|
|
209
|
+
else if (conversation.exitUntold)
|
|
210
|
+
this.process(conversation, false);
|
|
211
|
+
if (conversation.untold || conversation.told !== null)
|
|
212
|
+
this.retell(conversation);
|
|
213
|
+
}
|
|
148
214
|
}
|
|
149
215
|
watching(watched) {
|
|
150
216
|
const resend = watched && !this.watched;
|
|
@@ -166,9 +232,10 @@ export class Conversations {
|
|
|
166
232
|
}
|
|
167
233
|
async carry(conversation, input) {
|
|
168
234
|
if (input.kind === 'open')
|
|
169
|
-
return { ack:
|
|
235
|
+
return { ack: this.open(conversation, input) };
|
|
170
236
|
if (input.kind === 'message')
|
|
171
237
|
return this.message(conversation, input);
|
|
238
|
+
await this.launched(conversation);
|
|
172
239
|
if (input.kind === 'interrupt') {
|
|
173
240
|
if (--conversation.interrupts === 0)
|
|
174
241
|
conversation.interrupted = new AbortController();
|
|
@@ -192,22 +259,58 @@ export class Conversations {
|
|
|
192
259
|
}
|
|
193
260
|
return { ack: Promise.resolve({}) };
|
|
194
261
|
}
|
|
195
|
-
|
|
262
|
+
open(conversation, input) {
|
|
196
263
|
if (conversation.running !== null)
|
|
197
|
-
return {};
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
264
|
+
return Promise.resolve({});
|
|
265
|
+
const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, input.agent_id), null, true);
|
|
266
|
+
return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
|
|
267
|
+
}
|
|
268
|
+
launch(conversation, route, sessionId, empty) {
|
|
269
|
+
let opened;
|
|
270
|
+
let failed;
|
|
271
|
+
const bridge = new Promise((resolve, reject) => {
|
|
272
|
+
opened = resolve;
|
|
273
|
+
failed = reject;
|
|
274
|
+
});
|
|
275
|
+
bridge.catch(() => undefined);
|
|
276
|
+
const session = this.start(conversation, route, sessionId, empty, opened);
|
|
277
|
+
session.catch(failed);
|
|
278
|
+
const launch = { bridge, session };
|
|
279
|
+
conversation.launch = launch;
|
|
280
|
+
void session
|
|
281
|
+
.catch(() => undefined)
|
|
282
|
+
.then(() => {
|
|
283
|
+
if (conversation.launch !== launch)
|
|
284
|
+
return;
|
|
285
|
+
conversation.launch = null;
|
|
286
|
+
this.next(conversation);
|
|
287
|
+
});
|
|
288
|
+
this.next(conversation);
|
|
289
|
+
return launch;
|
|
290
|
+
}
|
|
291
|
+
async launched(conversation) {
|
|
292
|
+
while (conversation.launch !== null)
|
|
293
|
+
await conversation.launch.session.catch(() => undefined);
|
|
294
|
+
}
|
|
295
|
+
instructed(conversation, route) {
|
|
296
|
+
const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
|
|
297
|
+
if (bridge === undefined)
|
|
298
|
+
return null;
|
|
299
|
+
const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
|
|
300
|
+
read.catch(() => undefined);
|
|
301
|
+
return read;
|
|
204
302
|
}
|
|
205
303
|
async message(conversation, input) {
|
|
206
304
|
const content = [{ type: 'text', text: String(input.text) }];
|
|
207
305
|
let route;
|
|
306
|
+
let instructing = null;
|
|
208
307
|
try {
|
|
209
308
|
route = await this.route(conversation, input.agent_id);
|
|
309
|
+
if (input.first === true)
|
|
310
|
+
instructing = this.instructed(conversation, route);
|
|
210
311
|
const files = input.files;
|
|
312
|
+
if (files.length > 0)
|
|
313
|
+
this.prepares(conversation, 'files');
|
|
211
314
|
const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
|
|
212
315
|
const lines = await this.transcribed(conversation, files, paths);
|
|
213
316
|
if (lines.length > 0)
|
|
@@ -216,11 +319,13 @@ export class Conversations {
|
|
|
216
319
|
throw new Error(KILLED);
|
|
217
320
|
}
|
|
218
321
|
catch (error) {
|
|
219
|
-
|
|
322
|
+
this.prepares(conversation, null);
|
|
323
|
+
return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
220
324
|
}
|
|
325
|
+
conversation.preparing = null;
|
|
221
326
|
return {
|
|
222
327
|
ack: new Promise((settle) => {
|
|
223
|
-
conversation.waiting.push({ input, route, content, settle });
|
|
328
|
+
conversation.waiting.push({ input, route, content, instructing, settle });
|
|
224
329
|
this.next(conversation);
|
|
225
330
|
}),
|
|
226
331
|
};
|
|
@@ -229,8 +334,12 @@ export class Conversations {
|
|
|
229
334
|
if (conversation.sending !== null || conversation.open > 0)
|
|
230
335
|
return;
|
|
231
336
|
const waiting = conversation.waiting.shift();
|
|
232
|
-
if (waiting === undefined)
|
|
337
|
+
if (waiting === undefined) {
|
|
338
|
+
const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
|
|
339
|
+
if (conversation.starting !== step)
|
|
340
|
+
this.starts(conversation, step);
|
|
233
341
|
return;
|
|
342
|
+
}
|
|
234
343
|
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
235
344
|
conversation.sending = null;
|
|
236
345
|
this.next(conversation);
|
|
@@ -239,40 +348,80 @@ export class Conversations {
|
|
|
239
348
|
async send(conversation, waiting) {
|
|
240
349
|
const { input } = waiting;
|
|
241
350
|
let ack = {};
|
|
351
|
+
let started = false;
|
|
352
|
+
let instructing = waiting.instructing;
|
|
242
353
|
const prompt = [];
|
|
243
354
|
try {
|
|
244
355
|
if (conversation.running === null) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
356
|
+
this.starts(conversation, 'session');
|
|
357
|
+
await this.launched(conversation);
|
|
358
|
+
}
|
|
359
|
+
if (conversation.running === null) {
|
|
360
|
+
const stored = conversation.session ?? input.provider_session_id;
|
|
361
|
+
const empty = input.first === true && conversation.prompted !== stored;
|
|
362
|
+
const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
|
|
363
|
+
if (input.first === true)
|
|
364
|
+
instructing = this.instructed(conversation, waiting.route);
|
|
365
|
+
started = (await launch.session) !== stored;
|
|
366
|
+
}
|
|
367
|
+
const session = conversation.running.sessionId;
|
|
368
|
+
if (started || (input.first === true && session !== input.provider_session_id)) {
|
|
369
|
+
ack = { provider_session_id: session };
|
|
249
370
|
}
|
|
250
371
|
if (input.first === true) {
|
|
251
|
-
prompt.push({
|
|
252
|
-
type: 'text',
|
|
253
|
-
text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
|
|
254
|
-
});
|
|
372
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
255
373
|
}
|
|
256
374
|
if (conversation.kills > 0)
|
|
257
375
|
throw new Error(KILLED);
|
|
258
376
|
}
|
|
259
377
|
catch (error) {
|
|
260
|
-
|
|
378
|
+
this.starts(conversation, null);
|
|
379
|
+
if (started) {
|
|
261
380
|
conversation.session = null;
|
|
262
381
|
await this.kill(conversation);
|
|
263
382
|
}
|
|
264
|
-
waiting.settle({ refused: conversation
|
|
383
|
+
waiting.settle({ refused: refusal(conversation, error) });
|
|
265
384
|
return;
|
|
266
385
|
}
|
|
267
|
-
this.
|
|
386
|
+
this.starts(conversation, null);
|
|
387
|
+
this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.inputs.hands(String(input.input_id), ack));
|
|
268
388
|
waiting.settle(ack);
|
|
269
389
|
}
|
|
390
|
+
starts(conversation, step) {
|
|
391
|
+
conversation.starting = step;
|
|
392
|
+
if (conversation.shown !== null)
|
|
393
|
+
clearTimeout(conversation.shown);
|
|
394
|
+
conversation.shown = null;
|
|
395
|
+
if (step === null || conversation.told !== null) {
|
|
396
|
+
this.tell(conversation, step);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
conversation.shown = setTimeout(() => {
|
|
400
|
+
conversation.shown = null;
|
|
401
|
+
if (conversation.starting === step)
|
|
402
|
+
this.tell(conversation, step);
|
|
403
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
404
|
+
}
|
|
405
|
+
prepares(conversation, step) {
|
|
406
|
+
conversation.preparing = step;
|
|
407
|
+
this.next(conversation);
|
|
408
|
+
}
|
|
409
|
+
tell(conversation, step) {
|
|
410
|
+
if (conversation.told === step && !conversation.untold)
|
|
411
|
+
return;
|
|
412
|
+
conversation.told = step;
|
|
413
|
+
this.retell(conversation);
|
|
414
|
+
}
|
|
415
|
+
retell(conversation) {
|
|
416
|
+
conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
|
|
417
|
+
}
|
|
270
418
|
async transcribed(conversation, files, paths) {
|
|
271
419
|
const lines = [];
|
|
272
420
|
for (const [index, path] of paths.entries()) {
|
|
273
421
|
lines.push(path);
|
|
274
422
|
if (files[index].media_type?.startsWith('audio/') !== true)
|
|
275
423
|
continue;
|
|
424
|
+
this.prepares(conversation, 'transcription');
|
|
276
425
|
try {
|
|
277
426
|
const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
|
|
278
427
|
if (text !== null && text !== '')
|
|
@@ -339,14 +488,16 @@ export class Conversations {
|
|
|
339
488
|
}
|
|
340
489
|
return route;
|
|
341
490
|
}
|
|
342
|
-
async start(conversation,
|
|
491
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
492
|
+
const route = await routed;
|
|
343
493
|
const reader = readerFor(route.kind);
|
|
344
494
|
reader.session = sessionId;
|
|
345
495
|
const app = client({ name: '@agentshouse/kit' })
|
|
346
496
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
347
|
-
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
497
|
+
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
498
|
+
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
|
|
348
499
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
349
|
-
const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
500
|
+
const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
|
|
350
501
|
if (conversation.kills > 0) {
|
|
351
502
|
bridge.close();
|
|
352
503
|
throw new Error(KILLED);
|
|
@@ -354,39 +505,39 @@ export class Conversations {
|
|
|
354
505
|
conversation.opening = bridge;
|
|
355
506
|
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
356
507
|
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
508
|
+
let adapter = null;
|
|
357
509
|
try {
|
|
358
510
|
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
511
|
+
bridged(bridge);
|
|
359
512
|
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
360
|
-
const
|
|
513
|
+
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
514
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
361
515
|
work.read(message);
|
|
362
516
|
this.read(conversation, reader, message);
|
|
363
|
-
}, { ...bridge.env, ...
|
|
517
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
364
518
|
void adapter.exited.then(() => bridge.close());
|
|
365
519
|
const agent = adapter.connection.agent;
|
|
366
520
|
const cwd = route.working_directory;
|
|
367
521
|
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
|
|
522
|
+
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
523
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
371
524
|
reader.session = opened.sessionId;
|
|
372
|
-
if (sessionId
|
|
525
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
373
526
|
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
374
|
-
await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
|
|
527
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
375
528
|
}
|
|
376
529
|
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
377
|
-
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
530
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
378
531
|
void work.settled().then(() => scope.record());
|
|
379
532
|
conversation.running = running;
|
|
380
533
|
conversation.session = running.sessionId;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
this.options(conversation, options);
|
|
385
|
-
void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
|
|
534
|
+
conversation.options = options;
|
|
535
|
+
this.facts(conversation);
|
|
536
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
386
537
|
return running.sessionId;
|
|
387
538
|
}
|
|
388
539
|
catch (error) {
|
|
389
|
-
|
|
540
|
+
adapter?.retire();
|
|
390
541
|
work.close();
|
|
391
542
|
conversation.leftovers.add(scope);
|
|
392
543
|
bridge.close();
|
|
@@ -396,6 +547,18 @@ export class Conversations {
|
|
|
396
547
|
conversation.opening = null;
|
|
397
548
|
}
|
|
398
549
|
}
|
|
550
|
+
async resume(conversation, kind, adapter, request, empty) {
|
|
551
|
+
try {
|
|
552
|
+
return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
556
|
+
throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
|
|
557
|
+
}
|
|
558
|
+
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
399
562
|
async launchSettings(running, route, offered) {
|
|
400
563
|
let options = offered;
|
|
401
564
|
const settings = [
|
|
@@ -417,13 +580,10 @@ export class Conversations {
|
|
|
417
580
|
}
|
|
418
581
|
return options;
|
|
419
582
|
}
|
|
420
|
-
exited(conversation,
|
|
421
|
-
const running = conversation.running;
|
|
422
|
-
if (running?.adapter !== adapter)
|
|
423
|
-
return;
|
|
583
|
+
exited(conversation, running, cause) {
|
|
424
584
|
this.detach(conversation, running);
|
|
425
585
|
if (conversation.turn !== null) {
|
|
426
|
-
this.end(conversation, conversation.turn,
|
|
586
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
427
587
|
}
|
|
428
588
|
this.next(conversation);
|
|
429
589
|
this.kit.changed();
|
|
@@ -433,11 +593,22 @@ export class Conversations {
|
|
|
433
593
|
conversation.leftovers.add(running.scope);
|
|
434
594
|
conversation.running = null;
|
|
435
595
|
conversation.commands = null;
|
|
596
|
+
conversation.options = null;
|
|
436
597
|
for (const [id, question] of this.questions) {
|
|
437
598
|
if (question.conversation === conversation)
|
|
438
599
|
this.questions.delete(id);
|
|
439
600
|
}
|
|
440
|
-
this.
|
|
601
|
+
this.process(conversation, false);
|
|
602
|
+
}
|
|
603
|
+
process(conversation, running) {
|
|
604
|
+
conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
|
|
605
|
+
}
|
|
606
|
+
facts(conversation) {
|
|
607
|
+
this.process(conversation, true);
|
|
608
|
+
if (conversation.commands !== null)
|
|
609
|
+
this.commands(conversation, conversation.commands);
|
|
610
|
+
if (conversation.options !== null)
|
|
611
|
+
this.options(conversation, conversation.options);
|
|
441
612
|
}
|
|
442
613
|
commands(conversation, commands) {
|
|
443
614
|
conversation.commands = commands;
|
|
@@ -448,6 +619,7 @@ export class Conversations {
|
|
|
448
619
|
options(conversation, options) {
|
|
449
620
|
if (conversation.running === null)
|
|
450
621
|
return;
|
|
622
|
+
conversation.options = options;
|
|
451
623
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
452
624
|
}
|
|
453
625
|
deliver(conversation, turn, event, body) {
|
|
@@ -462,7 +634,7 @@ export class Conversations {
|
|
|
462
634
|
return delivered;
|
|
463
635
|
}
|
|
464
636
|
write(conversation, turn, operation) {
|
|
465
|
-
if (
|
|
637
|
+
if (turn.ended || !this.watched)
|
|
466
638
|
return;
|
|
467
639
|
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
468
640
|
}
|
|
@@ -473,12 +645,7 @@ export class Conversations {
|
|
|
473
645
|
begin(conversation) {
|
|
474
646
|
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
475
647
|
conversation.open++;
|
|
476
|
-
void this.deliver(conversation, turn, 'started', {})
|
|
477
|
-
.then(() => {
|
|
478
|
-
turn.admitted = true;
|
|
479
|
-
this.replay(conversation, turn);
|
|
480
|
-
})
|
|
481
|
-
.catch(logged);
|
|
648
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
482
649
|
conversation.turn = turn;
|
|
483
650
|
if (conversation.context !== null)
|
|
484
651
|
turn.parts.measure(conversation.context);
|
|
@@ -487,45 +654,63 @@ export class Conversations {
|
|
|
487
654
|
this.kit.changed();
|
|
488
655
|
return turn;
|
|
489
656
|
}
|
|
490
|
-
end(conversation, turn, failed) {
|
|
657
|
+
end(conversation, turn, failed, notice = null) {
|
|
491
658
|
if (turn.ended)
|
|
492
659
|
return;
|
|
660
|
+
if (failed !== null)
|
|
661
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
493
662
|
if (conversation.turn === turn) {
|
|
494
663
|
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
495
664
|
this.apply(conversation, turn, event);
|
|
496
665
|
}
|
|
666
|
+
if (notice !== null)
|
|
667
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
497
668
|
turn.ended = true;
|
|
498
669
|
conversation.open--;
|
|
499
670
|
if (conversation.turn === turn)
|
|
500
671
|
conversation.turn = null;
|
|
501
672
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
502
|
-
parts: turn.parts.
|
|
673
|
+
parts: turn.parts.saved(),
|
|
503
674
|
context: turn.parts.context,
|
|
504
|
-
...failed,
|
|
675
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
505
676
|
}));
|
|
506
677
|
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
507
678
|
this.next(conversation);
|
|
508
679
|
this.kit.changed();
|
|
509
680
|
}
|
|
510
|
-
prompt(conversation, prompt) {
|
|
681
|
+
prompt(conversation, prompt, taken) {
|
|
511
682
|
const running = conversation.running;
|
|
512
683
|
const turn = this.begin(conversation);
|
|
513
684
|
turn.prompted = true;
|
|
514
|
-
|
|
685
|
+
turn.taken = taken;
|
|
686
|
+
conversation.prompted = running.sessionId;
|
|
687
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
688
|
+
this.took(turn);
|
|
689
|
+
const notice = answeredFailure(answer);
|
|
690
|
+
if (notice !== null) {
|
|
691
|
+
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
695
|
+
}
|
|
696
|
+
}, async (error) => {
|
|
515
697
|
if (running.adapter.connection.signal.aborted) {
|
|
516
|
-
this.end(conversation, turn,
|
|
698
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
517
699
|
}
|
|
518
700
|
else {
|
|
701
|
+
this.took(turn);
|
|
519
702
|
if (conversation.open === 1 && conversation.running === running) {
|
|
520
|
-
running.
|
|
703
|
+
running.adapter.retire();
|
|
521
704
|
this.detach(conversation, running);
|
|
522
705
|
}
|
|
523
|
-
this.end(conversation, turn,
|
|
706
|
+
this.end(conversation, turn, answeredWith(error));
|
|
524
707
|
}
|
|
525
708
|
});
|
|
526
709
|
}
|
|
527
710
|
started(conversation) {
|
|
528
|
-
|
|
711
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
712
|
+
this.took(turn);
|
|
713
|
+
return turn;
|
|
529
714
|
}
|
|
530
715
|
ended(conversation) {
|
|
531
716
|
const turn = conversation.turn;
|
|
@@ -620,6 +805,11 @@ export class Conversations {
|
|
|
620
805
|
}
|
|
621
806
|
this.apply(conversation, this.started(conversation), event);
|
|
622
807
|
}
|
|
808
|
+
took(turn) {
|
|
809
|
+
const taken = turn.taken;
|
|
810
|
+
turn.taken = null;
|
|
811
|
+
taken?.();
|
|
812
|
+
}
|
|
623
813
|
apply(conversation, turn, event) {
|
|
624
814
|
switch (event.kind) {
|
|
625
815
|
case 'text':
|
|
@@ -644,7 +834,7 @@ export class Conversations {
|
|
|
644
834
|
turn.parts.plan(event.entries);
|
|
645
835
|
return;
|
|
646
836
|
case 'image':
|
|
647
|
-
turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
|
|
837
|
+
turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
|
|
648
838
|
this.kit.changed();
|
|
649
839
|
return;
|
|
650
840
|
}
|
|
@@ -664,7 +854,7 @@ export class Conversations {
|
|
|
664
854
|
else
|
|
665
855
|
conversation.turn.parts.start(part);
|
|
666
856
|
}
|
|
667
|
-
async image(conversation, turn, data, mediaType) {
|
|
857
|
+
async image(conversation, turn, reserved, data, mediaType) {
|
|
668
858
|
const bytes = Buffer.from(data, 'base64');
|
|
669
859
|
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
670
860
|
try {
|
|
@@ -677,10 +867,10 @@ export class Conversations {
|
|
|
677
867
|
const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
|
|
678
868
|
if (saved.save.status !== 'saved')
|
|
679
869
|
throw new Error(saved.save.failure ?? saved.save.status);
|
|
680
|
-
turn.parts.
|
|
870
|
+
turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
|
|
681
871
|
}
|
|
682
872
|
catch (error) {
|
|
683
|
-
turn.parts.
|
|
873
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
|
|
684
874
|
}
|
|
685
875
|
}
|
|
686
876
|
}
|