@zhushanwen/pi-system-prompt-trace 0.1.8 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-system-prompt-trace",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "System prompt trace extension: appends taiji:system-prompt entries to the session JSONL whenever the effective system prompt is established or changes",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -72,6 +72,8 @@ function makeHarness(initialPrompt: string, opts?: { noSessionFile?: boolean }):
72
72
 
73
73
  const env: TraceEnv = {
74
74
  readLastPromptFromFile: (filePath) => readLastPromptFromSessionFile(filePath),
75
+ // F1b:A12 聚焦基线恢复,不涉回落事实(回落专测在 a11 / wiring)
76
+ getPresetFallback: () => undefined,
75
77
  };
76
78
 
77
79
  const ctx: TraceContext = {
@@ -20,7 +20,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
20
20
 
21
21
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
22
22
 
23
- import { isSystemPromptTraceEntryData, SYSTEM_PROMPT_CUSTOM_TYPE } from "../types.js";
23
+ import { isSystemPromptTraceEntryData, PRESET_FALLBACK_ENV_KEYS, SYSTEM_PROMPT_CUSTOM_TYPE } from "../types.js";
24
24
  import type { SystemPromptTraceEntryData } from "../types.js";
25
25
 
26
26
  // trace.ts 的 computePromptHash 已收敛为包内私有(无外部消费方);测试本地同款实现计算期望值。
@@ -337,6 +337,61 @@ describe("index.ts wiring SDK 契约", () => {
337
337
  });
338
338
  });
339
339
 
340
+ it("F1b wiring:process.env 两键齐备 → entry 带 presetFallback(from/to);既有字段不削弱", async () => {
341
+ const ext = await loadExtension();
342
+ const h = createWiringHarness();
343
+ process.env[PRESET_FALLBACK_ENV_KEYS.FROM] = "custom:gone-uuid";
344
+ process.env[PRESET_FALLBACK_ENV_KEYS.TO] = "builtin:full";
345
+ try {
346
+ ext(h.pi);
347
+ const ctx = createCtx(() => P1, "sess-w-fb");
348
+ await emit(h, "session_start", { type: "session_start", reason: "startup" }, ctx);
349
+ await emit(h, "turn_start", { type: "turn_start", turnIndex: 0, timestamp: 0 }, ctx);
350
+ } finally {
351
+ delete process.env[PRESET_FALLBACK_ENV_KEYS.FROM];
352
+ delete process.env[PRESET_FALLBACK_ENV_KEYS.TO];
353
+ }
354
+ expect(h.entries).toHaveLength(1);
355
+ expect(entryData(h, 0).presetFallback).toEqual({ from: "custom:gone-uuid", to: "builtin:full" });
356
+ expect(entryData(h, 0)).toMatchObject({
357
+ version: 1,
358
+ reason: "initial",
359
+ fullText: P1,
360
+ charCount: P1.length,
361
+ hash: computePromptHash(P1),
362
+ });
363
+ });
364
+
365
+ it("F1b wiring:env 未注入 / 两键为空串(runtime 显式清除)→ entry 不含 presetFallback(无假披露)", async () => {
366
+ const ext = await loadExtension();
367
+ // 态 1:未注入
368
+ delete process.env[PRESET_FALLBACK_ENV_KEYS.FROM];
369
+ delete process.env[PRESET_FALLBACK_ENV_KEYS.TO];
370
+ const hAbsent = createWiringHarness();
371
+ ext(hAbsent.pi);
372
+ const ctxAbsent = createCtx(() => P1, "sess-w-fb-absent");
373
+ await emit(hAbsent, "session_start", { type: "session_start", reason: "startup" }, ctxAbsent);
374
+ await emit(hAbsent, "turn_start", { type: "turn_start", turnIndex: 0, timestamp: 0 }, ctxAbsent);
375
+ expect(hAbsent.entries).toHaveLength(1);
376
+ expect(Object.prototype.hasOwnProperty.call(entryData(hAbsent, 0), "presetFallback")).toBe(false);
377
+
378
+ // 态 2:空串(runtime 未回落时写空串显式清除继承值)——不得当回落事实
379
+ process.env[PRESET_FALLBACK_ENV_KEYS.FROM] = "";
380
+ process.env[PRESET_FALLBACK_ENV_KEYS.TO] = "";
381
+ const hEmpty = createWiringHarness();
382
+ try {
383
+ ext(hEmpty.pi);
384
+ const ctxEmpty = createCtx(() => P1, "sess-w-fb-empty");
385
+ await emit(hEmpty, "session_start", { type: "session_start", reason: "startup" }, ctxEmpty);
386
+ await emit(hEmpty, "turn_start", { type: "turn_start", turnIndex: 0, timestamp: 0 }, ctxEmpty);
387
+ } finally {
388
+ delete process.env[PRESET_FALLBACK_ENV_KEYS.FROM];
389
+ delete process.env[PRESET_FALLBACK_ENV_KEYS.TO];
390
+ }
391
+ expect(hEmpty.entries).toHaveLength(1);
392
+ expect(Object.prototype.hasOwnProperty.call(entryData(hEmpty, 0), "presetFallback")).toBe(false);
393
+ });
394
+
340
395
  it("getSystemPrompt 抛错 → handler 吞掉不写 entry(留痕是诊断旁路,不影响 agent 主流程)", async () => {
341
396
  const ext = await loadExtension();
342
397
  const h = createWiringHarness();
@@ -18,7 +18,7 @@ import { describe, expect, it } from "vitest";
18
18
  import { createSystemPromptTrace } from "../trace.js";
19
19
  import type { SystemPromptTrace, TraceContext, TraceEnv } from "../trace.js";
20
20
  import { isSystemPromptTraceEntryData, SYSTEM_PROMPT_CUSTOM_TYPE } from "../types.js";
21
- import type { SystemPromptTraceEntryData, SwitchStash } from "../types.js";
21
+ import type { PresetFallbackFact, SystemPromptTraceEntryData, SwitchStash } from "../types.js";
22
22
 
23
23
  // trace.ts 的 computePromptHash 已收敛为包内私有(无外部消费方);测试本地同款实现计算期望值。
24
24
  const computePromptHash = (text: string): string =>
@@ -37,13 +37,15 @@ interface Harness {
37
37
  newLogic(): SystemPromptTrace;
38
38
  }
39
39
 
40
- function makeHarness(initialPrompt: string): Harness {
40
+ function makeHarness(initialPrompt: string, presetFallback?: PresetFallbackFact): Harness {
41
41
  const entries: SystemPromptTraceEntryData[] = [];
42
42
  let prompt = initialPrompt;
43
43
  const stash: SwitchStash = { pending: null };
44
44
  // A11 不涉文件路径:三档基线全部 miss(直读 / fork prev 文件路径均不可读),隔离验证时机/去重/映射逻辑
45
45
  const env: TraceEnv = {
46
46
  readLastPromptFromFile: () => null,
47
+ // F1b:默认无回落事实;需要时经参数注入(进程 env 语义的 fake)
48
+ getPresetFallback: () => presetFallback,
47
49
  };
48
50
  const ctx: TraceContext = {
49
51
  getSystemPrompt: () => prompt,
@@ -134,6 +136,34 @@ describe("A11 留痕时机与去重", () => {
134
136
  expect(entry.parentVersionDiffSummary).toContain("+ [Available Models]");
135
137
  });
136
138
 
139
+ it("F1b 模式回落事实:env 携带 from/to → 本进程每条 entry 均带 presetFallback(resume 快照与 change 一致),既有字段不削弱", () => {
140
+ const fact: PresetFallbackFact = { from: "custom:gone-uuid", to: "builtin:full" };
141
+ const h = makeHarness(P1, fact);
142
+ h.logic.onSessionStart("resume", undefined, h.ctx);
143
+ h.logic.onTurnStart(h.ctx); // v1 resume
144
+ h.setPrompt(P2);
145
+ h.logic.onTurnStart(h.ctx); // v2 change
146
+ expect(h.entries).toHaveLength(2);
147
+ expect(h.entries[0]?.presetFallback).toEqual(fact);
148
+ expect(h.entries[1]?.presetFallback).toEqual(fact);
149
+ // 既有形状与语义不受影响
150
+ expect(h.entries[0]).toMatchObject({
151
+ version: 1, reason: "resume", fullText: P1, charCount: P1.length, hash: computePromptHash(P1),
152
+ });
153
+ expect(h.entries[1]).toMatchObject({
154
+ version: 2, reason: "change", fullText: P2, charCount: P2.length, hash: computePromptHash(P2),
155
+ });
156
+ });
157
+
158
+ it("F1b 无回落(getPresetFallback → undefined)→ entry 无 presetFallback 字段(形状向后兼容)", () => {
159
+ const h = makeHarness(P1);
160
+ h.logic.onSessionStart("startup", undefined, h.ctx);
161
+ h.logic.onTurnStart(h.ctx);
162
+ expect(h.entries).toHaveLength(1);
163
+ expect(h.entries[0]?.presetFallback).toBeUndefined();
164
+ expect(Object.prototype.hasOwnProperty.call(h.entries[0], "presetFallback")).toBe(false);
165
+ });
166
+
137
167
  it("prompt 变回旧值再写 change v3(时间线保留真实历史,只对当前版本去重)", () => {
138
168
  const h = makeHarness(P1);
139
169
  h.logic.onSessionStart("startup", undefined, h.ctx);
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ import { setPiHandle } from "@zhushanwen/pi-extension-logger";
19
19
  import { readLastPromptFromSessionFile } from "./baseline.js";
20
20
  import { createSystemPromptTrace } from "./trace.js";
21
21
  import type { TraceContext, TraceEnv } from "./trace.js";
22
+ import { readPresetFallbackFromEnv } from "./types.js";
22
23
  import type { SwitchStash } from "./types.js";
23
24
 
24
25
  // 模块级单例:session_before_switch(旧 runtime)→ session_start(新 runtime)之间传递
@@ -45,6 +46,9 @@ export default function systemPromptTraceExtension(pi: ExtensionAPI): void {
45
46
 
46
47
  const env: TraceEnv = {
47
48
  readLastPromptFromFile: (filePath) => readLastPromptFromSessionFile(filePath),
49
+ // F1b(E4 trace 披露面):模式回落事实由 runtime 经 spawn 出站 env 携带。
50
+ // 工厂调用时读一次(进程 env 生命期内不变);未回落 → 空串 → undefined。
51
+ getPresetFallback: () => readPresetFallbackFromEnv(),
48
52
  };
49
53
 
50
54
  const logic = createSystemPromptTrace(env, switchStash);
package/src/trace.ts CHANGED
@@ -20,6 +20,7 @@ const logger = getLogger("pi-system-prompt-trace");
20
20
  import { summarizePromptDiff } from "./diff.js";
21
21
  import { mapReasonForFirstWrite, SYSTEM_PROMPT_CUSTOM_TYPE } from "./types.js";
22
22
  import type {
23
+ PresetFallbackFact,
23
24
  PromptBaseline,
24
25
  SwitchStash,
25
26
  SystemPromptTraceEntryData,
@@ -38,6 +39,13 @@ export interface TraceContext {
38
39
  /** 文件系统侧依赖(wiring 用真实 fs;测试注入临时目录实现)。 */
39
40
  export interface TraceEnv {
40
41
  readLastPromptFromFile(filePath: string): PromptBaseline | null;
42
+ /**
43
+ * 本次 pi 进程启动时的模式回落事实(F1b,E4 trace 披露面);未回落 / 未注入 → undefined。
44
+ *
45
+ * 进程 env 在生命期内不变,故工厂构造时读一次即可;为空字符串(runtime 的「显式
46
+ * 清除」形态)时同样返回 undefined——不得把空串当回落事实。
47
+ */
48
+ getPresetFallback(): PresetFallbackFact | undefined;
41
49
  }
42
50
 
43
51
  export interface SystemPromptTrace {
@@ -65,6 +73,10 @@ export function createSystemPromptTrace(env: TraceEnv, stash: SwitchStash): Syst
65
73
  let sessionStartReason: SessionStartEvent["reason"] | null = null;
66
74
  let baseline: PromptBaseline | null = null;
67
75
  let current: CurrentPrompt | null = null;
76
+ // F1b(设计 `mode-system-composer-density` §7.5 E4 trace 披露面):
77
+ // 本次 pi 进程的模式回落事实由 spawn 出站 env 携带,进程生命期内恒定,构造时读一次。
78
+ // 未回落 / 未注入 → undefined,entry 的 presetFallback 字段不出现(既有形状向后兼容)。
79
+ const presetFallback = env.getPresetFallback();
68
80
 
69
81
  const write = (
70
82
  ctx: TraceContext,
@@ -84,6 +96,11 @@ export function createSystemPromptTrace(env: TraceEnv, stash: SwitchStash): Syst
84
96
  if (parentFullText !== undefined) {
85
97
  data.parentVersionDiffSummary = summarizePromptDiff(parentFullText, text);
86
98
  }
99
+ // 回落事实随本进程写出的每条 entry 携带:本次 pi 运行确实以回落档启动,
100
+ // resume 快照与后续 change 同属该运行,披露一致(事件语义 = 本次运行口径)。
101
+ if (presetFallback !== undefined) {
102
+ data.presetFallback = presetFallback;
103
+ }
87
104
  ctx.appendEntry(SYSTEM_PROMPT_CUSTOM_TYPE, data);
88
105
  };
89
106
 
package/src/types.ts CHANGED
@@ -11,6 +11,53 @@ import type { SessionStartEvent } from "@earendil-works/pi-coding-agent";
11
11
  /** 留痕 entry 的 customType(taiji: 前缀 = taiji 自定义命名空间)。 */
12
12
  export const SYSTEM_PROMPT_CUSTOM_TYPE = "taiji:system-prompt";
13
13
 
14
+ /**
15
+ * 模式回落事实 env 名(字面量镜像 `packages/shared/src/constants.ts` 的
16
+ * `PRESET_FALLBACK_ENV_KEYS`)。
17
+ *
18
+ * extension 独立发布体系不依赖 `@taiji/shared`,只能按字面量镜像——单侧改名即断链
19
+ * (消息见 write 产出的 `presetFallback` 字段消失)。一致性由机器守卫断言:
20
+ * `.githooks/check_env_whitelist_sync.py` 规则 3 A8(漂移即红)。写入方 =
21
+ * `packages/runtime/src/services/session/launch-params.ts` 的 `buildPresetFallbackEnv`。
22
+ */
23
+ export const PRESET_FALLBACK_ENV_KEYS = {
24
+ FROM: "TAIJI_PRESET_FALLBACK_FROM",
25
+ TO: "TAIJI_PRESET_FALLBACK_TO",
26
+ } as const;
27
+
28
+ /**
29
+ * 模式回落事实(F1b,设计 `mode-system-composer-density` §7.5 E4
30
+ * 的 trace 披露面)。
31
+ *
32
+ * 语义 = 本次 pi 进程启动时,sidecar 里的模式定义已不可得,runtime 已回落
33
+ * `builtin:full` 启动(工具面从模式的受限面弹回全工具)——记录在 trace entry 里,
34
+ * 事后审计从 `taiji:system-prompt` 即可得知「提示词为何变了」。
35
+ */
36
+ export interface PresetFallbackFact {
37
+ /** 原(悬空)模式 id。 */
38
+ from: string;
39
+ /** 回落目标模式 id(现行恒 `builtin:full`)。 */
40
+ to: string;
41
+ }
42
+
43
+ /**
44
+ * 从进程 env 读取模式回落事实(wiring 侧唯一 env 读取点;trace.ts 逻辑经 DI 消费)。
45
+ *
46
+ * - FROM 非空 → 返回 `{ from, to }`(TO 缺失时给空串——形状稳定,不抛错);
47
+ * - FROM 缺失 / 空串 → undefined(未回落,或未注入;空串是 runtime 的「显式清除」
48
+ * 语义,见 `buildPresetFallbackEnv`)。
49
+ *
50
+ * 不抛错:env 是外部输入,任何形态异常都退化为「无回落事实」,不阻断留痕。
51
+ */
52
+ export function readPresetFallbackFromEnv(
53
+ env: NodeJS.ProcessEnv = process.env,
54
+ ): PresetFallbackFact | undefined {
55
+ const from = env[PRESET_FALLBACK_ENV_KEYS.FROM];
56
+ if (typeof from !== "string" || from === "") return undefined;
57
+ const to = env[PRESET_FALLBACK_ENV_KEYS.TO];
58
+ return { from, to: typeof to === "string" ? to : "" };
59
+ }
60
+
14
61
  /** 落盘 reason 枚举(initial/resume/change,对齐 DSH request/header 语义,设计 D2)。 */
15
62
  export type TraceReason = "initial" | "resume" | "change";
16
63
 
@@ -27,6 +74,12 @@ export interface SystemPromptTraceEntryData {
27
74
  charCount: number;
28
75
  /** 与上一版的行级 diff 摘要;首条留痕(无 parent)时缺省。 */
29
76
  parentVersionDiffSummary?: string;
77
+ /**
78
+ * 模式回落事实(F1b,E4 trace 披露面):本次 pi 进程以「模式已删除 → 回落
79
+ * builtin:full」启动时出现;未回落(正常启动)时**字段不出现**——既有 entry
80
+ * 形状向后兼容。见 `PresetFallbackFact`。
81
+ */
82
+ presetFallback?: PresetFallbackFact;
30
83
  }
31
84
 
32
85
  /** 跨重启恢复的 hash 基线(三档解析统一产自 session JSONL 留痕 entry 直读)。 */
@@ -55,6 +108,12 @@ export function isSystemPromptTraceEntryData(value: unknown): value is SystemPro
55
108
  const reason = value["reason"];
56
109
  const fullText = value["fullText"];
57
110
  const charCount = value["charCount"];
111
+ // 可选字段(F1b):存在时须为 { from: string, to: string };缺失合法(向后兼容)
112
+ const presetFallback = value["presetFallback"];
113
+ if (presetFallback !== undefined) {
114
+ if (!isRecord(presetFallback)) return false;
115
+ if (typeof presetFallback["from"] !== "string" || typeof presetFallback["to"] !== "string") return false;
116
+ }
58
117
  return (
59
118
  typeof version === "number" &&
60
119
  Number.isFinite(version) &&