@agentshouse/kit 0.1.0-alpha.16 → 0.1.0-alpha.18
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 +69 -17
- package/bin/connect-linux.sh +14 -4
- package/bootstrap.json +3 -3
- package/package.json +2 -1
- package/src/adapter-mcp.js +0 -13
- package/src/bridge.js +1 -1
- package/src/contract.js +10 -2
- package/src/house-main.js +65 -8
- package/src/house.js +98 -86
- package/src/inbound.js +2 -18
- package/src/launcher.js +2 -3
- package/src/local-files.js +3 -11
- package/src/mcp.js +13 -0
- package/src/sync-library.js +7 -11
- package/src/sync.js +1 -1
- package/src/worker.js +81 -15
- package/src/working-copy.js +516 -47
package/src/house.js
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
MCP_PROTOCOL_VERSION,
|
|
30
30
|
WORKER_RUN_TOKEN_MARGIN_MS,
|
|
31
31
|
runOutcomePath,
|
|
32
|
+
runRequestPath,
|
|
32
33
|
secretInputPath,
|
|
33
34
|
SECRET_VALUE_MAX,
|
|
34
35
|
sessionInteractionsPath,
|
|
@@ -171,7 +172,7 @@ async function residentRelay(session, request, signal, draining) {
|
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
export async function serveResident(session, directory, signal, drain) {
|
|
175
|
+
export async function serveResident(session, directory, signal, drain, push) {
|
|
175
176
|
const socket = residentSocket(directory);
|
|
176
177
|
await mkdir(dirname(socket), { recursive: true, mode: 0o700 });
|
|
177
178
|
await rm(socket, { force: true });
|
|
@@ -228,12 +229,15 @@ export async function serveResident(session, directory, signal, drain) {
|
|
|
228
229
|
}
|
|
229
230
|
const cancelled = new AbortController();
|
|
230
231
|
relayed.set(request.id, cancelled);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
)
|
|
232
|
+
const relaySignal = AbortSignal.any([signal, cancelled.signal]);
|
|
233
|
+
const answered =
|
|
234
|
+
request.push === undefined
|
|
235
|
+
? residentRelay(session, request, relaySignal, draining)
|
|
236
|
+
: push(request.push, relaySignal).then((pushed) => ({ pushed }));
|
|
237
|
+
void answered.then((answer) => {
|
|
238
|
+
relayed.delete(request.id);
|
|
239
|
+
if (!connection.destroyed) writeFrame(connection, { id: request.id, ...answer });
|
|
240
|
+
});
|
|
237
241
|
},
|
|
238
242
|
onRefusal: () => connection.destroy(),
|
|
239
243
|
});
|
|
@@ -301,45 +305,54 @@ async function residentDelegation(directory, fetcher) {
|
|
|
301
305
|
}
|
|
302
306
|
connection.unref();
|
|
303
307
|
let next = 0;
|
|
308
|
+
function exchange(frame, signal) {
|
|
309
|
+
signal?.throwIfAborted();
|
|
310
|
+
if (connection.destroyed) return Promise.resolve(unavailable());
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
const id = (next += 1);
|
|
313
|
+
const cancel = () => {
|
|
314
|
+
pending.delete(id);
|
|
315
|
+
settle(id);
|
|
316
|
+
if (!connection.destroyed) writeFrame(connection, { id, cancel: true });
|
|
317
|
+
reject(signal.reason);
|
|
318
|
+
};
|
|
319
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
320
|
+
pending.set(id, (answer) => {
|
|
321
|
+
signal?.removeEventListener("abort", cancel);
|
|
322
|
+
resolve(answer);
|
|
323
|
+
});
|
|
324
|
+
connection.ref();
|
|
325
|
+
writeFrame(connection, { id, ...frame });
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async function push(request, signal) {
|
|
329
|
+
const answer = await exchange({ push: request }, signal);
|
|
330
|
+
if (answer.pushed === undefined) throw new KitRefusal("kit_resident_unavailable");
|
|
331
|
+
return answer.pushed;
|
|
332
|
+
}
|
|
304
333
|
async function delegated(url, init = {}) {
|
|
305
334
|
const { authorization, ...headers } = init.headers ?? {};
|
|
306
335
|
if (authorization !== RESIDENT) return fetcher(url, init);
|
|
307
|
-
init.signal?.throwIfAborted();
|
|
308
336
|
const address = new URL(url);
|
|
309
|
-
const answer =
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
resolve(answer);
|
|
323
|
-
});
|
|
324
|
-
connection.ref();
|
|
325
|
-
writeFrame(connection, {
|
|
326
|
-
id,
|
|
327
|
-
method: init.method ?? "GET",
|
|
328
|
-
path: `${address.pathname}${address.search}`,
|
|
329
|
-
headers,
|
|
330
|
-
...(typeof init.body === "string"
|
|
331
|
-
? { text: init.body }
|
|
332
|
-
: init.body === undefined
|
|
333
|
-
? {}
|
|
334
|
-
: { bytes: Buffer.from(init.body).toString("base64") }),
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
+
const answer = await exchange(
|
|
338
|
+
{
|
|
339
|
+
method: init.method ?? "GET",
|
|
340
|
+
path: `${address.pathname}${address.search}`,
|
|
341
|
+
headers,
|
|
342
|
+
...(typeof init.body === "string"
|
|
343
|
+
? { text: init.body }
|
|
344
|
+
: init.body === undefined
|
|
345
|
+
? {}
|
|
346
|
+
: { bytes: Buffer.from(init.body).toString("base64") }),
|
|
347
|
+
},
|
|
348
|
+
init.signal,
|
|
349
|
+
);
|
|
337
350
|
return new Response(answer.body === null ? null : Buffer.from(answer.body, "base64"), {
|
|
338
351
|
status: answer.status,
|
|
339
352
|
headers: answer.type === null ? {} : { "content-type": answer.type },
|
|
340
353
|
});
|
|
341
354
|
}
|
|
342
|
-
return { connection, fetch: delegated };
|
|
355
|
+
return { connection, fetch: delegated, push };
|
|
343
356
|
}
|
|
344
357
|
|
|
345
358
|
export async function openSession(options, dependencies = {}) {
|
|
@@ -358,6 +371,7 @@ export async function openSession(options, dependencies = {}) {
|
|
|
358
371
|
: await residentDelegation(options.directory, dependencies.fetch ?? fetch);
|
|
359
372
|
if (resident !== null) {
|
|
360
373
|
session.resident = resident.connection;
|
|
374
|
+
session.push = resident.push;
|
|
361
375
|
session.dependencies = { ...dependencies, fetch: resident.fetch };
|
|
362
376
|
return session;
|
|
363
377
|
}
|
|
@@ -734,11 +748,7 @@ export async function reportLogin(session, login, outcome, signal = session.sign
|
|
|
734
748
|
}
|
|
735
749
|
|
|
736
750
|
const envelope = declarationReader("kit_acquire_rejected");
|
|
737
|
-
|
|
738
|
-
function routineInput(value, declaration) {
|
|
739
|
-
if (typeof value !== "string" || Array.from(value).length > 20_000) envelope.refuse(declaration);
|
|
740
|
-
return value;
|
|
741
|
-
}
|
|
751
|
+
const input = declarationReader("kit_run_input_unreadable");
|
|
742
752
|
|
|
743
753
|
function acquiredRoute(value, declaration) {
|
|
744
754
|
const route = envelope.plain(value, declaration);
|
|
@@ -756,79 +766,82 @@ function acquiredRoute(value, declaration) {
|
|
|
756
766
|
|
|
757
767
|
function conversationText(value, declaration) {
|
|
758
768
|
if (value === null) return null;
|
|
759
|
-
if (typeof value !== "string" || value.length > 4096)
|
|
769
|
+
if (typeof value !== "string" || value.length > 4096) input.refuse(declaration);
|
|
760
770
|
return value;
|
|
761
771
|
}
|
|
762
772
|
|
|
763
773
|
function conversationMessage(value, declaration) {
|
|
764
|
-
const message =
|
|
774
|
+
const message = input.plain(value, declaration);
|
|
765
775
|
const text = conversationText(message.text, `${declaration}.text`);
|
|
766
776
|
const caption = conversationText(message.caption, `${declaration}.caption`);
|
|
767
|
-
if (!Array.isArray(message.files))
|
|
777
|
+
if (!Array.isArray(message.files)) input.refuse(`${declaration}.files`);
|
|
768
778
|
const files = message.files.map((value, index) => {
|
|
769
779
|
const name = `${declaration}.files[${index}]`;
|
|
770
|
-
const file =
|
|
780
|
+
const file = input.plain(value, name);
|
|
771
781
|
if (file.bytes !== null && (!Number.isSafeInteger(file.bytes) || file.bytes < 0)) {
|
|
772
|
-
|
|
782
|
+
input.refuse(`${name}.bytes`);
|
|
773
783
|
}
|
|
774
784
|
return {
|
|
775
|
-
path:
|
|
776
|
-
media_kind:
|
|
777
|
-
media_type: file.media_type === null ? null :
|
|
785
|
+
path: input.text(file.path, `${name}.path`, 2048),
|
|
786
|
+
media_kind: input.text(file.media_kind, `${name}.media_kind`, 128),
|
|
787
|
+
media_type: file.media_type === null ? null : input.text(file.media_type, `${name}.media_type`, 255),
|
|
778
788
|
bytes: file.bytes,
|
|
779
789
|
};
|
|
780
790
|
});
|
|
781
|
-
if (!text && !caption && files.length === 0)
|
|
791
|
+
if (!text && !caption && files.length === 0) input.refuse(declaration);
|
|
782
792
|
return {
|
|
783
|
-
message_id:
|
|
793
|
+
message_id: input.text(message.message_id, `${declaration}.message_id`, 128),
|
|
784
794
|
text, caption, files,
|
|
785
795
|
};
|
|
786
796
|
}
|
|
787
797
|
|
|
788
|
-
function
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const turn =
|
|
798
|
+
function runRequest(answer) {
|
|
799
|
+
const request = input.plain(answer, "request");
|
|
800
|
+
if (request.kind === "conversation") {
|
|
801
|
+
if (request.origin !== "user") input.refuse("request.origin");
|
|
802
|
+
const turn = input.plain(request.turn, "request.turn");
|
|
793
803
|
return {
|
|
804
|
+
kind: "conversation",
|
|
794
805
|
origin: "user",
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
.map((message, index) => conversationMessage(message, `${declaration}.conversation.turn.messages[${index}]`)),
|
|
801
|
-
},
|
|
806
|
+
turn: {
|
|
807
|
+
id: input.text(turn.id, "request.turn.id", 128),
|
|
808
|
+
sequence: input.positiveCount(turn.sequence, "request.turn.sequence"),
|
|
809
|
+
messages: input.filled(turn.messages, "request.turn.messages")
|
|
810
|
+
.map((message, index) => conversationMessage(message, `request.turn.messages[${index}]`)),
|
|
802
811
|
},
|
|
803
812
|
};
|
|
804
813
|
}
|
|
805
|
-
if (
|
|
806
|
-
const
|
|
807
|
-
|
|
814
|
+
if (request.kind !== "routine") input.refuse("request.kind");
|
|
815
|
+
const routine = input.plain(request.routine, "request.routine");
|
|
816
|
+
if (typeof request.payload !== "string" || Array.from(request.payload).length > 20_000) input.refuse("request.payload");
|
|
817
|
+
if (!Array.isArray(request.references)) input.refuse("request.references");
|
|
808
818
|
return {
|
|
809
|
-
|
|
819
|
+
kind: "routine",
|
|
820
|
+
origin: input.text(request.origin, "request.origin", 32),
|
|
810
821
|
routine: {
|
|
811
|
-
name:
|
|
812
|
-
version:
|
|
822
|
+
name: input.text(routine.name, "request.routine.name", 256),
|
|
823
|
+
version: input.positiveCount(routine.version, "request.routine.version"),
|
|
813
824
|
},
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
),
|
|
820
|
-
requested_at: envelope.text(
|
|
821
|
-
request.requested_at,
|
|
822
|
-
`${declaration}.request.requested_at`,
|
|
823
|
-
64,
|
|
824
|
-
),
|
|
825
|
-
deliver_at:
|
|
826
|
-
request.deliver_at === null
|
|
827
|
-
? null
|
|
828
|
-
: envelope.text(request.deliver_at, `${declaration}.request.deliver_at`, 64),
|
|
825
|
+
instruction: input.text(request.instruction, "request.instruction", 256 * 1024),
|
|
826
|
+
payload: request.payload,
|
|
827
|
+
references: request.references.map((reference, index) => input.plain(reference, `request.references[${index}]`)),
|
|
828
|
+
requested_by: input.text(request.requested_by, "request.requested_by", 128),
|
|
829
|
+
requested_at: input.text(request.requested_at, "request.requested_at", 64),
|
|
830
|
+
deliver_at: request.deliver_at === null ? null : input.text(request.deliver_at, "request.deliver_at", 64),
|
|
829
831
|
};
|
|
830
832
|
}
|
|
831
833
|
|
|
834
|
+
export async function readRunRequest(session, run, token, signal = session.signal) {
|
|
835
|
+
return runRequest(await houseRequest(
|
|
836
|
+
session.house,
|
|
837
|
+
`Bearer ${token}`,
|
|
838
|
+
runRequestPath(run),
|
|
839
|
+
{},
|
|
840
|
+
session.dependencies,
|
|
841
|
+
signal,
|
|
842
|
+
));
|
|
843
|
+
}
|
|
844
|
+
|
|
832
845
|
function acquiredAssignment(value, declaration) {
|
|
833
846
|
const acquired = envelope.plain(value, declaration);
|
|
834
847
|
const bridge = envelope.plain(acquired.session, `${declaration}.session`);
|
|
@@ -857,7 +870,6 @@ function acquiredAssignment(value, declaration) {
|
|
|
857
870
|
token: envelope.text(bridge.bridge_token, `${declaration}.session.bridge_token`, 512),
|
|
858
871
|
expiresAt: Date.parse(expiresAt),
|
|
859
872
|
},
|
|
860
|
-
request: acquiredRequest(acquired, declaration),
|
|
861
873
|
};
|
|
862
874
|
}
|
|
863
875
|
|
package/src/inbound.js
CHANGED
|
@@ -134,13 +134,9 @@ export async function reconcileInbound(directory, session) {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
export async function localRequest(directory, run, request) {
|
|
137
|
-
|
|
138
|
-
if (turn === null || turn === undefined) return request;
|
|
137
|
+
if (request.kind !== "conversation") return request;
|
|
139
138
|
const result = structuredClone(request);
|
|
140
|
-
const
|
|
141
|
-
if (!Array.isArray(localTurn.messages)) throw new KitRefusal("kit_run_input_unreadable");
|
|
142
|
-
for (const message of localTurn.messages) {
|
|
143
|
-
if (!Array.isArray(message.files)) throw new KitRefusal("kit_run_input_unreadable");
|
|
139
|
+
for (const message of result.turn.messages) {
|
|
144
140
|
for (const file of message.files) {
|
|
145
141
|
const parts = typeof file.path === "string" ? file.path.split("/") : [];
|
|
146
142
|
if (parts.length !== 4 || parts[0] !== ROOT || parts[1] !== run || !UUID.test(parts[1]) || !UUID.test(parts[2]) || !segment(parts[3])) {
|
|
@@ -161,15 +157,3 @@ export async function localRequest(directory, run, request) {
|
|
|
161
157
|
}
|
|
162
158
|
return result;
|
|
163
159
|
}
|
|
164
|
-
|
|
165
|
-
export async function localReply(directory, run, frame, reply) {
|
|
166
|
-
if (frame.method !== "tools/call" || frame.params?.name !== "session_get_request" || reply.error || reply.result?.isError) return reply;
|
|
167
|
-
const result = structuredClone(reply);
|
|
168
|
-
for (const item of result.result?.content ?? []) {
|
|
169
|
-
if (item.type !== "text") continue;
|
|
170
|
-
let input;
|
|
171
|
-
try { input = JSON.parse(item.text); } catch { throw new KitRefusal("kit_run_input_unreadable"); }
|
|
172
|
-
item.text = JSON.stringify(await localRequest(directory, run, input));
|
|
173
|
-
}
|
|
174
|
-
return result;
|
|
175
|
-
}
|
package/src/launcher.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { commandHouseInstructions
|
|
2
|
+
import { commandHouseInstructions } from "./adapter-mcp.js";
|
|
3
3
|
import {
|
|
4
4
|
compiledContract,
|
|
5
5
|
initializeCommandAdapter,
|
|
@@ -191,13 +191,12 @@ async function runTurn(channel, start, bridge, turn) {
|
|
|
191
191
|
loading = false;
|
|
192
192
|
}
|
|
193
193
|
await configureSession(acp, contract, providerSessionId, start.adapter);
|
|
194
|
-
const request = start.request === null ? await commandHouseRequest(bridge) : start.request;
|
|
195
194
|
turn.adapterSession = true;
|
|
196
195
|
turn.cancel = () => acp.cancel({ sessionId: providerSessionId });
|
|
197
196
|
const prompted = await acp.request("session/prompt", {
|
|
198
197
|
sessionId: providerSessionId,
|
|
199
198
|
prompt: [
|
|
200
|
-
{ type: "text", text: JSON.stringify(request) },
|
|
199
|
+
{ type: "text", text: JSON.stringify(start.request) },
|
|
201
200
|
{ type: "text", text: instructions },
|
|
202
201
|
...workingCopyContext(start.working_copies),
|
|
203
202
|
],
|
package/src/local-files.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { stringify } from "yaml";
|
|
3
3
|
import { AGENT_OPERATION_META, ATTACHMENT_FILE_META } from "./contract.js";
|
|
4
|
+
import { answerText, answerYaml } from "./mcp.js";
|
|
4
5
|
import { derivedOperationId } from "./operation.js";
|
|
5
6
|
import { localAttachment, sendLocalConversationFile, uploadLocalAttachment } from "./original.js";
|
|
6
7
|
import { KitRefusal } from "./refusal.js";
|
|
@@ -12,18 +13,9 @@ class HouseRefused extends Error {
|
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function parsed(text) {
|
|
16
|
-
try {
|
|
17
|
-
return parse(text);
|
|
18
|
-
} catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
function answerOf(frame) {
|
|
24
17
|
if (frame.error !== undefined || frame.result?.isError === true) throw new HouseRefused(frame);
|
|
25
|
-
const
|
|
26
|
-
const answer = parsed(text);
|
|
18
|
+
const answer = answerYaml(answerText(frame.result));
|
|
27
19
|
if (answer === null || typeof answer !== "object") throw new KitRefusal("kit_house_answer_unreadable");
|
|
28
20
|
return answer;
|
|
29
21
|
}
|
package/src/mcp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { parse } from "yaml";
|
|
2
3
|
import { bridgePeer } from "./peer.js";
|
|
3
4
|
import { KitRefusal } from "./refusal.js";
|
|
4
5
|
|
|
@@ -69,3 +70,15 @@ export async function bridgeCall(bridge, request, signal = undefined) {
|
|
|
69
70
|
await peer.end();
|
|
70
71
|
}
|
|
71
72
|
}
|
|
73
|
+
|
|
74
|
+
export function answerText(result) {
|
|
75
|
+
return (result?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function answerYaml(text) {
|
|
79
|
+
try {
|
|
80
|
+
return parse(text);
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/sync-library.js
CHANGED
|
@@ -100,13 +100,12 @@ function recordedMap(authority) {
|
|
|
100
100
|
return new Map(authority.files.map((file) => [file.path, file]));
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function structuredOrReplace(root, relative, recorded, content) {
|
|
103
|
+
export function structuredOrReplace(root, relative, recorded, content) {
|
|
104
104
|
const path = housePath(root, relative);
|
|
105
|
+
const whole = [{ op: "replace", path, base: recorded.revision, content }];
|
|
105
106
|
const before = parse(recorded.content, relative);
|
|
106
107
|
const after = parse(content, relative);
|
|
107
|
-
if (!before.ok || !after.ok)
|
|
108
|
-
return [{ op: "replace", path, base: recorded.revision, content }];
|
|
109
|
-
}
|
|
108
|
+
if (!before.ok || !after.ok) return whole;
|
|
110
109
|
const ops = [];
|
|
111
110
|
if (before.document.preamble !== after.document.preamble) {
|
|
112
111
|
ops.push({
|
|
@@ -124,9 +123,7 @@ function structuredOrReplace(root, relative, recorded, content) {
|
|
|
124
123
|
if (JSON.stringify(before.document.fields[name]) === JSON.stringify(after.document.fields[name])) {
|
|
125
124
|
continue;
|
|
126
125
|
}
|
|
127
|
-
if (after.document.fields[name] === undefined)
|
|
128
|
-
return [{ op: "replace", path, base: recorded.revision, content }];
|
|
129
|
-
}
|
|
126
|
+
if (after.document.fields[name] === undefined) return whole;
|
|
130
127
|
const change = { op: "set_field", path, field: name, value: after.document.fields[name] };
|
|
131
128
|
if (before.document.fields[name] !== undefined) change.base = before.document.fields[name];
|
|
132
129
|
ops.push(change);
|
|
@@ -139,12 +136,11 @@ function structuredOrReplace(root, relative, recorded, content) {
|
|
|
139
136
|
const prior = before.document.sections[name];
|
|
140
137
|
const next = after.document.sections[name];
|
|
141
138
|
if (prior === next) continue;
|
|
142
|
-
if (prior === undefined || next === undefined)
|
|
143
|
-
return [{ op: "replace", path, base: recorded.revision, content }];
|
|
144
|
-
}
|
|
139
|
+
if (prior === undefined || next === undefined) return whole;
|
|
145
140
|
ops.push({ op: "replace_section", path, section: name, base: prior, content: next });
|
|
146
141
|
}
|
|
147
|
-
|
|
142
|
+
if (ops.length === 0) return whole;
|
|
143
|
+
return applyChanges({ [relative]: recorded.content }, ops, root)[relative] === content ? ops : whole;
|
|
148
144
|
}
|
|
149
145
|
|
|
150
146
|
export function deriveChanges(state, authority, localFiles) {
|
package/src/sync.js
CHANGED
|
@@ -41,7 +41,7 @@ function sessionDependencies(options, dependencies) {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async function withSession(options, dependencies, operation) {
|
|
44
|
+
export async function withSession(options, dependencies, operation) {
|
|
45
45
|
const session = await openSession(options, sessionDependencies(options, dependencies));
|
|
46
46
|
return maintainControl(session, (signal) => operation(session, signal), options.signal);
|
|
47
47
|
}
|
package/src/worker.js
CHANGED
|
@@ -10,15 +10,16 @@ import {
|
|
|
10
10
|
} from "./agents.js";
|
|
11
11
|
import { readBindings, removeBinding, writeBinding } from "./binding.js";
|
|
12
12
|
import { readContinuation, readContinuationRuns, writeContinuation, removeContinuation } from "./continuation.js";
|
|
13
|
-
import { openBridge } from "./bridge.js";
|
|
13
|
+
import { bridgedFrame, openBridge } from "./bridge.js";
|
|
14
14
|
import { assistantBlocks } from "./blocks.js";
|
|
15
15
|
import { retryConversationFiles } from "./conversation-originals.js";
|
|
16
16
|
import { openAssistantDrafts } from "./draft.js";
|
|
17
|
-
import {
|
|
17
|
+
import { localRequest, reconcileInbound } from "./inbound.js";
|
|
18
18
|
import { localFiles } from "./local-files.js";
|
|
19
|
+
import { answerText, answerYaml } from "./mcp.js";
|
|
19
20
|
import { inheritedEnvironment } from "./command.js";
|
|
20
21
|
import { watchKitWork } from "./kit-work-stream.js";
|
|
21
|
-
import { applyWorkingCopies, workingCopyPaths } from "./working-copy.js";
|
|
22
|
+
import { applyWorkingCopies, ownerCaller, pushWorkingCopy, workingCopyPaths } from "./working-copy.js";
|
|
22
23
|
import { readConfiguration } from "./configuration.js";
|
|
23
24
|
import { launchBase, readyLaunchDirectory } from "./directory.js";
|
|
24
25
|
import {
|
|
@@ -32,8 +33,9 @@ import {
|
|
|
32
33
|
WORKER_HEARTBEAT_MS,
|
|
33
34
|
WORKER_INHERITED_ENVIRONMENT,
|
|
34
35
|
WORKER_SUPERVISION_POLL_MS,
|
|
36
|
+
WORKING_COPY_PUSH_METHOD,
|
|
35
37
|
} from "./contract.js";
|
|
36
|
-
import { EpochReportRequired, houseFailed, KitRefusal } from "./refusal.js";
|
|
38
|
+
import { EpochReportRequired, formatCommandError, houseFailed, KitRefusal } from "./refusal.js";
|
|
37
39
|
import {
|
|
38
40
|
acceptAssistantText,
|
|
39
41
|
acquireCommands,
|
|
@@ -52,6 +54,7 @@ import {
|
|
|
52
54
|
observeSessionStart,
|
|
53
55
|
observeSessionStop,
|
|
54
56
|
openSession,
|
|
57
|
+
readRunRequest,
|
|
55
58
|
replaceSessionAuthority,
|
|
56
59
|
reportContinuation,
|
|
57
60
|
reportInterruptOutcome,
|
|
@@ -269,6 +272,52 @@ function endOnEpochReport(ended, act) {
|
|
|
269
272
|
};
|
|
270
273
|
}
|
|
271
274
|
|
|
275
|
+
function refusedWith(text) {
|
|
276
|
+
const conflicts = answerYaml(text.slice(text.indexOf("\n") + 1))?.conflicts;
|
|
277
|
+
const current = Array.isArray(conflicts)
|
|
278
|
+
? conflicts.map(({ path, state, revision }) =>
|
|
279
|
+
state === "absent" ? { path, revision: null, content: null } : { path, revision },
|
|
280
|
+
)
|
|
281
|
+
: null;
|
|
282
|
+
return { refused: { code: text.split(":")[0].trim(), text, current } };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function acceptedWith(text) {
|
|
286
|
+
const written = answerYaml(text).results.map(({ path, op, revision }) =>
|
|
287
|
+
op === "remove" ? { path, revision: null, content: null } : { path, revision },
|
|
288
|
+
);
|
|
289
|
+
return { refused: null, written };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function sessionPush(context, bound, authority, frame, signal) {
|
|
293
|
+
const edit = async (body, sent) => {
|
|
294
|
+
const answer = await callHouseMcp(
|
|
295
|
+
context.session,
|
|
296
|
+
await sessionAuthority(context.session, bound.session, authority, sent),
|
|
297
|
+
bridgedFrame(
|
|
298
|
+
{ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "edit", arguments: { changes: body.changes } } },
|
|
299
|
+
body.operation_id,
|
|
300
|
+
),
|
|
301
|
+
sent,
|
|
302
|
+
);
|
|
303
|
+
if (answer.error !== undefined) return refusedWith(answer.error.message ?? "kit_house_refused");
|
|
304
|
+
const text = answerText(answer.result);
|
|
305
|
+
return answer.result?.isError === true ? refusedWith(text) : acceptedWith(text);
|
|
306
|
+
};
|
|
307
|
+
let outcome;
|
|
308
|
+
try {
|
|
309
|
+
outcome = await pushWorkingCopy(context, frame.params, { binding: { session: bound.session }, submit: edit }, signal);
|
|
310
|
+
} catch (refusal) {
|
|
311
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
312
|
+
outcome = { refused: true, text: refusal.message };
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
jsonrpc: "2.0",
|
|
316
|
+
id: frame.id,
|
|
317
|
+
result: { content: [{ type: "text", text: outcome.text }], ...(outcome.refused ? { isError: true } : {}) },
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
272
321
|
async function openRunBridge(context, bound, authority, start) {
|
|
273
322
|
const ended = new AbortController();
|
|
274
323
|
const handed = (act) => endOnEpochReport(ended, act);
|
|
@@ -290,14 +339,15 @@ async function openRunBridge(context, bound, authority, start) {
|
|
|
290
339
|
),
|
|
291
340
|
secret: (interaction, ceremony) => awaitSecretInput(context.session, interaction, ceremony),
|
|
292
341
|
settle: handed((interaction, outcome) => settleInteraction(context, interaction, outcome)),
|
|
293
|
-
forward: handed((frame, signal) =>
|
|
294
|
-
|
|
295
|
-
|
|
342
|
+
forward: handed((frame, signal) => frame.method === WORKING_COPY_PUSH_METHOD
|
|
343
|
+
? sessionPush(context, bound, authority, frame, signal)
|
|
344
|
+
: localFiles(async (sent) =>
|
|
345
|
+
callHouseMcp(
|
|
296
346
|
context.session,
|
|
297
347
|
await sessionAuthority(context.session, bound.session, authority, signal),
|
|
298
348
|
sent,
|
|
299
349
|
signal,
|
|
300
|
-
)
|
|
350
|
+
), context.session.dependencies, signal)(frame)),
|
|
301
351
|
});
|
|
302
352
|
return { bridge, drafts, ended };
|
|
303
353
|
}
|
|
@@ -467,7 +517,7 @@ async function prepareAssignment(context, assignment, resumed) {
|
|
|
467
517
|
reported: null,
|
|
468
518
|
};
|
|
469
519
|
const authority = { token: assignment.session.token, expiresAt: assignment.session.expiresAt };
|
|
470
|
-
const
|
|
520
|
+
const start = {
|
|
471
521
|
working_directory: assignment.workingDirectory,
|
|
472
522
|
working_copies: await appliedWorkingCopies(context),
|
|
473
523
|
execution_authority: context.configuration.executionAuthority,
|
|
@@ -479,10 +529,16 @@ async function prepareAssignment(context, assignment, resumed) {
|
|
|
479
529
|
effort: route.effort,
|
|
480
530
|
environment: {},
|
|
481
531
|
},
|
|
482
|
-
request:
|
|
532
|
+
request: null,
|
|
483
533
|
provider_session: assignment.providerSessionId ?? null,
|
|
484
|
-
}
|
|
485
|
-
|
|
534
|
+
};
|
|
535
|
+
const opened = await openRunBridge(context, bound, authority, start);
|
|
536
|
+
const request = async () => {
|
|
537
|
+
const token = await sessionAuthority(context.session, bound.session, authority);
|
|
538
|
+
start.request = await localRequest(context.directory, bound.run,
|
|
539
|
+
await readRunRequest(context.session, bound.run, token));
|
|
540
|
+
};
|
|
541
|
+
return { bound, opened, request };
|
|
486
542
|
}
|
|
487
543
|
|
|
488
544
|
async function refuseContinuation(context, command, failure) {
|
|
@@ -523,11 +579,12 @@ async function superviseAssignment(context, assignment, resumed = false) {
|
|
|
523
579
|
if (undecided(context, refusal)) throw refusal;
|
|
524
580
|
return refuseAssignment(context, assignment, failure);
|
|
525
581
|
}
|
|
526
|
-
const { bound, opened } = prepared;
|
|
582
|
+
const { bound, opened, request } = prepared;
|
|
527
583
|
return superviseBound(context, bound, opened, async (spawn) => {
|
|
528
584
|
if (!resumed) await commitRun(context.session, assignment.run);
|
|
529
585
|
spawn.committed = true;
|
|
530
586
|
await writeBinding(context.directory, bound);
|
|
587
|
+
await request();
|
|
531
588
|
await createSession(context.runtime, {
|
|
532
589
|
session: bound.tmux,
|
|
533
590
|
workingDirectory: assignment.workingDirectory,
|
|
@@ -549,7 +606,7 @@ async function superviseResumption(context, command) {
|
|
|
549
606
|
}
|
|
550
607
|
return superviseAssignment(context, {
|
|
551
608
|
...command, agentId: held.agentId, workingDirectory: held.workingDirectory,
|
|
552
|
-
conversation: held.conversation, providerSessionId: held.providerSessionId,
|
|
609
|
+
conversation: held.conversation, providerSessionId: held.providerSessionId,
|
|
553
610
|
}, true);
|
|
554
611
|
}
|
|
555
612
|
|
|
@@ -952,7 +1009,16 @@ export async function runResident(options, dependencies = {}) {
|
|
|
952
1009
|
const wake = residentWake();
|
|
953
1010
|
const update = residentUpdate(context, wake);
|
|
954
1011
|
context.update = update;
|
|
955
|
-
const served = await serveResident(
|
|
1012
|
+
const served = await serveResident(
|
|
1013
|
+
session,
|
|
1014
|
+
context.directory,
|
|
1015
|
+
options.signal,
|
|
1016
|
+
(serving) => update.request(serving),
|
|
1017
|
+
(request, signal) =>
|
|
1018
|
+
pushWorkingCopy({ directory: context.directory, session }, request, ownerCaller(session), signal).catch(
|
|
1019
|
+
(error) => ({ refused: true, text: formatCommandError(error) }),
|
|
1020
|
+
),
|
|
1021
|
+
);
|
|
956
1022
|
const serve = async (signal) => {
|
|
957
1023
|
context.busy = true;
|
|
958
1024
|
const watcher = new AbortController();
|