@agentshouse/kit 0.2.0 → 0.2.1-alpha.10
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 +20 -2
- package/dist/acp.js +120 -0
- package/dist/agents.js +96 -0
- package/dist/api.js +39 -3
- package/dist/authority-main.js +22 -0
- package/dist/blocks.js +51 -0
- package/dist/bootstrap.json +28 -0
- package/dist/bridge.js +214 -0
- package/dist/clis.js +114 -0
- package/dist/configure-main.js +6 -0
- package/dist/conversations.js +561 -0
- package/dist/copies.js +796 -0
- package/dist/enrol-main.js +4 -0
- package/dist/files.js +21 -0
- package/dist/git-main.js +153 -0
- package/dist/git.js +81 -0
- package/dist/home.js +19 -6
- package/dist/house-main.js +106 -3
- package/dist/instructions.js +33 -0
- package/dist/kit-main.js +2 -0
- package/dist/login.js +6 -2
- package/dist/operation.js +9 -0
- package/dist/resident.js +113 -2
- package/dist/secret-input.js +16 -0
- package/dist/sign-in.js +87 -0
- package/dist/skills.js +83 -0
- package/dist/stream.js +17 -1
- package/dist/translate.js +51 -0
- package/dist/update.js +116 -0
- package/how-we-work.md +25 -0
- package/package.json +29 -3
- package/skills/ask-matt/LICENSE +21 -0
- package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
- package/skills/ask-matt/SKILL.md +91 -0
- package/skills/ask-matt/agents/openai.yaml +4 -0
- package/skills/code-review/LICENSE +21 -0
- package/skills/code-review/SKILL.md +89 -0
- package/skills/code-review/agents/openai.yaml +3 -0
- package/skills/codebase-design/DEEPENING.md +37 -0
- package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/skills/codebase-design/LICENSE +21 -0
- package/skills/codebase-design/SKILL.md +116 -0
- package/skills/codebase-design/agents/openai.yaml +3 -0
- package/skills/diagnosing-bugs/LICENSE +21 -0
- package/skills/diagnosing-bugs/SKILL.md +140 -0
- package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/skills/domain-modeling/LICENSE +21 -0
- package/skills/domain-modeling/SKILL.md +76 -0
- package/skills/domain-modeling/agents/openai.yaml +3 -0
- package/skills/grill-me/LICENSE +21 -0
- package/skills/grill-me/SKILL.md +8 -0
- package/skills/grill-me/agents/openai.yaml +4 -0
- package/skills/grill-with-docs/LICENSE +21 -0
- package/skills/grill-with-docs/SKILL.md +8 -0
- package/skills/grill-with-docs/agents/openai.yaml +4 -0
- package/skills/grilling/LICENSE +21 -0
- package/skills/grilling/SKILL.md +30 -0
- package/skills/grilling/agents/openai.yaml +3 -0
- package/skills/handoff/LICENSE +21 -0
- package/skills/handoff/SKILL.md +17 -0
- package/skills/handoff/agents/openai.yaml +4 -0
- package/skills/implement/LICENSE +21 -0
- package/skills/implement/SKILL.md +16 -0
- package/skills/implement/agents/openai.yaml +4 -0
- package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/skills/improve-codebase-architecture/LICENSE +21 -0
- package/skills/improve-codebase-architecture/SKILL.md +72 -0
- package/skills/improve-codebase-architecture/agents/openai.yaml +4 -0
- package/skills/prototype/LICENSE +21 -0
- package/skills/prototype/LOGIC.md +67 -0
- package/skills/prototype/SKILL.md +28 -0
- package/skills/prototype/UI.md +112 -0
- package/skills/prototype/agents/openai.yaml +3 -0
- package/skills/research/LICENSE +21 -0
- package/skills/research/SKILL.md +14 -0
- package/skills/research/agents/openai.yaml +3 -0
- package/skills/resolving-merge-conflicts/LICENSE +21 -0
- package/skills/resolving-merge-conflicts/SKILL.md +16 -0
- package/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/skills/tdd/LICENSE +21 -0
- package/skills/tdd/SKILL.md +40 -0
- package/skills/tdd/agents/openai.yaml +3 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/tests.md +77 -0
- package/skills/teach/GLOSSARY-FORMAT.md +35 -0
- package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/skills/teach/LICENSE +21 -0
- package/skills/teach/MISSION-FORMAT.md +31 -0
- package/skills/teach/RESOURCES-FORMAT.md +32 -0
- package/skills/teach/SKILL.md +141 -0
- package/skills/teach/agents/openai.yaml +4 -0
- package/skills/to-questionnaire/LICENSE +21 -0
- package/skills/to-questionnaire/SKILL.md +55 -0
- package/skills/to-questionnaire/agents/openai.yaml +4 -0
- package/skills/to-spec/LICENSE +21 -0
- package/skills/to-spec/SKILL.md +76 -0
- package/skills/to-spec/agents/openai.yaml +4 -0
- package/skills/to-tickets/LICENSE +21 -0
- package/skills/to-tickets/SKILL.md +106 -0
- package/skills/to-tickets/agents/openai.yaml +4 -0
- package/skills/wait-what/LICENSE +21 -0
- package/skills/wait-what/SKILL.md +8 -0
- package/skills/wait-what/agents/openai.yaml +4 -0
- package/skills/wayfinder/LICENSE +21 -0
- package/skills/wayfinder/SKILL.md +129 -0
- package/skills/wayfinder/agents/openai.yaml +4 -0
- package/skills/writing-for-agents/LICENSE +21 -0
- package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/skills/writing-for-agents/SKILL.md +83 -0
- package/skills/writing-for-agents/agents/openai.yaml +3 -0
package/dist/clis.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { kitHome } from './home.js';
|
|
6
|
+
const RUNNING_JOB = new Set(['running', 'paused']);
|
|
7
|
+
function threadStatus({ method, params }) {
|
|
8
|
+
if (method !== 'session/update' || params?.update?.sessionUpdate !== 'session_info_update')
|
|
9
|
+
return undefined;
|
|
10
|
+
return params.update._meta?.codex?.threadStatus?.type;
|
|
11
|
+
}
|
|
12
|
+
function asyncTask({ method, params }) {
|
|
13
|
+
const update = params?.update;
|
|
14
|
+
if (method !== 'session/update' || update?.asyncTaskId === undefined)
|
|
15
|
+
return null;
|
|
16
|
+
if (update.sessionUpdate === 'async_task_spawned')
|
|
17
|
+
return { id: update.asyncTaskId, running: true };
|
|
18
|
+
if (update.sessionUpdate === 'async_task_state_update' && !RUNNING_JOB.has(update.state ?? '')) {
|
|
19
|
+
return { id: update.asyncTaskId, running: false };
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export const CLIS = {
|
|
24
|
+
'codex-acp': {
|
|
25
|
+
package: '@agentclientprotocol/codex-acp',
|
|
26
|
+
bin: 'codex-acp',
|
|
27
|
+
args: [],
|
|
28
|
+
signedIn: { command: ['cli', 'login', 'status'] },
|
|
29
|
+
login: { args: ['cli', 'login', '--device-auth'], code: 'show' },
|
|
30
|
+
queues: false,
|
|
31
|
+
turnStarted: (notice) => threadStatus(notice) === 'active',
|
|
32
|
+
turnEnded: (notice) => threadStatus(notice) === 'idle',
|
|
33
|
+
job: asyncTask,
|
|
34
|
+
},
|
|
35
|
+
'claude-agent-acp': {
|
|
36
|
+
package: '@agentclientprotocol/claude-agent-acp',
|
|
37
|
+
bin: 'claude-agent-acp',
|
|
38
|
+
args: [],
|
|
39
|
+
signedIn: { command: ['--cli', 'auth', 'status'] },
|
|
40
|
+
login: { args: ['--cli', 'auth', 'login', '--claudeai'], code: 'collect' },
|
|
41
|
+
queues: false,
|
|
42
|
+
turnStarted: () => false,
|
|
43
|
+
turnEnded: ({ method, params }) => method === 'session/update' &&
|
|
44
|
+
params?.update?.sessionUpdate === 'usage_update' &&
|
|
45
|
+
params.update._meta?.['_claude/origin'] !== undefined,
|
|
46
|
+
job: asyncTask,
|
|
47
|
+
},
|
|
48
|
+
'grok-build': {
|
|
49
|
+
package: '@xai-official/grok',
|
|
50
|
+
bin: 'grok',
|
|
51
|
+
args: ['agent', '--no-leader', 'stdio'],
|
|
52
|
+
signedIn: { initializeMeta: 'defaultAuthMethodId' },
|
|
53
|
+
login: { args: ['login', '--device-auth'], code: 'show' },
|
|
54
|
+
queues: true,
|
|
55
|
+
turnStarted: ({ method, params }) => method === '_x.ai/queue/changed' && params?.runningPromptId !== undefined,
|
|
56
|
+
turnEnded: ({ method, params }) => method === '_x.ai/session_notification' && params?.update?.sessionUpdate === 'turn_completed',
|
|
57
|
+
job: ({ method, params }) => {
|
|
58
|
+
if (method === '_x.ai/task_backgrounded')
|
|
59
|
+
return { id: params.update.task_id, running: true };
|
|
60
|
+
if (method === '_x.ai/task_completed')
|
|
61
|
+
return { id: params.update.task_snapshot.task_id, running: false };
|
|
62
|
+
return null;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
67
|
+
export const KIT_VERSION = KIT_PACKAGE.version;
|
|
68
|
+
const PROXY = /^https?_proxy$/i;
|
|
69
|
+
export function pinned(kind) {
|
|
70
|
+
return KIT_PACKAGE.peerDependencies[CLIS[kind].package];
|
|
71
|
+
}
|
|
72
|
+
function prefix(kind) {
|
|
73
|
+
return join(kitHome(), 'agents', kind);
|
|
74
|
+
}
|
|
75
|
+
export function cliCommand(kind) {
|
|
76
|
+
return join(prefix(kind), 'node_modules', '.bin', CLIS[kind].bin);
|
|
77
|
+
}
|
|
78
|
+
export async function installedRelease(kind) {
|
|
79
|
+
try {
|
|
80
|
+
const manifest = join(prefix(kind), 'node_modules', CLIS[kind].package, 'package.json');
|
|
81
|
+
return JSON.parse(await readFile(manifest, 'utf8')).version;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function withoutProxy() {
|
|
88
|
+
return Object.fromEntries(Object.entries(process.env).filter(([name]) => !PROXY.test(name)));
|
|
89
|
+
}
|
|
90
|
+
export function run(command, args, timeoutMs) {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], timeout: timeoutMs, env: withoutProxy() });
|
|
93
|
+
let output = '';
|
|
94
|
+
child.stdout.on('data', (chunk) => {
|
|
95
|
+
output += chunk.toString('utf8');
|
|
96
|
+
});
|
|
97
|
+
child.stderr.on('data', (chunk) => {
|
|
98
|
+
output += chunk.toString('utf8');
|
|
99
|
+
});
|
|
100
|
+
child.on('error', (error) => resolve({ status: null, output: error.message }));
|
|
101
|
+
child.on('close', (status) => resolve({ status, output }));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export async function install(kind) {
|
|
105
|
+
const release = pinned(kind);
|
|
106
|
+
if ((await installedRelease(kind)) === release)
|
|
107
|
+
return release;
|
|
108
|
+
const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', `${CLIS[kind].package}@${release}`], 15 * 60_000);
|
|
109
|
+
if (ran.status !== 0) {
|
|
110
|
+
process.stderr.write(`kit: ${kind} did not install: ${ran.output.trim()}\n`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return installedRelease(kind);
|
|
114
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { readConfiguration, writeConfiguration } from './home.js';
|
|
2
|
+
const skills = !process.argv.slice(2).includes('--no-skills');
|
|
3
|
+
const stored = await readConfiguration();
|
|
4
|
+
await writeConfiguration({ skills });
|
|
5
|
+
if (stored.skills !== skills)
|
|
6
|
+
process.stdout.write('changed\n');
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { client, } from '@agentclientprotocol/sdk';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { killMarked, startAdapter, TURN_ENDED, TURN_STARTED } from './acp.js';
|
|
6
|
+
import { blocksOf, firstChange } from './blocks.js';
|
|
7
|
+
import { openBridge } from './bridge.js';
|
|
8
|
+
import { CLIS } from './clis.js';
|
|
9
|
+
import { placeFiles } from './files.js';
|
|
10
|
+
import { instructions } from './instructions.js';
|
|
11
|
+
import { holdSecretInput } from './secret-input.js';
|
|
12
|
+
import { createHowWeWork } from './skills.js';
|
|
13
|
+
const LAUNCH_BASE = '/agents/house';
|
|
14
|
+
const KILLED = 'the conversation was killed';
|
|
15
|
+
function causeOf(error) {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
function answerMessage(request, response) {
|
|
19
|
+
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
20
|
+
}
|
|
21
|
+
function logged(error) {
|
|
22
|
+
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
23
|
+
}
|
|
24
|
+
function unlessKilled(work, signal) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const killed = () => reject(signal.reason);
|
|
27
|
+
signal.addEventListener('abort', killed, { once: true });
|
|
28
|
+
if (signal.aborted)
|
|
29
|
+
killed();
|
|
30
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function stop(bridge) {
|
|
34
|
+
killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
|
|
35
|
+
}
|
|
36
|
+
class Turn {
|
|
37
|
+
id = randomUUID();
|
|
38
|
+
text = '';
|
|
39
|
+
sent = [];
|
|
40
|
+
unsentFrom = null;
|
|
41
|
+
draftSequence = 0;
|
|
42
|
+
planSequence = 0;
|
|
43
|
+
entries = null;
|
|
44
|
+
questions = 0;
|
|
45
|
+
secrets = 0;
|
|
46
|
+
prompt = null;
|
|
47
|
+
ended = false;
|
|
48
|
+
}
|
|
49
|
+
class Conversation {
|
|
50
|
+
id;
|
|
51
|
+
running = null;
|
|
52
|
+
turn = null;
|
|
53
|
+
commands = null;
|
|
54
|
+
jobs = new Set();
|
|
55
|
+
queued = [];
|
|
56
|
+
late = [];
|
|
57
|
+
opening = null;
|
|
58
|
+
kills = 0;
|
|
59
|
+
killed = new AbortController();
|
|
60
|
+
open = 0;
|
|
61
|
+
queue = Promise.resolve();
|
|
62
|
+
reports = Promise.resolve();
|
|
63
|
+
constructor(id) {
|
|
64
|
+
this.id = id;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function formProperties(request) {
|
|
68
|
+
if (request.mode !== 'form')
|
|
69
|
+
return {};
|
|
70
|
+
return request.requestedSchema?.properties ?? {};
|
|
71
|
+
}
|
|
72
|
+
function asksSecret(request) {
|
|
73
|
+
return Object.values(formProperties(request)).some((property) => Object.values(property._meta ?? {}).some((meta) => typeof meta === 'object' && meta !== null && meta.isSecret === true));
|
|
74
|
+
}
|
|
75
|
+
export class Conversations {
|
|
76
|
+
kit;
|
|
77
|
+
conversations = new Map();
|
|
78
|
+
acks = new Map();
|
|
79
|
+
questions = new Map();
|
|
80
|
+
carrying = 0;
|
|
81
|
+
reporting = 0;
|
|
82
|
+
constructor(kit) {
|
|
83
|
+
this.kit = kit;
|
|
84
|
+
}
|
|
85
|
+
input(input) {
|
|
86
|
+
this.carrying++;
|
|
87
|
+
const carried = this.acks.get(input.input_id) ??
|
|
88
|
+
(() => {
|
|
89
|
+
const conversation = this.conversation(input.conversation_id);
|
|
90
|
+
if (input.kind === 'kill') {
|
|
91
|
+
conversation.kills++;
|
|
92
|
+
conversation.killed.abort(new Error(KILLED));
|
|
93
|
+
this.halt(conversation);
|
|
94
|
+
}
|
|
95
|
+
const work = conversation.queue.then(() => this.carry(conversation, input));
|
|
96
|
+
conversation.queue = work.catch(() => undefined);
|
|
97
|
+
this.acks.set(input.input_id, work);
|
|
98
|
+
return work;
|
|
99
|
+
})();
|
|
100
|
+
carried
|
|
101
|
+
.then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
|
|
102
|
+
.catch(logged)
|
|
103
|
+
.finally(() => {
|
|
104
|
+
this.carrying--;
|
|
105
|
+
this.kit.changed();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
idle() {
|
|
109
|
+
if (this.carrying > 0 || this.reporting > 0)
|
|
110
|
+
return false;
|
|
111
|
+
for (const conversation of this.conversations.values()) {
|
|
112
|
+
const turn = conversation.turn;
|
|
113
|
+
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
114
|
+
if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
opened() {
|
|
120
|
+
for (const conversation of this.conversations.values()) {
|
|
121
|
+
if (conversation.turn !== null)
|
|
122
|
+
conversation.turn.unsentFrom = 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
conversation(id) {
|
|
126
|
+
let conversation = this.conversations.get(id);
|
|
127
|
+
if (conversation === undefined) {
|
|
128
|
+
conversation = new Conversation(id);
|
|
129
|
+
this.conversations.set(id, conversation);
|
|
130
|
+
}
|
|
131
|
+
return conversation;
|
|
132
|
+
}
|
|
133
|
+
async carry(conversation, input) {
|
|
134
|
+
if (input.kind === 'open')
|
|
135
|
+
return this.open(conversation, input);
|
|
136
|
+
if (input.kind === 'message')
|
|
137
|
+
return this.message(conversation, input);
|
|
138
|
+
if (input.kind === 'interrupt')
|
|
139
|
+
await this.interrupt(conversation, String(input.turn_id));
|
|
140
|
+
if (input.kind === 'kill') {
|
|
141
|
+
await this.kill(conversation);
|
|
142
|
+
if (--conversation.kills === 0)
|
|
143
|
+
conversation.killed = new AbortController();
|
|
144
|
+
}
|
|
145
|
+
if (input.kind === 'option')
|
|
146
|
+
await this.option(conversation, String(input.option), input.value);
|
|
147
|
+
if (input.kind === 'answer') {
|
|
148
|
+
const question = this.questions.get(String(input.interaction_id));
|
|
149
|
+
if (question === undefined) {
|
|
150
|
+
return this.message(conversation, { ...input, text: answerMessage(input.request, input.response), files: [], first: false });
|
|
151
|
+
}
|
|
152
|
+
question.answer(input.response);
|
|
153
|
+
}
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
async open(conversation, input) {
|
|
157
|
+
if (conversation.running !== null)
|
|
158
|
+
return {};
|
|
159
|
+
try {
|
|
160
|
+
return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return { refused: causeOf(error) };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async message(conversation, input) {
|
|
167
|
+
let ack = {};
|
|
168
|
+
const prompt = [];
|
|
169
|
+
try {
|
|
170
|
+
const route = await this.route(conversation, input.agent_id);
|
|
171
|
+
const paths = await placeFiles(this.kit.house, route.working_directory, input.files, conversation.killed.signal);
|
|
172
|
+
if (conversation.running === null) {
|
|
173
|
+
const opened = await this.start(conversation, route, input.provider_session_id);
|
|
174
|
+
if (input.provider_session_id === null)
|
|
175
|
+
ack = { provider_session_id: opened };
|
|
176
|
+
}
|
|
177
|
+
if (input.first === true) {
|
|
178
|
+
prompt.push({
|
|
179
|
+
type: 'text',
|
|
180
|
+
text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
prompt.push({ type: 'text', text: String(input.text) });
|
|
184
|
+
if (paths.length > 0)
|
|
185
|
+
prompt.push({ type: 'text', text: paths.join('\n') });
|
|
186
|
+
if (conversation.kills > 0)
|
|
187
|
+
throw new Error(KILLED);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if ('provider_session_id' in ack)
|
|
191
|
+
await this.kill(conversation);
|
|
192
|
+
return { refused: conversation.kills > 0 ? KILLED : causeOf(error) };
|
|
193
|
+
}
|
|
194
|
+
this.prompt(conversation, prompt);
|
|
195
|
+
return ack;
|
|
196
|
+
}
|
|
197
|
+
async interrupt(conversation, turnId) {
|
|
198
|
+
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
199
|
+
return;
|
|
200
|
+
await conversation.running.adapter.connection.agent.notify('session/cancel', {
|
|
201
|
+
sessionId: conversation.running.sessionId,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
halt(conversation) {
|
|
205
|
+
if (conversation.opening !== null)
|
|
206
|
+
stop(conversation.opening);
|
|
207
|
+
if (conversation.running === null)
|
|
208
|
+
return;
|
|
209
|
+
conversation.running.killed = true;
|
|
210
|
+
stop(conversation.running.bridge);
|
|
211
|
+
}
|
|
212
|
+
async kill(conversation) {
|
|
213
|
+
const running = conversation.running;
|
|
214
|
+
this.halt(conversation);
|
|
215
|
+
await running?.adapter.exited;
|
|
216
|
+
}
|
|
217
|
+
async option(conversation, option, value) {
|
|
218
|
+
const running = conversation.running;
|
|
219
|
+
if (running === null)
|
|
220
|
+
return;
|
|
221
|
+
try {
|
|
222
|
+
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
223
|
+
sessionId: running.sessionId,
|
|
224
|
+
configId: option,
|
|
225
|
+
...(typeof value === 'boolean' ? { type: 'boolean', value } : { value: String(value) }),
|
|
226
|
+
});
|
|
227
|
+
this.options(conversation, answered.configOptions);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
logged(error);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async route(conversation, agentId) {
|
|
234
|
+
const signal = conversation.killed.signal;
|
|
235
|
+
await unlessKilled(this.kit.agents.read(), signal);
|
|
236
|
+
if (this.kit.agents.route(agentId) === undefined)
|
|
237
|
+
await unlessKilled(this.kit.agents.refresh(), signal);
|
|
238
|
+
const route = this.kit.agents.route(agentId);
|
|
239
|
+
if (route === undefined)
|
|
240
|
+
throw new Error(`this Environment hosts no Agent ${agentId}`);
|
|
241
|
+
if (route.working_directory === join(LAUNCH_BASE, route.agent_id)) {
|
|
242
|
+
await mkdir(route.working_directory, { recursive: true });
|
|
243
|
+
}
|
|
244
|
+
return route;
|
|
245
|
+
}
|
|
246
|
+
async start(conversation, route, sessionId) {
|
|
247
|
+
const app = client({ name: '@agentshouse/kit' })
|
|
248
|
+
.onNotification('session/update', ({ params }) => this.update(conversation, params))
|
|
249
|
+
.onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
|
|
250
|
+
.onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
|
|
251
|
+
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
252
|
+
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
|
|
253
|
+
await unlessKilled(this.kit.agents.installed(), conversation.killed.signal);
|
|
254
|
+
const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
255
|
+
if (conversation.kills > 0) {
|
|
256
|
+
bridge.close();
|
|
257
|
+
throw new Error(KILLED);
|
|
258
|
+
}
|
|
259
|
+
conversation.opening = bridge;
|
|
260
|
+
try {
|
|
261
|
+
await unlessKilled(createHowWeWork(bridge), conversation.killed.signal);
|
|
262
|
+
const adapter = await startAdapter(route.kind, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
|
|
263
|
+
void adapter.exited.then(() => bridge.close());
|
|
264
|
+
const agent = adapter.connection.agent;
|
|
265
|
+
const cwd = route.working_directory;
|
|
266
|
+
const opened = sessionId === null
|
|
267
|
+
? await agent.request('session/new', { cwd, mcpServers: [] })
|
|
268
|
+
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [] })), sessionId };
|
|
269
|
+
const running = {
|
|
270
|
+
adapter,
|
|
271
|
+
bridge,
|
|
272
|
+
sessionId: opened.sessionId,
|
|
273
|
+
killed: false,
|
|
274
|
+
queues: CLIS[route.kind].queues,
|
|
275
|
+
};
|
|
276
|
+
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
277
|
+
conversation.running = running;
|
|
278
|
+
this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
|
|
279
|
+
if (conversation.commands !== null)
|
|
280
|
+
this.commands(conversation, conversation.commands);
|
|
281
|
+
this.options(conversation, options);
|
|
282
|
+
void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
|
|
283
|
+
return running.sessionId;
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
stop(bridge);
|
|
287
|
+
bridge.close();
|
|
288
|
+
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
conversation.opening = null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async launchSettings(running, route, offered) {
|
|
295
|
+
let options = offered;
|
|
296
|
+
const settings = [
|
|
297
|
+
['model', route.model],
|
|
298
|
+
['thought_level', route.effort],
|
|
299
|
+
];
|
|
300
|
+
for (const [category, value] of settings) {
|
|
301
|
+
if (value === null)
|
|
302
|
+
continue;
|
|
303
|
+
const option = options.find((candidate) => candidate.category === category);
|
|
304
|
+
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
305
|
+
sessionId: running.sessionId,
|
|
306
|
+
configId: option?.id ?? category,
|
|
307
|
+
value,
|
|
308
|
+
});
|
|
309
|
+
options = answered.configOptions;
|
|
310
|
+
}
|
|
311
|
+
return options;
|
|
312
|
+
}
|
|
313
|
+
exited(conversation, adapter, cause) {
|
|
314
|
+
const running = conversation.running;
|
|
315
|
+
if (running?.adapter !== adapter)
|
|
316
|
+
return;
|
|
317
|
+
conversation.running = null;
|
|
318
|
+
conversation.commands = null;
|
|
319
|
+
conversation.jobs.clear();
|
|
320
|
+
conversation.queued.length = 0;
|
|
321
|
+
for (const [id, question] of this.questions) {
|
|
322
|
+
if (question.conversation === conversation)
|
|
323
|
+
this.questions.delete(id);
|
|
324
|
+
}
|
|
325
|
+
this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
|
|
326
|
+
if (conversation.turn !== null) {
|
|
327
|
+
this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
|
|
328
|
+
}
|
|
329
|
+
this.kit.changed();
|
|
330
|
+
}
|
|
331
|
+
commands(conversation, commands) {
|
|
332
|
+
conversation.commands = commands;
|
|
333
|
+
if (conversation.running === null)
|
|
334
|
+
return;
|
|
335
|
+
this.kit.send({ type: 'commands', conversation_id: conversation.id, commands });
|
|
336
|
+
}
|
|
337
|
+
options(conversation, options) {
|
|
338
|
+
if (conversation.running === null)
|
|
339
|
+
return;
|
|
340
|
+
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
341
|
+
}
|
|
342
|
+
deliver(conversation, turn, event, body) {
|
|
343
|
+
this.reporting++;
|
|
344
|
+
const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
|
|
345
|
+
conversation.reports = delivered
|
|
346
|
+
.catch(() => undefined)
|
|
347
|
+
.finally(() => {
|
|
348
|
+
this.reporting--;
|
|
349
|
+
this.kit.changed();
|
|
350
|
+
});
|
|
351
|
+
return delivered;
|
|
352
|
+
}
|
|
353
|
+
report(conversation) {
|
|
354
|
+
const turn = new Turn();
|
|
355
|
+
conversation.open++;
|
|
356
|
+
void this.deliver(conversation, turn, 'started', {})
|
|
357
|
+
.then(() => this.admitted(conversation, turn))
|
|
358
|
+
.catch(logged);
|
|
359
|
+
return turn;
|
|
360
|
+
}
|
|
361
|
+
admitted(conversation, turn) {
|
|
362
|
+
if (turn.text !== '') {
|
|
363
|
+
turn.unsentFrom = 0;
|
|
364
|
+
this.draft(conversation, turn);
|
|
365
|
+
}
|
|
366
|
+
if (turn.entries !== null)
|
|
367
|
+
this.plan(conversation, turn);
|
|
368
|
+
}
|
|
369
|
+
begin(conversation) {
|
|
370
|
+
const turn = this.report(conversation);
|
|
371
|
+
conversation.turn = turn;
|
|
372
|
+
this.kit.changed();
|
|
373
|
+
return turn;
|
|
374
|
+
}
|
|
375
|
+
end(conversation, turn, outcome) {
|
|
376
|
+
if (turn.ended)
|
|
377
|
+
return;
|
|
378
|
+
turn.ended = true;
|
|
379
|
+
conversation.open--;
|
|
380
|
+
if (conversation.turn === turn)
|
|
381
|
+
conversation.turn = null;
|
|
382
|
+
void this.deliver(conversation, turn, 'ended', outcome).catch(logged);
|
|
383
|
+
const late = conversation.open === 0 ? conversation.late.shift() : undefined;
|
|
384
|
+
if (late !== undefined)
|
|
385
|
+
this.failed(conversation, late);
|
|
386
|
+
this.kit.changed();
|
|
387
|
+
}
|
|
388
|
+
failed(conversation, cause) {
|
|
389
|
+
if (conversation.open > 0)
|
|
390
|
+
conversation.late.push(cause);
|
|
391
|
+
else
|
|
392
|
+
this.end(conversation, this.report(conversation), { failed: cause });
|
|
393
|
+
}
|
|
394
|
+
prompt(conversation, prompt) {
|
|
395
|
+
const running = conversation.running;
|
|
396
|
+
const sent = { turn: null };
|
|
397
|
+
if (running.queues && (conversation.turn !== null || conversation.queued.length > 0)) {
|
|
398
|
+
conversation.queued.push(sent);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
this.attach(conversation.turn ?? this.begin(conversation), sent);
|
|
402
|
+
}
|
|
403
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
|
|
404
|
+
if (!running.adapter.connection.signal.aborted)
|
|
405
|
+
this.answered(conversation, sent, causeOf(error));
|
|
406
|
+
else if (sent.turn !== null) {
|
|
407
|
+
this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
attach(turn, sent) {
|
|
412
|
+
turn.prompt = sent;
|
|
413
|
+
sent.turn = turn;
|
|
414
|
+
}
|
|
415
|
+
answered(conversation, sent, failed) {
|
|
416
|
+
const turn = sent.turn;
|
|
417
|
+
if (turn === null) {
|
|
418
|
+
const queued = conversation.queued.indexOf(sent);
|
|
419
|
+
if (queued < 0)
|
|
420
|
+
return;
|
|
421
|
+
conversation.queued.splice(queued, 1);
|
|
422
|
+
if (failed !== null)
|
|
423
|
+
this.failed(conversation, failed);
|
|
424
|
+
}
|
|
425
|
+
else if (failed !== null) {
|
|
426
|
+
if (turn.ended)
|
|
427
|
+
this.failed(conversation, failed);
|
|
428
|
+
else
|
|
429
|
+
this.end(conversation, turn, { failed });
|
|
430
|
+
}
|
|
431
|
+
else if (turn.prompt === sent) {
|
|
432
|
+
this.end(conversation, turn, { text: turn.text });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
started(conversation) {
|
|
436
|
+
if (conversation.turn !== null)
|
|
437
|
+
return conversation.turn;
|
|
438
|
+
const turn = this.begin(conversation);
|
|
439
|
+
const queued = conversation.queued.shift();
|
|
440
|
+
if (queued !== undefined)
|
|
441
|
+
this.attach(turn, queued);
|
|
442
|
+
return turn;
|
|
443
|
+
}
|
|
444
|
+
ended(conversation) {
|
|
445
|
+
const turn = conversation.turn;
|
|
446
|
+
if (turn === null)
|
|
447
|
+
return;
|
|
448
|
+
if (turn.prompt === null) {
|
|
449
|
+
this.end(conversation, turn, { text: turn.text });
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
conversation.turn = null;
|
|
453
|
+
this.kit.changed();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async ask(conversation, method, params, secret, signal) {
|
|
457
|
+
const turn = this.started(conversation);
|
|
458
|
+
const interactionId = randomUUID();
|
|
459
|
+
const answered = new Promise((answer, fail) => {
|
|
460
|
+
this.questions.set(interactionId, { conversation, answer: (response) => answer(response) });
|
|
461
|
+
signal.addEventListener('abort', () => answer(null), { once: true });
|
|
462
|
+
this.deliver(conversation, turn, 'interactions', {
|
|
463
|
+
interaction_id: interactionId,
|
|
464
|
+
request: { method, params },
|
|
465
|
+
secret,
|
|
466
|
+
}).then(() => {
|
|
467
|
+
if (secret)
|
|
468
|
+
void this.holdSecret(interactionId, params);
|
|
469
|
+
}, fail);
|
|
470
|
+
});
|
|
471
|
+
turn.questions++;
|
|
472
|
+
if (secret)
|
|
473
|
+
turn.secrets++;
|
|
474
|
+
this.kit.changed();
|
|
475
|
+
try {
|
|
476
|
+
const response = await answered;
|
|
477
|
+
if (response === null)
|
|
478
|
+
throw signal.reason;
|
|
479
|
+
return response;
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
this.questions.delete(interactionId);
|
|
483
|
+
turn.questions--;
|
|
484
|
+
if (secret)
|
|
485
|
+
turn.secrets--;
|
|
486
|
+
this.kit.changed();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async holdSecret(interactionId, request) {
|
|
490
|
+
const steps = Object.entries(formProperties(request)).map(([name, property]) => ({
|
|
491
|
+
kind: 'collect',
|
|
492
|
+
label: property.title ?? name,
|
|
493
|
+
name,
|
|
494
|
+
...(property.description ? { description: property.description } : {}),
|
|
495
|
+
}));
|
|
496
|
+
const content = await holdSecretInput(this.kit.house, interactionId, steps, () => this.questions.has(interactionId));
|
|
497
|
+
if (content !== null)
|
|
498
|
+
this.questions.get(interactionId)?.answer({ action: 'accept', content });
|
|
499
|
+
}
|
|
500
|
+
update(conversation, notification) {
|
|
501
|
+
const update = notification.update;
|
|
502
|
+
if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
|
|
503
|
+
const turn = this.started(conversation);
|
|
504
|
+
turn.text += update.content.text;
|
|
505
|
+
this.draft(conversation, turn);
|
|
506
|
+
}
|
|
507
|
+
else if (update.sessionUpdate === 'plan') {
|
|
508
|
+
const turn = this.started(conversation);
|
|
509
|
+
turn.entries = update.entries;
|
|
510
|
+
this.plan(conversation, turn);
|
|
511
|
+
}
|
|
512
|
+
else if (update.sessionUpdate === 'agent_thought_chunk' || update.sessionUpdate === 'tool_call') {
|
|
513
|
+
this.started(conversation);
|
|
514
|
+
}
|
|
515
|
+
else if (update.sessionUpdate === 'available_commands_update') {
|
|
516
|
+
this.commands(conversation, update.availableCommands);
|
|
517
|
+
}
|
|
518
|
+
else if (update.sessionUpdate === 'config_option_update') {
|
|
519
|
+
this.options(conversation, update.configOptions);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
job(conversation, job) {
|
|
523
|
+
if (job.running)
|
|
524
|
+
conversation.jobs.add(job.id);
|
|
525
|
+
else
|
|
526
|
+
conversation.jobs.delete(job.id);
|
|
527
|
+
this.kit.changed();
|
|
528
|
+
}
|
|
529
|
+
draft(conversation, turn) {
|
|
530
|
+
const blocks = blocksOf(turn.text);
|
|
531
|
+
const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
|
|
532
|
+
const sent = this.kit.send({
|
|
533
|
+
type: 'draft',
|
|
534
|
+
conversation_id: conversation.id,
|
|
535
|
+
turn_id: turn.id,
|
|
536
|
+
sequence: turn.draftSequence + 1,
|
|
537
|
+
from,
|
|
538
|
+
blocks: blocks.slice(from),
|
|
539
|
+
});
|
|
540
|
+
if (sent) {
|
|
541
|
+
turn.draftSequence++;
|
|
542
|
+
turn.sent = blocks;
|
|
543
|
+
turn.unsentFrom = null;
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
turn.unsentFrom = from;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
plan(conversation, turn) {
|
|
550
|
+
const sent = this.kit.send({
|
|
551
|
+
type: 'plan',
|
|
552
|
+
conversation_id: conversation.id,
|
|
553
|
+
turn_id: turn.id,
|
|
554
|
+
sequence: turn.planSequence + 1,
|
|
555
|
+
from: 0,
|
|
556
|
+
steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
|
|
557
|
+
});
|
|
558
|
+
if (sent)
|
|
559
|
+
turn.planSequence++;
|
|
560
|
+
}
|
|
561
|
+
}
|