@dylanrussell/agent-router 2.0.2 → 2.1.1

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/dist/tui.d.ts CHANGED
@@ -26,6 +26,17 @@ interface SidebarTheme {
26
26
  readonly warning?: unknown;
27
27
  readonly success?: unknown;
28
28
  }
29
+ /** Live model selection observed for one agent in a session. */
30
+ interface LiveSelection extends ModelAssignment {
31
+ readonly agent: string;
32
+ }
33
+ /** Routing mode of the viewed session, as reported by the status service. */
34
+ interface RoutingStatus {
35
+ readonly mode: "automatic" | "pinned";
36
+ readonly reason?: string | undefined;
37
+ /** Epoch ms of the last quota evaluation behind the current mode, if tracked. */
38
+ readonly checkedAt?: number | null | undefined;
39
+ }
29
40
  interface SidebarContext {
30
41
  /** Active stack when the TUI booted — differing means a restart is due. */
31
42
  readonly bootActive: string | null;
@@ -34,6 +45,14 @@ interface SidebarContext {
34
45
  readonly current?: (ModelAssignment & {
35
46
  readonly agent: string;
36
47
  }) | undefined;
48
+ /**
49
+ * Selections of running direct children of the viewed session, at its location.
50
+ * Conflicting models are all retained. The viewed session takes precedence.
51
+ */
52
+ readonly live?: readonly LiveSelection[] | undefined;
53
+ readonly defaults?: readonly LiveSelection[] | undefined;
54
+ /** Routing mode of the viewed session; omitted when no status source answers. */
55
+ readonly routing?: RoutingStatus | undefined;
37
56
  }
38
57
 
39
58
  /**
@@ -115,6 +134,11 @@ interface RouterTuiApi {
115
134
  readonly provider?: unknown;
116
135
  };
117
136
  readonly currentModel?: (sessionID: string) => SidebarContext["current"];
137
+ readonly configuredModels?: () => readonly LiveSelection[];
138
+ /** Running direct-child selections at the viewed parent's location; retain conflicts. */
139
+ readonly liveSelections?: ((sessionID: string) => readonly LiveSelection[]) | undefined;
140
+ /** Read-only routing-status query; undefined means no status source is available. */
141
+ readonly routingStatus?: ((sessionID: string) => Promise<RoutingStatus | undefined>) | undefined;
118
142
  }
119
143
 
120
144
  /**
package/dist/tui.js CHANGED
@@ -15872,6 +15872,9 @@ function normalizeVariant(variant) {
15872
15872
  function sameModel(a, b) {
15873
15873
  return a.model === b.model && normalizeVariant(a.variant) === normalizeVariant(b.variant);
15874
15874
  }
15875
+ function quotaAge(checkedAt, now = Date.now()) {
15876
+ return `${Math.max(0, Math.round((now - checkedAt) / 1e3))}s ago`;
15877
+ }
15875
15878
  function buildSidebarNodes(snapshot, ctx) {
15876
15879
  const theme = ctx.theme ?? {};
15877
15880
  const nodes = [text("Agent Stacks", { fg: theme.text, attributes: TEXT_ATTR_BOLD })];
@@ -15894,16 +15897,32 @@ function buildSidebarNodes(snapshot, ctx) {
15894
15897
  marginTop: 1
15895
15898
  })
15896
15899
  );
15900
+ if (ctx.routing) {
15901
+ const parts = [`Routing \xB7 ${ctx.routing.mode === "pinned" ? "Pinned" : "Automatic"}`];
15902
+ if (ctx.routing.reason) parts.push(ctx.routing.reason);
15903
+ parts.push(
15904
+ ctx.routing.checkedAt != null ? `quota ${quotaAge(ctx.routing.checkedAt)}` : "quota freshness unknown"
15905
+ );
15906
+ nodes.push(text(parts.join(" \xB7 "), { fg: theme.textMuted }));
15907
+ } else {
15908
+ nodes.push(text("Routing \xB7 unknown \xB7 freshness unknown", { fg: theme.textMuted }));
15909
+ }
15897
15910
  if (snapshot.agents.length === 0) {
15898
15911
  nodes.push(text("\u2022 (none)", { fg: theme.textMuted }));
15899
15912
  } else {
15900
- nodes.push(text("Precedence \u2193 \xB7 \u25CF current", { fg: theme.textMuted }));
15913
+ nodes.push(text("Precedence \u2193 \xB7 \u25CF selected", { fg: theme.textMuted }));
15901
15914
  for (const assignment of snapshot.agents) {
15902
15915
  const chain = [assignment, ...assignment.fallbacks ?? []];
15903
15916
  const current = ctx.current?.agent === assignment.agent ? ctx.current : void 0;
15904
- const selected = current ? chain.findIndex((model) => sameModel(model, current)) : -1;
15905
- const modelRow = (model, active, external = false) => text(`${active ? "\u25CF " : " "}${external ? "Current \xB7 " : ""}${modelLabel(model)}`, {
15906
- fg: active ? theme.success : theme.textMuted,
15917
+ const children = (ctx.live ?? []).filter((entry) => entry.agent === assignment.agent);
15918
+ const configured = ctx.defaults?.find((entry) => entry.agent === assignment.agent);
15919
+ const selections = current ? [current] : children.length ? children : [configured ?? assignment];
15920
+ const unique = selections.filter(
15921
+ (entry, index) => selections.findIndex((other) => sameModel(entry, other)) === index
15922
+ );
15923
+ const source = current ? "viewed session" : children.length ? `active children${unique.length > 1 ? " \xB7 multiple models" : ""}` : configured ? "native default" : "stack default";
15924
+ const modelRow = (model, active) => text(`${active ? "\u25CF " : " "}${modelLabel(model)}`, {
15925
+ fg: active ? theme.warning : theme.textMuted,
15907
15926
  attributes: active ? TEXT_ATTR_BOLD : 0,
15908
15927
  wrapMode: "char",
15909
15928
  width: "100%"
@@ -15912,13 +15931,18 @@ function buildSidebarNodes(snapshot, ctx) {
15912
15931
  kind: "box",
15913
15932
  props: { flexDirection: "column", marginTop: 1 },
15914
15933
  children: [
15915
- text(assignment.agent, { fg: theme.text, attributes: TEXT_ATTR_BOLD }),
15934
+ text(`${assignment.agent} \xB7 ${source}`, { fg: theme.text, attributes: TEXT_ATTR_BOLD }),
15916
15935
  {
15917
15936
  kind: "box",
15918
15937
  props: { flexDirection: "column", paddingLeft: 2 },
15919
15938
  children: [
15920
- ...chain.map((model, index) => modelRow(model, index === selected)),
15921
- ...current && selected === -1 ? [modelRow(current, true, true)] : []
15939
+ ...chain.map(
15940
+ (model) => modelRow(
15941
+ model,
15942
+ unique.some((selected) => sameModel(model, selected))
15943
+ )
15944
+ ),
15945
+ ...unique.filter((selected) => !chain.some((model) => sameModel(model, selected))).map((selected) => modelRow(selected, true))
15922
15946
  ]
15923
15947
  }
15924
15948
  ]
@@ -15933,6 +15957,7 @@ function buildSidebarNodes(snapshot, ctx) {
15933
15957
 
15934
15958
  // src/tui/index.ts
15935
15959
  var POLL_INTERVAL_MS = 1500;
15960
+ var ROUTING_INTERVAL_MS = 3e3;
15936
15961
  var SIDEBAR_ORDER = 850;
15937
15962
  function debugLog(message) {
15938
15963
  const target = process.env.AGENT_ROUTER_TUI_DEBUG ?? process.env.OMO_TUI_DEBUG;
@@ -16018,16 +16043,25 @@ var tui = async (api) => {
16018
16043
  if (!paths) return;
16019
16044
  let snapshot = await readStackSnapshot(paths);
16020
16045
  const bootActive = snapshot.active;
16046
+ let routing;
16047
+ let routingSessionID;
16048
+ let lastSessionID;
16021
16049
  const viewContext = (sessionID) => ({
16022
16050
  bootActive,
16023
16051
  theme: api.theme?.current,
16024
- current: sessionID ? api.currentModel?.(sessionID) : void 0
16052
+ current: sessionID ? api.currentModel?.(sessionID) : void 0,
16053
+ live: sessionID ? api.liveSelections?.(sessionID) : void 0,
16054
+ defaults: api.configuredModels?.(),
16055
+ routing: sessionID === routingSessionID ? routing : void 0
16025
16056
  });
16026
16057
  try {
16027
16058
  api.slots.register({
16028
16059
  order: SIDEBAR_ORDER,
16029
16060
  slots: {
16030
- sidebar_content: (sessionID) => materialize(buildSidebarNodes(snapshot, viewContext(sessionID)), solid)
16061
+ sidebar_content: (sessionID) => {
16062
+ if (sessionID) lastSessionID = sessionID;
16063
+ return materialize(buildSidebarNodes(snapshot, viewContext(sessionID)), solid);
16064
+ }
16031
16065
  }
16032
16066
  });
16033
16067
  debugLog("slots.register ok");
@@ -16060,6 +16094,20 @@ var tui = async (api) => {
16060
16094
  }
16061
16095
  }
16062
16096
  });
16097
+ const stopRouting = api.routingStatus ? createSidebarPoller({
16098
+ read: async () => {
16099
+ const sessionID = lastSessionID;
16100
+ const status2 = sessionID ? await api.routingStatus?.(sessionID).catch(() => void 0) : void 0;
16101
+ return { routing: status2, sessionID, key: JSON.stringify([sessionID, status2 ?? null]) };
16102
+ },
16103
+ intervalMs: ROUTING_INTERVAL_MS,
16104
+ initial: { routing: void 0, sessionID: void 0, key: "boot" },
16105
+ onChange: (next) => {
16106
+ routing = next.routing;
16107
+ routingSessionID = next.sessionID;
16108
+ api.renderer.requestRender();
16109
+ }
16110
+ }) : void 0;
16063
16111
  const deps = { api, paths, refresh };
16064
16112
  const status = () => api.ui.toast({
16065
16113
  title: "agent-router",
@@ -16073,6 +16121,7 @@ var tui = async (api) => {
16073
16121
  debugLog(`command.register FAILED: ${e.message}`);
16074
16122
  }
16075
16123
  api.lifecycle.onDispose(stopPolling);
16124
+ if (stopRouting) api.lifecycle.onDispose(stopRouting);
16076
16125
  debugLog(`tui() init complete \u2014 active=${snapshot.active ?? "(none)"}`);
16077
16126
  };
16078
16127
  var agentRouterTui = {
@@ -16195,6 +16244,38 @@ var agentRouterTui = {
16195
16244
  variant: session.model.variant ?? null
16196
16245
  };
16197
16246
  },
16247
+ liveSelections: (sessionID) => {
16248
+ const sessions = ctx.data.session;
16249
+ if (typeof sessions.list !== "function" || typeof sessions.status !== "function") return [];
16250
+ try {
16251
+ const parent = sessions.get(sessionID);
16252
+ if (!parent?.location) return [];
16253
+ const selections = [];
16254
+ for (const session of sessions.list()) {
16255
+ if (session.parentID !== sessionID || !session.agent || !session.model) continue;
16256
+ if (session.location?.directory !== parent.location.directory) continue;
16257
+ if (sessions.status(session.id) !== "running") continue;
16258
+ const { agent, model } = session;
16259
+ selections.push({
16260
+ agent,
16261
+ model: `${model.providerID}/${model.id}`,
16262
+ variant: model.variant ?? null
16263
+ });
16264
+ }
16265
+ return selections;
16266
+ } catch {
16267
+ return [];
16268
+ }
16269
+ },
16270
+ configuredModels: () => (ctx.data.location.agent?.list(ctx.location) ?? []).flatMap(
16271
+ (agent) => agent.model ? [
16272
+ {
16273
+ agent: agent.name,
16274
+ model: `${agent.model.providerID}/${agent.model.id}`,
16275
+ variant: agent.model.variant ?? null
16276
+ }
16277
+ ] : []
16278
+ ),
16198
16279
  state: {
16199
16280
  get provider() {
16200
16281
  const models = ctx.data.location.model.list(ctx.location) ?? [];