@agentshouse/kit 0.1.0-alpha.6 → 0.1.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/package.json +2 -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.7/connect-linux.sh)
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The release asset pins one immutable multi-architecture image digest. It creates
|
|
@@ -48,7 +48,8 @@ Runtime dependencies stay minimal and exactly pinned in `package.json` and
|
|
|
48
48
|
`package-lock.json`: House's own `@agentshouse/mdmodel`, `yaml`, which reads
|
|
49
49
|
House's text answers, and the Agent Client Protocol SDK
|
|
50
50
|
`@agentclientprotocol/sdk` with its peer `zod`, which is this Kit's one protocol
|
|
51
|
-
client for the compiled adapter contracts
|
|
51
|
+
client for the compiled adapter contracts, and `ws` for the authenticated Kit
|
|
52
|
+
control stream. The SDK's approval
|
|
52
53
|
receipt is [`docs/agents/acp-sdk-dependency-approval.json`](docs/agents/acp-sdk-dependency-approval.json),
|
|
53
54
|
which the User wrote once this adoption's proof had passed. The adapter
|
|
54
55
|
packages themselves are Environment prerequisites, not Kit dependencies, and
|
|
@@ -575,6 +576,17 @@ minimum's major - under
|
|
|
575
576
|
`$HOUSE_KIT_HOME/agents/<kind>/releases/<release>/`, never touching what the
|
|
576
577
|
owner installed globally, and an acquired route's command is that installed
|
|
577
578
|
executable rather than a path anyone writes into `kit.json`.
|
|
579
|
+
The resident Kit also listens for an `agents` availability pointer on its
|
|
580
|
+
control stream. It reads the whole wanted list from House over HTTPS, removes
|
|
581
|
+
Kit-owned CLI directories that are no longer wanted, installs newly wanted
|
|
582
|
+
kinds, and reports its held state immediately, including while another Agent
|
|
583
|
+
run remains active. A newly wanted kind that needs provider authorization uses
|
|
584
|
+
the existing guided House login ceremony; Kit reports readiness again when it
|
|
585
|
+
ends. A reconnect reads the list again, so a pointer missed during a stream
|
|
586
|
+
outage changes no authority.
|
|
587
|
+
Removal waits for an active worker pass or any unresolved live WorkerSession
|
|
588
|
+
binding to stop using a CLI directory; the Kit keeps reporting that kind until
|
|
589
|
+
it removes the directory.
|
|
578
590
|
|
|
579
591
|
Every candidate release is proved before it serves: the Kit installs it beside
|
|
580
592
|
the current release, runs the contract's own conformance check - the protocol's
|
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.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@agentclientprotocol/sdk": "1.5.0",
|
|
16
16
|
"@agentshouse/mdmodel": "0.1.0-alpha.13",
|
|
17
|
+
"ws": "8.22.0",
|
|
17
18
|
"yaml": "2.9.1",
|
|
18
19
|
"zod": "4.6.5"
|
|
19
20
|
},
|
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
|
}
|