@agentshouse/kit 0.1.0-alpha.9 → 0.2.1-alpha.1
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 +14 -1740
- package/dist/acp.js +120 -0
- package/dist/agents.js +90 -0
- package/dist/api.js +45 -0
- package/dist/blocks.js +51 -0
- package/dist/bridge.js +124 -0
- package/dist/clis.js +110 -0
- package/dist/conversations.js +538 -0
- package/dist/files.js +21 -0
- package/dist/home.js +24 -0
- package/dist/house-main.js +92 -0
- package/dist/instructions.js +28 -0
- package/dist/kit-main.js +16 -0
- package/dist/login.js +112 -0
- package/dist/resident.js +66 -0
- package/dist/secret-input.js +16 -0
- package/dist/sign-in.js +83 -0
- package/dist/stream.js +47 -0
- package/package.json +34 -10
- package/bin/connect-linux.sh +0 -134
- package/bin/kit.js +0 -1023
- package/bootstrap.json +0 -17
- package/src/acp.js +0 -99
- package/src/adapter-mcp.js +0 -44
- package/src/adapter.js +0 -252
- package/src/agent-logins.js +0 -65
- package/src/agents.js +0 -504
- package/src/apply.js +0 -29
- package/src/attach.js +0 -52
- package/src/binding.js +0 -90
- package/src/blocks.js +0 -156
- package/src/bridge.js +0 -449
- package/src/bytes.js +0 -89
- package/src/channel.js +0 -37
- package/src/command.js +0 -109
- package/src/configuration.js +0 -465
- package/src/connector-login.js +0 -91
- package/src/connector.js +0 -107
- package/src/continuation.js +0 -61
- package/src/contract.js +0 -363
- package/src/conversation-originals.js +0 -143
- package/src/custom/authorization.js +0 -63
- package/src/declaration.js +0 -35
- package/src/delivery.js +0 -191
- package/src/directory.js +0 -102
- package/src/draft.js +0 -89
- package/src/google/calendar.js +0 -177
- package/src/google/declared.js +0 -146
- package/src/google/drive.js +0 -435
- package/src/google/gateway.js +0 -166
- package/src/google/gmail.js +0 -249
- package/src/google/id.js +0 -1
- package/src/google/oauth.js +0 -273
- package/src/google/source.js +0 -25
- package/src/hook.js +0 -376
- package/src/host.js +0 -121
- package/src/house-main.js +0 -79
- package/src/house.js +0 -1090
- package/src/inbound.js +0 -175
- package/src/instance/google.js +0 -1
- package/src/kit-work-stream.js +0 -84
- package/src/launcher-main.js +0 -19
- package/src/launcher.js +0 -276
- package/src/local-files.js +0 -113
- package/src/login.js +0 -274
- package/src/mcp.js +0 -71
- package/src/migrator.js +0 -248
- package/src/mount.js +0 -582
- package/src/operation.js +0 -20
- package/src/original.js +0 -631
- package/src/peer.js +0 -124
- package/src/plan.js +0 -57
- package/src/question.js +0 -316
- package/src/refusal.js +0 -79
- package/src/repository.js +0 -521
- package/src/session-bin/house +0 -2
- package/src/settlement.js +0 -57
- package/src/setup.js +0 -190
- package/src/source-originals.js +0 -116
- package/src/source.js +0 -286
- package/src/starter-set/grilling/LICENSE +0 -21
- package/src/starter-set/grilling/SKILL.md +0 -30
- package/src/starter-set/grilling/agents/openai.yaml +0 -3
- package/src/starter-set.js +0 -17
- package/src/state.js +0 -157
- package/src/sync-library.js +0 -293
- package/src/sync-state.js +0 -120
- package/src/sync.js +0 -482
- package/src/telegram.js +0 -1045
- package/src/terminal.js +0 -57
- package/src/tmux.js +0 -119
- package/src/worker.js +0 -925
- package/src/workspace.js +0 -500
|
@@ -0,0 +1,538 @@
|
|
|
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
|
+
const LAUNCH_BASE = '/agents/house';
|
|
13
|
+
const KILLED = 'the conversation was killed';
|
|
14
|
+
function causeOf(error) {
|
|
15
|
+
return error instanceof Error ? error.message : String(error);
|
|
16
|
+
}
|
|
17
|
+
function logged(error) {
|
|
18
|
+
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
19
|
+
}
|
|
20
|
+
function unlessKilled(work, signal) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const killed = () => reject(signal.reason);
|
|
23
|
+
signal.addEventListener('abort', killed, { once: true });
|
|
24
|
+
if (signal.aborted)
|
|
25
|
+
killed();
|
|
26
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function stop(bridge) {
|
|
30
|
+
killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
|
|
31
|
+
}
|
|
32
|
+
class Turn {
|
|
33
|
+
id = randomUUID();
|
|
34
|
+
text = '';
|
|
35
|
+
sent = [];
|
|
36
|
+
unsentFrom = null;
|
|
37
|
+
draftSequence = 0;
|
|
38
|
+
planSequence = 0;
|
|
39
|
+
questions = 0;
|
|
40
|
+
secrets = 0;
|
|
41
|
+
prompt = null;
|
|
42
|
+
ended = false;
|
|
43
|
+
ready = Promise.resolve();
|
|
44
|
+
}
|
|
45
|
+
class Conversation {
|
|
46
|
+
id;
|
|
47
|
+
running = null;
|
|
48
|
+
turn = null;
|
|
49
|
+
commands = null;
|
|
50
|
+
jobs = new Set();
|
|
51
|
+
queued = [];
|
|
52
|
+
late = [];
|
|
53
|
+
opening = null;
|
|
54
|
+
kills = 0;
|
|
55
|
+
killed = new AbortController();
|
|
56
|
+
open = 0;
|
|
57
|
+
queue = Promise.resolve();
|
|
58
|
+
reports = Promise.resolve();
|
|
59
|
+
constructor(id) {
|
|
60
|
+
this.id = id;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function formProperties(request) {
|
|
64
|
+
if (request.mode !== 'form')
|
|
65
|
+
return {};
|
|
66
|
+
return request.requestedSchema?.properties ?? {};
|
|
67
|
+
}
|
|
68
|
+
function asksSecret(request) {
|
|
69
|
+
return Object.values(formProperties(request)).some((property) => Object.values(property._meta ?? {}).some((meta) => typeof meta === 'object' && meta !== null && meta.isSecret === true));
|
|
70
|
+
}
|
|
71
|
+
export class Conversations {
|
|
72
|
+
kit;
|
|
73
|
+
conversations = new Map();
|
|
74
|
+
acks = new Map();
|
|
75
|
+
questions = new Map();
|
|
76
|
+
carrying = 0;
|
|
77
|
+
reporting = 0;
|
|
78
|
+
constructor(kit) {
|
|
79
|
+
this.kit = kit;
|
|
80
|
+
}
|
|
81
|
+
input(input) {
|
|
82
|
+
this.carrying++;
|
|
83
|
+
const carried = this.acks.get(input.input_id) ??
|
|
84
|
+
(() => {
|
|
85
|
+
const conversation = this.conversation(input.conversation_id);
|
|
86
|
+
if (input.kind === 'kill') {
|
|
87
|
+
conversation.kills++;
|
|
88
|
+
conversation.killed.abort(new Error(KILLED));
|
|
89
|
+
this.halt(conversation);
|
|
90
|
+
}
|
|
91
|
+
const work = conversation.queue.then(() => this.carry(conversation, input));
|
|
92
|
+
conversation.queue = work.catch(() => undefined);
|
|
93
|
+
this.acks.set(input.input_id, work);
|
|
94
|
+
return work;
|
|
95
|
+
})();
|
|
96
|
+
carried
|
|
97
|
+
.then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
|
|
98
|
+
.catch(logged)
|
|
99
|
+
.finally(() => {
|
|
100
|
+
this.carrying--;
|
|
101
|
+
this.kit.changed();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
idle() {
|
|
105
|
+
if (this.carrying > 0 || this.reporting > 0)
|
|
106
|
+
return false;
|
|
107
|
+
for (const conversation of this.conversations.values()) {
|
|
108
|
+
const turn = conversation.turn;
|
|
109
|
+
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
110
|
+
if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
opened() {
|
|
116
|
+
for (const conversation of this.conversations.values()) {
|
|
117
|
+
if (conversation.turn !== null)
|
|
118
|
+
conversation.turn.unsentFrom = 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
conversation(id) {
|
|
122
|
+
let conversation = this.conversations.get(id);
|
|
123
|
+
if (conversation === undefined) {
|
|
124
|
+
conversation = new Conversation(id);
|
|
125
|
+
this.conversations.set(id, conversation);
|
|
126
|
+
}
|
|
127
|
+
return conversation;
|
|
128
|
+
}
|
|
129
|
+
async carry(conversation, input) {
|
|
130
|
+
if (input.kind === 'open')
|
|
131
|
+
return this.open(conversation, input);
|
|
132
|
+
if (input.kind === 'message')
|
|
133
|
+
return this.message(conversation, input);
|
|
134
|
+
if (input.kind === 'interrupt')
|
|
135
|
+
await this.interrupt(conversation, String(input.turn_id));
|
|
136
|
+
if (input.kind === 'kill') {
|
|
137
|
+
await this.kill(conversation);
|
|
138
|
+
if (--conversation.kills === 0)
|
|
139
|
+
conversation.killed = new AbortController();
|
|
140
|
+
}
|
|
141
|
+
if (input.kind === 'option')
|
|
142
|
+
await this.option(conversation, String(input.option), input.value);
|
|
143
|
+
if (input.kind === 'answer')
|
|
144
|
+
this.questions.get(String(input.interaction_id))?.answer(input.response);
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
async open(conversation, input) {
|
|
148
|
+
if (conversation.running !== null)
|
|
149
|
+
return {};
|
|
150
|
+
try {
|
|
151
|
+
return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
return { refused: causeOf(error) };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async message(conversation, input) {
|
|
158
|
+
let ack = {};
|
|
159
|
+
const prompt = [];
|
|
160
|
+
try {
|
|
161
|
+
const route = await this.route(conversation, input.agent_id);
|
|
162
|
+
const paths = await placeFiles(this.kit.house, route.working_directory, input.files, conversation.killed.signal);
|
|
163
|
+
if (conversation.running === null) {
|
|
164
|
+
const opened = await this.start(conversation, route, input.provider_session_id);
|
|
165
|
+
if (input.provider_session_id === null)
|
|
166
|
+
ack = { provider_session_id: opened };
|
|
167
|
+
}
|
|
168
|
+
if (input.first === true) {
|
|
169
|
+
prompt.push({ type: 'text', text: await instructions(conversation.running.bridge, route.base_instructions) });
|
|
170
|
+
}
|
|
171
|
+
prompt.push({ type: 'text', text: String(input.text) });
|
|
172
|
+
if (paths.length > 0)
|
|
173
|
+
prompt.push({ type: 'text', text: paths.join('\n') });
|
|
174
|
+
if (conversation.kills > 0)
|
|
175
|
+
throw new Error(KILLED);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if ('provider_session_id' in ack)
|
|
179
|
+
await this.kill(conversation);
|
|
180
|
+
return { refused: conversation.kills > 0 ? KILLED : causeOf(error) };
|
|
181
|
+
}
|
|
182
|
+
this.prompt(conversation, prompt);
|
|
183
|
+
return ack;
|
|
184
|
+
}
|
|
185
|
+
async interrupt(conversation, turnId) {
|
|
186
|
+
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
187
|
+
return;
|
|
188
|
+
await conversation.running.adapter.connection.agent.notify('session/cancel', {
|
|
189
|
+
sessionId: conversation.running.sessionId,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
halt(conversation) {
|
|
193
|
+
if (conversation.opening !== null)
|
|
194
|
+
stop(conversation.opening);
|
|
195
|
+
if (conversation.running === null)
|
|
196
|
+
return;
|
|
197
|
+
conversation.running.killed = true;
|
|
198
|
+
stop(conversation.running.bridge);
|
|
199
|
+
}
|
|
200
|
+
async kill(conversation) {
|
|
201
|
+
const running = conversation.running;
|
|
202
|
+
this.halt(conversation);
|
|
203
|
+
await running?.adapter.exited;
|
|
204
|
+
}
|
|
205
|
+
async option(conversation, option, value) {
|
|
206
|
+
const running = conversation.running;
|
|
207
|
+
if (running === null)
|
|
208
|
+
return;
|
|
209
|
+
try {
|
|
210
|
+
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
211
|
+
sessionId: running.sessionId,
|
|
212
|
+
configId: option,
|
|
213
|
+
...(typeof value === 'boolean' ? { type: 'boolean', value } : { value: String(value) }),
|
|
214
|
+
});
|
|
215
|
+
this.options(conversation, answered.configOptions);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
logged(error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async route(conversation, agentId) {
|
|
222
|
+
const signal = conversation.killed.signal;
|
|
223
|
+
await unlessKilled(this.kit.agents.read(), signal);
|
|
224
|
+
if (this.kit.agents.route(agentId) === undefined)
|
|
225
|
+
await unlessKilled(this.kit.agents.refresh(), signal);
|
|
226
|
+
const route = this.kit.agents.route(agentId);
|
|
227
|
+
if (route === undefined)
|
|
228
|
+
throw new Error(`this Environment hosts no Agent ${agentId}`);
|
|
229
|
+
if (route.working_directory === join(LAUNCH_BASE, route.agent_id)) {
|
|
230
|
+
await mkdir(route.working_directory, { recursive: true });
|
|
231
|
+
}
|
|
232
|
+
return route;
|
|
233
|
+
}
|
|
234
|
+
async start(conversation, route, sessionId) {
|
|
235
|
+
const app = client({ name: '@agentshouse/kit' })
|
|
236
|
+
.onNotification('session/update', ({ params }) => this.update(conversation, params))
|
|
237
|
+
.onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
|
|
238
|
+
.onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
|
|
239
|
+
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
240
|
+
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
|
|
241
|
+
await unlessKilled(this.kit.agents.installed(), conversation.killed.signal);
|
|
242
|
+
const bridge = await openBridge(this.kit.house, conversation.id, conversation.killed.signal);
|
|
243
|
+
if (conversation.kills > 0) {
|
|
244
|
+
bridge.close();
|
|
245
|
+
throw new Error(KILLED);
|
|
246
|
+
}
|
|
247
|
+
conversation.opening = bridge;
|
|
248
|
+
try {
|
|
249
|
+
const adapter = await startAdapter(route.kind, route.working_directory, app, (job) => this.job(conversation, job), { ...process.env, ...bridge.env });
|
|
250
|
+
void adapter.exited.then(() => bridge.close());
|
|
251
|
+
const agent = adapter.connection.agent;
|
|
252
|
+
const cwd = route.working_directory;
|
|
253
|
+
const opened = sessionId === null
|
|
254
|
+
? await agent.request('session/new', { cwd, mcpServers: [] })
|
|
255
|
+
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [] })), sessionId };
|
|
256
|
+
const running = {
|
|
257
|
+
adapter,
|
|
258
|
+
bridge,
|
|
259
|
+
sessionId: opened.sessionId,
|
|
260
|
+
killed: false,
|
|
261
|
+
queues: CLIS[route.kind].queues,
|
|
262
|
+
};
|
|
263
|
+
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
264
|
+
conversation.running = running;
|
|
265
|
+
this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
|
|
266
|
+
if (conversation.commands !== null)
|
|
267
|
+
this.commands(conversation, conversation.commands);
|
|
268
|
+
this.options(conversation, options);
|
|
269
|
+
void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
|
|
270
|
+
return running.sessionId;
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
stop(bridge);
|
|
274
|
+
bridge.close();
|
|
275
|
+
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
conversation.opening = null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async launchSettings(running, route, offered) {
|
|
282
|
+
let options = offered;
|
|
283
|
+
const settings = [
|
|
284
|
+
['model', route.model],
|
|
285
|
+
['thought_level', route.effort],
|
|
286
|
+
];
|
|
287
|
+
for (const [category, value] of settings) {
|
|
288
|
+
if (value === null)
|
|
289
|
+
continue;
|
|
290
|
+
const option = options.find((candidate) => candidate.category === category);
|
|
291
|
+
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
292
|
+
sessionId: running.sessionId,
|
|
293
|
+
configId: option?.id ?? category,
|
|
294
|
+
value,
|
|
295
|
+
});
|
|
296
|
+
options = answered.configOptions;
|
|
297
|
+
}
|
|
298
|
+
return options;
|
|
299
|
+
}
|
|
300
|
+
exited(conversation, adapter, cause) {
|
|
301
|
+
const running = conversation.running;
|
|
302
|
+
if (running?.adapter !== adapter)
|
|
303
|
+
return;
|
|
304
|
+
conversation.running = null;
|
|
305
|
+
conversation.commands = null;
|
|
306
|
+
conversation.jobs.clear();
|
|
307
|
+
conversation.queued.length = 0;
|
|
308
|
+
for (const [id, question] of this.questions) {
|
|
309
|
+
if (question.conversation === conversation)
|
|
310
|
+
this.questions.delete(id);
|
|
311
|
+
}
|
|
312
|
+
this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
|
|
313
|
+
if (conversation.turn !== null) {
|
|
314
|
+
this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
|
|
315
|
+
}
|
|
316
|
+
this.kit.changed();
|
|
317
|
+
}
|
|
318
|
+
commands(conversation, commands) {
|
|
319
|
+
conversation.commands = commands;
|
|
320
|
+
if (conversation.running === null)
|
|
321
|
+
return;
|
|
322
|
+
this.kit.send({ type: 'commands', conversation_id: conversation.id, commands });
|
|
323
|
+
}
|
|
324
|
+
options(conversation, options) {
|
|
325
|
+
if (conversation.running === null)
|
|
326
|
+
return;
|
|
327
|
+
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
328
|
+
}
|
|
329
|
+
deliver(conversation, turn, event, body) {
|
|
330
|
+
this.reporting++;
|
|
331
|
+
const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
|
|
332
|
+
conversation.reports = delivered
|
|
333
|
+
.catch(() => undefined)
|
|
334
|
+
.finally(() => {
|
|
335
|
+
this.reporting--;
|
|
336
|
+
this.kit.changed();
|
|
337
|
+
});
|
|
338
|
+
return delivered;
|
|
339
|
+
}
|
|
340
|
+
report(conversation) {
|
|
341
|
+
const turn = new Turn();
|
|
342
|
+
conversation.open++;
|
|
343
|
+
turn.ready = this.deliver(conversation, turn, 'started', {}).then(() => undefined, logged);
|
|
344
|
+
return turn;
|
|
345
|
+
}
|
|
346
|
+
begin(conversation) {
|
|
347
|
+
const turn = this.report(conversation);
|
|
348
|
+
conversation.turn = turn;
|
|
349
|
+
this.kit.changed();
|
|
350
|
+
return turn;
|
|
351
|
+
}
|
|
352
|
+
end(conversation, turn, outcome) {
|
|
353
|
+
if (turn.ended)
|
|
354
|
+
return;
|
|
355
|
+
turn.ended = true;
|
|
356
|
+
conversation.open--;
|
|
357
|
+
if (conversation.turn === turn)
|
|
358
|
+
conversation.turn = null;
|
|
359
|
+
void this.deliver(conversation, turn, 'ended', outcome).catch(logged);
|
|
360
|
+
const late = conversation.open === 0 ? conversation.late.shift() : undefined;
|
|
361
|
+
if (late !== undefined)
|
|
362
|
+
this.failed(conversation, late);
|
|
363
|
+
this.kit.changed();
|
|
364
|
+
}
|
|
365
|
+
failed(conversation, cause) {
|
|
366
|
+
if (conversation.open > 0)
|
|
367
|
+
conversation.late.push(cause);
|
|
368
|
+
else
|
|
369
|
+
this.end(conversation, this.report(conversation), { failed: cause });
|
|
370
|
+
}
|
|
371
|
+
prompt(conversation, prompt) {
|
|
372
|
+
const running = conversation.running;
|
|
373
|
+
const sent = { turn: null };
|
|
374
|
+
if (running.queues && (conversation.turn !== null || conversation.queued.length > 0)) {
|
|
375
|
+
conversation.queued.push(sent);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
this.attach(conversation.turn ?? this.begin(conversation), sent);
|
|
379
|
+
}
|
|
380
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
|
|
381
|
+
if (!running.adapter.connection.signal.aborted)
|
|
382
|
+
this.answered(conversation, sent, causeOf(error));
|
|
383
|
+
else if (sent.turn !== null) {
|
|
384
|
+
this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
attach(turn, sent) {
|
|
389
|
+
turn.prompt = sent;
|
|
390
|
+
sent.turn = turn;
|
|
391
|
+
}
|
|
392
|
+
answered(conversation, sent, failed) {
|
|
393
|
+
const turn = sent.turn;
|
|
394
|
+
if (turn === null) {
|
|
395
|
+
const queued = conversation.queued.indexOf(sent);
|
|
396
|
+
if (queued < 0)
|
|
397
|
+
return;
|
|
398
|
+
conversation.queued.splice(queued, 1);
|
|
399
|
+
if (failed !== null)
|
|
400
|
+
this.failed(conversation, failed);
|
|
401
|
+
}
|
|
402
|
+
else if (failed !== null) {
|
|
403
|
+
if (turn.ended)
|
|
404
|
+
this.failed(conversation, failed);
|
|
405
|
+
else
|
|
406
|
+
this.end(conversation, turn, { failed });
|
|
407
|
+
}
|
|
408
|
+
else if (turn.prompt === sent) {
|
|
409
|
+
this.end(conversation, turn, { text: turn.text });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
started(conversation) {
|
|
413
|
+
if (conversation.turn !== null)
|
|
414
|
+
return conversation.turn;
|
|
415
|
+
const turn = this.begin(conversation);
|
|
416
|
+
const queued = conversation.queued.shift();
|
|
417
|
+
if (queued !== undefined)
|
|
418
|
+
this.attach(turn, queued);
|
|
419
|
+
return turn;
|
|
420
|
+
}
|
|
421
|
+
ended(conversation) {
|
|
422
|
+
const turn = conversation.turn;
|
|
423
|
+
if (turn === null)
|
|
424
|
+
return;
|
|
425
|
+
if (turn.prompt === null) {
|
|
426
|
+
this.end(conversation, turn, { text: turn.text });
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
conversation.turn = null;
|
|
430
|
+
this.kit.changed();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async ask(conversation, method, params, secret, signal) {
|
|
434
|
+
const turn = this.started(conversation);
|
|
435
|
+
const interactionId = randomUUID();
|
|
436
|
+
const answered = new Promise((answer, fail) => {
|
|
437
|
+
this.questions.set(interactionId, { conversation, answer: (response) => answer(response) });
|
|
438
|
+
signal.addEventListener('abort', () => answer(null), { once: true });
|
|
439
|
+
this.deliver(conversation, turn, 'interactions', {
|
|
440
|
+
interaction_id: interactionId,
|
|
441
|
+
request: { method, params },
|
|
442
|
+
secret,
|
|
443
|
+
}).then(() => {
|
|
444
|
+
if (secret)
|
|
445
|
+
void this.holdSecret(interactionId, params);
|
|
446
|
+
}, fail);
|
|
447
|
+
});
|
|
448
|
+
turn.questions++;
|
|
449
|
+
if (secret)
|
|
450
|
+
turn.secrets++;
|
|
451
|
+
this.kit.changed();
|
|
452
|
+
try {
|
|
453
|
+
const response = await answered;
|
|
454
|
+
if (response === null)
|
|
455
|
+
throw signal.reason;
|
|
456
|
+
return response;
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
this.questions.delete(interactionId);
|
|
460
|
+
turn.questions--;
|
|
461
|
+
if (secret)
|
|
462
|
+
turn.secrets--;
|
|
463
|
+
this.kit.changed();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async holdSecret(interactionId, request) {
|
|
467
|
+
const steps = Object.entries(formProperties(request)).map(([name, property]) => ({
|
|
468
|
+
kind: 'collect',
|
|
469
|
+
label: property.title ?? name,
|
|
470
|
+
name,
|
|
471
|
+
...(property.description ? { description: property.description } : {}),
|
|
472
|
+
}));
|
|
473
|
+
const content = await holdSecretInput(this.kit.house, interactionId, steps, () => this.questions.has(interactionId));
|
|
474
|
+
if (content !== null)
|
|
475
|
+
this.questions.get(interactionId)?.answer({ action: 'accept', content });
|
|
476
|
+
}
|
|
477
|
+
update(conversation, notification) {
|
|
478
|
+
const update = notification.update;
|
|
479
|
+
if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
|
|
480
|
+
const turn = this.started(conversation);
|
|
481
|
+
turn.text += update.content.text;
|
|
482
|
+
const text = turn.text;
|
|
483
|
+
void turn.ready.then(() => this.draft(conversation, turn, text));
|
|
484
|
+
}
|
|
485
|
+
else if (update.sessionUpdate === 'plan') {
|
|
486
|
+
const turn = this.started(conversation);
|
|
487
|
+
void turn.ready.then(() => this.plan(conversation, turn, update.entries));
|
|
488
|
+
}
|
|
489
|
+
else if (update.sessionUpdate === 'agent_thought_chunk' || update.sessionUpdate === 'tool_call') {
|
|
490
|
+
this.started(conversation);
|
|
491
|
+
}
|
|
492
|
+
else if (update.sessionUpdate === 'available_commands_update') {
|
|
493
|
+
this.commands(conversation, update.availableCommands);
|
|
494
|
+
}
|
|
495
|
+
else if (update.sessionUpdate === 'config_option_update') {
|
|
496
|
+
this.options(conversation, update.configOptions);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
job(conversation, job) {
|
|
500
|
+
if (job.running)
|
|
501
|
+
conversation.jobs.add(job.id);
|
|
502
|
+
else
|
|
503
|
+
conversation.jobs.delete(job.id);
|
|
504
|
+
this.kit.changed();
|
|
505
|
+
}
|
|
506
|
+
draft(conversation, turn, text) {
|
|
507
|
+
const blocks = blocksOf(text);
|
|
508
|
+
const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
|
|
509
|
+
const sent = this.kit.send({
|
|
510
|
+
type: 'draft',
|
|
511
|
+
conversation_id: conversation.id,
|
|
512
|
+
turn_id: turn.id,
|
|
513
|
+
sequence: turn.draftSequence + 1,
|
|
514
|
+
from,
|
|
515
|
+
blocks: blocks.slice(from),
|
|
516
|
+
});
|
|
517
|
+
if (sent) {
|
|
518
|
+
turn.draftSequence++;
|
|
519
|
+
turn.sent = blocks;
|
|
520
|
+
turn.unsentFrom = null;
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
turn.unsentFrom = from;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
plan(conversation, turn, entries) {
|
|
527
|
+
const sent = this.kit.send({
|
|
528
|
+
type: 'plan',
|
|
529
|
+
conversation_id: conversation.id,
|
|
530
|
+
turn_id: turn.id,
|
|
531
|
+
sequence: turn.planSequence + 1,
|
|
532
|
+
from: 0,
|
|
533
|
+
steps: entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
|
|
534
|
+
});
|
|
535
|
+
if (sent)
|
|
536
|
+
turn.planSequence++;
|
|
537
|
+
}
|
|
538
|
+
}
|
package/dist/files.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export async function placeFiles(house, directory, files, signal) {
|
|
4
|
+
const paths = [];
|
|
5
|
+
for (const file of files) {
|
|
6
|
+
const original = await house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal);
|
|
7
|
+
const answer = await fetch(original.download.url, { signal });
|
|
8
|
+
if (!answer.ok)
|
|
9
|
+
throw new Error(`${file.name} did not download: ${answer.status}`);
|
|
10
|
+
const folder = join(directory, '.house', 'files', original.message, file.version);
|
|
11
|
+
await mkdir(join(directory, '.house')).catch((error) => {
|
|
12
|
+
if (error.code !== 'EEXIST')
|
|
13
|
+
throw error;
|
|
14
|
+
});
|
|
15
|
+
await mkdir(folder, { recursive: true });
|
|
16
|
+
const path = join(folder, file.name);
|
|
17
|
+
await writeFile(path, Buffer.from(await answer.arrayBuffer()));
|
|
18
|
+
paths.push(path);
|
|
19
|
+
}
|
|
20
|
+
return paths;
|
|
21
|
+
}
|
package/dist/home.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const ENROLMENT = 'credential.json';
|
|
5
|
+
export function kitHome() {
|
|
6
|
+
return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
|
|
7
|
+
}
|
|
8
|
+
export async function readEnrolment() {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(await readFile(join(kitHome(), ENROLMENT), 'utf8'));
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === 'ENOENT')
|
|
14
|
+
return null;
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function writeEnrolment(enrolment) {
|
|
19
|
+
const home = kitHome();
|
|
20
|
+
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
21
|
+
const temporary = join(home, `${ENROLMENT}.${process.pid}`);
|
|
22
|
+
await writeFile(temporary, `${JSON.stringify(enrolment)}\n`, { mode: 0o600 });
|
|
23
|
+
await rename(temporary, join(home, ENROLMENT));
|
|
24
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { request } from 'node:http';
|
|
5
|
+
import { basename } from 'node:path';
|
|
6
|
+
const USAGE = "usage: house <tool> ['<arguments as JSON>']";
|
|
7
|
+
const socketPath = process.env.HOUSE_BRIDGE;
|
|
8
|
+
function bridged(path, body) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const sent = request({ socketPath, path, method: 'POST', headers: { 'content-type': 'application/json' } }, (answer) => {
|
|
11
|
+
let text = '';
|
|
12
|
+
answer.setEncoding('utf8');
|
|
13
|
+
answer.on('data', (chunk) => {
|
|
14
|
+
text += chunk;
|
|
15
|
+
});
|
|
16
|
+
answer.on('end', () => {
|
|
17
|
+
if (answer.statusCode === 200)
|
|
18
|
+
resolve(text);
|
|
19
|
+
else
|
|
20
|
+
reject(new Error(`${path} answered ${answer.statusCode}: ${text}`));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
sent.on('error', reject);
|
|
24
|
+
sent.end(JSON.stringify(body));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function mcp(method, params) {
|
|
28
|
+
const answer = JSON.parse(await bridged('/', { jsonrpc: '2.0', id: 1, method, params }));
|
|
29
|
+
if (answer.error !== undefined)
|
|
30
|
+
throw new Error(answer.error.message);
|
|
31
|
+
return answer.result;
|
|
32
|
+
}
|
|
33
|
+
async function tools() {
|
|
34
|
+
return (await mcp('tools/list', {})).tools;
|
|
35
|
+
}
|
|
36
|
+
async function upload(path, roomRef) {
|
|
37
|
+
const bytes = await readFile(path);
|
|
38
|
+
const declared = JSON.parse(await bridged('/kit/attachments/upload', {
|
|
39
|
+
...(roomRef === undefined ? {} : { room_ref: roomRef }),
|
|
40
|
+
version: randomUUID(),
|
|
41
|
+
name: basename(path),
|
|
42
|
+
bytes: bytes.length,
|
|
43
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
44
|
+
}));
|
|
45
|
+
const answer = await fetch(declared.upload.url, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'content-type': 'application/octet-stream', 'x-house-byte-operation': declared.upload.operation },
|
|
48
|
+
body: bytes,
|
|
49
|
+
});
|
|
50
|
+
const text = await answer.text();
|
|
51
|
+
if (!answer.ok)
|
|
52
|
+
throw new Error(`House refused the bytes of ${path} with ${answer.status}: ${text}`);
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
async function house([verb, argument]) {
|
|
56
|
+
if (verb === undefined || verb === '--help') {
|
|
57
|
+
return [USAGE, ...(await tools()).map((tool) => `${tool.name}: ${tool.description}`)].join('\n');
|
|
58
|
+
}
|
|
59
|
+
if (argument === '--help') {
|
|
60
|
+
const tool = (await tools()).find((listed) => listed.name === verb);
|
|
61
|
+
if (tool === undefined)
|
|
62
|
+
throw new Error(`House has no tool ${verb}`);
|
|
63
|
+
return `${tool.description}\n${JSON.stringify(tool.inputSchema, null, 2)}`;
|
|
64
|
+
}
|
|
65
|
+
const args = (argument === undefined ? {} : JSON.parse(argument));
|
|
66
|
+
if (verb === 'upload_attachment')
|
|
67
|
+
return upload(String(args.path), args.room_ref);
|
|
68
|
+
if (verb === 'append_record' && Array.isArray(args.attachments)) {
|
|
69
|
+
const attachments = [];
|
|
70
|
+
for (const path of args.attachments) {
|
|
71
|
+
attachments.push(JSON.parse(await upload(String(path), args.room_ref)).attachment);
|
|
72
|
+
}
|
|
73
|
+
args.attachments = attachments;
|
|
74
|
+
}
|
|
75
|
+
const result = await mcp('tools/call', { name: verb, arguments: args });
|
|
76
|
+
const text = result.content.map((content) => content.text).join('\n');
|
|
77
|
+
if (result.isError === true)
|
|
78
|
+
throw new Error(text);
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
81
|
+
if (socketPath === undefined) {
|
|
82
|
+
process.stderr.write('house: house runs inside an Agent conversation\n');
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
house(process.argv.slice(2)).then((text) => {
|
|
87
|
+
process.stdout.write(`${text}\n`);
|
|
88
|
+
}, (error) => {
|
|
89
|
+
process.stderr.write(`house: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
});
|
|
92
|
+
}
|