@staix/agent-hub 0.12.39 → 0.12.41

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +16 -10
  3. package/docs/agent-notes/adapters.md +4 -1
  4. package/docs/agent-notes/budget.md +2 -0
  5. package/docs/agent-notes/bus.md +2 -2
  6. package/docs/agent-notes/daemon.md +10 -6
  7. package/docs/agent-notes/models.md +4 -4
  8. package/docs/agent-notes/tasks.md +3 -0
  9. package/docs/agent-notes/tests.md +1 -1
  10. package/docs/memory.md +1 -1
  11. package/docs/operations.md +152 -28
  12. package/docs/quickstart.md +23 -5
  13. package/docs/security.md +20 -8
  14. package/docs/smoke.md +2 -2
  15. package/docs/specs/2026-09-19-agent-hub-design.md +158 -70
  16. package/docs/specs/2026-10-04-switchyard-source-port-design.md +12 -7
  17. package/docs/verification/2026-10-20-0.12.40.md +63 -0
  18. package/docs/verification/2026-10-20-0.12.41.md +44 -0
  19. package/docs/verified.json +38 -19
  20. package/package.json +1 -1
  21. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  22. package/plugins/agent-hub/server.js +415 -151
  23. package/src/adapters/acp.ts +34 -1
  24. package/src/adapters/claude-channel.ts +35 -4
  25. package/src/adapters/codex-appserver.ts +12 -3
  26. package/src/adapters/definitions/builtin-metadata.ts +4 -2
  27. package/src/adapters/definitions/native-cli-codex-launch.ts +2 -2
  28. package/src/adapters/definitions/native-cli-doctor.ts +8 -5
  29. package/src/adapters/definitions/native-cli-entry-claude.ts +7 -2
  30. package/src/adapters/definitions/native-cli-entry-codex.ts +13 -14
  31. package/src/adapters/definitions/native-cli-entry-jcode.ts +13 -3
  32. package/src/adapters/definitions/native-cli-entry-pi.ts +9 -7
  33. package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
  34. package/src/adapters/definitions/native-cli-jcode-mcp.ts +55 -1
  35. package/src/adapters/jcode.ts +110 -4
  36. package/src/adapters/local-worker.ts +21 -37
  37. package/src/adapters/pi.ts +7 -0
  38. package/src/adapters/registry-types.ts +2 -0
  39. package/src/cli/console-state.ts +1 -0
  40. package/src/cli/console.ts +11 -1
  41. package/src/cli/help.ts +13 -8
  42. package/src/cli/identity-audit.ts +33 -7
  43. package/src/cli/jcode-mcp.ts +1 -1
  44. package/src/cli/launch.ts +1 -1
  45. package/src/cli/main.ts +134 -24
  46. package/src/cli/output.ts +5 -2
  47. package/src/cli/status-lines.ts +1 -1
  48. package/src/cli/terminal-recovery.ts +54 -4
  49. package/src/cli/upgrade-interactive.ts +5 -0
  50. package/src/cli/upgrade-runtime.ts +13 -1
  51. package/src/cli/upgrade.ts +67 -1
  52. package/src/hub/board.ts +4 -2
  53. package/src/hub/bus.ts +8 -3
  54. package/src/hub/checks.ts +15 -5
  55. package/src/hub/conductor.ts +2 -1
  56. package/src/hub/config-apply.ts +20 -2
  57. package/src/hub/config-reload.ts +2 -8
  58. package/src/hub/control-client.ts +26 -3
  59. package/src/hub/daemon.ts +155 -68
  60. package/src/hub/delivery-superseded.ts +19 -2
  61. package/src/hub/events.ts +4 -1
  62. package/src/hub/hub-tools.ts +4 -4
  63. package/src/hub/inference.ts +4 -8
  64. package/src/hub/lifecycle.ts +3 -3
  65. package/src/hub/main-checkout.ts +36 -0
  66. package/src/hub/memory-profile-check.ts +6 -2
  67. package/src/hub/memory-profile.ts +18 -1
  68. package/src/hub/peers.ts +2 -0
  69. package/src/hub/ports.ts +1 -0
  70. package/src/hub/routing.ts +24 -14
  71. package/src/hub/settings.ts +1 -1
  72. package/src/hub/task-repository.ts +136 -0
  73. package/src/hub/tasks.ts +101 -21
  74. package/src/hub/terminal-launch.ts +130 -0
  75. package/src/hub/usage.ts +44 -18
  76. package/src/models/route/config.ts +1 -1
  77. package/src/omniroute/client.ts +7 -19
  78. package/templates/AGENTS.block.md +1 -0
  79. package/templates/routing.toml +10 -44
  80. package/src/switchyard/config.ts +0 -45
  81. package/src/switchyard/sidecar.ts +0 -136
@@ -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
- # [targets.*] and [routes.*] use Switchyard's own table shapes (v0.2.0 toml_schema): the hub copies them into the
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 = "sy/coding" # Switchyard route the local worker asks for
13
- fixed_model = "vllm/deepseek-ai/DeepSeek-V4-Flash-0731" # straight to OmniRoute when the sidecar is absent or unhealthy
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 = "sy/coding"
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
- route = "sy/fast"
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
- route = "sy/fast"
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
- route = "sy/fast"
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
- route = "sy/fast"
67
+ fixed_model = "fast"
102
68
  pi_backend = "mlx"
103
69
  escalate_to = ["codex", "kimi"]
104
70
 
105
- # In-process L2 routes are opt-in: set local.route or a class route to hub/<id>.
106
- # They never enter the optional Switchyard sidecar TOML. Defaults are efficient fast, capable coding.
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"
@@ -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
- }