@mono-agent/agent-runtime 0.20.10 → 0.20.14

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 (101) hide show
  1. package/MIGRATION.md +264 -25
  2. package/README.md +139 -448
  3. package/package.json +7 -43
  4. package/src/agent/tools/agent-tool.js +5 -5
  5. package/src/agent/tools/bash.js +1 -24
  6. package/src/agent/tools/codex-subscription-search.js +1 -1
  7. package/src/agent/tools/index.js +7 -0
  8. package/src/agent/tools/monitor.js +140 -0
  9. package/src/agent/tools/pi-bridge.js +92 -7
  10. package/src/agent/tools/shared/bash-environment.js +31 -0
  11. package/src/agent/tools/shared/monitors.js +274 -0
  12. package/src/agent/tools/shared/process-runner.js +23 -5
  13. package/src/ai/cost.js +13 -68
  14. package/src/ai/index.js +4 -17
  15. package/src/ai/pi-interop.js +65 -1
  16. package/src/ai/pi-oauth-compat.js +1 -1
  17. package/src/ai/providers/codex/app-server-client.js +592 -0
  18. package/src/ai/providers/pi-models.js +18 -10
  19. package/src/ai/providers/pi-native/compaction-driver.js +49 -21
  20. package/src/ai/providers/pi-native/harness-adapter.js +338 -0
  21. package/src/ai/providers/pi-native/result-builder.js +10 -10
  22. package/src/ai/providers/pi-native/session-lifecycle.js +95 -40
  23. package/src/ai/providers/pi-native/stream-subscriber.js +23 -5
  24. package/src/ai/providers/pi-native/turn-runner.js +34 -15
  25. package/src/ai/providers/pi-native.js +48 -22
  26. package/src/ai/runtime/capabilities.js +11 -56
  27. package/src/ai/runtime/model-refs.js +118 -153
  28. package/src/ai/runtime/registry.js +22 -56
  29. package/src/ai/runtime/router.js +48 -408
  30. package/src/ai/runtime/session-liveness.js +3 -4
  31. package/src/ai/runtime/sessions.js +4 -5
  32. package/src/ai/runtime/tool-policy.js +0 -2
  33. package/src/ai/types.js +12 -108
  34. package/src/index.js +0 -6
  35. package/src/runtime.js +8 -14
  36. package/types/agent/tools/agent-tool.d.ts +1 -2
  37. package/types/agent/tools/codex-subscription-search.d.ts +1 -1
  38. package/types/agent/tools/index.d.ts +1 -0
  39. package/types/agent/tools/monitor.d.ts +44 -0
  40. package/types/agent/tools/pi-bridge.d.ts +3 -2
  41. package/types/agent/tools/shared/bash-environment.d.ts +4 -0
  42. package/types/agent/tools/shared/monitors.d.ts +83 -0
  43. package/types/agent/tools/shared/process-runner.d.ts +11 -2
  44. package/types/ai/cost.d.ts +1 -2
  45. package/types/ai/index.d.ts +1 -4
  46. package/types/ai/pi-interop.d.ts +20 -0
  47. package/types/ai/providers/codex/app-server-client.d.ts +37 -0
  48. package/types/ai/providers/pi-native/harness-adapter.d.ts +56 -0
  49. package/types/ai/providers/pi-native/result-builder.d.ts +3 -3
  50. package/types/ai/providers/pi-native/session-lifecycle.d.ts +2 -1
  51. package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
  52. package/types/ai/providers/pi-native/turn-runner.d.ts +33 -6
  53. package/types/ai/providers/pi-native.d.ts +21 -4
  54. package/types/ai/runtime/capabilities.d.ts +21 -70
  55. package/types/ai/runtime/model-refs.d.ts +0 -24
  56. package/types/ai/runtime/router.d.ts +3 -10
  57. package/types/ai/runtime/tool-policy.d.ts +0 -2
  58. package/types/ai/types.d.ts +28 -250
  59. package/types/index.d.ts +0 -1
  60. package/src/ai/providers/acp-client.js +0 -1149
  61. package/src/ai/providers/acp-privacy.js +0 -124
  62. package/src/ai/providers/acp-public.js +0 -21
  63. package/src/ai/providers/acp-session-tokens.js +0 -282
  64. package/src/ai/providers/acp-transport.js +0 -356
  65. package/src/ai/providers/acp.js +0 -543
  66. package/src/ai/providers/claude-cli.js +0 -883
  67. package/src/ai/providers/claude-sandbox.js +0 -71
  68. package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
  69. package/src/ai/providers/claude-sdk-discovery.js +0 -352
  70. package/src/ai/providers/claude-sdk.js +0 -1127
  71. package/src/ai/providers/claude-subagent-activity.js +0 -719
  72. package/src/ai/providers/claude-subagents.js +0 -88
  73. package/src/ai/providers/codex-app.js +0 -2946
  74. package/src/ai/providers/opencode-app.js +0 -1109
  75. package/src/ai/providers/opencode-discovery.js +0 -39
  76. package/src/ai/providers/opencode-server.js +0 -508
  77. package/src/ai/runtime/context-windows.js +0 -46
  78. package/src/ai/runtime/fast-mode.js +0 -8
  79. package/src/ai/streaming/codex-events.js +0 -146
  80. package/src/ai/streaming/opencode-events.js +0 -59
  81. package/types/ai/providers/acp-client.d.ts +0 -227
  82. package/types/ai/providers/acp-privacy.d.ts +0 -25
  83. package/types/ai/providers/acp-public.d.ts +0 -7
  84. package/types/ai/providers/acp-session-tokens.d.ts +0 -41
  85. package/types/ai/providers/acp-transport.d.ts +0 -45
  86. package/types/ai/providers/acp.d.ts +0 -93
  87. package/types/ai/providers/claude-cli.d.ts +0 -305
  88. package/types/ai/providers/claude-sandbox.d.ts +0 -79
  89. package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
  90. package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
  91. package/types/ai/providers/claude-sdk.d.ts +0 -138
  92. package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
  93. package/types/ai/providers/claude-subagents.d.ts +0 -18
  94. package/types/ai/providers/codex-app.d.ts +0 -151
  95. package/types/ai/providers/opencode-app.d.ts +0 -96
  96. package/types/ai/providers/opencode-discovery.d.ts +0 -4
  97. package/types/ai/providers/opencode-server.d.ts +0 -20
  98. package/types/ai/runtime/context-windows.d.ts +0 -9
  99. package/types/ai/runtime/fast-mode.d.ts +0 -2
  100. package/types/ai/streaming/codex-events.d.ts +0 -40
  101. package/types/ai/streaming/opencode-events.d.ts +0 -42
@@ -1,7 +1,4 @@
1
- import {
2
- TOOL_POLICY_ALLOW_ALL_ONLY,
3
- TOOL_POLICY_PROJECTED,
4
- } from "./tool-policy.js";
1
+ import { TOOL_POLICY_PROJECTED } from "./tool-policy.js";
5
2
 
6
3
  export const COMMON_CAPABILITIES = {
7
4
  streaming: true,
@@ -15,18 +12,9 @@ export const COMMON_CAPABILITIES = {
15
12
  supports_live_input: true,
16
13
  supports_native_subagents: true,
17
14
  supports_request_tool_environment: false,
18
- supports_fast_mode: false,
19
- tool_policy: TOOL_POLICY_PROJECTED,
20
15
  };
21
16
 
22
17
  export const RUNTIME_CAPABILITIES = {
23
- claude: {
24
- runtime: "sdk",
25
- ...COMMON_CAPABILITIES,
26
- // The Claude Agent SDK persists sessions on disk; the bridge resumes
27
- // them via queryOptions.resume when options.sessionId is supplied.
28
- supports_session_resume: true,
29
- },
30
18
  pi: {
31
19
  runtime: "pi-agent",
32
20
  ...COMMON_CAPABILITIES,
@@ -38,50 +26,17 @@ export const RUNTIME_CAPABILITIES = {
38
26
  supports_native_subagents: false,
39
27
  supports_request_tool_environment: true,
40
28
  supports_mcp_apps: true,
41
- },
42
- codex: {
43
- runtime: "cli",
44
- ...COMMON_CAPABILITIES,
45
- // The codex-app bridge keeps the app-server subprocess + thread alive
46
- // when options.sessionKeepAlive is set; resumed turns reuse the thread.
47
- supports_session_resume: true,
48
- supports_fast_mode: true,
49
- tool_policy: TOOL_POLICY_ALLOW_ALL_ONLY,
50
- },
51
- opencode: {
52
- runtime: "cli",
53
- ...COMMON_CAPABILITIES,
54
- structured_output: false,
55
- supports_session_resume: false,
56
- supports_mcp: false,
57
- supports_skills: false,
58
- supports_live_input: false,
59
- supports_native_subagents: false,
60
- tool_policy: TOOL_POLICY_ALLOW_ALL_ONLY,
61
- },
62
- acp: {
63
- runtime: "acp-stdio",
64
- ...COMMON_CAPABILITIES,
65
- structured_output: false,
66
- supports_session_resume: true,
67
- // Runtime request-scoped MCP servers are not projected into ACP sessions.
68
- // Profiles may own static ACP MCP configuration, but that is not the
69
- // supports_mcp contract advertised to the route capability gate.
70
- supports_mcp: false,
71
- supports_skills: false,
72
- supports_builtin_tools: false,
73
- supports_live_input: false,
74
- supports_native_subagents: false,
75
- supports_request_tool_environment: false,
76
- tool_policy: TOOL_POLICY_ALLOW_ALL_ONLY,
29
+ tool_policy: TOOL_POLICY_PROJECTED,
77
30
  },
78
31
  };
79
32
 
80
- export function runtimeCapabilities(sdkOrModel) {
81
- if (!sdkOrModel) throw new Error("runtimeCapabilities requires a model reference or sdk kind");
82
- const sdk = typeof sdkOrModel === "string" ? sdkOrModel : sdkOrModel?.sdk;
83
- if (!sdk) throw new Error("runtimeCapabilities: unrecognized argument");
84
- const caps = RUNTIME_CAPABILITIES[sdk];
85
- if (!caps) throw new Error(`unknown provider sdk: ${sdk}`);
86
- return { kind: sdk, ...caps };
33
+ /**
34
+ * @param {*} [_legacySelection]
35
+ * @returns {{kind: "pi"} & typeof RUNTIME_CAPABILITIES.pi}
36
+ */
37
+ export function runtimeCapabilities(_legacySelection) {
38
+ // Capability lookup no longer selects a backend. The ignored optional value
39
+ // keeps the still-separate router migration type-safe without allowing its
40
+ // legacy route ids to influence the sole surviving runtime contract.
41
+ return { kind: "pi", ...RUNTIME_CAPABILITIES.pi };
87
42
  }
@@ -2,70 +2,139 @@
2
2
 
3
3
  /** @typedef {import('../types.js').RuntimeModelRef} RuntimeModelRef */
4
4
 
5
- const RESERVED_RUNTIME_IDS = new Set(["openai", "vercel", "claude-code", "codex-cli"]);
6
- const ACTIVE_RUNTIME_IDS = new Set(["claude", "pi", "codex", "opencode", "acp"]);
7
- const ACP_PROFILE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
5
+ const PROVIDER_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
8
6
 
9
- function requirePart(value, message) {
10
- if (!value || typeof value !== "string" || value.trim() !== value) {
11
- throw new Error(message);
7
+ /**
8
+ * Control (`Cc`), format (`Cf`), line- and paragraph-separator code points. Every one of them
9
+ * either moves the cursor or is invisible, which is what lets a model id restyle or extend
10
+ * the diagnostic that quotes it -- and an *accepted* reference is quoted verbatim, without
11
+ * re-validation, by `mono-agent validate`, `doctor`, the TUI, the web console, the daemon log
12
+ * and launchd's captured stdout, all of them line-oriented and durable.
13
+ *
14
+ * Deliberately the same set that `DIAGNOSTIC_UNSAFE_CHARACTERS` in @mono-agent/runtime-adapter
15
+ * escapes when it quotes a *rejected* value: that layer makes an unparseable value safe to
16
+ * print, this one makes a parsed value safe to print. runtime-adapter's model-reference bound
17
+ * suite asserts the two agree code point by code point rather than by comment.
18
+ */
19
+ const UNQUOTABLE_REFERENCE_CHARACTERS = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
20
+
21
+ /**
22
+ * @param {string} provider
23
+ * @param {string} model
24
+ */
25
+ function rejectRemovedRuntimeReference(provider, model) {
26
+ if (provider === "codex") {
27
+ throw new Error(`codex is no longer a runtime backend; use openai-codex:${model}`);
28
+ }
29
+ if (provider === "claude" || provider === "claude-code") {
30
+ throw new Error(`${provider} is no longer a runtime backend; use anthropic:${model}`);
31
+ }
32
+ if (provider === "codex-cli") {
33
+ throw new Error(`codex-cli is no longer a runtime backend; use openai-codex:${model}`);
34
+ }
35
+ if (provider === "acp") {
36
+ throw new Error(
37
+ "ACP is no longer a runtime backend; use <provider>:<model> for Pi models, or mono-agent bridge acp to serve mono-agent over ACP",
38
+ );
39
+ }
40
+ if (provider === "vercel") {
41
+ const replacement = model.includes(":") ? model : "<provider>:<model>";
42
+ throw new Error(`vercel:<provider>:<model> is no longer supported; use ${replacement} directly`);
43
+ }
44
+ if (provider === "opencode" && model.includes(":")) {
45
+ throw new Error(`the opencode:<provider>:<model> runtime form is no longer supported; use ${model}`);
46
+ }
47
+ }
48
+
49
+ /** @param {string} provider */
50
+ function requireProvider(provider) {
51
+ if (!PROVIDER_ID_RE.test(provider)) {
52
+ throw new Error("invalid provider id; expected [A-Za-z0-9][A-Za-z0-9._-]*");
12
53
  }
13
- return value;
14
54
  }
15
55
 
16
- function rejectTierAlias(model) {
56
+ /** @param {string} model */
57
+ function requireModel(model) {
58
+ if (!model || model.trim() !== model) {
59
+ throw new Error("model id must be a non-empty trimmed string");
60
+ }
17
61
  if (["haiku", "sonnet", "opus"].includes(model)) {
18
- throw new Error("tier aliases are not valid model references; use an exact model id");
62
+ throw new Error("tier aliases are not valid model ids; use an exact model id");
19
63
  }
20
64
  }
21
65
 
22
66
  /**
23
- * @param {string} value
24
- * @returns {string}
67
+ * The one thing a parsed reference is guaranteed to CONTAIN nothing of.
68
+ *
69
+ * A reference this parser returns is handed to every operator surface that names a model and
70
+ * quoted verbatim by each of them, without re-validation: `mono-agent validate`, `doctor`, the
71
+ * TUI, the web console, the daemon log, launchd's captured stdout. All of them are
72
+ * line-oriented and most are durable, so a control or formatting code point in the value is
73
+ * enough to restyle the diagnostic quoting it or forge a second line inside it. No legitimate
74
+ * model id contains one, nothing downstream can repair one after the fact, and escaping it at
75
+ * six renderers is six chances to miss. So it is refused here, at the source, absolutely.
76
+ *
77
+ * There is deliberately NO length rule to go with it. A grammar layer does not get to decide
78
+ * what a provider may call a model, and three rounds of trying produced three wrong answers:
79
+ * 96 bytes refused a Hugging Face GGUF repo Ollama serves today, 160 bytes refused an
80
+ * `ollama:<model>:<tag>` reference whose two halves Ollama itself validates at 80 bytes each,
81
+ * and no maximum is published in common across Ollama, LM Studio, OpenRouter and custom
82
+ * `openai_compat` endpoints from which a third guess would be any better. mono-agent's own
83
+ * `discoverLocalProviderModels` was returning ids this parser then refused.
84
+ *
85
+ * The requirements that ceiling was carrying are all met by the layers that render or transmit
86
+ * a reference, where the answer to "too long" is a shorter STRING rather than a lost route:
87
+ * - a diagnostic echo is clamped by truncation, marking the cut -- `sanitizeModelReferenceText`
88
+ * on `MODEL_REFERENCE_ECHO_MAX_BYTES`, @mono-agent/runtime-adapter;
89
+ * - an adapter error body is clamped by `sendJsonError`;
90
+ * - the `/v1/info` payload is bounded by per-contributor measured budgets and a total
91
+ * serializer fence that sheds whole fields (`channel-drivers/tui.ts`, `sendBoundedInfo`).
92
+ * Each of those is asserted at its own layer; none of them is asserted here, because none of
93
+ * them is this function's job.
94
+ *
95
+ * Runs last, after `rejectRemovedRuntimeReference`, so a retired backend still gets its
96
+ * concrete repair named (`codex:x` -> `openai-codex:x`) instead of a generic shape complaint.
97
+ * Rejection messages are operator-supplied text too, and are bounded where they are rendered.
98
+ *
99
+ * @param {string} reference
25
100
  */
26
- export function canonicalizeLegacyModelReference(value) {
27
- if (!value || typeof value !== "string") throw new Error("model reference required");
28
-
29
- if (value.startsWith("openai:")) {
30
- const model = requirePart(value.slice("openai:".length), "model id required");
31
- return `pi:openai:${model}`;
32
- }
33
- if (value.startsWith("codex:")) {
34
- const model = requirePart(value.slice("codex:".length), "model id required");
35
- return `codex:${model}`;
36
- }
37
- if (value.startsWith("vercel:")) {
38
- const rest = value.slice("vercel:".length);
39
- const i = rest.indexOf(":");
40
- if (i <= 0 || i === rest.length - 1) {
41
- throw new Error("invalid vercel model reference; expected vercel:<providerId>:<modelName>");
42
- }
43
- const provider = requirePart(rest.slice(0, i), "provider id required");
44
- const model = requirePart(rest.slice(i + 1), "model name required");
45
- return `pi:${provider}:${model}`;
46
- }
47
- if (value.startsWith("claude-code:")) {
48
- const model = requirePart(value.slice("claude-code:".length), "model id required");
49
- return `claude:${model}`;
101
+ function requireQuotableReference(reference) {
102
+ if (UNQUOTABLE_REFERENCE_CHARACTERS.test(reference)) {
103
+ throw new Error("model reference must not contain control or formatting characters");
50
104
  }
51
- return value;
52
105
  }
53
106
 
54
107
  /**
108
+ * Parse the provider/model pair after the optional legacy `pi:` wrapper has
109
+ * been removed. Pi model ids are opaque and commonly contain further colons,
110
+ * so only the first colon is structural.
111
+ *
55
112
  * @param {string} value
56
113
  * @returns {RuntimeModelRef}
57
114
  */
58
- export function normalizeRuntimeModelReference(value) {
59
- return parseRuntimeModelReference(canonicalizeLegacyModelReference(value));
115
+ function parseCanonicalReference(value) {
116
+ const separator = value.indexOf(":");
117
+ if (separator <= 0 || separator === value.length - 1) {
118
+ throw new Error("invalid model reference; expected <provider>:<model>");
119
+ }
120
+
121
+ const provider = value.slice(0, separator);
122
+ const model = value.slice(separator + 1);
123
+ const reference = `${provider}:${model}`;
124
+ requireProvider(provider);
125
+ requireModel(model);
126
+ rejectRemovedRuntimeReference(provider, model);
127
+ requireQuotableReference(reference);
128
+
129
+ return { provider, model, reference };
60
130
  }
61
131
 
62
132
  /**
63
133
  * @param {string} value
64
- * @returns {RuntimeModelRef["sdk"]}
134
+ * @returns {RuntimeModelRef}
65
135
  */
66
- export function sdkFromModelReference(value) {
67
- const parsed = parseRuntimeModelReference(value);
68
- return parsed.sdk;
136
+ export function normalizeRuntimeModelReference(value) {
137
+ return parseRuntimeModelReference(value);
69
138
  }
70
139
 
71
140
  /**
@@ -73,121 +142,17 @@ export function sdkFromModelReference(value) {
73
142
  * @returns {RuntimeModelRef}
74
143
  */
75
144
  export function parseRuntimeModelReference(value) {
76
- if (!value || typeof value !== "string") throw new Error("model reference required");
77
-
78
- if (value.startsWith("acp:")) {
79
- const profileId = requirePart(value.slice("acp:".length), "ACP profile id required");
80
- if (!ACP_PROFILE_ID_RE.test(profileId)) {
81
- throw new Error("invalid acp model reference; expected acp:<profile-id>");
82
- }
83
- return { sdk: "acp", model: profileId, reference: value };
145
+ if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
146
+ throw new Error("model reference must be a non-empty trimmed string");
84
147
  }
85
148
 
86
149
  if (value.startsWith("pi:")) {
87
- const rest = value.slice("pi:".length);
88
- const i = rest.indexOf(":");
89
- if (i <= 0 || i === rest.length - 1) {
90
- throw new Error("invalid pi model reference; expected pi:<providerId>:<modelName>");
91
- }
92
- const provider = requirePart(rest.slice(0, i), "provider id required");
93
- const model = requirePart(rest.slice(i + 1), "model id required");
94
- return { sdk: "pi", provider, model, reference: value };
95
- }
96
-
97
- if (value.startsWith("opencode:")) {
98
- // opencode:<providerID>:<modelID> — providerID/modelID come from OpenCode's own
99
- // provider registry (auth.json). Only the first colon separates them; modelID may
100
- // contain slashes (e.g. openrouter's `anthropic/claude-3.5-sonnet`).
101
- const rest = value.slice("opencode:".length);
102
- const i = rest.indexOf(":");
103
- if (i <= 0 || i === rest.length - 1) {
104
- throw new Error("invalid opencode model reference; expected opencode:<providerId>:<modelId>");
105
- }
106
- const provider = requirePart(rest.slice(0, i), "provider id required");
107
- const model = requirePart(rest.slice(i + 1), "model id required");
108
- return { sdk: "opencode", provider, model, reference: value };
109
- }
110
-
111
- const i = value.indexOf(":");
112
- if (i <= 0 || i === value.length - 1) {
113
- throw new Error("invalid model reference; expected <sdk>:<modelId>");
114
- }
115
- const sdk = value.slice(0, i);
116
- const model = requirePart(value.slice(i + 1), "model id required");
117
-
118
- if (RESERVED_RUNTIME_IDS.has(sdk)) {
119
- throw new Error(`reserved runtime id: ${sdk}; use a canonical pi:*, claude:*, or codex:* model reference`);
120
- }
121
- if (!ACTIVE_RUNTIME_IDS.has(sdk)) {
122
- throw new Error(`unknown sdk: ${sdk}`);
123
- }
124
- rejectTierAlias(model);
125
- return { sdk, model, reference: value };
126
- }
127
-
128
- export const ACTIVE_RUNTIME_KINDS = [...ACTIVE_RUNTIME_IDS];
129
- export const RESERVED_RUNTIME_KINDS = [...RESERVED_RUNTIME_IDS];
130
-
131
- // intelligence-ramp: which model refs can run under which execution_mode.
132
- // sdk='acp' → ACP only (dedicated stdio client mode)
133
- // sdk='claude' → CLI (claude binary) or SDK (Anthropic)
134
- // sdk='codex' → CLI only (codex app-server)
135
- // sdk='opencode' → CLI only (opencode server via @opencode-ai/sdk)
136
- // sdk='pi' → SDK only (the pi-native bridge handles openai-codex and other providers)
137
-
138
- // Returns null when the combo is fine; otherwise a short reason string the
139
- // UI / API can show.
140
- /**
141
- * @param {string|RuntimeModelRef} modelRefOrParsed
142
- * @param {string} executionMode
143
- * @returns {string|null}
144
- */
145
- export function executionModeIncompatibilityReason(modelRefOrParsed, executionMode) {
146
- let parsed;
147
- try {
148
- parsed = typeof modelRefOrParsed === "string"
149
- ? parseRuntimeModelReference(modelRefOrParsed)
150
- : modelRefOrParsed;
151
- } catch {
152
- return null;
153
- }
154
- if (!parsed) return null;
155
- if (!executionMode) return null;
156
- if (executionMode === "acp") {
157
- return parsed.sdk === "acp"
158
- ? null
159
- : `sdk \`${parsed.sdk}\` is not supported under ACP execution mode.`;
160
- }
161
- if (executionMode === "sdk") {
162
- if (parsed.sdk === "codex") {
163
- return "Codex CLI requires CLI execution mode.";
150
+ const wrapped = value.slice("pi:".length);
151
+ if (wrapped.indexOf(":") <= 0 || wrapped.endsWith(":")) {
152
+ throw new Error("invalid pi model reference; use pi:<provider>:<model>");
164
153
  }
165
- if (parsed.sdk === "opencode") {
166
- return "OpenCode CLI requires CLI execution mode.";
167
- }
168
- if (parsed.sdk === "acp") {
169
- return "ACP profiles require ACP execution mode.";
170
- }
171
- return null;
172
- }
173
- if (executionMode !== "cli") return null;
174
- if (parsed.sdk === "claude") return null;
175
- if (parsed.sdk === "codex") return null;
176
- if (parsed.sdk === "opencode") return null;
177
- if (parsed.sdk === "acp") return "ACP profiles require ACP execution mode.";
178
- if (parsed.sdk === "pi") {
179
- const provider = parsed.provider || "unknown";
180
- const suffix = provider === "openai-codex" ? "; use codex:<model> for Codex CLI" : "";
181
- return `Provider \`${provider}\` only runs under SDK execution mode${suffix}.`;
154
+ return parseCanonicalReference(wrapped);
182
155
  }
183
- return `sdk \`${parsed.sdk}\` is not supported under CLI execution mode.`;
184
- }
185
156
 
186
- /**
187
- * @param {string|RuntimeModelRef} modelRefOrParsed
188
- * @param {string} executionMode
189
- * @returns {boolean}
190
- */
191
- export function isModelCompatibleWithExecutionMode(modelRefOrParsed, executionMode) {
192
- return executionModeIncompatibilityReason(modelRefOrParsed, executionMode) === null;
157
+ return parseCanonicalReference(value);
193
158
  }
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
2
 
3
- import { COMMON_CAPABILITIES, runtimeCapabilities } from "./capabilities.js";
3
+ import { runtimeCapabilities } from "./capabilities.js";
4
4
 
5
5
  /**
6
6
  * `RuntimeModelRef` is referenced inline (not aliased with a top-level
@@ -20,64 +20,19 @@ import { COMMON_CAPABILITIES, runtimeCapabilities } from "./capabilities.js";
20
20
  * @property {(options?: Object) => Promise<RuntimeBridge>} load
21
21
  */
22
22
 
23
- // CLI bridges are checked first when execution_mode='cli'. Without that flag
24
- // the resolver falls through to the SDK bridges below, preserving the
25
- // pre-Phase-2 behaviour for any agent that hasn't opted in.
26
23
  /** @type {Object<string, BridgeSpec>} */
27
24
  const builtinBridgeSpecs = {
28
- "acp-stdio": {
29
- id: "acp-stdio",
30
- supports: (ref, options) => ref?.sdk === "acp" && options?.executionMode === "acp",
31
- capabilities: () => runtimeCapabilities("acp"),
32
- load: async () => (await import("../providers/acp.js")).acpRuntimeBridge,
33
- },
34
- "claude-code": {
35
- id: "claude-code",
36
- supports: (ref, options) => ref?.sdk === "claude" && options?.executionMode === "cli",
37
- // The claude CLI resumes prior sessions via `--resume <sessionId>`.
38
- capabilities: () => ({
39
- kind: "claude-code",
40
- runtime: "cli",
41
- ...COMMON_CAPABILITIES,
42
- supports_session_resume: true,
43
- // The one-shot CLI bridge has no bidirectional stdin steering channel.
44
- supports_live_input: false,
45
- }),
46
- load: async () => (await import("../providers/claude-cli.js")).claudeCodeRuntimeBridge,
47
- },
48
- "codex-app": {
49
- id: "codex-app",
50
- supports: (ref, options) => ref?.sdk === "codex" && options?.executionMode === "cli",
51
- // The codex-app bridge keeps the app-server subprocess + thread alive
52
- // across turns when options.sessionKeepAlive is set.
53
- capabilities: () => {
54
- const { kind: _sdkKind, ...capabilities } = runtimeCapabilities("codex");
55
- return { kind: "codex-app", ...capabilities };
56
- },
57
- load: async () => (await import("../providers/codex-app.js")).codexAppRuntimeBridge,
58
- },
59
- "opencode-app": {
60
- id: "opencode-app",
61
- supports: (ref, options) => ref?.sdk === "opencode" && options?.executionMode === "cli",
62
- capabilities: () => {
63
- const { kind: _sdkKind, ...capabilities } = runtimeCapabilities("opencode");
64
- return { kind: "opencode-app", ...capabilities };
65
- },
66
- load: async () => (await import("../providers/opencode-app.js")).opencodeAppRuntimeBridge,
67
- },
68
- claude: {
69
- id: "claude",
70
- supports: (ref) => ref?.sdk === "claude",
71
- capabilities: () => runtimeCapabilities("claude"),
72
- load: async () => (await import("../providers/claude-sdk.js")).claudeRuntimeBridge,
73
- },
74
25
  pi: {
75
26
  id: "pi",
76
- supports: (ref) => ref?.sdk === "pi",
77
- capabilities: () => runtimeCapabilities("pi"),
78
- // The pi-native AgentHarness bridge is the sole pi runtime path. The
79
- // hand-rolled pi-sdk bridge was removed once native reached parity.
80
- load: async () => (await import("../providers/pi-native.js")).piNativeRuntimeBridge,
27
+ supports: () => true,
28
+ capabilities: () => runtimeCapabilities(),
29
+ load: async () => {
30
+ const { piNativeRuntimeBridge } = await import("../providers/pi-native.js");
31
+ // JavaScript widens the bridge object's `id` property to string. Keep the
32
+ // literal-safe registry contract here so the Pi provider module remains
33
+ // untouched by this bridge-removal work package.
34
+ return /** @type {RuntimeBridge} */ (piNativeRuntimeBridge);
35
+ },
81
36
  },
82
37
  };
83
38
 
@@ -98,10 +53,21 @@ export function listRuntimeBridges() {
98
53
  * @returns {Promise<RuntimeBridge>}
99
54
  */
100
55
  export async function resolveRuntimeBridge(modelRef, options = {}) {
56
+ // Direct kernel callers may bypass the parser, so malformed references must
57
+ // still fail before the sole bridge receives them.
58
+ if (
59
+ typeof modelRef?.provider !== "string"
60
+ || modelRef.provider.length === 0
61
+ || typeof modelRef.model !== "string"
62
+ || modelRef.model.length === 0
63
+ || modelRef.reference !== `${modelRef.provider}:${modelRef.model}`
64
+ ) {
65
+ throw new Error("unsupported model reference: expected <provider>:<model>");
66
+ }
101
67
  for (const spec of Object.values(builtinBridgeSpecs)) {
102
68
  if (spec.supports(modelRef, options)) return spec.load(options);
103
69
  }
104
- throw new Error(`unsupported sdk: ${modelRef?.sdk || "unknown"}`);
70
+ throw new Error(`unsupported model reference: ${modelRef.reference}`);
105
71
  }
106
72
 
107
73
  export { RUNTIME_CAPABILITIES, runtimeCapabilities } from "./capabilities.js";