@agentshouse/kit 0.1.0-alpha.4 → 0.1.0-alpha.41
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 +41 -1671
- package/bin/connect-linux.sh +501 -53
- package/bin/connect-windows.ps1 +440 -0
- package/bin/kit.js +200 -159
- package/bootstrap.json +28 -0
- package/docs/configuration.md +104 -0
- package/docs/google.md +33 -0
- package/docs/linux.md +77 -0
- package/docs/macos.md +26 -0
- package/docs/telegram.md +29 -0
- package/docs/windows.md +37 -0
- package/package.json +7 -2
- package/src/adapter-mcp.js +92 -12
- package/src/agents.js +63 -8
- package/src/binding.js +52 -0
- package/src/blocks.js +2 -26
- package/src/bridge.js +28 -14
- package/src/bytes.js +19 -8
- package/src/configuration.js +14 -211
- package/src/connector-login.js +3 -84
- package/src/contract.js +44 -12
- package/src/conversation-originals.js +1 -3
- package/src/delivery.js +4 -7
- package/src/directory.js +6 -26
- package/src/draft.js +4 -82
- package/src/git-main.js +138 -0
- package/src/git.js +62 -0
- package/src/google/calendar.js +39 -45
- package/src/google/drive.js +61 -186
- package/src/google/entries.js +349 -0
- package/src/google/gateway.js +3 -6
- package/src/google/gmail.js +33 -23
- package/src/google/oauth.js +106 -66
- package/src/google/source.js +21 -23
- package/src/host.js +6 -2
- package/src/house-main.js +65 -8
- package/src/house.js +446 -127
- package/src/how-we-work.md +25 -0
- package/src/inbound.js +2 -18
- package/src/kit-work-stream.js +120 -0
- package/src/launcher.js +46 -10
- package/src/local-files.js +3 -11
- package/src/login.js +27 -14
- package/src/mcp.js +18 -0
- package/src/mount.js +9 -18
- package/src/original.js +4 -4
- package/src/pacing.js +41 -0
- package/src/plan.js +37 -45
- package/src/repository.js +3 -61
- package/src/session-bin/git +2 -0
- package/src/setup.js +14 -15
- package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
- package/src/skill-set/ask-matt/SKILL.md +91 -0
- package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
- package/src/skill-set/code-review/LICENSE +21 -0
- package/src/skill-set/code-review/SKILL.md +89 -0
- package/src/skill-set/code-review/agents/openai.yaml +3 -0
- package/src/skill-set/codebase-design/DEEPENING.md +37 -0
- package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/src/skill-set/codebase-design/LICENSE +21 -0
- package/src/skill-set/codebase-design/SKILL.md +116 -0
- package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
- package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
- package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
- package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/src/skill-set/domain-modeling/LICENSE +21 -0
- package/src/skill-set/domain-modeling/SKILL.md +76 -0
- package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
- package/src/skill-set/grill-me/LICENSE +21 -0
- package/src/skill-set/grill-me/SKILL.md +8 -0
- package/src/skill-set/grill-me/agents/openai.yaml +4 -0
- package/src/skill-set/grill-with-docs/LICENSE +21 -0
- package/src/skill-set/grill-with-docs/SKILL.md +8 -0
- package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
- package/src/skill-set/grilling/LICENSE +21 -0
- package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
- package/src/skill-set/handoff/LICENSE +21 -0
- package/src/skill-set/handoff/SKILL.md +17 -0
- package/src/skill-set/handoff/agents/openai.yaml +4 -0
- package/src/skill-set/implement/LICENSE +21 -0
- package/src/skill-set/implement/SKILL.md +16 -0
- package/src/skill-set/implement/agents/openai.yaml +4 -0
- package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
- package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
- package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
- package/src/skill-set/prototype/LICENSE +21 -0
- package/src/skill-set/prototype/LOGIC.md +67 -0
- package/src/skill-set/prototype/SKILL.md +28 -0
- package/src/skill-set/prototype/UI.md +112 -0
- package/src/skill-set/prototype/agents/openai.yaml +3 -0
- package/src/skill-set/research/LICENSE +21 -0
- package/src/skill-set/research/SKILL.md +14 -0
- package/src/skill-set/research/agents/openai.yaml +3 -0
- package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
- package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
- package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/LICENSE +21 -0
- package/src/skill-set/tdd/SKILL.md +40 -0
- package/src/skill-set/tdd/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/mocking.md +59 -0
- package/src/skill-set/tdd/tests.md +77 -0
- package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
- package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/src/skill-set/teach/LICENSE +21 -0
- package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
- package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
- package/src/skill-set/teach/SKILL.md +141 -0
- package/src/skill-set/teach/agents/openai.yaml +4 -0
- package/src/skill-set/to-questionnaire/LICENSE +21 -0
- package/src/skill-set/to-questionnaire/SKILL.md +55 -0
- package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
- package/src/skill-set/to-spec/LICENSE +21 -0
- package/src/skill-set/to-spec/SKILL.md +76 -0
- package/src/skill-set/to-spec/agents/openai.yaml +4 -0
- package/src/skill-set/to-tickets/LICENSE +21 -0
- package/src/skill-set/to-tickets/SKILL.md +106 -0
- package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
- package/src/skill-set/wait-what/LICENSE +21 -0
- package/src/skill-set/wait-what/SKILL.md +8 -0
- package/src/skill-set/wait-what/agents/openai.yaml +4 -0
- package/src/skill-set/wayfinder/LICENSE +21 -0
- package/src/skill-set/wayfinder/SKILL.md +129 -0
- package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
- package/src/skill-set/writing-for-agents/LICENSE +21 -0
- package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/src/skill-set/writing-for-agents/SKILL.md +83 -0
- package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
- package/src/skill-set.js +85 -0
- package/src/source.js +93 -54
- package/src/state.js +19 -1
- package/src/sync-library.js +7 -11
- package/src/sync-state.js +12 -3
- package/src/sync.js +142 -43
- package/src/tasks.js +92 -0
- package/src/telegram-view.js +217 -0
- package/src/telegram.js +204 -69
- package/src/turn-frames.js +69 -0
- package/src/worker.js +738 -189
- package/src/working-copy.js +1274 -0
- package/src/workspace.js +299 -88
- package/src/google/declared.js +0 -146
- package/src/google/id.js +0 -1
- package/src/instance/google.js +0 -1
- package/src/starter-set.js +0 -17
- /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
- /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# How we work
|
|
2
|
+
|
|
3
|
+
A Room is where information people share is kept; it is not the place for
|
|
4
|
+
private information. Work goes into the Room it belongs to, or the First Room
|
|
5
|
+
when none is named. Read and write Rooms with the `house` CLI.
|
|
6
|
+
|
|
7
|
+
The skills are already set up; never run `setup-matt-pocock-skills`.
|
|
8
|
+
|
|
9
|
+
## Where skill output goes
|
|
10
|
+
|
|
11
|
+
- Glossary (`CONTEXT.md`): `library/glossary.md`
|
|
12
|
+
- Decisions (ADRs): `library/decisions/NNNN-<slug>.md`
|
|
13
|
+
- Plan (spec): `library/projects/<name>/plan.md`
|
|
14
|
+
- Tasks (tickets): `library/projects/<name>/tasks/NN-<slug>.md`, with a
|
|
15
|
+
`Status:` line (`needs-triage`, `needs-info`, `ready-for-agent`,
|
|
16
|
+
`ready-for-human`, `wontfix`), a `Blocked by:` line and comments under
|
|
17
|
+
`## Comments`
|
|
18
|
+
- Map (Wayfinding): `library/projects/<name>/map.md`, its tasks under
|
|
19
|
+
`tasks/` with `Type:`, `Status: claimed|resolved` and `Blocked by:` lines
|
|
20
|
+
- Research: `library/research/<slug>.md`
|
|
21
|
+
- Questionnaires: `library/questionnaires/<slug>.md`
|
|
22
|
+
- Learning: `library/learning/<topic>/`
|
|
23
|
+
- Files a person should open, such as HTML pages, are uploaded to the Room and
|
|
24
|
+
linked from the document they belong to; a prototype is one HTML file with
|
|
25
|
+
every variant on one page.
|
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
|
-
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
import {
|
|
4
|
+
KIT_CONTRACT_VERSION,
|
|
5
|
+
KIT_STREAM_RECONNECT_MAX_MS,
|
|
6
|
+
KIT_STREAM_RECONNECT_MS,
|
|
7
|
+
KIT_STREAM_REJOIN_MS,
|
|
8
|
+
} from "./contract.js";
|
|
9
|
+
import { heldControl } from "./house.js";
|
|
10
|
+
import { KitRefusal } from "./refusal.js";
|
|
11
|
+
|
|
12
|
+
const PROTOCOL = "house.kit.stream.1";
|
|
13
|
+
const WORK_SUBJECTS = ["runs", "commands", "agents", "working_copy", "sources"];
|
|
14
|
+
const FINAL_REFUSALS = ["kit_upgrade_required", "kit_stream_replaced"];
|
|
15
|
+
const VIEW_FRAMES = ["draft", "plan"];
|
|
16
|
+
|
|
17
|
+
function streamAddress(house) {
|
|
18
|
+
const url = new URL("/kit/stream", house);
|
|
19
|
+
if (url.protocol === "https:") url.protocol = "wss:";
|
|
20
|
+
else if (url.protocol === "http:") url.protocol = "ws:";
|
|
21
|
+
else throw new KitRefusal("kit_house_unavailable", "/kit/stream");
|
|
22
|
+
return url;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function frameOf(data) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(data.toString());
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function held(session, signal, heard, dependencies) {
|
|
34
|
+
const authorization = await heldControl(session, signal);
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const Socket = dependencies.workSocket ?? WebSocket;
|
|
37
|
+
const socket = new Socket(streamAddress(session.house), PROTOCOL, {
|
|
38
|
+
headers: { authorization, "x-house-kit-version": KIT_CONTRACT_VERSION },
|
|
39
|
+
handshakeTimeout: 10_000,
|
|
40
|
+
perMessageDeflate: false,
|
|
41
|
+
});
|
|
42
|
+
let finished = false;
|
|
43
|
+
const carried = new Map();
|
|
44
|
+
const finish = (reason = null, refusal = null) => {
|
|
45
|
+
if (finished) return;
|
|
46
|
+
finished = true;
|
|
47
|
+
signal.removeEventListener("abort", stop);
|
|
48
|
+
heard.closed?.();
|
|
49
|
+
if (refusal === null) resolve(reason);
|
|
50
|
+
else reject(refusal);
|
|
51
|
+
};
|
|
52
|
+
const stop = () => {
|
|
53
|
+
socket.terminate();
|
|
54
|
+
finish();
|
|
55
|
+
};
|
|
56
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
57
|
+
socket.on("open", () => {
|
|
58
|
+
if (finished) return;
|
|
59
|
+
heard.opened?.((frame) => socket.send(JSON.stringify(frame), () => undefined));
|
|
60
|
+
heard.available(null);
|
|
61
|
+
});
|
|
62
|
+
socket.on("message", (data) => {
|
|
63
|
+
const frame = frameOf(data);
|
|
64
|
+
if (frame?.type === "work_available" && WORK_SUBJECTS.includes(frame.subject)) {
|
|
65
|
+
heard.available(frame.subject);
|
|
66
|
+
} else if (frame?.type === "entries") {
|
|
67
|
+
heard.changed(frame, carried.get(frame.authority) ?? null);
|
|
68
|
+
carried.set(frame.authority, frame.position);
|
|
69
|
+
} else if (VIEW_FRAMES.includes(frame?.type)) {
|
|
70
|
+
heard.viewed?.(frame);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
socket.on("unexpected-response", (_request, response) => {
|
|
74
|
+
response.resume();
|
|
75
|
+
if (response.statusCode === 409) session.dependencies.contractRefused?.();
|
|
76
|
+
finish(null, response.statusCode === 409 ? new KitRefusal("kit_upgrade_required") : null);
|
|
77
|
+
socket.terminate();
|
|
78
|
+
});
|
|
79
|
+
socket.on("error", () => {
|
|
80
|
+
socket.terminate();
|
|
81
|
+
finish();
|
|
82
|
+
});
|
|
83
|
+
socket.on("close", (_code, reason) => {
|
|
84
|
+
const closed = reason?.toString() ?? "";
|
|
85
|
+
finish(closed, closed === "replaced" ? new KitRefusal("kit_stream_replaced") : null);
|
|
86
|
+
});
|
|
87
|
+
if (signal.aborted) stop();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function watchKitWork(session, signal, heard, dependencies = {}) {
|
|
92
|
+
const random = dependencies.random ?? Math.random;
|
|
93
|
+
const sleep = dependencies.streamSleep ?? ((milliseconds) => delay(milliseconds, undefined, { signal }));
|
|
94
|
+
let failures = 0;
|
|
95
|
+
while (!signal.aborted) {
|
|
96
|
+
const began = Date.now();
|
|
97
|
+
let reason = null;
|
|
98
|
+
try {
|
|
99
|
+
reason = await held(session, signal, heard, dependencies);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error instanceof KitRefusal && FINAL_REFUSALS.includes(error.code)) throw error;
|
|
102
|
+
}
|
|
103
|
+
if (signal.aborted) return;
|
|
104
|
+
let wait;
|
|
105
|
+
if (reason === "authority_changed") {
|
|
106
|
+
failures = 0;
|
|
107
|
+
wait = random() * KIT_STREAM_REJOIN_MS;
|
|
108
|
+
} else {
|
|
109
|
+
if (Date.now() - began >= KIT_STREAM_RECONNECT_MAX_MS) failures = 0;
|
|
110
|
+
const ceiling = Math.min(KIT_STREAM_RECONNECT_MAX_MS, KIT_STREAM_RECONNECT_MS * 2 ** failures);
|
|
111
|
+
failures += 1;
|
|
112
|
+
wait = ceiling / 2 + (random() * ceiling) / 2;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await sleep(wait);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (!signal.aborted) throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/launcher.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
commandHouseCreateHowWeWork,
|
|
4
|
+
commandHouseHowWeWork,
|
|
5
|
+
commandHouseInstructions,
|
|
6
|
+
} from "./adapter-mcp.js";
|
|
3
7
|
import {
|
|
4
8
|
compiledContract,
|
|
5
9
|
initializeCommandAdapter,
|
|
@@ -7,7 +11,7 @@ import {
|
|
|
7
11
|
startCommandAdapter,
|
|
8
12
|
stopCommandAdapter,
|
|
9
13
|
} from "./adapter.js";
|
|
10
|
-
import { PERMISSION_OPTION_KINDS, SESSION_SET_CONFIG_OPTION } from "./contract.js";
|
|
14
|
+
import { HOUSE_CLI_LINE, PERMISSION_OPTION_KINDS, SESSION_SET_CONFIG_OPTION } from "./contract.js";
|
|
11
15
|
import { observedAssistantDrafts } from "./draft.js";
|
|
12
16
|
import { observedPlans } from "./plan.js";
|
|
13
17
|
import { bridgePeer } from "./peer.js";
|
|
@@ -129,6 +133,20 @@ async function configureSession(acp, contract, sessionId, adapter) {
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
async function sessionOpening(channel, bridge, start) {
|
|
137
|
+
if (start.create_how_we_work === true && (await commandHouseCreateHowWeWork(bridge))) {
|
|
138
|
+
channel.send({ type: "how_we_work" });
|
|
139
|
+
}
|
|
140
|
+
const parts = [HOUSE_CLI_LINE, start.base_instructions, await commandHouseHowWeWork(bridge)];
|
|
141
|
+
return [{ type: "text", text: parts.filter((part) => part.trim() !== "").join("\n\n") }];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function workingCopyContext(copies) {
|
|
145
|
+
if (!Array.isArray(copies) || copies.length === 0) return [];
|
|
146
|
+
const lines = copies.map((copy) => `${copy.path}: ${copy.repository}`);
|
|
147
|
+
return [{ type: "text", text: ["Local Git working copies of House paths:", ...lines].join("\n") }];
|
|
148
|
+
}
|
|
149
|
+
|
|
132
150
|
async function runTurn(channel, start, bridge, turn) {
|
|
133
151
|
const contract = compiledContract(start.adapter?.kind);
|
|
134
152
|
const adapter = startCommandAdapter(
|
|
@@ -137,11 +155,23 @@ async function runTurn(channel, start, bridge, turn) {
|
|
|
137
155
|
);
|
|
138
156
|
const said = [];
|
|
139
157
|
const message = { text: "" };
|
|
140
|
-
const
|
|
158
|
+
const sequences = { draft: 0, plan: 0 };
|
|
159
|
+
const drafts = observedAssistantDrafts({
|
|
160
|
+
send: (text) => channel.send({ type: "draft", text, sequence: (sequences.draft += 1) }),
|
|
161
|
+
});
|
|
141
162
|
const plans = observedPlans({
|
|
142
|
-
send: (
|
|
143
|
-
channel.send({
|
|
163
|
+
send: (steps) =>
|
|
164
|
+
channel.send({
|
|
165
|
+
type: "plan",
|
|
166
|
+
provider_turn: turn.providerTurn,
|
|
167
|
+
sequence: (sequences.plan += 1),
|
|
168
|
+
steps,
|
|
169
|
+
}),
|
|
144
170
|
});
|
|
171
|
+
turn.plan = () => {
|
|
172
|
+
const steps = plans.latest();
|
|
173
|
+
return steps === null ? null : { provider_turn: turn.providerTurn, steps };
|
|
174
|
+
};
|
|
145
175
|
const wholeSoFar = () => [...said, message.text].filter((text) => text !== "").join("\n\n");
|
|
146
176
|
let loading = false;
|
|
147
177
|
const flush = () => {
|
|
@@ -149,7 +179,7 @@ async function runTurn(channel, start, bridge, turn) {
|
|
|
149
179
|
channel.send({ type: "assistant", text: message.text });
|
|
150
180
|
said.push(message.text);
|
|
151
181
|
message.text = "";
|
|
152
|
-
drafts.
|
|
182
|
+
drafts.observe(wholeSoFar());
|
|
153
183
|
};
|
|
154
184
|
const handlers = {
|
|
155
185
|
onPermission: (params) => resolvedPermission(params, start.execution_authority),
|
|
@@ -173,6 +203,8 @@ async function runTurn(channel, start, bridge, turn) {
|
|
|
173
203
|
const { acp } = await initializeCommandAdapter(adapter, contract, handlers);
|
|
174
204
|
const instructions = await commandHouseInstructions(bridge);
|
|
175
205
|
let providerSessionId = start.provider_session ?? null;
|
|
206
|
+
const opening =
|
|
207
|
+
providerSessionId === null ? await sessionOpening(channel, bridge, start) : [];
|
|
176
208
|
const setup = { cwd: start.working_directory, mcpServers: [] };
|
|
177
209
|
if (providerSessionId === null) {
|
|
178
210
|
providerSessionId = openedSession(await acp.request("session/new", setup));
|
|
@@ -185,28 +217,29 @@ async function runTurn(channel, start, bridge, turn) {
|
|
|
185
217
|
loading = false;
|
|
186
218
|
}
|
|
187
219
|
await configureSession(acp, contract, providerSessionId, start.adapter);
|
|
188
|
-
const request = start.request === null ? await commandHouseRequest(bridge) : start.request;
|
|
189
220
|
turn.adapterSession = true;
|
|
190
221
|
turn.cancel = () => acp.cancel({ sessionId: providerSessionId });
|
|
191
222
|
const prompted = await acp.request("session/prompt", {
|
|
192
223
|
sessionId: providerSessionId,
|
|
193
224
|
prompt: [
|
|
194
|
-
|
|
225
|
+
...opening,
|
|
226
|
+
{ type: "text", text: JSON.stringify(start.request) },
|
|
195
227
|
{ type: "text", text: instructions },
|
|
228
|
+
...workingCopyContext(start.working_copies),
|
|
196
229
|
],
|
|
197
230
|
});
|
|
198
231
|
await new Promise((resolve) => setImmediate(resolve));
|
|
199
232
|
flush();
|
|
200
|
-
plans.complete();
|
|
201
233
|
channel.send({
|
|
202
234
|
type: "turn_ended",
|
|
203
235
|
stop_outcome: turnOutcome(contract, prompted?.stopReason),
|
|
204
236
|
provider_continuation: contract.sessionLoad,
|
|
205
237
|
provider_session_id: providerSessionId,
|
|
238
|
+
plan: turn.plan(),
|
|
206
239
|
});
|
|
207
240
|
} finally {
|
|
208
241
|
turn.cancel = null;
|
|
209
|
-
plans.
|
|
242
|
+
plans.close();
|
|
210
243
|
abandonQuestions(channel, turn);
|
|
211
244
|
drafts.close();
|
|
212
245
|
stopCommandAdapter(adapter);
|
|
@@ -219,6 +252,7 @@ export async function launchWorkerSession(bridge) {
|
|
|
219
252
|
providerTurn: randomUUID(),
|
|
220
253
|
questions: new Map(),
|
|
221
254
|
cancel: null,
|
|
255
|
+
plan: () => null,
|
|
222
256
|
};
|
|
223
257
|
const peer = bridgePeer({
|
|
224
258
|
socket: bridge.socket,
|
|
@@ -246,6 +280,7 @@ export async function launchWorkerSession(bridge) {
|
|
|
246
280
|
type: "interrupt_applied",
|
|
247
281
|
interrupt: frame.interrupt,
|
|
248
282
|
outcome: "interrupted",
|
|
283
|
+
plan: turn.plan(),
|
|
249
284
|
}),
|
|
250
285
|
() =>
|
|
251
286
|
peer.send({
|
|
@@ -268,6 +303,7 @@ export async function launchWorkerSession(bridge) {
|
|
|
268
303
|
type: "failed",
|
|
269
304
|
code: refusal instanceof KitRefusal ? refusal.code : "kit_adapter_failed",
|
|
270
305
|
detail: refusal instanceof Error ? refusal.message : null,
|
|
306
|
+
plan: turn.plan(),
|
|
271
307
|
});
|
|
272
308
|
}
|
|
273
309
|
peer.send({ type: "exiting" });
|
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/login.js
CHANGED
|
@@ -8,12 +8,13 @@ import { createInterface } from "node:readline/promises";
|
|
|
8
8
|
import { stdin, stdout } from "node:process";
|
|
9
9
|
import { KIT_CREDENTIAL } from "./contract.js";
|
|
10
10
|
import { launchBase, prepareLaunchBase } from "./directory.js";
|
|
11
|
-
import { houseRequest } from "./house.js";
|
|
11
|
+
import { houseRequest, refuseDrained } from "./house.js";
|
|
12
12
|
import { KitRefusal } from "./refusal.js";
|
|
13
13
|
import { writeText } from "./state.js";
|
|
14
14
|
|
|
15
15
|
const ENVIRONMENT =
|
|
16
16
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
17
|
+
const DEFAULT_HOUSE = "https://agents.house";
|
|
17
18
|
|
|
18
19
|
function verifier() {
|
|
19
20
|
return randomBytes(32).toString("base64url");
|
|
@@ -112,19 +113,29 @@ function houseOrigin(value) {
|
|
|
112
113
|
return value;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
let value;
|
|
116
|
+
async function recordedHouse(directory) {
|
|
117
117
|
try {
|
|
118
|
-
|
|
118
|
+
return (await readFile(join(directory, "house"), "utf8")).trim();
|
|
119
119
|
} catch (error) {
|
|
120
|
-
if (error.code === "ENOENT")
|
|
121
|
-
if ((firstLogin || requested !== undefined) &&
|
|
122
|
-
await storedEnvironment(directory) === null && !await storedCredentialPresent(directory)) {
|
|
123
|
-
return houseOrigin(requested ?? "https://agents.house");
|
|
124
|
-
}
|
|
125
|
-
}
|
|
120
|
+
if (error.code === "ENOENT") return null;
|
|
126
121
|
throw new KitRefusal("kit_house_invalid", error.code ?? "unreadable");
|
|
127
122
|
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function storedHouse(directory) {
|
|
126
|
+
const value = await recordedHouse(directory);
|
|
127
|
+
if (value === null) throw new KitRefusal("kit_house_invalid", "ENOENT");
|
|
128
|
+
return houseOrigin(value);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function loginHouse(directory, requested) {
|
|
132
|
+
const value = await recordedHouse(directory);
|
|
133
|
+
if (value === null) {
|
|
134
|
+
if (await storedEnvironment(directory) !== null || await storedCredentialPresent(directory)) {
|
|
135
|
+
throw new KitRefusal("kit_house_invalid", "ENOENT");
|
|
136
|
+
}
|
|
137
|
+
return houseOrigin(requested ?? DEFAULT_HOUSE);
|
|
138
|
+
}
|
|
128
139
|
houseOrigin(value);
|
|
129
140
|
if (requested !== undefined && requested !== value) {
|
|
130
141
|
throw new KitRefusal("kit_house_mismatch", "this Environment is bound to another House origin");
|
|
@@ -133,7 +144,7 @@ export async function storedHouse(directory, firstLogin = false, requested = und
|
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
export function enrollmentAct(options, retained, host) {
|
|
136
|
-
if (options.
|
|
147
|
+
if (options.environment !== undefined && !ENVIRONMENT.test(options.environment)) {
|
|
137
148
|
throw new KitRefusal("kit_login_rejected", ENROLLMENT_USAGE);
|
|
138
149
|
}
|
|
139
150
|
const environment = options.environment ?? retained;
|
|
@@ -205,11 +216,12 @@ export async function login(options, dependencies = {}) {
|
|
|
205
216
|
return { environment: exchanged.environment };
|
|
206
217
|
}
|
|
207
218
|
|
|
208
|
-
export async function loopback() {
|
|
219
|
+
export async function loopback(published) {
|
|
209
220
|
const server = createServer();
|
|
210
221
|
await new Promise((resolve, reject) => {
|
|
211
222
|
server.once("error", reject);
|
|
212
|
-
server.listen(0, "127.0.0.1", resolve);
|
|
223
|
+
if (published === undefined) server.listen(0, "127.0.0.1", resolve);
|
|
224
|
+
else server.listen(Number(published), "0.0.0.0", resolve);
|
|
213
225
|
});
|
|
214
226
|
const address = server.address();
|
|
215
227
|
const code = new Promise((resolve, reject) => {
|
|
@@ -260,9 +272,10 @@ export async function openBrowser(url) {
|
|
|
260
272
|
}
|
|
261
273
|
|
|
262
274
|
export async function connectHouse(options, dependencies = {}) {
|
|
275
|
+
refuseDrained(options.directory);
|
|
263
276
|
await prepareLaunchBase(launchBase(dependencies));
|
|
264
277
|
if (options.presentation === "manual") return login(options, dependencies);
|
|
265
|
-
const callback = await loopback();
|
|
278
|
+
const callback = await loopback(options.loginPort);
|
|
266
279
|
try {
|
|
267
280
|
return await login(
|
|
268
281
|
{ ...options, presentation: "loopback", loopbackUri: callback.uri },
|
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,20 @@ 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
|
+
}
|
|
85
|
+
|
|
86
|
+
export function editRefusal(text) {
|
|
87
|
+
const conflicts = answerYaml(text.slice(text.indexOf("\n") + 1))?.conflicts;
|
|
88
|
+
return { code: text.split(":")[0].trim(), conflicts: Array.isArray(conflicts) ? conflicts : null };
|
|
89
|
+
}
|
package/src/mount.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import { apply, links, parse, serialize } from "@agentshouse/mdmodel";
|
|
4
|
+
import { apply, links, parse, retarget, serialize } from "@agentshouse/mdmodel";
|
|
5
5
|
import {
|
|
6
6
|
KIT_DOOR_BATCH_PATH,
|
|
7
7
|
KIT_DOOR_BOOTSTRAP_PATH,
|
|
@@ -84,7 +84,6 @@ export function mountTarget(subtree) {
|
|
|
84
84
|
refuse("kit_mount_subtree_invalid", subtree);
|
|
85
85
|
}
|
|
86
86
|
return {
|
|
87
|
-
kind: own ? "private" : "room",
|
|
88
87
|
handle: own ? null : segments[1],
|
|
89
88
|
root: own ? "/private" : `/rooms/${segments[1]}`,
|
|
90
89
|
subtree,
|
|
@@ -129,16 +128,11 @@ function authorityFor(discovery, target) {
|
|
|
129
128
|
};
|
|
130
129
|
}
|
|
131
130
|
|
|
132
|
-
function hrefSuffix(href) {
|
|
133
|
-
const match = /[?#]/.exec(href);
|
|
134
|
-
return match === null ? "" : href.slice(match.index);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
131
|
function replaceLink(document, href, destination) {
|
|
138
132
|
const marker = `house-kit-link:${operationId()}`;
|
|
139
133
|
const located = apply(document, { kind: "link", href, to: marker });
|
|
140
134
|
if (located.outcome !== "applied") refuse("kit_mount_link_unwritable", href);
|
|
141
|
-
const cleared = parse(serialize(located.document).replaceAll(
|
|
135
|
+
const cleared = parse(serialize(located.document).replaceAll(retarget(href, marker), marker));
|
|
142
136
|
if (!cleared.ok) refuse("kit_mount_link_unwritable", href);
|
|
143
137
|
const changed = apply(cleared.document, { kind: "link", href: marker, to: destination });
|
|
144
138
|
if (changed.outcome !== "applied") refuse("kit_mount_link_unwritable", href);
|
|
@@ -179,11 +173,8 @@ export function rewriteAttachmentLinks(held, mintVersion) {
|
|
|
179
173
|
refuse("kit_migrator_referrer_undeclared", `${relative} -> ${path}`);
|
|
180
174
|
}
|
|
181
175
|
const version = mintVersion(path);
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
href: link.href,
|
|
185
|
-
to: `${PENDING_LINK_SCHEME}:${version}`,
|
|
186
|
-
});
|
|
176
|
+
const pending = `${PENDING_LINK_SCHEME}:${version}`;
|
|
177
|
+
const applied = apply(document, { kind: "link", href: link.href, to: pending });
|
|
187
178
|
if (applied.outcome !== "applied") refuse("kit_mount_link_unwritable", link.href);
|
|
188
179
|
document = applied.document;
|
|
189
180
|
let save = saves.find((entry) => entry.path === path);
|
|
@@ -192,8 +183,8 @@ export function rewriteAttachmentLinks(held, mintVersion) {
|
|
|
192
183
|
saves.push(save);
|
|
193
184
|
}
|
|
194
185
|
if (!save.documents.includes(relative)) save.documents.push(relative);
|
|
195
|
-
const suffix =
|
|
196
|
-
const href =
|
|
186
|
+
const suffix = retarget(link.href, "");
|
|
187
|
+
const href = retarget(link.href, pending);
|
|
197
188
|
if (!save.hrefs.some((entry) => entry.document === relative && entry.href === href)) {
|
|
198
189
|
save.hrefs.push({ document: relative, href, suffix });
|
|
199
190
|
}
|
|
@@ -327,7 +318,7 @@ function pendingTransfer(held, save) {
|
|
|
327
318
|
};
|
|
328
319
|
}
|
|
329
320
|
|
|
330
|
-
async function submitAttachments(session, held,
|
|
321
|
+
async function submitAttachments(session, held, authority, plan, signal) {
|
|
331
322
|
const transfers = [];
|
|
332
323
|
for (const save of plan.saves) {
|
|
333
324
|
const local = held.attachments.get(save.path);
|
|
@@ -342,7 +333,7 @@ async function submitAttachments(session, held, target, authority, plan, signal)
|
|
|
342
333
|
version: save.version,
|
|
343
334
|
name: declared.name,
|
|
344
335
|
mediaType: declared.mediaType ?? undefined,
|
|
345
|
-
room:
|
|
336
|
+
room: authority.authority,
|
|
346
337
|
body,
|
|
347
338
|
signal,
|
|
348
339
|
});
|
|
@@ -472,7 +463,7 @@ export async function mountPackage(held, options, dependencies = {}) {
|
|
|
472
463
|
};
|
|
473
464
|
const localReport = join(held.root, "MIGRATION.json");
|
|
474
465
|
await writeFile(localReport, `${JSON.stringify(result, null, 2)}\n`);
|
|
475
|
-
result.transfers = await submitAttachments(session, held,
|
|
466
|
+
result.transfers = await submitAttachments(session, held, authority, plan, signal);
|
|
476
467
|
|
|
477
468
|
const report = `${target.subtree}/${MOUNT_REPORT_NAME}`;
|
|
478
469
|
await writeFile(localReport, `${JSON.stringify(result, null, 2)}\n`);
|
package/src/original.js
CHANGED
|
@@ -133,9 +133,9 @@ function outcomeOf(answer) {
|
|
|
133
133
|
|
|
134
134
|
function grantOf(value, kind) {
|
|
135
135
|
if (value === undefined) return null;
|
|
136
|
-
if (typeof value?.url !== "string"
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
if (typeof value?.url !== "string") refuse("kit_original_grant_rejected", kind);
|
|
137
|
+
if (kind === "download") return { url: value.url };
|
|
138
|
+
if (typeof value.operation !== "string") refuse("kit_original_grant_rejected", kind);
|
|
139
139
|
return { url: value.url, operation: value.operation };
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -148,7 +148,7 @@ function sendBytes(dependencies, grant, body, signal) {
|
|
|
148
148
|
|
|
149
149
|
async function receiveBytes(session, grant, signal) {
|
|
150
150
|
const transferred = await transferBytes(
|
|
151
|
-
{ url: grant.url, method: "GET",
|
|
151
|
+
{ url: grant.url, method: "GET", signal },
|
|
152
152
|
session.dependencies,
|
|
153
153
|
);
|
|
154
154
|
return transferred.bytes;
|
package/src/pacing.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const PACING_INTERVAL_MS = 500;
|
|
2
|
+
|
|
3
|
+
export function pacedLatest(options) {
|
|
4
|
+
const intervalMs = options.intervalMs ?? PACING_INTERVAL_MS;
|
|
5
|
+
const now = options.now ?? (() => Date.now());
|
|
6
|
+
let timer = null;
|
|
7
|
+
let sentAt = null;
|
|
8
|
+
let sent = null;
|
|
9
|
+
let latest = null;
|
|
10
|
+
let closed = false;
|
|
11
|
+
|
|
12
|
+
function write() {
|
|
13
|
+
timer = null;
|
|
14
|
+
const serialized = JSON.stringify(latest);
|
|
15
|
+
if (latest === null || serialized === sent) return;
|
|
16
|
+
sent = serialized;
|
|
17
|
+
sentAt = now();
|
|
18
|
+
options.send(latest);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
observe(value) {
|
|
23
|
+
latest = value;
|
|
24
|
+
if (closed || timer !== null) return;
|
|
25
|
+
const wait = sentAt === null ? 0 : sentAt + intervalMs - now();
|
|
26
|
+
if (wait <= 0) {
|
|
27
|
+
write();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
timer = setTimeout(write, wait);
|
|
31
|
+
timer.unref?.();
|
|
32
|
+
},
|
|
33
|
+
latest: () => latest,
|
|
34
|
+
close() {
|
|
35
|
+
closed = true;
|
|
36
|
+
if (timer === null) return;
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
timer = null;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|