@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/CHANGELOG.md +15 -0
- package/README.md +97 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.js +623 -168
- package/dist/plugin.js.map +1 -1
- package/dist/tui.d.ts +24 -0
- package/dist/tui.js +90 -9
- package/dist/tui.js.map +1 -1
- package/package.json +2 -1
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
|
|
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
|
|
15905
|
-
const
|
|
15906
|
-
|
|
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
|
|
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(
|
|
15921
|
-
|
|
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) =>
|
|
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) ?? [];
|