@dylanrussell/agent-router 2.1.0 → 2.1.2

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,17 +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,
15907
- attributes: active ? TEXT_ATTR_BOLD : 0,
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 modelRow = (model, active) => text(`${active ? "\u25CF " : " "}${modelLabel(model)}`, {
15924
+ fg: active ? theme.warning : theme.textMuted,
15925
+ attributes: 0,
15908
15926
  wrapMode: "char",
15909
15927
  width: "100%"
15910
15928
  });
@@ -15917,8 +15935,13 @@ function buildSidebarNodes(snapshot, ctx) {
15917
15935
  kind: "box",
15918
15936
  props: { flexDirection: "column", paddingLeft: 2 },
15919
15937
  children: [
15920
- ...chain.map((model, index) => modelRow(model, index === selected)),
15921
- ...current && selected === -1 ? [modelRow(current, true, true)] : []
15938
+ ...chain.map(
15939
+ (model) => modelRow(
15940
+ model,
15941
+ unique.some((selected) => sameModel(model, selected))
15942
+ )
15943
+ ),
15944
+ ...unique.filter((selected) => !chain.some((model) => sameModel(model, selected))).map((selected) => modelRow(selected, true))
15922
15945
  ]
15923
15946
  }
15924
15947
  ]
@@ -15933,6 +15956,7 @@ function buildSidebarNodes(snapshot, ctx) {
15933
15956
 
15934
15957
  // src/tui/index.ts
15935
15958
  var POLL_INTERVAL_MS = 1500;
15959
+ var ROUTING_INTERVAL_MS = 3e3;
15936
15960
  var SIDEBAR_ORDER = 850;
15937
15961
  function debugLog(message) {
15938
15962
  const target = process.env.AGENT_ROUTER_TUI_DEBUG ?? process.env.OMO_TUI_DEBUG;
@@ -16018,16 +16042,25 @@ var tui = async (api) => {
16018
16042
  if (!paths) return;
16019
16043
  let snapshot = await readStackSnapshot(paths);
16020
16044
  const bootActive = snapshot.active;
16045
+ let routing;
16046
+ let routingSessionID;
16047
+ let lastSessionID;
16021
16048
  const viewContext = (sessionID) => ({
16022
16049
  bootActive,
16023
16050
  theme: api.theme?.current,
16024
- current: sessionID ? api.currentModel?.(sessionID) : void 0
16051
+ current: sessionID ? api.currentModel?.(sessionID) : void 0,
16052
+ live: sessionID ? api.liveSelections?.(sessionID) : void 0,
16053
+ defaults: api.configuredModels?.(),
16054
+ routing: sessionID === routingSessionID ? routing : void 0
16025
16055
  });
16026
16056
  try {
16027
16057
  api.slots.register({
16028
16058
  order: SIDEBAR_ORDER,
16029
16059
  slots: {
16030
- sidebar_content: (sessionID) => materialize(buildSidebarNodes(snapshot, viewContext(sessionID)), solid)
16060
+ sidebar_content: (sessionID) => {
16061
+ if (sessionID) lastSessionID = sessionID;
16062
+ return materialize(buildSidebarNodes(snapshot, viewContext(sessionID)), solid);
16063
+ }
16031
16064
  }
16032
16065
  });
16033
16066
  debugLog("slots.register ok");
@@ -16060,6 +16093,20 @@ var tui = async (api) => {
16060
16093
  }
16061
16094
  }
16062
16095
  });
16096
+ const stopRouting = api.routingStatus ? createSidebarPoller({
16097
+ read: async () => {
16098
+ const sessionID = lastSessionID;
16099
+ const status2 = sessionID ? await api.routingStatus?.(sessionID).catch(() => void 0) : void 0;
16100
+ return { routing: status2, sessionID, key: JSON.stringify([sessionID, status2 ?? null]) };
16101
+ },
16102
+ intervalMs: ROUTING_INTERVAL_MS,
16103
+ initial: { routing: void 0, sessionID: void 0, key: "boot" },
16104
+ onChange: (next) => {
16105
+ routing = next.routing;
16106
+ routingSessionID = next.sessionID;
16107
+ api.renderer.requestRender();
16108
+ }
16109
+ }) : void 0;
16063
16110
  const deps = { api, paths, refresh };
16064
16111
  const status = () => api.ui.toast({
16065
16112
  title: "agent-router",
@@ -16073,6 +16120,7 @@ var tui = async (api) => {
16073
16120
  debugLog(`command.register FAILED: ${e.message}`);
16074
16121
  }
16075
16122
  api.lifecycle.onDispose(stopPolling);
16123
+ if (stopRouting) api.lifecycle.onDispose(stopRouting);
16076
16124
  debugLog(`tui() init complete \u2014 active=${snapshot.active ?? "(none)"}`);
16077
16125
  };
16078
16126
  var agentRouterTui = {
@@ -16195,6 +16243,38 @@ var agentRouterTui = {
16195
16243
  variant: session.model.variant ?? null
16196
16244
  };
16197
16245
  },
16246
+ liveSelections: (sessionID) => {
16247
+ const sessions = ctx.data.session;
16248
+ if (typeof sessions.list !== "function" || typeof sessions.status !== "function") return [];
16249
+ try {
16250
+ const parent = sessions.get(sessionID);
16251
+ if (!parent?.location) return [];
16252
+ const selections = [];
16253
+ for (const session of sessions.list()) {
16254
+ if (session.parentID !== sessionID || !session.agent || !session.model) continue;
16255
+ if (session.location?.directory !== parent.location.directory) continue;
16256
+ if (sessions.status(session.id) !== "running") continue;
16257
+ const { agent, model } = session;
16258
+ selections.push({
16259
+ agent,
16260
+ model: `${model.providerID}/${model.id}`,
16261
+ variant: model.variant ?? null
16262
+ });
16263
+ }
16264
+ return selections;
16265
+ } catch {
16266
+ return [];
16267
+ }
16268
+ },
16269
+ configuredModels: () => (ctx.data.location.agent?.list(ctx.location) ?? []).flatMap(
16270
+ (agent) => agent.model ? [
16271
+ {
16272
+ agent: agent.name,
16273
+ model: `${agent.model.providerID}/${agent.model.id}`,
16274
+ variant: agent.model.variant ?? null
16275
+ }
16276
+ ] : []
16277
+ ),
16198
16278
  state: {
16199
16279
  get provider() {
16200
16280
  const models = ctx.data.location.model.list(ctx.location) ?? [];