@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/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
  }