@agentshouse/kit 0.2.1-cca496635583e → 0.2.1-ccf188e02985d
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 +23 -22
- package/dist/acp.js +34 -14
- package/dist/agents.js +108 -21
- package/dist/api.js +5 -1
- package/dist/app-git.js +10 -26
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +3 -6
- package/dist/chatgpt.js +32 -0
- package/dist/clis.js +51 -9
- package/dist/conversations.js +137 -70
- package/dist/copies.js +78 -34
- package/dist/end.js +5 -0
- package/dist/events.js +4 -0
- package/dist/files.js +8 -2
- package/dist/git.js +10 -8
- package/dist/home.js +56 -5
- package/dist/house-cli.js +291 -0
- package/dist/house-main.js +7 -189
- package/dist/inputs.js +37 -0
- package/dist/instructions.js +4 -28
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -22
- package/dist/login.js +47 -13
- package/dist/logout.js +0 -2
- package/dist/parts.js +34 -16
- package/dist/placement.js +30 -0
- package/dist/relay.js +1 -1
- package/dist/resident.js +65 -16
- package/dist/service.js +22 -0
- package/dist/sign-in.js +5 -23
- package/dist/skills.js +81 -12
- package/dist/stream.js +66 -17
- package/dist/transcription.js +3 -3
- package/dist/update.js +48 -29
- package/dist/wire.js +6 -0
- package/dist/work.js +3 -5
- package/how-we-work.md +13 -3
- package/package.json +2 -1
- 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/clis.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { constants, readFileSync } from 'node:fs';
|
|
3
|
-
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { homedir, userInfo } from 'node:os';
|
|
5
|
-
import { delimiter, isAbsolute, join } from 'node:path';
|
|
6
|
+
import { delimiter, dirname, isAbsolute, join } from 'node:path';
|
|
6
7
|
import { kitHome } from './home.js';
|
|
8
|
+
const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
|
|
9
|
+
function houseRule(houses) {
|
|
10
|
+
return `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`;
|
|
11
|
+
}
|
|
7
12
|
const RAW_SUBTYPES = [
|
|
8
13
|
'background_tasks_changed',
|
|
9
14
|
'task_started',
|
|
@@ -22,17 +27,42 @@ async function executable(path) {
|
|
|
22
27
|
return false;
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
|
-
async function
|
|
30
|
+
async function listedHouses(file) {
|
|
31
|
+
let text;
|
|
32
|
+
try {
|
|
33
|
+
text = await readFile(file, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === 'ENOENT')
|
|
37
|
+
return [];
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const listed = JSON.parse(HOUSE_EXECUTABLES.exec(text)?.[1] ?? '[]');
|
|
42
|
+
return Array.isArray(listed) ? listed.filter((house) => typeof house === 'string') : [];
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function allowHouseInCodex(path) {
|
|
26
49
|
const candidates = [...new Set(path.split(delimiter).filter((directory) => isAbsolute(directory)))].map((directory) => join(directory, 'house'));
|
|
27
|
-
const houses = [];
|
|
28
|
-
for (const candidate of candidates)
|
|
29
|
-
if (await executable(candidate))
|
|
30
|
-
houses.push(candidate);
|
|
31
50
|
const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
|
|
51
|
+
const file = join(rules, 'house.rules');
|
|
52
|
+
const houses = [];
|
|
53
|
+
for (const house of [...(await listedHouses(file)), ...candidates]) {
|
|
54
|
+
if (!houses.includes(house) && (await executable(house)))
|
|
55
|
+
houses.push(house);
|
|
56
|
+
}
|
|
32
57
|
await mkdir(rules, { recursive: true });
|
|
33
|
-
|
|
58
|
+
const temporary = `${file}.${randomUUID()}`;
|
|
59
|
+
await writeFile(temporary, houseRule(houses));
|
|
60
|
+
await rename(temporary, file);
|
|
34
61
|
}
|
|
35
62
|
async function nothing() { }
|
|
63
|
+
function answered(error) {
|
|
64
|
+
return typeof error === 'object' && error !== null ? error : {};
|
|
65
|
+
}
|
|
36
66
|
function bridgeInClaudeSandbox(bridge) {
|
|
37
67
|
return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
|
|
38
68
|
}
|
|
@@ -50,6 +80,10 @@ export const CLIS = {
|
|
|
50
80
|
args: [],
|
|
51
81
|
fullAccess: 'agent-full-access',
|
|
52
82
|
sessionMeta: () => ({}),
|
|
83
|
+
unknownSession: (error) => {
|
|
84
|
+
const { code, data } = answered(error);
|
|
85
|
+
return code === -32603 && typeof data?.details === 'string' && data.details.startsWith('no rollout found');
|
|
86
|
+
},
|
|
53
87
|
renames: true,
|
|
54
88
|
allowHouse: allowHouseInCodex,
|
|
55
89
|
signedIn: { command: ['login', 'status'] },
|
|
@@ -79,6 +113,7 @@ export const CLIS = {
|
|
|
79
113
|
emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
|
|
80
114
|
},
|
|
81
115
|
}),
|
|
116
|
+
unknownSession: (error) => answered(error).code === -32002,
|
|
82
117
|
renames: false,
|
|
83
118
|
allowHouse: nothing,
|
|
84
119
|
signedIn: { command: ['auth', 'status'] },
|
|
@@ -93,6 +128,7 @@ export const CLIS = {
|
|
|
93
128
|
args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
|
|
94
129
|
fullAccess: null,
|
|
95
130
|
sessionMeta: () => ({}),
|
|
131
|
+
unknownSession: (error) => answered(error).data?.code === 'FS_NOT_FOUND',
|
|
96
132
|
renames: false,
|
|
97
133
|
allowHouse: nothing,
|
|
98
134
|
signedIn: { initializeMeta: 'defaultAuthMethodId' },
|
|
@@ -173,7 +209,7 @@ export async function installAdapter(kind, signal) {
|
|
|
173
209
|
const release = KIT_PACKAGE.peerDependencies[adapter.package];
|
|
174
210
|
if ((await installedAdapter(kind)) === release)
|
|
175
211
|
return null;
|
|
176
|
-
const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
|
|
212
|
+
const ran = await run('npm', ['install', '--prefix', prefix(kind), '--cache', join(kitHome(), 'npm-cache'), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
|
|
177
213
|
return ran.status === 0 ? null : failureOf(ran, 'npm');
|
|
178
214
|
}
|
|
179
215
|
export async function installCli(kind, signal) {
|
|
@@ -198,6 +234,12 @@ export async function loginPath() {
|
|
|
198
234
|
const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
|
|
199
235
|
return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
|
|
200
236
|
}
|
|
237
|
+
export async function writeCodexRule() {
|
|
238
|
+
const shell = await loginPath().catch(() => '');
|
|
239
|
+
await allowHouseInCodex([dirname(process.execPath), process.env.PATH ?? '', shell].join(delimiter)).catch((error) => {
|
|
240
|
+
throw new Error(`Codex's rule for house could not be written: ${error.message}`);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
201
243
|
export async function versionOf(path, signal) {
|
|
202
244
|
return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
|
|
203
245
|
}
|
package/dist/conversations.js
CHANGED
|
@@ -1,31 +1,82 @@
|
|
|
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 NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
|
|
35
|
+
class NotStarted extends Error {
|
|
36
|
+
}
|
|
37
|
+
async function step(reason, work) {
|
|
38
|
+
try {
|
|
39
|
+
return await work;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function unbridged(error) {
|
|
46
|
+
return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
|
|
47
|
+
}
|
|
48
|
+
const EXIT_PLAN_MODE = {
|
|
49
|
+
parse(value) {
|
|
50
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
51
|
+
throw new Error('_x.ai/exit_plan_mode needs an object');
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
|
|
20
57
|
function causeOf(error) {
|
|
21
58
|
return error instanceof Error ? error.message : String(error);
|
|
22
59
|
}
|
|
60
|
+
function answeredWith(error) {
|
|
61
|
+
const signedOut = typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
|
|
62
|
+
return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
63
|
+
}
|
|
64
|
+
function exitedWith(running, cause) {
|
|
65
|
+
return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
|
|
66
|
+
}
|
|
23
67
|
function answerMessage(request, response) {
|
|
24
68
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
25
69
|
}
|
|
26
70
|
function logged(error) {
|
|
27
71
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
28
72
|
}
|
|
73
|
+
function refusal(conversation, error) {
|
|
74
|
+
if (conversation.kills > 0)
|
|
75
|
+
return KILLED;
|
|
76
|
+
const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
77
|
+
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
78
|
+
return causeOf(error);
|
|
79
|
+
}
|
|
29
80
|
function marker(bridge) {
|
|
30
81
|
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
31
82
|
}
|
|
@@ -56,6 +107,7 @@ class Conversation {
|
|
|
56
107
|
subagents = new Map();
|
|
57
108
|
held = [];
|
|
58
109
|
opening = null;
|
|
110
|
+
prompted = null;
|
|
59
111
|
kills = 0;
|
|
60
112
|
killed = new AbortController();
|
|
61
113
|
interrupts = 0;
|
|
@@ -78,9 +130,7 @@ function asksSecret(request) {
|
|
|
78
130
|
export class Conversations {
|
|
79
131
|
kit;
|
|
80
132
|
conversations = new Map();
|
|
81
|
-
acks = new Map();
|
|
82
133
|
questions = new Map();
|
|
83
|
-
carrying = 0;
|
|
84
134
|
reporting = 0;
|
|
85
135
|
watched = false;
|
|
86
136
|
constructor(kit) {
|
|
@@ -89,37 +139,26 @@ export class Conversations {
|
|
|
89
139
|
setInterval(() => this.look(), 2_000);
|
|
90
140
|
}
|
|
91
141
|
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();
|
|
142
|
+
void this.kit.inputs.carry(input.input_id, () => {
|
|
143
|
+
const conversation = this.conversation(input.conversation_id);
|
|
144
|
+
if (input.kind === 'kill') {
|
|
145
|
+
conversation.kills++;
|
|
146
|
+
conversation.killed.abort(new Error(KILLED));
|
|
147
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
148
|
+
waiting.settle({ refused: KILLED });
|
|
149
|
+
this.halt(conversation);
|
|
150
|
+
}
|
|
151
|
+
if (input.kind === 'interrupt') {
|
|
152
|
+
conversation.interrupts++;
|
|
153
|
+
conversation.interrupted.abort();
|
|
154
|
+
}
|
|
155
|
+
const handled = conversation.queue.then(() => this.carry(conversation, input));
|
|
156
|
+
conversation.queue = handled.catch(() => undefined);
|
|
157
|
+
return handled.then(async ({ ack }) => ({ ack: await ack }));
|
|
119
158
|
});
|
|
120
159
|
}
|
|
121
160
|
idle() {
|
|
122
|
-
if (this.
|
|
161
|
+
if (this.reporting > 0)
|
|
123
162
|
return false;
|
|
124
163
|
for (const conversation of this.conversations.values()) {
|
|
125
164
|
const turn = conversation.turn;
|
|
@@ -199,10 +238,10 @@ export class Conversations {
|
|
|
199
238
|
if (conversation.running !== null)
|
|
200
239
|
return {};
|
|
201
240
|
try {
|
|
202
|
-
return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
|
|
241
|
+
return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
|
|
203
242
|
}
|
|
204
243
|
catch (error) {
|
|
205
|
-
return { refused:
|
|
244
|
+
return { refused: refusal(conversation, error) };
|
|
206
245
|
}
|
|
207
246
|
}
|
|
208
247
|
async message(conversation, input) {
|
|
@@ -219,7 +258,7 @@ export class Conversations {
|
|
|
219
258
|
throw new Error(KILLED);
|
|
220
259
|
}
|
|
221
260
|
catch (error) {
|
|
222
|
-
return { ack: Promise.resolve({ refused: conversation
|
|
261
|
+
return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
223
262
|
}
|
|
224
263
|
return {
|
|
225
264
|
ack: new Promise((settle) => {
|
|
@@ -242,13 +281,17 @@ export class Conversations {
|
|
|
242
281
|
async send(conversation, waiting) {
|
|
243
282
|
const { input } = waiting;
|
|
244
283
|
let ack = {};
|
|
284
|
+
let started = false;
|
|
245
285
|
const prompt = [];
|
|
246
286
|
try {
|
|
247
287
|
if (conversation.running === null) {
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
288
|
+
const stored = conversation.session ?? input.provider_session_id;
|
|
289
|
+
const empty = input.first === true && conversation.prompted !== stored;
|
|
290
|
+
started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
|
|
291
|
+
}
|
|
292
|
+
const session = conversation.running.sessionId;
|
|
293
|
+
if (started || (input.first === true && session !== input.provider_session_id)) {
|
|
294
|
+
ack = { provider_session_id: session };
|
|
252
295
|
}
|
|
253
296
|
if (input.first === true) {
|
|
254
297
|
prompt.push({
|
|
@@ -260,11 +303,11 @@ export class Conversations {
|
|
|
260
303
|
throw new Error(KILLED);
|
|
261
304
|
}
|
|
262
305
|
catch (error) {
|
|
263
|
-
if (
|
|
306
|
+
if (started) {
|
|
264
307
|
conversation.session = null;
|
|
265
308
|
await this.kill(conversation);
|
|
266
309
|
}
|
|
267
|
-
waiting.settle({ refused: conversation
|
|
310
|
+
waiting.settle({ refused: refusal(conversation, error) });
|
|
268
311
|
return;
|
|
269
312
|
}
|
|
270
313
|
this.prompt(conversation, [...prompt, ...waiting.content]);
|
|
@@ -342,14 +385,15 @@ export class Conversations {
|
|
|
342
385
|
}
|
|
343
386
|
return route;
|
|
344
387
|
}
|
|
345
|
-
async start(conversation, route, sessionId) {
|
|
388
|
+
async start(conversation, route, sessionId, empty) {
|
|
346
389
|
const reader = readerFor(route.kind);
|
|
347
390
|
reader.session = sessionId;
|
|
348
391
|
const app = client({ name: '@agentshouse/kit' })
|
|
349
392
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
350
|
-
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
393
|
+
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
394
|
+
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
|
|
351
395
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
352
|
-
const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
396
|
+
const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
|
|
353
397
|
if (conversation.kills > 0) {
|
|
354
398
|
bridge.close();
|
|
355
399
|
throw new Error(KILLED);
|
|
@@ -357,27 +401,28 @@ export class Conversations {
|
|
|
357
401
|
conversation.opening = bridge;
|
|
358
402
|
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
359
403
|
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
404
|
+
let adapter = null;
|
|
360
405
|
try {
|
|
361
406
|
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
362
407
|
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
363
|
-
const
|
|
408
|
+
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
409
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
364
410
|
work.read(message);
|
|
365
411
|
this.read(conversation, reader, message);
|
|
366
|
-
}, { ...bridge.env, ...
|
|
412
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
367
413
|
void adapter.exited.then(() => bridge.close());
|
|
368
414
|
const agent = adapter.connection.agent;
|
|
369
415
|
const cwd = route.working_directory;
|
|
370
416
|
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
|
|
417
|
+
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
418
|
+
const opened = resumed ?? (await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
374
419
|
reader.session = opened.sessionId;
|
|
375
|
-
if (sessionId
|
|
420
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
376
421
|
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
377
|
-
await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
|
|
422
|
+
await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
378
423
|
}
|
|
379
424
|
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
380
|
-
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
425
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
381
426
|
void work.settled().then(() => scope.record());
|
|
382
427
|
conversation.running = running;
|
|
383
428
|
conversation.session = running.sessionId;
|
|
@@ -385,11 +430,11 @@ export class Conversations {
|
|
|
385
430
|
if (conversation.commands !== null)
|
|
386
431
|
this.commands(conversation, conversation.commands);
|
|
387
432
|
this.options(conversation, options);
|
|
388
|
-
void adapter.
|
|
433
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
389
434
|
return running.sessionId;
|
|
390
435
|
}
|
|
391
436
|
catch (error) {
|
|
392
|
-
|
|
437
|
+
adapter?.retire();
|
|
393
438
|
work.close();
|
|
394
439
|
conversation.leftovers.add(scope);
|
|
395
440
|
bridge.close();
|
|
@@ -399,6 +444,18 @@ export class Conversations {
|
|
|
399
444
|
conversation.opening = null;
|
|
400
445
|
}
|
|
401
446
|
}
|
|
447
|
+
async resume(conversation, kind, adapter, request, empty) {
|
|
448
|
+
try {
|
|
449
|
+
return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
453
|
+
throw new NotStarted(NO_RESUME, { cause: error });
|
|
454
|
+
}
|
|
455
|
+
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
402
459
|
async launchSettings(running, route, offered) {
|
|
403
460
|
let options = offered;
|
|
404
461
|
const settings = [
|
|
@@ -420,13 +477,10 @@ export class Conversations {
|
|
|
420
477
|
}
|
|
421
478
|
return options;
|
|
422
479
|
}
|
|
423
|
-
exited(conversation,
|
|
424
|
-
const running = conversation.running;
|
|
425
|
-
if (running?.adapter !== adapter)
|
|
426
|
-
return;
|
|
480
|
+
exited(conversation, running, cause) {
|
|
427
481
|
this.detach(conversation, running);
|
|
428
482
|
if (conversation.turn !== null) {
|
|
429
|
-
this.end(conversation, conversation.turn,
|
|
483
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
430
484
|
}
|
|
431
485
|
this.next(conversation);
|
|
432
486
|
this.kit.changed();
|
|
@@ -490,21 +544,25 @@ export class Conversations {
|
|
|
490
544
|
this.kit.changed();
|
|
491
545
|
return turn;
|
|
492
546
|
}
|
|
493
|
-
end(conversation, turn, failed) {
|
|
547
|
+
end(conversation, turn, failed, notice = null) {
|
|
494
548
|
if (turn.ended)
|
|
495
549
|
return;
|
|
550
|
+
if (failed !== null)
|
|
551
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
496
552
|
if (conversation.turn === turn) {
|
|
497
553
|
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
498
554
|
this.apply(conversation, turn, event);
|
|
499
555
|
}
|
|
556
|
+
if (notice !== null)
|
|
557
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
500
558
|
turn.ended = true;
|
|
501
559
|
conversation.open--;
|
|
502
560
|
if (conversation.turn === turn)
|
|
503
561
|
conversation.turn = null;
|
|
504
562
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
505
|
-
parts: turn.parts.
|
|
563
|
+
parts: turn.parts.saved(),
|
|
506
564
|
context: turn.parts.context,
|
|
507
|
-
...failed,
|
|
565
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
508
566
|
}));
|
|
509
567
|
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
510
568
|
this.next(conversation);
|
|
@@ -514,16 +572,25 @@ export class Conversations {
|
|
|
514
572
|
const running = conversation.running;
|
|
515
573
|
const turn = this.begin(conversation);
|
|
516
574
|
turn.prompted = true;
|
|
517
|
-
|
|
575
|
+
conversation.prompted = running.sessionId;
|
|
576
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
577
|
+
const notice = answeredFailure(answer);
|
|
578
|
+
if (notice !== null) {
|
|
579
|
+
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
583
|
+
}
|
|
584
|
+
}, async (error) => {
|
|
518
585
|
if (running.adapter.connection.signal.aborted) {
|
|
519
|
-
this.end(conversation, turn,
|
|
586
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
520
587
|
}
|
|
521
588
|
else {
|
|
522
589
|
if (conversation.open === 1 && conversation.running === running) {
|
|
523
|
-
running.
|
|
590
|
+
running.adapter.retire();
|
|
524
591
|
this.detach(conversation, running);
|
|
525
592
|
}
|
|
526
|
-
this.end(conversation, turn,
|
|
593
|
+
this.end(conversation, turn, answeredWith(error));
|
|
527
594
|
}
|
|
528
595
|
});
|
|
529
596
|
}
|
|
@@ -647,7 +714,7 @@ export class Conversations {
|
|
|
647
714
|
turn.parts.plan(event.entries);
|
|
648
715
|
return;
|
|
649
716
|
case 'image':
|
|
650
|
-
turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
|
|
717
|
+
turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
|
|
651
718
|
this.kit.changed();
|
|
652
719
|
return;
|
|
653
720
|
}
|
|
@@ -667,7 +734,7 @@ export class Conversations {
|
|
|
667
734
|
else
|
|
668
735
|
conversation.turn.parts.start(part);
|
|
669
736
|
}
|
|
670
|
-
async image(conversation, turn, data, mediaType) {
|
|
737
|
+
async image(conversation, turn, reserved, data, mediaType) {
|
|
671
738
|
const bytes = Buffer.from(data, 'base64');
|
|
672
739
|
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
673
740
|
try {
|
|
@@ -680,10 +747,10 @@ export class Conversations {
|
|
|
680
747
|
const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
|
|
681
748
|
if (saved.save.status !== 'saved')
|
|
682
749
|
throw new Error(saved.save.failure ?? saved.save.status);
|
|
683
|
-
turn.parts.
|
|
750
|
+
turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
|
|
684
751
|
}
|
|
685
752
|
catch (error) {
|
|
686
|
-
turn.parts.
|
|
753
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
|
|
687
754
|
}
|
|
688
755
|
}
|
|
689
756
|
}
|