@junghanacs/entwurf 0.52.0 → 0.53.1
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/AGENTS.md +1 -1
- package/BASELINE.md +116 -397
- package/CHANGELOG.md +131 -2
- package/CONTRIBUTING.md +4 -4
- package/DELIVERY.md +2 -2
- package/ENTWURF-CLI.json +1 -1
- package/README.md +7 -4
- package/VERIFY.md +332 -142
- package/demo/demo-baseline.sh +1 -1
- package/demo/demo.sh +1 -1
- package/docs/acp-backend-rail.md +248 -34
- package/docs/setup-clean-host.md +50 -65
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +90 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +4 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +15 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/model-ids.js +9 -12
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +41 -56
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +33 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -1
- package/package.json +4 -4
- package/pi/settings.reference.json +0 -1
- package/pi-extensions/acp-provider.ts +12 -6
- package/pi-extensions/lib/acp/acp-client.ts +6 -7
- package/pi-extensions/lib/acp/backend-adapter.ts +139 -15
- package/pi-extensions/lib/acp/backend.ts +261 -32
- package/pi-extensions/lib/acp/config.ts +4 -9
- package/pi-extensions/lib/acp/context.ts +17 -0
- package/pi-extensions/lib/acp/event-mapper.ts +236 -17
- package/pi-extensions/lib/acp/model-ids.js +9 -12
- package/pi-extensions/lib/acp/models.ts +46 -57
- package/pi-extensions/lib/acp/tool-surface.ts +34 -6
- package/pi-extensions/lib/fresh-call-composition.ts +37 -3
- package/pi-extensions/lib/herdr-fresh-call.ts +3 -1
- package/pi-extensions/lib/mux-fresh-call.ts +3 -1
- package/run.sh +25 -86
- package/scripts/check-acp-carrier-augment.ts +3 -21
- package/scripts/check-acp-prompt-lifecycle.ts +24 -3
- package/scripts/check-acp-provider-surface.ts +17 -26
- package/scripts/check-acp-session-reuse.ts +23 -4
- package/scripts/check-acp-stop-reason.ts +22 -3
- package/scripts/check-acp-stream-hooks.ts +24 -3
- package/scripts/check-acp-usage-accounting.ts +53 -24
- package/scripts/check-copilot-receive-arm.ts +19 -2
- package/scripts/check-fresh-call-dispatch.ts +9 -3
- package/scripts/check-gate-qualification.ts +5 -5
- package/scripts/check-herdr-fresh-call.ts +1 -0
- package/scripts/check-install-surface.ts +6 -0
- package/scripts/check-probe-ordering.ts +34 -24
- package/scripts/check-release-gate-outcomes.ts +9 -23
- package/scripts/lib/probe-cli-target.ts +3 -2
- package/scripts/mutants/acp-augment.json +0 -26
- package/scripts/mutants/acp-cortex.json +10 -183
- package/scripts/mutants/acp-foreground-only.json +11 -0
- package/scripts/mutants/acp-launch-namespace.json +3 -1
- package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
- package/scripts/mutants/acp-set-model-readback.json +488 -0
- package/scripts/mutants/mux-fresh-call.json +12 -1
- package/scripts/mutants/pi-floor.json +2 -2
- package/scripts/mutants/release-gate.json +3 -3
- package/scripts/raw-acp-compaction-measure/probe.ts +8 -2
- package/scripts/smoke-acp-cortex-live.ts +4 -408
- package/scripts/smoke-acp-ordering-probe-live.ts +7 -1
- package/scripts/smoke-acp-session-reuse-live.ts +11 -2
- package/scripts/smoke-herdr-raw-install-live.sh +4 -4
- package/scripts/check-acp-cortex.ts +0 -710
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
// ACP plugin — curated model surface
|
|
2
|
-
// in 0.13).
|
|
1
|
+
// ACP plugin — curated Claude model surface. Cortex ACP is disabled.
|
|
3
2
|
//
|
|
4
3
|
// entwurf is an ACP *plugin* on the v2 core, NOT a general-purpose
|
|
5
4
|
// Anthropic provider. It deliberately does not expose the full pi-ai model
|
|
6
5
|
// registry — the surface is curated to the ids the plugin commits to driving
|
|
7
6
|
// through a real ACP backend.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// the `cortex-` prefixed Snowflake Cortex Code ids (the second backend, landed
|
|
12
|
-
// 0.13.0 — docs/acp-backend-rail.md “Cortex Code audit”). Codex has native
|
|
13
|
-
// delivery-probe evidence but no managed citizen lane; neither it nor Gemini is an ACP backend here.
|
|
14
|
-
// A third governed backend EXTENDS this file the same way — it does not change
|
|
15
|
-
// the pattern.
|
|
8
|
+
// Only the unprefixed Claude IDs are registered. Historical Cortex IDs are
|
|
9
|
+
// reserved refusal inputs; native citizens are independent of this provider.
|
|
16
10
|
// pi 0.80 migration: the standalone root `getModels()` moved to the deprecated
|
|
17
11
|
// `@earendil-works/pi-ai/compat` entrypoint (the global-API churn). We import it
|
|
18
12
|
// from `/compat` directly.
|
|
@@ -36,7 +30,7 @@ import { CURATED_ANCHOR_MODEL_ID, SUPPORTED_ANTHROPIC_MODEL_IDS } from "./model-
|
|
|
36
30
|
// The provider id and the curated ID lists live in the dependency-free
|
|
37
31
|
// `model-ids.js` leaf, shared with the launch seams (#128); this file is the only
|
|
38
32
|
// one that loads the upstream catalog to turn them into registry rows.
|
|
39
|
-
export { CORTEX_MODEL_PREFIX, CURATED_ANCHOR_MODEL_ID, PROVIDER_ID, SUPPORTED_ANTHROPIC_MODEL_IDS,
|
|
33
|
+
export { CORTEX_MODEL_PREFIX, CURATED_ANCHOR_MODEL_ID, LEGACY_CORTEX_MODEL_IDS, PROVIDER_ID, SUPPORTED_ANTHROPIC_MODEL_IDS, } from "./model-ids.js";
|
|
40
34
|
// #26 auth-boundary sentinel. `pi.registerProvider` requires an apiKey when a
|
|
41
35
|
// provider defines custom models, but the ACP plugin consumes NO key: backend
|
|
42
36
|
// auth belongs to the operator's own Claude CLI child process (AGENTS
|
|
@@ -70,54 +64,44 @@ function requireRegistryModel(models, id) {
|
|
|
70
64
|
function claudeContextWindow(model) {
|
|
71
65
|
return Math.min(model.contextWindow, CLAUDE_CONTEXT_DEFAULT);
|
|
72
66
|
}
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// entwurf's ACP rail is backend-extensible: Cortex is the first NON-claude
|
|
76
|
-
// backend to land through it (docs/acp-backend-rail.md, “Shipped adapters”). pi-ai carries no
|
|
77
|
-
// snowflake/cortex model source, so this surface is HAND-CURATED. Every id
|
|
78
|
-
// carries the reserved `cortex-` prefix: the prefix is the SINGLE routing
|
|
79
|
-
// authority (backend-adapter.ts `routeModel`) and keeps the ids off the Claude
|
|
80
|
-
// native ids Cortex routes to (`cortex-claude-sonnet-5` vs the unprefixed
|
|
81
|
-
// `claude-sonnet-5-5` the claude adapter owns). `routeModel` strips the prefix to
|
|
82
|
-
// recover the native id, which is enforced per-turn via
|
|
83
|
-
// session/set_config_option — never a launch `-m` pin (CP0-M).
|
|
84
|
-
//
|
|
85
|
-
// The 4-row set is the GLG-decided curation (2026-07-29) — a subset of the 11
|
|
86
|
-
// native ids cortex v1.1.52 exposed live when authenticated. A curated id that
|
|
87
|
-
// the running cortex no longer serves fails LOUD at set-model, before the
|
|
88
|
-
// prompt (measured: `Unsupported model: …` names the live set). Adding an id
|
|
89
|
-
// here is the same verify-both-axes commitment as the Claude set — do not
|
|
90
|
-
// extend casually. The prefix and the four ids are declared in model-ids.js.
|
|
91
|
-
// Hand-set conservative context window. Cortex reports the LIVE window via its
|
|
92
|
-
// own ACP session config, not this curated metadata — this is a floor for the
|
|
93
|
-
// registry surface, deliberately not inflated to the Claude 1M.
|
|
94
|
-
const CORTEX_CONTEXT_DEFAULT = 200_000;
|
|
67
|
+
// Cortex ACP is disabled; its former IDs are refusal inputs, not model rows.
|
|
95
68
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
69
|
+
* What an ACP row declares it can take as input: text only. The rail delivers pi's transcript as
|
|
70
|
+
* ACP text blocks; an image becomes the marker `[image omitted: <mime>]` (`context.ts`), and no
|
|
71
|
+
* other path carries image bytes to the backend. pi's catalog lists the Anthropic models as
|
|
72
|
+
* `["text", "image"]` — true of the model behind the API, not of this transport — so copying it
|
|
73
|
+
* would advertise a capability the rail does not deliver (#142 landing review). A backend gains
|
|
74
|
+
* `image` here only together with a real image path and its proof.
|
|
102
75
|
*/
|
|
76
|
+
const ACP_ROW_INPUT = ["text"];
|
|
77
|
+
/** The retired Cortex adapter contributes no supported model rows. */
|
|
103
78
|
export function curatedCortexModels() {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The effort values claude-agent-acp offers on its `effort` config option (category
|
|
83
|
+
* `thought_level`). `[측정 2026-11-13]` 0.106.0, authenticated, no prompt (#142): the option lists
|
|
84
|
+
* `default, low, medium, high, xhigh, max`; `high → low → max` each read back on one live session and
|
|
85
|
+
* `minimal` was refused `-32602`. `default` is the vendor's own choice, so no pi level maps to it.
|
|
86
|
+
*/
|
|
87
|
+
export const CLAUDE_ACP_EFFORT_VALUES = ["low", "medium", "high", "xhigh", "max"];
|
|
88
|
+
/**
|
|
89
|
+
* The pi `thinkingLevelMap` an ACP Claude row registers (#142). pi's documented model seam: the
|
|
90
|
+
* value is what the provider sends for each pi level, `null` marks a level unsupported, and xhigh/max
|
|
91
|
+
* appear only when mapped (`getSupportedThinkingLevels`). Each supported pi level maps to the effort
|
|
92
|
+
* value of the same name. `off` and `minimal` have no effort value, so they are `null` and never
|
|
93
|
+
* offered; a level the registry already marks unsupported for this model stays unsupported, and
|
|
94
|
+
* xhigh/max stay hidden where the registry does not map them. Nothing here is a model catalog: the
|
|
95
|
+
* registry decides which levels the model has, this decides which of them ACP can carry.
|
|
96
|
+
*/
|
|
97
|
+
export function claudeAcpThinkingLevelMap(registry) {
|
|
98
|
+
const map = { off: null, minimal: null };
|
|
99
|
+
for (const level of CLAUDE_ACP_EFFORT_VALUES) {
|
|
100
|
+
const declared = registry?.[level];
|
|
101
|
+
const unmapped = declared === undefined && (level === "xhigh" || level === "max");
|
|
102
|
+
map[level] = declared === null || unmapped ? null : level;
|
|
103
|
+
}
|
|
104
|
+
return map;
|
|
121
105
|
}
|
|
122
106
|
/**
|
|
123
107
|
* The curated Claude model rows handed to `pi.registerProvider({ models })`.
|
|
@@ -130,7 +114,8 @@ export function curatedClaudeModels() {
|
|
|
130
114
|
id: m.id,
|
|
131
115
|
name: m.name,
|
|
132
116
|
reasoning: m.reasoning,
|
|
133
|
-
|
|
117
|
+
thinkingLevelMap: claudeAcpThinkingLevelMap(m.thinkingLevelMap),
|
|
118
|
+
input: [...ACP_ROW_INPUT],
|
|
134
119
|
cost: m.cost,
|
|
135
120
|
contextWindow: claudeContextWindow(m),
|
|
136
121
|
maxTokens: m.maxTokens,
|
|
@@ -12,13 +12,10 @@
|
|
|
12
12
|
//
|
|
13
13
|
// The module's TWO jobs have different backend scopes — do not collapse them:
|
|
14
14
|
// - `buildClaudeSessionMeta` is claude-only by CONSTRUCTION, with exactly one
|
|
15
|
-
// caller (`claudeAdapter.buildSessionMeta`).
|
|
16
|
-
// `_meta.systemPrompt` and no tools/permission declaration surface (native
|
|
17
|
-
// tools + its own overlay mcp.json projection), so its adapter returns
|
|
18
|
-
// `undefined` and that function is never reached for a cortex turn.
|
|
15
|
+
// caller (`claudeAdapter.buildSessionMeta`).
|
|
19
16
|
// - `assertExcludeToolsHonored` is BACKEND-INVARIANT: backend.ts runs it for
|
|
20
|
-
// every adapter before any spawn, passing `backend: adapter.backend`
|
|
21
|
-
// resolved `config.tools`.
|
|
17
|
+
// every admitted adapter before any spawn, passing `backend: adapter.backend`
|
|
18
|
+
// and the resolved `config.tools`. Cortex ACP is disabled.
|
|
22
19
|
// That split is why the `backend` field exists: the honest-divergence logic stays
|
|
23
20
|
// explicit and the matrix gate can exercise both the "claude narrows via tools"
|
|
24
21
|
// and "native always exposes" branches.
|
|
@@ -104,6 +101,28 @@ export function assertExcludeToolsHonored(activeToolNames, resolved) {
|
|
|
104
101
|
". Extension tools (entwurf_v2, entwurf_peers) can be excluded freely — they are pi-side and never reach the backend.");
|
|
105
102
|
}
|
|
106
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* #163: is this Pi's own summarization request — the one tool-less turn the preflight above
|
|
106
|
+
* honours instead of refusing?
|
|
107
|
+
*
|
|
108
|
+
* Pi summarizes for `/compact`, the compaction threshold, overflow recovery and `/tree` branch
|
|
109
|
+
* summaries through one call (pi-coding-agent 1.5.2 `dist/core/compaction/compaction.js:476-489`,
|
|
110
|
+
* `completeSummarization`; branch summaries at `compaction/branch-summarization.js:233-237`). Its
|
|
111
|
+
* context declares no tools (`buildSummarizationContext`, `compaction.js:499-509`) and its options
|
|
112
|
+
* carry `cacheRetention: "none"` (`compaction.js:482`), which an ordinary agent-loop turn does not
|
|
113
|
+
* (pi-agent-core `dist/agent.js:306-330`).
|
|
114
|
+
*
|
|
115
|
+
* BOTH are required. An empty surface alone is also an operator who excluded every tool on an
|
|
116
|
+
* ordinary turn, which keeps today's refusal; `cacheRetention` alone is a request option, not a
|
|
117
|
+
* surface. Neither the summarization prompt's text nor a missing session id is read: Pi fills the
|
|
118
|
+
* id with a fresh `uuidv7()` (`compaction.js:481`), and matching its prompt would copy it.
|
|
119
|
+
* `cacheRetention` is a Pi convention, not a contract — the oracle beside this calls Pi's own
|
|
120
|
+
* `completeSummarization` so a Pi upgrade that drops it turns that test red rather than this
|
|
121
|
+
* path silently back into a refusal.
|
|
122
|
+
*/
|
|
123
|
+
export function isPiSummaryRequest(activeToolNames, cacheRetention) {
|
|
124
|
+
return activeToolNames.length === 0 && cacheRetention === "none";
|
|
125
|
+
}
|
|
107
126
|
/**
|
|
108
127
|
* Build the `_meta` object handed to `newSession` for a Claude ACP session.
|
|
109
128
|
*
|
|
@@ -144,6 +163,14 @@ export function buildClaudeSessionMeta(params, normalizedSystemPrompt) {
|
|
|
144
163
|
// the carrier's "doesn't-know" — keeping both means a future preset/SDK
|
|
145
164
|
// change cannot silently re-open memory through a channel the strip misses.
|
|
146
165
|
autoMemoryEnabled: false,
|
|
166
|
+
// No turn of the adapter's own (#142). claude-agent-acp 0.101.0 compacts an idle session
|
|
167
|
+
// before its 1h prompt cache expires by sending `/compact` as a prompt nobody here issued;
|
|
168
|
+
// a retained session's callbacks drop updates that arrive with no active turn, so that
|
|
169
|
+
// model turn would spend quota and replace context invisibly to pi. 0.106.0 (#1555) reads
|
|
170
|
+
// this inline key when no managed tier sets one. Only the IDLE trigger is off:
|
|
171
|
+
// `autoCompactEnabled` stays untouched, so Claude Code's own compaction inside a prompt
|
|
172
|
+
// we sent keeps working.
|
|
173
|
+
idleCompaction: false,
|
|
147
174
|
},
|
|
148
175
|
};
|
|
149
176
|
if (params.skillPlugins.length > 0) {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* (#116 D1), and its two-step launch breaks the ordering argument outright.
|
|
23
23
|
*/
|
|
24
24
|
import { randomBytes } from "node:crypto";
|
|
25
|
+
import { PROVIDER_ID, SUPPORTED_ANTHROPIC_MODEL_IDS } from "./acp/model-ids.js";
|
|
25
26
|
/** The backends this rail can open. Fixed set, not a profile — a further one is a decision,
|
|
26
27
|
* not a config entry. `copilot` was added by #82 RAIL 9, `omp` by #87 Bundle C, `codex`
|
|
27
28
|
* by #95 after its system birth and app-server rails were measured, and `pi-durable` by #129
|
|
@@ -142,6 +143,35 @@ export const FRESH_CALL_TOOL_LOAD_HINT = {
|
|
|
142
143
|
codex: [],
|
|
143
144
|
"pi-durable": [],
|
|
144
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* Which dialect table spells the bridge tools for THIS launch. The backend picks the runtime; the
|
|
148
|
+
* model can pick a different tool surface underneath it.
|
|
149
|
+
*
|
|
150
|
+
* `[측정 2026-11-13, D1]` a pi sibling opened on an ACP Claude model (`entwurf/claude-*`) does not
|
|
151
|
+
* see Pi's MCP names: its turns run inside the Claude Agent SDK, which mints the bridge tools as
|
|
152
|
+
* `mcp__entwurf-bridge__*` (the ACP Sonnet interview of 2026-11-13, n=1, called them by the schema
|
|
153
|
+
* it was given, not by the pi spelling its framing named). So the NAMES follow the claude-code
|
|
154
|
+
* table. The load hint stays the pi row (none): in that n=1 the bridge tools were directly callable
|
|
155
|
+
* without loading. Whether ToolSearch exists on this surface, or deferral can occur with more
|
|
156
|
+
* declared servers (`acp/tool-surface.ts` names the SDK's deferred tools), is unmeasured — not a
|
|
157
|
+
* claim that it cannot.
|
|
158
|
+
*
|
|
159
|
+
* Only an exact curated Anthropic id switches. A thinking suffix (`:high`) or fuzzy alias never
|
|
160
|
+
* reaches here through the public verb: the dispatch root refuses any `entwurf/` id that is not
|
|
161
|
+
* exactly curated (`isUncuratedEntwurfPiModel`, #128), and the test pins that alignment. Retired
|
|
162
|
+
* Cortex ids are refused at the dispatch root before framing. The ACP id list arrives from
|
|
163
|
+
* `acp/model-ids.js`, an import-free module.
|
|
164
|
+
*/
|
|
165
|
+
export function freshCallToolDialect(backend, model) {
|
|
166
|
+
if (backend !== "pi")
|
|
167
|
+
return backend;
|
|
168
|
+
const trimmed = model.trim();
|
|
169
|
+
const slash = trimmed.indexOf("/");
|
|
170
|
+
if (slash < 0 || trimmed.slice(0, slash) !== PROVIDER_ID)
|
|
171
|
+
return backend;
|
|
172
|
+
const id = trimmed.slice(slash + 1);
|
|
173
|
+
return SUPPORTED_ANTHROPIC_MODEL_IDS.includes(id) ? "claude-code" : backend;
|
|
174
|
+
}
|
|
145
175
|
/**
|
|
146
176
|
* The omp bootstrap flag, spelled WITHOUT dashes — the vendor's flag map is keyed by bare
|
|
147
177
|
* name (`extensions/loader.ts:221-228`) and this composition adds the `--` itself.
|
|
@@ -379,9 +409,10 @@ export function composeBackendArgs(backend, composition, model, resolveCodexSock
|
|
|
379
409
|
* corroborate the caller instead of an instruction not to look.
|
|
380
410
|
*/
|
|
381
411
|
export function composeFreshCallFraming(params) {
|
|
382
|
-
const
|
|
383
|
-
const
|
|
384
|
-
const
|
|
412
|
+
const dialect = freshCallToolDialect(params.backend, params.model);
|
|
413
|
+
const birthTool = FRESH_CALL_CALLBACK_TOOL[dialect];
|
|
414
|
+
const deliveryTool = FRESH_CALL_DELIVERY_TOOL[dialect];
|
|
415
|
+
const peersTool = FRESH_CALL_PEERS_TOOL[dialect];
|
|
385
416
|
// The hint is a per-backend TEMPLATE so the two tool dialects stay the single source of their
|
|
386
417
|
// own spelling here too — a hint that hard-coded Claude's name would go stale the day a dialect
|
|
387
418
|
// moves, and it would go stale silently, in the one sentence a stuck child depends on.
|
|
@@ -554,6 +554,7 @@ export async function herdrFreshCall(params, run, env, nonce, clock = HERDR_REAL
|
|
|
554
554
|
return { ok: false, reason: badCwd };
|
|
555
555
|
const framing = composeFreshCallFraming({
|
|
556
556
|
backend,
|
|
557
|
+
model,
|
|
557
558
|
callerGardenId,
|
|
558
559
|
nonce,
|
|
559
560
|
openingLine: HERDR_FRESH_CALL_OPENING_LINE,
|
|
@@ -1220,6 +1221,7 @@ async function herdrLayoutFreshCall(params, context, env, nonce, deps) {
|
|
|
1220
1221
|
return { ok: false, reason: "herdr-socket-path-missing" };
|
|
1221
1222
|
const prompt = composeFreshCallPrompt({
|
|
1222
1223
|
backend: "pi-durable",
|
|
1224
|
+
model,
|
|
1223
1225
|
task,
|
|
1224
1226
|
callerGardenId,
|
|
1225
1227
|
nonce,
|
|
@@ -1287,7 +1289,7 @@ export const HERDR_REJECT_HINT = {
|
|
|
1287
1289
|
"model-invalid": "the model is not in the accepted grammar (no whitespace, no leading dash)",
|
|
1288
1290
|
// #128: decided at the dispatch root (`fresh-call-dispatch.ts`), worded here because this rail's
|
|
1289
1291
|
// import fence admits no ACP module. The curated list itself lives in `acp/model-ids.js`.
|
|
1290
|
-
"acp-model-not-curated": "an entwurf model must be one exact curated ACP id
|
|
1292
|
+
"acp-model-not-curated": "an entwurf model must be one exact curated ACP id; Cortex ACP is disabled. Pi may otherwise pick another row by substring, so nothing was opened or aliased",
|
|
1291
1293
|
"task-empty": "a sibling opened with nothing to do is a window nobody asked for",
|
|
1292
1294
|
"task-too-long": `the task is over the ${TASK_MAX_CHARS}-character interface bound this verb shares with entwurf_v2`,
|
|
1293
1295
|
"herdr-context-missing": "HERDR_ENV/HERDR_BIN_PATH are absent, so this process is not inside herdr and this rail does not exist here",
|
|
@@ -331,7 +331,7 @@ export function freshCall(params, env = process.env, nonce = mintNonce()) {
|
|
|
331
331
|
const runtime = layout === null ? null : checkPiDurableRuntime(layout);
|
|
332
332
|
const missing = piDurableFreshPreflight({
|
|
333
333
|
model,
|
|
334
|
-
firstInput: buildFreshCallPrompt({ backend: params.backend, task, callerGardenId, nonce }),
|
|
334
|
+
firstInput: buildFreshCallPrompt({ backend: params.backend, model, task, callerGardenId, nonce }),
|
|
335
335
|
layout,
|
|
336
336
|
runtime: runtime === null || runtime.ok ? null : runtime.reason,
|
|
337
337
|
});
|
|
@@ -373,6 +373,7 @@ export function freshCall(params, env = process.env, nonce = mintNonce()) {
|
|
|
373
373
|
const composition = {
|
|
374
374
|
prompt: buildFreshCallPrompt({
|
|
375
375
|
backend: params.backend,
|
|
376
|
+
model,
|
|
376
377
|
task,
|
|
377
378
|
callerGardenId,
|
|
378
379
|
nonce,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junghanacs/entwurf",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Visible sibling sessions across independent agent harnesses: launch in Herdr or tmux, retire on request, visibly reopen supported sessions under the same id, and deliver messages by garden id; native bridges and Claude
|
|
3
|
+
"version": "0.53.1",
|
|
4
|
+
"description": "Visible sibling sessions across independent agent harnesses: launch in Herdr or tmux, retire on request, visibly reopen supported sessions under the same id, and deliver messages by garden id; native bridges and a Claude ACP adapter.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Junghan Kim",
|
|
@@ -102,8 +102,8 @@
|
|
|
102
102
|
"claudeCodeFloor": ">=2.1.306"
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
105
|
-
"@agentclientprotocol/claude-agent-acp": "0.
|
|
106
|
-
"@agentclientprotocol/sdk": "
|
|
105
|
+
"@agentclientprotocol/claude-agent-acp": "0.107.0",
|
|
106
|
+
"@agentclientprotocol/sdk": "2.6.0",
|
|
107
107
|
"@anthropic-ai/sdk": "0.100.1",
|
|
108
108
|
"@earendil-works/chord": "1.5.2",
|
|
109
109
|
"@earendil-works/pi-agent-core": "1.5.2",
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
// This is the pi-extension entry point that registers `entwurf` as a pi
|
|
4
4
|
// session provider/model. It is intentionally THIN: it stands up the provider
|
|
5
5
|
// surface (every registered adapter's curated rows + the no-auth sentinel) and
|
|
6
|
-
// wires streamSimple to the real ACP backend (lib/acp/backend.ts — a
|
|
7
|
-
//
|
|
8
|
-
// on which adapter the model id routes to). It does NOT build a
|
|
6
|
+
// wires streamSimple to the real ACP backend (lib/acp/backend.ts — a Claude
|
|
7
|
+
// ACP child whose lifetime follows pi's lifecycle). It does NOT build a
|
|
9
8
|
// socket/peers/citizen protocol or touch the v2 core — socket-citizenship is
|
|
10
9
|
// supplied by the host `--entwurf-control` pi session (AGENTS §ACP Plugin
|
|
11
10
|
// Boundary).
|
|
@@ -13,8 +12,8 @@
|
|
|
13
12
|
// The model set comes from `allCuratedModels()` — the union across the adapter
|
|
14
13
|
// registry (backend-adapter.ts), NOT a per-backend list spelled here. Adding a
|
|
15
14
|
// backend therefore never edits this file. check-acp-provider-surface pins that
|
|
16
|
-
// the compiled entry
|
|
17
|
-
// ([QK:
|
|
15
|
+
// the compiled entry registers the exact supported Claude-only set
|
|
16
|
+
// ([QK:ACP-PROVIDER-CLAUDE-ONLY]).
|
|
18
17
|
//
|
|
19
18
|
// Fence: this entry rides the emit-capable root tsconfig (it is not in the root
|
|
20
19
|
// `exclude` list); its lib modules are imported with `.js` suffixes (the root
|
|
@@ -22,7 +21,7 @@
|
|
|
22
21
|
// strip-types fence is introduced.
|
|
23
22
|
|
|
24
23
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
|
-
import { streamShellAcp } from "./lib/acp/backend.js";
|
|
24
|
+
import { closeRetainedAcpSession, streamShellAcp } from "./lib/acp/backend.js";
|
|
26
25
|
import { allCuratedModels } from "./lib/acp/backend-adapter.js";
|
|
27
26
|
import { ENTWURF_ACP_NO_AUTH_SENTINEL, PROVIDER_ID } from "./lib/acp/models.js";
|
|
28
27
|
|
|
@@ -65,6 +64,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
65
64
|
streamSimple: streamShellAcp,
|
|
66
65
|
});
|
|
67
66
|
|
|
67
|
+
// #163, GLG 2026-11-14: a successful compaction closes the Claude child of this Pi session at
|
|
68
|
+
// once. The next turn opens a new child from the compacted transcript (backend.ts
|
|
69
|
+
// `closeRetainedAcpSession`). A session that never used an ACP model has nothing to close.
|
|
70
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
71
|
+
closeRetainedAcpSession(ctx.sessionManager.getSessionId());
|
|
72
|
+
});
|
|
73
|
+
|
|
68
74
|
// Mark only AFTER a successful registration. If allCuratedModels() (which
|
|
69
75
|
// fail-fasts on an unowned/duplicate curated id, and whose claude rows carry a
|
|
70
76
|
// fail-loud anchor check) or registerProvider throws, the runtime is not left
|
|
@@ -17,13 +17,12 @@
|
|
|
17
17
|
// smokes both drive ONE adapter — the SDK method-name mapping lives here only,
|
|
18
18
|
// and the backend's orchestration + the gate fakes stay untouched.
|
|
19
19
|
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} from "@agentclientprotocol/sdk";
|
|
20
|
+
import { AGENT_METHODS, CLIENT_METHODS, client, type Stream } from "@agentclientprotocol/sdk";
|
|
21
|
+
// SDK 2.x moved the experimental `Usage` out of the stable root into the `./unstable`
|
|
22
|
+
// entry (`dist/v1/unstable/schema/types.gen.d.ts`, marked UNSTABLE/@experimental). A
|
|
23
|
+
// type-only import: nothing of it reaches the emitted JS, and the wire carries the
|
|
24
|
+
// same `usage` object either way.
|
|
25
|
+
import type { Usage as AcpWireUsage } from "@agentclientprotocol/sdk/unstable";
|
|
27
26
|
import type { AcpTextBlock } from "./context.js";
|
|
28
27
|
|
|
29
28
|
/**
|
|
@@ -27,11 +27,12 @@ import type { AcpConnectionLike } from "./acp-client.js";
|
|
|
27
27
|
import { enrichMcpServersWithEnvelope, type ResolvedAcpConfig } from "./config.js";
|
|
28
28
|
import { loadEngraving } from "./engraving.js";
|
|
29
29
|
import {
|
|
30
|
+
CLAUDE_ACP_EFFORT_VALUES,
|
|
30
31
|
CORTEX_MODEL_PREFIX,
|
|
31
32
|
curatedClaudeModels,
|
|
32
33
|
curatedCortexModels,
|
|
34
|
+
LEGACY_CORTEX_MODEL_IDS,
|
|
33
35
|
SUPPORTED_ANTHROPIC_MODEL_IDS,
|
|
34
|
-
SUPPORTED_CORTEX_MODEL_IDS,
|
|
35
36
|
} from "./models.js";
|
|
36
37
|
import { claudeLaunchEnvDefaults, ensureClaudeConfigOverlay, ensureCortexDualHomeOverlay } from "./overlay.js";
|
|
37
38
|
import { buildClaudeSessionMeta } from "./tool-surface.js";
|
|
@@ -57,7 +58,10 @@ export interface AcpLaunchSpec {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/** A curated provider model row (the shape pi.registerProvider({ models }) takes). */
|
|
60
|
-
|
|
61
|
+
/** `thinkingLevelMap` is optional: Claude rows declare the effort levels its driver can set (#142);
|
|
62
|
+
* cortex rows declare none and keep pi's default thinking handling. */
|
|
63
|
+
type ClaudeModelRow = ReturnType<typeof curatedClaudeModels>[number];
|
|
64
|
+
export type AcpModelRow = Omit<ClaudeModelRow, "thinkingLevelMap"> & Partial<Pick<ClaudeModelRow, "thinkingLevelMap">>;
|
|
61
65
|
|
|
62
66
|
/** routeModel result — the backend-native model id with the curation prefix stripped. */
|
|
63
67
|
export interface AcpRoutedModel {
|
|
@@ -112,6 +116,23 @@ export interface AcpEnforceModelParams {
|
|
|
112
116
|
nativeModelId: string;
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
/** resolveThinking input — pi's requested level and the registered row's level map. `reasoning`
|
|
120
|
+
* is `undefined` both when the pi thinking level is `off` and when none was given: pi-agent-core
|
|
121
|
+
* sends `thinkingLevel === "off" ? undefined : level`, so the provider cannot tell them apart. */
|
|
122
|
+
export interface AcpResolveThinkingParams {
|
|
123
|
+
modelId: string;
|
|
124
|
+
reasoning: string | undefined;
|
|
125
|
+
thinkingLevelMap: Readonly<Record<string, string | null | undefined>> | undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** enforceThinking input — the live connection + acp session + the resolved backend value. */
|
|
129
|
+
export interface AcpEnforceThinkingParams {
|
|
130
|
+
connection: AcpConnectionLike;
|
|
131
|
+
acpSessionId: string;
|
|
132
|
+
modelId: string;
|
|
133
|
+
value: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
115
136
|
/** ensureOverlay result — env overrides backend.ts merges into the spawn env. */
|
|
116
137
|
export interface AcpOverlayResult {
|
|
117
138
|
envOverrides: Record<string, string>;
|
|
@@ -211,6 +232,15 @@ export interface AcpBackendAdapter {
|
|
|
211
232
|
* launch-pinned backend: no-op here). backend.ts wraps the call in withTimeout. */
|
|
212
233
|
enforceModel(params: AcpEnforceModelParams): Promise<void>;
|
|
213
234
|
|
|
235
|
+
/** OPTIONAL capability (#142): carry pi's thinking level to the backend. Both or neither.
|
|
236
|
+
* `resolveThinking` is PURE — it maps the requested level to the backend value through the
|
|
237
|
+
* registered row's `thinkingLevelMap` and throws a named refusal when there is none, before
|
|
238
|
+
* any process or session is touched. `enforceThinking` sets that value on the live session
|
|
239
|
+
* and refuses unless the answer reads it back. ABSENT (cortex) keeps the old behaviour: the
|
|
240
|
+
* level is not carried, and nothing claims it is. */
|
|
241
|
+
resolveThinking?(params: AcpResolveThinkingParams): string;
|
|
242
|
+
enforceThinking?(params: AcpEnforceThinkingParams): Promise<void>;
|
|
243
|
+
|
|
214
244
|
/** Backend-specific fields folded into bridgeConfigSignature (reuse invalidation):
|
|
215
245
|
* connection/profile/env-derived STABLE ids only — never raw env values / secrets.
|
|
216
246
|
* Reads ONLY this backend's opaque `adapterSettings` (NOT the whole config), so the
|
|
@@ -345,7 +375,7 @@ export const CLAUDE_FOREGROUND_ONLY_ENV = { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS
|
|
|
345
375
|
* The set-model answer is read back before the prompt (#128). claude-agent-acp resolves the
|
|
346
376
|
* requested id onto one of the rows it offers and answers with that row's `value` as the model
|
|
347
377
|
* option's `currentValue` (0.85.1 `dist/acp-agent.js:5459-5499`). `[측정 2026-10-12]` 0.85.1, no
|
|
348
|
-
* prompt: `claude-fable-5-1` answers its own row, while `claude-sonnet-5-5` and `claude-opus-5-5`
|
|
378
|
+
* prompt: `claude-fable-5-1` answers its own row (and, at 0.106.0 #142, so does `claude-fable-5-2`), while `claude-sonnet-5-5` and `claude-opus-5-5`
|
|
349
379
|
* answer the alias rows `sonnet` and `opus`, named `Sonnet 5.5` and `Opus 5.5`. The id an alias row
|
|
350
380
|
* resolves to (`ModelInfo.resolvedModel`) is not on the ACP wire, so an alias answer is accepted
|
|
351
381
|
* only as the exact measured row, value AND name, declared here for the requested id. No other
|
|
@@ -385,6 +415,61 @@ export function confirmClaudeSetModelAnswer(response: unknown, nativeModelId: st
|
|
|
385
415
|
);
|
|
386
416
|
}
|
|
387
417
|
|
|
418
|
+
const CLAUDE_EFFORT_VALUE_SET: ReadonlySet<string> = new Set(CLAUDE_ACP_EFFORT_VALUES);
|
|
419
|
+
|
|
420
|
+
/** The named refusal for a thinking level ACP Claude cannot carry (#142). The text is fixed and
|
|
421
|
+
* must read as NON-transient to pi's retry and overflow classifiers: replaying the turn cannot
|
|
422
|
+
* change the level that was asked for. */
|
|
423
|
+
export const ACP_THINKING_LEVEL_UNSUPPORTED = "acp-thinking-level-unsupported";
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Map pi's requested level to the claude-agent-acp effort value, or refuse by name (#142).
|
|
427
|
+
* The value is the registered row's `thinkingLevelMap` entry for that level — the documented pi
|
|
428
|
+
* meaning of the map — and must be one of the measured effort values. `undefined` (pi off, or no
|
|
429
|
+
* level) is refused rather than left at the vendor `default`: that would let the turn run at a
|
|
430
|
+
* depth nobody chose while the operator believes a level was applied.
|
|
431
|
+
*/
|
|
432
|
+
export function resolveClaudeEffort(
|
|
433
|
+
modelId: string,
|
|
434
|
+
reasoning: string | undefined,
|
|
435
|
+
thinkingLevelMap: Readonly<Record<string, string | null | undefined>> | undefined,
|
|
436
|
+
): string {
|
|
437
|
+
const value = reasoning === undefined ? undefined : thinkingLevelMap?.[reasoning];
|
|
438
|
+
if (typeof value === "string" && CLAUDE_EFFORT_VALUE_SET.has(value)) return value;
|
|
439
|
+
const asked =
|
|
440
|
+
reasoning === undefined
|
|
441
|
+
? "no thinking level (pi sends none for `off`, and none when no level is set; the two are indistinguishable here)"
|
|
442
|
+
: `thinking level ${JSON.stringify(reasoning)}`;
|
|
443
|
+
throw new Error(
|
|
444
|
+
`entwurf: ${ACP_THINKING_LEVEL_UNSUPPORTED} — ${modelId} over Claude ACP carries effort ` +
|
|
445
|
+
`${CLAUDE_ACP_EFFORT_VALUES.join("/")} only, and was asked for ${asked}. Choose one of those levels; ` +
|
|
446
|
+
`the turn was not sent, and repeating it at the same level cannot change that.`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function claudeEffortRefusal(modelId: string, value: string, what: string, cause?: unknown): Error {
|
|
451
|
+
const error = new Error(
|
|
452
|
+
`entwurf: claude-agent-acp ${what} for effort ${JSON.stringify(value)} on ${modelId}. Refusing before the prompt ` +
|
|
453
|
+
`so the turn cannot run at an effort nobody chose; repeating the turn cannot change the driver's answer.`,
|
|
454
|
+
);
|
|
455
|
+
if (cause !== undefined) (error as Error & { cause?: unknown }).cause = cause;
|
|
456
|
+
return error;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Refuse unless the set-effort answer reads back the requested value. */
|
|
460
|
+
export function confirmClaudeEffortAnswer(response: unknown, modelId: string, value: string): void {
|
|
461
|
+
const configOptions = (response as { configOptions?: unknown } | null | undefined)?.configOptions;
|
|
462
|
+
const effort = Array.isArray(configOptions)
|
|
463
|
+
? (configOptions as Array<Record<string, unknown> | null>).find((option) => option?.id === "effort")
|
|
464
|
+
: undefined;
|
|
465
|
+
if (effort?.currentValue === value) return;
|
|
466
|
+
throw claudeEffortRefusal(
|
|
467
|
+
modelId,
|
|
468
|
+
value,
|
|
469
|
+
effort == null ? "answered with no effort option" : `answered effort ${jsonText(effort.currentValue)}`,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
388
473
|
export const claudeAdapter: AcpBackendAdapter = {
|
|
389
474
|
backend: "claude",
|
|
390
475
|
|
|
@@ -456,6 +541,24 @@ export const claudeAdapter: AcpBackendAdapter = {
|
|
|
456
541
|
confirmClaudeSetModelAnswer(answer, nativeModelId);
|
|
457
542
|
},
|
|
458
543
|
|
|
544
|
+
resolveThinking({ modelId, reasoning, thinkingLevelMap }) {
|
|
545
|
+
return resolveClaudeEffort(modelId, reasoning, thinkingLevelMap);
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
async enforceThinking({ connection, acpSessionId, modelId, value }) {
|
|
549
|
+
const setConfig = connection.setSessionConfigOption;
|
|
550
|
+
if (typeof setConfig !== "function") {
|
|
551
|
+
throw new Error(`setSessionConfigOption unsupported — cannot set effort for ${modelId}`);
|
|
552
|
+
}
|
|
553
|
+
let answer: unknown;
|
|
554
|
+
try {
|
|
555
|
+
answer = await setConfig.call(connection, { sessionId: acpSessionId, configId: "effort", value });
|
|
556
|
+
} catch (err) {
|
|
557
|
+
throw claudeEffortRefusal(modelId, value, "refused the set", err);
|
|
558
|
+
}
|
|
559
|
+
confirmClaudeEffortAnswer(answer, modelId, value);
|
|
560
|
+
},
|
|
561
|
+
|
|
459
562
|
configSignatureFields(_adapterSettings) {
|
|
460
563
|
// Claude folds no extra backend-specific fields beyond backend + nativeModelId
|
|
461
564
|
// (which backend.ts adds). A future backend reads its own stable id off
|
|
@@ -480,17 +583,11 @@ export const claudeAdapter: AcpBackendAdapter = {
|
|
|
480
583
|
// Registry — modelId → adapter
|
|
481
584
|
// ---------------------------------------------------------------------------
|
|
482
585
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
* adapters claim one id. */
|
|
487
|
-
// ---------------------------------------------------------------------------
|
|
488
|
-
// cortex adapter — Snowflake Cortex Code, the first non-claude backend on the
|
|
489
|
-
// rail (docs/acp-backend-rail.md, “Shipped adapters”). It adds ZERO to the common layer:
|
|
490
|
-
// everything cortex-specific lives here + models.ts + overlay.ts + the gates.
|
|
491
|
-
// ---------------------------------------------------------------------------
|
|
586
|
+
// The former Cortex implementation is retained privately for historical source
|
|
587
|
+
// continuity only. The exported adapter below denies every side-effecting entry,
|
|
588
|
+
// and the production registry does not contain this implementation.
|
|
492
589
|
|
|
493
|
-
const SUPPORTED_CORTEX_IDS: ReadonlySet<string> = new Set(
|
|
590
|
+
const SUPPORTED_CORTEX_IDS: ReadonlySet<string> = new Set(LEGACY_CORTEX_MODEL_IDS);
|
|
494
591
|
|
|
495
592
|
/** Cortex's OWN settings: a Snowflake connection name, or null. Opaque to
|
|
496
593
|
* config.ts / backend.ts — only cortexAdapter reads it (casting back). */
|
|
@@ -503,7 +600,7 @@ export interface CortexAdapterSettings {
|
|
|
503
600
|
* ENTWURF_ACP_* convention (PR #40's legacy PI_SHELL_ACP* cortex-connection var renamed). */
|
|
504
601
|
export const CORTEX_CONNECTION_ENV = "ENTWURF_ACP_CORTEX_CONNECTION";
|
|
505
602
|
|
|
506
|
-
|
|
603
|
+
const retiredCortexAdapter: AcpBackendAdapter = {
|
|
507
604
|
backend: "cortex",
|
|
508
605
|
|
|
509
606
|
// Cortex owns the reserved `cortex-` prefix (rail “Adapter contract”). routeModel strips it to the
|
|
@@ -649,7 +746,31 @@ export const cortexAdapter: AcpBackendAdapter = {
|
|
|
649
746
|
// errorMessage. Borrowing claude's `incomplete_tool_call` reader would be a guess.
|
|
650
747
|
};
|
|
651
748
|
|
|
652
|
-
const
|
|
749
|
+
const cortexDisabled = (): never => {
|
|
750
|
+
throw new Error("entwurf: cortex-acp-disabled: Cortex ACP is not supported");
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
/** Retained exported shape refuses the retired adapter before a launch or overlay can be made. */
|
|
754
|
+
export const cortexAdapter: AcpBackendAdapter = {
|
|
755
|
+
...retiredCortexAdapter,
|
|
756
|
+
routeModel: () => undefined,
|
|
757
|
+
curatedModels: () => [],
|
|
758
|
+
resolveAdapterSettings: cortexDisabled,
|
|
759
|
+
resolveLaunch: cortexDisabled,
|
|
760
|
+
launchEnvDefaults: cortexDisabled,
|
|
761
|
+
ensureOverlay: cortexDisabled,
|
|
762
|
+
loadCarrier: cortexDisabled,
|
|
763
|
+
buildSessionMeta: cortexDisabled,
|
|
764
|
+
enforceModel: cortexDisabled,
|
|
765
|
+
configSignatureFields: cortexDisabled,
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter];
|
|
769
|
+
|
|
770
|
+
/** A declared backend is only a guard, never a replacement routing authority. */
|
|
771
|
+
export function assertSupportedDeclaredBackend(backend: string | undefined): void {
|
|
772
|
+
if (backend?.toLowerCase() === "cortex") cortexDisabled();
|
|
773
|
+
}
|
|
653
774
|
|
|
654
775
|
/**
|
|
655
776
|
* Resolve the backend adapter that owns `modelId`.
|
|
@@ -662,6 +783,9 @@ const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter, cortexAdapter];
|
|
|
662
783
|
* so callers get `{ adapter, nativeModelId }` and never re-parse the id string.
|
|
663
784
|
*/
|
|
664
785
|
export function resolveAcpBackendAdapter(modelId: string): { adapter: AcpBackendAdapter; nativeModelId: string } {
|
|
786
|
+
if (modelId.toLowerCase().startsWith(CORTEX_MODEL_PREFIX)) {
|
|
787
|
+
throw new Error(`entwurf: cortex-acp-disabled: ${JSON.stringify(modelId)} is not supported`);
|
|
788
|
+
}
|
|
665
789
|
const matches: Array<{ adapter: AcpBackendAdapter; nativeModelId: string }> = [];
|
|
666
790
|
for (const adapter of ADAPTERS) {
|
|
667
791
|
const routed = adapter.routeModel(modelId);
|