@agentshouse/kit 0.2.1-cbabe8ad337ff → 0.2.1-cc719fffeec71
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 +140 -70
- package/dist/copies.js +195 -119
- package/dist/end.js +18 -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 +37 -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 +30 -0
- package/dist/relay.js +12 -2
- package/dist/resident.js +82 -26
- 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/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 +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/bridge.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { once } from 'node:events';
|
|
3
3
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
-
import { mkdir } from 'node:fs/promises';
|
|
5
4
|
import { createServer, request as httpRequest } from 'node:http';
|
|
6
5
|
import { request as httpsRequest } from 'node:https';
|
|
7
6
|
import { delimiter, dirname, join } from 'node:path';
|
|
@@ -9,7 +8,7 @@ import { parse } from 'yaml';
|
|
|
9
8
|
import { finalRefusal, HouseRefusal, retryDelay, retrying } from './api.js';
|
|
10
9
|
import { loginPath } from './clis.js';
|
|
11
10
|
import { changedPaths, } from './copies.js';
|
|
12
|
-
import { kitHome, readEnrolment } from './home.js';
|
|
11
|
+
import { kitHome, readEnrolment, socketDirectory } from './home.js';
|
|
13
12
|
import { callHouse, mcpBody, rpc, UNREACHABLE } from './mcp.js';
|
|
14
13
|
import { operationId } from './operation.js';
|
|
15
14
|
import { refusalOf, retryable } from './refusals.js';
|
|
@@ -82,7 +81,7 @@ function editAnswer(forwarded) {
|
|
|
82
81
|
export async function openBridge(house, copies, conversationId, signal) {
|
|
83
82
|
const path = [join(kitHome(), 'shim'), dirname(process.execPath), await loginPath()].join(delimiter);
|
|
84
83
|
const origin = (await readEnrolment()).house;
|
|
85
|
-
const { credential } = await house.post(`/kit/conversations/${conversationId}/credential`, {}, signal);
|
|
84
|
+
const { credential } = await retrying(() => house.post(`/kit/conversations/${conversationId}/credential`, {}, signal), signal);
|
|
86
85
|
const closed = new AbortController();
|
|
87
86
|
const sent = (message, operation) => callHouse(origin, credential, message, operation, closed.signal);
|
|
88
87
|
const send = async (message, operation) => {
|
|
@@ -97,7 +96,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
97
96
|
const after = resendAfter(forwarded, attempt);
|
|
98
97
|
if (after === null)
|
|
99
98
|
return forwarded;
|
|
100
|
-
// A
|
|
99
|
+
// A Tool call House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
|
|
101
100
|
await delay(after, undefined, { signal: closed.signal });
|
|
102
101
|
}
|
|
103
102
|
};
|
|
@@ -135,23 +134,9 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
135
134
|
const held = JSON.parse(text).transfer;
|
|
136
135
|
return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
|
|
137
136
|
};
|
|
138
|
-
|
|
139
|
-
const writes = async (tool) => {
|
|
140
|
-
mutations ??= house
|
|
141
|
-
.post('/kit/tools/mutations', {}, closed.signal)
|
|
142
|
-
.then(({ tools }) => new Set(tools.filter((listed) => listed.writes).map((listed) => listed.name)));
|
|
143
|
-
try {
|
|
144
|
-
return (await mutations).has(tool);
|
|
145
|
-
}
|
|
146
|
-
catch (error) {
|
|
147
|
-
mutations = null;
|
|
148
|
-
throw error;
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
const forward = async (text) => {
|
|
137
|
+
const forward = (text) => {
|
|
152
138
|
const message = JSON.parse(text);
|
|
153
|
-
|
|
154
|
-
return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
|
|
139
|
+
return send(message, message.method === 'tools/call' ? operationId() : null);
|
|
155
140
|
};
|
|
156
141
|
const caller = {
|
|
157
142
|
key: `conversation:${conversationId}`,
|
|
@@ -173,15 +158,15 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
173
158
|
const { cwd, commit } = JSON.parse(text);
|
|
174
159
|
return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
|
|
175
160
|
}
|
|
161
|
+
if (path === '/git/fetch')
|
|
162
|
+
return { status: 200, text: JSON.stringify(await copies.fetch()) };
|
|
176
163
|
if (path === '/git/observed') {
|
|
177
164
|
await copies.observed(JSON.parse(text));
|
|
178
165
|
return { status: 200, text: '{}' };
|
|
179
166
|
}
|
|
180
167
|
return path === '/' ? forward(text) : { status: 404, text: '' };
|
|
181
168
|
};
|
|
182
|
-
const
|
|
183
|
-
await mkdir(sockets, { recursive: true, mode: 0o700 });
|
|
184
|
-
const socketPath = join(sockets, `${randomUUID()}.sock`);
|
|
169
|
+
const socketPath = join(socketDirectory(), `${randomUUID()}.sock`);
|
|
185
170
|
const bridge = createServer(async (request, response) => {
|
|
186
171
|
try {
|
|
187
172
|
const path = request.url ?? '';
|
|
@@ -197,7 +182,8 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
197
182
|
response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
|
|
198
183
|
return;
|
|
199
184
|
}
|
|
200
|
-
|
|
185
|
+
const verb = request.url === '/git/push' || request.url === '/git/fetch';
|
|
186
|
+
response.writeHead(502).end(verb && error instanceof Error ? error.message : UNREACHABLE);
|
|
201
187
|
}
|
|
202
188
|
});
|
|
203
189
|
bridge.listen(socketPath);
|
package/dist/chatgpt.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { placeAppGit } from './app-git.js';
|
|
3
|
+
import { kitHome, readOwnAgent, writeOwnAgent } from './home.js';
|
|
4
|
+
import { accepted, DEFAULT_HOUSE } from './login.js';
|
|
5
|
+
const KEY_SOURCE = 'New key for a script on Connections';
|
|
6
|
+
const USAGE = `usage: kit chatgpt --key <key> [--house <origin>]
|
|
7
|
+
--key <key> a key from ${KEY_SOURCE}
|
|
8
|
+
--house <origin> the House of the key, ${DEFAULT_HOUSE} unless one is stored
|
|
9
|
+
`;
|
|
10
|
+
export async function chatgpt(argv) {
|
|
11
|
+
const { values } = parseArgs({
|
|
12
|
+
args: argv,
|
|
13
|
+
options: { key: { type: 'string' }, house: { type: 'string' }, help: { type: 'boolean', default: false } },
|
|
14
|
+
});
|
|
15
|
+
if (values.help) {
|
|
16
|
+
process.stdout.write(USAGE);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (values.key === undefined)
|
|
20
|
+
throw new Error(`pass --key with a key from ${KEY_SOURCE}`);
|
|
21
|
+
const before = await readOwnAgent();
|
|
22
|
+
const house = values.house ?? before?.house ?? DEFAULT_HOUSE;
|
|
23
|
+
if (!(await accepted(house, values.key)))
|
|
24
|
+
throw new Error(`${house} does not accept this key; pass one from ${KEY_SOURCE}`);
|
|
25
|
+
const relay = process.env.HOUSE_KIT_RELAY || undefined;
|
|
26
|
+
await writeOwnAgent({ house, ...(relay === undefined ? {} : { relay }), credential: values.key });
|
|
27
|
+
await placeAppGit(await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
|
|
28
|
+
if (before !== null && before.credential !== values.key) {
|
|
29
|
+
process.stdout.write('This replaced the key this Kit home held; give each session its own HOUSE_KIT_HOME to keep its own key.\n');
|
|
30
|
+
}
|
|
31
|
+
process.stdout.write(`house now calls ${house} as you, with the key kept in ${kitHome()}.\n`);
|
|
32
|
+
}
|
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;
|
|
@@ -132,6 +171,9 @@ export class Conversations {
|
|
|
132
171
|
return false;
|
|
133
172
|
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
134
173
|
}
|
|
174
|
+
quiet() {
|
|
175
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
|
|
176
|
+
}
|
|
135
177
|
look() {
|
|
136
178
|
for (const conversation of this.conversations.values()) {
|
|
137
179
|
for (const scope of conversation.leftovers) {
|
|
@@ -196,10 +238,10 @@ export class Conversations {
|
|
|
196
238
|
if (conversation.running !== null)
|
|
197
239
|
return {};
|
|
198
240
|
try {
|
|
199
|
-
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) };
|
|
200
242
|
}
|
|
201
243
|
catch (error) {
|
|
202
|
-
return { refused:
|
|
244
|
+
return { refused: refusal(conversation, error) };
|
|
203
245
|
}
|
|
204
246
|
}
|
|
205
247
|
async message(conversation, input) {
|
|
@@ -216,7 +258,7 @@ export class Conversations {
|
|
|
216
258
|
throw new Error(KILLED);
|
|
217
259
|
}
|
|
218
260
|
catch (error) {
|
|
219
|
-
return { ack: Promise.resolve({ refused: conversation
|
|
261
|
+
return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
220
262
|
}
|
|
221
263
|
return {
|
|
222
264
|
ack: new Promise((settle) => {
|
|
@@ -239,13 +281,17 @@ export class Conversations {
|
|
|
239
281
|
async send(conversation, waiting) {
|
|
240
282
|
const { input } = waiting;
|
|
241
283
|
let ack = {};
|
|
284
|
+
let started = false;
|
|
242
285
|
const prompt = [];
|
|
243
286
|
try {
|
|
244
287
|
if (conversation.running === null) {
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
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 };
|
|
249
295
|
}
|
|
250
296
|
if (input.first === true) {
|
|
251
297
|
prompt.push({
|
|
@@ -257,11 +303,11 @@ export class Conversations {
|
|
|
257
303
|
throw new Error(KILLED);
|
|
258
304
|
}
|
|
259
305
|
catch (error) {
|
|
260
|
-
if (
|
|
306
|
+
if (started) {
|
|
261
307
|
conversation.session = null;
|
|
262
308
|
await this.kill(conversation);
|
|
263
309
|
}
|
|
264
|
-
waiting.settle({ refused: conversation
|
|
310
|
+
waiting.settle({ refused: refusal(conversation, error) });
|
|
265
311
|
return;
|
|
266
312
|
}
|
|
267
313
|
this.prompt(conversation, [...prompt, ...waiting.content]);
|
|
@@ -339,14 +385,15 @@ export class Conversations {
|
|
|
339
385
|
}
|
|
340
386
|
return route;
|
|
341
387
|
}
|
|
342
|
-
async start(conversation, route, sessionId) {
|
|
388
|
+
async start(conversation, route, sessionId, empty) {
|
|
343
389
|
const reader = readerFor(route.kind);
|
|
344
390
|
reader.session = sessionId;
|
|
345
391
|
const app = client({ name: '@agentshouse/kit' })
|
|
346
392
|
.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))
|
|
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));
|
|
348
395
|
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);
|
|
396
|
+
const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
|
|
350
397
|
if (conversation.kills > 0) {
|
|
351
398
|
bridge.close();
|
|
352
399
|
throw new Error(KILLED);
|
|
@@ -354,27 +401,28 @@ export class Conversations {
|
|
|
354
401
|
conversation.opening = bridge;
|
|
355
402
|
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
356
403
|
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
404
|
+
let adapter = null;
|
|
357
405
|
try {
|
|
358
406
|
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
359
407
|
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
360
|
-
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) => {
|
|
361
410
|
work.read(message);
|
|
362
411
|
this.read(conversation, reader, message);
|
|
363
|
-
}, { ...bridge.env, ...
|
|
412
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
364
413
|
void adapter.exited.then(() => bridge.close());
|
|
365
414
|
const agent = adapter.connection.agent;
|
|
366
415
|
const cwd = route.working_directory;
|
|
367
416
|
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 };
|
|
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 })));
|
|
371
419
|
reader.session = opened.sessionId;
|
|
372
|
-
if (sessionId
|
|
420
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
373
421
|
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
374
|
-
await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
|
|
422
|
+
await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
375
423
|
}
|
|
376
424
|
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
377
|
-
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
425
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
378
426
|
void work.settled().then(() => scope.record());
|
|
379
427
|
conversation.running = running;
|
|
380
428
|
conversation.session = running.sessionId;
|
|
@@ -382,11 +430,11 @@ export class Conversations {
|
|
|
382
430
|
if (conversation.commands !== null)
|
|
383
431
|
this.commands(conversation, conversation.commands);
|
|
384
432
|
this.options(conversation, options);
|
|
385
|
-
void adapter.
|
|
433
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
386
434
|
return running.sessionId;
|
|
387
435
|
}
|
|
388
436
|
catch (error) {
|
|
389
|
-
|
|
437
|
+
adapter?.retire();
|
|
390
438
|
work.close();
|
|
391
439
|
conversation.leftovers.add(scope);
|
|
392
440
|
bridge.close();
|
|
@@ -396,6 +444,18 @@ export class Conversations {
|
|
|
396
444
|
conversation.opening = null;
|
|
397
445
|
}
|
|
398
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
|
+
}
|
|
399
459
|
async launchSettings(running, route, offered) {
|
|
400
460
|
let options = offered;
|
|
401
461
|
const settings = [
|
|
@@ -417,13 +477,10 @@ export class Conversations {
|
|
|
417
477
|
}
|
|
418
478
|
return options;
|
|
419
479
|
}
|
|
420
|
-
exited(conversation,
|
|
421
|
-
const running = conversation.running;
|
|
422
|
-
if (running?.adapter !== adapter)
|
|
423
|
-
return;
|
|
480
|
+
exited(conversation, running, cause) {
|
|
424
481
|
this.detach(conversation, running);
|
|
425
482
|
if (conversation.turn !== null) {
|
|
426
|
-
this.end(conversation, conversation.turn,
|
|
483
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
427
484
|
}
|
|
428
485
|
this.next(conversation);
|
|
429
486
|
this.kit.changed();
|
|
@@ -487,21 +544,25 @@ export class Conversations {
|
|
|
487
544
|
this.kit.changed();
|
|
488
545
|
return turn;
|
|
489
546
|
}
|
|
490
|
-
end(conversation, turn, failed) {
|
|
547
|
+
end(conversation, turn, failed, notice = null) {
|
|
491
548
|
if (turn.ended)
|
|
492
549
|
return;
|
|
550
|
+
if (failed !== null)
|
|
551
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
493
552
|
if (conversation.turn === turn) {
|
|
494
553
|
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
495
554
|
this.apply(conversation, turn, event);
|
|
496
555
|
}
|
|
556
|
+
if (notice !== null)
|
|
557
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
497
558
|
turn.ended = true;
|
|
498
559
|
conversation.open--;
|
|
499
560
|
if (conversation.turn === turn)
|
|
500
561
|
conversation.turn = null;
|
|
501
562
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
502
|
-
parts: turn.parts.
|
|
563
|
+
parts: turn.parts.saved(),
|
|
503
564
|
context: turn.parts.context,
|
|
504
|
-
...failed,
|
|
565
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
505
566
|
}));
|
|
506
567
|
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
507
568
|
this.next(conversation);
|
|
@@ -511,16 +572,25 @@ export class Conversations {
|
|
|
511
572
|
const running = conversation.running;
|
|
512
573
|
const turn = this.begin(conversation);
|
|
513
574
|
turn.prompted = true;
|
|
514
|
-
|
|
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) => {
|
|
515
585
|
if (running.adapter.connection.signal.aborted) {
|
|
516
|
-
this.end(conversation, turn,
|
|
586
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
517
587
|
}
|
|
518
588
|
else {
|
|
519
589
|
if (conversation.open === 1 && conversation.running === running) {
|
|
520
|
-
running.
|
|
590
|
+
running.adapter.retire();
|
|
521
591
|
this.detach(conversation, running);
|
|
522
592
|
}
|
|
523
|
-
this.end(conversation, turn,
|
|
593
|
+
this.end(conversation, turn, answeredWith(error));
|
|
524
594
|
}
|
|
525
595
|
});
|
|
526
596
|
}
|
|
@@ -644,7 +714,7 @@ export class Conversations {
|
|
|
644
714
|
turn.parts.plan(event.entries);
|
|
645
715
|
return;
|
|
646
716
|
case 'image':
|
|
647
|
-
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));
|
|
648
718
|
this.kit.changed();
|
|
649
719
|
return;
|
|
650
720
|
}
|
|
@@ -664,7 +734,7 @@ export class Conversations {
|
|
|
664
734
|
else
|
|
665
735
|
conversation.turn.parts.start(part);
|
|
666
736
|
}
|
|
667
|
-
async image(conversation, turn, data, mediaType) {
|
|
737
|
+
async image(conversation, turn, reserved, data, mediaType) {
|
|
668
738
|
const bytes = Buffer.from(data, 'base64');
|
|
669
739
|
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
670
740
|
try {
|
|
@@ -677,10 +747,10 @@ export class Conversations {
|
|
|
677
747
|
const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
|
|
678
748
|
if (saved.save.status !== 'saved')
|
|
679
749
|
throw new Error(saved.save.failure ?? saved.save.status);
|
|
680
|
-
turn.parts.
|
|
750
|
+
turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
|
|
681
751
|
}
|
|
682
752
|
catch (error) {
|
|
683
|
-
turn.parts.
|
|
753
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
|
|
684
754
|
}
|
|
685
755
|
}
|
|
686
756
|
}
|