@dylanrussell/agent-router 1.0.6 → 1.1.0
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/README.md +82 -9
- package/dist/cli.js +421 -25
- package/dist/cli.js.map +1 -1
- package/dist/plugin.js +609 -25
- package/dist/plugin.js.map +1 -1
- package/dist/tui.js +411 -16
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -5,6 +5,7 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// src/plugin.ts
|
|
8
|
+
import { readFileSync } from "fs";
|
|
8
9
|
import { tool } from "@opencode-ai/plugin";
|
|
9
10
|
|
|
10
11
|
// src/core/config.ts
|
|
@@ -14618,14 +14619,26 @@ function date4(params) {
|
|
|
14618
14619
|
config(en_default());
|
|
14619
14620
|
|
|
14620
14621
|
// src/core/schema.ts
|
|
14622
|
+
var FallbackSchema = external_exports.object({
|
|
14623
|
+
model: external_exports.string().regex(/^[^/\s]+\/\S+$/, "Expected provider/model"),
|
|
14624
|
+
variant: external_exports.string().min(1).optional()
|
|
14625
|
+
}).strict();
|
|
14626
|
+
var RoutingEntrySchema = external_exports.object({
|
|
14627
|
+
model: external_exports.string().min(1),
|
|
14628
|
+
variant: external_exports.string().min(1).nullable().optional(),
|
|
14629
|
+
fallbacks: external_exports.array(FallbackSchema).max(8).optional()
|
|
14630
|
+
}).strict();
|
|
14621
14631
|
var StateFileSchema = external_exports.object({
|
|
14622
14632
|
version: external_exports.literal(1),
|
|
14623
14633
|
active: external_exports.string().min(1),
|
|
14624
14634
|
previousActive: external_exports.string().min(1).nullable(),
|
|
14625
|
-
lastSwitchedAt: external_exports.string().min(1)
|
|
14635
|
+
lastSwitchedAt: external_exports.string().min(1),
|
|
14636
|
+
fallbackAgents: external_exports.record(external_exports.string(), RoutingEntrySchema).optional()
|
|
14626
14637
|
}).strict();
|
|
14627
14638
|
var AgentEntrySchema = external_exports.object({
|
|
14628
|
-
model: external_exports.string().min(1)
|
|
14639
|
+
model: external_exports.string().min(1),
|
|
14640
|
+
variant: external_exports.string().min(1).nullable().optional(),
|
|
14641
|
+
fallbacks: external_exports.array(FallbackSchema).max(8).optional()
|
|
14629
14642
|
}).passthrough();
|
|
14630
14643
|
var StackFileSchema = external_exports.object({
|
|
14631
14644
|
agents: external_exports.record(external_exports.string(), AgentEntrySchema)
|
|
@@ -14701,12 +14714,455 @@ async function resolvePathsWithConfig(options = {}) {
|
|
|
14701
14714
|
return resolvePaths(next);
|
|
14702
14715
|
}
|
|
14703
14716
|
|
|
14717
|
+
// src/core/failover.ts
|
|
14718
|
+
var Model = external_exports.object({
|
|
14719
|
+
providerID: external_exports.string(),
|
|
14720
|
+
modelID: external_exports.string(),
|
|
14721
|
+
variant: external_exports.string().optional()
|
|
14722
|
+
});
|
|
14723
|
+
var ErrorInfo = external_exports.object({
|
|
14724
|
+
name: external_exports.string(),
|
|
14725
|
+
data: external_exports.object({ statusCode: external_exports.number().optional(), isRetryable: external_exports.boolean().optional() }).passthrough()
|
|
14726
|
+
});
|
|
14727
|
+
var Event = external_exports.object({
|
|
14728
|
+
type: external_exports.string(),
|
|
14729
|
+
properties: external_exports.object({
|
|
14730
|
+
sessionID: external_exports.string().optional(),
|
|
14731
|
+
error: ErrorInfo.optional(),
|
|
14732
|
+
info: external_exports.object({
|
|
14733
|
+
id: external_exports.string().optional(),
|
|
14734
|
+
sessionID: external_exports.string().optional(),
|
|
14735
|
+
role: external_exports.string().optional(),
|
|
14736
|
+
parentID: external_exports.string().optional(),
|
|
14737
|
+
agent: external_exports.string().optional(),
|
|
14738
|
+
providerID: external_exports.string().optional(),
|
|
14739
|
+
modelID: external_exports.string().optional(),
|
|
14740
|
+
variant: external_exports.string().optional(),
|
|
14741
|
+
error: ErrorInfo.optional()
|
|
14742
|
+
}).passthrough().optional()
|
|
14743
|
+
}).passthrough()
|
|
14744
|
+
});
|
|
14745
|
+
function selection(entry) {
|
|
14746
|
+
const slash = entry.model.indexOf("/");
|
|
14747
|
+
return {
|
|
14748
|
+
providerID: entry.model.slice(0, slash),
|
|
14749
|
+
modelID: entry.model.slice(slash + 1),
|
|
14750
|
+
variant: entry.variant ?? void 0
|
|
14751
|
+
};
|
|
14752
|
+
}
|
|
14753
|
+
function same(a, b) {
|
|
14754
|
+
return a.providerID === b.providerID && a.modelID === b.modelID && a.variant === b.variant;
|
|
14755
|
+
}
|
|
14756
|
+
function createFailover(routes, notice) {
|
|
14757
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
14758
|
+
let enabled = true;
|
|
14759
|
+
const chains = new Map(
|
|
14760
|
+
Object.entries(routes).map(([agent, entry]) => {
|
|
14761
|
+
const chain = [entry, ...entry.fallbacks ?? []].map(selection);
|
|
14762
|
+
return [
|
|
14763
|
+
agent,
|
|
14764
|
+
chain.filter((value, i) => !chain.slice(0, i).some((previous) => same(previous, value)))
|
|
14765
|
+
];
|
|
14766
|
+
})
|
|
14767
|
+
);
|
|
14768
|
+
const notify = (message) => notice(`agent-router: ${message}`).catch(() => {
|
|
14769
|
+
});
|
|
14770
|
+
return {
|
|
14771
|
+
disable() {
|
|
14772
|
+
enabled = false;
|
|
14773
|
+
sessions.clear();
|
|
14774
|
+
},
|
|
14775
|
+
async message(input, output) {
|
|
14776
|
+
if (!enabled) return;
|
|
14777
|
+
const message = output.message;
|
|
14778
|
+
const chain = chains.get(message.agent);
|
|
14779
|
+
if (!chain || chain.length < 2) {
|
|
14780
|
+
const existing = sessions.get(input.sessionID);
|
|
14781
|
+
if (existing) {
|
|
14782
|
+
existing.turn = void 0;
|
|
14783
|
+
existing.pending = void 0;
|
|
14784
|
+
}
|
|
14785
|
+
return;
|
|
14786
|
+
}
|
|
14787
|
+
let state = sessions.get(input.sessionID);
|
|
14788
|
+
if (!state) {
|
|
14789
|
+
if (sessions.size >= 1024) return;
|
|
14790
|
+
state = { disabled: false, indices: /* @__PURE__ */ new Map() };
|
|
14791
|
+
sessions.set(input.sessionID, state);
|
|
14792
|
+
}
|
|
14793
|
+
if (state.disabled) return;
|
|
14794
|
+
const current = chain[state.indices.get(message.agent) ?? 0];
|
|
14795
|
+
const primary = chain[0];
|
|
14796
|
+
if (!current || !primary) return;
|
|
14797
|
+
const incoming = message.model;
|
|
14798
|
+
const previous = state.turn?.agent === message.agent ? state.turn : void 0;
|
|
14799
|
+
const pending = previous ? state.pending : void 0;
|
|
14800
|
+
if (!same(incoming, current) && !same(incoming, primary) && !(pending && previous && same(incoming, previous.model))) {
|
|
14801
|
+
state.disabled = true;
|
|
14802
|
+
state.pending = void 0;
|
|
14803
|
+
return;
|
|
14804
|
+
}
|
|
14805
|
+
state.pending = void 0;
|
|
14806
|
+
if (pending) message.model = { ...pending };
|
|
14807
|
+
else message.model = { ...current };
|
|
14808
|
+
state.turn = {
|
|
14809
|
+
agent: message.agent,
|
|
14810
|
+
parent: message.id,
|
|
14811
|
+
model: { ...message.model },
|
|
14812
|
+
handled: false
|
|
14813
|
+
};
|
|
14814
|
+
if (pending)
|
|
14815
|
+
await notify(
|
|
14816
|
+
`[${input.sessionID}] selected ${pending.providerID}/${pending.modelID} for this new turn. No previous prompt or tool was replayed.`
|
|
14817
|
+
);
|
|
14818
|
+
},
|
|
14819
|
+
async event(input) {
|
|
14820
|
+
if (!enabled) return;
|
|
14821
|
+
const parsed = Event.safeParse(input.event);
|
|
14822
|
+
if (!parsed.success) return;
|
|
14823
|
+
const { type, properties: p } = parsed.data;
|
|
14824
|
+
const sessionID = p.sessionID ?? p.info?.sessionID ?? (type === "session.deleted" ? p.info?.id : void 0);
|
|
14825
|
+
if (!sessionID) return;
|
|
14826
|
+
const state = sessions.get(sessionID);
|
|
14827
|
+
if (!state || state.disabled) return;
|
|
14828
|
+
if (type === "session.deleted" || type === "session.next.model.switched" || type === "session.next.agent.switched" || type === "session.error" && p.error?.name === "MessageAbortedError") {
|
|
14829
|
+
state.disabled = true;
|
|
14830
|
+
state.pending = void 0;
|
|
14831
|
+
return;
|
|
14832
|
+
}
|
|
14833
|
+
if (type !== "message.updated") return;
|
|
14834
|
+
const info = p.info;
|
|
14835
|
+
const turn = state.turn;
|
|
14836
|
+
if (!info || !turn || info.role !== "assistant" || info.parentID !== turn.parent || info.agent !== turn.agent || info.providerID !== turn.model.providerID || info.modelID !== turn.model.modelID || !info.error)
|
|
14837
|
+
return;
|
|
14838
|
+
if (info.error.name === "MessageAbortedError") {
|
|
14839
|
+
state.disabled = true;
|
|
14840
|
+
state.pending = void 0;
|
|
14841
|
+
return;
|
|
14842
|
+
}
|
|
14843
|
+
if (turn.handled) return;
|
|
14844
|
+
turn.handled = true;
|
|
14845
|
+
const error51 = info.error;
|
|
14846
|
+
const status = error51.data.statusCode;
|
|
14847
|
+
if (error51.name !== "APIError" || !error51.data.isRetryable || status === void 0 || ![429, 500, 502, 503, 504].includes(status))
|
|
14848
|
+
return;
|
|
14849
|
+
const chain = chains.get(turn.agent);
|
|
14850
|
+
if (!chain) return;
|
|
14851
|
+
const index = (state.indices.get(turn.agent) ?? 0) + 1;
|
|
14852
|
+
const next = chain[index];
|
|
14853
|
+
if (!next) {
|
|
14854
|
+
await notify(
|
|
14855
|
+
`[${sessionID}] fallback chain exhausted for ${turn.agent}. Choose a model manually; no retry was sent.`
|
|
14856
|
+
);
|
|
14857
|
+
return;
|
|
14858
|
+
}
|
|
14859
|
+
state.indices.set(turn.agent, index);
|
|
14860
|
+
state.pending = next;
|
|
14861
|
+
await notify(
|
|
14862
|
+
`[${sessionID}] ${turn.agent} failed (HTTP ${status}). Next user turn will use ${next.providerID}/${next.modelID}${next.variant ? ` (${next.variant})` : ""}. Send an explicit retry/continue instruction after reviewing partial output and completed tools. Nothing was retried automatically.`
|
|
14863
|
+
);
|
|
14864
|
+
}
|
|
14865
|
+
};
|
|
14866
|
+
}
|
|
14867
|
+
|
|
14704
14868
|
// src/core/frontmatter.ts
|
|
14705
14869
|
import { existsSync as existsSync2 } from "fs";
|
|
14706
14870
|
import { readFile as readFile2, readdir } from "fs/promises";
|
|
14707
14871
|
import path3 from "path";
|
|
14872
|
+
|
|
14873
|
+
// src/core/yaml-lite.ts
|
|
14874
|
+
var RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
14875
|
+
"null",
|
|
14876
|
+
"true",
|
|
14877
|
+
"false",
|
|
14878
|
+
"yes",
|
|
14879
|
+
"no",
|
|
14880
|
+
"on",
|
|
14881
|
+
"off",
|
|
14882
|
+
"~",
|
|
14883
|
+
// YAML 1.1 nulls
|
|
14884
|
+
"Null",
|
|
14885
|
+
"NULL",
|
|
14886
|
+
"True",
|
|
14887
|
+
"False",
|
|
14888
|
+
"Yes",
|
|
14889
|
+
"No",
|
|
14890
|
+
"On",
|
|
14891
|
+
"Off"
|
|
14892
|
+
]);
|
|
14893
|
+
var BARE_STRING_RE = /^[A-Za-z0-9._\-\/+@$%^&()~]+$/;
|
|
14894
|
+
function needsQuoting(s) {
|
|
14895
|
+
if (s === "") return true;
|
|
14896
|
+
if (RESERVED_WORDS.has(s)) return true;
|
|
14897
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(s)) return true;
|
|
14898
|
+
if (/^0x[0-9A-Fa-f]+$/.test(s)) return true;
|
|
14899
|
+
if (/^[+-]?\.(inf|Inf|INF|nan|NaN|NAN)$/.test(s)) return true;
|
|
14900
|
+
if (!BARE_STRING_RE.test(s)) return true;
|
|
14901
|
+
return false;
|
|
14902
|
+
}
|
|
14903
|
+
function serializeOptionValue(value) {
|
|
14904
|
+
if (value === null || value === void 0) return "null";
|
|
14905
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
14906
|
+
if (typeof value === "number") {
|
|
14907
|
+
if (!Number.isFinite(value)) return String(value);
|
|
14908
|
+
return JSON.stringify(value);
|
|
14909
|
+
}
|
|
14910
|
+
if (typeof value === "bigint") return String(value);
|
|
14911
|
+
if (typeof value === "string") return needsQuoting(value) ? JSON.stringify(value) : value;
|
|
14912
|
+
return JSON.stringify(value);
|
|
14913
|
+
}
|
|
14914
|
+
function stripComment(line) {
|
|
14915
|
+
let inSingle = false;
|
|
14916
|
+
let inDouble = false;
|
|
14917
|
+
for (let i = 0; i < line.length; i++) {
|
|
14918
|
+
const ch = line[i];
|
|
14919
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
14920
|
+
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
14921
|
+
else if (ch === "#" && !inSingle && !inDouble) {
|
|
14922
|
+
if (i === 0 || /\s/.test(line[i - 1] ?? "")) return line.slice(0, i);
|
|
14923
|
+
}
|
|
14924
|
+
}
|
|
14925
|
+
return line;
|
|
14926
|
+
}
|
|
14927
|
+
function tokenize(block) {
|
|
14928
|
+
const out = [];
|
|
14929
|
+
for (const raw of block.split(/\r?\n/)) {
|
|
14930
|
+
if (raw.trim() === "") continue;
|
|
14931
|
+
const indentMatch = /^( *)/.exec(raw);
|
|
14932
|
+
const indent = indentMatch?.[1]?.length ?? 0;
|
|
14933
|
+
const content = stripComment(raw.slice(indent));
|
|
14934
|
+
if (content.trim() === "") continue;
|
|
14935
|
+
out.push({ indent, text: content.trimEnd(), raw });
|
|
14936
|
+
}
|
|
14937
|
+
return out;
|
|
14938
|
+
}
|
|
14939
|
+
function coerceScalar(raw) {
|
|
14940
|
+
const v = raw.trim();
|
|
14941
|
+
if (v === "" || v === "null" || v === "~" || v === "Null" || v === "NULL") return null;
|
|
14942
|
+
if (v === "true" || v === "True" || v === "TRUE") return true;
|
|
14943
|
+
if (v === "false" || v === "False" || v === "FALSE") return false;
|
|
14944
|
+
if (v === "yes" || v === "Yes" || v === "YES") return true;
|
|
14945
|
+
if (v === "no" || v === "No" || v === "NO") return false;
|
|
14946
|
+
if (v === "on" || v === "On" || v === "ON") return true;
|
|
14947
|
+
if (v === "off" || v === "Off" || v === "OFF") return false;
|
|
14948
|
+
if (/^[+-]?\d+$/.test(v)) return Number.parseInt(v, 10);
|
|
14949
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(v)) return Number.parseFloat(v);
|
|
14950
|
+
if (/^0x[0-9A-Fa-f]+$/.test(v)) return Number.parseInt(v, 16);
|
|
14951
|
+
return v;
|
|
14952
|
+
}
|
|
14953
|
+
function parseQuoted(s) {
|
|
14954
|
+
const quote = s[0] ?? "";
|
|
14955
|
+
if (quote !== '"' && quote !== "'") return null;
|
|
14956
|
+
if (quote === '"') {
|
|
14957
|
+
let out2 = "";
|
|
14958
|
+
for (let i = 1; i < s.length; i++) {
|
|
14959
|
+
const ch = s[i] ?? "";
|
|
14960
|
+
if (ch === "\\" && i + 1 < s.length) {
|
|
14961
|
+
const next = s[i + 1] ?? "";
|
|
14962
|
+
const map2 = {
|
|
14963
|
+
n: "\n",
|
|
14964
|
+
t: " ",
|
|
14965
|
+
r: "\r",
|
|
14966
|
+
'"': '"',
|
|
14967
|
+
"\\": "\\",
|
|
14968
|
+
"/": "/",
|
|
14969
|
+
b: "\b",
|
|
14970
|
+
f: "\f"
|
|
14971
|
+
};
|
|
14972
|
+
out2 += map2[next] ?? next;
|
|
14973
|
+
i++;
|
|
14974
|
+
} else if (ch === '"') {
|
|
14975
|
+
return { value: out2, rest: s.slice(i + 1) };
|
|
14976
|
+
} else {
|
|
14977
|
+
out2 += ch;
|
|
14978
|
+
}
|
|
14979
|
+
}
|
|
14980
|
+
return null;
|
|
14981
|
+
}
|
|
14982
|
+
let out = "";
|
|
14983
|
+
for (let i = 1; i < s.length; i++) {
|
|
14984
|
+
const ch = s[i] ?? "";
|
|
14985
|
+
if (ch === "'") {
|
|
14986
|
+
if (s[i + 1] === "'") {
|
|
14987
|
+
out += "'";
|
|
14988
|
+
i++;
|
|
14989
|
+
} else {
|
|
14990
|
+
return { value: out, rest: s.slice(i + 1) };
|
|
14991
|
+
}
|
|
14992
|
+
} else {
|
|
14993
|
+
out += ch;
|
|
14994
|
+
}
|
|
14995
|
+
}
|
|
14996
|
+
return null;
|
|
14997
|
+
}
|
|
14998
|
+
function splitFlow(raw) {
|
|
14999
|
+
const parts = [];
|
|
15000
|
+
let depth = 0;
|
|
15001
|
+
let inSingle = false;
|
|
15002
|
+
let inDouble = false;
|
|
15003
|
+
let start = 0;
|
|
15004
|
+
for (let i = 0; i < raw.length; i++) {
|
|
15005
|
+
const ch = raw[i];
|
|
15006
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
15007
|
+
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
15008
|
+
else if (!inSingle && !inDouble) {
|
|
15009
|
+
if (ch === "{" || ch === "[") depth++;
|
|
15010
|
+
else if (ch === "}" || ch === "]") depth--;
|
|
15011
|
+
else if (ch === "," && depth === 0) {
|
|
15012
|
+
parts.push(raw.slice(start, i));
|
|
15013
|
+
start = i + 1;
|
|
15014
|
+
}
|
|
15015
|
+
}
|
|
15016
|
+
}
|
|
15017
|
+
parts.push(raw.slice(start));
|
|
15018
|
+
return parts;
|
|
15019
|
+
}
|
|
15020
|
+
function parseFlowValue(raw) {
|
|
15021
|
+
const s = raw.trim();
|
|
15022
|
+
if (s === "") return null;
|
|
15023
|
+
const first = s[0] ?? "";
|
|
15024
|
+
if (first === "{") {
|
|
15025
|
+
const inner = s.slice(1, s.length - 1);
|
|
15026
|
+
const obj = {};
|
|
15027
|
+
for (const part of splitFlow(inner)) {
|
|
15028
|
+
const t = part.trim();
|
|
15029
|
+
if (t === "") continue;
|
|
15030
|
+
const colon = findColon(t);
|
|
15031
|
+
if (colon < 0) {
|
|
15032
|
+
obj[t] = null;
|
|
15033
|
+
} else {
|
|
15034
|
+
const k = t.slice(0, colon).trim();
|
|
15035
|
+
const v = t.slice(colon + 1).trim();
|
|
15036
|
+
obj[stripKeyQuotes(k)] = parseFlowValue(v);
|
|
15037
|
+
}
|
|
15038
|
+
}
|
|
15039
|
+
return obj;
|
|
15040
|
+
}
|
|
15041
|
+
if (first === "[") {
|
|
15042
|
+
const inner = s.slice(1, s.length - 1);
|
|
15043
|
+
return splitFlow(inner).filter((p) => p.trim() !== "").map((p) => parseFlowValue(p));
|
|
15044
|
+
}
|
|
15045
|
+
if (first === '"' || first === "'") {
|
|
15046
|
+
const q = parseQuoted(s);
|
|
15047
|
+
return q ? q.value : s;
|
|
15048
|
+
}
|
|
15049
|
+
return coerceScalar(s);
|
|
15050
|
+
}
|
|
15051
|
+
function findColon(s) {
|
|
15052
|
+
let inSingle = false;
|
|
15053
|
+
let inDouble = false;
|
|
15054
|
+
let depth = 0;
|
|
15055
|
+
for (let i = 0; i < s.length; i++) {
|
|
15056
|
+
const ch = s[i];
|
|
15057
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
15058
|
+
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
15059
|
+
else if (!inSingle && !inDouble) {
|
|
15060
|
+
if (ch === "{" || ch === "[") depth++;
|
|
15061
|
+
else if (ch === "}" || ch === "]") depth--;
|
|
15062
|
+
else if (ch === ":" && depth === 0) return i;
|
|
15063
|
+
}
|
|
15064
|
+
}
|
|
15065
|
+
return -1;
|
|
15066
|
+
}
|
|
15067
|
+
function stripKeyQuotes(k) {
|
|
15068
|
+
if (k.length >= 2 && (k[0] === '"' && k.at(-1) === '"' || k[0] === "'" && k.at(-1) === "'")) {
|
|
15069
|
+
return k.slice(1, -1);
|
|
15070
|
+
}
|
|
15071
|
+
return k;
|
|
15072
|
+
}
|
|
15073
|
+
function parseMapping(lines, startIdx, indent) {
|
|
15074
|
+
const out = {};
|
|
15075
|
+
let i = startIdx;
|
|
15076
|
+
while (i < lines.length) {
|
|
15077
|
+
const line = lines[i];
|
|
15078
|
+
if (!line) break;
|
|
15079
|
+
if (line.indent < indent) break;
|
|
15080
|
+
if (line.indent > indent) {
|
|
15081
|
+
i++;
|
|
15082
|
+
continue;
|
|
15083
|
+
}
|
|
15084
|
+
const colon = findColon(line.text);
|
|
15085
|
+
if (colon < 0) {
|
|
15086
|
+
i++;
|
|
15087
|
+
continue;
|
|
15088
|
+
}
|
|
15089
|
+
const key = stripKeyQuotes(line.text.slice(0, colon).trim());
|
|
15090
|
+
if (key === "") {
|
|
15091
|
+
i++;
|
|
15092
|
+
continue;
|
|
15093
|
+
}
|
|
15094
|
+
const rest = line.text.slice(colon + 1).trim();
|
|
15095
|
+
if (rest !== "") {
|
|
15096
|
+
out[key] = parseFlowValue(rest);
|
|
15097
|
+
i++;
|
|
15098
|
+
} else {
|
|
15099
|
+
const childIndent = lines[i + 1]?.indent ?? line.indent + 1;
|
|
15100
|
+
let j = i + 1;
|
|
15101
|
+
while (j < lines.length && (lines[j]?.indent ?? -1) >= childIndent) j++;
|
|
15102
|
+
if (j === i + 1) {
|
|
15103
|
+
out[key] = null;
|
|
15104
|
+
i++;
|
|
15105
|
+
} else {
|
|
15106
|
+
const childBlock = lines.slice(i + 1, j);
|
|
15107
|
+
const firstChild = childBlock[0];
|
|
15108
|
+
if (firstChild?.text.trimStart().startsWith("- ")) {
|
|
15109
|
+
out[key] = parseSequence(childBlock, childIndent);
|
|
15110
|
+
} else {
|
|
15111
|
+
out[key] = parseMapping(childBlock, 0, childIndent).value;
|
|
15112
|
+
}
|
|
15113
|
+
i = j;
|
|
15114
|
+
}
|
|
15115
|
+
}
|
|
15116
|
+
}
|
|
15117
|
+
return { value: out, next: i };
|
|
15118
|
+
}
|
|
15119
|
+
function parseSequence(lines, indent) {
|
|
15120
|
+
const out = [];
|
|
15121
|
+
let i = 0;
|
|
15122
|
+
while (i < lines.length) {
|
|
15123
|
+
const line = lines[i];
|
|
15124
|
+
if (!line) break;
|
|
15125
|
+
if (line.indent < indent) break;
|
|
15126
|
+
const t = line.text.trimStart();
|
|
15127
|
+
if (!t.startsWith("- ")) {
|
|
15128
|
+
i++;
|
|
15129
|
+
continue;
|
|
15130
|
+
}
|
|
15131
|
+
const item = t.slice(2).trim();
|
|
15132
|
+
if (item === "") {
|
|
15133
|
+
const childIndent = lines[i + 1]?.indent ?? line.indent + 1;
|
|
15134
|
+
let j = i + 1;
|
|
15135
|
+
while (j < lines.length && (lines[j]?.indent ?? -1) >= childIndent) j++;
|
|
15136
|
+
out.push(parseMapping(lines.slice(i + 1, j), 0, childIndent).value);
|
|
15137
|
+
i = j;
|
|
15138
|
+
} else {
|
|
15139
|
+
out.push(parseFlowValue(item));
|
|
15140
|
+
i++;
|
|
15141
|
+
}
|
|
15142
|
+
}
|
|
15143
|
+
return out;
|
|
15144
|
+
}
|
|
15145
|
+
function parseFrontmatterBlock(block) {
|
|
15146
|
+
const lines = tokenize(block);
|
|
15147
|
+
if (lines.length === 0) return null;
|
|
15148
|
+
const { value } = parseMapping(lines, 0, 0);
|
|
15149
|
+
return value;
|
|
15150
|
+
}
|
|
15151
|
+
|
|
15152
|
+
// src/core/frontmatter.ts
|
|
14708
15153
|
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
14709
15154
|
var MODEL_LINE_RE = /^model:[ \t]*(.*)$/m;
|
|
15155
|
+
var RESERVED_AGENT_KEYS = /* @__PURE__ */ new Set([
|
|
15156
|
+
"name",
|
|
15157
|
+
"mode",
|
|
15158
|
+
"description",
|
|
15159
|
+
"permission",
|
|
15160
|
+
"color",
|
|
15161
|
+
"tools",
|
|
15162
|
+
"prompt",
|
|
15163
|
+
"steps",
|
|
15164
|
+
"maxSteps"
|
|
15165
|
+
]);
|
|
14710
15166
|
function cleanModelValue(raw) {
|
|
14711
15167
|
let v = raw;
|
|
14712
15168
|
const hash2 = v.search(/[ \t]#/);
|
|
@@ -14735,6 +15191,55 @@ function setFrontmatterModel(content, model) {
|
|
|
14735
15191
|
const nextFm = fm[0].slice(0, blockStart) + nextBlock + fm[0].slice(blockStart + block.length);
|
|
14736
15192
|
return nextFm + content.slice(fm[0].length);
|
|
14737
15193
|
}
|
|
15194
|
+
function getFrontmatterOptions(content) {
|
|
15195
|
+
const fm = FRONTMATTER_RE.exec(content);
|
|
15196
|
+
if (!fm?.[1]) return {};
|
|
15197
|
+
const parsed = parseFrontmatterBlock(fm[1]);
|
|
15198
|
+
if (!parsed) return {};
|
|
15199
|
+
const out = {};
|
|
15200
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
15201
|
+
if (k === "model" || RESERVED_AGENT_KEYS.has(k)) continue;
|
|
15202
|
+
out[k] = v;
|
|
15203
|
+
}
|
|
15204
|
+
return out;
|
|
15205
|
+
}
|
|
15206
|
+
function setFrontmatterOptions(content, options) {
|
|
15207
|
+
const fm = FRONTMATTER_RE.exec(content);
|
|
15208
|
+
if (!fm?.[1]) throw new Error("no frontmatter block");
|
|
15209
|
+
const block = fm[1];
|
|
15210
|
+
const keys = Object.keys(options);
|
|
15211
|
+
if (keys.length === 0) return content;
|
|
15212
|
+
const eol = block.includes("\r\n") ? "\r\n" : "\n";
|
|
15213
|
+
const lines = block.split(/\r?\n/);
|
|
15214
|
+
const handled = /* @__PURE__ */ new Set();
|
|
15215
|
+
for (let i = 0; i < lines.length; i++) {
|
|
15216
|
+
const m = /^([A-Za-z0-9_-]+):[ \t]*(.*)$/.exec(lines[i] ?? "");
|
|
15217
|
+
if (!m) continue;
|
|
15218
|
+
const key = m[1] ?? "";
|
|
15219
|
+
if (!(key in options)) continue;
|
|
15220
|
+
handled.add(key);
|
|
15221
|
+
let end = i + 1;
|
|
15222
|
+
while (end < lines.length && /^[ \t]+/.test(lines[end] ?? "")) end++;
|
|
15223
|
+
const value = options[key];
|
|
15224
|
+
if (value === null || value === void 0) {
|
|
15225
|
+
lines.splice(i, end - i);
|
|
15226
|
+
i--;
|
|
15227
|
+
} else {
|
|
15228
|
+
lines.splice(i, end - i, `${key}: ${serializeOptionValue(value)}`);
|
|
15229
|
+
}
|
|
15230
|
+
}
|
|
15231
|
+
const appended = [];
|
|
15232
|
+
for (const [key, value] of Object.entries(options)) {
|
|
15233
|
+
if (handled.has(key)) continue;
|
|
15234
|
+
if (value === null || value === void 0) continue;
|
|
15235
|
+
appended.push(`${key}: ${serializeOptionValue(value)}`);
|
|
15236
|
+
}
|
|
15237
|
+
const nextBlock = appended.length > 0 ? lines.join(eol) + eol + appended.join(eol) : lines.join(eol);
|
|
15238
|
+
if (nextBlock === block) return content;
|
|
15239
|
+
const blockStart = fm[0].indexOf(block);
|
|
15240
|
+
const nextFm = fm[0].slice(0, blockStart) + nextBlock + fm[0].slice(blockStart + block.length);
|
|
15241
|
+
return nextFm + content.slice(fm[0].length);
|
|
15242
|
+
}
|
|
14738
15243
|
function agentFilePath(agentsDir, name) {
|
|
14739
15244
|
return path3.join(agentsDir, `${name}.md`);
|
|
14740
15245
|
}
|
|
@@ -14748,7 +15253,7 @@ async function listAgentFiles(agentsDir) {
|
|
|
14748
15253
|
}
|
|
14749
15254
|
return names.filter((n) => n.endsWith(".md")).map((n) => n.slice(0, -".md".length)).sort();
|
|
14750
15255
|
}
|
|
14751
|
-
async function
|
|
15256
|
+
async function readAgentEntries(agentsDir) {
|
|
14752
15257
|
const out = {};
|
|
14753
15258
|
for (const name of await listAgentFiles(agentsDir)) {
|
|
14754
15259
|
const filePath = agentFilePath(agentsDir, name);
|
|
@@ -14759,10 +15264,17 @@ async function readAgentModels(agentsDir) {
|
|
|
14759
15264
|
throw new IOError(`Failed to read ${filePath}: ${cause.message}`, cause);
|
|
14760
15265
|
}
|
|
14761
15266
|
const model = getFrontmatterModel(content);
|
|
14762
|
-
if (model
|
|
15267
|
+
if (model === null) continue;
|
|
15268
|
+
out[name] = { model, options: getFrontmatterOptions(content) };
|
|
14763
15269
|
}
|
|
14764
15270
|
return out;
|
|
14765
15271
|
}
|
|
15272
|
+
async function readAgentModels(agentsDir) {
|
|
15273
|
+
const entries = await readAgentEntries(agentsDir);
|
|
15274
|
+
const out = {};
|
|
15275
|
+
for (const [name, entry] of Object.entries(entries)) out[name] = entry.model;
|
|
15276
|
+
return out;
|
|
15277
|
+
}
|
|
14766
15278
|
async function readAgentFileStrict(agentsDir, name) {
|
|
14767
15279
|
const filePath = agentFilePath(agentsDir, name);
|
|
14768
15280
|
if (!existsSync2(filePath)) {
|
|
@@ -14778,7 +15290,7 @@ async function readAgentFileStrict(agentsDir, name) {
|
|
|
14778
15290
|
if (model === null) {
|
|
14779
15291
|
throw new AgentFileError(name, filePath, "no frontmatter `model:` line to rewrite");
|
|
14780
15292
|
}
|
|
14781
|
-
return { filePath, content, model };
|
|
15293
|
+
return { filePath, content, model, options: getFrontmatterOptions(content) };
|
|
14782
15294
|
}
|
|
14783
15295
|
|
|
14784
15296
|
// src/core/stack-manager.ts
|
|
@@ -14964,6 +15476,9 @@ function collectModelRefs(stack) {
|
|
|
14964
15476
|
const refs = [];
|
|
14965
15477
|
for (const [k, v] of Object.entries(stack.agents)) {
|
|
14966
15478
|
refs.push({ path: `agents.${k}.model`, modelId: v.model });
|
|
15479
|
+
for (const [index, candidate] of (v.fallbacks ?? []).entries()) {
|
|
15480
|
+
refs.push({ path: `agents.${k}.fallbacks.${index}.model`, modelId: candidate.model });
|
|
15481
|
+
}
|
|
14967
15482
|
}
|
|
14968
15483
|
return refs;
|
|
14969
15484
|
}
|
|
@@ -15036,15 +15551,14 @@ async function applyStack(paths, name, options = {}) {
|
|
|
15036
15551
|
const pending = [];
|
|
15037
15552
|
for (const [agent, entry] of Object.entries(target.agents)) {
|
|
15038
15553
|
const { filePath, content, model } = await readAgentFileStrict(paths.agentsDir, agent);
|
|
15039
|
-
|
|
15040
|
-
|
|
15041
|
-
|
|
15042
|
-
|
|
15043
|
-
});
|
|
15554
|
+
const wantOptions = entryOptions(entry);
|
|
15555
|
+
const afterOptions = setFrontmatterOptions(content, wantOptions);
|
|
15556
|
+
const nextContent = entry.model === model ? afterOptions : setFrontmatterModel(afterOptions, entry.model);
|
|
15557
|
+
pending.push({ agent, filePath, next: nextContent === content ? null : nextContent });
|
|
15044
15558
|
}
|
|
15045
15559
|
const prevState = await readState(paths.statePath);
|
|
15046
15560
|
const prevActive = prevState?.active ?? null;
|
|
15047
|
-
const displaced = { agents:
|
|
15561
|
+
const displaced = { agents: await captureAgents(paths) };
|
|
15048
15562
|
const historyId = await appendHistory(
|
|
15049
15563
|
paths.historyDir,
|
|
15050
15564
|
prevActive ?? "(none)",
|
|
@@ -15062,7 +15576,20 @@ async function applyStack(paths, name, options = {}) {
|
|
|
15062
15576
|
version: 1,
|
|
15063
15577
|
active: name,
|
|
15064
15578
|
previousActive: prevActive,
|
|
15065
|
-
lastSwitchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15579
|
+
lastSwitchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15580
|
+
fallbackAgents: Object.fromEntries(
|
|
15581
|
+
Object.entries(target.agents).filter(([, entry]) => entry.fallbacks?.length).map(([agent, entry]) => {
|
|
15582
|
+
const variant = "variant" in entry ? entry.variant : displaced.agents[agent]?.variant;
|
|
15583
|
+
return [
|
|
15584
|
+
agent,
|
|
15585
|
+
{
|
|
15586
|
+
model: entry.model,
|
|
15587
|
+
variant: typeof variant === "string" ? variant : void 0,
|
|
15588
|
+
fallbacks: entry.fallbacks
|
|
15589
|
+
}
|
|
15590
|
+
];
|
|
15591
|
+
})
|
|
15592
|
+
)
|
|
15066
15593
|
});
|
|
15067
15594
|
await trimHistory(paths.historyDir).catch(() => {
|
|
15068
15595
|
});
|
|
@@ -15074,14 +15601,40 @@ async function applyStack(paths, name, options = {}) {
|
|
|
15074
15601
|
restartRequired: true
|
|
15075
15602
|
};
|
|
15076
15603
|
}
|
|
15077
|
-
function
|
|
15604
|
+
function entryOptions(entry) {
|
|
15605
|
+
const rec = entry;
|
|
15078
15606
|
const out = {};
|
|
15079
|
-
for (const k of Object.
|
|
15080
|
-
|
|
15081
|
-
|
|
15607
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
15608
|
+
if (k === "model" || k === "fallbacks" || RESERVED_AGENT_KEYS.has(k)) continue;
|
|
15609
|
+
out[k] = v;
|
|
15082
15610
|
}
|
|
15083
15611
|
return out;
|
|
15084
15612
|
}
|
|
15613
|
+
function entriesToStackAgents(entries) {
|
|
15614
|
+
const out = {};
|
|
15615
|
+
for (const name of Object.keys(entries).sort()) {
|
|
15616
|
+
const entry = entries[name];
|
|
15617
|
+
if (!entry) continue;
|
|
15618
|
+
const { model, options } = entry;
|
|
15619
|
+
const stackEntry = { model };
|
|
15620
|
+
for (const [k, v] of Object.entries(options)) {
|
|
15621
|
+
if (k !== "fallbacks") stackEntry[k] = v;
|
|
15622
|
+
}
|
|
15623
|
+
out[name] = stackEntry;
|
|
15624
|
+
}
|
|
15625
|
+
return out;
|
|
15626
|
+
}
|
|
15627
|
+
async function captureAgents(paths) {
|
|
15628
|
+
const agents = entriesToStackAgents(await readAgentEntries(paths.agentsDir));
|
|
15629
|
+
const state = await readState(paths.statePath);
|
|
15630
|
+
for (const [name, entry] of Object.entries(agents)) {
|
|
15631
|
+
const routing = state?.fallbackAgents?.[name];
|
|
15632
|
+
if (routing && routing.model === entry.model && (routing.variant ?? void 0) === (entry.variant ?? void 0)) {
|
|
15633
|
+
entry.fallbacks = routing.fallbacks;
|
|
15634
|
+
}
|
|
15635
|
+
}
|
|
15636
|
+
return agents;
|
|
15637
|
+
}
|
|
15085
15638
|
async function back(paths, n = 1, options = {}) {
|
|
15086
15639
|
if (n < 1) throw new UserError("`back -n` must be at least 1.");
|
|
15087
15640
|
const state = await readState(paths.statePath);
|
|
@@ -15115,8 +15668,7 @@ async function captureStack(paths, name, options = {}) {
|
|
|
15115
15668
|
if (existsSync5(dest) && !options.force) {
|
|
15116
15669
|
throw new UserError(`Stack "${name}" already exists. Use --force to overwrite.`);
|
|
15117
15670
|
}
|
|
15118
|
-
const
|
|
15119
|
-
const agents = modelsToStackAgents(models);
|
|
15671
|
+
const agents = await captureAgents(paths);
|
|
15120
15672
|
if (Object.keys(agents).length === 0) {
|
|
15121
15673
|
throw new UserError(
|
|
15122
15674
|
`No agent .md files with a frontmatter \`model:\` line found in ${paths.agentsDir}.`
|
|
@@ -15128,11 +15680,17 @@ async function captureStack(paths, name, options = {}) {
|
|
|
15128
15680
|
}
|
|
15129
15681
|
|
|
15130
15682
|
// src/version.ts
|
|
15131
|
-
var VERSION = "1.0
|
|
15683
|
+
var VERSION = "1.1.0";
|
|
15132
15684
|
|
|
15133
15685
|
// src/plugin.ts
|
|
15134
15686
|
async function safeToast(client, message, variant = "success") {
|
|
15135
15687
|
try {
|
|
15688
|
+
if (client.tui?.showToast) {
|
|
15689
|
+
const result = await client.tui.showToast({ body: { message, variant, duration: 15e3 } });
|
|
15690
|
+
if (result && typeof result === "object" && "error" in result && result.error)
|
|
15691
|
+
throw result.error;
|
|
15692
|
+
return;
|
|
15693
|
+
}
|
|
15136
15694
|
if (client.tui?.toast?.show) {
|
|
15137
15695
|
await client.tui.toast.show({ body: { message, variant } });
|
|
15138
15696
|
return;
|
|
@@ -15154,6 +15712,30 @@ function errOut(e) {
|
|
|
15154
15712
|
var AgentRouterPlugin = async (ctx) => {
|
|
15155
15713
|
const paths = await resolvePathsWithConfig();
|
|
15156
15714
|
const client = ctx.client;
|
|
15715
|
+
const stateBytes = () => {
|
|
15716
|
+
try {
|
|
15717
|
+
return readFileSync(paths.statePath, "utf8");
|
|
15718
|
+
} catch {
|
|
15719
|
+
return void 0;
|
|
15720
|
+
}
|
|
15721
|
+
};
|
|
15722
|
+
const initialState = stateBytes();
|
|
15723
|
+
const captured = await captureAgents(paths).catch(() => ({}));
|
|
15724
|
+
const routes = Object.fromEntries(
|
|
15725
|
+
Object.entries(captured).flatMap(([agent, entry]) => {
|
|
15726
|
+
const parsed = RoutingEntrySchema.safeParse({
|
|
15727
|
+
model: entry.model,
|
|
15728
|
+
variant: entry.variant,
|
|
15729
|
+
fallbacks: entry.fallbacks
|
|
15730
|
+
});
|
|
15731
|
+
return parsed.success && parsed.data.fallbacks?.length ? [[agent, parsed.data]] : [];
|
|
15732
|
+
})
|
|
15733
|
+
);
|
|
15734
|
+
const failover = createFailover(routes, async (message) => {
|
|
15735
|
+
await client.app?.log?.({ body: { service: "agent-router", level: "warn", message } }).catch(() => {
|
|
15736
|
+
});
|
|
15737
|
+
await safeToast(client, message, "warning");
|
|
15738
|
+
});
|
|
15157
15739
|
await client.app?.log?.({
|
|
15158
15740
|
body: {
|
|
15159
15741
|
service: "agent-router",
|
|
@@ -15168,6 +15750,11 @@ var AgentRouterPlugin = async (ctx) => {
|
|
|
15168
15750
|
}).catch(() => {
|
|
15169
15751
|
});
|
|
15170
15752
|
return {
|
|
15753
|
+
event: failover.event,
|
|
15754
|
+
"chat.message": async (input, output) => {
|
|
15755
|
+
if (stateBytes() !== initialState) failover.disable();
|
|
15756
|
+
await failover.message(input, output);
|
|
15757
|
+
},
|
|
15171
15758
|
tool: {
|
|
15172
15759
|
router_status: tool({
|
|
15173
15760
|
description: "Report the active agent-router stack, the current agent \u2192 model frontmatter mapping, and all available stacks.",
|
|
@@ -15208,6 +15795,7 @@ var AgentRouterPlugin = async (ctx) => {
|
|
|
15208
15795
|
},
|
|
15209
15796
|
async execute(args) {
|
|
15210
15797
|
try {
|
|
15798
|
+
failover.disable();
|
|
15211
15799
|
const r = await applyStack(paths, args.name, {
|
|
15212
15800
|
validate: args.validate ?? true
|
|
15213
15801
|
});
|
|
@@ -15251,12 +15839,7 @@ var AgentRouterPlugin = async (ctx) => {
|
|
|
15251
15839
|
try {
|
|
15252
15840
|
let stack;
|
|
15253
15841
|
if (args.active) {
|
|
15254
|
-
|
|
15255
|
-
stack = {
|
|
15256
|
-
agents: Object.fromEntries(
|
|
15257
|
-
Object.entries(models).map(([k, model]) => [k, { model }])
|
|
15258
|
-
)
|
|
15259
|
-
};
|
|
15842
|
+
stack = { agents: await captureAgents(paths) };
|
|
15260
15843
|
} else if (args.name) {
|
|
15261
15844
|
stack = await readStack(paths, args.name);
|
|
15262
15845
|
} else {
|
|
@@ -15280,6 +15863,7 @@ var AgentRouterPlugin = async (ctx) => {
|
|
|
15280
15863
|
},
|
|
15281
15864
|
async execute(args) {
|
|
15282
15865
|
try {
|
|
15866
|
+
failover.disable();
|
|
15283
15867
|
const r = await back(paths, args.n ?? 1);
|
|
15284
15868
|
await safeToast(
|
|
15285
15869
|
client,
|