@agentshouse/kit 0.1.0-alpha.6 → 0.1.0-alpha.8
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 +51 -2
- package/bin/connect-linux.sh +9 -2
- package/bootstrap.json +17 -0
- package/package.json +5 -1
- package/src/agents.js +63 -8
- package/src/contract.js +1 -1
- package/src/kit-work-stream.js +84 -0
- package/src/setup.js +7 -2
- package/src/worker.js +193 -24
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ On Linux `amd64` or `arm64` with a working Docker Engine, run the published,
|
|
|
10
10
|
versioned bootstrap once:
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
13
|
+
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.8/connect-linux.sh)
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The release asset pins one immutable multi-architecture image digest. It creates
|
|
@@ -23,6 +23,43 @@ another House and is retained in Kit home. A repeated invocation reuses the
|
|
|
23
23
|
same container, credential and Environment; a different installed image is
|
|
24
24
|
left for the explicit update journey.
|
|
25
25
|
|
|
26
|
+
The versioned `bootstrap.json` is the machine-readable command source for this
|
|
27
|
+
release. It is exported from the npm package as `@agentshouse/kit/bootstrap`
|
|
28
|
+
(JSON import) and published byte-for-byte beside `connect-linux.sh` in the
|
|
29
|
+
GitHub release. Its schema version is `1`, its `version` is the Kit package
|
|
30
|
+
version, and `hosts` contains only bootstrap scripts published in that release.
|
|
31
|
+
The package exports only the `./bootstrap` subpath; `./package.json` and source
|
|
32
|
+
subpaths are not exported. For this release, only `hosts.linux` exists. In this
|
|
33
|
+
schema example, `<version>` stands for the package's version:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"schemaVersion": 1,
|
|
38
|
+
"version": "<version>",
|
|
39
|
+
"hosts": {
|
|
40
|
+
"linux": {
|
|
41
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v<version>/connect-linux.sh)",
|
|
42
|
+
"asset": {
|
|
43
|
+
"name": "connect-linux.sh",
|
|
44
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v<version>/connect-linux.sh"
|
|
45
|
+
},
|
|
46
|
+
"workspace": { "argument": "--workspace", "quoting": "posix" }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`command` is complete for the default workspace. To show a selected absolute
|
|
53
|
+
workspace, append the declared `workspace.argument` and the path as one POSIX
|
|
54
|
+
shell-quoted argument. For example, `/home/user/My Agents' House` becomes
|
|
55
|
+
`--workspace '/home/user/My Agents'\'' House'`. Quote the entire path before
|
|
56
|
+
adding it to a copyable shell command; never interpolate it as shell syntax.
|
|
57
|
+
The bootstrap validates an absolute workspace root and encodes it as one Docker
|
|
58
|
+
`--mount` CSV source field, including commas and double quotes in the name.
|
|
59
|
+
The package manifest contains no image digest: the release workflow
|
|
60
|
+
fills the immutable image digest into the published script after the package
|
|
61
|
+
has been built.
|
|
62
|
+
|
|
26
63
|
The bootstrap does not install native Kit on an ordinary computer. The native
|
|
27
64
|
package remains for managed Nodes and an explicitly selected eligible Kit host.
|
|
28
65
|
|
|
@@ -48,7 +85,8 @@ Runtime dependencies stay minimal and exactly pinned in `package.json` and
|
|
|
48
85
|
`package-lock.json`: House's own `@agentshouse/mdmodel`, `yaml`, which reads
|
|
49
86
|
House's text answers, and the Agent Client Protocol SDK
|
|
50
87
|
`@agentclientprotocol/sdk` with its peer `zod`, which is this Kit's one protocol
|
|
51
|
-
client for the compiled adapter contracts
|
|
88
|
+
client for the compiled adapter contracts, and `ws` for the authenticated Kit
|
|
89
|
+
control stream. The SDK's approval
|
|
52
90
|
receipt is [`docs/agents/acp-sdk-dependency-approval.json`](docs/agents/acp-sdk-dependency-approval.json),
|
|
53
91
|
which the User wrote once this adoption's proof had passed. The adapter
|
|
54
92
|
packages themselves are Environment prerequisites, not Kit dependencies, and
|
|
@@ -575,6 +613,17 @@ minimum's major - under
|
|
|
575
613
|
`$HOUSE_KIT_HOME/agents/<kind>/releases/<release>/`, never touching what the
|
|
576
614
|
owner installed globally, and an acquired route's command is that installed
|
|
577
615
|
executable rather than a path anyone writes into `kit.json`.
|
|
616
|
+
The resident Kit also listens for an `agents` availability pointer on its
|
|
617
|
+
control stream. It reads the whole wanted list from House over HTTPS, removes
|
|
618
|
+
Kit-owned CLI directories that are no longer wanted, installs newly wanted
|
|
619
|
+
kinds, and reports its held state immediately, including while another Agent
|
|
620
|
+
run remains active. A newly wanted kind that needs provider authorization uses
|
|
621
|
+
the existing guided House login ceremony; Kit reports readiness again when it
|
|
622
|
+
ends. A reconnect reads the list again, so a pointer missed during a stream
|
|
623
|
+
outage changes no authority.
|
|
624
|
+
Removal waits for an active worker pass or any unresolved live WorkerSession
|
|
625
|
+
binding to stop using a CLI directory; the Kit keeps reporting that kind until
|
|
626
|
+
it removes the directory.
|
|
578
627
|
|
|
579
628
|
Every candidate release is proved before it serves: the Kit installs it beside
|
|
580
629
|
the current release, runs the contract's own conformance check - the protocol's
|
package/bin/connect-linux.sh
CHANGED
|
@@ -14,6 +14,11 @@ refuse() {
|
|
|
14
14
|
exit 1
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
bind_mount() {
|
|
18
|
+
local source=${1//\"/\"\"}
|
|
19
|
+
printf 'type=bind,"src=%s",dst=%s' "$source" "$2"
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
while (($#)); do
|
|
18
23
|
case "$1" in
|
|
19
24
|
--workspace|--house)
|
|
@@ -65,6 +70,8 @@ for path in "$KIT_HOME" "$WORKSPACE"; do
|
|
|
65
70
|
done
|
|
66
71
|
KIT_HOME=$(cd "$KIT_HOME" && pwd -P)
|
|
67
72
|
WORKSPACE=$(cd "$WORKSPACE" && pwd -P)
|
|
73
|
+
KIT_HOME_MOUNT=$(bind_mount "$KIT_HOME" /kit-home)
|
|
74
|
+
WORKSPACE_MOUNT=$(bind_mount "$WORKSPACE" /agents/house)
|
|
68
75
|
if [[ -f "$KIT_HOME/credential" || -f "$KIT_HOME/environment" ]] && [[ ! -f "$KIT_HOME/house" ]]; then
|
|
69
76
|
refuse 'stored House origin is missing for an enrolled Environment'
|
|
70
77
|
fi
|
|
@@ -76,7 +83,7 @@ if [[ -f "$KIT_HOME/house" ]]; then
|
|
|
76
83
|
HOUSE="$BOUND_HOUSE"
|
|
77
84
|
fi
|
|
78
85
|
[[ "$HOUSE" == https://* || "$HOUSE" == http://127.0.0.1:* || "$HOUSE" == http://localhost:* ]] || refuse 'House origin must use HTTPS or local loopback HTTP'
|
|
79
|
-
DOCKER_RUN=(docker run --rm --network host --user "$(id -u):$(id -g)" --mount "
|
|
86
|
+
DOCKER_RUN=(docker run --rm --network host --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 --env "HOUSE_KIT_HOSTNAME=$(hostname)" "$IMAGE")
|
|
80
87
|
|
|
81
88
|
if docker container inspect "$NAME" >/dev/null 2>&1; then
|
|
82
89
|
[[ "$(docker inspect --format '{{.Config.Image}}' "$NAME")" == "$IMAGE" ]] || refuse 'an installed Kit uses a different image; update it explicitly'
|
|
@@ -123,5 +130,5 @@ else
|
|
|
123
130
|
fi
|
|
124
131
|
|
|
125
132
|
"${DOCKER_RUN[@]}" apply --house "$HOUSE"
|
|
126
|
-
docker run -d --name "$NAME" --restart unless-stopped --user "$(id -u):$(id -g)" --mount "
|
|
133
|
+
docker run -d --name "$NAME" --restart unless-stopped --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --label "agentshouse.house=$HOUSE" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 "$IMAGE" resident --house "$HOUSE" >/dev/null
|
|
127
134
|
printf 'House Kit connected for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
|
package/bootstrap.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"version": "0.1.0-alpha.8",
|
|
4
|
+
"hosts": {
|
|
5
|
+
"linux": {
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.8/connect-linux.sh)",
|
|
7
|
+
"asset": {
|
|
8
|
+
"name": "connect-linux.sh",
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.8/connect-linux.sh"
|
|
10
|
+
},
|
|
11
|
+
"workspace": {
|
|
12
|
+
"argument": "--workspace",
|
|
13
|
+
"quoting": "posix"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentshouse/kit",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -11,9 +11,13 @@
|
|
|
11
11
|
"bin": {
|
|
12
12
|
"kit": "bin/kit.js"
|
|
13
13
|
},
|
|
14
|
+
"exports": {
|
|
15
|
+
"./bootstrap": "./bootstrap.json"
|
|
16
|
+
},
|
|
14
17
|
"dependencies": {
|
|
15
18
|
"@agentclientprotocol/sdk": "1.5.0",
|
|
16
19
|
"@agentshouse/mdmodel": "0.1.0-alpha.13",
|
|
20
|
+
"ws": "8.22.0",
|
|
17
21
|
"yaml": "2.9.1",
|
|
18
22
|
"zod": "4.6.5"
|
|
19
23
|
},
|
package/src/agents.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { readFile, readdir, rm } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "./adapter.js";
|
|
12
12
|
import { inheritedEnvironment } from "./command.js";
|
|
13
13
|
import {
|
|
14
|
+
ADAPTER_KINDS,
|
|
14
15
|
AGENT_HOME,
|
|
15
16
|
AGENT_INSTALL_TIMEOUT_MS,
|
|
16
17
|
AGENT_RELEASES_HOME,
|
|
@@ -261,6 +262,60 @@ export async function updateDeclaredAgents(options, configuration, dependencies
|
|
|
261
262
|
return updated;
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
export async function removeUndeclaredAgents(directory, wanted, keep = false) {
|
|
266
|
+
const home = join(directory, AGENT_HOME);
|
|
267
|
+
let entries;
|
|
268
|
+
try {
|
|
269
|
+
entries = await readdir(home, { withFileTypes: true });
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error.code === "ENOENT") return [];
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
const retained = [];
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
if (!wanted.includes(entry.name)) {
|
|
277
|
+
if (keep && ADAPTER_KINDS.includes(entry.name)) retained.push(entry.name);
|
|
278
|
+
else await rm(join(home, entry.name), { recursive: true, force: true });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return retained;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function reconcileAgentKinds(
|
|
285
|
+
options,
|
|
286
|
+
session,
|
|
287
|
+
configuration,
|
|
288
|
+
dependencies = {},
|
|
289
|
+
signal = session.signal,
|
|
290
|
+
state = {},
|
|
291
|
+
) {
|
|
292
|
+
const selected = { ...configuration, agents: await desiredAgents(session, signal) };
|
|
293
|
+
state.onDesired?.(selected.agents);
|
|
294
|
+
const newlyWanted = selected.agents.filter((kind) => !state.lastDesired?.includes(kind));
|
|
295
|
+
const retained = await removeUndeclaredAgents(options.directory, selected.agents, state.busy);
|
|
296
|
+
const pointerKinds = state.busy
|
|
297
|
+
? (
|
|
298
|
+
await Promise.all(
|
|
299
|
+
newlyWanted.map(async (kind) =>
|
|
300
|
+
(await readAgentState(options.directory, kind)).release === null ? kind : null,
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
).filter((kind) => kind !== null)
|
|
304
|
+
: newlyWanted;
|
|
305
|
+
const updated = await updateDeclaredAgents(
|
|
306
|
+
{ directory: options.directory, at: dependencies.now?.(), forced: state.forced },
|
|
307
|
+
{ ...selected, agents: state.pointer ? pointerKinds : selected.agents },
|
|
308
|
+
dependencies,
|
|
309
|
+
);
|
|
310
|
+
const readiness = await agentReadiness(
|
|
311
|
+
{ directory: options.directory },
|
|
312
|
+
{ ...selected, agents: [...selected.agents, ...retained] },
|
|
313
|
+
dependencies,
|
|
314
|
+
);
|
|
315
|
+
await reportAgentCapabilities(session, readiness.agents, signal);
|
|
316
|
+
return { selected, updated, reported: readiness.agents, refused: readiness.refused, retained };
|
|
317
|
+
}
|
|
318
|
+
|
|
264
319
|
export async function agentSignedIn(directory, kind, dependencies = {}) {
|
|
265
320
|
const contract = compiledContract(kind);
|
|
266
321
|
const state = await readAgentState(directory, kind);
|
|
@@ -341,15 +396,15 @@ export async function updateAgents(options, dependencies = {}) {
|
|
|
341
396
|
return maintainControl(
|
|
342
397
|
session,
|
|
343
398
|
async (signal) => {
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
399
|
+
const result = await reconcileAgentKinds(
|
|
400
|
+
options,
|
|
401
|
+
session,
|
|
402
|
+
configuration,
|
|
348
403
|
dependencies,
|
|
404
|
+
signal,
|
|
405
|
+
{ forced: true },
|
|
349
406
|
);
|
|
350
|
-
|
|
351
|
-
await reportAgentCapabilities(session, agents, signal);
|
|
352
|
-
return { updated, reported: agents };
|
|
407
|
+
return { updated: result.updated, reported: result.reported };
|
|
353
408
|
},
|
|
354
409
|
options.signal,
|
|
355
410
|
);
|
package/src/contract.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CLAUDE_PASTED_CODE, CODEX_DEVICE_CODE, GROK_DEVICE_CODE } from "./agent-logins.js";
|
|
2
2
|
|
|
3
|
-
export const KIT_CONTRACT_VERSION = "2026-09-27-
|
|
3
|
+
export const KIT_CONTRACT_VERSION = "2026-09-27-wanted-agent-clis";
|
|
4
4
|
export const KIT_CAPTURE_BATCH_MAX = 100;
|
|
5
5
|
export const COMMAND_INPUT_MAX_BYTES = 4 * 1024;
|
|
6
6
|
export const AUTHORIZATION_OUTPUT_MAX_BYTES = 4 * 1024;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
import { KIT_CONTRACT_VERSION } from "./contract.js";
|
|
4
|
+
import { heldControl } from "./house.js";
|
|
5
|
+
import { KitRefusal } from "./refusal.js";
|
|
6
|
+
|
|
7
|
+
const PROTOCOL = "house.kit.stream.1";
|
|
8
|
+
|
|
9
|
+
function streamAddress(house) {
|
|
10
|
+
const url = new URL("/kit/stream", house);
|
|
11
|
+
if (url.protocol === "https:") url.protocol = "wss:";
|
|
12
|
+
else if (url.protocol === "http:") url.protocol = "ws:";
|
|
13
|
+
else throw new KitRefusal("kit_house_unavailable", "/kit/stream");
|
|
14
|
+
return url;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function connected(session, signal, available, dependencies) {
|
|
18
|
+
const authorization = await heldControl(session, signal);
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const Socket = dependencies.workSocket ?? WebSocket;
|
|
21
|
+
const socket = new Socket(streamAddress(session.house), PROTOCOL, {
|
|
22
|
+
headers: { authorization, "x-house-kit-version": KIT_CONTRACT_VERSION },
|
|
23
|
+
handshakeTimeout: 10_000,
|
|
24
|
+
maxPayload: 4096,
|
|
25
|
+
perMessageDeflate: false,
|
|
26
|
+
});
|
|
27
|
+
let finished = false;
|
|
28
|
+
const finish = (error = null) => {
|
|
29
|
+
if (finished) return;
|
|
30
|
+
finished = true;
|
|
31
|
+
signal.removeEventListener("abort", stop);
|
|
32
|
+
if (error === null) resolve();
|
|
33
|
+
else reject(error);
|
|
34
|
+
};
|
|
35
|
+
const stop = () => {
|
|
36
|
+
socket.terminate();
|
|
37
|
+
finish();
|
|
38
|
+
};
|
|
39
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
40
|
+
socket.on("open", available);
|
|
41
|
+
socket.on("message", (data) => {
|
|
42
|
+
let frame;
|
|
43
|
+
try {
|
|
44
|
+
frame = JSON.parse(data.toString());
|
|
45
|
+
} catch {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (frame?.type === "work_available" && frame.subject === "agents") available();
|
|
49
|
+
});
|
|
50
|
+
socket.on("unexpected-response", (_request, response) => {
|
|
51
|
+
response.resume();
|
|
52
|
+
const refusal =
|
|
53
|
+
response.statusCode === 409 ? new KitRefusal("kit_upgrade_required") : null;
|
|
54
|
+
socket.terminate();
|
|
55
|
+
finish(refusal);
|
|
56
|
+
});
|
|
57
|
+
socket.on("error", () => {
|
|
58
|
+
socket.terminate();
|
|
59
|
+
finish();
|
|
60
|
+
});
|
|
61
|
+
socket.on("close", () => finish());
|
|
62
|
+
if (signal.aborted) stop();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function watchKitAgentWork(session, signal, available, dependencies = {}) {
|
|
67
|
+
let retryMs = 500;
|
|
68
|
+
while (!signal.aborted) {
|
|
69
|
+
const began = Date.now();
|
|
70
|
+
try {
|
|
71
|
+
await connected(session, signal, available, dependencies);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof KitRefusal && error.code === "kit_upgrade_required") throw error;
|
|
74
|
+
if (signal.aborted) return;
|
|
75
|
+
}
|
|
76
|
+
if (signal.aborted) return;
|
|
77
|
+
retryMs = Date.now() - began >= 30_000 ? 1000 : Math.min(retryMs * 2, 30_000);
|
|
78
|
+
try {
|
|
79
|
+
await delay(dependencies.workReconnectMs ?? retryMs, undefined, { signal });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (!signal.aborted) throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/setup.js
CHANGED
|
@@ -114,14 +114,14 @@ async function signedIn(module) {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
async function setUpModule(context, module) {
|
|
117
|
+
async function setUpModule(context, module, install = true) {
|
|
118
118
|
const summary = (status, reason = null) => ({
|
|
119
119
|
category: module.category,
|
|
120
120
|
name: module.name,
|
|
121
121
|
status,
|
|
122
122
|
reason,
|
|
123
123
|
});
|
|
124
|
-
if (module.install !== null) {
|
|
124
|
+
if (install && module.install !== null) {
|
|
125
125
|
try {
|
|
126
126
|
await module.install();
|
|
127
127
|
} catch (error) {
|
|
@@ -144,10 +144,15 @@ async function setUpModule(context, module) {
|
|
|
144
144
|
return summary("ready");
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
export function signInWantedAgent(context, kind) {
|
|
148
|
+
return setUpModule(context, agentModule(context, kind), false);
|
|
149
|
+
}
|
|
150
|
+
|
|
147
151
|
export async function runSetup(context, request) {
|
|
148
152
|
await prepareLaunchBase(launchBase(context.dependencies));
|
|
149
153
|
const modules = [];
|
|
150
154
|
for (const kind of context.configuration.agents) {
|
|
155
|
+
await context.waitAgentLogin?.(kind);
|
|
151
156
|
modules.push(await setUpModule(context, agentModule(context, kind)));
|
|
152
157
|
}
|
|
153
158
|
for (const instance of context.configuration.instances) {
|
package/src/worker.js
CHANGED
|
@@ -2,7 +2,11 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
installedAgentCommand,
|
|
7
|
+
readAgentState,
|
|
8
|
+
reconcileAgentKinds,
|
|
9
|
+
} from "./agents.js";
|
|
6
10
|
import { readBindings, removeBinding, writeBinding } from "./binding.js";
|
|
7
11
|
import { readContinuation, readContinuationRuns, writeContinuation, removeContinuation } from "./continuation.js";
|
|
8
12
|
import { openBridge } from "./bridge.js";
|
|
@@ -12,6 +16,7 @@ import { openAssistantDrafts } from "./draft.js";
|
|
|
12
16
|
import { localReply, localRequest, reconcileInbound } from "./inbound.js";
|
|
13
17
|
import { localFiles } from "./local-files.js";
|
|
14
18
|
import { inheritedEnvironment } from "./command.js";
|
|
19
|
+
import { watchKitAgentWork } from "./kit-work-stream.js";
|
|
15
20
|
import { readConfiguration } from "./configuration.js";
|
|
16
21
|
import { launchBase, readyLaunchDirectory } from "./directory.js";
|
|
17
22
|
import {
|
|
@@ -31,7 +36,6 @@ import {
|
|
|
31
36
|
acceptAssistantText,
|
|
32
37
|
acquireCommands,
|
|
33
38
|
acquireSetup,
|
|
34
|
-
reportAgentCapabilities,
|
|
35
39
|
acquireOneRun,
|
|
36
40
|
acquireOneResumption,
|
|
37
41
|
admitInteraction,
|
|
@@ -40,7 +44,6 @@ import {
|
|
|
40
44
|
rejectResumption,
|
|
41
45
|
callHouseMcp,
|
|
42
46
|
commitRun,
|
|
43
|
-
desiredAgents,
|
|
44
47
|
maintainControl,
|
|
45
48
|
mutatingCall,
|
|
46
49
|
observeSessionStart,
|
|
@@ -58,7 +61,7 @@ import {
|
|
|
58
61
|
streamAssistantDraft,
|
|
59
62
|
} from "./house.js";
|
|
60
63
|
import { operationId } from "./operation.js";
|
|
61
|
-
import { runSetup } from "./setup.js";
|
|
64
|
+
import { runSetup, signInWantedAgent } from "./setup.js";
|
|
62
65
|
import { readSettlements, removeSettlement, writeSettlement } from "./settlement.js";
|
|
63
66
|
import { failedReport, terminalReport } from "./terminal.js";
|
|
64
67
|
import {
|
|
@@ -666,26 +669,33 @@ async function afterEpochReport(context, pass) {
|
|
|
666
669
|
return pass();
|
|
667
670
|
}
|
|
668
671
|
|
|
669
|
-
async function workerPass(context, dependencies) {
|
|
670
|
-
const {
|
|
672
|
+
async function workerPass(context, dependencies, baseConfiguration) {
|
|
673
|
+
const { session, signal } = context;
|
|
671
674
|
const reconciled = await reconcile(context);
|
|
672
675
|
if (reconciled.run !== null || reconciled.unresolved.length > 0) return reconciled;
|
|
676
|
+
context.busy = false;
|
|
677
|
+
const needsLogin = await queueAgentRefresh(context, dependencies, signal, baseConfiguration);
|
|
678
|
+
context.startLogins?.(needsLogin);
|
|
679
|
+
context.busy = true;
|
|
680
|
+
const { configuration } = context;
|
|
673
681
|
context.inbound = await reconcileInbound(context.directory, session);
|
|
674
682
|
const setup = await acquireSetup(session, signal);
|
|
675
683
|
if (setup !== null) {
|
|
676
684
|
context.setup = await runSetup(
|
|
677
|
-
{
|
|
685
|
+
{
|
|
686
|
+
session,
|
|
687
|
+
configuration,
|
|
688
|
+
directory: context.directory,
|
|
689
|
+
dependencies,
|
|
690
|
+
signal,
|
|
691
|
+
waitAgentLogin: context.waitAgentLogin,
|
|
692
|
+
},
|
|
678
693
|
setup.request,
|
|
679
694
|
);
|
|
695
|
+
await context.recordSetupLogins?.(context.setup);
|
|
696
|
+
const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
697
|
+
context.startLogins?.(needsLogin);
|
|
680
698
|
}
|
|
681
|
-
context.agents = await updateDeclaredAgents(
|
|
682
|
-
{ directory: context.directory, at: dependencies.now?.() },
|
|
683
|
-
configuration,
|
|
684
|
-
dependencies,
|
|
685
|
-
);
|
|
686
|
-
const readiness = await agentReadiness({ directory: context.directory }, configuration, dependencies);
|
|
687
|
-
await reportAgentCapabilities(session, readiness.agents);
|
|
688
|
-
if (readiness.refused !== null) throw readiness.refused;
|
|
689
699
|
const resumption = await acquireOneResumption(session);
|
|
690
700
|
if (resumption !== null) return { ...reconciled, run: await superviseResumption(context, resumption) };
|
|
691
701
|
const assignment = await acquireOneRun(session);
|
|
@@ -709,15 +719,59 @@ async function workerContext(options, dependencies) {
|
|
|
709
719
|
directory: options.directory,
|
|
710
720
|
files: { originals: [], failures: [] },
|
|
711
721
|
epochReport: null,
|
|
722
|
+
agentRefresh: Promise.resolve(),
|
|
723
|
+
configurationPath: options.configuration,
|
|
724
|
+
busy: false,
|
|
725
|
+
deferredRemoval: false,
|
|
712
726
|
};
|
|
713
727
|
return { context, tmux };
|
|
714
728
|
}
|
|
715
729
|
|
|
730
|
+
async function refreshAgentCapabilities(context, dependencies, signal, configuration, pointer = false) {
|
|
731
|
+
const base = configuration ?? (await readConfiguration(context.configurationPath));
|
|
732
|
+
const busy = context.busy || (await readBindings(context.directory)).length > 0;
|
|
733
|
+
const result = await reconcileAgentKinds(
|
|
734
|
+
{ directory: context.directory },
|
|
735
|
+
context.session,
|
|
736
|
+
base,
|
|
737
|
+
dependencies,
|
|
738
|
+
signal,
|
|
739
|
+
{
|
|
740
|
+
pointer,
|
|
741
|
+
busy,
|
|
742
|
+
lastDesired: context.lastDesired,
|
|
743
|
+
onDesired: (wanted) => context.cancelLogins?.(wanted),
|
|
744
|
+
},
|
|
745
|
+
);
|
|
746
|
+
context.deferredRemoval = result.retained.length > 0;
|
|
747
|
+
context.configuration = result.selected;
|
|
748
|
+
context.agents = result.updated;
|
|
749
|
+
context.lastDesired = [...result.selected.agents];
|
|
750
|
+
if (result.refused !== null) throw result.refused;
|
|
751
|
+
return result.reported.filter(
|
|
752
|
+
(agent) =>
|
|
753
|
+
result.selected.agents.includes(agent.kind) && agent.release !== null && !agent.signed_in,
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function queueAgentRefresh(context, dependencies, signal, configuration, pointer = false) {
|
|
758
|
+
const refreshed = context.agentRefresh.then(() =>
|
|
759
|
+
refreshAgentCapabilities(context, dependencies, signal, configuration, pointer),
|
|
760
|
+
);
|
|
761
|
+
context.agentRefresh = refreshed.catch(() => {});
|
|
762
|
+
return refreshed;
|
|
763
|
+
}
|
|
764
|
+
|
|
716
765
|
async function controlledPass(context, dependencies, configuration, signal) {
|
|
717
|
-
context.configuration = { ...configuration, agents: await desiredAgents(context.session, signal) };
|
|
718
766
|
context.signal = signal;
|
|
719
767
|
context.session.signal = signal;
|
|
720
|
-
|
|
768
|
+
context.busy = true;
|
|
769
|
+
try {
|
|
770
|
+
return await afterEpochReport(context, () => workerPass(context, dependencies, configuration));
|
|
771
|
+
} finally {
|
|
772
|
+
context.busy = false;
|
|
773
|
+
if (context.deferredRemoval) context.onIdle?.();
|
|
774
|
+
}
|
|
721
775
|
}
|
|
722
776
|
|
|
723
777
|
export async function runWorkers(options, dependencies = {}) {
|
|
@@ -740,17 +794,132 @@ export async function runWorkers(options, dependencies = {}) {
|
|
|
740
794
|
};
|
|
741
795
|
}
|
|
742
796
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
797
|
+
function residentWake() {
|
|
798
|
+
let pending = false;
|
|
799
|
+
let release = null;
|
|
800
|
+
return {
|
|
801
|
+
notify() {
|
|
802
|
+
pending = true;
|
|
803
|
+
release?.();
|
|
804
|
+
},
|
|
805
|
+
async wait(milliseconds, signal) {
|
|
806
|
+
if (pending) {
|
|
807
|
+
pending = false;
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const pause = new AbortController();
|
|
749
811
|
try {
|
|
750
|
-
await
|
|
812
|
+
await Promise.race([
|
|
813
|
+
new Promise((resolve) => {
|
|
814
|
+
release = resolve;
|
|
815
|
+
}),
|
|
816
|
+
delay(milliseconds, undefined, { signal: AbortSignal.any([signal, pause.signal]) }),
|
|
817
|
+
]);
|
|
751
818
|
} catch (error) {
|
|
752
819
|
if (!signal.aborted) throw error;
|
|
820
|
+
} finally {
|
|
821
|
+
release = null;
|
|
822
|
+
pending = false;
|
|
823
|
+
pause.abort();
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export async function runResident(options, dependencies = {}) {
|
|
830
|
+
const { context } = await workerContext(options, dependencies);
|
|
831
|
+
await maintainControl(context.session, async (signal) => {
|
|
832
|
+
context.busy = true;
|
|
833
|
+
const wake = residentWake();
|
|
834
|
+
const watcher = new AbortController();
|
|
835
|
+
let failure = null;
|
|
836
|
+
const logins = new Map();
|
|
837
|
+
const attemptedLogins = new Map();
|
|
838
|
+
context.cancelLogins = (wanted) => {
|
|
839
|
+
for (const [kind, running] of logins) {
|
|
840
|
+
if (!wanted.includes(kind)) running.controller.abort();
|
|
841
|
+
}
|
|
842
|
+
for (const kind of attemptedLogins.keys()) {
|
|
843
|
+
if (!wanted.includes(kind)) attemptedLogins.delete(kind);
|
|
753
844
|
}
|
|
845
|
+
};
|
|
846
|
+
context.recordSetupLogins = async (modules) => {
|
|
847
|
+
for (const module of modules) {
|
|
848
|
+
if (module.category !== "agent" || module.status !== "needs_attention") continue;
|
|
849
|
+
const release = (await readAgentState(context.directory, module.name)).release;
|
|
850
|
+
if (release !== null) attemptedLogins.set(module.name, release);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
context.startLogins = (agents) => {
|
|
854
|
+
for (const agent of agents) {
|
|
855
|
+
const { kind, release } = agent;
|
|
856
|
+
if (logins.has(kind) || attemptedLogins.get(kind) === release) continue;
|
|
857
|
+
const controller = new AbortController();
|
|
858
|
+
const loginSignal = AbortSignal.any([signal, controller.signal]);
|
|
859
|
+
const task = (async () => {
|
|
860
|
+
if ((await acquireSetup(context.session, loginSignal)) !== null) return;
|
|
861
|
+
attemptedLogins.set(kind, release);
|
|
862
|
+
await signInWantedAgent({ ...context, dependencies, signal: loginSignal }, kind);
|
|
863
|
+
await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
864
|
+
})()
|
|
865
|
+
.catch((error) => {
|
|
866
|
+
if (loginSignal.aborted) return;
|
|
867
|
+
failure = error;
|
|
868
|
+
wake.notify();
|
|
869
|
+
})
|
|
870
|
+
.finally(() => logins.delete(kind));
|
|
871
|
+
logins.set(kind, { controller, task });
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
context.waitAgentLogin = async (kind) => {
|
|
875
|
+
await logins.get(kind)?.task;
|
|
876
|
+
};
|
|
877
|
+
let pointerPending = false;
|
|
878
|
+
let pointerTask = null;
|
|
879
|
+
const pointer = () => {
|
|
880
|
+
pointerPending = true;
|
|
881
|
+
if (pointerTask !== null) return;
|
|
882
|
+
pointerTask = (async () => {
|
|
883
|
+
while (pointerPending && !signal.aborted) {
|
|
884
|
+
pointerPending = false;
|
|
885
|
+
const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
886
|
+
context.startLogins(needsLogin);
|
|
887
|
+
}
|
|
888
|
+
})()
|
|
889
|
+
.catch((error) => {
|
|
890
|
+
if (signal.aborted) return;
|
|
891
|
+
failure = error;
|
|
892
|
+
wake.notify();
|
|
893
|
+
})
|
|
894
|
+
.finally(() => {
|
|
895
|
+
pointerTask = null;
|
|
896
|
+
if (pointerPending && !signal.aborted) pointer();
|
|
897
|
+
});
|
|
898
|
+
};
|
|
899
|
+
context.onIdle = pointer;
|
|
900
|
+
const watching = (dependencies.watchKitAgentWork ?? watchKitAgentWork)(
|
|
901
|
+
context.session,
|
|
902
|
+
AbortSignal.any([signal, watcher.signal]),
|
|
903
|
+
pointer,
|
|
904
|
+
dependencies,
|
|
905
|
+
).catch((error) => {
|
|
906
|
+
failure = error;
|
|
907
|
+
wake.notify();
|
|
908
|
+
});
|
|
909
|
+
try {
|
|
910
|
+
while (!signal.aborted) {
|
|
911
|
+
if (failure !== null) throw failure;
|
|
912
|
+
const configuration = await readConfiguration(options.configuration);
|
|
913
|
+
await controlledPass(context, dependencies, configuration, signal);
|
|
914
|
+
await wake.wait(dependencies.residentIntervalMs ?? 30_000, signal);
|
|
915
|
+
}
|
|
916
|
+
} finally {
|
|
917
|
+
watcher.abort();
|
|
918
|
+
await watching;
|
|
919
|
+
if (pointerTask !== null) await pointerTask;
|
|
920
|
+
for (const running of logins.values()) running.controller.abort();
|
|
921
|
+
await Promise.all([...logins.values()].map((running) => running.task));
|
|
922
|
+
await context.agentRefresh;
|
|
754
923
|
}
|
|
755
924
|
}, options.signal);
|
|
756
925
|
}
|