@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
|
@@ -16,15 +16,10 @@
|
|
|
16
16
|
//
|
|
17
17
|
// The COMMON keys parsed here are Claude-SHAPED by origin (tools / permissionAllow /
|
|
18
18
|
// disallowedTools / settingSources / skillPlugins are claude-agent-acp's surface).
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// the backend-invariant exclude-tools preflight and folds every one of them into
|
|
24
|
-
// `bridgeConfigSignature` (so changing one still invalidates a reused cortex
|
|
25
|
-
// session). Backend-OWNED settings never land here: they ride the opaque
|
|
26
|
-
// `adapterSettings` seam (rail “Adapter contract”), which is what keeps a backend-named key like
|
|
27
|
-
// `cortexConnection` off this common shape.
|
|
19
|
+
// The common path reads `tools` for the backend-invariant exclude-tools preflight
|
|
20
|
+
// and folds these keys into `bridgeConfigSignature`. Backend-owned settings ride
|
|
21
|
+
// the opaque `adapterSettings` seam. A dormant `cortexConnection` does not select
|
|
22
|
+
// or enable Cortex ACP, which is disabled.
|
|
28
23
|
//
|
|
29
24
|
// PURITY / SIGNATURE contract (NEXT oracle C / 핀1 / GPT `…2f9325` boost):
|
|
30
25
|
// - `normalizeMcpServers` is pure: a SORTED, validated server list + a sha256
|
|
@@ -57,6 +57,17 @@ function textFromAssistantContent(content) {
|
|
|
57
57
|
.map((c) => c.text)
|
|
58
58
|
.join(""));
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The harness fact pi already holds about an assistant turn that was cut
|
|
62
|
+
* (`stopReason: "aborted"` — an operator abort, or a vendor `cancelled` mapped to it; the line
|
|
63
|
+
* names no cause), stated so a NEW session's transcript does not read a
|
|
64
|
+
* cancelled turn as a clean refusal (#142: a fresh session concluded a command had
|
|
65
|
+
* not run when its first effect was on disk). It adds no claim about what did or
|
|
66
|
+
* did not happen — only that the turn was cut and that a cut is not a rollback.
|
|
67
|
+
* Only the full transcript carries it: a reused session already holds its own
|
|
68
|
+
* record of the interruption and receives just the latest user delta.
|
|
69
|
+
*/
|
|
70
|
+
export const ABORTED_TURN_STATUS = "[turn status: aborted — actions this turn had started may already have taken effect; cancellation is not rollback]";
|
|
60
71
|
/** Render one pi message as a transcript line, or undefined to skip it. */
|
|
61
72
|
function renderMessage(message) {
|
|
62
73
|
switch (message.role) {
|
|
@@ -66,6 +77,10 @@ function renderMessage(message) {
|
|
|
66
77
|
}
|
|
67
78
|
case "assistant": {
|
|
68
79
|
const text = textFromAssistantContent(message.content).trim();
|
|
80
|
+
if (message.stopReason === "aborted") {
|
|
81
|
+
// Kept even when the cut turn left no text: the cut itself is the fact.
|
|
82
|
+
return text ? `Assistant: ${text}\n${ABORTED_TURN_STATUS}` : `Assistant: ${ABORTED_TURN_STATUS}`;
|
|
83
|
+
}
|
|
69
84
|
return text ? `Assistant: ${text}` : undefined;
|
|
70
85
|
}
|
|
71
86
|
case "toolResult": {
|
|
@@ -12,12 +12,9 @@
|
|
|
12
12
|
// would need a `.ts` specifier the root config cannot allow.
|
|
13
13
|
//
|
|
14
14
|
// WHY THE LAUNCH SEAMS ASK AT ALL. Pi's `--model` resolver falls back to a
|
|
15
|
-
// case-insensitive substring match among
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// silently open on the Snowflake Cortex rail instead of the Claude subscription.
|
|
19
|
-
// An exact match against this list is the only admission; nothing is aliased,
|
|
20
|
-
// substituted or rewritten.
|
|
15
|
+
// case-insensitive substring match among provider rows. The old multi-backend
|
|
16
|
+
// surface could silently match a retired Claude id to a Cortex row. Cortex is
|
|
17
|
+
// now disabled; exact membership remains the admission, with no substitution.
|
|
21
18
|
/** The single ACP provider id under which every curated row is registered. */
|
|
22
19
|
export const PROVIDER_ID = "entwurf";
|
|
23
20
|
// The curated Claude ids. Adding one here is a commitment to verify it across
|
|
@@ -26,20 +23,21 @@ export const SUPPORTED_ANTHROPIC_MODEL_IDS = /** @type {const} */ ([
|
|
|
26
23
|
"claude-sonnet-5-5",
|
|
27
24
|
"claude-opus-5-5",
|
|
28
25
|
"claude-fable-5-1",
|
|
26
|
+
"claude-fable-5-2",
|
|
29
27
|
]);
|
|
30
28
|
/** The anchor model whose absence is a hard registry regression, not a soft skip. */
|
|
31
29
|
export const CURATED_ANCHOR_MODEL_ID = "claude-opus-5-5";
|
|
32
|
-
//
|
|
33
|
-
//
|
|
30
|
+
// These former IDs are retained only as historical refusal inputs. The prefix
|
|
31
|
+
// is reserved so a disabled Cortex request never falls through to Claude.
|
|
34
32
|
export const CORTEX_MODEL_PREFIX = "cortex-";
|
|
35
|
-
export const
|
|
33
|
+
export const LEGACY_CORTEX_MODEL_IDS = /** @type {const} */ ([
|
|
36
34
|
"cortex-auto",
|
|
37
35
|
"cortex-claude-opus-5",
|
|
38
36
|
"cortex-claude-sonnet-5",
|
|
39
37
|
"cortex-openai-gpt-5.4",
|
|
40
38
|
]);
|
|
41
39
|
/** @type {ReadonlySet<string>} */
|
|
42
|
-
const CURATED_ACP_MODEL_IDS = new Set(
|
|
40
|
+
const CURATED_ACP_MODEL_IDS = new Set(SUPPORTED_ANTHROPIC_MODEL_IDS);
|
|
43
41
|
/**
|
|
44
42
|
* Exact membership only — no trim, no case folding, no prefix.
|
|
45
43
|
* @param {string} model
|
|
@@ -51,8 +49,7 @@ export function isCuratedAcpModelId(model) {
|
|
|
51
49
|
/** The named refusal both launch seams answer with. */
|
|
52
50
|
export const ACP_MODEL_NOT_CURATED = /** @type {const} */ ("acp-model-not-curated");
|
|
53
51
|
export const ACP_MODEL_NOT_CURATED_HINT = `an ${PROVIDER_ID} model must be one exact curated ACP id (${[...CURATED_ACP_MODEL_IDS].join(", ")}); ` +
|
|
54
|
-
"Pi
|
|
55
|
-
"so nothing was opened, and nothing is aliased or rewritten";
|
|
52
|
+
"Cortex ACP is disabled; Pi may otherwise pick another row by substring, so nothing was opened and nothing is aliased or rewritten";
|
|
56
53
|
/** Carries `reason` so callers match a FIELD, never the wording. */
|
|
57
54
|
export class AcpModelNotCuratedError extends Error {
|
|
58
55
|
/** @param {string} model */
|
|
@@ -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) {
|
|
@@ -29,7 +29,10 @@ import { win32Native } from "./win32-native.js";
|
|
|
29
29
|
* renders nothing (etiquette marker, not transport contract).
|
|
30
30
|
*/
|
|
31
31
|
export function formatSenderInfoBlock(sender, wantsReply = false) {
|
|
32
|
-
|
|
32
|
+
const replyGuidance = sender.replyable === true
|
|
33
|
+
? "\n\n[entwurf: When a reply is requested, send the result via entwurf_v2 to the requested recipient (default: sender_info.sessionId); a local final is not delivery. Honor routing and no-send instructions; an absent or false wants_reply does not cancel a request. A replyable address alone needs no acknowledgment.]"
|
|
34
|
+
: "";
|
|
35
|
+
return `${replyGuidance}\n\n<sender_info>${JSON.stringify({
|
|
33
36
|
sessionId: sender.sessionId,
|
|
34
37
|
agentId: sender.agentId,
|
|
35
38
|
cwd: sender.cwd,
|
|
@@ -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",
|
|
@@ -64,7 +64,7 @@ export function formatMetaMailboxBody(sender, message, wantsReply) {
|
|
|
64
64
|
// SENDING host, so it is neither abbreviated against this host's HOME nor shown as a local path.
|
|
65
65
|
const remote = !replyable ? sender.remote : undefined;
|
|
66
66
|
const sessionLine = replyable
|
|
67
|
-
? `${sender.sessionId} (${kind}replyable — reply
|
|
67
|
+
? `${sender.sessionId} (${kind}replyable — when a reply is requested, send the result via entwurf_v2 (intent=fire-and-forget) to the requested recipient, defaulting to this sessionId; a local final is not delivery. Honor routing and no-send instructions; an absent or false wants_reply does not cancel a request. A replyable address alone needs no acknowledgment)`
|
|
68
68
|
: remote
|
|
69
69
|
? `${sender.sessionId} (remote via ${remote.via}; ${remote.replyRoute === "declared" ? "reply route declared, not probed" : "no reply route declared"})`
|
|
70
70
|
: isMeta
|
|
@@ -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,19 +102,19 @@
|
|
|
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
|
-
"@earendil-works/chord": "1.5.
|
|
109
|
-
"@earendil-works/pi-agent-core": "1.5.
|
|
110
|
-
"@earendil-works/pi-ai": "1.5.
|
|
111
|
-
"@earendil-works/pi-codemode": "1.5.
|
|
112
|
-
"@earendil-works/pi-coding-agent": "1.5.
|
|
113
|
-
"@earendil-works/pi-durable": "1.5.
|
|
114
|
-
"@earendil-works/pi-mcp": "1.5.
|
|
115
|
-
"@earendil-works/pi-schema": "1.5.
|
|
116
|
-
"@earendil-works/pi-telemetry": "1.5.
|
|
117
|
-
"@earendil-works/pi-tui": "1.5.
|
|
108
|
+
"@earendil-works/chord": "1.5.2",
|
|
109
|
+
"@earendil-works/pi-agent-core": "1.5.2",
|
|
110
|
+
"@earendil-works/pi-ai": "1.5.2",
|
|
111
|
+
"@earendil-works/pi-codemode": "1.5.2",
|
|
112
|
+
"@earendil-works/pi-coding-agent": "1.5.2",
|
|
113
|
+
"@earendil-works/pi-durable": "1.5.2",
|
|
114
|
+
"@earendil-works/pi-mcp": "1.5.2",
|
|
115
|
+
"@earendil-works/pi-schema": "1.5.2",
|
|
116
|
+
"@earendil-works/pi-telemetry": "1.5.2",
|
|
117
|
+
"@earendil-works/pi-tui": "1.5.2",
|
|
118
118
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
119
119
|
"proper-lockfile": "4.1.2",
|
|
120
120
|
"smol-toml": "^1.8.0",
|
|
@@ -82,8 +82,8 @@
|
|
|
82
82
|
{
|
|
83
83
|
"name": "harness-setup.js",
|
|
84
84
|
"source": "pi/pi-durable/src/experimental/durable/harness-setup.ts",
|
|
85
|
-
"blob": "
|
|
86
|
-
"sha256": "
|
|
85
|
+
"blob": "5a799bfbfbbedbec229761b26f639fbe2702b623",
|
|
86
|
+
"sha256": "70b50044ab14c1293a2ace53b375dc48ad940ea9eeacc131d498ccb1ce040617"
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
"name": "prompt.js",
|
|
@@ -100,8 +100,8 @@
|
|
|
100
100
|
{
|
|
101
101
|
"name": "subagent.js",
|
|
102
102
|
"source": "pi/pi-durable/src/experimental/durable/subagent.ts",
|
|
103
|
-
"blob": "
|
|
104
|
-
"sha256": "
|
|
103
|
+
"blob": "8c18e24a030f0b701167f15a33579195303f5ac5",
|
|
104
|
+
"sha256": "208618441e8e81813de2f44fa9ad5d5bdcdbb78a754679bb7a93d82480e7bece"
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
107
|
"name": "session-name.js",
|
|
@@ -138,18 +138,18 @@
|
|
|
138
138
|
]
|
|
139
139
|
},
|
|
140
140
|
"sdk": {
|
|
141
|
-
"comment": "The published library set the carrier runs against, derived from the 1.5.
|
|
141
|
+
"comment": "The published library set the carrier runs against, derived from the 1.5.2 declarations. members: the @earendil-works packages the carrier imports and every @earendil-works package those declare (1.5.0 adds pi-schema, declared by chord, pi-agent-core, pi-ai and pi-durable). direct: a third-party package a carrier file imports itself (sessions.ts: proper-lockfile, at the version pi-coding-agent 1.5.2 pins). Each is an exact production dependency in package.json. Since 1.3.0 the members' own edges are exact too (upstream #10865), and the installed physical edges are still what must equal these versions.",
|
|
142
142
|
"members": {
|
|
143
|
-
"@earendil-works/chord": "1.5.
|
|
144
|
-
"@earendil-works/pi-agent-core": "1.5.
|
|
145
|
-
"@earendil-works/pi-ai": "1.5.
|
|
146
|
-
"@earendil-works/pi-codemode": "1.5.
|
|
147
|
-
"@earendil-works/pi-coding-agent": "1.5.
|
|
148
|
-
"@earendil-works/pi-durable": "1.5.
|
|
149
|
-
"@earendil-works/pi-mcp": "1.5.
|
|
150
|
-
"@earendil-works/pi-schema": "1.5.
|
|
151
|
-
"@earendil-works/pi-telemetry": "1.5.
|
|
152
|
-
"@earendil-works/pi-tui": "1.5.
|
|
143
|
+
"@earendil-works/chord": "1.5.2",
|
|
144
|
+
"@earendil-works/pi-agent-core": "1.5.2",
|
|
145
|
+
"@earendil-works/pi-ai": "1.5.2",
|
|
146
|
+
"@earendil-works/pi-codemode": "1.5.2",
|
|
147
|
+
"@earendil-works/pi-coding-agent": "1.5.2",
|
|
148
|
+
"@earendil-works/pi-durable": "1.5.2",
|
|
149
|
+
"@earendil-works/pi-mcp": "1.5.2",
|
|
150
|
+
"@earendil-works/pi-schema": "1.5.2",
|
|
151
|
+
"@earendil-works/pi-telemetry": "1.5.2",
|
|
152
|
+
"@earendil-works/pi-tui": "1.5.2"
|
|
153
153
|
},
|
|
154
154
|
"direct": {
|
|
155
155
|
"proper-lockfile": "4.1.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
|
/**
|