@junghanacs/entwurf 0.51.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 +191 -2
- package/CONTRIBUTING.md +4 -4
- package/DELIVERY.md +2 -2
- package/ENTWURF-CLI.json +1 -1
- package/ENTWURF.json +10 -4
- package/README.md +11 -8
- package/VERIFY.md +333 -143
- package/demo/README.md +1 -1
- 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 +75 -80
- package/mcp/entwurf-bridge/dist/ENTWURF.json +10 -4
- 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/entwurf-control-rpc.js +4 -1
- 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/meta-mailbox-body.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -1
- package/package.json +14 -14
- package/pi/pi-durable/carrier/harness-setup.js +1 -1
- package/pi/pi-durable/carrier/subagent.js +1 -1
- package/pi/pi-durable/overlay/upstream-pin.json +15 -15
- 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/entwurf-control-rpc.ts +5 -1
- package/pi-extensions/lib/fresh-call-composition.ts +37 -3
- package/pi-extensions/lib/herdr-fresh-call.ts +3 -1
- package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
- package/pi-extensions/lib/mux-fresh-call.ts +3 -1
- package/pi-extensions/meta-bridge-pi-durable.ts +1 -1
- package/run.sh +62 -120
- 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-entwurf-v2-surface.ts +7 -21
- package/scripts/check-fresh-call-dispatch.ts +9 -3
- package/scripts/check-gate-qualification.ts +6 -6
- package/scripts/check-herdr-fresh-call.ts +1 -0
- package/scripts/check-install-surface.ts +6 -0
- package/scripts/check-pi-durable-send.ts +4 -1
- package/scripts/check-probe-ordering.ts +34 -24
- package/scripts/check-release-gate-outcomes.ts +9 -23
- package/scripts/lib/codex-fresh-source-receipts.ts +12 -6
- 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/agent-guidance.json +2 -2
- package/scripts/mutants/codex-native.json +2 -2
- package/scripts/mutants/control-send-receipt.json +47 -7
- package/scripts/mutants/mux-fresh-call.json +12 -1
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/pi-floor.json +8 -10
- package/scripts/mutants/release-gate.json +3 -3
- package/scripts/mutants/v2-surface.json +4 -6
- 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-acp-v2-send-live.ts +1 -1
- package/scripts/smoke-herdr-raw-install-live.sh +4 -4
- package/scripts/check-acp-cortex.ts +0 -710
|
@@ -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);
|