@junghanacs/entwurf 0.12.0
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 +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- package/scripts/tsconfig.json +34 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// ACP client adapter — the ONE place that touches the @agentclientprotocol/sdk
|
|
2
|
+
// 0.29 fluent surface (`client({ name }).connect(stream)`).
|
|
3
|
+
//
|
|
4
|
+
// The wire SDK deprecated the `new ClientSideConnection(toClient, stream)`
|
|
5
|
+
// constructor in favour of the fluent `client()` builder. The two have
|
|
6
|
+
// different shapes: the deprecated class implemented `Agent` directly (so
|
|
7
|
+
// `.initialize()`/`.newSession()`/`.prompt()` were methods on the returned
|
|
8
|
+
// object), while `client(...).connect(stream)` returns a persistent
|
|
9
|
+
// `ClientConnection` whose `.agent` is a `ClientContext` driven by
|
|
10
|
+
// `request(<method>, params)`.
|
|
11
|
+
//
|
|
12
|
+
// `connect()` (NOT `connectWith()`) is the right primitive here: the backend
|
|
13
|
+
// retains the connection on a BridgeSession and reuses it across turns
|
|
14
|
+
// (backend.ts), so the op-scoped close semantics of `connectWith` do not fit.
|
|
15
|
+
//
|
|
16
|
+
// This module owns the `AcpConnectionLike` seam so the backend and the live
|
|
17
|
+
// smokes both drive ONE adapter — the SDK method-name mapping lives here only,
|
|
18
|
+
// and the backend's orchestration + the gate fakes stay untouched.
|
|
19
|
+
|
|
20
|
+
import { AGENT_METHODS, CLIENT_METHODS, client, type Stream } from "@agentclientprotocol/sdk";
|
|
21
|
+
import type { AcpTextBlock } from "./context.js";
|
|
22
|
+
|
|
23
|
+
/** The subset of the ACP agent connection the backend drives (real or fake). */
|
|
24
|
+
export interface AcpConnectionLike {
|
|
25
|
+
initialize(params: unknown): Promise<unknown>;
|
|
26
|
+
newSession(params: unknown): Promise<{ sessionId?: string }>;
|
|
27
|
+
prompt(params: { sessionId: string; prompt: AcpTextBlock[] }): Promise<{ stopReason?: string }>;
|
|
28
|
+
setSessionConfigOption?(params: unknown): Promise<unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Closes the underlying SDK connection before child process teardown. With
|
|
31
|
+
* the fluent SDK connection this is load-bearing: otherwise a successful
|
|
32
|
+
* live turn can print PASS but keep Node's event loop alive.
|
|
33
|
+
*
|
|
34
|
+
* Implementations MUST be best-effort (never throw): callers invoke it on
|
|
35
|
+
* success, error, and reuse-error teardown paths, so a close failure must not
|
|
36
|
+
* mask the turn's real outcome nor skip the child teardown that follows it.
|
|
37
|
+
*/
|
|
38
|
+
close?(error?: unknown): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The ACP client-side callbacks. They delegate to the session's mutable handler. */
|
|
42
|
+
export interface AcpClientHandlers {
|
|
43
|
+
sessionUpdate(notification: { update?: Record<string, unknown>; sessionId?: string }): Promise<void>;
|
|
44
|
+
requestPermission(request: { options?: Array<{ optionId: string; kind?: string }> }): Promise<{
|
|
45
|
+
outcome: { outcome: "selected"; optionId: string } | { outcome: "cancelled" };
|
|
46
|
+
}>;
|
|
47
|
+
readTextFile(request: { path: string }): Promise<{ content: string }>;
|
|
48
|
+
// `void` (not `never`): the backend's handler denies by throwing, but the
|
|
49
|
+
// shared smokes legitimately return a write response ({}); both satisfy the
|
|
50
|
+
// SDK's `WriteTextFileResponse | void` handler contract.
|
|
51
|
+
writeTextFile(request: unknown): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Production factory — wrap the SDK 0.29 fluent `client()` into the
|
|
56
|
+
* `AcpConnectionLike` seam the backend (and the live smokes) drive.
|
|
57
|
+
*
|
|
58
|
+
* Client-side handlers register by ACP method name; agent-side calls go through
|
|
59
|
+
* the persistent connection's `ClientContext` (`conn.agent`). Both the params a
|
|
60
|
+
* handler receives (`ctx.params`) and the throw-to-JSON-RPC-error behaviour
|
|
61
|
+
* match the deprecated `ClientSideConnection`, so this is behaviour-preserving.
|
|
62
|
+
*/
|
|
63
|
+
export function connectAcpClient(stream: Stream, handlers: AcpClientHandlers): AcpConnectionLike {
|
|
64
|
+
const conn = client({ name: "entwurf" })
|
|
65
|
+
.onNotification(CLIENT_METHODS.session_update, (ctx) => handlers.sessionUpdate(ctx.params as never))
|
|
66
|
+
.onRequest(CLIENT_METHODS.session_request_permission, (ctx) => handlers.requestPermission(ctx.params as never))
|
|
67
|
+
.onRequest(CLIENT_METHODS.fs_read_text_file, (ctx) => handlers.readTextFile(ctx.params as never))
|
|
68
|
+
.onRequest(CLIENT_METHODS.fs_write_text_file, (ctx) => handlers.writeTextFile(ctx.params as never))
|
|
69
|
+
.connect(stream);
|
|
70
|
+
|
|
71
|
+
const agent = conn.agent;
|
|
72
|
+
return {
|
|
73
|
+
initialize: (params) => agent.request(AGENT_METHODS.initialize, params as never),
|
|
74
|
+
newSession: (params) =>
|
|
75
|
+
agent.request(AGENT_METHODS.session_new, params as never) as Promise<{ sessionId?: string }>,
|
|
76
|
+
prompt: (params) =>
|
|
77
|
+
agent.request(AGENT_METHODS.session_prompt, params as never) as Promise<{ stopReason?: string }>,
|
|
78
|
+
setSessionConfigOption: (params) => agent.request(AGENT_METHODS.session_set_config_option, params as never),
|
|
79
|
+
close: (error) => {
|
|
80
|
+
// Best-effort by contract (see AcpConnectionLike.close): a teardown-path
|
|
81
|
+
// close that threw would mask the turn's real error and skip the child
|
|
82
|
+
// teardown that runs after it.
|
|
83
|
+
try {
|
|
84
|
+
conn.close(error);
|
|
85
|
+
} catch {
|
|
86
|
+
// connection already closed / SDK teardown race — nothing to recover.
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// ACP plugin — first-user-message context augment + entwurf de-dup (S2d-1c).
|
|
2
|
+
//
|
|
3
|
+
// The rich context the ACP-side agent needs — bridge identity, the
|
|
4
|
+
// task-execution stance, pi's base intro + tool surface, the operator's
|
|
5
|
+
// `~/AGENTS.md`, the project's `cwd/AGENTS.md`, date + cwd — is delivered as a
|
|
6
|
+
// text block PREPENDED to the first prompt of a `bootstrapPath="new"` session,
|
|
7
|
+
// NOT via `_meta.systemPrompt`.
|
|
8
|
+
//
|
|
9
|
+
// Why first-user-message and not the carrier (NEXT §S2-scout 핀1, oracle A):
|
|
10
|
+
// growing `_meta.systemPrompt` materially past the SDK-default size reclassifies a
|
|
11
|
+
// subscription call as metered → HTTP 400. A long FIRST USER MESSAGE is
|
|
12
|
+
// structurally identical to any other user prompt and does not touch billing
|
|
13
|
+
// classification, so the rich context rides here while engraving.ts keeps the
|
|
14
|
+
// carrier tiny (a small non-empty string whose real job is replacing the preset
|
|
15
|
+
// to strip auto-memory, not carrying context).
|
|
16
|
+
//
|
|
17
|
+
// once-only (NEXT §S2d gate ②, GPT c32a6c8): this augment is prepended to the
|
|
18
|
+
// `new` prompt ONLY. `new` happens exactly once per ACP session lifecycle
|
|
19
|
+
// (subsequent turns are `reuse` delta-only; an incompatible drift opens a
|
|
20
|
+
// genuinely new ACP session that correctly gets the augment again). The augment
|
|
21
|
+
// is prepended at the WIRE level (to the AcpTextBlock[]), never mutated into the
|
|
22
|
+
// pi Context, so `contextMessageSignatures(context)` never sees it — that is what
|
|
23
|
+
// keeps the augment out of the reuse-compat signature with no extra bookkeeping.
|
|
24
|
+
//
|
|
25
|
+
// Entwurf de-dup: when an entwurf-spawned task is the first user message,
|
|
26
|
+
// `enrichTaskWithProjectContext` (entwurf-core.ts) has already prepended a
|
|
27
|
+
// `<project-context path="${cwd}/AGENTS.md">` block carrying the cwd AGENTS.md.
|
|
28
|
+
// Augmenting that prompt with the SAME cwd AGENTS.md section would duplicate it,
|
|
29
|
+
// so we drop only the cwd-AGENTS section and keep the rest of the augment.
|
|
30
|
+
|
|
31
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
32
|
+
import { homedir } from "node:os";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
import { ENTWURF_PROJECT_CONTEXT_OPEN_TAG } from "../../../protocol.js";
|
|
35
|
+
import type { AcpTextBlock } from "./context.js";
|
|
36
|
+
|
|
37
|
+
const MAX_AUGMENT_BYTES = 50 * 1024;
|
|
38
|
+
|
|
39
|
+
export interface PiContextAugmentParams {
|
|
40
|
+
/** Claude-only this cut; kept as a field so the narrative names the backend. */
|
|
41
|
+
backend: string;
|
|
42
|
+
cwd: string;
|
|
43
|
+
mcpServerNames: readonly string[];
|
|
44
|
+
emacsAgentSocket?: string;
|
|
45
|
+
/** Home dir to read `~/AGENTS.md` from. Defaults to os.homedir() (gate seam). */
|
|
46
|
+
homeDir?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the augment text to prepend to the first user message of a NEW ACP
|
|
51
|
+
* session. Day-granularity date so two prompts on the same calendar day produce
|
|
52
|
+
* the same augment. Truncated to 50KB. Pure w.r.t. (cwd, backend, sorted
|
|
53
|
+
* mcpServerNames, AGENTS.md contents, calendar day).
|
|
54
|
+
*/
|
|
55
|
+
export function buildPiContextAugment(params: PiContextAugmentParams): string {
|
|
56
|
+
const names = [...params.mcpServerNames].sort();
|
|
57
|
+
const mcpList = names.length > 0 ? names.join(", ") : "(none registered)";
|
|
58
|
+
|
|
59
|
+
const sections: string[] = [];
|
|
60
|
+
|
|
61
|
+
// Bridge identity + caller-side sibling stance. The closing line is a
|
|
62
|
+
// caller-perspective statement: when THIS session throws an entwurf, the peer
|
|
63
|
+
// on the other end is a sibling, not a worker.
|
|
64
|
+
sections.push(
|
|
65
|
+
[
|
|
66
|
+
"You are operating through entwurf, an ACP bridge between pi (the harness) and the underlying model.",
|
|
67
|
+
`Backend: ${params.backend}.`,
|
|
68
|
+
`Connected MCP servers: ${mcpList}.`,
|
|
69
|
+
"When entwurf is invoked, you do not spawn workers — you summon sibling agents through this bridge.",
|
|
70
|
+
].join("\n"),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Receiver-side task-execution stance. A SEPARATE paragraph so the caller
|
|
74
|
+
// stance above does not bleed into how incoming tasks are read.
|
|
75
|
+
sections.push(
|
|
76
|
+
"A task is a task: execute what you receive, regardless of transport (spawn/resume/send) or source (GLG or sibling). The only thing you do not do on your own is forward your task to another sibling — if you cannot or should not do it, say so to GLG instead.",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
sections.push(
|
|
80
|
+
[
|
|
81
|
+
"You are an expert coding assistant operating inside pi, a coding agent harness.",
|
|
82
|
+
"You help users by reading files, executing commands, editing code, and writing new files.",
|
|
83
|
+
"",
|
|
84
|
+
"Tool surface:",
|
|
85
|
+
"- Treat the actual callable function/tool schema exposed in this session as the source of truth.",
|
|
86
|
+
"- Do not assume a tool exists only because this context or AGENTS.md mentions it.",
|
|
87
|
+
"- Pi-level work generally includes reading files, running shell commands, editing files, and writing files; concrete tool names differ by backend.",
|
|
88
|
+
"- Native pi may expose read/bash/edit/write; Claude ACP may expose Read/Bash/Edit/Write/Skill; Codex ACP may expose exec_command/apply_patch/write_stdin/update_plan.",
|
|
89
|
+
"- MCP/custom tools are usable only when they appear in the actual tool schema for this session.",
|
|
90
|
+
].join("\n"),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (params.emacsAgentSocket) {
|
|
94
|
+
sections.push(
|
|
95
|
+
[
|
|
96
|
+
"Emacs integration:",
|
|
97
|
+
`- Agent Emacs socket: ${params.emacsAgentSocket}`,
|
|
98
|
+
'- When using emacsclient from shell/Bash, prefer `emacsclient -s "$PI_EMACS_AGENT_SOCKET" --eval ...`.',
|
|
99
|
+
"- Do not hardcode `-s server` unless the user explicitly asks for that socket.",
|
|
100
|
+
].join("\n"),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const projectContextParts: string[] = [];
|
|
105
|
+
const homeBase = params.homeDir ?? homedir();
|
|
106
|
+
const homeAgents = join(homeBase, "AGENTS.md");
|
|
107
|
+
const cwdAgents = join(params.cwd, "AGENTS.md");
|
|
108
|
+
|
|
109
|
+
if (existsSync(homeAgents)) {
|
|
110
|
+
const content = readAgents(homeAgents);
|
|
111
|
+
if (content) projectContextParts.push(`## ${homeAgents}\n\n${content}`);
|
|
112
|
+
}
|
|
113
|
+
if (existsSync(cwdAgents) && cwdAgents !== homeAgents) {
|
|
114
|
+
const content = readAgents(cwdAgents);
|
|
115
|
+
if (content) projectContextParts.push(`## ${cwdAgents}\n\n${content}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (projectContextParts.length > 0) {
|
|
119
|
+
sections.push(
|
|
120
|
+
["# Project Context", "", "Project-specific instructions and guidelines:", "", ...projectContextParts].join("\n"),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const currentDate = new Intl.DateTimeFormat("en-CA", {
|
|
125
|
+
timeZone: "Asia/Seoul",
|
|
126
|
+
year: "numeric",
|
|
127
|
+
month: "2-digit",
|
|
128
|
+
day: "2-digit",
|
|
129
|
+
}).format(new Date());
|
|
130
|
+
sections.push([`Current date: ${currentDate}`, `Current working directory: ${params.cwd}`].join("\n"));
|
|
131
|
+
|
|
132
|
+
return truncateAugment(sections.join("\n\n"));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readAgents(filePath: string): string {
|
|
136
|
+
try {
|
|
137
|
+
return readFileSync(filePath, "utf8").trim();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Failed to read AGENTS.md at ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function truncateAugment(text: string): string {
|
|
146
|
+
if (Buffer.byteLength(text, "utf8") <= MAX_AUGMENT_BYTES) return text;
|
|
147
|
+
const marker = `\n\n[entwurf: context augment truncated to ${MAX_AUGMENT_BYTES} bytes; read AGENTS.md files directly if more detail is needed.]`;
|
|
148
|
+
const markerBytes = Buffer.byteLength(marker, "utf8");
|
|
149
|
+
let end = text.length;
|
|
150
|
+
while (end > 0 && Buffer.byteLength(text.slice(0, end), "utf8") + markerBytes > MAX_AUGMENT_BYTES) {
|
|
151
|
+
end = Math.max(0, end - 1024);
|
|
152
|
+
}
|
|
153
|
+
while (end < text.length && Buffer.byteLength(text.slice(0, end + 1), "utf8") + markerBytes <= MAX_AUGMENT_BYTES) {
|
|
154
|
+
end++;
|
|
155
|
+
}
|
|
156
|
+
return `${text.slice(0, end).trimEnd()}${marker}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** trimEnd + a trailing blank line so the augment block reads as its own unit. */
|
|
160
|
+
function ensurePromptSeparator(text: string): string {
|
|
161
|
+
const trimmed = text.trimEnd();
|
|
162
|
+
return trimmed.length > 0 ? `${trimmed}\n\n` : trimmed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The exact opening tag `enrichTaskWithProjectContext` emits for THIS cwd. The
|
|
167
|
+
* path is included so the check cannot false-match a project-context block for a
|
|
168
|
+
* different directory.
|
|
169
|
+
*/
|
|
170
|
+
function entwurfCwdContextOpenTag(cwd: string): string {
|
|
171
|
+
return `${ENTWURF_PROJECT_CONTEXT_OPEN_TAG} path="${join(cwd, "AGENTS.md")}">`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* True when the prompt we are about to send already carries the entwurf
|
|
176
|
+
* project-context block for this cwd (so the augment's cwd-AGENTS section would
|
|
177
|
+
* duplicate it). In this cut the `new` prompt is the whole flattened transcript,
|
|
178
|
+
* so the marker can sit anywhere inside it — `includes`, not `startsWith`.
|
|
179
|
+
*/
|
|
180
|
+
export function promptCarriesEntwurfCwdContext(promptText: string, cwd: string): boolean {
|
|
181
|
+
return promptText.includes(entwurfCwdContextOpenTag(cwd));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Remove ONLY the `## ${cwd}/AGENTS.md` subsection from the augment, keeping the
|
|
186
|
+
* bridge narrative, pi base, home AGENTS.md, and date/cwd. If dropping it leaves
|
|
187
|
+
* the "# Project Context" section with no remaining subsection, drop the empty
|
|
188
|
+
* header too. Ported verbatim from 0.11.0 acp-bridge.ts.
|
|
189
|
+
*/
|
|
190
|
+
export function removeCwdAgentsSectionFromAugment(text: string, cwd: string): string {
|
|
191
|
+
const heading = `## ${join(cwd, "AGENTS.md")}\n\n`;
|
|
192
|
+
const start = text.indexOf(heading);
|
|
193
|
+
if (start < 0) return text;
|
|
194
|
+
|
|
195
|
+
const afterHeading = start + heading.length;
|
|
196
|
+
const nextProjectHeading = text.indexOf("\n\n## ", afterHeading);
|
|
197
|
+
const currentDateSection = text.indexOf("\n\nCurrent date:", afterHeading);
|
|
198
|
+
const candidates = [nextProjectHeading, currentDateSection].filter((idx) => idx >= 0);
|
|
199
|
+
const end = candidates.length > 0 ? Math.min(...candidates) : text.length;
|
|
200
|
+
let result = `${text.slice(0, start).trimEnd()}${text.slice(end)}`;
|
|
201
|
+
|
|
202
|
+
const projectHeader = "# Project Context\n\nProject-specific instructions and guidelines:";
|
|
203
|
+
const projectStart = result.indexOf(projectHeader);
|
|
204
|
+
if (projectStart >= 0) {
|
|
205
|
+
const projectEnd = result.indexOf("\n\nCurrent date:", projectStart + projectHeader.length);
|
|
206
|
+
const projectBody =
|
|
207
|
+
projectEnd >= 0
|
|
208
|
+
? result.slice(projectStart + projectHeader.length, projectEnd)
|
|
209
|
+
: result.slice(projectStart + projectHeader.length);
|
|
210
|
+
if (!projectBody.includes("\n## ")) {
|
|
211
|
+
result =
|
|
212
|
+
projectEnd >= 0
|
|
213
|
+
? `${result.slice(0, projectStart).trimEnd()}${result.slice(projectEnd)}`
|
|
214
|
+
: result.slice(0, projectStart).trimEnd();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return result.trim();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Prepend the first-user-message augment to a `new` session's prompt blocks.
|
|
223
|
+
*
|
|
224
|
+
* De-dup: if `promptBlocks` already carry the entwurf project-context for this
|
|
225
|
+
* cwd, the augment's cwd-AGENTS section is dropped first. The pi Context is NEVER
|
|
226
|
+
* touched — the augment lives only on the wire, so it stays out of
|
|
227
|
+
* `contextMessageSignatures`. A whitespace-only augment is omitted entirely.
|
|
228
|
+
*/
|
|
229
|
+
export function prependNewPromptAugment(promptBlocks: AcpTextBlock[], params: PiContextAugmentParams): AcpTextBlock[] {
|
|
230
|
+
const promptText = promptBlocks.map((b) => b.text).join("\n");
|
|
231
|
+
let augment = buildPiContextAugment(params);
|
|
232
|
+
if (promptCarriesEntwurfCwdContext(promptText, params.cwd)) {
|
|
233
|
+
augment = removeCwdAgentsSectionFromAugment(augment, params.cwd);
|
|
234
|
+
}
|
|
235
|
+
const separated = ensurePromptSeparator(augment);
|
|
236
|
+
if (!separated) return promptBlocks;
|
|
237
|
+
return [{ type: "text", text: separated }, ...promptBlocks];
|
|
238
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// ACP backend adapter rail — the PRODUCT seam by which a curated model id selects
|
|
2
|
+
// which ACP backend (claude / future backend / …) drives a turn. See docs/acp-backend-rail.md §9.
|
|
3
|
+
//
|
|
4
|
+
// This seam is DISTINCT from `AcpTurnDeps` (backend.ts), which is the test/runtime
|
|
5
|
+
// seam (fake spawn/connection/clock for the gates). The two are kept apart on
|
|
6
|
+
// purpose (GPT-agreed §9-2): merging them would make a fake-deps fixture look like
|
|
7
|
+
// a fake backend and force the adapter to carry clock/sessionDir/createConnection.
|
|
8
|
+
// The wiring is `defaultDeps(adapter)` — the turn loop in backend.ts stays
|
|
9
|
+
// backend-invariant; only these per-backend functions change with `adapter`.
|
|
10
|
+
//
|
|
11
|
+
// STATUS: Step A+B done — backend.ts is wired to this rail. The turn loop in
|
|
12
|
+
// backend.ts delegates every per-backend step through the resolved adapter
|
|
13
|
+
// (resolveLaunch/ensureOverlay/loadCarrier/buildSessionMeta/enforceModel/
|
|
14
|
+
// launchEnvDefaults); there is no private resolveLaunch copy in backend.ts.
|
|
15
|
+
// `resolveClaudeLaunch` below is the single source for the claude launch spec.
|
|
16
|
+
//
|
|
17
|
+
// Fence: imported by the root program with `.js` suffixes, same as the sibling
|
|
18
|
+
// lib/acp modules — no new strip-types fence.
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { createRequire } from "node:module";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
|
|
24
|
+
import type { AcpConnectionLike } from "./acp-client.js";
|
|
25
|
+
import type { ResolvedAcpConfig } from "./config.js";
|
|
26
|
+
import { loadEngraving } from "./engraving.js";
|
|
27
|
+
import { curatedClaudeModels, SUPPORTED_ANTHROPIC_MODEL_IDS } from "./models.js";
|
|
28
|
+
import { claudeLaunchEnvDefaults, ensureClaudeConfigOverlay } from "./overlay.js";
|
|
29
|
+
import { buildClaudeSessionMeta } from "./tool-surface.js";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Shared shapes
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/** Launch spec for the ACP server child process. */
|
|
36
|
+
export interface AcpLaunchSpec {
|
|
37
|
+
command: string;
|
|
38
|
+
args: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A curated provider model row (the shape pi.registerProvider({ models }) takes). */
|
|
42
|
+
export type AcpModelRow = ReturnType<typeof curatedClaudeModels>[number];
|
|
43
|
+
|
|
44
|
+
/** routeModel result — the backend-native model id with the curation prefix stripped. */
|
|
45
|
+
export interface AcpRoutedModel {
|
|
46
|
+
nativeModelId: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** resolveLaunch input — native (prefix-stripped) model id + resolved operator config. */
|
|
50
|
+
export interface AcpLaunchParams {
|
|
51
|
+
cwd: string;
|
|
52
|
+
modelId: string;
|
|
53
|
+
nativeModelId: string;
|
|
54
|
+
config: ResolvedAcpConfig;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** loadCarrier input — the mcp server names plus the resolved config (so a backend
|
|
58
|
+
* whose carrier depends on its own `config.adapterSettings` can read it). backend.ts
|
|
59
|
+
* still never inspects config — it just passes it through. */
|
|
60
|
+
export interface AcpCarrierParams {
|
|
61
|
+
mcpServerNames: string[];
|
|
62
|
+
config: ResolvedAcpConfig;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** ensureOverlay input — cwd + (native) model id + the resolved config. A backend
|
|
66
|
+
* whose overlay/env depends on its OWN settings reads them off `config.adapterSettings`
|
|
67
|
+
* here; backend.ts never inspects config. Same shape as AcpLaunchParams (overlay and
|
|
68
|
+
* launch are distinct phases, so they keep distinct names). */
|
|
69
|
+
export interface AcpOverlayParams {
|
|
70
|
+
cwd: string;
|
|
71
|
+
modelId: string;
|
|
72
|
+
nativeModelId: string;
|
|
73
|
+
config: ResolvedAcpConfig;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** buildSessionMeta input — mirrors the newSession `_meta` inputs. */
|
|
77
|
+
export interface AcpSessionMetaParams {
|
|
78
|
+
modelId: string;
|
|
79
|
+
nativeModelId: string;
|
|
80
|
+
config: ResolvedAcpConfig;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** enforceModel input — the live connection + acp session + native id. */
|
|
84
|
+
export interface AcpEnforceModelParams {
|
|
85
|
+
connection: AcpConnectionLike;
|
|
86
|
+
acpSessionId: string;
|
|
87
|
+
modelId: string;
|
|
88
|
+
nativeModelId: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** ensureOverlay result — env overrides backend.ts merges into the spawn env. */
|
|
92
|
+
export interface AcpOverlayResult {
|
|
93
|
+
envOverrides: Record<string, string>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** resolveAdapterSettings input — the RAW (untyped) `entwurfProvider` blocks plus
|
|
97
|
+
* their file paths. This is the ONE seam by which a backend reads its OWN settings
|
|
98
|
+
* (e.g. a connection id, a profile/tenant, a state-home path) WITHOUT those
|
|
99
|
+
* backend-specific keys ever touching the common ResolvedAcpConfig (fat-bridge
|
|
100
|
+
* regression). `mergedBlock` is
|
|
101
|
+
* the project-over-global merge (project keys win); the per-file blocks + paths are
|
|
102
|
+
* for error attribution. A backend with no own settings returns `undefined`. */
|
|
103
|
+
export interface AcpAdapterSettingsParams {
|
|
104
|
+
globalBlock: Record<string, unknown>;
|
|
105
|
+
projectBlock: Record<string, unknown>;
|
|
106
|
+
mergedBlock: Record<string, unknown>;
|
|
107
|
+
globalPath: string;
|
|
108
|
+
projectPath: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// The adapter interface
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
export interface AcpBackendAdapter {
|
|
116
|
+
/** Discriminator. backend.ts stores this on BridgeSession/configSig so reuse
|
|
117
|
+
* and diagnostics never re-parse the model-id string. */
|
|
118
|
+
readonly backend: string;
|
|
119
|
+
|
|
120
|
+
/** Does this adapter own `modelId`? If so, return the backend-native id (prefix
|
|
121
|
+
* stripped); else undefined. Returning the native id here (vs a bare boolean)
|
|
122
|
+
* keeps the registry from leaning on adapter order and gives resolveLaunch /
|
|
123
|
+
* enforceModel the value they must actually send to the backend. */
|
|
124
|
+
routeModel(modelId: string): AcpRoutedModel | undefined;
|
|
125
|
+
|
|
126
|
+
/** Curated model rows this backend contributes to the single `entwurf` provider. */
|
|
127
|
+
curatedModels(): AcpModelRow[];
|
|
128
|
+
|
|
129
|
+
/** Parse this backend's OWN settings from the raw entwurfProvider blocks, returning
|
|
130
|
+
* an opaque value config.ts stores on `ResolvedAcpConfig.adapterSettings`. backend.ts
|
|
131
|
+
* NEVER inspects the result; only this adapter's other methods read it (casting their
|
|
132
|
+
* own type back). A backend with no own settings returns `undefined`. This keeps the
|
|
133
|
+
* common config free of backend-named fields (see AcpAdapterSettingsParams). */
|
|
134
|
+
resolveAdapterSettings(params: AcpAdapterSettingsParams): unknown;
|
|
135
|
+
|
|
136
|
+
/** Resolve the ACP server launch (command + args), honoring an env override. */
|
|
137
|
+
resolveLaunch(params: AcpLaunchParams): AcpLaunchSpec;
|
|
138
|
+
|
|
139
|
+
/** Static launch env defaults merged over process.env at spawn. */
|
|
140
|
+
launchEnvDefaults(): Record<string, string>;
|
|
141
|
+
|
|
142
|
+
/** Materialize the config overlay (auth passthrough + state hiding) and return
|
|
143
|
+
* the env overrides to merge at spawn. A no-op backend returns { envOverrides: {} }.
|
|
144
|
+
* Receives the resolved config so a settings-dependent overlay can read its own
|
|
145
|
+
* `config.adapterSettings`; settings-derived spawn env rides the returned
|
|
146
|
+
* `envOverrides` (launchEnvDefaults stays static). */
|
|
147
|
+
ensureOverlay(params: AcpOverlayParams): AcpOverlayResult;
|
|
148
|
+
|
|
149
|
+
/** Render the optional short operator carrier (engraving). Kept SEPARATE from
|
|
150
|
+
* buildSessionMeta so backend.ts can load it ONCE and fold the same value into
|
|
151
|
+
* both the config signature and the session meta (they must agree). A carrier-
|
|
152
|
+
* less backend returns null WITHOUT calling loadEngraving, so it never trips the
|
|
153
|
+
* shipped-engraving / appendSystemPrompt signature. */
|
|
154
|
+
loadCarrier(params: AcpCarrierParams): string | null;
|
|
155
|
+
|
|
156
|
+
/** Build the `_meta` handed to newSession. `undefined` → backend.ts omits the
|
|
157
|
+
* `_meta` key entirely (carrier-less backend). Rich operator context rides the
|
|
158
|
+
* first-user augment regardless, never this carrier. */
|
|
159
|
+
buildSessionMeta(params: AcpSessionMetaParams, carrier: string | null): Record<string, unknown> | undefined;
|
|
160
|
+
|
|
161
|
+
/** Enforce the requested model on the live ACP session. Single method absorbs
|
|
162
|
+
* the per-backend difference (claude: per-turn session/set_config_option; a
|
|
163
|
+
* launch-pinned backend: no-op here). backend.ts wraps the call in withTimeout. */
|
|
164
|
+
enforceModel(params: AcpEnforceModelParams): Promise<void>;
|
|
165
|
+
|
|
166
|
+
/** Backend-specific fields folded into bridgeConfigSignature (reuse invalidation):
|
|
167
|
+
* connection/profile/env-derived STABLE ids only — never raw env values / secrets.
|
|
168
|
+
* Reads ONLY this backend's opaque `adapterSettings` (NOT the whole config), so the
|
|
169
|
+
* signature contract can never accidentally fold a common field. MUST be a flat,
|
|
170
|
+
* sorted-stable primitive map (JSON.stringify determinism — no nested objects /
|
|
171
|
+
* non-deterministic order). `backend` + `nativeModelId` are added by backend.ts. */
|
|
172
|
+
configSignatureFields(adapterSettings: unknown): Record<string, unknown>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// claude adapter — the first implementation (the rail's reference backend)
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
const SUPPORTED_CLAUDE_IDS: ReadonlySet<string> = new Set(SUPPORTED_ANTHROPIC_MODEL_IDS);
|
|
180
|
+
|
|
181
|
+
/** Resolve the claude-agent-acp launch — package bin (resolve), env override for debug.
|
|
182
|
+
* This is the single source for the claude launch spec; backend.ts holds no private copy. */
|
|
183
|
+
function resolveClaudeLaunch(): AcpLaunchSpec {
|
|
184
|
+
const override = process.env.CLAUDE_AGENT_ACP_COMMAND?.trim();
|
|
185
|
+
if (override) return { command: "bash", args: ["-lc", override] };
|
|
186
|
+
const require = createRequire(import.meta.url);
|
|
187
|
+
const pkgJsonPath = require.resolve("@agentclientprotocol/claude-agent-acp/package.json");
|
|
188
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as { bin?: string | Record<string, string> };
|
|
189
|
+
const binPath = typeof pkgJson.bin === "string" ? pkgJson.bin : pkgJson.bin?.["claude-agent-acp"];
|
|
190
|
+
if (!binPath) throw new Error("@agentclientprotocol/claude-agent-acp resolved but exposes no bin entry");
|
|
191
|
+
return { command: process.execPath, args: [join(dirname(pkgJsonPath), binPath)] };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const claudeAdapter: AcpBackendAdapter = {
|
|
195
|
+
backend: "claude",
|
|
196
|
+
|
|
197
|
+
// Claude owns its UNPREFIXED curated ids only (GPT-agreed §9-1). The native id
|
|
198
|
+
// equals the curated id — claude carries no curation prefix to strip.
|
|
199
|
+
routeModel(modelId) {
|
|
200
|
+
return SUPPORTED_CLAUDE_IDS.has(modelId) ? { nativeModelId: modelId } : undefined;
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
curatedModels() {
|
|
204
|
+
return curatedClaudeModels();
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
// Claude carries no backend-specific settings — its entire surface is common
|
|
208
|
+
// config (tools/permissions/settingSources/…). undefined → config.adapterSettings
|
|
209
|
+
// is undefined and no claude method reads it.
|
|
210
|
+
resolveAdapterSettings() {
|
|
211
|
+
return undefined;
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
resolveLaunch() {
|
|
215
|
+
return resolveClaudeLaunch();
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
launchEnvDefaults() {
|
|
219
|
+
return claudeLaunchEnvDefaults();
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
ensureOverlay() {
|
|
223
|
+
// Claude's overlay is constant (no settings dependence), so it ignores params.
|
|
224
|
+
ensureClaudeConfigOverlay();
|
|
225
|
+
// CLAUDE_CONFIG_DIR rides launchEnvDefaults(); the overlay materialization
|
|
226
|
+
// itself contributes no extra spawn env.
|
|
227
|
+
return { envOverrides: {} };
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
loadCarrier({ mcpServerNames }) {
|
|
231
|
+
// Claude's carrier is the shipped engraving — it does not read config.
|
|
232
|
+
return loadEngraving({ backend: "claude", mcpServerNames });
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
buildSessionMeta({ nativeModelId, config }, carrier) {
|
|
236
|
+
// buildClaudeSessionMeta always returns an object (it only omits the
|
|
237
|
+
// systemPrompt KEY when carrier is absent), so claude never yields undefined.
|
|
238
|
+
return buildClaudeSessionMeta(
|
|
239
|
+
{
|
|
240
|
+
modelId: nativeModelId,
|
|
241
|
+
tools: config.tools,
|
|
242
|
+
permissionAllow: config.permissionAllow,
|
|
243
|
+
disallowedTools: config.disallowedTools,
|
|
244
|
+
settingSources: config.settingSources,
|
|
245
|
+
strictMcpConfig: config.strictMcpConfig,
|
|
246
|
+
skillPlugins: config.skillPlugins,
|
|
247
|
+
},
|
|
248
|
+
carrier ?? undefined,
|
|
249
|
+
);
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
async enforceModel({ connection, acpSessionId, nativeModelId, modelId }) {
|
|
253
|
+
const setConfig = connection.setSessionConfigOption;
|
|
254
|
+
if (typeof setConfig !== "function") {
|
|
255
|
+
throw new Error(`setSessionConfigOption unsupported — cannot enforce model ${modelId}`);
|
|
256
|
+
}
|
|
257
|
+
await setConfig.call(connection, { sessionId: acpSessionId, configId: "model", value: nativeModelId });
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
configSignatureFields(_adapterSettings) {
|
|
261
|
+
// Claude folds no extra backend-specific fields beyond backend + nativeModelId
|
|
262
|
+
// (which backend.ts adds). A future backend reads its own stable id off
|
|
263
|
+
// `_adapterSettings` here.
|
|
264
|
+
return {};
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Registry — modelId → adapter
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
/** Registered adapters. Order carries NO routing authority — routeModel decides.
|
|
273
|
+
* Step A: claude only. A second backend appends here with its reserved prefix
|
|
274
|
+
* (e.g. `<backend>-*`), and the fail-fast below proves no two adapters claim one id. */
|
|
275
|
+
const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter];
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Resolve the backend adapter that owns `modelId`.
|
|
279
|
+
*
|
|
280
|
+
* GPT-agreed §9-1 fail-fast contract:
|
|
281
|
+
* - 0 matches → throw (unknown model — no silent default).
|
|
282
|
+
* - 2+ matches → throw (prefix collision — a startup-visible registry bug).
|
|
283
|
+
*
|
|
284
|
+
* The thrown native id is recovered from the single matching adapter's routeModel,
|
|
285
|
+
* so callers get `{ adapter, nativeModelId }` and never re-parse the id string.
|
|
286
|
+
*/
|
|
287
|
+
export function resolveAcpBackendAdapter(modelId: string): { adapter: AcpBackendAdapter; nativeModelId: string } {
|
|
288
|
+
const matches: Array<{ adapter: AcpBackendAdapter; nativeModelId: string }> = [];
|
|
289
|
+
for (const adapter of ADAPTERS) {
|
|
290
|
+
const routed = adapter.routeModel(modelId);
|
|
291
|
+
if (routed) matches.push({ adapter, nativeModelId: routed.nativeModelId });
|
|
292
|
+
}
|
|
293
|
+
if (matches.length === 0) {
|
|
294
|
+
throw new Error(`entwurf: no ACP backend adapter owns model id ${JSON.stringify(modelId)}`);
|
|
295
|
+
}
|
|
296
|
+
if (matches.length > 1) {
|
|
297
|
+
const owners = matches.map((m) => m.adapter.backend).join(", ");
|
|
298
|
+
throw new Error(`entwurf: model id ${JSON.stringify(modelId)} is claimed by multiple adapters (${owners})`);
|
|
299
|
+
}
|
|
300
|
+
return matches[0];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Every curated model row across all registered adapters — for provider registration.
|
|
304
|
+
* GPT-agreed §9-6 registration-time fail-fast: every curated id must route to EXACTLY
|
|
305
|
+
* one adapter and no id may be duplicated across adapters. Catching it here means a
|
|
306
|
+
* prefix-collision / duplicate surfaces at provider registration, not mid-turn. */
|
|
307
|
+
export function allCuratedModels(): AcpModelRow[] {
|
|
308
|
+
const rows = ADAPTERS.flatMap((adapter) => adapter.curatedModels());
|
|
309
|
+
const seen = new Set<string>();
|
|
310
|
+
for (const row of rows) {
|
|
311
|
+
if (seen.has(row.id)) {
|
|
312
|
+
throw new Error(`entwurf: duplicate curated model id across ACP backend adapters: ${row.id}`);
|
|
313
|
+
}
|
|
314
|
+
seen.add(row.id);
|
|
315
|
+
// Throws on 0 matches (unowned) or 2+ matches (prefix collision).
|
|
316
|
+
resolveAcpBackendAdapter(row.id);
|
|
317
|
+
}
|
|
318
|
+
return rows;
|
|
319
|
+
}
|