@junghanacs/entwurf 0.18.0 → 0.18.2

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.
@@ -0,0 +1,196 @@
1
+ // RAW MEASUREMENT PROBE — not a gate, not in any check tier.
2
+ //
3
+ // LIVE=1 node --experimental-strip-types scripts/raw-acp-compaction-measure/probe.ts
4
+ //
5
+ // WHY THIS EXISTS. claude-agent-acp 0.75.0 (#991, `f74a517`) stopped surfacing Claude's
6
+ // context compaction as assistant text and started surfacing it as a SYNTHETIC ACP tool
7
+ // lifecycle: a `tool_call` with `kind: "think"`, title `Compact conversation`, and
8
+ // `_meta.contextCompaction` schema v1. Reading the dist says that much
9
+ // (`dist/acp-agent.js:31`, `:1877`, `:2711-2742`); it does not say what an entwurf
10
+ // operator actually SEES, because that depends on our own `applyAcpSessionUpdate`.
11
+ //
12
+ // So this probe drives one real `/compact` turn on the pinned adapter, captures the
13
+ // tool-lifecycle notifications verbatim, and then replays those exact notifications
14
+ // through the PRODUCTION event mapper — the same function backend.ts calls — and prints
15
+ // the pi-side transcript fragment they produce. The point is the join: vendor wire on one
16
+ // side, our rendered notice on the other, in one receipt.
17
+ //
18
+ // It asserts nothing and blocks nothing. The receipt goes in README.md next to it.
19
+
20
+ import { type ChildProcessByStdio, spawn } from "node:child_process";
21
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
22
+ import { createRequire } from "node:module";
23
+ import { tmpdir } from "node:os";
24
+ import { join } from "node:path";
25
+ import { Readable, Writable } from "node:stream";
26
+ import { ndJsonStream, PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
27
+ import { connectAcpClient } from "../../pi-extensions/lib/acp/acp-client.ts";
28
+ import { applyAcpSessionUpdate, createAcpStreamState } from "../../pi-extensions/lib/acp/event-mapper.ts";
29
+ import { terminateChild } from "../lib/acp-child-cleanup.ts";
30
+
31
+ const MODEL_ID = process.env.ENTWURF_ACP_RAW_TURN_MODEL ?? "claude-sonnet-5";
32
+ const OUT = process.env.RAW_COMPACTION_OUT ?? join(tmpdir(), "raw-acp-compaction.json");
33
+
34
+ if (process.env.LIVE !== "1") {
35
+ console.error("[raw-acp-compaction] set LIVE=1 to spend a real turn.");
36
+ process.exit(0);
37
+ }
38
+
39
+ function withTimeout<T>(label: string, p: Promise<T>, ms: number): Promise<T> {
40
+ let timer: ReturnType<typeof setTimeout> | undefined;
41
+ const timeout = new Promise<never>((_, reject) => {
42
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
43
+ });
44
+ return Promise.race([p, timeout]).finally(() => {
45
+ if (timer) clearTimeout(timer);
46
+ });
47
+ }
48
+
49
+ const require = createRequire(import.meta.url);
50
+ const bin = require.resolve("@agentclientprotocol/claude-agent-acp/dist/index.js");
51
+
52
+ const scratch = await mkdtemp(join(tmpdir(), "raw-acp-compaction-"));
53
+ const child: ChildProcessByStdio<Writable, Readable, Readable> = spawn(process.execPath, [bin], {
54
+ cwd: scratch,
55
+ env: { ...process.env },
56
+ stdio: ["pipe", "pipe", "pipe"],
57
+ }) as ChildProcessByStdio<Writable, Readable, Readable>;
58
+
59
+ const stderrTail: string[] = [];
60
+ child.stderr.on("data", (c) => {
61
+ stderrTail.push(c.toString());
62
+ if (stderrTail.length > 200) stderrTail.shift();
63
+ });
64
+
65
+ const stream = ndJsonStream(
66
+ Writable.toWeb(child.stdin) as unknown as WritableStream<Uint8Array>,
67
+ Readable.toWeb(child.stdout) as unknown as ReadableStream<Uint8Array>,
68
+ );
69
+
70
+ // Every tool-lifecycle notification, verbatim, in arrival order.
71
+ const toolUpdates: Record<string, unknown>[] = [];
72
+ let agentText = "";
73
+
74
+ const connection = connectAcpClient(
75
+ stream as never,
76
+ {
77
+ sessionUpdate: async (notification: { update?: Record<string, unknown> }) => {
78
+ const u = notification?.update;
79
+ if (!u) return;
80
+ if (u.sessionUpdate === "tool_call" || u.sessionUpdate === "tool_call_update") {
81
+ toolUpdates.push(structuredClone(u));
82
+ }
83
+ if (u.sessionUpdate === "agent_message_chunk") {
84
+ const t = (u.content as { text?: unknown } | undefined)?.text;
85
+ if (typeof t === "string") agentText += t;
86
+ }
87
+ },
88
+ requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
89
+ readTextFile: async () => {
90
+ throw new Error("unexpected readTextFile");
91
+ },
92
+ writeTextFile: async () => {
93
+ throw new Error("unexpected writeTextFile");
94
+ },
95
+ } as never,
96
+ );
97
+
98
+ let failure: Error | null = null;
99
+ try {
100
+ await withTimeout(
101
+ "initialize",
102
+ connection.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} } as never),
103
+ 30_000,
104
+ );
105
+ const session = (await withTimeout(
106
+ "newSession",
107
+ connection.newSession({ cwd: scratch, mcpServers: [] } as never),
108
+ 60_000,
109
+ )) as { sessionId?: string };
110
+ const sessionId = session?.sessionId;
111
+ if (!sessionId) throw new Error("newSession returned no sessionId");
112
+
113
+ const setConfig = (connection as unknown as { setSessionConfigOption?: unknown }).setSessionConfigOption;
114
+ if (typeof setConfig === "function") {
115
+ await withTimeout(
116
+ "setSessionConfigOption",
117
+ (setConfig as (a: unknown) => Promise<unknown>).call(connection, {
118
+ sessionId,
119
+ configId: "model",
120
+ value: MODEL_ID,
121
+ }),
122
+ 30_000,
123
+ );
124
+ }
125
+
126
+ // One tiny turn so the transcript has something to compact.
127
+ await withTimeout(
128
+ "seed prompt",
129
+ connection.prompt({
130
+ sessionId,
131
+ prompt: [{ type: "text", text: "Reply with exactly SEED and nothing else." }],
132
+ } as never),
133
+ 180_000,
134
+ );
135
+ const seedText = agentText;
136
+ agentText = "";
137
+ const beforeCompact = toolUpdates.length;
138
+
139
+ // The compaction turn. `/compact` is the vendor's own trigger for the lifecycle
140
+ // under measurement (`dist/acp-agent.js:2054`, `:2103`).
141
+ const compactResult = (await withTimeout(
142
+ "compact prompt",
143
+ connection.prompt({ sessionId, prompt: [{ type: "text", text: "/compact" }] } as never),
144
+ 300_000,
145
+ )) as { stopReason?: string };
146
+
147
+ const compactionUpdates = toolUpdates.slice(beforeCompact);
148
+
149
+ // Replay the captured notifications through the PRODUCTION mapper.
150
+ const notices: string[] = [];
151
+ const state = createAcpStreamState(
152
+ { push: (ev: unknown) => notices.push(JSON.stringify(ev)) } as never,
153
+ { api: "entwurf", provider: "entwurf", model: MODEL_ID } as never,
154
+ { timestamp: 0 },
155
+ );
156
+ for (const u of compactionUpdates) applyAcpSessionUpdate(state, u);
157
+
158
+ const receipt = {
159
+ measuredAt: new Date().toISOString(),
160
+ adapter: JSON.parse(
161
+ (await import("node:fs")).readFileSync(
162
+ require.resolve("@agentclientprotocol/claude-agent-acp/package.json"),
163
+ "utf8",
164
+ ),
165
+ ).version,
166
+ model: MODEL_ID,
167
+ seedText: seedText.trim().slice(0, 80),
168
+ compactStopReason: compactResult?.stopReason,
169
+ compactionToolUpdates: compactionUpdates,
170
+ mappedContent: state.output.content,
171
+ mappedStreamEvents: notices,
172
+ };
173
+ await writeFile(OUT, `${JSON.stringify(receipt, null, "\t")}\n`, "utf8");
174
+ console.log(`[raw-acp-compaction] receipt -> ${OUT}`);
175
+ console.log(` adapter: ${receipt.adapter}`);
176
+ console.log(` compact stopReason: ${receipt.compactStopReason}`);
177
+ console.log(` tool updates: ${compactionUpdates.length}`);
178
+ for (const u of compactionUpdates) {
179
+ console.log(
180
+ ` ${String(u.sessionUpdate)} kind=${String(u.kind ?? "-")} status=${String(u.status ?? "-")} title=${JSON.stringify(u.title ?? null)} meta=${JSON.stringify(u._meta ?? null)}`,
181
+ );
182
+ }
183
+ console.log(` pi-side content blocks: ${JSON.stringify(state.output.content)}`);
184
+ } catch (err) {
185
+ failure = err instanceof Error ? err : new Error(String(err));
186
+ console.error(`[raw-acp-compaction] stderr tail:\n${stderrTail.slice(-20).join("")}`);
187
+ } finally {
188
+ connection.close?.();
189
+ await terminateChild(child);
190
+ await rm(scratch, { recursive: true, force: true }).catch(() => {});
191
+ }
192
+
193
+ if (failure) {
194
+ console.error(`[raw-acp-compaction] FAILED: ${failure.message}`);
195
+ process.exit(1);
196
+ }
@@ -3,7 +3,7 @@
3
3
  // LIVE=1 ./run.sh smoke-acp-raw-turn-live
4
4
  //
5
5
  // What this proves (and ONLY this): the pinned Claude ACP adapter
6
- // (@agentclientprotocol/claude-agent-acp@0.73.0) spawns, speaks the ACP wire
6
+ // (@agentclientprotocol/claude-agent-acp@0.75.1) spawns, speaks the ACP wire
7
7
  // protocol over stdio NDJSON, and returns one real model turn. It is the
8
8
  // bytes-flow proof that the S2a dep surface is not just installable but
9
9
  // actually drivable — before any provider/overlay/streamSimple code (S2b+).