@dylanrussell/agent-router 1.1.2 → 2.0.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/dist/plugin.js CHANGED
@@ -5,104 +5,7 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // src/plugin.ts
8
- import { readFileSync } from "fs";
9
- import { tool } from "@opencode-ai/plugin";
10
-
11
- // src/core/config.ts
12
- import { existsSync } from "fs";
13
- import { readFile } from "fs/promises";
14
- import { homedir as homedir2 } from "os";
15
- import path2 from "path";
16
-
17
- // src/core/errors.ts
18
- var RouterError = class extends Error {
19
- /** CLI exit code for this error class. Subclasses override. */
20
- static exitCode = 1;
21
- name = "RouterError";
22
- };
23
- var StackNotFoundError = class extends RouterError {
24
- constructor(stackName, available) {
25
- super(
26
- `Stack "${stackName}" not found. Available: ${available.length ? available.join(", ") : "(none)"}.`
27
- );
28
- this.stackName = stackName;
29
- this.available = available;
30
- }
31
- stackName;
32
- available;
33
- static exitCode = 2;
34
- name = "StackNotFoundError";
35
- };
36
- var AgentFileError = class extends RouterError {
37
- constructor(agentName, filePath, reason) {
38
- super(`Agent "${agentName}" (${filePath}): ${reason}`);
39
- this.agentName = agentName;
40
- this.filePath = filePath;
41
- }
42
- agentName;
43
- filePath;
44
- static exitCode = 2;
45
- name = "AgentFileError";
46
- };
47
- var ValidationError = class extends RouterError {
48
- constructor(message, path8, issues) {
49
- super(message);
50
- this.path = path8;
51
- this.issues = issues;
52
- }
53
- path;
54
- issues;
55
- static exitCode = 4;
56
- name = "ValidationError";
57
- };
58
- var ModelValidationError = class extends RouterError {
59
- constructor(stackName, missing) {
60
- super(
61
- `Stack "${stackName}" references ${missing.length} unreachable model ID${missing.length === 1 ? "" : "s"}.`
62
- );
63
- this.stackName = stackName;
64
- this.missing = missing;
65
- }
66
- stackName;
67
- missing;
68
- static exitCode = 4;
69
- name = "ModelValidationError";
70
- };
71
- var IOError = class extends RouterError {
72
- static exitCode = 3;
73
- name = "IOError";
74
- causedBy;
75
- constructor(message, causedBy) {
76
- super(message);
77
- this.causedBy = causedBy;
78
- }
79
- };
80
- var UserError = class extends RouterError {
81
- static exitCode = 1;
82
- name = "UserError";
83
- };
84
-
85
- // src/core/paths.ts
86
- import { homedir } from "os";
87
- import path from "path";
88
- function resolvePaths(options = {}) {
89
- const env = options.env ?? process.env;
90
- const opencodeConfigDir = options.opencodeConfigDir ?? path.join(env.XDG_CONFIG_HOME ?? path.join(homedir(), ".config"), "opencode");
91
- const routerHome = options.routerHome ?? env.AGENT_ROUTER_HOME ?? env.OMO_ROUTER_HOME ?? path.join(opencodeConfigDir, "agent-router");
92
- const agentsDir = options.agentsDir ?? env.AGENT_ROUTER_AGENTS_DIR ?? path.join(opencodeConfigDir, "agents");
93
- const stacksDir = options.stacksDir ?? env.AGENT_ROUTER_STACKS_DIR ?? path.join(routerHome, "stacks");
94
- return {
95
- opencodeConfigDir,
96
- opencodeJsonPath: path.join(opencodeConfigDir, "opencode.json"),
97
- tuiJsonPath: path.join(opencodeConfigDir, "tui.json"),
98
- agentsDir,
99
- opencodeBackupsDir: path.join(opencodeConfigDir, ".backups"),
100
- routerHome,
101
- statePath: path.join(routerHome, "state.json"),
102
- stacksDir,
103
- historyDir: path.join(routerHome, "history")
104
- };
105
- }
8
+ import { readFileSync as readFileSync2 } from "fs";
106
9
 
107
10
  // node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
108
11
  var external_exports = {};
@@ -14618,6 +14521,105 @@ function date4(params) {
14618
14521
  // node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
14619
14522
  config(en_default());
14620
14523
 
14524
+ // src/v2.ts
14525
+ import { readFileSync } from "fs";
14526
+
14527
+ // src/core/config.ts
14528
+ import { existsSync } from "fs";
14529
+ import { readFile } from "fs/promises";
14530
+ import { homedir as homedir2 } from "os";
14531
+ import path2 from "path";
14532
+
14533
+ // src/core/errors.ts
14534
+ var RouterError = class extends Error {
14535
+ /** CLI exit code for this error class. Subclasses override. */
14536
+ static exitCode = 1;
14537
+ name = "RouterError";
14538
+ };
14539
+ var StackNotFoundError = class extends RouterError {
14540
+ constructor(stackName, available) {
14541
+ super(
14542
+ `Stack "${stackName}" not found. Available: ${available.length ? available.join(", ") : "(none)"}.`
14543
+ );
14544
+ this.stackName = stackName;
14545
+ this.available = available;
14546
+ }
14547
+ stackName;
14548
+ available;
14549
+ static exitCode = 2;
14550
+ name = "StackNotFoundError";
14551
+ };
14552
+ var AgentFileError = class extends RouterError {
14553
+ constructor(agentName, filePath, reason) {
14554
+ super(`Agent "${agentName}" (${filePath}): ${reason}`);
14555
+ this.agentName = agentName;
14556
+ this.filePath = filePath;
14557
+ }
14558
+ agentName;
14559
+ filePath;
14560
+ static exitCode = 2;
14561
+ name = "AgentFileError";
14562
+ };
14563
+ var ValidationError = class extends RouterError {
14564
+ constructor(message, path8, issues) {
14565
+ super(message);
14566
+ this.path = path8;
14567
+ this.issues = issues;
14568
+ }
14569
+ path;
14570
+ issues;
14571
+ static exitCode = 4;
14572
+ name = "ValidationError";
14573
+ };
14574
+ var ModelValidationError = class extends RouterError {
14575
+ constructor(stackName, missing) {
14576
+ super(
14577
+ `Stack "${stackName}" references ${missing.length} unreachable model ID${missing.length === 1 ? "" : "s"}.`
14578
+ );
14579
+ this.stackName = stackName;
14580
+ this.missing = missing;
14581
+ }
14582
+ stackName;
14583
+ missing;
14584
+ static exitCode = 4;
14585
+ name = "ModelValidationError";
14586
+ };
14587
+ var IOError = class extends RouterError {
14588
+ static exitCode = 3;
14589
+ name = "IOError";
14590
+ causedBy;
14591
+ constructor(message, causedBy) {
14592
+ super(message);
14593
+ this.causedBy = causedBy;
14594
+ }
14595
+ };
14596
+ var UserError = class extends RouterError {
14597
+ static exitCode = 1;
14598
+ name = "UserError";
14599
+ };
14600
+
14601
+ // src/core/paths.ts
14602
+ import { homedir } from "os";
14603
+ import path from "path";
14604
+ function resolvePaths(options = {}) {
14605
+ const env = options.env ?? process.env;
14606
+ const opencodeConfigDir = options.opencodeConfigDir ?? path.join(env.XDG_CONFIG_HOME ?? path.join(homedir(), ".config"), "opencode");
14607
+ const routerHome = options.routerHome ?? env.AGENT_ROUTER_HOME ?? env.OMO_ROUTER_HOME ?? path.join(opencodeConfigDir, "agent-router");
14608
+ const agentsDir = options.agentsDir ?? env.AGENT_ROUTER_AGENTS_DIR ?? path.join(opencodeConfigDir, "agents");
14609
+ const stacksDir = options.stacksDir ?? env.AGENT_ROUTER_STACKS_DIR ?? path.join(routerHome, "stacks");
14610
+ return {
14611
+ opencodeConfigDir,
14612
+ opencodeJsonPath: path.join(opencodeConfigDir, "opencode.json"),
14613
+ tuiJsonPath: path.join(opencodeConfigDir, "cli.json"),
14614
+ agentsDir,
14615
+ opencodeBackupsDir: path.join(opencodeConfigDir, ".backups"),
14616
+ routerHome,
14617
+ statePath: path.join(routerHome, "state.json"),
14618
+ stacksDir,
14619
+ historyDir: path.join(routerHome, "history")
14620
+ };
14621
+ }
14622
+
14621
14623
  // src/core/schema.ts
14622
14624
  var FallbackSchema = external_exports.object({
14623
14625
  model: external_exports.string().regex(/^[^/\s]+\/\S+$/, "Expected provider/model"),
@@ -14652,7 +14654,13 @@ var ConfigFileSchema = external_exports.object({
14652
14654
  stacksDir: external_exports.string().min(1).optional()
14653
14655
  }).strict();
14654
14656
  var OpencodeJsonSchema = external_exports.object({
14655
- plugin: external_exports.array(external_exports.string()).optional()
14657
+ plugin: external_exports.array(external_exports.string()).optional(),
14658
+ plugins: external_exports.array(
14659
+ external_exports.union([
14660
+ external_exports.string(),
14661
+ external_exports.object({ package: external_exports.string(), options: external_exports.record(external_exports.string(), external_exports.unknown()).optional() }).passthrough()
14662
+ ])
14663
+ ).optional()
14656
14664
  }).passthrough();
14657
14665
 
14658
14666
  // src/core/config.ts
@@ -14768,6 +14776,10 @@ function createFailover(routes, notice) {
14768
14776
  const notify = (message) => notice(`agent-router: ${message}`).catch(() => {
14769
14777
  });
14770
14778
  return {
14779
+ active(sessionID) {
14780
+ const state = sessions.get(sessionID);
14781
+ return enabled && Boolean(state?.turn) && !state?.disabled;
14782
+ },
14771
14783
  disable() {
14772
14784
  enabled = false;
14773
14785
  sessions.clear();
@@ -14865,10 +14877,54 @@ function createFailover(routes, notice) {
14865
14877
  };
14866
14878
  }
14867
14879
 
14880
+ // src/core/stack-manager.ts
14881
+ import { existsSync as existsSync5 } from "fs";
14882
+ import { copyFile as copyFile2, mkdir as mkdir4, readFile as readFile4, readdir as readdir3, unlink as unlink3 } from "fs/promises";
14883
+ import path7 from "path";
14884
+
14885
+ // src/core/atomic-write.ts
14886
+ import { randomBytes } from "crypto";
14887
+ import { mkdir, realpath, rename, unlink, writeFile } from "fs/promises";
14888
+ import path3 from "path";
14889
+ async function resolveDestination(destPath) {
14890
+ try {
14891
+ return await realpath(destPath);
14892
+ } catch {
14893
+ try {
14894
+ const dir = await realpath(path3.dirname(destPath));
14895
+ return path3.join(dir, path3.basename(destPath));
14896
+ } catch {
14897
+ return destPath;
14898
+ }
14899
+ }
14900
+ }
14901
+ async function atomicWriteFile(destPath, contents) {
14902
+ await mkdir(path3.dirname(destPath), { recursive: true });
14903
+ const dest = await resolveDestination(destPath);
14904
+ const dir = path3.dirname(dest);
14905
+ const tmp = path3.join(
14906
+ dir,
14907
+ `.${path3.basename(dest)}.artmp-${process.pid}-${randomBytes(4).toString("hex")}`
14908
+ );
14909
+ try {
14910
+ await writeFile(tmp, contents, { encoding: "utf8", mode: 420 });
14911
+ await rename(tmp, dest);
14912
+ } catch (cause) {
14913
+ await unlink(tmp).catch(() => {
14914
+ });
14915
+ throw new IOError(`Atomic write failed for ${destPath}: ${cause.message}`, cause);
14916
+ }
14917
+ }
14918
+ async function atomicWriteJson(destPath, value) {
14919
+ const text = `${JSON.stringify(value, null, 2)}
14920
+ `;
14921
+ await atomicWriteFile(destPath, text);
14922
+ }
14923
+
14868
14924
  // src/core/frontmatter.ts
14869
14925
  import { existsSync as existsSync2 } from "fs";
14870
14926
  import { readFile as readFile2, readdir } from "fs/promises";
14871
- import path3 from "path";
14927
+ import path4 from "path";
14872
14928
 
14873
14929
  // src/core/yaml-lite.ts
14874
14930
  var RESERVED_WORDS = /* @__PURE__ */ new Set([
@@ -15157,6 +15213,11 @@ var RESERVED_AGENT_KEYS = /* @__PURE__ */ new Set([
15157
15213
  "mode",
15158
15214
  "description",
15159
15215
  "permission",
15216
+ "permissions",
15217
+ "disabled",
15218
+ "disable",
15219
+ "system",
15220
+ "hidden",
15160
15221
  "color",
15161
15222
  "tools",
15162
15223
  "prompt",
@@ -15241,7 +15302,7 @@ function setFrontmatterOptions(content, options) {
15241
15302
  return nextFm + content.slice(fm[0].length);
15242
15303
  }
15243
15304
  function agentFilePath(agentsDir, name) {
15244
- return path3.join(agentsDir, `${name}.md`);
15305
+ return path4.join(agentsDir, `${name}.md`);
15245
15306
  }
15246
15307
  async function listAgentFiles(agentsDir) {
15247
15308
  if (!existsSync2(agentsDir)) return [];
@@ -15293,50 +15354,6 @@ async function readAgentFileStrict(agentsDir, name) {
15293
15354
  return { filePath, content, model, options: getFrontmatterOptions(content) };
15294
15355
  }
15295
15356
 
15296
- // src/core/stack-manager.ts
15297
- import { existsSync as existsSync5 } from "fs";
15298
- import { copyFile as copyFile2, mkdir as mkdir4, readFile as readFile4, readdir as readdir3, unlink as unlink3 } from "fs/promises";
15299
- import path7 from "path";
15300
-
15301
- // src/core/atomic-write.ts
15302
- import { randomBytes } from "crypto";
15303
- import { mkdir, realpath, rename, unlink, writeFile } from "fs/promises";
15304
- import path4 from "path";
15305
- async function resolveDestination(destPath) {
15306
- try {
15307
- return await realpath(destPath);
15308
- } catch {
15309
- try {
15310
- const dir = await realpath(path4.dirname(destPath));
15311
- return path4.join(dir, path4.basename(destPath));
15312
- } catch {
15313
- return destPath;
15314
- }
15315
- }
15316
- }
15317
- async function atomicWriteFile(destPath, contents) {
15318
- await mkdir(path4.dirname(destPath), { recursive: true });
15319
- const dest = await resolveDestination(destPath);
15320
- const dir = path4.dirname(dest);
15321
- const tmp = path4.join(
15322
- dir,
15323
- `.${path4.basename(dest)}.artmp-${process.pid}-${randomBytes(4).toString("hex")}`
15324
- );
15325
- try {
15326
- await writeFile(tmp, contents, { encoding: "utf8", mode: 420 });
15327
- await rename(tmp, dest);
15328
- } catch (cause) {
15329
- await unlink(tmp).catch(() => {
15330
- });
15331
- throw new IOError(`Atomic write failed for ${destPath}: ${cause.message}`, cause);
15332
- }
15333
- }
15334
- async function atomicWriteJson(destPath, value) {
15335
- const text = `${JSON.stringify(value, null, 2)}
15336
- `;
15337
- await atomicWriteFile(destPath, text);
15338
- }
15339
-
15340
15357
  // src/core/history.ts
15341
15358
  import { mkdir as mkdir3, readdir as readdir2, unlink as unlink2 } from "fs/promises";
15342
15359
  import path6 from "path";
@@ -15455,7 +15472,7 @@ import { execFile } from "child_process";
15455
15472
  import { promisify } from "util";
15456
15473
  var execFileAsync = promisify(execFile);
15457
15474
  var DEFAULT_RUNNER = async () => {
15458
- const { stdout } = await execFileAsync("opencode", ["models"], {
15475
+ const { stdout } = await execFileAsync("opencode", ["api", "model.list"], {
15459
15476
  encoding: "utf8",
15460
15477
  timeout: 3e4,
15461
15478
  maxBuffer: 4 * 1024 * 1024
@@ -15464,6 +15481,17 @@ var DEFAULT_RUNNER = async () => {
15464
15481
  };
15465
15482
  function parseModelList(stdout) {
15466
15483
  const out = /* @__PURE__ */ new Set();
15484
+ if (/^[\[{]/.test(stdout.trimStart())) {
15485
+ const parsed = JSON.parse(stdout);
15486
+ const models = Array.isArray(parsed) ? parsed : parsed.data;
15487
+ for (const model of models) {
15488
+ if (model.enabled === false) continue;
15489
+ const id = `${model.providerID}/${model.id}`;
15490
+ out.add(id);
15491
+ for (const variant of model.variants ?? []) out.add(`${id}#${variant.id}`);
15492
+ }
15493
+ return out;
15494
+ }
15467
15495
  for (const raw of stdout.split(/\r?\n/)) {
15468
15496
  const line = raw.trim();
15469
15497
  if (!line) continue;
@@ -15488,6 +15516,19 @@ async function validateStack(stack, options = {}) {
15488
15516
  const available = parseModelList(stdout);
15489
15517
  const refs = collectModelRefs(stack);
15490
15518
  const missing = refs.filter((r) => !available.has(r.modelId));
15519
+ if (/^[\[{]/.test(stdout.trimStart())) {
15520
+ for (const [agent, entry] of Object.entries(stack.agents)) {
15521
+ for (const [index, candidate] of [entry, ...entry.fallbacks ?? []].entries()) {
15522
+ if (!candidate.variant) continue;
15523
+ const modelId = `${candidate.model}#${candidate.variant}`;
15524
+ if (!available.has(modelId))
15525
+ missing.push({
15526
+ path: index === 0 ? `agents.${agent}.variant` : `agents.${agent}.fallbacks.${index - 1}.variant`,
15527
+ modelId
15528
+ });
15529
+ }
15530
+ }
15531
+ }
15491
15532
  return { ok: missing.length === 0, checked: refs.length, missing, available };
15492
15533
  }
15493
15534
  async function validateStackOrThrow(stackName, stack, options = {}) {
@@ -15552,8 +15593,15 @@ async function applyStack(paths, name, options = {}) {
15552
15593
  for (const [agent, entry] of Object.entries(target.agents)) {
15553
15594
  const { filePath, content, model } = await readAgentFileStrict(paths.agentsDir, agent);
15554
15595
  const wantOptions = entryOptions(entry);
15596
+ let wantModel = entry.model;
15597
+ if (model.includes("#") || wantModel.includes("#")) {
15598
+ const [base, inline] = wantModel.split("#");
15599
+ const variant = "variant" in entry ? entry.variant : inline ?? model.split("#")[1];
15600
+ wantModel = `${base}${variant ? `#${variant}` : ""}`;
15601
+ wantOptions.variant = null;
15602
+ }
15555
15603
  const afterOptions = setFrontmatterOptions(content, wantOptions);
15556
- const nextContent = entry.model === model ? afterOptions : setFrontmatterModel(afterOptions, entry.model);
15604
+ const nextContent = wantModel === model ? afterOptions : setFrontmatterModel(afterOptions, wantModel);
15557
15605
  pending.push({ agent, filePath, next: nextContent === content ? null : nextContent });
15558
15606
  }
15559
15607
  const prevState = await readState(paths.statePath);
@@ -15620,6 +15668,10 @@ function entriesToStackAgents(entries) {
15620
15668
  for (const [k, v] of Object.entries(options)) {
15621
15669
  if (k !== "fallbacks") stackEntry[k] = v;
15622
15670
  }
15671
+ if (model.includes("#")) {
15672
+ stackEntry.model = model.slice(0, model.indexOf("#"));
15673
+ stackEntry.variant = model.slice(model.indexOf("#") + 1);
15674
+ }
15623
15675
  out[name] = stackEntry;
15624
15676
  }
15625
15677
  return out;
@@ -15679,10 +15731,186 @@ async function captureStack(paths, name, options = {}) {
15679
15731
  return { name, path: dest, agents: Object.keys(agents).length };
15680
15732
  }
15681
15733
 
15734
+ // src/v2.ts
15735
+ async function setupV2(ctx) {
15736
+ const legacy = await AgentRouterPlugin(
15737
+ { client: {} },
15738
+ {
15739
+ runOpencodeModels: async () => JSON.stringify(await ctx.model.list())
15740
+ }
15741
+ );
15742
+ await ctx.tool.transform((editor) => {
15743
+ for (const [name, definition] of Object.entries(legacy.tool ?? {})) {
15744
+ editor.add({
15745
+ name,
15746
+ description: definition.description,
15747
+ input: external_exports.toJSONSchema(external_exports.object(definition.args)),
15748
+ async execute(input) {
15749
+ const execute = definition.execute;
15750
+ const result = await execute(external_exports.object(definition.args).parse(input));
15751
+ return { content: typeof result === "string" ? result : result.output };
15752
+ }
15753
+ });
15754
+ }
15755
+ });
15756
+ const paths = await resolvePathsWithConfig();
15757
+ const state = () => {
15758
+ try {
15759
+ return readFileSync(paths.statePath, "utf8");
15760
+ } catch {
15761
+ return void 0;
15762
+ }
15763
+ };
15764
+ const initial = state();
15765
+ const captured = await captureAgents(paths);
15766
+ const routes = Object.fromEntries(
15767
+ Object.entries(captured).flatMap(([id, entry]) => {
15768
+ const parsed = RoutingEntrySchema.safeParse({
15769
+ model: entry.model,
15770
+ variant: entry.variant,
15771
+ fallbacks: entry.fallbacks
15772
+ });
15773
+ return parsed.success && parsed.data.fallbacks?.length ? [[id, parsed.data]] : [];
15774
+ })
15775
+ );
15776
+ const failover = createFailover(routes, async (message) => {
15777
+ console.warn(message);
15778
+ });
15779
+ const turns = /* @__PURE__ */ new Map();
15780
+ const kinds = /* @__PURE__ */ new Map();
15781
+ const ownSelections = /* @__PURE__ */ new Map();
15782
+ const controller = new AbortController();
15783
+ const events = (async () => {
15784
+ for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
15785
+ if (!("sessionID" in event.data)) continue;
15786
+ const sessionID = String(event.data.sessionID);
15787
+ if (!turns.has(sessionID)) continue;
15788
+ if (event.type === "session.model.selected") {
15789
+ const model = event.data.model;
15790
+ if (ownSelections.get(sessionID) === `${model.providerID}/${model.id}#${model.variant ?? ""}`) {
15791
+ ownSelections.delete(sessionID);
15792
+ continue;
15793
+ }
15794
+ }
15795
+ if ([
15796
+ "session.model.selected",
15797
+ "session.agent.selected",
15798
+ "session.execution.interrupted",
15799
+ "session.deleted"
15800
+ ].includes(event.type)) {
15801
+ await failover.event({
15802
+ event: { type: "session.next.model.switched", properties: { sessionID } }
15803
+ });
15804
+ turns.delete(sessionID);
15805
+ kinds.delete(sessionID);
15806
+ }
15807
+ }
15808
+ })().catch((error51) => {
15809
+ if (!controller.signal.aborted) console.warn("agent-router event subscription failed", error51);
15810
+ });
15811
+ await ctx.session.hook("model.request", (event) => {
15812
+ if (turns.has(event.sessionID)) kinds.set(event.sessionID, event.kind);
15813
+ });
15814
+ await ctx.session.hook("prompt", async (event) => {
15815
+ if (turns.get(event.sessionID)?.id === event.messageID) return;
15816
+ if (state() !== initial) {
15817
+ failover.disable();
15818
+ turns.clear();
15819
+ return;
15820
+ }
15821
+ const session = await ctx.session.get({ sessionID: event.sessionID });
15822
+ if (!session.agent || !session.model || !routes[session.agent]) return;
15823
+ const message = {
15824
+ id: event.messageID,
15825
+ agent: session.agent,
15826
+ model: {
15827
+ providerID: session.model.providerID,
15828
+ modelID: session.model.id,
15829
+ variant: session.model.variant
15830
+ }
15831
+ };
15832
+ const before = JSON.stringify(message.model);
15833
+ await failover.message({ sessionID: event.sessionID }, { message });
15834
+ if (!failover.active(event.sessionID)) {
15835
+ turns.delete(event.sessionID);
15836
+ return;
15837
+ }
15838
+ if (before !== JSON.stringify(message.model)) {
15839
+ const latest = await ctx.session.get({ sessionID: event.sessionID });
15840
+ if (!failover.active(event.sessionID) || latest.agent !== session.agent || JSON.stringify(latest.model) !== JSON.stringify(session.model)) {
15841
+ await failover.event({
15842
+ event: {
15843
+ type: "session.next.model.switched",
15844
+ properties: { sessionID: event.sessionID }
15845
+ }
15846
+ });
15847
+ turns.delete(event.sessionID);
15848
+ return;
15849
+ }
15850
+ ownSelections.set(
15851
+ event.sessionID,
15852
+ `${message.model.providerID}/${message.model.modelID}#${message.model.variant ?? ""}`
15853
+ );
15854
+ try {
15855
+ await ctx.session.switchModel({
15856
+ sessionID: event.sessionID,
15857
+ model: {
15858
+ providerID: message.model.providerID,
15859
+ id: message.model.modelID,
15860
+ ...message.model.variant ? { variant: message.model.variant } : {}
15861
+ }
15862
+ });
15863
+ } catch (error51) {
15864
+ ownSelections.delete(event.sessionID);
15865
+ throw error51;
15866
+ }
15867
+ }
15868
+ turns.set(event.sessionID, message);
15869
+ });
15870
+ await ctx.session.hook("retry", async (event) => {
15871
+ const turn = turns.get(event.sessionID);
15872
+ if (!turn || kinds.get(event.sessionID) !== "primary" || turn.agent !== event.agent || turn.model.providerID !== event.model.providerID || turn.model.modelID !== event.model.id)
15873
+ return;
15874
+ if (!event.decision.retry || ![429, 500, 502, 503, 504].includes(event.error.status ?? 0))
15875
+ return;
15876
+ event.decision = { retry: false };
15877
+ await failover.event({
15878
+ event: {
15879
+ type: "message.updated",
15880
+ properties: {
15881
+ info: {
15882
+ sessionID: event.sessionID,
15883
+ role: "assistant",
15884
+ parentID: turn.id,
15885
+ agent: turn.agent,
15886
+ ...turn.model,
15887
+ error: {
15888
+ name: "APIError",
15889
+ data: { statusCode: event.error.status, isRetryable: true }
15890
+ }
15891
+ }
15892
+ }
15893
+ }
15894
+ });
15895
+ });
15896
+ return async () => {
15897
+ controller.abort();
15898
+ failover.disable();
15899
+ turns.clear();
15900
+ kinds.clear();
15901
+ ownSelections.clear();
15902
+ await events;
15903
+ };
15904
+ }
15905
+
15682
15906
  // src/version.ts
15683
- var VERSION = "1.1.2";
15907
+ var VERSION = "2.0.0";
15684
15908
 
15685
15909
  // src/plugin.ts
15910
+ var tool = Object.assign(
15911
+ (definition) => definition,
15912
+ { schema: external_exports }
15913
+ );
15686
15914
  async function safeToast(client, message, variant = "success") {
15687
15915
  try {
15688
15916
  if (client.tui?.showToast) {
@@ -15709,12 +15937,12 @@ function errOut(e) {
15709
15937
  const msg = e instanceof RouterError ? `${e.name}: ${e.message}` : `${e.name ?? "Error"}: ${e.message}`;
15710
15938
  return { output: JSON.stringify({ error: msg }, null, 2), metadata: { error: msg } };
15711
15939
  }
15712
- var AgentRouterPlugin = async (ctx) => {
15940
+ var AgentRouterPlugin = async (ctx, validateOptions) => {
15713
15941
  const paths = await resolvePathsWithConfig();
15714
15942
  const client = ctx.client;
15715
15943
  const stateBytes = () => {
15716
15944
  try {
15717
- return readFileSync(paths.statePath, "utf8");
15945
+ return readFileSync2(paths.statePath, "utf8");
15718
15946
  } catch {
15719
15947
  return void 0;
15720
15948
  }
@@ -15797,7 +16025,8 @@ var AgentRouterPlugin = async (ctx) => {
15797
16025
  try {
15798
16026
  failover.disable();
15799
16027
  const r = await applyStack(paths, args.name, {
15800
- validate: args.validate ?? true
16028
+ validate: args.validate ?? true,
16029
+ ...validateOptions ? { validateOptions } : {}
15801
16030
  });
15802
16031
  await safeToast(
15803
16032
  client,
@@ -15845,7 +16074,7 @@ var AgentRouterPlugin = async (ctx) => {
15845
16074
  } else {
15846
16075
  return errOut(new Error("Pass `name` or `active: true`."));
15847
16076
  }
15848
- const r = await validateStack(StackFileSchema.parse(stack));
16077
+ const r = await validateStack(StackFileSchema.parse(stack), validateOptions);
15849
16078
  return ok({
15850
16079
  ok: r.ok,
15851
16080
  checked: r.checked,
@@ -15882,7 +16111,7 @@ var AgentRouterPlugin = async (ctx) => {
15882
16111
  }
15883
16112
  };
15884
16113
  };
15885
- var plugin_default = AgentRouterPlugin;
16114
+ var plugin_default = { id: "agent-router", setup: setupV2 };
15886
16115
  export {
15887
16116
  AgentRouterPlugin,
15888
16117
  plugin_default as default