@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/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, dependencies, signal, summary = emptySummary()) {
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
- const gateway = gatewayContext(session, options, dependencies, options.signal);
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`, options.signal);
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
- const gateway = gatewayContext(session, options, dependencies, options.signal);
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", options.signal);
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, dependencies, shared, summary);
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 { agentReadiness, installedAgentCommand, updateDeclaredAgents } from "./agents.js";
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 { configuration, session, signal } = context;
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
- { session, configuration, directory: context.directory, dependencies, signal },
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
- return afterEpochReport(context, () => workerPass(context, dependencies));
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
- export async function runResident(options, dependencies = {}) {
744
- const { context } = await workerContext(options, dependencies);
745
- await maintainControl(context.session, async (signal) => {
746
- while (!signal.aborted) {
747
- const configuration = await readConfiguration(options.configuration);
748
- await controlledPass(context, dependencies, configuration, signal);
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 delay(dependencies.residentIntervalMs ?? 30_000, undefined, { signal });
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
  }