@istuen/pt 0.1.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.
Files changed (166) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/dist/agent/api-bridge.d.ts +8 -0
  4. package/dist/agent/api-bridge.js +83 -0
  5. package/dist/agent/index.d.ts +2 -0
  6. package/dist/agent/index.js +3 -0
  7. package/dist/agent/pi-adapter.d.ts +35 -0
  8. package/dist/agent/pi-adapter.js +251 -0
  9. package/dist/agent/registry.d.ts +4 -0
  10. package/dist/agent/registry.js +40 -0
  11. package/dist/asset-health.d.ts +50 -0
  12. package/dist/asset-health.js +267 -0
  13. package/dist/asset-pack/loader.d.ts +47 -0
  14. package/dist/asset-pack/loader.js +121 -0
  15. package/dist/asset-pack/manifest.d.ts +23 -0
  16. package/dist/asset-pack/manifest.js +96 -0
  17. package/dist/asset-pack/md-file-pack.d.ts +41 -0
  18. package/dist/asset-pack/md-file-pack.js +153 -0
  19. package/dist/asset-pack/validate.d.ts +55 -0
  20. package/dist/asset-pack/validate.js +131 -0
  21. package/dist/builtin/assets/blueprints/dev-knowledge.blueprint.yaml +13 -0
  22. package/dist/builtin/assets/domains/agent-info.md +15 -0
  23. package/dist/builtin/assets/domains/authoring.md +201 -0
  24. package/dist/builtin/assets/domains/pack-repair.md +47 -0
  25. package/dist/builtin/assets/domains/project-analysis.md +52 -0
  26. package/dist/builtin/assets/domains/usage.md +81 -0
  27. package/dist/builtin/assets/domains/user-info.md +15 -0
  28. package/dist/builtin/assets/profiles/guide.profile.md +28 -0
  29. package/dist/commands.d.ts +69 -0
  30. package/dist/commands.js +344 -0
  31. package/dist/compile/agent-context.d.ts +48 -0
  32. package/dist/compile/agent-context.js +423 -0
  33. package/dist/compile/index.d.ts +1 -0
  34. package/dist/compile/index.js +5 -0
  35. package/dist/compile/resolve-use.d.ts +36 -0
  36. package/dist/compile/resolve-use.js +171 -0
  37. package/dist/compile/type-guards.d.ts +29 -0
  38. package/dist/compile/type-guards.js +107 -0
  39. package/dist/config.d.ts +31 -0
  40. package/dist/config.js +133 -0
  41. package/dist/constants.d.ts +52 -0
  42. package/dist/constants.js +81 -0
  43. package/dist/diagnostics.d.ts +14 -0
  44. package/dist/diagnostics.js +29 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.js +838 -0
  47. package/dist/injection-status.d.ts +45 -0
  48. package/dist/injection-status.js +134 -0
  49. package/dist/log.d.ts +64 -0
  50. package/dist/log.js +139 -0
  51. package/dist/manual-session.d.ts +24 -0
  52. package/dist/manual-session.js +152 -0
  53. package/dist/manual-track.d.ts +35 -0
  54. package/dist/manual-track.js +107 -0
  55. package/dist/parse/blueprint.d.ts +5 -0
  56. package/dist/parse/blueprint.js +59 -0
  57. package/dist/parse/domain-renderers.d.ts +8 -0
  58. package/dist/parse/domain-renderers.js +115 -0
  59. package/dist/parse/domain.d.ts +7 -0
  60. package/dist/parse/domain.js +69 -0
  61. package/dist/parse/index.d.ts +9 -0
  62. package/dist/parse/index.js +223 -0
  63. package/dist/parse/profile.d.ts +18 -0
  64. package/dist/parse/profile.js +117 -0
  65. package/dist/parse/ref-resolver.d.ts +63 -0
  66. package/dist/parse/ref-resolver.js +174 -0
  67. package/dist/parse/shared.d.ts +76 -0
  68. package/dist/parse/shared.js +273 -0
  69. package/dist/profile-persist.d.ts +22 -0
  70. package/dist/profile-persist.js +53 -0
  71. package/dist/render/cache.d.ts +15 -0
  72. package/dist/render/cache.js +135 -0
  73. package/dist/render/index.d.ts +3 -0
  74. package/dist/render/index.js +14 -0
  75. package/dist/render/session-inject.d.ts +5 -0
  76. package/dist/render/session-inject.js +24 -0
  77. package/dist/render/turn-inject.d.ts +36 -0
  78. package/dist/render/turn-inject.js +248 -0
  79. package/dist/schema.d.ts +448 -0
  80. package/dist/schema.js +45 -0
  81. package/dist/session.d.ts +82 -0
  82. package/dist/session.js +102 -0
  83. package/dist/slog.d.ts +3 -0
  84. package/dist/slog.js +27 -0
  85. package/dist/transpile.d.ts +34 -0
  86. package/dist/transpile.js +220 -0
  87. package/dist/verify/file-hash.d.ts +2 -0
  88. package/dist/verify/file-hash.js +30 -0
  89. package/dist/verify/fs-content-match.d.ts +2 -0
  90. package/dist/verify/fs-content-match.js +26 -0
  91. package/dist/verify/fs-exists.d.ts +2 -0
  92. package/dist/verify/fs-exists.js +13 -0
  93. package/dist/verify/fs-not-exists.d.ts +2 -0
  94. package/dist/verify/fs-not-exists.js +13 -0
  95. package/dist/verify/git-status-clean.d.ts +2 -0
  96. package/dist/verify/git-status-clean.js +19 -0
  97. package/dist/verify/index.d.ts +7 -0
  98. package/dist/verify/index.js +46 -0
  99. package/dist/verify/lint-check.d.ts +2 -0
  100. package/dist/verify/lint-check.js +17 -0
  101. package/dist/verify/read-stderr.d.ts +5 -0
  102. package/dist/verify/read-stderr.js +20 -0
  103. package/dist/verify/ref-check.d.ts +13 -0
  104. package/dist/verify/ref-check.js +88 -0
  105. package/dist/verify/test-pass.d.ts +2 -0
  106. package/dist/verify/test-pass.js +17 -0
  107. package/dist/verify/ts-compiles.d.ts +2 -0
  108. package/dist/verify/ts-compiles.js +17 -0
  109. package/package.json +62 -0
  110. package/src/agent/api-bridge.ts +105 -0
  111. package/src/agent/index.ts +4 -0
  112. package/src/agent/pi-adapter.ts +326 -0
  113. package/src/agent/registry.ts +44 -0
  114. package/src/asset-health.ts +327 -0
  115. package/src/asset-pack/loader.ts +141 -0
  116. package/src/asset-pack/manifest.ts +118 -0
  117. package/src/asset-pack/md-file-pack.ts +202 -0
  118. package/src/asset-pack/validate.ts +186 -0
  119. package/src/builtin/assets/blueprints/dev-knowledge.blueprint.yaml +13 -0
  120. package/src/builtin/assets/domains/agent-info.md +15 -0
  121. package/src/builtin/assets/domains/authoring.md +201 -0
  122. package/src/builtin/assets/domains/pack-repair.md +47 -0
  123. package/src/builtin/assets/domains/project-analysis.md +52 -0
  124. package/src/builtin/assets/domains/usage.md +81 -0
  125. package/src/builtin/assets/domains/user-info.md +15 -0
  126. package/src/builtin/assets/profiles/guide.profile.md +28 -0
  127. package/src/commands.ts +405 -0
  128. package/src/compile/agent-context.ts +487 -0
  129. package/src/compile/index.ts +5 -0
  130. package/src/compile/resolve-use.ts +208 -0
  131. package/src/compile/type-guards.ts +132 -0
  132. package/src/config.ts +154 -0
  133. package/src/constants.ts +104 -0
  134. package/src/diagnostics.ts +36 -0
  135. package/src/index.ts +1028 -0
  136. package/src/injection-status.ts +155 -0
  137. package/src/log.ts +173 -0
  138. package/src/manual-session.ts +190 -0
  139. package/src/manual-track.ts +127 -0
  140. package/src/parse/blueprint.ts +82 -0
  141. package/src/parse/domain-renderers.ts +120 -0
  142. package/src/parse/domain.ts +78 -0
  143. package/src/parse/index.ts +266 -0
  144. package/src/parse/profile.ts +139 -0
  145. package/src/parse/ref-resolver.ts +198 -0
  146. package/src/parse/shared.ts +331 -0
  147. package/src/profile-persist.ts +60 -0
  148. package/src/render/cache.ts +150 -0
  149. package/src/render/index.ts +14 -0
  150. package/src/render/session-inject.ts +26 -0
  151. package/src/render/turn-inject.ts +278 -0
  152. package/src/schema.ts +542 -0
  153. package/src/session.ts +190 -0
  154. package/src/slog.ts +32 -0
  155. package/src/transpile.ts +302 -0
  156. package/src/verify/file-hash.ts +29 -0
  157. package/src/verify/fs-content-match.ts +28 -0
  158. package/src/verify/fs-exists.ts +14 -0
  159. package/src/verify/fs-not-exists.ts +17 -0
  160. package/src/verify/git-status-clean.ts +23 -0
  161. package/src/verify/index.ts +57 -0
  162. package/src/verify/lint-check.ts +21 -0
  163. package/src/verify/read-stderr.ts +24 -0
  164. package/src/verify/ref-check.ts +118 -0
  165. package/src/verify/test-pass.ts +18 -0
  166. package/src/verify/ts-compiles.ts +21 -0
@@ -0,0 +1,88 @@
1
+ // src/verify/ref-check.ts — Profile→Blueprint→Domain 引用完整性校验
2
+ //
3
+ // pt 引用图是三层星型(非 OXN Domain→Domain 网状),无环风险。
4
+ // 校验重点是完整性:悬空引用检测。
5
+ // 不校验 Blueprint.groups[].modules(H2 段名是模块类型声明,非 Domain 引用)。
6
+ //
7
+ // v11.x:Profile 全局 domains 覆盖是主用例——"未实例化"警告在该模式下静默
8
+ // (未实例化 = 走全局分发,无 warning;只有 Profile 完全空 + 聚合组全空时才报 warning)。
9
+ /** 校验单个 Profile 的引用完整性。
10
+ * 传入该 Profile 引用的 Blueprint(若存在)+ 全集 Domains。 */
11
+ export function checkProfileRefs(profile, blueprints, domains) {
12
+ const errors = [];
13
+ const warnings = [];
14
+ const domainNames = new Set(domains.map((d) => d.name));
15
+ const _blueprintNames = new Set(blueprints.map((b) => b.name));
16
+ // 1. Profile → Blueprint 存在性
17
+ const bp = blueprints.find((b) => b.name === profile.blueprint);
18
+ if (!bp) {
19
+ errors.push(`Profile "${profile.name}" 引用的 Blueprint "${profile.blueprint}" 不存在`);
20
+ }
21
+ // 2. Profile → Domain(全局 domains)
22
+ for (const dn of profile.domains) {
23
+ if (!domainNames.has(dn)) {
24
+ errors.push(`Profile "${profile.name}" 的 domains 引用悬空 Domain "${dn}"`);
25
+ }
26
+ }
27
+ // 3. Profile.groups → Domain + 聚合组名匹配
28
+ for (const group of profile.groups) {
29
+ // 3a. 聚合组名应在 Blueprint 里有对应
30
+ if (bp) {
31
+ const bpGroup = bp.groups.find((bg) => bg.name === group.name);
32
+ if (!bpGroup) {
33
+ errors.push(`Profile "${profile.name}" 的聚合组 "${group.name}" 在 Blueprint "${bp.name}" 里无对应`);
34
+ }
35
+ }
36
+ // 3b. 聚合组引用的 Domain 存在
37
+ for (const dn of group.domains) {
38
+ if (!domainNames.has(dn)) {
39
+ errors.push(`Profile "${profile.name}" 聚合组 "${group.name}" 引用悬空 Domain "${dn}"`);
40
+ }
41
+ }
42
+ }
43
+ // 4. 警告:Blueprint 声明了聚合组但 Profile 未实例化(非错误——Profile 可只实例化部分聚合组)
44
+ // v11.x:Profile 有全局 domains 时,聚合组未 H2 实例化是合法用法(全局分发到所有聚合组),
45
+ // 静默不报。只有 Profile 全空(无全局 domains + 无 H2 实例化)时才报"未实例化" warning。
46
+ if (bp && profile.domains.length === 0) {
47
+ for (const bpGroup of bp.groups) {
48
+ const hasProfileGroup = profile.groups.some((pg) => pg.name === bpGroup.name);
49
+ if (!hasProfileGroup) {
50
+ warnings.push(`Blueprint "${bp.name}" 的聚合组 "${bpGroup.name}" 在 Profile "${profile.name}" 里未实例化`);
51
+ }
52
+ }
53
+ }
54
+ return { ok: errors.length === 0, errors, warnings };
55
+ }
56
+ /** 校验所有 Profile 的引用完整性(批量入口)。 */
57
+ export function checkAllRefs(profiles, blueprints, domains) {
58
+ const allErrors = [];
59
+ const allWarnings = [];
60
+ for (const profile of profiles) {
61
+ const r = checkProfileRefs(profile, blueprints, domains);
62
+ allErrors.push(...r.errors);
63
+ allWarnings.push(...r.warnings);
64
+ }
65
+ return { ok: allErrors.length === 0, errors: allErrors, warnings: allWarnings };
66
+ }
67
+ /** 把 RefCheckResult 格式化为人类可读文本(pt_check_refs tool 输出用)。 */
68
+ export function formatRefCheckResult(r) {
69
+ const lines = [];
70
+ if (r.ok && r.warnings.length === 0) {
71
+ lines.push("✓ 引用完整性检查通过,无悬空引用");
72
+ return lines.join("\n");
73
+ }
74
+ if (r.errors.length > 0) {
75
+ lines.push(`✗ ${r.errors.length} 个悬空引用:`);
76
+ for (const e of r.errors)
77
+ lines.push(` - ${e}`);
78
+ }
79
+ if (r.warnings.length > 0) {
80
+ lines.push(`⚠ ${r.warnings.length} 个警告:`);
81
+ for (const w of r.warnings)
82
+ lines.push(` - ${w}`);
83
+ }
84
+ if (r.ok && r.warnings.length > 0) {
85
+ lines.unshift("✓ 引用完整性检查通过(有警告)");
86
+ }
87
+ return lines.join("\n");
88
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProbeOutcome } from "../schema.js";
2
+ export declare function testPass(cwd: string, params: Record<string, string>): Promise<ProbeOutcome>;
@@ -0,0 +1,17 @@
1
+ // src/verify/test-pass.ts — 验证测试通过
2
+ import { execSync } from "node:child_process";
3
+ import { readStderr } from "./read-stderr.js";
4
+ export async function testPass(cwd, params) {
5
+ const cmd = params.cmd ?? "npx vitest run tests/verify/";
6
+ try {
7
+ execSync(cmd, { cwd, stdio: "pipe", timeout: 120000 });
8
+ return { outcome: "COMPLETED", message: `测试通过: ${cmd}` };
9
+ }
10
+ catch (e) {
11
+ return {
12
+ outcome: "DEVIATED",
13
+ message: `测试失败: ${cmd}`,
14
+ actual: readStderr(e).slice(0, 500),
15
+ };
16
+ }
17
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProbeOutcome } from "../schema.js";
2
+ export declare function tsCompiles(cwd: string, params: Record<string, string>): Promise<ProbeOutcome>;
@@ -0,0 +1,17 @@
1
+ // src/verify/ts-compiles.ts — 验证 TS 编译通过
2
+ import { execSync } from "node:child_process";
3
+ import { readStderr } from "./read-stderr.js";
4
+ export async function tsCompiles(cwd, params) {
5
+ const cmd = params.cmd ?? "npx tsc --noEmit";
6
+ try {
7
+ execSync(cmd, { cwd, stdio: "pipe", timeout: 60000 });
8
+ return { outcome: "COMPLETED", message: `TS 编译通过: ${cmd}` };
9
+ }
10
+ catch (e) {
11
+ return {
12
+ outcome: "DEVIATED",
13
+ message: `TS 编译失败: ${cmd}`,
14
+ actual: readStderr(e).slice(0, 500),
15
+ };
16
+ }
17
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@istuen/pt",
3
+ "version": "0.1.0",
4
+ "description": "Pt \u2014 Turn domain knowledge into configurable Agent Context.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package"
8
+ ],
9
+ "files": [
10
+ "src",
11
+ "dist"
12
+ ],
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "scripts": {
16
+ "typecheck": "tsc --noEmit",
17
+ "lint": "biome check",
18
+ "lint:fix": "biome check --write",
19
+ "format": "biome format --write",
20
+ "verify": "biome check && vitest run tests/verify/",
21
+ "verify:phase9": "vitest run tests/verify/phase9.test.ts",
22
+ "verify:flows": "vitest run tests/verify/flows.test.ts",
23
+ "prebuild": "tsc --noEmit && biome check",
24
+ "build": "tsup",
25
+ "prepublishOnly": "npm run build && npm run verify"
26
+ },
27
+ "pi": {
28
+ "extensions": [
29
+ "./src/index.ts"
30
+ ]
31
+ },
32
+ "peerDependencies": {
33
+ "@earendil-works/pi-ai": "*",
34
+ "@earendil-works/pi-coding-agent": "*",
35
+ "typebox": "*"
36
+ },
37
+ "devDependencies": {
38
+ "@biomejs/biome": "^2.5.11",
39
+ "@types/node": "^26.4.0",
40
+ "tsup": "^8.5.1",
41
+ "tsx": "^4.23.13",
42
+ "typescript": "^7.0.2",
43
+ "vitest": "^3.2.7"
44
+ },
45
+ "dependencies": {
46
+ "yaml": "^2.9.0"
47
+ },
48
+ "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
49
+ "license": "MIT",
50
+ "author": "istuen <issac.tuen@gmail.com>",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/istuen/pt.git"
54
+ },
55
+ "homepage": "https://github.com/istuen/pt#readme",
56
+ "bugs": {
57
+ "url": "https://github.com/istuen/pt/issues"
58
+ },
59
+ "engines": {
60
+ "node": ">=18"
61
+ }
62
+ }
@@ -0,0 +1,105 @@
1
+ // src/agent/api-bridge.ts — Pi ExtensionAPI ↔ AgentAPI 桥接(P1.3 抽出)
2
+ //
3
+ // 把 Pi ExtensionAPI 转换成 AgentAPI(结构类型子集)供 AgentAdapter 使用。
4
+ // 同一个 Pi runtime 复用同一个 AgentAPI wrapper——否则 PiAdapter 每次 registerInject
5
+ // 都会得到不同的对象身份,导致 system prompt handler 被重复注册。
6
+ //
7
+ // v12.x(issue pt-session-singleton-pi-web-pollution 修复):
8
+ // - 原设计 `api.log` getter / `onInjected` 直接读写 module-level `session` 单例
9
+ // → tab B 的 wrapper.onInjected 写 tab B 的 lastBuiltPrompt 但走的是 module 单例
10
+ // → tab A 跑 turn 时 `lastBuiltPrompt` 也是 tab B 的(被覆盖)
11
+ // - 改造为 per-pi wrapper 内部维护 `currentSessionId` 闭包变量:
12
+ // - wrapper 的 `api.on` 包装函数在每个 handler 触发前自动从 `args[1]?.sessionManager`
13
+ // 拿 sessionId 写入 `currentSessionId`
14
+ // - `api.log` getter / `onInjected` 通过 `currentSessionId` 走 `getSessionById(...)`
15
+ // 取 per-session state
16
+ // - TUI 模式下只有一个 pi 实例,行为等同 module-level 单例(同一 sessionId 始终)
17
+
18
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
+ import type { AgentAPI, AgentUI } from "../schema.js";
20
+ import { getSessionById } from "../session.js";
21
+
22
+ /** AgentAPI 的 ui 字段类型(NonNullable<AgentAPI["ui"]>)。 */
23
+ type AgentUIContext = AgentUI;
24
+
25
+ /** 将 Pi ExtensionAPI 转换为 AgentAPI(结构类型子集,运行时透明)。
26
+ * Pi ExtensionAPI 是 AgentAPI 的超集,多余方法(registerCommand/registerFlag 等)不暴露给 Adapter。
27
+ * v9.1:提供 ui 能力,adapter 可走 ui.notify 报错 / ui.setStatus 设状态(不需 console)。
28
+ * v10.x:提供 log 能力,adapter 的 try/catch 异常走 session.logger(不再 swallow)。
29
+ * ctx 用 Pi 扩展的 ctx(ExtensionContext/ExtensionCommandContext 都含 ui)——子集够用。
30
+ *
31
+ * 同一个 Pi runtime 复用同一个 AgentAPI wrapper,否则 PiAdapter 每次 registerInject
32
+ * 都会得到不同的对象身份,导致系统 prompt handler 被重复注册。 */
33
+ const agentApiCache = new WeakMap<
34
+ ExtensionAPI,
35
+ {
36
+ api: AgentAPI;
37
+ setContext: (ctx: { ui: AgentUIContext }) => void;
38
+ /** v12.x:当前触发中的 sessionId(wrapper.on 包装在 handler 触发前写入)。
39
+ * api.log / api.onInjected 通过此字段拿 per-session state。 */
40
+ currentSessionId: string | undefined;
41
+ }
42
+ >();
43
+
44
+ export function toAgentAPI(pi: ExtensionAPI, ctx: { ui: AgentUIContext }): AgentAPI {
45
+ let holder = agentApiCache.get(pi);
46
+ if (!holder) {
47
+ let currentCtx = ctx;
48
+ // v12.x:closure 捕获 currentSessionId,供 api.log/onInjected 拿 per-session state
49
+ const stateRef: { currentSessionId: string | undefined } = { currentSessionId: undefined };
50
+
51
+ // v12.x:on 包装——handler 触发前自动从 args[1] ctx 拿 sessionId 注入 stateRef
52
+ const onWrapped = (event: string, handler: (...args: unknown[]) => unknown): void => {
53
+ pi.on(
54
+ event as Parameters<ExtensionAPI["on"]>[0],
55
+ ((event: unknown, extCtx: unknown) => {
56
+ // 提取 sessionId:extCtx 是 ExtensionContext
57
+ const c = extCtx as { sessionManager?: { getSessionId?: () => string } } | undefined;
58
+ stateRef.currentSessionId = c?.sessionManager?.getSessionId?.();
59
+ return handler(event, extCtx as unknown);
60
+ }) as Parameters<ExtensionAPI["on"]>[1]
61
+ );
62
+ };
63
+
64
+ const api: AgentAPI = {
65
+ on: onWrapped,
66
+ registerCommand: (name, spec) =>
67
+ pi.registerCommand(name, spec as Parameters<ExtensionAPI["registerCommand"]>[1]),
68
+ registerFlag: (name, spec) =>
69
+ pi.registerFlag(name, spec as Parameters<ExtensionAPI["registerFlag"]>[1]),
70
+ getFlag: (name) => pi.getFlag(name),
71
+ ui: {
72
+ notify: (msg, level) => currentCtx.ui.notify(msg, level),
73
+ setStatus: (name, text) => currentCtx.ui.setStatus(name, text),
74
+ },
75
+ // v12.x:api.log 走 per-session logger(避免 tab A 写到 tab B 的日志文件)
76
+ get log() {
77
+ const sid = stateRef.currentSessionId;
78
+ return sid ? getSessionById(sid).logger?.toWriter() : undefined;
79
+ },
80
+ // v12.x:onInjected 写 per-session lastBuiltPrompt
81
+ onInjected: (systemPrompt) => {
82
+ const sid = stateRef.currentSessionId;
83
+ if (sid) getSessionById(sid).lastBuiltPrompt = systemPrompt;
84
+ },
85
+ };
86
+ holder = {
87
+ api,
88
+ setContext: (nextCtx) => {
89
+ currentCtx = nextCtx;
90
+ },
91
+ currentSessionId: undefined,
92
+ };
93
+ // 把 stateRef 同步到 holder(保持单一来源)
94
+ Object.defineProperty(holder, "currentSessionId", {
95
+ get: () => stateRef.currentSessionId,
96
+ set: (v: string | undefined) => {
97
+ stateRef.currentSessionId = v;
98
+ },
99
+ });
100
+ agentApiCache.set(pi, holder);
101
+ } else {
102
+ holder.setContext(ctx);
103
+ }
104
+ return holder.api;
105
+ }
@@ -0,0 +1,4 @@
1
+ // src/agent/index.ts — Agent 适配器入口
2
+
3
+ export { PiAdapter } from "./pi-adapter.js";
4
+ export { getAgentAdapter } from "./registry.js";
@@ -0,0 +1,326 @@
1
+ // src/agent/pi-adapter.ts — PiAdapter:封装 Pi Agent 注入机制
2
+ //
3
+ // Phase 9.6:v9 新增 — AgentAdapter 的 Pi 实现。
4
+ // - system_prompt 注入:api.on("before_agent_start") 每轮追加 segment
5
+ // - context_message 触发:api.on("input") 拦截 /manual:xxx 和 /<flow-name>
6
+ // - 这些是 Agent Runtime 层(Pi API),本 Adapter 在该层做 session→system_prompt、turn→context_message 映射
7
+ //
8
+ // Pt 核心只调 AgentAdapter 接口,不直接调 Pi API。加新 Agent 只加 Adapter。
9
+ //
10
+ // Tech Debt T6: 全用 type guard 收窄,不用 as 断言(pt-quality #1)
11
+ // Tech Debt T2: 用 constants 模块名常量(pt-quality #5)
12
+ //
13
+ // v12.x(issue pt-session-singleton-pi-web-pollution 修复):
14
+ // - handler 内部读 `args[1]?.sessionManager?.getSessionId()` 拿 per-session sessionId
15
+ // - 通过 `getSessionById(sessionId)` 拿 per-session SessionState,**不再读 module-level 单例**
16
+ // - 配合 `src/agent/registry.ts` 的 per-pi WeakMap 改造,
17
+ // `this.ctx / this.blueprint / this.domains / this.segment / this.injectedApi` 五个实例字段
18
+ // 现在是 per-pi 隔离——其他 session 的 setContext 不会覆盖本 session 的 segment
19
+
20
+ import { AGENT_PI, MOD_FLOWS, MOD_RULES } from "../constants.js";
21
+ import { isFlowTemplateArray, isRuleArray } from "../compile/type-guards.js";
22
+ import { renderInjectionFooter } from "../injection-status.js";
23
+ import { getSessionById } from "../session.js";
24
+ import {
25
+ filterDomainsByProfile,
26
+ type AgentAdapter,
27
+ type AgentAPI,
28
+ type AgentContext,
29
+ type Blueprint,
30
+ type Domain,
31
+ type Profile,
32
+ } from "../schema.js";
33
+ import { renderTurnInject } from "../render/turn-inject.js";
34
+ import { renderSessionInject } from "../render/session-inject.js";
35
+
36
+ /** v12.x:从 handler 的 args[1] ctx 提取 sessionId。
37
+ * pi 的 `pi.on(event, handler)` 触发时传 `(event, ctx)` 两个参数;通过 AgentAPI 包装后
38
+ * handler 拿到 `(...args)`,args[0] = event, args[1] = ctx (ExtensionContext)。 */
39
+ function sessionIdFromArgs(args: unknown[]): string | undefined {
40
+ const ctx = args[1] as { sessionManager?: { getSessionId?: () => string } } | undefined;
41
+ return ctx?.sessionManager?.getSessionId?.();
42
+ }
43
+
44
+ /** PiAdapter:封装 Pi Agent 的注入机制。 */
45
+ export class PiAdapter implements AgentAdapter {
46
+ name = AGENT_PI;
47
+ /** Phase term-P4.3:保留 Pi API 名 system_prompt/context_message——
48
+ * 这是 AgentAdapter 映射边界声明(Pi 支持哪些 Agent Runtime 层注入位置)。
49
+ * Blueprint 用 session/turn 语义值(聚合组 inject 字段),Adapter 内部映射到此字段声明的 Pi API 名。 */
50
+ supportedTargets = ["system_prompt", "context_message"];
51
+
52
+ private ctx: AgentContext | null = null;
53
+ private blueprint: Blueprint | null = null;
54
+ private domains: Domain[] = [];
55
+ private segment: string | null = null;
56
+ /** 与当前 Pi runtime 的 AgentAPI 绑定;同一 runtime 不重复注册 handler。
57
+ * v12.x:per-pi 实例字段——`registry.ts` 给每个 pi 一个新 PiAdapter,所以 `injectedApi`
58
+ * 不会被其他 session 覆盖。 */
59
+ private injectedApi: AgentAPI | null = null;
60
+ /** v13.x(issue pt-turn-inject-not-profile-scoped):当前 Profile——turn 路径 scope 过滤用。 */
61
+ private profile: Profile | null = null;
62
+
63
+ /** 设置编译产物(transpile 后调)。v12.x:per-pi 实例字段——本 session 的 segment
64
+ * 不会被其他 session 覆盖。
65
+ * v13.x(issue pt-turn-inject-not-profile-scoped):加 profile 参数——turn 路径 scope 过滤用。 */
66
+ setAgentContext(
67
+ ctx: AgentContext,
68
+ blueprint: Blueprint,
69
+ domains: Domain[],
70
+ profile: Profile
71
+ ): void {
72
+ this.ctx = ctx;
73
+ this.blueprint = blueprint;
74
+ this.domains = domains;
75
+ this.profile = profile;
76
+ this.segment = renderSessionInject(ctx, blueprint);
77
+ }
78
+
79
+ /** 清除当前 session 的 context;保留当前 runtime 的 handler 绑定。
80
+ * v12.x:per-pi 实例字段——只清本 session 的状态。 */
81
+ resetInjection(): void {
82
+ this.ctx = null;
83
+ this.blueprint = null;
84
+ this.domains = [];
85
+ this.profile = null;
86
+ this.segment = null;
87
+ }
88
+
89
+ /** 启动时注册:把 AgentContext 注入到 Agent。 */
90
+ registerInject(
91
+ api: AgentAPI,
92
+ ctx: AgentContext,
93
+ blueprint: Blueprint,
94
+ domains: Domain[] = this.domains,
95
+ profile: Profile | null = this.profile
96
+ ): void {
97
+ // 先更新状态;同一 runtime 的后续 Profile 切换不能重新注册 handler,
98
+ // 但 handler 会在事件发生时读取最新的 this.segment / this.ctx。
99
+ this.ctx = ctx;
100
+ this.blueprint = blueprint;
101
+ this.domains = domains;
102
+ this.profile = profile;
103
+ this.segment = renderSessionInject(ctx, blueprint);
104
+
105
+ if (this.injectedApi === api) {
106
+ api.log?.debug("agent:registerInject skipped (already injected)");
107
+ return;
108
+ }
109
+ this.injectedApi = api;
110
+
111
+ // session 注入(inject=session → Pi system_prompt 事件):每轮追加 segment
112
+ // v10.x:包 try/catch,运行时异常走 api.log.error + ui.notify,不再 swallow
113
+ // v11.x(issue pt-injection-status-manual-track):三分支写 session.injectionState +
114
+ // 调 api.ui?.setStatus,让 footer 三态文字真实反映注入结果(自报,不检测 Pi)
115
+ // v12.x:从 args[1] ctx 拿 per-session sessionId,写 per-session SessionState
116
+ api.on("before_agent_start", async (...args: unknown[]) => {
117
+ const t0 = Date.now();
118
+ try {
119
+ const sessionId = sessionIdFromArgs(args);
120
+ const sessionState = sessionId ? getSessionById(sessionId) : null;
121
+
122
+ const currentSegment = this.segment;
123
+ // v14.x(issue pt-asset-migration-visibility Layer 2):footer 末尾追加 ⚠ N issues。
124
+ // 从 sessionState.assetHealthIssues 读计数——session_start 已批量体检过。
125
+ const healthCount = sessionState?.assetHealthIssues?.length ?? 0;
126
+ // v14.x(tagline):footer 拼 `: <tagline>`。从 sessionState.cachedProfile 读——transpile 时已存。
127
+ const tagline = sessionState?.cachedProfile?.tagline ?? null;
128
+ if (!currentSegment) {
129
+ // 无 segment(未加载 Profile / 已被 reset)→ idle
130
+ if (sessionState) {
131
+ sessionState.injectionState = "idle";
132
+ sessionState.injectionError = null;
133
+ api.ui?.setStatus(
134
+ "pt",
135
+ renderInjectionFooter(
136
+ "idle",
137
+ sessionState.activeProfile,
138
+ null,
139
+ healthCount,
140
+ "auto",
141
+ tagline
142
+ )
143
+ );
144
+ }
145
+ return undefined;
146
+ }
147
+ const event = args[0];
148
+ if (!isSystemPromptEvent(event)) {
149
+ // 事件形状异常:归类为 idle(不视为失败——Pi 可能改了事件签名)
150
+ if (sessionState) {
151
+ sessionState.injectionState = "idle";
152
+ sessionState.injectionError = null;
153
+ api.ui?.setStatus(
154
+ "pt",
155
+ renderInjectionFooter(
156
+ "idle",
157
+ sessionState.activeProfile,
158
+ null,
159
+ healthCount,
160
+ "auto",
161
+ tagline
162
+ )
163
+ );
164
+ }
165
+ return undefined;
166
+ }
167
+ const final = `${event.systemPrompt}\n\n## 当前任务上下文\n\n${currentSegment}`;
168
+ api.log?.debug("agent:before_agent_start ok", {
169
+ originalLen: event.systemPrompt.length,
170
+ injectedLen: final.length,
171
+ deltaLen: final.length - event.systemPrompt.length,
172
+ durationMs: Date.now() - t0,
173
+ });
174
+ if (sessionState) {
175
+ api.onInjected?.(final);
176
+ // 成功注入 → injected
177
+ sessionState.injectionState = "injected";
178
+ sessionState.injectionError = null;
179
+ api.ui?.setStatus(
180
+ "pt",
181
+ renderInjectionFooter(
182
+ "injected",
183
+ sessionState.activeProfile,
184
+ null,
185
+ healthCount,
186
+ "auto",
187
+ tagline
188
+ )
189
+ );
190
+ } else {
191
+ api.onInjected?.(final);
192
+ }
193
+ return { systemPrompt: final };
194
+ } catch (e) {
195
+ const msg = e instanceof Error ? e.message : String(e);
196
+ api.log?.error("agent:before_agent_start failed", {
197
+ err: msg,
198
+ durationMs: Date.now() - t0,
199
+ });
200
+ api.ui?.notify(`[pt] before_agent_start failed: ${msg}`, "error");
201
+ // 异常 → failed + 错误消息(footer 追加)
202
+ const sessionId = sessionIdFromArgs(args);
203
+ const sessionState = sessionId ? getSessionById(sessionId) : null;
204
+ // v14.x:catch 路径同样透传 healthCount + tagline(避免修复丢告警 / tagline)
205
+ const healthCount = sessionState?.assetHealthIssues?.length ?? 0;
206
+ const tagline = sessionState?.cachedProfile?.tagline ?? null;
207
+ if (sessionState) {
208
+ sessionState.injectionState = "failed";
209
+ sessionState.injectionError = msg;
210
+ api.ui?.setStatus(
211
+ "pt",
212
+ renderInjectionFooter(
213
+ "failed",
214
+ sessionState.activeProfile,
215
+ msg,
216
+ healthCount,
217
+ "auto",
218
+ tagline
219
+ )
220
+ );
221
+ }
222
+ return undefined; // 失败降级, 不影响主流程
223
+ }
224
+ });
225
+
226
+ // context_message 触发:/manual:xxx + /<flow-name>
227
+ // v10.x:包 try/catch,renderTurnInject 抛错不再 swallow
228
+ api.on("input", async (...args: unknown[]) => {
229
+ const t0 = Date.now();
230
+ try {
231
+ if (!this.ctx || !this.blueprint) return { action: "continue" };
232
+ const event = args[0];
233
+ if (!isInputEvent(event)) return { action: "continue" };
234
+ // Phase term-P4.3:renderContextMessage → renderTurnInject
235
+ // v13.x(issue pt-turn-inject-not-profile-scoped):按 Profile scope 过滤 domains + 传 profile
236
+ // 让 /manual:<domain> 在未引用该 domain 的 Profile 下返 null(与 /pt flows 列表一致)
237
+ const scoped = filterDomainsByProfile(this.domains, this.profile);
238
+ const result = renderTurnInject(this.ctx, this.blueprint, scoped, this.profile, event.text);
239
+ const durationMs = Date.now() - t0;
240
+ if (result === null) {
241
+ api.log?.debug("agent:input passthrough", {
242
+ inputPreview: event.text.slice(0, 80),
243
+ durationMs,
244
+ });
245
+ return { action: "continue" };
246
+ }
247
+ api.log?.info("agent:input transformed", {
248
+ inputPreview: event.text.slice(0, 80),
249
+ outputLen: result.length,
250
+ durationMs,
251
+ });
252
+ return { action: "transform", text: result };
253
+ } catch (e) {
254
+ const msg = e instanceof Error ? e.message : String(e);
255
+ api.log?.error("agent:input render failed", {
256
+ err: msg,
257
+ input: args[0],
258
+ durationMs: Date.now() - t0,
259
+ });
260
+ api.ui?.notify(`[pt] input render failed: ${msg}`, "error");
261
+ return { action: "continue" }; // 失败降级: 不拦截 input, 让原文本过 LLM
262
+ }
263
+ });
264
+ }
265
+
266
+ /** 查询可用手册(/pt flows 用)。
267
+ * v9:遍历 Blueprint 的 inject=turn 聚合组 → 引用 Domain → 找 FlowTemplate + Rules 段的 Rule。 */
268
+ listManuals(
269
+ _ctx: AgentContext,
270
+ blueprint: Blueprint,
271
+ domains: Domain[]
272
+ ): Array<{ name: string; hint?: string; domain: string }> {
273
+ const flows: Array<{ name: string; hint?: string; domain: string }> = [];
274
+
275
+ for (const group of blueprint.groups) {
276
+ // Phase term-P4.3:inject 语义值 context_message → turn
277
+ // Phase term-naming:字段名 target → inject
278
+ if (group.inject !== "turn") continue;
279
+ // v13.x(issue pt-turn-inject-not-profile-scoped):按 Profile scope 过滤 domains——
280
+ // adapter 内部统一过滤,调用方(commands.ts flowsText)无需预过滤。
281
+ const scoped = filterDomainsByProfile(domains, this.profile);
282
+ for (const d of scoped) {
283
+ // Phase term-P9.2:FlowTemplate 在 ## Flows 段;Rule[] 在 ## Rules 段;Checklist[] 在 ## Checklists 段。
284
+ const flowsContent = d.modules[MOD_FLOWS];
285
+ if (isFlowTemplateArray(flowsContent)) {
286
+ for (const t of flowsContent) {
287
+ flows.push({ name: t.name, hint: t.argumentHint, domain: d.name });
288
+ }
289
+ }
290
+ const rulesContent = d.modules[MOD_RULES];
291
+ if (isRuleArray(rulesContent) && rulesContent.length > 0) {
292
+ // term-Domain 的 Rule[] 作为 /manual:<domain> 暴露
293
+ flows.push({
294
+ name: `/manual:${d.name}`,
295
+ hint: `${rulesContent.length} 条规范`,
296
+ domain: d.name,
297
+ });
298
+ }
299
+ // Checklist[] 暂不单独暴露——/manual:<domain> 命令会统一处理(renderDomainManual)
300
+ }
301
+ }
302
+
303
+ // 去重(按 name)
304
+ const seen = new Set<string>();
305
+ return flows.filter((f) => {
306
+ const key = `${f.domain}/${f.name}`;
307
+ if (seen.has(key)) return false;
308
+ seen.add(key);
309
+ return true;
310
+ });
311
+ }
312
+ }
313
+
314
+ // ==================== Pi ExtensionAPI 事件 type guards ====================
315
+
316
+ function isSystemPromptEvent(x: unknown): x is { systemPrompt: string } {
317
+ return (
318
+ !!x &&
319
+ typeof x === "object" &&
320
+ typeof (x as { systemPrompt?: unknown }).systemPrompt === "string"
321
+ );
322
+ }
323
+
324
+ function isInputEvent(x: unknown): x is { text: string } {
325
+ return !!x && typeof x === "object" && typeof (x as { text?: unknown }).text === "string";
326
+ }
@@ -0,0 +1,44 @@
1
+ // src/agent/registry.ts — AgentAdapter 注册表
2
+ //
3
+ // Phase 9.6:v9 新增 — 按 Agent 名取 Adapter 实例。
4
+ // 加新 Agent(如 Codex / OpenCode)只在这里加一行注册 + 实现 Adapter。
5
+ //
6
+ // v12.x(issue pt-session-singleton-pi-web-pollution 修复):
7
+ // - 原设计 `agentAdapters: Record<string, AgentAdapter>` 是 module-level 单例
8
+ // → 所有 session 共享同一个 PiAdapter 实例 → 单例 `this.segment / this.ctx /
9
+ // this.blueprint / this.domains / this.injectedApi` 跨 session 被覆盖
10
+ // → tab B 调 setContext 覆盖 this.segment → tab A 的 before_agent_start handler
11
+ // 读到 tab B 的 segment → LLM 拿到错业务上下文(业务上下文级污染)
12
+ // - 改造为 `WeakMap<ExtensionAPI, Record<string, AgentAdapter>>`:
13
+ // - 每 session 一份 `Record<name, Adapter>` 缓存
14
+ // - pi 实例销毁时(pi-web session 退出)WeakMap 自动 GC
15
+ // - TUI 模式只有一个 pi 实例,行为等同 module-level 单例
16
+ // - getAgentAdapter 签名改成 `(pi, name)`:调用方传 pi 作 key
17
+ // - `src/index.ts:105` `transpileActive` 改 `getAgentAdapter(pi, ...)`
18
+ // - `src/index.ts:70-77` `registerInjectionIfReady` 调 `adapter.registerInject(...)`
19
+ // 时拿到的 adapter 已是 per-pi 实例,handler 闭包持 this(per-pi),this.segment
20
+ // 不被其他 session 覆盖
21
+
22
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
23
+ import type { AgentAdapter } from "../schema.js";
24
+ import { PiAdapter } from "./pi-adapter.js";
25
+
26
+ /** v12.x:per-pi Adapter 缓存(弱引用,pi 实例销毁后自动 GC)。 */
27
+ const adapterCache = new WeakMap<ExtensionAPI, Record<string, AgentAdapter>>();
28
+
29
+ /** 取指定 pi + name 的 AgentAdapter。未找到返 pi 的 per-pi 实例(fallback)。 */
30
+ export function getAgentAdapter(pi: ExtensionAPI, name: string): AgentAdapter {
31
+ let cache = adapterCache.get(pi);
32
+ if (!cache) {
33
+ cache = {};
34
+ adapterCache.set(pi, cache);
35
+ }
36
+ let adapter = cache[name];
37
+ if (!adapter) {
38
+ // 当前只支持 pi;未来 codex/opencode 在这里 switch name
39
+ adapter = new PiAdapter();
40
+ cache[name] = adapter;
41
+ if (!cache.pi) cache.pi = adapter; // 把 fallback 也指到同一实例
42
+ }
43
+ return adapter;
44
+ }