pi-learning-agent 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 (187) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/CONTRIBUTING.md +34 -0
  3. package/LICENSE +21 -0
  4. package/README.md +154 -0
  5. package/SECURITY.md +9 -0
  6. package/docs/architecture.md +75 -0
  7. package/docs/images/learning-workspace.png +0 -0
  8. package/docs/releasing.md +51 -0
  9. package/extension/commands.ts +181 -0
  10. package/extension/index.ts +50 -0
  11. package/extension/learning-session.ts +75 -0
  12. package/extension/runner/code-runner.ts +275 -0
  13. package/extension/server/interaction-broker.ts +345 -0
  14. package/extension/server/learning-server.ts +753 -0
  15. package/extension/server/sse-hub.ts +63 -0
  16. package/extension/state/learning-state.ts +458 -0
  17. package/extension/state/session-persistence.ts +72 -0
  18. package/extension/tools/ask-code.ts +97 -0
  19. package/extension/tools/ask-free-response.ts +88 -0
  20. package/extension/tools/ask-multi-choice.ts +109 -0
  21. package/extension/tools/ask-record-attempt.ts +50 -0
  22. package/extension/tools/ask-single-choice.ts +110 -0
  23. package/extension/tools/index.ts +39 -0
  24. package/extension/tools/tui-presenter.ts +609 -0
  25. package/extension/transcript-sync.ts +197 -0
  26. package/extension/tutor-prompt.ts +64 -0
  27. package/extension/utils/browser.ts +35 -0
  28. package/extension/utils/ids.ts +5 -0
  29. package/extension/utils/validation.ts +138 -0
  30. package/package.json +82 -0
  31. package/shared/learning-state.ts +62 -0
  32. package/shared/protocol.ts +236 -0
  33. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  34. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  35. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  36. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  37. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  38. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  39. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  40. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  41. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  42. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  43. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  44. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  45. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  46. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  47. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  48. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  49. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  50. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  51. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  52. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  53. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  54. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  55. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  56. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  57. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  58. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  59. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  60. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  61. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  62. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  63. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  64. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  65. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  66. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  67. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  68. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  69. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  70. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  71. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  72. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  73. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  74. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  75. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  76. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  77. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  78. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  79. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  80. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  81. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  82. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  83. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  84. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  85. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  86. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  87. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  88. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  89. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  90. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  91. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  92. package/web/dist/assets/abap-08VXUWAP.js +1 -0
  93. package/web/dist/assets/apex-BWPQTe0t.js +1 -0
  94. package/web/dist/assets/azcli-Bc_sGQ0U.js +1 -0
  95. package/web/dist/assets/bat-i0X4ZdIN.js +1 -0
  96. package/web/dist/assets/bicep-B5-_aFwp.js +2 -0
  97. package/web/dist/assets/cameligo-DMUM7wLl.js +1 -0
  98. package/web/dist/assets/clojure-Cm7r79vr.js +1 -0
  99. package/web/dist/assets/codicon-Brq4_Ui5.ttf +0 -0
  100. package/web/dist/assets/coffee-Ba7i2nA0.js +1 -0
  101. package/web/dist/assets/cpp-C7h46wYY.js +1 -0
  102. package/web/dist/assets/csharp-BKxtCVv1.js +1 -0
  103. package/web/dist/assets/csp-bTuwJoIa.js +1 -0
  104. package/web/dist/assets/css-DIMkf-bt.js +3 -0
  105. package/web/dist/assets/css.worker-DRH2OcAG.js +93 -0
  106. package/web/dist/assets/cssMode-BpvqYhpN.js +1 -0
  107. package/web/dist/assets/cypher-CVaqCwHa.js +1 -0
  108. package/web/dist/assets/dart-onAF5SnQ.js +1 -0
  109. package/web/dist/assets/dockerfile-DZFCIeNp.js +1 -0
  110. package/web/dist/assets/ecl-D05T4iGw.js +1 -0
  111. package/web/dist/assets/editor.worker-D9zwrD0f.js +30 -0
  112. package/web/dist/assets/elixir-6RTg0lbw.js +1 -0
  113. package/web/dist/assets/flow9-C5_-GSwl.js +1 -0
  114. package/web/dist/assets/freemarker2-CmTivYCM.js +3 -0
  115. package/web/dist/assets/fsharp-C8Ef5oNN.js +1 -0
  116. package/web/dist/assets/go-C-y9NEjX.js +1 -0
  117. package/web/dist/assets/graphql-fmXr3nnJ.js +1 -0
  118. package/web/dist/assets/handlebars-ClhpojlY.js +1 -0
  119. package/web/dist/assets/hcl-CpzslTdj.js +1 -0
  120. package/web/dist/assets/html-CTqIxFH7.js +1 -0
  121. package/web/dist/assets/html.worker-DQLddT9h.js +506 -0
  122. package/web/dist/assets/htmlMode-BKCTQ4_a.js +1 -0
  123. package/web/dist/assets/index-6N_lnfyE.js +1243 -0
  124. package/web/dist/assets/index-B5x0MzHm.css +1 -0
  125. package/web/dist/assets/ini-sBoK_t0W.js +1 -0
  126. package/web/dist/assets/java-BEtHBSE6.js +1 -0
  127. package/web/dist/assets/javascript-Cp_q6aWP.js +1 -0
  128. package/web/dist/assets/json.worker-D_Dqrls5.js +62 -0
  129. package/web/dist/assets/jsonMode-CMDRrSMX.js +7 -0
  130. package/web/dist/assets/julia-Bri6UV-V.js +1 -0
  131. package/web/dist/assets/kotlin-BOotOW0E.js +1 -0
  132. package/web/dist/assets/less-B9JPFI3C.js +2 -0
  133. package/web/dist/assets/lexon-CfSJPG6W.js +1 -0
  134. package/web/dist/assets/liquid-BFK8j46k.js +1 -0
  135. package/web/dist/assets/lspLanguageFeatures-nT2b0EDJ.js +4 -0
  136. package/web/dist/assets/lua-CsQS60Ue.js +1 -0
  137. package/web/dist/assets/m3-D-oSqn_W.js +1 -0
  138. package/web/dist/assets/markdown-Cimd5fb3.js +1 -0
  139. package/web/dist/assets/mdx-cEJtnt-p.js +1 -0
  140. package/web/dist/assets/mips-CIPQ_RoX.js +1 -0
  141. package/web/dist/assets/msdax-DauUninz.js +1 -0
  142. package/web/dist/assets/mysql-SOo6toE5.js +1 -0
  143. package/web/dist/assets/objective-c-FvmIjYaQ.js +1 -0
  144. package/web/dist/assets/pascal-DrH0SRf2.js +1 -0
  145. package/web/dist/assets/pascaligo-D-ptJ9y-.js +1 -0
  146. package/web/dist/assets/perl-oz_6vUea.js +1 -0
  147. package/web/dist/assets/pgsql-DTj74zXo.js +1 -0
  148. package/web/dist/assets/php-nr791fC2.js +1 -0
  149. package/web/dist/assets/pla-CopQ2nXW.js +1 -0
  150. package/web/dist/assets/postiats-43DmfD33.js +1 -0
  151. package/web/dist/assets/powerquery-D3hlyOfw.js +1 -0
  152. package/web/dist/assets/powershell-DmHpPYUd.js +1 -0
  153. package/web/dist/assets/protobuf-C531GsRP.js +2 -0
  154. package/web/dist/assets/pug-Z5eAx3Zn.js +1 -0
  155. package/web/dist/assets/python-bhI_n2_n.js +1 -0
  156. package/web/dist/assets/qsharp-DkqhCAOL.js +1 -0
  157. package/web/dist/assets/r-BwWrilGY.js +1 -0
  158. package/web/dist/assets/razor-D40-I8aA.js +1 -0
  159. package/web/dist/assets/redis-ClamHrr6.js +1 -0
  160. package/web/dist/assets/redshift-DT7zqm-g.js +1 -0
  161. package/web/dist/assets/restructuredtext-BYgofb2h.js +1 -0
  162. package/web/dist/assets/ruby-DezsRK8O.js +1 -0
  163. package/web/dist/assets/rust-DdL9SqIa.js +1 -0
  164. package/web/dist/assets/sb-CcwsVR0C.js +1 -0
  165. package/web/dist/assets/scala-DHpiXF5c.js +1 -0
  166. package/web/dist/assets/scheme-BeGwcela.js +1 -0
  167. package/web/dist/assets/scss-gp-XZpBa.js +3 -0
  168. package/web/dist/assets/shell-CC2rA5mh.js +1 -0
  169. package/web/dist/assets/solidity-BEEn4gHE.js +1 -0
  170. package/web/dist/assets/sophia-CRfGWb83.js +1 -0
  171. package/web/dist/assets/sparql-D_Lu-MrJ.js +1 -0
  172. package/web/dist/assets/sql-NEE52Syq.js +1 -0
  173. package/web/dist/assets/st-DbInun42.js +1 -0
  174. package/web/dist/assets/swift-Bxkupp3x.js +1 -0
  175. package/web/dist/assets/systemverilog-Bz4Y3fRF.js +1 -0
  176. package/web/dist/assets/tcl-DISqw1ZD.js +1 -0
  177. package/web/dist/assets/ts.worker-DkYoA6bF.js +67738 -0
  178. package/web/dist/assets/tsMode-C9OJ2Aji.js +11 -0
  179. package/web/dist/assets/twig-De2hgUGE.js +1 -0
  180. package/web/dist/assets/typescript-CZ5RbSdB.js +1 -0
  181. package/web/dist/assets/typespec-B8J7ngcE.js +1 -0
  182. package/web/dist/assets/vb-DV3o63ZY.js +1 -0
  183. package/web/dist/assets/wgsl-DpFanUEy.js +298 -0
  184. package/web/dist/assets/workers-qb_EZhxe.js +1 -0
  185. package/web/dist/assets/xml-BdDnAXLl.js +1 -0
  186. package/web/dist/assets/yaml-BSYHoVQy.js +1 -0
  187. package/web/dist/index.html +23 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # 变更记录
2
+
3
+ ## 0.1.0 — 2026-09-17
4
+
5
+ - 修复模型只调用评分工具时工作台看不到判定与讲解的问题;评分工具要求提供讲解,并在成功后同步反馈。
6
+ - 修正状态提示:评分不再显示等待回答,工具执行结束不再误报整轮讲解结束。
7
+ - 修复单选题数字或字母快捷键选中后焦点不同步,确保方向键从当前选项继续导航。
8
+ - 修复切换主题等待旧题、合法概念名评分失败和评分证据类型不匹配的问题。
9
+ - 保留 Markdown、代码转义和公式结构,避免中文输入法确认候选时误提交;补齐 SSE 先到时的题干。
10
+ - 修复 Monaco 只读区随编辑及草稿刷新后的定位,并保证 Reset 显示与提交一致;增加不调用模型的真实浏览器回归。
11
+ - 停服关闭活动 HTTP 连接,并通知正在执行的本地自测取消。
12
+
13
+ - 统一评分、重置和 Pi 会话切换的生命周期,阻止跳过题目、重置前旧题和其他会话的旧题计分。
14
+ - Extension 与 Web 共用学习协议和状态类型,发布包包含 `shared/`。
15
+ - 修复旧 HTTP 快照覆盖 SSE 状态、页面退出后旧初始化重新连接的问题。
16
+ - Windows 取消代码运行时等待进程关闭后清理临时目录。
17
+ - 增加真实 tarball 离线安装、Pi 加载、Web 脚本和 Monaco worker 验收。
18
+ - 日常测试与发布检查分开,增加 Windows/Linux、Node.js 22/24 的持续集成配置。
19
+ - 补充贡献、架构、安全和发布文档,明确社区扩展身份与 MIT 许可证。
@@ -0,0 +1,34 @@
1
+ # 参与贡献
2
+
3
+ 本项目是使用 Pi 官方包格式分发的 MIT 社区扩展。开发基线为 Pi 0.84.0、Node.js >=22.19.0 与 npm >=11。
4
+
5
+ ## 开发与验证
6
+
7
+ 根 `package-lock.json` 管理整个 npm workspace。不要在 `web/` 新建第二份锁文件。
8
+
9
+ ```sh
10
+ npm ci
11
+ npm run check
12
+ npm run build:web
13
+ npm run pi
14
+ ```
15
+
16
+ 在 Pi 中运行 `/learn rust trait bounds`。完整交互需要扩展提供的本地 HTTP/SSE 与会话 token。
17
+
18
+ `npm run check` 包含两端类型检查以及日常测试,不构建发布包。改动安装、协议、目录或静态资源后运行 `npm run check:package` 和 `npm run smoke`;发布前统一运行 `npm run check:release`。
19
+
20
+ 归档验收会真实打包、在系统临时目录离线安装,再用 Pi 加载包内入口并检查工作台和 Monaco worker;使用独立测试目录,不读取个人 Pi 上下文。
21
+
22
+ 真实模型测试通过 `npm run e2e -- a --timeout 540` 与 `npm run e2e:browser` 单独运行。它们使用 Pi 配置的提供商和凭据,会消耗模型额度;浏览器测试要求 Microsoft Edge。普通持续集成不运行模型 E2E。
23
+
24
+ `npm run test:workspace` 使用真实 Microsoft Edge 和临时 loopback Vite 服务,通过虚构 API 验证 Monaco 编辑行为,不启动 Pi 或调用模型。修改代码编辑、只读范围或草稿恢复时运行它;配置独立于模型 E2E,目前不加入缺少 Edge 的日常 CI。
25
+
26
+ ## 修改约定
27
+
28
+ 保留结构化 tool result、顺序作答、跳过与取消的区别,以及按当前 Pi 分支恢复状态的语义。涉及协议与发布目录时同时检查 shared、package.json.files 和归档验收。
29
+
30
+ 新增行为通过公开入口测试,避免替换内部 Broker/状态对象或依赖私有函数。按一个行为一次的红—绿—重构循环推进;修复确认的失败后再扩大检查范围。
31
+
32
+ 每次落盘修改在 work.md 记录时间、问题、解决方式、变更文件、验证结果与撤回方式。提交 pull request 时说明具体场景、最终行为、检查结果和剩余限制。
33
+
34
+ 不要提交会话 token、模型凭据、个人学习内容、node_modules、web/dist 或本地 .tgz。敏感问题的处理方式见 [SECURITY.md](SECURITY.md)。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 金琦亮
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,154 @@
1
+ # Pi Learning Agent
2
+
3
+ **在 Pi 中开启一门课,把讲解、练习和反馈放进同一个学习工作台。**
4
+
5
+ Pi Learning Agent 是 [Pi Coding Agent](https://pi.dev/) 的社区学习扩展。导师根据课程和主题讲解、出题,你在浏览器或终端中作答;答案作为结构化工具结果返回导师,评估后更新知识点掌握度。
6
+
7
+ ## 界面预览
8
+
9
+ ![Pi Learning 学习工作台:左侧是数据库设计讲解,右侧是自由回答练习](docs/images/learning-workspace.png)
10
+
11
+ *数据库设计学习示例:左侧回顾导师讲解,右侧完成当前练习;顶栏可展开知识点进度。*
12
+
13
+ ## 可以怎样学习
14
+
15
+ 1. **指定方向**:用 `/learn <课程> <主题>` 开始学习,导师从简短诊断切入。
16
+ 2. **理解与练习**:阅读讲解,在当前题目中选择答案、描述思路或编写代码。
17
+ 3. **获得反馈**:提交后由导师评阅,再根据表现继续讲解和出题。
18
+ 4. **查看进度**:按知识点查看掌握度、尝试次数和正确次数。
19
+
20
+ ### 支持的题型
21
+
22
+ | 作答形式 | 适合的练习 | 交互方式 |
23
+ | --- | --- | --- |
24
+ | 单选题 | 概念辨析、判断正误、预测代码输出 | 选择一个答案后提交 |
25
+ | 多选题 | 规则判断、选择所有满足条件的情况 | 选择多个答案后提交 |
26
+ | 单行简答 | 短答案、简单填空 | 输入文本,按 Enter 提交 |
27
+ | 多行自由回答 | 解释概念、分析场景、描述解题思路 | 输入多段文本,支持 Ctrl/Cmd+Enter 提交 |
28
+ | 代码题 | 函数实现、代码补全、修改错误 | 在 Monaco 编辑器中作答,支持 Ctrl/Cmd+Enter 提交 |
29
+
30
+ 单行与多行回答共用自由回答工具。选择题可以由导师配置是否允许跳过;多空填空、拖拽排序和连线题尚无专用组件。
31
+
32
+ ### 为专注学习设计
33
+
34
+ - **可调双栏**:拖动分隔条分配讲解与作答空间,也支持方向键调整;手机端自动切换为上下布局。
35
+ - **阅读不被打断**:在底部时跟随导师输出;向上翻阅后暂停跟随,通过“有新内容”回到最新讲解。
36
+ - **按需查看进度**:顶栏显示阶段和掌握度摘要,详细概念进度在侧栏展开,支持 Escape 关闭。
37
+ - **清晰的内容展示**:区分导师讲解与个人回答,支持流式文本、Markdown、代码块和 KaTeX 公式。
38
+ - **保留作答草稿**:自由回答和代码题的草稿保存在当前浏览器本地,刷新后可以继续编辑。
39
+ - **浏览器与终端衔接**:浏览器连接时优先使用 Web 工作台;没有浏览器或等待作答时断开连接,可回退到终端界面(TUI)。
40
+
41
+ ## 快速开始
42
+
43
+ 最低要求为 **Node.js 22.19.0**。已验证的环境基线是 Node.js 24、npm 11、Pi Coding Agent 0.84.0;CI 覆盖 Windows/Linux 与 Node.js 22/24。开始学习前,请确保 Pi 已配置可用模型。
44
+
45
+ ### 从源码运行
46
+
47
+ ```sh
48
+ git clone https://github.com/marskim1130/pi-learning.git
49
+ cd pi-learning
50
+ npm ci
51
+ npm run build:web
52
+ npm run pi
53
+ ```
54
+
55
+ 在启动的 Pi 会话中输入:
56
+
57
+ ```text
58
+ /learn 数据库设计 如何设计出一个好的数据库
59
+ ```
60
+
61
+ 也可以尝试:
62
+
63
+ ```text
64
+ /learn rust ownership and borrowing
65
+ /learn 算法 二分查找的边界条件
66
+ ```
67
+
68
+ 第一个词是课程,其余文本是主题。扩展会打开浏览器工作台;如果窗口未自动出现,在同一个 Pi 会话中运行 `/learn-open`。工作台链接带有会话 token,请勿公开分享。
69
+
70
+ ### 安装为 Pi 扩展包
71
+
72
+ 完成源码构建后,可将本地目录安装到 Pi:
73
+
74
+ ```sh
75
+ pi install /absolute/path/to/pi-learning
76
+ ```
77
+
78
+ 将示例路径替换为本机项目目录;路径含空格时使用引号。
79
+
80
+ 从 npm 安装预构建版本:
81
+
82
+ ```sh
83
+ pi install npm:pi-learning-agent@0.1.0
84
+ ```
85
+
86
+ 本项目采用 [Pi 官方包格式](https://pi.dev/docs/latest/packages)。当前支持预构建 npm 包与源码构建流程;Pi 0.84.0 的 Git 安装默认省略开发依赖,因此直接使用 `pi install git:...` 无法获得完整的 Web 工作台。
87
+
88
+ ## 常用命令
89
+
90
+ | 命令 | 用途 |
91
+ | --- | --- |
92
+ | `/learn <课程> <主题>` | 开始学习并启动导师教学 |
93
+ | `/learn-open` | 打开当前浏览器工作台 |
94
+ | `/learn-status` | 查看课程、主题、阶段和等待中的题目 |
95
+ | `/learn-stop` | 停止学习模式,取消等待中的交互 |
96
+ | `/learn-reset` | 确认后清空当前学习进度和题目资格,保留课程与主题 |
97
+
98
+ ## 工作原理与使用边界
99
+
100
+ 扩展向导师提供以下工具,四个作答工具顺序执行:
101
+
102
+ | 工具 | 作用 |
103
+ | --- | --- |
104
+ | `learning_ask_single_choice` | 单选题,返回 `optionId` 或结构化跳过结果 |
105
+ | `learning_ask_multi_choice` | 多选题,返回 `optionIds` 或结构化跳过结果 |
106
+ | `learning_ask_free_response` | 单行或多行文本回答 |
107
+ | `learning_ask_code` | 代码作答,返回 `language` 和 `code` |
108
+ | `learning_record_attempt` | 记录导师评估与必填讲解,在工作台显示判定和讲解,更新知识点掌握度 |
109
+
110
+ 回答直接结束对应的工具调用,不会被伪装成新的用户消息。跳过的题目不能计分,已完成评分的题目不能重复评分;重置、停止学习和切换 Pi 分支会释放旧交互。
111
+
112
+ - **代码评阅**:默认把代码交给导师评阅,不在主机执行。只有宿主显式注入 runner 时才开放支持语言的本地自测;该 runner 不构成沙箱。
113
+ - **编辑器能力**:Monaco 提供代码编辑界面,目前没有 Language Server。
114
+ - **进度保存**:学习状态保存在当前 Pi 分支,尚无跨会话长期学习档案;题目完成凭据保存在进程内,重新加载后不恢复未评分旧题的资格。
115
+ - **连接切换**:Web/TUI 断连切换采用轮询。
116
+
117
+ 完整的安全范围见 [SECURITY.md](SECURITY.md)。
118
+
119
+ ## 开发与验证
120
+
121
+ | 目录或文件 | 职责 |
122
+ | --- | --- |
123
+ | `web/` | React 工作台、作答组件、草稿和事件状态 |
124
+ | `shared/` | Extension 与 Web 共用的协议和学习状态类型 |
125
+ | `extension/learning-session.ts` | 评分资格、重置与会话生命周期 |
126
+ | `extension/server/` | 题目等待、HTTP/SSE、token 与静态资源 |
127
+ | `extension/tools/` | Pi 工具及 Web/TUI 呈现 |
128
+
129
+ ```sh
130
+ npm run check # 两端类型检查与日常测试
131
+ npm run test:workspace # 真实 Edge 工作台回归,虚构 API,不调用模型
132
+ npm run build:web # 构建浏览器工作台
133
+ npm run check:package # 构建、归档离线安装与 Pi/资源验收
134
+ npm run smoke # 真实 Pi RPC 加载,不调用模型
135
+ npm run check:release # 完整本地发布验收
136
+ npm run release:prepare # 验收后生成可发布 .tgz
137
+ ```
138
+
139
+ 真实模型端到端测试单独运行,会消耗模型额度:
140
+
141
+ ```sh
142
+ npm run e2e -- a --timeout 540
143
+ npm run e2e:browser
144
+ ```
145
+
146
+ 浏览器端到端测试当前要求 Microsoft Edge。[历史端到端报告](docs/e2e-report.md)记录的是对应阶段的验证结果。
147
+
148
+ `test:workspace` 独立验证真实 Monaco 的只读区移动、刷新恢复和重置后提交一致性,只启动临时本地服务,不需要模型配置。
149
+
150
+ 更多说明:[架构设计](docs/architecture.md) · [贡献指南](CONTRIBUTING.md) · [发布流程](docs/releasing.md) · [更新记录](CHANGELOG.md)。
151
+
152
+ ## 许可证
153
+
154
+ [MIT](LICENSE)。这是 Pi Coding Agent 的社区扩展。
package/SECURITY.md ADDED
@@ -0,0 +1,9 @@
1
+ # 安全说明
2
+
3
+ 本项目采用 Pi 扩展运行机制,扩展代码拥有宿主进程权限。本地 HTTP 工作台只监听 `127.0.0.1`,使用随机会话 token;不要分享包含 token 的工作台链接。
4
+
5
+ 本扩展默认关闭学习者代码执行。`LocalCodeRunner` 是供受控环境注入的实现:临时目录、环境白名单、超时和输出限制不构成沙箱,也不能阻止代码访问宿主文件、网络或启动其他进程。
6
+
7
+ 学习记录通过 Pi 的会话自定义条目保存。涉及问题诊断时,请先删除 token、模型凭据、个人学习内容和本地敏感路径。
8
+
9
+ 发现漏洞时,请优先使用仓库 GitHub 页面提供的私密漏洞报告入口(如果维护者已启用)。若该入口不可用,请先提交不含漏洞利用细节和敏感数据的问题,联系维护者确定私下沟通方式。项目尚未承诺固定的响应时间。
@@ -0,0 +1,75 @@
1
+ # 架构
2
+
3
+ 本项目以 Pi Extension 为运行入口,由本地 Web 工作台与 TUI 接收学习者作答。浏览器不启动模型,也不解析 Tutor 自然语言来猜测题型。
4
+
5
+ ## 模块与依赖
6
+
7
+ ```mermaid
8
+ flowchart TD
9
+ Pi[Pi 运行时] --> Entry[扩展装配]
10
+ Entry --> Commands[学习命令]
11
+ Entry --> Tools[学习工具]
12
+ Commands --> Session[LearningSession]
13
+ Tools --> Session
14
+ Session --> State[LearningStateStore]
15
+ Session --> Broker[InteractionBroker]
16
+ Tools --> Presenter[Web / TUI 呈现]
17
+ Presenter --> Broker
18
+ Broker --> Transport[本地 HTTP / SSE]
19
+ Transport --> Web[React 工作台]
20
+ State --> Persistence[Pi 分支快照]
21
+ Shared[shared 学习协议] -.类型.-> Tools
22
+ Shared -.类型.-> Transport
23
+ Shared -.类型.-> Web
24
+ ```
25
+
26
+ | 位置 | 职责 |
27
+ | --- | --- |
28
+ | shared/protocol.ts、shared/learning-state.ts | 题目、答案、事件与快照的唯一类型定义;无 Node、Pi 或 React 运行时依赖 |
29
+ | extension/learning-session.ts | 评分资格、掌握度、停止、重置与会话清理的协调规则 |
30
+ | extension/state/learning-state.ts | 掌握度算法、已计分账本、快照校验与状态变更 |
31
+ | extension/state/session-persistence.ts | 从当前 Pi 分支恢复最新有效 version=1 快照 |
32
+ | extension/server/interaction-broker.ts | 等待 Promise、答案校验、跳过、取消与有界完成凭据 |
33
+ | extension/server/learning-server.ts | loopback HTTP/SSE、token、请求体限制与包内静态资源 |
34
+ | extension/tools/ | Pi 参数与结果翻译,题目在 Web/TUI 间选择和回退 |
35
+ | web/src/state/store.ts | 连接生命周期、权威快照与事件重放、提交去重、Tutor 流式文本 |
36
+
37
+ ## 作答与评分
38
+
39
+ 四个作答工具顺序执行:呈现题目 → Broker 等待 → Web POST 或 TUI 输入 → 校验 → 返回结构化 tool result。只有用户主动执行的学习命令会发出教学启动消息。
40
+
41
+ 评分工具要求提供非空白的 `explanation`,说明正确答案及关键理由。成功结果将讲解保留在 details,同步层通过 `tutor.message` 将中文判定和讲解一起发送到工作台,因此模型只调用工具时也能显示反馈。讲解不改变掌握度算法或持久化快照;旧评分结果缺少讲解时,仅回退显示已有误区说明。修改加载到 Pi 扩展后用于后续评分,历史记录不会自动补回。
42
+
43
+ 状态提示对应真实教学阶段:仅 `learning_ask_*` 作答工具开始时发送 `waiting`,仅 `agent_settled` 发送 `idle`。评分开始不要求学习者作答,工具返回也不代表整轮讲解结束。
44
+
45
+ 完成凭据仅保留题型和是否跳过,最多 1,000 条,不留存整份代码答案。评分时由 LearningSession 检查题目已完成且没有跳过;LearningStateStore 检查是否重复计分。掌握度算法保持既有规则:初始 0.20、按证据形式调整、限制在 0..1,超过 0.75 需要近期连续正确中出现不同证据形式。
46
+
47
+ 重置学习时清空掌握度/计分账本与完成凭据,并取消等待。停止学习只取消等待、关闭学习模式,保留记录。
48
+
49
+ 切换课程或主题时,命令层先中止旧教学轮次并等待宿主空闲;会话层清除旧题等待和完成凭据后设置新目标。已保存的累计概念进度继续保留。评分证据必须与完成凭据的实际题型一致,错误参数不会消耗评分资格。
50
+
51
+ ## Pi 生命周期与持久化
52
+
53
+ session_start 与 session_tree 共用恢复路径:取消旧等待、清空进程内题目资格和旧学习状态,再从 ctx.sessionManager.getBranch() 恢复最新有效快照,最后刷新工作台。Pi 原生 /tree 不会把上一分支状态写入新分支。
54
+
55
+ 快照版本仍为 1,兼容缺少可选历史字段的旧数据。完成凭据不持久化;恢复进度不代表恢复旧题的计分资格。shutdown 取消等待并关闭 HTTP/SSE。
56
+
57
+ 关闭服务时同时终止活动 HTTP 连接,响应断开通过 AbortSignal 通知注入的 runner 取消。连接关闭不等于 runner 自身的异步资源清理已经完成。
58
+
59
+ ## 浏览器同步
60
+
61
+ 启动先读取 health 与学习快照,再连接 SSE。连接代次使停止后返回的旧请求失效;请求序号阻止旧刷新覆盖新刷新。
62
+
63
+ 重连使用服务器 pending 快照作为基准,重放请求期间的 presented/resolved/cancelled 和本地提交成功操作。因此既能清除断线时漏掉的旧题,也不会重新显示刚完成的题目。会话字段独立保留请求之后到达的更新。POST 与 SSE 到达顺序不会重复添加 transcript。
64
+
65
+ 提交记录在 SSE 到达时保留当前题干,POST 返回后补齐元数据。Markdown 与数学 token 共用一次解析,统一进行 HTML 消毒,保留代码转义及原段落/列表结构。
66
+
67
+ Monaco 只读区域跟随装饰范围移动;范围和对应代码作为伴随草稿元数据保存,恢复时检查代码一致及范围有效。旧纯文本草稿仍可读取,缺少有效元数据时沿用题目原始范围。Reset 直接恢复编辑器模型并重建原始只读区,避免显示代码与提交内容分离。
68
+
69
+ ## 分发与验证
70
+
71
+ pi.extensions 定位入口,files 包含 extension、shared、构建后的 Web 和维护文档。Pi 内置库使用 peerDependencies,浏览器依赖打入静态资源。
72
+
73
+ 正式支持 npm 预构建包与本地源码构建。Pi 0.84.0 的 Git 安装省略 devDependencies,不承诺直接 Git 安装能构建 Web。本地 runner 的限制见 SECURITY.md。
74
+
75
+ 日常测试与归档检查分别运行。公开工具/命令、分支恢复、HTTP/SSE 往返、浏览器输入与同步有自动测试;发布检查真实离线安装 tarball,经 Pi 加载包内入口并验证首页、脚本和 Monaco worker。真实模型 E2E 保持独立。
@@ -0,0 +1,51 @@
1
+ # 开源发布流程
2
+
3
+ 本项目是社区扩展,采用 [Pi 官方包格式](https://pi.dev/docs/latest/packages)。符合格式不代表官方维护或已经在包目录展示。兼容验证基线为 Pi 0.84.0。
4
+
5
+ ## 发布结构
6
+
7
+ package.json.pi.extensions 指向 extension/index.ts,pi-package 关键字用于发现。Pi 核心库与 typebox 使用 peerDependencies: "*";开发依赖实际版本由根锁文件固定。
8
+
9
+ 归档包含 extension、shared、web/dist、README、MIT LICENSE 和维护文档。Monaco、worker、Markdown/KaTeX 资源本地打包,安装者不需要前端源码或构建工具。
10
+
11
+ prepack 保证普通 npm pack 在打包前构建。支持 npm 包与已构建本地源码;不提供直接 Pi Git 安装支持,因为 Pi 0.84.0 默认运行 npm install --omit=dev。
12
+
13
+ ## 发布准备
14
+
15
+ 在干净检出中运行:
16
+
17
+ ```sh
18
+ npm ci
19
+ npm run release:prepare
20
+ ```
21
+
22
+ release:prepare 依次执行:
23
+
24
+ 1. Extension/Web 类型检查与日常测试。
25
+ 2. Web 构建和真实 tarball 验收:离线安装、Pi 入口、五个工具与五个命令、首页、JavaScript 和 Monaco worker。
26
+ 3. 真实 Pi RPC smoke,不调用模型。
27
+ 4. 使用已验证产物生成根目录 pi-learning-agent-<version>.tgz,最后一步不再构建。
28
+
29
+ 日常开发可分别运行 npm run check、npm run check:package 与 npm run smoke。Windows/Linux、Node.js 22/24 的 CI 配置位于 .github/workflows/ci.yml;本机运行不代表远程矩阵已经执行。
30
+
31
+ 真实模型 E2E 见 CONTRIBUTING.md;历史报告不能代替当前版本验收。
32
+
33
+ ## 发布
34
+
35
+ 维护者核对 npm 包名所有权、版本、仓库地址和全部结果后,发布生成的那个文件:
36
+
37
+ ```sh
38
+ npm publish ./pi-learning-agent-<version>.tgz --access public
39
+ ```
40
+
41
+ 将 <version> 替换为实际版本。此步骤需要发布者 npm 权限,准备脚本不会执行它。发布后在干净 Pi 环境验证:
42
+
43
+ ```sh
44
+ pi install npm:pi-learning-agent@<version>
45
+ ```
46
+
47
+ 再验证学习启动、作答、停止与分支恢复,并实际检查包目录是否展示该版本。
48
+
49
+ ## 撤回
50
+
51
+ 发布前可删除归档并撤回代码。发布后采用新的修补版本修正;用户可显式安装此前可用版本。不要覆盖已发布版本或未经影响评估执行 npm unpublish。
@@ -0,0 +1,181 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext
4
+ } from "@earendil-works/pi-coding-agent";
5
+
6
+ import type { InteractionBroker } from "./server/interaction-broker.js";
7
+ import type { LearningServer } from "./server/learning-server.js";
8
+ import type { LearningStateStore } from "./state/learning-state.js";
9
+ import type { StartLearningInput } from "./state/learning-state.js";
10
+ import type { LearningSession } from "./learning-session.js";
11
+ import { openWorkspace as defaultOpenWorkspace } from "./utils/browser.js";
12
+
13
+ export interface LearningCommandDependencies {
14
+ session: LearningSession;
15
+ server: LearningServer;
16
+ /** Injectable for tests; defaults to the system browser opener. */
17
+ openWorkspace?: (url: string) => Promise<void>;
18
+ }
19
+
20
+ export function registerLearningCommands(
21
+ pi: ExtensionAPI,
22
+ dependencies: LearningCommandDependencies
23
+ ): void {
24
+ const { session, server } = dependencies;
25
+ const { state, broker } = session;
26
+ const openWorkspace = dependencies.openWorkspace ?? defaultOpenWorkspace;
27
+
28
+ pi.registerCommand("learn", {
29
+ description: "Start a structured learning session: /learn <course> <topic>",
30
+ handler: async (args, ctx) => {
31
+ const target = parseLearningTarget(args);
32
+ if (target === undefined) {
33
+ ctx.ui.notify("Usage: /learn <course> <topic>", "warning");
34
+ return;
35
+ }
36
+
37
+ if (!session.isCurrentTarget(target) && !ctx.isIdle()) {
38
+ ctx.abort();
39
+ await ctx.waitForIdle();
40
+ }
41
+ session.start(target);
42
+ pi.setSessionName(
43
+ `Learn: ${target.course.title} - ${target.topic.title}`
44
+ );
45
+ const workspaceUrl = await ensureServerStarted(server, ctx);
46
+ if (workspaceUrl !== undefined) {
47
+ await openWorkspace(workspaceUrl);
48
+ }
49
+ const workspaceLine =
50
+ workspaceUrl === undefined ? "" : `\nWorkspace: ${workspaceUrl}`;
51
+ ctx.ui.notify(
52
+ `Learning Mode enabled: ${target.course.title} / ${target.topic.title}${workspaceLine}`,
53
+ "info"
54
+ );
55
+
56
+ const kickoff = [
57
+ `Begin a structured learning session for ${target.course.title} / ${target.topic.title}.`,
58
+ "Start with a concise diagnostic and teach interactively.",
59
+ "Use the registered learning_* tools whenever the learner must respond."
60
+ ].join(" ");
61
+ if (ctx.isIdle()) {
62
+ pi.sendUserMessage(kickoff);
63
+ } else {
64
+ pi.sendUserMessage(kickoff, { deliverAs: "followUp" });
65
+ }
66
+ }
67
+ });
68
+
69
+ pi.registerCommand("learn-open", {
70
+ description: "Open the learning workspace in a browser",
71
+ handler: async (_args, ctx) => {
72
+ const url = await ensureServerStarted(server, ctx);
73
+ if (url === undefined) {
74
+ return;
75
+ }
76
+ await openWorkspace(url);
77
+ ctx.ui.notify(`Learning workspace: ${url}`, "info");
78
+ }
79
+ });
80
+
81
+ pi.registerCommand("learn-status", {
82
+ description: "Show the current learning session status",
83
+ handler: async (_args, ctx) => {
84
+ ctx.ui.notify(formatLearningStatus(state, broker), "info");
85
+ }
86
+ });
87
+
88
+ pi.registerCommand("learn-stop", {
89
+ description: "Stop the current learning session",
90
+ handler: async (_args, ctx) => {
91
+ session.stop();
92
+ ctx.ui.notify("Learning Mode disabled.", "info");
93
+ }
94
+ });
95
+
96
+ pi.registerCommand("learn-reset", {
97
+ description: "Reset the current topic's learner state (requires confirmation)",
98
+ handler: async (_args, ctx) => {
99
+ const snapshot = state.snapshot();
100
+ if (!snapshot.enabled || snapshot.topic === undefined) {
101
+ ctx.ui.notify("No active learning topic to reset.", "warning");
102
+ return;
103
+ }
104
+ const confirmed = await ctx.ui.confirm(
105
+ "Reset learner state?",
106
+ `This clears all concepts, mastery and attempt history for "${snapshot.topic.title}". It cannot be undone.`
107
+ );
108
+ if (!confirmed) {
109
+ ctx.ui.notify("Reset cancelled.", "info");
110
+ return;
111
+ }
112
+ session.resetTopic();
113
+ ctx.ui.notify(
114
+ `Learner state for "${snapshot.topic.title}" has been reset.`,
115
+ "info"
116
+ );
117
+ }
118
+ });
119
+ }
120
+
121
+ export function formatLearningStatus(
122
+ state: LearningStateStore,
123
+ broker: InteractionBroker
124
+ ): string {
125
+ const snapshot = state.snapshot();
126
+ const course = snapshot.course?.title ?? "none";
127
+ const topic = snapshot.topic?.title ?? "none";
128
+ const pending = broker.getPending();
129
+ return [
130
+ `Learning Mode: ${snapshot.enabled ? "ON" : "OFF"}`,
131
+ `Course: ${course}`,
132
+ `Topic: ${topic}`,
133
+ `Phase: ${snapshot.phase}`,
134
+ `Pending interaction: ${pending[0]?.id ?? "none"}`,
135
+ `Concepts: ${Object.keys(snapshot.concepts).length}`
136
+ ].join("\n");
137
+ }
138
+
139
+ export function parseLearningTarget(args: string): StartLearningInput | undefined {
140
+ const parts = args.trim().split(/\s+/u).filter(Boolean);
141
+ const courseName = parts[0];
142
+ const topicParts = parts.slice(1);
143
+ if (courseName === undefined || topicParts.length === 0) {
144
+ return undefined;
145
+ }
146
+
147
+ const topicName = topicParts.join(" ");
148
+ return {
149
+ course: { id: toId(courseName), title: toTitle(courseName) },
150
+ topic: { id: toId(topicName), title: toTitle(topicName) }
151
+ };
152
+ }
153
+
154
+ function toId(value: string): string {
155
+ return value.trim().toLocaleLowerCase().replace(/\s+/gu, "-");
156
+ }
157
+
158
+ /** Start the workspace server (lazy) and return its URL, or undefined on failure. */
159
+ async function ensureServerStarted(
160
+ server: LearningServer,
161
+ ctx: ExtensionCommandContext
162
+ ): Promise<string | undefined> {
163
+ try {
164
+ await server.start();
165
+ return server.url();
166
+ } catch (error) {
167
+ ctx.ui.notify(
168
+ `Learning workspace server failed to start: ${String(error)}`,
169
+ "warning"
170
+ );
171
+ return undefined;
172
+ }
173
+ }
174
+
175
+ function toTitle(value: string): string {
176
+ return value
177
+ .trim()
178
+ .split(/\s+/u)
179
+ .map((part) => `${part.slice(0, 1).toLocaleUpperCase()}${part.slice(1)}`)
180
+ .join(" ");
181
+ }
@@ -0,0 +1,50 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { registerLearningCommands } from "./commands.js";
4
+ import { LearningSession } from "./learning-session.js";
5
+ import { InteractionBroker } from "./server/interaction-broker.js";
6
+ import { LearningServer } from "./server/learning-server.js";
7
+ import { LearningStateStore } from "./state/learning-state.js";
8
+ import {
9
+ LEARNING_STATE_ENTRY_TYPE,
10
+ LEARNING_STATE_VERSION,
11
+ registerLearningStatePersistence
12
+ } from "./state/session-persistence.js";
13
+ import { registerLearningTools } from "./tools/index.js";
14
+ import { registerTranscriptSync } from "./transcript-sync.js";
15
+ import { registerTutorPrompt } from "./tutor-prompt.js";
16
+
17
+ export default function learningExtension(pi: ExtensionAPI): void {
18
+ const broker = new InteractionBroker();
19
+ // server 在 onChange 首次触发(/learn、record_attempt)前同步完成构造。
20
+ let server: LearningServer;
21
+ const state = new LearningStateStore({
22
+ // 任何 state 变化(/learn、/learn-stop、learning_record_attempt)后广播。
23
+ // server 可能未 start:broadcastSessionUpdated 在无 SSE 客户端时安全 no-op。
24
+ onChange: (snapshot) => {
25
+ server.broadcastSessionUpdated();
26
+ pi.appendEntry(LEARNING_STATE_ENTRY_TYPE, {
27
+ version: LEARNING_STATE_VERSION,
28
+ state: snapshot
29
+ });
30
+ }
31
+ });
32
+ server = new LearningServer({ broker, state });
33
+ const session = new LearningSession(state, broker);
34
+
35
+ // Spec 15: a presented question migrates explaining → checking.
36
+ broker.subscribe({
37
+ onPresented: () => state.questionPresented()
38
+ });
39
+
40
+ registerLearningTools(pi, { session, server });
41
+ registerLearningCommands(pi, { session, server });
42
+ registerTutorPrompt(pi, { state });
43
+ registerTranscriptSync(pi, server);
44
+ registerLearningStatePersistence(pi, session, () => server.broadcastSessionUpdated());
45
+
46
+ pi.on("session_shutdown", async () => {
47
+ broker.cancelAll("session_shutdown");
48
+ await server.close();
49
+ });
50
+ }