@staix/agent-hub 0.12.38 → 0.12.40
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 +34 -0
- package/README.md +19 -10
- package/docs/agent-notes/adapters.md +6 -1
- package/docs/agent-notes/benchmarks.md +1 -1
- package/docs/agent-notes/budget.md +2 -0
- package/docs/agent-notes/bus.md +2 -2
- package/docs/agent-notes/daemon.md +10 -5
- package/docs/agent-notes/models.md +4 -4
- package/docs/agent-notes/tasks.md +3 -0
- package/docs/agent-notes/tests.md +2 -2
- package/docs/memory.md +1 -1
- package/docs/operations.md +167 -30
- package/docs/quickstart.md +25 -5
- package/docs/security.md +20 -8
- package/docs/smoke.md +2 -2
- package/docs/specs/2026-09-19-agent-hub-design.md +167 -72
- package/docs/specs/2026-10-04-switchyard-source-port-design.md +12 -7
- package/docs/verification/2026-10-20-0.12.39.md +55 -0
- package/docs/verification/2026-10-20-0.12.40.md +63 -0
- package/docs/verification/2026-10-20-jcode-tui-0.98.1.md +39 -0
- package/docs/verified.json +38 -19
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +679 -193
- package/src/adapters/acp.ts +54 -3
- package/src/adapters/claude-channel.ts +35 -4
- package/src/adapters/codex-appserver.ts +12 -3
- package/src/adapters/definitions/builtin-metadata.ts +6 -4
- package/src/adapters/definitions/native-cli-doctor.ts +8 -5
- package/src/adapters/definitions/native-cli-entry-claude.ts +7 -2
- package/src/adapters/definitions/native-cli-entry-codex.ts +13 -14
- package/src/adapters/definitions/native-cli-entry-jcode.ts +90 -11
- package/src/adapters/definitions/native-cli-entry-pi.ts +9 -7
- package/src/adapters/definitions/native-cli-entry-types.ts +3 -0
- package/src/adapters/definitions/native-cli-jcode-mcp.ts +55 -1
- package/src/adapters/definitions/native-cli-preview-common.ts +1 -0
- package/src/adapters/definitions/native-cli-preview-jcode.ts +7 -7
- package/src/adapters/definitions/native-daemon-policy.ts +1 -1
- package/src/adapters/jcode.ts +165 -2
- package/src/adapters/local-worker.ts +21 -37
- package/src/adapters/pi.ts +7 -0
- package/src/adapters/registry.ts +1 -1
- package/src/cli/console-state.ts +1 -0
- package/src/cli/console.ts +11 -1
- package/src/cli/help.ts +13 -8
- package/src/cli/identity-audit.ts +33 -7
- package/src/cli/jcode-mcp.ts +1 -1
- package/src/cli/main.ts +138 -28
- package/src/cli/output.ts +5 -2
- package/src/cli/preview.ts +2 -2
- package/src/cli/status-lines.ts +1 -1
- package/src/cli/terminal-recovery.ts +54 -4
- package/src/cli/upgrade-interactive.ts +5 -0
- package/src/cli/upgrade-runtime.ts +13 -1
- package/src/cli/upgrade.ts +67 -1
- package/src/hub/board.ts +4 -2
- package/src/hub/bus.ts +8 -3
- package/src/hub/checks.ts +15 -5
- package/src/hub/conductor.ts +2 -1
- package/src/hub/config-apply.ts +20 -2
- package/src/hub/config-reload.ts +2 -8
- package/src/hub/control-client.ts +26 -3
- package/src/hub/daemon.ts +166 -67
- package/src/hub/delivery-superseded.ts +19 -2
- package/src/hub/events.ts +4 -1
- package/src/hub/hub-tools.ts +4 -4
- package/src/hub/inference.ts +4 -8
- package/src/hub/lifecycle.ts +3 -3
- package/src/hub/main-checkout.ts +36 -0
- package/src/hub/memory-profile-check.ts +6 -2
- package/src/hub/memory-profile.ts +18 -1
- package/src/hub/peers.ts +4 -2
- package/src/hub/ports.ts +1 -0
- package/src/hub/routing.ts +24 -14
- package/src/hub/settings.ts +1 -1
- package/src/hub/start-mode.ts +1 -1
- package/src/hub/task-repository.ts +136 -0
- package/src/hub/tasks.ts +101 -21
- package/src/hub/terminal-launch.ts +130 -0
- package/src/hub/usage.ts +44 -18
- package/src/models/route/config.ts +1 -1
- package/src/omniroute/client.ts +7 -19
- package/templates/AGENTS.block.md +1 -0
- package/templates/routing.toml +10 -44
- package/src/switchyard/config.ts +0 -45
- package/src/switchyard/sidecar.ts +0 -136
package/templates/routing.toml
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# agent-hub routing: who takes which kind of task (L1), and which model route the local worker asks for (L2).
|
|
2
|
-
#
|
|
3
|
-
# sidecar config and adds the gateway client, so anything Switchyard accepts can be written here.
|
|
2
|
+
# Hub routes run in-process and name gateway aliases directly.
|
|
4
3
|
|
|
5
4
|
# Top-level keys, before any table. Pi's hub/auto and the hub stage routes keep a session's model through a tool loop and
|
|
6
5
|
# switch only where it pays (#197): "off", "shadow" (record in events.jsonl what the planner would do, routing unchanged)
|
|
@@ -9,41 +8,8 @@ stay_switch = "shadow"
|
|
|
9
8
|
max_switch_prefill_tokens = 32000
|
|
10
9
|
|
|
11
10
|
[local]
|
|
12
|
-
route = "
|
|
13
|
-
fixed_model = "
|
|
14
|
-
|
|
15
|
-
[targets.dsv4f]
|
|
16
|
-
id = "vllm/deepseek-ai/DeepSeek-V4-Flash-0731"
|
|
17
|
-
|
|
18
|
-
[routes."sy/coding"]
|
|
19
|
-
type = "passthrough"
|
|
20
|
-
target = "dsv4f"
|
|
21
|
-
|
|
22
|
-
[routes."sy/fast"]
|
|
23
|
-
type = "passthrough"
|
|
24
|
-
target = "dsv4f"
|
|
25
|
-
|
|
26
|
-
# With a second model served (here GLM-5.3-Flash), replace sy/coding with the two-tier route and add the review escalation:
|
|
27
|
-
#
|
|
28
|
-
# [targets.glm53f]
|
|
29
|
-
# id = "vllm/zai-org/GLM-5.3-Flash"
|
|
30
|
-
#
|
|
31
|
-
# [routes."sy/coding"]
|
|
32
|
-
# type = "stage_router"
|
|
33
|
-
# efficient_target = "glm53f"
|
|
34
|
-
# capable_target = "dsv4f"
|
|
35
|
-
# picker = "efficient_first"
|
|
36
|
-
# confidence_threshold = 0.5
|
|
37
|
-
#
|
|
38
|
-
# [routes."sy/review"]
|
|
39
|
-
# type = "llm_classifier"
|
|
40
|
-
# mode = "escalation"
|
|
41
|
-
# classifier_target = "glm53f"
|
|
42
|
-
# weak_target = "glm53f"
|
|
43
|
-
# strong_target = "dsv4f"
|
|
44
|
-
#
|
|
45
|
-
# [routes."sy/review".escalation] # required by the 0.2.0 binary for mode = "escalation"
|
|
46
|
-
# confirmations = 2
|
|
11
|
+
route = "hub/stage" # hub-owned model routing
|
|
12
|
+
fixed_model = "coding" # direct gateway fallback when the route is unavailable
|
|
47
13
|
|
|
48
14
|
# L1: task assignment. `peers` is the preference order among attached, unpaused peers (idle before busy).
|
|
49
15
|
# `route` / `fixed_model` apply when the task lands on the local worker. `tasks.escalate_after` consecutive
|
|
@@ -70,19 +36,19 @@ local_allowed = false
|
|
|
70
36
|
|
|
71
37
|
[classes.implement]
|
|
72
38
|
peers = ["pi", "local", "codex", "kimi"]
|
|
73
|
-
route = "
|
|
39
|
+
route = "hub/stage"
|
|
74
40
|
pi_backend = "dgx"
|
|
75
41
|
escalate_to = ["codex", "kimi", "claude"]
|
|
76
42
|
|
|
77
43
|
[classes.bulk_edit]
|
|
78
44
|
peers = ["pi", "local", "kimi"]
|
|
79
|
-
|
|
45
|
+
fixed_model = "fast"
|
|
80
46
|
pi_backend = "dgx"
|
|
81
47
|
escalate_to = ["codex", "kimi"]
|
|
82
48
|
|
|
83
49
|
[classes.test]
|
|
84
50
|
peers = ["pi", "local", "kimi", "codex"]
|
|
85
|
-
|
|
51
|
+
fixed_model = "fast"
|
|
86
52
|
pi_backend = "dgx"
|
|
87
53
|
escalate_to = ["codex", "kimi"]
|
|
88
54
|
|
|
@@ -92,18 +58,18 @@ local_allowed = false # judgment classes never go to local
|
|
|
92
58
|
|
|
93
59
|
[classes.summarize]
|
|
94
60
|
peers = ["pi", "local", "kimi"]
|
|
95
|
-
|
|
61
|
+
fixed_model = "fast"
|
|
96
62
|
pi_backend = "mlx"
|
|
97
63
|
escalate_to = ["codex", "kimi"]
|
|
98
64
|
|
|
99
65
|
[classes.triage]
|
|
100
66
|
peers = ["pi", "local", "kimi"]
|
|
101
|
-
|
|
67
|
+
fixed_model = "fast"
|
|
102
68
|
pi_backend = "mlx"
|
|
103
69
|
escalate_to = ["codex", "kimi"]
|
|
104
70
|
|
|
105
|
-
# In-process L2 routes
|
|
106
|
-
#
|
|
71
|
+
# In-process L2 routes: set local.route or a class route to hub/<id>.
|
|
72
|
+
# A class with fixed_model and no route calls that alias directly. Defaults are efficient fast, capable coding.
|
|
107
73
|
[hub_routes."hub/stage"]
|
|
108
74
|
type = "stage"
|
|
109
75
|
efficient = "fast"
|
package/src/switchyard/config.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { Routing } from "../hub/routing.ts";
|
|
2
|
-
|
|
3
|
-
export const KEY_ENV = "OMNIROUTE_API_KEY";
|
|
4
|
-
|
|
5
|
-
const scalar = (v: unknown): string => {
|
|
6
|
-
if (typeof v === "string") return JSON.stringify(v);
|
|
7
|
-
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
8
|
-
if (Array.isArray(v)) return `[${v.map(scalar).join(", ")}]`;
|
|
9
|
-
throw new Error(`unsupported TOML value: ${JSON.stringify(v)}`);
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/** Bare where TOML allows it, quoted otherwise: `glm-5.3` as a bare header would parse as the nested table "glm-5"."3". */
|
|
13
|
-
const key = (k: string) => (/^[A-Za-z0-9_-]+$/.test(k) ? k : JSON.stringify(k));
|
|
14
|
-
|
|
15
|
-
function table(header: string, entries: Record<string, unknown>): string {
|
|
16
|
-
const flat = Object.entries(entries).filter(([, v]) => typeof v !== "object" || Array.isArray(v));
|
|
17
|
-
const nested = Object.entries(entries).filter(([, v]) => v && typeof v === "object" && !Array.isArray(v));
|
|
18
|
-
return [
|
|
19
|
-
`[${header}]`,
|
|
20
|
-
...flat.map(([k, v]) => `${key(k)} = ${scalar(v)}`),
|
|
21
|
-
"",
|
|
22
|
-
...nested.map(([k, v]) => table(`${header}.${key(k)}`, v as Record<string, unknown>)),
|
|
23
|
-
].join("\n");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Switchyard v0.2.0 server config. routing.toml already uses Switchyard's table shapes, so this only adds what the
|
|
28
|
-
* hub knows: the gateway client, `llm_client` on targets and the public `id` on routes. The API key is referenced
|
|
29
|
-
* by env var name and never written; Access headers, when the live URL needs them, are the one secret in the file.
|
|
30
|
-
*/
|
|
31
|
-
export function switchyardToml(routing: Routing, gateway: { baseUrl: string; extraHeaders: Record<string, string> }): string {
|
|
32
|
-
const headers = Object.entries(gateway.extraHeaders);
|
|
33
|
-
const client = [
|
|
34
|
-
"[llm_clients.gateway]",
|
|
35
|
-
'format = "openai_chat"',
|
|
36
|
-
`base_url = ${scalar(gateway.baseUrl)}`,
|
|
37
|
-
`api_key_env = ${scalar(KEY_ENV)}`,
|
|
38
|
-
...(headers.length ? [`extra_headers = { ${headers.map(([k, v]) => `${scalar(k)} = ${scalar(v)}`).join(", ")} }`] : []),
|
|
39
|
-
// no timeout_ms: the 0.2.0 binary rejects it (its llm_clients fields are format, base_url, api_key_env, extra_headers, max_retries)
|
|
40
|
-
"",
|
|
41
|
-
].join("\n");
|
|
42
|
-
const targets = Object.entries(routing.targets).map(([name, t]) => table(`targets.${key(name)}`, { ...t, llm_client: "gateway" }));
|
|
43
|
-
const routes = Object.entries(routing.routes).map(([id, r], i) => table(`routes.r${i}`, { id, ...r }));
|
|
44
|
-
return ["schema_version = 1", "", client, ...targets, ...routes].join("\n");
|
|
45
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
2
|
-
import { chmodSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import type { Routing } from "../hub/routing.ts";
|
|
5
|
-
import type { OmniRoute } from "../omniroute/client.ts";
|
|
6
|
-
import { KEY_ENV, switchyardToml } from "./config.ts";
|
|
7
|
-
import { stopOwnedProcess } from "../hub/child-process.ts";
|
|
8
|
-
|
|
9
|
-
class GatewayDown extends Error {}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export interface SidecarOptions {
|
|
13
|
-
routing: Routing;
|
|
14
|
-
omni: OmniRoute;
|
|
15
|
-
stateDir: string;
|
|
16
|
-
port: number;
|
|
17
|
-
/** `AGENTHUB_SWITCHYARD_BIN`, else `switchyard-server` on PATH. */
|
|
18
|
-
bin?: string;
|
|
19
|
-
log: (line: string) => void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* L2 as a session-scoped sidecar: started on the first hub-owned model call, stopped with the hub, never resident.
|
|
24
|
-
* Every failure (no binary, bad config, unhealthy, a failed call) turns it off for the rest of the hub run and the
|
|
25
|
-
* caller falls back to `fixed_model` on OmniRoute directly.
|
|
26
|
-
*/
|
|
27
|
-
export class Sidecar {
|
|
28
|
-
private proc: ChildProcess | undefined;
|
|
29
|
-
private starting: Promise<string | undefined> | undefined;
|
|
30
|
-
private generation = 0;
|
|
31
|
-
private stopped = false;
|
|
32
|
-
private stopping: Promise<void> | undefined;
|
|
33
|
-
private off = "";
|
|
34
|
-
/** The gateway URL the running sidecar was generated with; calls through it go there whatever the client probes later. */
|
|
35
|
-
upstream: string | undefined;
|
|
36
|
-
|
|
37
|
-
constructor(private readonly opts: SidecarOptions) {}
|
|
38
|
-
|
|
39
|
-
get status(): string {
|
|
40
|
-
return this.off ? `off (${this.off})` : this.proc ? `127.0.0.1:${this.opts.port}` : "not started";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Base URL to send chat calls to, or undefined when the hub should call OmniRoute directly. */
|
|
44
|
-
endpoint(): Promise<string | undefined> {
|
|
45
|
-
if (this.off || this.stopped) return Promise.resolve(undefined);
|
|
46
|
-
return (this.starting ??= this.start().catch((e: Error) => {
|
|
47
|
-
// An unreachable gateway says nothing about Switchyard: this call goes direct, the next one tries the sidecar again.
|
|
48
|
-
if (e instanceof GatewayDown) this.starting = undefined;
|
|
49
|
-
else this.disable(e.message);
|
|
50
|
-
return undefined;
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** A call through the sidecar failed: stop trusting it. Logged once. */
|
|
55
|
-
disable(reason: string): void {
|
|
56
|
-
if (this.off) return;
|
|
57
|
-
this.off = reason;
|
|
58
|
-
this.opts.log(`switchyard: off, falling back to fixed_model on OmniRoute (${reason})`);
|
|
59
|
-
void this.stop().catch((e: Error) => this.opts.log(`switchyard: shutdown incomplete (${e.message})`));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async stop(): Promise<void> {
|
|
63
|
-
if (this.stopping) return this.stopping;
|
|
64
|
-
this.stopped = true;
|
|
65
|
-
++this.generation;
|
|
66
|
-
const proc = this.proc;
|
|
67
|
-
// Preserve the historical synchronous file cleanup; the returned promise
|
|
68
|
-
// still represents the child process shutdown and must be awaited by the hub.
|
|
69
|
-
rmSync(join(this.opts.stateDir, "switchyard.toml"), { force: true });
|
|
70
|
-
this.stopping = (async () => {
|
|
71
|
-
if (proc) await stopOwnedProcess(proc);
|
|
72
|
-
if (this.proc === proc) this.proc = undefined;
|
|
73
|
-
})();
|
|
74
|
-
return this.stopping;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private async start(): Promise<string | undefined> {
|
|
78
|
-
const generation = this.generation;
|
|
79
|
-
if (this.stopped) return undefined;
|
|
80
|
-
const { routing, omni, stateDir, port, log } = this.opts;
|
|
81
|
-
if (!Object.keys(routing.routes).length) throw new Error("routing.toml defines no routes");
|
|
82
|
-
const bin = this.opts.bin ?? process.env.AGENTHUB_SWITCHYARD_BIN ?? "switchyard-server";
|
|
83
|
-
const baseUrl = await omni.base();
|
|
84
|
-
const key = omni.apiKey();
|
|
85
|
-
if (!baseUrl || !key) throw new GatewayDown("no OmniRoute endpoint or key");
|
|
86
|
-
if (this.stopped || generation !== this.generation) return undefined;
|
|
87
|
-
this.upstream = baseUrl;
|
|
88
|
-
|
|
89
|
-
const file = join(stateDir, "switchyard.toml");
|
|
90
|
-
writeFileSync(file, switchyardToml(routing, { baseUrl, extraHeaders: omni.accessHeaders(baseUrl) }), { mode: 0o600 });
|
|
91
|
-
chmodSync(file, 0o600);
|
|
92
|
-
const env = { PATH: process.env.PATH ?? "", HOME: process.env.HOME ?? "", [KEY_ENV]: key };
|
|
93
|
-
const check = spawnSync(bin, ["--config", file, "--dry-run"], { env, encoding: "utf8" });
|
|
94
|
-
if (check.error) throw new Error(`${bin} not runnable: ${check.error.message}`);
|
|
95
|
-
if (check.status !== 0) throw new Error(`config rejected: ${(check.stderr || check.stdout).trim().slice(0, 300)}`);
|
|
96
|
-
|
|
97
|
-
// Switchyard's default host is 0.0.0.0; the hub binds loopback only.
|
|
98
|
-
const proc = spawn(bin, ["--config", file, "--host", "127.0.0.1", "--port", String(port)], { env, stdio: ["ignore", "ignore", "pipe"] });
|
|
99
|
-
if (this.stopped || generation !== this.generation) {
|
|
100
|
-
proc.kill("SIGTERM");
|
|
101
|
-
await stopOwnedProcess(proc);
|
|
102
|
-
return undefined;
|
|
103
|
-
}
|
|
104
|
-
this.proc = proc;
|
|
105
|
-
let exited = false;
|
|
106
|
-
// Switchyard logs every request at INFO on stderr; only problems belong in hub.log.
|
|
107
|
-
proc.stderr?.on("data", (d) => {
|
|
108
|
-
for (const line of String(d).split("\n")) if (/\b(WARN|ERROR)\b/.test(line)) log(`[switchyard] ${line.trim().slice(0, 500)}`);
|
|
109
|
-
});
|
|
110
|
-
proc.on("error", () => (exited = true));
|
|
111
|
-
proc.on("exit", (code) => {
|
|
112
|
-
exited = true;
|
|
113
|
-
if (this.proc === proc) this.disable(`exited with code ${code}`);
|
|
114
|
-
});
|
|
115
|
-
for (let i = 0; i < 100 && !exited; i++) {
|
|
116
|
-
if (this.stopped || generation !== this.generation) {
|
|
117
|
-
await stopOwnedProcess(proc);
|
|
118
|
-
if (this.proc === proc) this.proc = undefined;
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
if (await fetch(`http://127.0.0.1:${port}/health`).then((r) => r.ok, () => false)) {
|
|
122
|
-
if (this.stopped || generation !== this.generation) {
|
|
123
|
-
await stopOwnedProcess(proc);
|
|
124
|
-
if (this.proc === proc) this.proc = undefined;
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
log(`switchyard: up on 127.0.0.1:${port}, routes ${Object.keys(routing.routes).join(", ")}`);
|
|
128
|
-
return `http://127.0.0.1:${port}/v1`;
|
|
129
|
-
}
|
|
130
|
-
await Bun.sleep(100);
|
|
131
|
-
}
|
|
132
|
-
await stopOwnedProcess(proc);
|
|
133
|
-
if (this.proc === proc) this.proc = undefined;
|
|
134
|
-
throw new Error(exited ? "exited during startup" : "not healthy within 10 s");
|
|
135
|
-
}
|
|
136
|
-
}
|