@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/CHANGELOG.md +19 -0
- package/README.md +23 -17
- package/dist/cli.js +70 -20
- package/dist/cli.js.map +1 -1
- package/dist/plugin.d.ts +190 -3
- package/dist/plugin.js +382 -153
- package/dist/plugin.js.map +1 -1
- package/dist/tui.d.ts +3 -1
- package/dist/tui.js +166 -5
- package/dist/tui.js.map +1 -1
- package/index.ts +2 -0
- package/package.json +7 -3
- package/tui.ts +2 -0
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
|
|
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
|
|
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", ["
|
|
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 =
|
|
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 = "
|
|
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
|
|
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 =
|
|
16114
|
+
var plugin_default = { id: "agent-router", setup: setupV2 };
|
|
15886
16115
|
export {
|
|
15887
16116
|
AgentRouterPlugin,
|
|
15888
16117
|
plugin_default as default
|