@agentshouse/kit 0.1.0-alpha.1 → 0.1.0-alpha.10
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 +86 -6
- package/bin/connect-linux.sh +58 -12
- package/bin/kit.js +1 -1
- package/bootstrap.json +17 -0
- package/package.json +6 -2
- package/src/agents.js +63 -8
- package/src/contract.js +3 -3
- package/src/house.js +197 -7
- package/src/kit-work-stream.js +84 -0
- package/src/mount.js +7 -7
- package/src/setup.js +7 -2
- package/src/source.js +10 -6
- package/src/sync-state.js +4 -0
- package/src/sync.js +11 -6
- package/src/telegram.js +24 -8
- package/src/worker.js +197 -25
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,
|
|
@@ -54,11 +57,12 @@ import {
|
|
|
54
57
|
reportRunOutcome,
|
|
55
58
|
reportSessionHeartbeat,
|
|
56
59
|
reportSessions,
|
|
60
|
+
serveResident,
|
|
57
61
|
sessionAuthority,
|
|
58
62
|
streamAssistantDraft,
|
|
59
63
|
} from "./house.js";
|
|
60
64
|
import { operationId } from "./operation.js";
|
|
61
|
-
import { runSetup } from "./setup.js";
|
|
65
|
+
import { runSetup, signInWantedAgent } from "./setup.js";
|
|
62
66
|
import { readSettlements, removeSettlement, writeSettlement } from "./settlement.js";
|
|
63
67
|
import { failedReport, terminalReport } from "./terminal.js";
|
|
64
68
|
import {
|
|
@@ -666,26 +670,33 @@ async function afterEpochReport(context, pass) {
|
|
|
666
670
|
return pass();
|
|
667
671
|
}
|
|
668
672
|
|
|
669
|
-
async function workerPass(context, dependencies) {
|
|
670
|
-
const {
|
|
673
|
+
async function workerPass(context, dependencies, baseConfiguration) {
|
|
674
|
+
const { session, signal } = context;
|
|
671
675
|
const reconciled = await reconcile(context);
|
|
672
676
|
if (reconciled.run !== null || reconciled.unresolved.length > 0) return reconciled;
|
|
677
|
+
context.busy = false;
|
|
678
|
+
const needsLogin = await queueAgentRefresh(context, dependencies, signal, baseConfiguration);
|
|
679
|
+
context.startLogins?.(needsLogin);
|
|
680
|
+
context.busy = true;
|
|
681
|
+
const { configuration } = context;
|
|
673
682
|
context.inbound = await reconcileInbound(context.directory, session);
|
|
674
683
|
const setup = await acquireSetup(session, signal);
|
|
675
684
|
if (setup !== null) {
|
|
676
685
|
context.setup = await runSetup(
|
|
677
|
-
{
|
|
686
|
+
{
|
|
687
|
+
session,
|
|
688
|
+
configuration,
|
|
689
|
+
directory: context.directory,
|
|
690
|
+
dependencies,
|
|
691
|
+
signal,
|
|
692
|
+
waitAgentLogin: context.waitAgentLogin,
|
|
693
|
+
},
|
|
678
694
|
setup.request,
|
|
679
695
|
);
|
|
696
|
+
await context.recordSetupLogins?.(context.setup);
|
|
697
|
+
const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
698
|
+
context.startLogins?.(needsLogin);
|
|
680
699
|
}
|
|
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
700
|
const resumption = await acquireOneResumption(session);
|
|
690
701
|
if (resumption !== null) return { ...reconciled, run: await superviseResumption(context, resumption) };
|
|
691
702
|
const assignment = await acquireOneRun(session);
|
|
@@ -698,7 +709,7 @@ async function workerContext(options, dependencies) {
|
|
|
698
709
|
const runtime = workerRuntime(options.directory, dependencies.tmux ?? "tmux");
|
|
699
710
|
const launcher = dependencies.launcher ?? [process.execPath, LAUNCHER_MAIN];
|
|
700
711
|
const tmux = await readyTmux(runtime);
|
|
701
|
-
const session = await openSession(options, dependencies);
|
|
712
|
+
const session = await openSession({ ...options, acquire: true }, dependencies);
|
|
702
713
|
const context = {
|
|
703
714
|
configuration,
|
|
704
715
|
runtime,
|
|
@@ -709,15 +720,59 @@ async function workerContext(options, dependencies) {
|
|
|
709
720
|
directory: options.directory,
|
|
710
721
|
files: { originals: [], failures: [] },
|
|
711
722
|
epochReport: null,
|
|
723
|
+
agentRefresh: Promise.resolve(),
|
|
724
|
+
configurationPath: options.configuration,
|
|
725
|
+
busy: false,
|
|
726
|
+
deferredRemoval: false,
|
|
712
727
|
};
|
|
713
728
|
return { context, tmux };
|
|
714
729
|
}
|
|
715
730
|
|
|
731
|
+
async function refreshAgentCapabilities(context, dependencies, signal, configuration, pointer = false) {
|
|
732
|
+
const base = configuration ?? (await readConfiguration(context.configurationPath));
|
|
733
|
+
const busy = context.busy || (await readBindings(context.directory)).length > 0;
|
|
734
|
+
const result = await reconcileAgentKinds(
|
|
735
|
+
{ directory: context.directory },
|
|
736
|
+
context.session,
|
|
737
|
+
base,
|
|
738
|
+
dependencies,
|
|
739
|
+
signal,
|
|
740
|
+
{
|
|
741
|
+
pointer,
|
|
742
|
+
busy,
|
|
743
|
+
lastDesired: context.lastDesired,
|
|
744
|
+
onDesired: (wanted) => context.cancelLogins?.(wanted),
|
|
745
|
+
},
|
|
746
|
+
);
|
|
747
|
+
context.deferredRemoval = result.retained.length > 0;
|
|
748
|
+
context.configuration = result.selected;
|
|
749
|
+
context.agents = result.updated;
|
|
750
|
+
context.lastDesired = [...result.selected.agents];
|
|
751
|
+
if (result.refused !== null) throw result.refused;
|
|
752
|
+
return result.reported.filter(
|
|
753
|
+
(agent) =>
|
|
754
|
+
result.selected.agents.includes(agent.kind) && agent.release !== null && !agent.signed_in,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function queueAgentRefresh(context, dependencies, signal, configuration, pointer = false) {
|
|
759
|
+
const refreshed = context.agentRefresh.then(() =>
|
|
760
|
+
refreshAgentCapabilities(context, dependencies, signal, configuration, pointer),
|
|
761
|
+
);
|
|
762
|
+
context.agentRefresh = refreshed.catch(() => {});
|
|
763
|
+
return refreshed;
|
|
764
|
+
}
|
|
765
|
+
|
|
716
766
|
async function controlledPass(context, dependencies, configuration, signal) {
|
|
717
|
-
context.configuration = { ...configuration, agents: await desiredAgents(context.session, signal) };
|
|
718
767
|
context.signal = signal;
|
|
719
768
|
context.session.signal = signal;
|
|
720
|
-
|
|
769
|
+
context.busy = true;
|
|
770
|
+
try {
|
|
771
|
+
return await afterEpochReport(context, () => workerPass(context, dependencies, configuration));
|
|
772
|
+
} finally {
|
|
773
|
+
context.busy = false;
|
|
774
|
+
if (context.deferredRemoval) context.onIdle?.();
|
|
775
|
+
}
|
|
721
776
|
}
|
|
722
777
|
|
|
723
778
|
export async function runWorkers(options, dependencies = {}) {
|
|
@@ -740,17 +795,134 @@ export async function runWorkers(options, dependencies = {}) {
|
|
|
740
795
|
};
|
|
741
796
|
}
|
|
742
797
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
798
|
+
function residentWake() {
|
|
799
|
+
let pending = false;
|
|
800
|
+
let release = null;
|
|
801
|
+
return {
|
|
802
|
+
notify() {
|
|
803
|
+
pending = true;
|
|
804
|
+
release?.();
|
|
805
|
+
},
|
|
806
|
+
async wait(milliseconds, signal) {
|
|
807
|
+
if (pending) {
|
|
808
|
+
pending = false;
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const pause = new AbortController();
|
|
749
812
|
try {
|
|
750
|
-
await
|
|
813
|
+
await Promise.race([
|
|
814
|
+
new Promise((resolve) => {
|
|
815
|
+
release = resolve;
|
|
816
|
+
}),
|
|
817
|
+
delay(milliseconds, undefined, { signal: AbortSignal.any([signal, pause.signal]) }),
|
|
818
|
+
]);
|
|
751
819
|
} catch (error) {
|
|
752
820
|
if (!signal.aborted) throw error;
|
|
821
|
+
} finally {
|
|
822
|
+
release = null;
|
|
823
|
+
pending = false;
|
|
824
|
+
pause.abort();
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export async function runResident(options, dependencies = {}) {
|
|
831
|
+
const { context } = await workerContext(options, dependencies);
|
|
832
|
+
await maintainControl(context.session, async (signal) => {
|
|
833
|
+
const closeResident = await serveResident(context.session, context.directory, signal);
|
|
834
|
+
context.busy = true;
|
|
835
|
+
const wake = residentWake();
|
|
836
|
+
const watcher = new AbortController();
|
|
837
|
+
let failure = null;
|
|
838
|
+
const logins = new Map();
|
|
839
|
+
const attemptedLogins = new Map();
|
|
840
|
+
context.cancelLogins = (wanted) => {
|
|
841
|
+
for (const [kind, running] of logins) {
|
|
842
|
+
if (!wanted.includes(kind)) running.controller.abort();
|
|
843
|
+
}
|
|
844
|
+
for (const kind of attemptedLogins.keys()) {
|
|
845
|
+
if (!wanted.includes(kind)) attemptedLogins.delete(kind);
|
|
753
846
|
}
|
|
847
|
+
};
|
|
848
|
+
context.recordSetupLogins = async (modules) => {
|
|
849
|
+
for (const module of modules) {
|
|
850
|
+
if (module.category !== "agent" || module.status !== "needs_attention") continue;
|
|
851
|
+
const release = (await readAgentState(context.directory, module.name)).release;
|
|
852
|
+
if (release !== null) attemptedLogins.set(module.name, release);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
context.startLogins = (agents) => {
|
|
856
|
+
for (const agent of agents) {
|
|
857
|
+
const { kind, release } = agent;
|
|
858
|
+
if (logins.has(kind) || attemptedLogins.get(kind) === release) continue;
|
|
859
|
+
const controller = new AbortController();
|
|
860
|
+
const loginSignal = AbortSignal.any([signal, controller.signal]);
|
|
861
|
+
const task = (async () => {
|
|
862
|
+
if ((await acquireSetup(context.session, loginSignal)) !== null) return;
|
|
863
|
+
attemptedLogins.set(kind, release);
|
|
864
|
+
await signInWantedAgent({ ...context, dependencies, signal: loginSignal }, kind);
|
|
865
|
+
await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
866
|
+
})()
|
|
867
|
+
.catch((error) => {
|
|
868
|
+
if (loginSignal.aborted) return;
|
|
869
|
+
failure = error;
|
|
870
|
+
wake.notify();
|
|
871
|
+
})
|
|
872
|
+
.finally(() => logins.delete(kind));
|
|
873
|
+
logins.set(kind, { controller, task });
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
context.waitAgentLogin = async (kind) => {
|
|
877
|
+
await logins.get(kind)?.task;
|
|
878
|
+
};
|
|
879
|
+
let pointerPending = false;
|
|
880
|
+
let pointerTask = null;
|
|
881
|
+
const pointer = () => {
|
|
882
|
+
pointerPending = true;
|
|
883
|
+
if (pointerTask !== null) return;
|
|
884
|
+
pointerTask = (async () => {
|
|
885
|
+
while (pointerPending && !signal.aborted) {
|
|
886
|
+
pointerPending = false;
|
|
887
|
+
const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
888
|
+
context.startLogins(needsLogin);
|
|
889
|
+
}
|
|
890
|
+
})()
|
|
891
|
+
.catch((error) => {
|
|
892
|
+
if (signal.aborted) return;
|
|
893
|
+
failure = error;
|
|
894
|
+
wake.notify();
|
|
895
|
+
})
|
|
896
|
+
.finally(() => {
|
|
897
|
+
pointerTask = null;
|
|
898
|
+
if (pointerPending && !signal.aborted) pointer();
|
|
899
|
+
});
|
|
900
|
+
};
|
|
901
|
+
context.onIdle = pointer;
|
|
902
|
+
const watching = (dependencies.watchKitAgentWork ?? watchKitAgentWork)(
|
|
903
|
+
context.session,
|
|
904
|
+
AbortSignal.any([signal, watcher.signal]),
|
|
905
|
+
pointer,
|
|
906
|
+
dependencies,
|
|
907
|
+
).catch((error) => {
|
|
908
|
+
failure = error;
|
|
909
|
+
wake.notify();
|
|
910
|
+
});
|
|
911
|
+
try {
|
|
912
|
+
while (!signal.aborted) {
|
|
913
|
+
if (failure !== null) throw failure;
|
|
914
|
+
const configuration = await readConfiguration(options.configuration);
|
|
915
|
+
await controlledPass(context, dependencies, configuration, signal);
|
|
916
|
+
await wake.wait(dependencies.residentIntervalMs ?? 30_000, signal);
|
|
917
|
+
}
|
|
918
|
+
} finally {
|
|
919
|
+
watcher.abort();
|
|
920
|
+
await watching;
|
|
921
|
+
if (pointerTask !== null) await pointerTask;
|
|
922
|
+
for (const running of logins.values()) running.controller.abort();
|
|
923
|
+
await Promise.all([...logins.values()].map((running) => running.task));
|
|
924
|
+
await context.agentRefresh;
|
|
925
|
+
await closeResident();
|
|
754
926
|
}
|
|
755
927
|
}, options.signal);
|
|
756
928
|
}
|