@agentshouse/kit 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +89 -10
- 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/host.js +6 -2
- package/src/house.js +199 -15
- package/src/kit-work-stream.js +84 -0
- package/src/mount.js +7 -7
- package/src/setup.js +7 -2
- package/src/source.js +44 -30
- 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/telegram.js
CHANGED
|
@@ -955,14 +955,14 @@ async function proveOwner(gateway, token, telegramBotUserId) {
|
|
|
955
955
|
throw new KitRefusal("telegram_owner_not_proven");
|
|
956
956
|
}
|
|
957
957
|
|
|
958
|
-
function gatewayContext(session, options,
|
|
958
|
+
function gatewayContext(session, options, signal, summary = emptySummary()) {
|
|
959
959
|
return {
|
|
960
960
|
session,
|
|
961
961
|
directory: options.directory,
|
|
962
|
-
dependencies,
|
|
962
|
+
dependencies: session.dependencies,
|
|
963
963
|
signal,
|
|
964
964
|
gate: rateGate(() => session.now().getTime()),
|
|
965
|
-
wait: dependencies.wait ?? pause,
|
|
965
|
+
wait: session.dependencies.wait ?? pause,
|
|
966
966
|
passes: options.passes ?? Number.POSITIVE_INFINITY,
|
|
967
967
|
summary,
|
|
968
968
|
};
|
|
@@ -970,7 +970,15 @@ function gatewayContext(session, options, dependencies, signal, summary = emptyS
|
|
|
970
970
|
|
|
971
971
|
export async function connectPersonalBot(options, dependencies = {}) {
|
|
972
972
|
const session = await openSession(options, dependencies);
|
|
973
|
-
|
|
973
|
+
return maintainControl(
|
|
974
|
+
session,
|
|
975
|
+
(signal) => connectBot(session, options, dependencies, signal),
|
|
976
|
+
options.signal,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
async function connectBot(session, options, dependencies, signal) {
|
|
981
|
+
const gateway = gatewayContext(session, options, signal);
|
|
974
982
|
const ask = dependencies.prompt ?? (() => askForToken(dependencies));
|
|
975
983
|
const token = (await ask()).trim();
|
|
976
984
|
if (token.length === 0) throw new KitRefusal("telegram_bot_token_required");
|
|
@@ -989,7 +997,7 @@ export async function connectPersonalBot(options, dependencies = {}) {
|
|
|
989
997
|
owner_telegram_user_id: await proveOwner(gateway, token, identity.telegramBotUserId),
|
|
990
998
|
has_topics_enabled: true,
|
|
991
999
|
};
|
|
992
|
-
await writeOwned(tokenPath(options.directory), `${token}\n`,
|
|
1000
|
+
await writeOwned(tokenPath(options.directory), `${token}\n`, signal);
|
|
993
1001
|
try {
|
|
994
1002
|
await houseCall(gateway, "personal-bot", setup);
|
|
995
1003
|
} catch (error) {
|
|
@@ -1001,7 +1009,15 @@ export async function connectPersonalBot(options, dependencies = {}) {
|
|
|
1001
1009
|
|
|
1002
1010
|
export async function disconnectPersonalBot(options, dependencies = {}) {
|
|
1003
1011
|
const session = await openSession(options, dependencies);
|
|
1004
|
-
|
|
1012
|
+
return maintainControl(
|
|
1013
|
+
session,
|
|
1014
|
+
(signal) => disconnectBot(session, options, signal),
|
|
1015
|
+
options.signal,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async function disconnectBot(session, options, signal) {
|
|
1020
|
+
const gateway = gatewayContext(session, options, signal);
|
|
1005
1021
|
const directory = options.directory;
|
|
1006
1022
|
const marked = await disconnecting(directory);
|
|
1007
1023
|
let token = null;
|
|
@@ -1010,7 +1026,7 @@ export async function disconnectPersonalBot(options, dependencies = {}) {
|
|
|
1010
1026
|
} catch (error) {
|
|
1011
1027
|
if (!marked) throw error;
|
|
1012
1028
|
}
|
|
1013
|
-
if (!marked) await writeOwned(disconnectPath(directory), "{}\n",
|
|
1029
|
+
if (!marked) await writeOwned(disconnectPath(directory), "{}\n", signal);
|
|
1014
1030
|
if (token !== null) {
|
|
1015
1031
|
await telegramCall(gateway, token, "deleteWebhook", { drop_pending_updates: false });
|
|
1016
1032
|
await Promise.all([
|
|
@@ -1032,7 +1048,7 @@ export async function serveTelegramGateway(options, dependencies = {}) {
|
|
|
1032
1048
|
async (signal) => {
|
|
1033
1049
|
const stopping = new AbortController();
|
|
1034
1050
|
const shared = AbortSignal.any([signal, stopping.signal]);
|
|
1035
|
-
const gateway = gatewayContext(session, options,
|
|
1051
|
+
const gateway = gatewayContext(session, options, shared, summary);
|
|
1036
1052
|
summary.abandoned = abandonedOf(await houseCall(gateway, "deliveries/abandoned"));
|
|
1037
1053
|
await serveActivities(gateway, stopping);
|
|
1038
1054
|
},
|
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
|
}
|