@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 istuen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # Pt
2
+
3
+ Pt 把领域知识做成可配置的智能体上下文。通过切换配置,Agent 具备不同 Session Context(每轮注入),推理时可通过 Turn Context(按需触发)查阅手册。
4
+
5
+ ---
6
+
7
+ ## 用 Pt 做什么
8
+
9
+ - **复用 Prompt,不必每次重写**:把重复的 prompt 写成 Pt Domain,配一份 Profile,每次会话切换立即获取领域知识——换项目、换场景,不用重写。
10
+ - **拆分固定与按需,控制上下文成本**:领域知识拆成 Session Context(每轮注入的固定知识 + 参考索引)和 Turn Context(按索引触发的详细手册)——背景每轮都在,详细内容推理时按需查阅。
11
+ - **沉淀对话与经验为可迭代资产**:把对话里的决策、经验、知识用文档显化,入 git 可追溯、可迭代——知识更新改一处,所有会话生效。
12
+ - **连接外部系统与数据源**:领域知识不限于项目内——外部数据源的查询方式、外部系统的操作指引,都可写成 Domain 注入 Agent。
13
+
14
+ ---
15
+
16
+ ## 安装
17
+
18
+ Pt 是 Pi 扩展,两种安装方式任选其一。
19
+
20
+ ### 方式一:Pi 插件安装(推荐)
21
+
22
+ ```bash
23
+ pi install npm:@istuen/pt
24
+ ```
25
+
26
+ Pi 自动加载 Pt。项目级安装(团队共享,写入 `.pi/settings.json`)加 `-l`:
27
+
28
+ ```bash
29
+ pi install -l npm:@istuen/pt
30
+ ```
31
+
32
+ ### 方式二:npm 安装 + 手动配置
33
+
34
+ ```bash
35
+ npm install @istuen/pt
36
+ ```
37
+
38
+ 然后在 `.pi/settings.json` 或 `~/.pi/agent/settings.json` 配置扩展入口:
39
+
40
+ ```json
41
+ {
42
+ "pi": {
43
+ "extensions": ["./node_modules/@istuen/pt/dist/index.js"]
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## 快速开始
49
+
50
+ ### 1. 激活内建 Profile
51
+
52
+ 进入 pi 后用 `/pt-profile` 选择 `guide`,或直接 `/pt-profile guide`:
53
+
54
+ ```
55
+ /pt-profile guide
56
+ ```
57
+
58
+ 也可以启动时用 flag 激活:
59
+
60
+ ```bash
61
+ pi --pt-profile guide
62
+ ```
63
+
64
+ 内建 `guide` Profile 用 dev-knowledge Blueprint + 五个内建 Domain(`user-info` / `agent-info` / `project-analysis` / `authoring` / `usage`)——Agent 立刻有“用户身份 + Agent 身份 + 怎么写 Pt 资产 / 怎么分析项目 / 怎么用 Pt”的知识。
65
+
66
+ ### 2. 加自己的知识
67
+
68
+ 用自然语言告诉 Agent 你的项目情况——`guide` Profile 会分析当前项目,帮你创建合适的 Domain 和 Profile,或直接告诉你该怎么创建。
69
+
70
+ ---
71
+
72
+ ## 命令速查
73
+
74
+ | 命令 | 作用 |
75
+ |---|---|
76
+ | `/pt-profile` | 列出所有可用 Profile |
77
+ | `/pt-profile <name>` | 切换 Profile(下一轮生效) |
78
+ | `/pt` | 查看当前编译状态 |
79
+ | `/pt flows` | 列出可触发手册 |
80
+ | `/manual:<domain>` | 注入该 Domain 的手册段到 Turn Inject |
81
+ | `/<flow> <args>` | 触发 Domain 的 FlowTemplate |
82
+ | `--pt-profile <name>` | Pi 启动时激活 Profile(CLI 优先级最高) |
83
+
84
+ LLM 工具(Agent 可调用):`pt_status` / `pt_flows` / `pt_manual` / `pt_verify` / `pt_check_refs`。
85
+
86
+ ---
87
+
88
+ ## Pt 的机制:四个概念
89
+
90
+ Pt 用四个概念组织——写领域知识,定转换结构,组装身份配置,编译出上下文:
91
+
92
+ ```
93
+ Pt Domain ──→ Blueprint ──→ Pt Profile ──→ Agent Context
94
+ 领域知识 转换结构 身份配置 编译后上下文
95
+ ```
96
+
97
+ ### Pt Domain — 领域知识
98
+
99
+ 领域知识,一个 `.md` 文件。用 MD 语法组成:**H2 做 Module**(内容段,如 `## Scene`、`## Flows`)、**H3 做语义**(项名,如 `### 角色分工`)、**列表项做描述**(`- desc: ...`)。可写业务领域的设计语义、工作流说明、外部系统操作指导。Module 允许自定义,通过 Profile 引用即可。放 `.pt/assets/domains/`。
100
+
101
+ 例子:
102
+ ```markdown
103
+ ---
104
+ name: user-info
105
+ ---
106
+ # user-info
107
+
108
+ ## User
109
+ ### who-am-i
110
+ - desc: 我是这个项目的开发者
111
+
112
+ ### preferences
113
+ - desc: 偏好类型安全、模块化设计
114
+ ```
115
+
116
+ ### Blueprint — 转换结构
117
+
118
+ 转换结构,一个 `.blueprint.yaml` 文件。定义 Profile 可以把哪些 Domain 注入到 Agent——声明有哪些聚合组(`groups`),每个组聚合哪些 Module,注入到哪(`inject: session` 会话级 / `inject: turn` 轮次级)。跨项目复用。放 `.pt/assets/blueprints/`。
119
+
120
+ ```yaml
121
+ name: dev-knowledge
122
+ groups:
123
+ - name: 会话背景
124
+ inject: session
125
+ modules: [Scene, Participant]
126
+ - name: 触发索引
127
+ inject: session
128
+ modules: [Trigger]
129
+ - name: 参考手册
130
+ inject: turn
131
+ modules: [Rules, Flows, Checklists]
132
+ ```
133
+
134
+ ### Pt Profile — 身份配置
135
+
136
+ Profile 是 Pt 最核心的功能,承接用户与 Agent 之间的会话配置。切换 Profile 会影响:会话背景、主题、用户与 Agent 的身份信息、参考信息等——让 Agent 专注当前推理范围。一个 `.profile.md` 文件,选一个 Blueprint + 列要用的 Domain。项目级,不跨项目复用。放 `.pt/assets/profiles/`。
137
+
138
+ 例子:
139
+ ```markdown
140
+ ---
141
+ name: my-dev
142
+ blueprint: dev-knowledge
143
+ domains: [user-info, authoring, usage]
144
+ ---
145
+ ```
146
+
147
+ ### Agent Context — 编译后上下文
148
+
149
+ Profile 编译后的产物,分两面——**Session Context**(每轮注入:会话背景、身份信息、触发索引)和 **Turn Context**(按需触发:推理时查阅的参考手册)。带 hash 缓存(`.pt/cache/agent-contexts/`),资产变了自动重编译。
150
+
151
+ **一句话串起来**:写 Domain → 用 Blueprint 定转换结构 → 用 Profile 组装身份 → 编译出 Agent Context 两面注入。
152
+
153
+ ---
154
+
155
+ ## Pt 资产包(Pack)
156
+
157
+ v15.x 起,Pt 资产按 **Pack** 组织——一个 Pack 是一个目录(含 `domains/` + `blueprints/` + `profiles/` 子目录 + 可选 `pt-asset-pack.yaml` manifest)。Pt 加载 4 类 Pack,优先级从高到低:
158
+
159
+ | Pack | 来源 | 寻址名 | 说明 |
160
+ |---|---|---|---|
161
+ | **project** | `<cwd>/.pt/assets/`(或 `pt.project-pack-dir` 配置) | `@prj` | 项目专属资产,永远最高优先 |
162
+ | **settings** | `.pi/settings.json` 的 `pt.asset-packs[]` | `@<manifest-name>` | 团队共享 / 第三方 Pack,按声明顺序后者赢 |
163
+ | **global** | `~/.pt/assets/` | `@gbl` | 用户跨项目共用资产 |
164
+ | **builtin** | `src/builtin/assets/`(随 npm 包) | `@pt` | 内建 fallback(guide / dev-knowledge 等) |
165
+
166
+ ### 跨 Pack 引用
167
+
168
+ Profile / Domain / Blueprint 引用可限定 Pack:`@pack-name/asset-name`。不限定时按优先级自动解析(project > settings > global > builtin)。
169
+
170
+ ```yaml
171
+ # profile.md frontmatter
172
+ blueprint: @pt-internal/dev-knowledge # 限定到 pt-internal pack
173
+ domains: [@gbl/team-stdlib, workflow] # @gbl 限定 + 无前缀自动解析
174
+ ```
175
+
176
+ ### settings 声明 Pack
177
+
178
+ 在 `.pi/settings.json` 加载第三方 / 团队 Pack(只声明 path,name 从 manifest 读):
179
+
180
+ ```json
181
+ {
182
+ "pt": {
183
+ "asset-packs": [
184
+ { "path": "~/projects/pt-team-stdlib/assets" },
185
+ { "path": "../shared-pt-assets" }
186
+ ],
187
+ "project-pack-dir": ".pt/assets"
188
+ }
189
+ }
190
+ ```
191
+
192
+ `project-pack-dir` 可指向项目外路径(`~` / 绝对 / 相对 cwd 都支持)。
193
+
194
+ ### Profile `use` 单继承
195
+
196
+ Profile 可 `use` 另一 Profile 作为基础,增量覆盖:
197
+
198
+ ```yaml
199
+ ---
200
+ name: my-dev
201
+ use: @pt-internal/pt-dev # 继承 pt-dev 作为基础
202
+ blueprint: @team-stdlib/minimal # 覆盖 use 的 blueprint
203
+ domains: [my-domain] # 追加到 use 的 domains
204
+ ---
205
+ ```
206
+
207
+ 合并规则:`name` 强制(不继承)/ `blueprint` 覆盖 / `tagline` 覆盖 / `domains` 追加去重 / `groups` 同名替换。不写 `use` = 完全独立 Profile(back-compat)。
208
+
209
+ ---
210
+
211
+ ## 开发
212
+
213
+ 技术栈:TypeScript + tsup + Vitest + Biome。零运行时依赖(除 yaml 库用于 Blueprint 解析)。
214
+
215
+ ---
216
+
217
+ ## License
218
+
219
+ MIT
@@ -0,0 +1,8 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { AgentAPI, AgentUI } from "../schema.js";
3
+ /** AgentAPI 的 ui 字段类型(NonNullable<AgentAPI["ui"]>)。 */
4
+ type AgentUIContext = AgentUI;
5
+ export declare function toAgentAPI(pi: ExtensionAPI, ctx: {
6
+ ui: AgentUIContext;
7
+ }): AgentAPI;
8
+ export {};
@@ -0,0 +1,83 @@
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
+ import { getSessionById } from "../session.js";
18
+ /** 将 Pi ExtensionAPI 转换为 AgentAPI(结构类型子集,运行时透明)。
19
+ * Pi ExtensionAPI 是 AgentAPI 的超集,多余方法(registerCommand/registerFlag 等)不暴露给 Adapter。
20
+ * v9.1:提供 ui 能力,adapter 可走 ui.notify 报错 / ui.setStatus 设状态(不需 console)。
21
+ * v10.x:提供 log 能力,adapter 的 try/catch 异常走 session.logger(不再 swallow)。
22
+ * ctx 用 Pi 扩展的 ctx(ExtensionContext/ExtensionCommandContext 都含 ui)——子集够用。
23
+ *
24
+ * 同一个 Pi runtime 复用同一个 AgentAPI wrapper,否则 PiAdapter 每次 registerInject
25
+ * 都会得到不同的对象身份,导致系统 prompt handler 被重复注册。 */
26
+ const agentApiCache = new WeakMap();
27
+ export function toAgentAPI(pi, ctx) {
28
+ let holder = agentApiCache.get(pi);
29
+ if (!holder) {
30
+ let currentCtx = ctx;
31
+ // v12.x:closure 捕获 currentSessionId,供 api.log/onInjected 拿 per-session state
32
+ const stateRef = { currentSessionId: undefined };
33
+ // v12.x:on 包装——handler 触发前自动从 args[1] ctx 拿 sessionId 注入 stateRef
34
+ const onWrapped = (event, handler) => {
35
+ pi.on(event, ((event, extCtx) => {
36
+ // 提取 sessionId:extCtx 是 ExtensionContext
37
+ const c = extCtx;
38
+ stateRef.currentSessionId = c?.sessionManager?.getSessionId?.();
39
+ return handler(event, extCtx);
40
+ }));
41
+ };
42
+ const api = {
43
+ on: onWrapped,
44
+ registerCommand: (name, spec) => pi.registerCommand(name, spec),
45
+ registerFlag: (name, spec) => pi.registerFlag(name, spec),
46
+ getFlag: (name) => pi.getFlag(name),
47
+ ui: {
48
+ notify: (msg, level) => currentCtx.ui.notify(msg, level),
49
+ setStatus: (name, text) => currentCtx.ui.setStatus(name, text),
50
+ },
51
+ // v12.x:api.log 走 per-session logger(避免 tab A 写到 tab B 的日志文件)
52
+ get log() {
53
+ const sid = stateRef.currentSessionId;
54
+ return sid ? getSessionById(sid).logger?.toWriter() : undefined;
55
+ },
56
+ // v12.x:onInjected 写 per-session lastBuiltPrompt
57
+ onInjected: (systemPrompt) => {
58
+ const sid = stateRef.currentSessionId;
59
+ if (sid)
60
+ getSessionById(sid).lastBuiltPrompt = systemPrompt;
61
+ },
62
+ };
63
+ holder = {
64
+ api,
65
+ setContext: (nextCtx) => {
66
+ currentCtx = nextCtx;
67
+ },
68
+ currentSessionId: undefined,
69
+ };
70
+ // 把 stateRef 同步到 holder(保持单一来源)
71
+ Object.defineProperty(holder, "currentSessionId", {
72
+ get: () => stateRef.currentSessionId,
73
+ set: (v) => {
74
+ stateRef.currentSessionId = v;
75
+ },
76
+ });
77
+ agentApiCache.set(pi, holder);
78
+ }
79
+ else {
80
+ holder.setContext(ctx);
81
+ }
82
+ return holder.api;
83
+ }
@@ -0,0 +1,2 @@
1
+ export { PiAdapter } from "./pi-adapter.js";
2
+ export { getAgentAdapter } from "./registry.js";
@@ -0,0 +1,3 @@
1
+ // src/agent/index.ts — Agent 适配器入口
2
+ export { PiAdapter } from "./pi-adapter.js";
3
+ export { getAgentAdapter } from "./registry.js";
@@ -0,0 +1,35 @@
1
+ import { type AgentAdapter, type AgentAPI, type AgentContext, type Blueprint, type Domain, type Profile } from "../schema.js";
2
+ /** PiAdapter:封装 Pi Agent 的注入机制。 */
3
+ export declare class PiAdapter implements AgentAdapter {
4
+ name: string;
5
+ /** Phase term-P4.3:保留 Pi API 名 system_prompt/context_message——
6
+ * 这是 AgentAdapter 映射边界声明(Pi 支持哪些 Agent Runtime 层注入位置)。
7
+ * Blueprint 用 session/turn 语义值(聚合组 inject 字段),Adapter 内部映射到此字段声明的 Pi API 名。 */
8
+ supportedTargets: string[];
9
+ private ctx;
10
+ private blueprint;
11
+ private domains;
12
+ private segment;
13
+ /** 与当前 Pi runtime 的 AgentAPI 绑定;同一 runtime 不重复注册 handler。
14
+ * v12.x:per-pi 实例字段——`registry.ts` 给每个 pi 一个新 PiAdapter,所以 `injectedApi`
15
+ * 不会被其他 session 覆盖。 */
16
+ private injectedApi;
17
+ /** v13.x(issue pt-turn-inject-not-profile-scoped):当前 Profile——turn 路径 scope 过滤用。 */
18
+ private profile;
19
+ /** 设置编译产物(transpile 后调)。v12.x:per-pi 实例字段——本 session 的 segment
20
+ * 不会被其他 session 覆盖。
21
+ * v13.x(issue pt-turn-inject-not-profile-scoped):加 profile 参数——turn 路径 scope 过滤用。 */
22
+ setAgentContext(ctx: AgentContext, blueprint: Blueprint, domains: Domain[], profile: Profile): void;
23
+ /** 清除当前 session 的 context;保留当前 runtime 的 handler 绑定。
24
+ * v12.x:per-pi 实例字段——只清本 session 的状态。 */
25
+ resetInjection(): void;
26
+ /** 启动时注册:把 AgentContext 注入到 Agent。 */
27
+ registerInject(api: AgentAPI, ctx: AgentContext, blueprint: Blueprint, domains?: Domain[], profile?: Profile | null): void;
28
+ /** 查询可用手册(/pt flows 用)。
29
+ * v9:遍历 Blueprint 的 inject=turn 聚合组 → 引用 Domain → 找 FlowTemplate + Rules 段的 Rule。 */
30
+ listManuals(_ctx: AgentContext, blueprint: Blueprint, domains: Domain[]): Array<{
31
+ name: string;
32
+ hint?: string;
33
+ domain: string;
34
+ }>;
35
+ }
@@ -0,0 +1,251 @@
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
+ import { AGENT_PI, MOD_FLOWS, MOD_RULES } from "../constants.js";
20
+ import { isFlowTemplateArray, isRuleArray } from "../compile/type-guards.js";
21
+ import { renderInjectionFooter } from "../injection-status.js";
22
+ import { getSessionById } from "../session.js";
23
+ import { filterDomainsByProfile, } from "../schema.js";
24
+ import { renderTurnInject } from "../render/turn-inject.js";
25
+ import { renderSessionInject } from "../render/session-inject.js";
26
+ /** v12.x:从 handler 的 args[1] ctx 提取 sessionId。
27
+ * pi 的 `pi.on(event, handler)` 触发时传 `(event, ctx)` 两个参数;通过 AgentAPI 包装后
28
+ * handler 拿到 `(...args)`,args[0] = event, args[1] = ctx (ExtensionContext)。 */
29
+ function sessionIdFromArgs(args) {
30
+ const ctx = args[1];
31
+ return ctx?.sessionManager?.getSessionId?.();
32
+ }
33
+ /** PiAdapter:封装 Pi Agent 的注入机制。 */
34
+ export class PiAdapter {
35
+ name = AGENT_PI;
36
+ /** Phase term-P4.3:保留 Pi API 名 system_prompt/context_message——
37
+ * 这是 AgentAdapter 映射边界声明(Pi 支持哪些 Agent Runtime 层注入位置)。
38
+ * Blueprint 用 session/turn 语义值(聚合组 inject 字段),Adapter 内部映射到此字段声明的 Pi API 名。 */
39
+ supportedTargets = ["system_prompt", "context_message"];
40
+ ctx = null;
41
+ blueprint = null;
42
+ domains = [];
43
+ segment = null;
44
+ /** 与当前 Pi runtime 的 AgentAPI 绑定;同一 runtime 不重复注册 handler。
45
+ * v12.x:per-pi 实例字段——`registry.ts` 给每个 pi 一个新 PiAdapter,所以 `injectedApi`
46
+ * 不会被其他 session 覆盖。 */
47
+ injectedApi = null;
48
+ /** v13.x(issue pt-turn-inject-not-profile-scoped):当前 Profile——turn 路径 scope 过滤用。 */
49
+ profile = null;
50
+ /** 设置编译产物(transpile 后调)。v12.x:per-pi 实例字段——本 session 的 segment
51
+ * 不会被其他 session 覆盖。
52
+ * v13.x(issue pt-turn-inject-not-profile-scoped):加 profile 参数——turn 路径 scope 过滤用。 */
53
+ setAgentContext(ctx, blueprint, domains, profile) {
54
+ this.ctx = ctx;
55
+ this.blueprint = blueprint;
56
+ this.domains = domains;
57
+ this.profile = profile;
58
+ this.segment = renderSessionInject(ctx, blueprint);
59
+ }
60
+ /** 清除当前 session 的 context;保留当前 runtime 的 handler 绑定。
61
+ * v12.x:per-pi 实例字段——只清本 session 的状态。 */
62
+ resetInjection() {
63
+ this.ctx = null;
64
+ this.blueprint = null;
65
+ this.domains = [];
66
+ this.profile = null;
67
+ this.segment = null;
68
+ }
69
+ /** 启动时注册:把 AgentContext 注入到 Agent。 */
70
+ registerInject(api, ctx, blueprint, domains = this.domains, profile = this.profile) {
71
+ // 先更新状态;同一 runtime 的后续 Profile 切换不能重新注册 handler,
72
+ // 但 handler 会在事件发生时读取最新的 this.segment / this.ctx。
73
+ this.ctx = ctx;
74
+ this.blueprint = blueprint;
75
+ this.domains = domains;
76
+ this.profile = profile;
77
+ this.segment = renderSessionInject(ctx, blueprint);
78
+ if (this.injectedApi === api) {
79
+ api.log?.debug("agent:registerInject skipped (already injected)");
80
+ return;
81
+ }
82
+ this.injectedApi = api;
83
+ // session 注入(inject=session → Pi system_prompt 事件):每轮追加 segment
84
+ // v10.x:包 try/catch,运行时异常走 api.log.error + ui.notify,不再 swallow
85
+ // v11.x(issue pt-injection-status-manual-track):三分支写 session.injectionState +
86
+ // 调 api.ui?.setStatus,让 footer 三态文字真实反映注入结果(自报,不检测 Pi)
87
+ // v12.x:从 args[1] ctx 拿 per-session sessionId,写 per-session SessionState
88
+ api.on("before_agent_start", async (...args) => {
89
+ const t0 = Date.now();
90
+ try {
91
+ const sessionId = sessionIdFromArgs(args);
92
+ const sessionState = sessionId ? getSessionById(sessionId) : null;
93
+ const currentSegment = this.segment;
94
+ // v14.x(issue pt-asset-migration-visibility Layer 2):footer 末尾追加 ⚠ N issues。
95
+ // 从 sessionState.assetHealthIssues 读计数——session_start 已批量体检过。
96
+ const healthCount = sessionState?.assetHealthIssues?.length ?? 0;
97
+ // v14.x(tagline):footer 拼 `: <tagline>`。从 sessionState.cachedProfile 读——transpile 时已存。
98
+ const tagline = sessionState?.cachedProfile?.tagline ?? null;
99
+ if (!currentSegment) {
100
+ // 无 segment(未加载 Profile / 已被 reset)→ idle
101
+ if (sessionState) {
102
+ sessionState.injectionState = "idle";
103
+ sessionState.injectionError = null;
104
+ api.ui?.setStatus("pt", renderInjectionFooter("idle", sessionState.activeProfile, null, healthCount, "auto", tagline));
105
+ }
106
+ return undefined;
107
+ }
108
+ const event = args[0];
109
+ if (!isSystemPromptEvent(event)) {
110
+ // 事件形状异常:归类为 idle(不视为失败——Pi 可能改了事件签名)
111
+ if (sessionState) {
112
+ sessionState.injectionState = "idle";
113
+ sessionState.injectionError = null;
114
+ api.ui?.setStatus("pt", renderInjectionFooter("idle", sessionState.activeProfile, null, healthCount, "auto", tagline));
115
+ }
116
+ return undefined;
117
+ }
118
+ const final = `${event.systemPrompt}\n\n## 当前任务上下文\n\n${currentSegment}`;
119
+ api.log?.debug("agent:before_agent_start ok", {
120
+ originalLen: event.systemPrompt.length,
121
+ injectedLen: final.length,
122
+ deltaLen: final.length - event.systemPrompt.length,
123
+ durationMs: Date.now() - t0,
124
+ });
125
+ if (sessionState) {
126
+ api.onInjected?.(final);
127
+ // 成功注入 → injected
128
+ sessionState.injectionState = "injected";
129
+ sessionState.injectionError = null;
130
+ api.ui?.setStatus("pt", renderInjectionFooter("injected", sessionState.activeProfile, null, healthCount, "auto", tagline));
131
+ }
132
+ else {
133
+ api.onInjected?.(final);
134
+ }
135
+ return { systemPrompt: final };
136
+ }
137
+ catch (e) {
138
+ const msg = e instanceof Error ? e.message : String(e);
139
+ api.log?.error("agent:before_agent_start failed", {
140
+ err: msg,
141
+ durationMs: Date.now() - t0,
142
+ });
143
+ api.ui?.notify(`[pt] before_agent_start failed: ${msg}`, "error");
144
+ // 异常 → failed + 错误消息(footer 追加)
145
+ const sessionId = sessionIdFromArgs(args);
146
+ const sessionState = sessionId ? getSessionById(sessionId) : null;
147
+ // v14.x:catch 路径同样透传 healthCount + tagline(避免修复丢告警 / tagline)
148
+ const healthCount = sessionState?.assetHealthIssues?.length ?? 0;
149
+ const tagline = sessionState?.cachedProfile?.tagline ?? null;
150
+ if (sessionState) {
151
+ sessionState.injectionState = "failed";
152
+ sessionState.injectionError = msg;
153
+ api.ui?.setStatus("pt", renderInjectionFooter("failed", sessionState.activeProfile, msg, healthCount, "auto", tagline));
154
+ }
155
+ return undefined; // 失败降级, 不影响主流程
156
+ }
157
+ });
158
+ // context_message 触发:/manual:xxx + /<flow-name>
159
+ // v10.x:包 try/catch,renderTurnInject 抛错不再 swallow
160
+ api.on("input", async (...args) => {
161
+ const t0 = Date.now();
162
+ try {
163
+ if (!this.ctx || !this.blueprint)
164
+ return { action: "continue" };
165
+ const event = args[0];
166
+ if (!isInputEvent(event))
167
+ return { action: "continue" };
168
+ // Phase term-P4.3:renderContextMessage → renderTurnInject
169
+ // v13.x(issue pt-turn-inject-not-profile-scoped):按 Profile scope 过滤 domains + 传 profile
170
+ // 让 /manual:<domain> 在未引用该 domain 的 Profile 下返 null(与 /pt flows 列表一致)
171
+ const scoped = filterDomainsByProfile(this.domains, this.profile);
172
+ const result = renderTurnInject(this.ctx, this.blueprint, scoped, this.profile, event.text);
173
+ const durationMs = Date.now() - t0;
174
+ if (result === null) {
175
+ api.log?.debug("agent:input passthrough", {
176
+ inputPreview: event.text.slice(0, 80),
177
+ durationMs,
178
+ });
179
+ return { action: "continue" };
180
+ }
181
+ api.log?.info("agent:input transformed", {
182
+ inputPreview: event.text.slice(0, 80),
183
+ outputLen: result.length,
184
+ durationMs,
185
+ });
186
+ return { action: "transform", text: result };
187
+ }
188
+ catch (e) {
189
+ const msg = e instanceof Error ? e.message : String(e);
190
+ api.log?.error("agent:input render failed", {
191
+ err: msg,
192
+ input: args[0],
193
+ durationMs: Date.now() - t0,
194
+ });
195
+ api.ui?.notify(`[pt] input render failed: ${msg}`, "error");
196
+ return { action: "continue" }; // 失败降级: 不拦截 input, 让原文本过 LLM
197
+ }
198
+ });
199
+ }
200
+ /** 查询可用手册(/pt flows 用)。
201
+ * v9:遍历 Blueprint 的 inject=turn 聚合组 → 引用 Domain → 找 FlowTemplate + Rules 段的 Rule。 */
202
+ listManuals(_ctx, blueprint, domains) {
203
+ const flows = [];
204
+ for (const group of blueprint.groups) {
205
+ // Phase term-P4.3:inject 语义值 context_message → turn
206
+ // Phase term-naming:字段名 target → inject
207
+ if (group.inject !== "turn")
208
+ continue;
209
+ // v13.x(issue pt-turn-inject-not-profile-scoped):按 Profile scope 过滤 domains——
210
+ // adapter 内部统一过滤,调用方(commands.ts flowsText)无需预过滤。
211
+ const scoped = filterDomainsByProfile(domains, this.profile);
212
+ for (const d of scoped) {
213
+ // Phase term-P9.2:FlowTemplate 在 ## Flows 段;Rule[] 在 ## Rules 段;Checklist[] 在 ## Checklists 段。
214
+ const flowsContent = d.modules[MOD_FLOWS];
215
+ if (isFlowTemplateArray(flowsContent)) {
216
+ for (const t of flowsContent) {
217
+ flows.push({ name: t.name, hint: t.argumentHint, domain: d.name });
218
+ }
219
+ }
220
+ const rulesContent = d.modules[MOD_RULES];
221
+ if (isRuleArray(rulesContent) && rulesContent.length > 0) {
222
+ // term-Domain 的 Rule[] 作为 /manual:<domain> 暴露
223
+ flows.push({
224
+ name: `/manual:${d.name}`,
225
+ hint: `${rulesContent.length} 条规范`,
226
+ domain: d.name,
227
+ });
228
+ }
229
+ // Checklist[] 暂不单独暴露——/manual:<domain> 命令会统一处理(renderDomainManual)
230
+ }
231
+ }
232
+ // 去重(按 name)
233
+ const seen = new Set();
234
+ return flows.filter((f) => {
235
+ const key = `${f.domain}/${f.name}`;
236
+ if (seen.has(key))
237
+ return false;
238
+ seen.add(key);
239
+ return true;
240
+ });
241
+ }
242
+ }
243
+ // ==================== Pi ExtensionAPI 事件 type guards ====================
244
+ function isSystemPromptEvent(x) {
245
+ return (!!x &&
246
+ typeof x === "object" &&
247
+ typeof x.systemPrompt === "string");
248
+ }
249
+ function isInputEvent(x) {
250
+ return !!x && typeof x === "object" && typeof x.text === "string";
251
+ }
@@ -0,0 +1,4 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { AgentAdapter } from "../schema.js";
3
+ /** 取指定 pi + name 的 AgentAdapter。未找到返 pi 的 per-pi 实例(fallback)。 */
4
+ export declare function getAgentAdapter(pi: ExtensionAPI, name: string): AgentAdapter;