oh-my-agent-web 0.2.0 → 0.4.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 (250) hide show
  1. package/README.md +86 -447
  2. package/dist/server/approval/read-only.js +107 -0
  3. package/dist/server/approval/rules.js +85 -0
  4. package/dist/server/approval/scope.js +150 -0
  5. package/dist/server/brand.js +2 -0
  6. package/dist/server/cli-config.js +52 -0
  7. package/dist/server/cli-info.js +101 -0
  8. package/dist/server/cli.js +304 -0
  9. package/dist/server/engine/builtin.js +61 -0
  10. package/dist/server/engine/catalog.js +8 -0
  11. package/dist/server/engine/codex-approvals.js +45 -0
  12. package/dist/server/engine/codex-map.js +298 -0
  13. package/dist/server/engine/codex-sessions.js +159 -0
  14. package/dist/server/engine/codex-transport.js +49 -0
  15. package/dist/server/engine/codex.js +314 -0
  16. package/dist/server/engine/dsh-history.js +195 -0
  17. package/dist/server/engine/dsh-log.js +245 -0
  18. package/dist/server/engine/dsh-map.js +221 -0
  19. package/dist/server/engine/dsh-sessions.js +207 -0
  20. package/dist/server/engine/dsh-transport.js +37 -0
  21. package/dist/server/engine/dsh.js +284 -0
  22. package/dist/server/engine/fake.js +231 -0
  23. package/dist/server/engine/jsonrpc-stdio.js +241 -0
  24. package/dist/server/engine/model-registry-reload.js +115 -0
  25. package/dist/server/engine/pi-events.js +148 -0
  26. package/dist/server/engine/pi-rename.js +14 -0
  27. package/dist/server/engine/pi-sdk.js +343 -0
  28. package/dist/server/engine/pi-session-model.js +23 -0
  29. package/dist/server/engine/pi-thinking.js +34 -0
  30. package/dist/server/engine/pi-transcript.js +225 -0
  31. package/dist/server/engine/pi-version.js +11 -0
  32. package/dist/server/engine/port.js +1 -0
  33. package/dist/server/engine/registry.js +47 -0
  34. package/dist/server/engine/scratch.js +13 -0
  35. package/dist/server/engine/suggestion-guidance.js +9 -0
  36. package/dist/server/engine/switchable.js +22 -0
  37. package/dist/server/engine/wiring.js +107 -0
  38. package/dist/server/files/dir-routes.js +40 -0
  39. package/dist/server/files/file-search.js +63 -0
  40. package/dist/server/files/file-service.js +232 -0
  41. package/dist/server/files/files-routes.js +182 -0
  42. package/dist/server/files/git-info.js +186 -0
  43. package/dist/server/http/auth-routes.js +99 -0
  44. package/dist/server/http/auth.js +191 -0
  45. package/dist/server/http/aux-routes.js +24 -0
  46. package/dist/server/http/compress.js +75 -0
  47. package/dist/server/http/health-and-static.js +33 -0
  48. package/dist/server/http/live-routes.js +113 -0
  49. package/dist/server/http/login-page.js +200 -0
  50. package/dist/server/index.js +342 -520
  51. package/dist/server/markers/actions.js +262 -0
  52. package/dist/server/protocol-parse.js +201 -0
  53. package/dist/server/protocol-settings.js +1 -0
  54. package/dist/server/protocol.js +6 -0
  55. package/dist/server/providers/catalog-store.js +85 -0
  56. package/dist/server/providers/catalog-writes.js +197 -0
  57. package/dist/server/providers/catalog.js +77 -0
  58. package/dist/server/providers/enabled-models.js +118 -0
  59. package/dist/server/providers/import-models.js +69 -0
  60. package/dist/server/providers/import-routes.js +136 -0
  61. package/dist/server/providers/model-test.js +52 -0
  62. package/dist/server/providers/probe.js +64 -0
  63. package/dist/server/providers/provider-handlers.js +203 -0
  64. package/dist/server/providers/provider-store.js +327 -0
  65. package/dist/server/providers/request-headers.js +7 -0
  66. package/dist/server/providers/validate.js +241 -0
  67. package/dist/server/push/push-routes.js +114 -0
  68. package/dist/server/push/push.js +120 -0
  69. package/dist/server/session/approval-bridge.js +103 -0
  70. package/dist/server/session/attachment-resolve.js +21 -0
  71. package/dist/server/session/client-view.js +82 -0
  72. package/dist/server/session/conversation-create.js +65 -0
  73. package/dist/server/session/delete-session.js +84 -0
  74. package/dist/server/session/engine-switch.js +108 -0
  75. package/dist/server/session/lock.js +82 -0
  76. package/dist/server/session/model-controller.js +111 -0
  77. package/dist/server/session/notices.js +31 -0
  78. package/dist/server/session/pi-opener.js +123 -0
  79. package/dist/server/session/pi-refresh.js +51 -0
  80. package/dist/server/session/pi-sessions-provider.js +46 -0
  81. package/dist/server/session/pi-sessions.js +272 -0
  82. package/dist/server/session/refresh-actions.js +42 -0
  83. package/dist/server/session/rename-session.js +23 -0
  84. package/dist/server/session/runtime-services.js +28 -0
  85. package/dist/server/session/runtime-types.js +1 -0
  86. package/dist/server/session/runtime.js +358 -0
  87. package/dist/server/session/session-stats.js +88 -0
  88. package/dist/server/session/settings.js +367 -0
  89. package/dist/server/session/snapshot-for.js +7 -0
  90. package/dist/server/session/stats-refresher.js +50 -0
  91. package/dist/server/session/switch-conversation.js +86 -0
  92. package/dist/server/session/throttle.js +14 -0
  93. package/dist/server/session/turn-events.js +126 -0
  94. package/dist/server/session/turn-finish.js +79 -0
  95. package/dist/server/session/turn-markers.js +26 -0
  96. package/dist/server/session/turn-queue.js +111 -0
  97. package/dist/server/session/turn-send.js +88 -0
  98. package/dist/server/session/turn-watchdog.js +77 -0
  99. package/dist/server/sessions/session-routes.js +17 -0
  100. package/dist/server/skills/skill-routes.js +28 -0
  101. package/dist/server/skills/skill-store.js +134 -0
  102. package/dist/server/snapshot/fan-out.js +10 -0
  103. package/dist/server/snapshot/messages.js +75 -0
  104. package/dist/server/snapshot/sender.js +62 -0
  105. package/dist/server/snapshot/state.js +193 -0
  106. package/dist/server/terminal/terminal-routes.js +176 -0
  107. package/dist/server/terminal/terminal-session.js +140 -0
  108. package/dist/server/uploads/upload-routes.js +64 -0
  109. package/dist/server/uploads/upload-store.js +115 -0
  110. package/dist/server/util/data-dir.js +36 -0
  111. package/dist/server/util/env.js +140 -0
  112. package/dist/server/util/ids.js +28 -0
  113. package/dist/server/util/log-file.js +67 -0
  114. package/dist/server/util/log.js +52 -0
  115. package/dist/server/util/origin.js +75 -0
  116. package/dist/server/util/paths.js +60 -0
  117. package/dist/server/util/pricing-index.js +121 -0
  118. package/dist/server/util/pricing.js +214 -0
  119. package/dist/server/util/startup.js +34 -0
  120. package/dist/server/util/upstream-version.js +140 -0
  121. package/dist/server/util/usage.js +19 -0
  122. package/dist/server/util/version.js +50 -0
  123. package/dist/server/ws/handle-message.js +95 -0
  124. package/package.json +49 -63
  125. package/web/dist/apple-touch-icon-precomposed.png +0 -0
  126. package/web/dist/apple-touch-icon.png +0 -0
  127. package/web/dist/assets/abnfDiagram-WEYOIA3C-NLp09iEC.js +1 -0
  128. package/web/dist/assets/arc-DviL9o3c.js +1 -0
  129. package/web/dist/assets/architecture-WOLXFQ4H-fjsM_Sy3.js +1 -0
  130. package/web/dist/assets/architectureDiagram-ZHVYKT53-Cn3B4TE8.js +36 -0
  131. package/web/dist/assets/array-BifhSqXX.js +1 -0
  132. package/web/dist/assets/blockDiagram-RJAE7QA3-CIuvxWvi.js +140 -0
  133. package/web/dist/assets/c4Diagram-XDPPPE54-DvdKmEpF.js +38 -0
  134. package/web/dist/assets/channel-C3AQ6nAs.js +1 -0
  135. package/web/dist/assets/chunk-2E7WHVMD-HNNASGCS.js +88 -0
  136. package/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  137. package/web/dist/assets/chunk-4EMNKR2T-Cg-eRwBE.js +1 -0
  138. package/web/dist/assets/chunk-55XCU76K-BweiisZL.js +1 -0
  139. package/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  140. package/web/dist/assets/chunk-7S6XSTFO-BCzIiP-6.js +2 -0
  141. package/web/dist/assets/chunk-AQDTSKZB-Dl0NRFuU.js +72 -0
  142. package/web/dist/assets/chunk-DSNLSHJZ-zhHCjjN9.js +129 -0
  143. package/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  144. package/web/dist/assets/chunk-HAGFOTED-BDQqU2EH.js +62 -0
  145. package/web/dist/assets/chunk-HNE4Q36P-B-qRzMjX.js +1 -0
  146. package/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  147. package/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  148. package/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  149. package/web/dist/assets/chunk-NQOKRVIF-CNcvhMn_.js +213 -0
  150. package/web/dist/assets/chunk-UHW7PFBP-Dwr8LUzr.js +1 -0
  151. package/web/dist/assets/chunk-VQ5W62QI-O8sN6jJu.js +1 -0
  152. package/web/dist/assets/chunk-WQQL4CDK-B6-59Zg2.js +10 -0
  153. package/web/dist/assets/chunk-XXDRQBXY-DvSbeUlZ.js +1 -0
  154. package/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  155. package/web/dist/assets/chunk-YZO67HFG-BXpKYKsw.js +1 -0
  156. package/web/dist/assets/chunk-ZAUX2LTX-B1Z5VSkz.js +1 -0
  157. package/web/dist/assets/chunk-ZCHMQSOR-h_5mBLIA.js +2 -0
  158. package/web/dist/assets/classDiagram-v2-4MYWR3UA-Dr6JnaaV.js +217 -0
  159. package/web/dist/assets/cose-bilkent-JH36ORCC-36ShkR7R.js +1 -0
  160. package/web/dist/assets/cynefin-EF2NZ3EQ-Dg6bI7pC.js +1 -0
  161. package/web/dist/assets/cynefinDiagram-WF24UDRP-B0YHpBlR.js +62 -0
  162. package/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  163. package/web/dist/assets/dagre-NK4GIZVX-Cx3mn7YS.js +4 -0
  164. package/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  165. package/web/dist/assets/diagram-CBK3MWTC-l8aZG_Bm.js +24 -0
  166. package/web/dist/assets/diagram-I5XIIV2D-Bd5f14_h.js +24 -0
  167. package/web/dist/assets/diagram-ODIL2UCW-hBsxCYUj.js +30 -0
  168. package/web/dist/assets/diagram-PFBEJUXE-BgpIBUYl.js +3 -0
  169. package/web/dist/assets/diagram-RJA3I4VV-CDU1gUVD.js +41 -0
  170. package/web/dist/assets/diagram-TYUNFSNR-Iuxh7Tqf.js +200 -0
  171. package/web/dist/assets/dist-C86cKTuE.js +1 -0
  172. package/web/dist/assets/ebnfDiagram-V4P3TTPT-Cms0HFKh.js +1 -0
  173. package/web/dist/assets/elk-QONTBCTQ-B7yEy0PQ.js +27 -0
  174. package/web/dist/assets/erDiagram-KYQIGQAA-CL48j4aU.js +99 -0
  175. package/web/dist/assets/eventmodeling-K75KTNOO-CFvkaejh.js +1 -0
  176. package/web/dist/assets/flowDiagram-IUGLLWBU-6UyKXpqG.js +1 -0
  177. package/web/dist/assets/ganttDiagram-R7GPNTJY-vdMr-0ZV.js +292 -0
  178. package/web/dist/assets/gitGraph-VSP46ZUC-2z85Wuwo.js +1 -0
  179. package/web/dist/assets/gitGraphDiagram-PDYL7XHY-BY4GPiqC.js +106 -0
  180. package/web/dist/assets/graphlib-DS17s2tU.js +1 -0
  181. package/web/dist/assets/index-Bjn56a2i.js +76 -0
  182. package/web/dist/assets/index-Drp6e5iU.css +1 -0
  183. package/web/dist/assets/info-OHQRW6UA-ECzkYTx-.js +1 -0
  184. package/web/dist/assets/infoDiagram-MVDRKAX5-DDk5EAeb.js +2 -0
  185. package/web/dist/assets/init-C-OQMol4.js +1 -0
  186. package/web/dist/assets/ishikawaDiagram-HPEHV2GE-9yPH4J94.js +70 -0
  187. package/web/dist/assets/journeyDiagram-BOSHW7O7-BPTIap3l.js +139 -0
  188. package/web/dist/assets/kanban-definition-FNSA4HC5-BbADHrA8.js +89 -0
  189. package/web/dist/assets/katex-B_JcIwqj.js +257 -0
  190. package/web/dist/assets/line-CHV9_4lL.js +1 -0
  191. package/web/dist/assets/linear-C-eZsBTv.js +1 -0
  192. package/web/dist/assets/mermaid-parser.core-BiAhX1l1.js +7 -0
  193. package/web/dist/assets/mermaid.core-BxP_A_zj.js +44 -0
  194. package/web/dist/assets/mindmap-definition-6OY57UG3-BMWlit95.js +98 -0
  195. package/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  196. package/web/dist/assets/packet-JDAUHWVQ-FVApuKOp.js +1 -0
  197. package/web/dist/assets/path-fybaL0A-.js +1 -0
  198. package/web/dist/assets/pegDiagram-ECIUJNBX-WjWJvDHM.js +1 -0
  199. package/web/dist/assets/pie-XZMESJXO-BGCZnvhp.js +1 -0
  200. package/web/dist/assets/pieDiagram-GNMPY74T-DJpAC_JC.js +39 -0
  201. package/web/dist/assets/quadrantDiagram-D57K4MC3-hxx5c7oq.js +7 -0
  202. package/web/dist/assets/radar-ABXABTNO-BU9Shc-O.js +1 -0
  203. package/web/dist/assets/railroad-I3PHUGI6-pWj8d-G4.js +1 -0
  204. package/web/dist/assets/railroad-abnf-LNEFI6M7-D7HZBTAj.js +1 -0
  205. package/web/dist/assets/railroad-ebnf-DWYD2UWJ-CKiXJSuM.js +1 -0
  206. package/web/dist/assets/railroad-peg-JR7BVNR7-DB3VJHgR.js +1 -0
  207. package/web/dist/assets/railroadDiagram-Y72NRBL7-Cev7Y65k.js +1 -0
  208. package/web/dist/assets/requirementDiagram-EQYSW5RA-CqbwMLD8.js +84 -0
  209. package/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  210. package/web/dist/assets/sankeyDiagram-SY24JHO5-V2cyVi8q.js +40 -0
  211. package/web/dist/assets/sequenceDiagram-BLEHNHQU-By6NJOPM.js +169 -0
  212. package/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  213. package/web/dist/assets/src-Da91fG3g.js +1 -0
  214. package/web/dist/assets/stateDiagram-v2-A6IESKWZ-BnPGX5UI.js +291 -0
  215. package/web/dist/assets/swimlanes-B3ENRKFQ-C58BpIpm.js +1 -0
  216. package/web/dist/assets/swimlanesDiagram-AYQRV4AK-RaivxSsc.js +8 -0
  217. package/web/dist/assets/timeline-definition-JXYKGLGB-Hgo1DhnS.js +120 -0
  218. package/web/dist/assets/treeView-D4JQ5SDB-DF0dfjqB.js +1 -0
  219. package/web/dist/assets/treemap-SAJKECNS-CSSM5FqB.js +1 -0
  220. package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkBBD1NY.js +328 -0
  221. package/web/dist/assets/vennDiagram-Q3Y3IDCW-Bm28keX_.js +34 -0
  222. package/web/dist/assets/wardley-7MLQ67FV-By7KgEQX.js +1 -0
  223. package/web/dist/assets/wardleyDiagram-IVAIDKWA-oVtIrnGa.js +78 -0
  224. package/web/dist/assets/xychartDiagram-4URNHDU4-aI8FH4g2.js +7 -0
  225. package/web/dist/favicon-32.png +0 -0
  226. package/web/dist/favicon.ico +0 -0
  227. package/web/dist/favicon.svg +20 -8
  228. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  229. package/web/dist/icons/icon-192.png +0 -0
  230. package/web/dist/icons/icon-512.png +0 -0
  231. package/web/dist/icons/icon-maskable-512.png +0 -0
  232. package/web/dist/index.html +21 -27
  233. package/web/dist/manifest.webmanifest +17 -0
  234. package/web/dist/maskable.svg +25 -0
  235. package/web/dist/sw.js +40 -0
  236. package/bin/oh-my-agent-web-ps.mjs +0 -203
  237. package/bin/oh-my-agent-web-ws.mjs +0 -593
  238. package/bin/oh-my-agent-web.mjs +0 -321
  239. package/bin/update-check.mjs +0 -142
  240. package/dist/server/codex-client.js +0 -310
  241. package/dist/server/codex-supervisor.js +0 -446
  242. package/dist/server/fs-service.js +0 -144
  243. package/dist/server/request-inspector.js +0 -458
  244. package/dist/shared/protocol.js +0 -112
  245. package/scripts/manage-server.mjs +0 -155
  246. package/scripts/omaw.mjs +0 -74
  247. package/shared/protocol.ts +0 -228
  248. package/web/dist/assets/index-B8-VzDf2.css +0 -1
  249. package/web/dist/assets/index-DeC2eiiq.js +0 -42
  250. package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
@@ -0,0 +1,343 @@
1
+ /**
2
+ * 真实引擎:用 pi SDK 实现「引擎端口」(ADR-0003)。
3
+ *
4
+ * ⚠️ 这是**唯一**允许 import `@earendil-works/*` 的地方(守卫 `check-engine-imports.mjs` 强制)。
5
+ * 业务代码只见 `port.ts` 里的类型。
6
+ *
7
+ * 与 SDK 的对接方式(M3 侦察结论):
8
+ */
9
+ import { DefaultResourceLoader, SessionManager, createAgentSession, getAgentDir, } from "@earendil-works/pi-coding-agent";
10
+ import { classifyScope } from "../approval/scope.js";
11
+ import { mapSessionEvent } from "./pi-events.js";
12
+ import { readSessionModel } from "./pi-session-model.js";
13
+ import { entriesToUiMessages } from "./pi-transcript.js";
14
+ function asRecord(value) {
15
+ return typeof value === "object" && value !== null ? value : {};
16
+ }
17
+ /** 教模型怎么写「下一步建议」(ADR-0022):刻意写得短(每次请求都付 token) */
18
+ import { reloadModelRegistry } from "./model-registry-reload.js";
19
+ import { SUGGESTION_GUIDANCE } from "./suggestion-guidance.js";
20
+ export { SUGGESTION_GUIDANCE };
21
+ import { createPiThinking } from "./pi-thinking.js";
22
+ import { piSdkVersion } from "./pi-version.js";
23
+ import { renameSessionIn } from "./pi-rename.js";
24
+ export function createPiSdkEngine(options = {}) {
25
+ const sessions = new Map();
26
+ const thinking = createPiThinking(sessions);
27
+ let approvalHandler = null;
28
+ const notice = (text, level = "warn") => options.onNotice?.(text, level);
29
+ /** 审批用的 inline 扩展:拦在工具真正执行之前(SDK 的公开拦截点) */
30
+ function makeApprovalExtension(workspace) {
31
+ return {
32
+ name: "agent-web-approval",
33
+ hidden: true,
34
+ factory: (pi) => {
35
+ pi.on("tool_call", async (event) => {
36
+ const record = asRecord(event);
37
+ const scope = classifyScope({
38
+ toolName: String(record.toolName ?? ""),
39
+ args: record.input,
40
+ workspace,
41
+ });
42
+ if (!scope.dangerous)
43
+ return undefined; // 只读工具不问
44
+ const decision = approvalHandler
45
+ ? await approvalHandler({
46
+ toolName: scope.toolName,
47
+ argsSummary: scope.argsSummary,
48
+ scopeKey: scope.scopeKey,
49
+ scopeHint: scope.scopeHint,
50
+ riskNote: scope.riskNote,
51
+ })
52
+ : "deny";
53
+ if (decision === "deny") {
54
+ return { block: true, reason: "用户拒绝了这次操作(不要在未获批准时重试同一操作)" };
55
+ }
56
+ return undefined;
57
+ });
58
+ },
59
+ };
60
+ }
61
+ /** 建 resourceLoader 并拉起一个 AgentSession(createSession 与 openSession 共用) */
62
+ async function startSession(sessionId, workspace, sessionManager, sessionPath = null) {
63
+ const agentDir = options.agentDir ?? getAgentDir();
64
+ const resourceLoader = new DefaultResourceLoader({
65
+ cwd: workspace,
66
+ agentDir,
67
+ extensionFactories: [makeApprovalExtension(workspace)],
68
+ // 教模型「下一步建议」的写法(ADR-0022):纯文本标记,不需要调用任何工具
69
+ appendSystemPrompt: [SUGGESTION_GUIDANCE],
70
+ });
71
+ await resourceLoader.reload();
72
+ const { session } = await createAgentSession({
73
+ cwd: workspace,
74
+ agentDir,
75
+ resourceLoader,
76
+ // ADR-0021 §2:转录的事实源就是 SDK 自己的会话文件(新建或打开的那个)
77
+ sessionManager,
78
+ });
79
+ const subscriptions = new Set();
80
+ const handle = {
81
+ session: session,
82
+ workspace,
83
+ unsubscribe: () => {
84
+ for (const off of subscriptions)
85
+ off();
86
+ subscriptions.clear();
87
+ },
88
+ subscriptions,
89
+ aborting: false,
90
+ sessionManager: sessionManager,
91
+ sessionPath: sessionPath ?? sessionManager.getSessionFile() ?? null,
92
+ };
93
+ sessions.set(sessionId, handle);
94
+ return handle;
95
+ }
96
+ const PREWARM_ID = "__prewarm__";
97
+ async function warmUp() {
98
+ if (sessions.has(PREWARM_ID))
99
+ return;
100
+ try {
101
+ await startSession(PREWARM_ID, process.cwd(), SessionManager.inMemory());
102
+ }
103
+ catch (error) {
104
+ options.onNotice?.(`模型表预热失败:${error instanceof Error ? error.message : String(error)}`, "warn");
105
+ }
106
+ }
107
+ return {
108
+ /** 预热:pi 的 modelRuntime 挂在会话上,没会话就列不出模型(内存会话,不落盘) */
109
+ async prewarm() {
110
+ if (sessions.has(PREWARM_ID))
111
+ return;
112
+ try {
113
+ await startSession(PREWARM_ID, process.cwd(), SessionManager.inMemory());
114
+ }
115
+ catch (error) {
116
+ options.onNotice?.(`模型表预热失败:${error instanceof Error ? error.message : String(error)}`, "warn");
117
+ }
118
+ },
119
+ version: piSdkVersion,
120
+ reloadModels: () => reloadModelRegistry(sessions, PREWARM_ID, warmUp, options.agentDir ?? getAgentDir()),
121
+ async createSession(sessionOptions) {
122
+ // ADR-0021 §2:新建会话就落在 **pi 的会话存储**里(不再用 inMemory + 自建 jsonl),
123
+ // 用我们的 sessionId 当 SDK 的会话 id → 两边 id 空间完全一致。
124
+ const sessionManager = SessionManager.create(sessionOptions.workspace, options.sessionDir, {
125
+ id: sessionOptions.sessionId,
126
+ });
127
+ const handle = await startSession(sessionOptions.sessionId, sessionOptions.workspace, sessionManager);
128
+ return { sessionPath: handle.sessionPath };
129
+ },
130
+ async openSession(openOptions) {
131
+ const sessionPath = openOptions.sessionPath ?? openOptions.engineSessionId;
132
+ if (!sessionPath)
133
+ throw new Error("pi 打开会话需要 sessionPath(文件路径)");
134
+ const sessionManager = SessionManager.open(sessionPath, options.sessionDir); // header 里的 cwd + 同一个文件续写
135
+ const workspace = sessionManager.getCwd() || process.cwd();
136
+ await startSession(openOptions.sessionId, workspace, sessionManager, sessionPath);
137
+ },
138
+ renameSession: (sessionPath, name) => renameSessionIn(sessions, sessionPath, name, options.sessionDir),
139
+ async readUiMessages(sessionId) {
140
+ const handle = sessions.get(sessionId);
141
+ if (!handle)
142
+ return [];
143
+ return entriesToUiMessages(handle.sessionManager.getEntries());
144
+ },
145
+ async sendMessage(sessionId, text, onEvent, messageOptions) {
146
+ const handle = sessions.get(sessionId);
147
+ if (!handle) {
148
+ onEvent({ type: "error", code: "engine.no_session", message: `引擎里没有这个会话:${sessionId}` });
149
+ onEvent({ type: "turn_end", reason: "error" });
150
+ return;
151
+ }
152
+ handle.aborting = false;
153
+ let interrupted = false;
154
+ let providerError = false;
155
+ let failed = null;
156
+ let settled = null;
157
+ let markSettled = null;
158
+ settled = new Promise((resolve) => {
159
+ markSettled = resolve;
160
+ });
161
+ // 订阅**按次**持有:steer 会并发再进一次 sendMessage,两轮各自摘自己的监听器
162
+ const off = handle.session.subscribe((event) => {
163
+ const mapped = mapSessionEvent(event);
164
+ if (mapped) {
165
+ // provider 报错也要算「这一轮失败」:不能最后还发 turn_end: done
166
+ if (mapped.type === "error")
167
+ providerError = true;
168
+ onEvent(mapped);
169
+ return;
170
+ }
171
+ const record = asRecord(event);
172
+ if (record.type === "agent_settled") {
173
+ interrupted = record.aborted === true || handle.aborting;
174
+ markSettled?.();
175
+ }
176
+ });
177
+ handle.subscriptions.add(off);
178
+ try {
179
+ // 图片走 SDK 的 images 参数(视觉注入);没有图片时不传,避免空数组影响提示词缓存
180
+ const images = messageOptions?.images?.map((image) => ({
181
+ type: "image",
182
+ data: image.data,
183
+ mimeType: image.mimeType,
184
+ }));
185
+ // streamingBehavior 交给 pi 自己排队:steer = 打断当前轮插入,followUp = 等这轮结束
186
+ const promptOptions = {};
187
+ if (images && images.length > 0)
188
+ promptOptions.images = images;
189
+ if (messageOptions?.streamingBehavior)
190
+ promptOptions.streamingBehavior = messageOptions.streamingBehavior;
191
+ await handle.session.prompt(text, promptOptions);
192
+ // 等 settle:prompt() 返回后仍可能有收尾事件
193
+ await Promise.race([settled, new Promise((resolve) => setTimeout(resolve, 50))]);
194
+ }
195
+ catch (error) {
196
+ failed = {
197
+ code: "engine.prompt_failed",
198
+ message: error instanceof Error ? error.message : String(error),
199
+ };
200
+ }
201
+ finally {
202
+ off();
203
+ handle.subscriptions.delete(off);
204
+ }
205
+ if (failed) {
206
+ onEvent({ type: "error", code: failed.code, message: failed.message });
207
+ onEvent({ type: "turn_end", reason: "error" });
208
+ return;
209
+ }
210
+ onEvent({ type: "turn_end", reason: interrupted ? "interrupted" : providerError ? "error" : "done" });
211
+ },
212
+ async interrupt(sessionId) {
213
+ const handle = sessions.get(sessionId);
214
+ if (!handle)
215
+ return;
216
+ handle.aborting = true;
217
+ try {
218
+ await handle.session.abort();
219
+ }
220
+ catch (error) {
221
+ notice(`中断失败:${error instanceof Error ? error.message : String(error)}`);
222
+ }
223
+ },
224
+ async readTranscript(sessionId) {
225
+ const handle = sessions.get(sessionId);
226
+ if (!handle)
227
+ return [];
228
+ return handle.session.messages.map((message) => {
229
+ const record = asRecord(message);
230
+ const content = asRecord(record.content);
231
+ const text = typeof record.content === "string"
232
+ ? record.content
233
+ : Array.isArray(content)
234
+ ? content
235
+ .map((part) => {
236
+ const item = asRecord(part);
237
+ return typeof item.text === "string" ? item.text : "";
238
+ })
239
+ .join("")
240
+ : "";
241
+ const role = record.role === "assistant" ? "assistant" : "user";
242
+ return { role, text, ts: typeof record.timestamp === "number" ? record.timestamp : Date.now() };
243
+ });
244
+ },
245
+ setApprovalHandler(handler) {
246
+ approvalHandler = handler;
247
+ },
248
+ async listModels(sessionId) {
249
+ // 模型注册表挂在会话的 modelRuntime 上:没有会话时给不出(界面在打开会话后才显示列表)
250
+ const handle = sessionId ? sessions.get(sessionId) : [...sessions.values()].at(-1);
251
+ const runtime = handle?.session
252
+ ?.modelRuntime;
253
+ if (!runtime?.getAvailable)
254
+ return [];
255
+ try {
256
+ const models = (await runtime.getAvailable());
257
+ const current = await this.currentModel?.(sessionId ?? "");
258
+ return models
259
+ .map((model) => {
260
+ const id = `${String(model.provider ?? "?")}/${String(model.id ?? "?")}`;
261
+ return {
262
+ id,
263
+ label: String(model.name ?? model.id ?? id),
264
+ ...(typeof model.baseUrl === "string" ? { baseUrl: model.baseUrl } : {}),
265
+ ...(typeof model.api === "string" ? { api: model.api } : {}),
266
+ current: current ? id === `${current.provider}/${current.model}` : false,
267
+ };
268
+ })
269
+ .sort((a, b) => Number(b.current) - Number(a.current) || a.id.localeCompare(b.id));
270
+ }
271
+ catch {
272
+ return [];
273
+ }
274
+ },
275
+ listThinkingLevels: thinking.listThinkingLevels,
276
+ getThinkingLevel: thinking.getThinkingLevel,
277
+ setThinkingLevel: thinking.setThinkingLevel,
278
+ async switchModel(sessionId, modelId) {
279
+ // ModelRuntime 上没有 find(那是 ModelRegistry 的方法)→ 从可用列表里自己匹配
280
+ const handle = sessions.get(sessionId);
281
+ const runtime = handle?.session
282
+ ?.modelRuntime;
283
+ if (!handle || !runtime?.getAvailable)
284
+ return false;
285
+ const [provider, ...rest] = modelId.split("/");
286
+ const wanted = rest.join("/");
287
+ const available = (await runtime.getAvailable());
288
+ const model = available.find((item) => String(item.provider) === provider && String(item.id) === wanted);
289
+ if (!model)
290
+ return false;
291
+ try {
292
+ await handle.session.setModel(model);
293
+ return true;
294
+ }
295
+ catch (error) {
296
+ options.onNotice?.(`切换模型失败:${error instanceof Error ? error.message : String(error)}`, "error");
297
+ return false;
298
+ }
299
+ },
300
+ async currentModel(sessionId) {
301
+ const handle = sessions.get(sessionId);
302
+ const model = handle?.session?.model;
303
+ if (!model)
304
+ return null;
305
+ return { provider: String(model.provider ?? "?"), model: String(model.id ?? "?") };
306
+ },
307
+ sessionModel: async (sessionId) => readSessionModel(sessions.get(sessionId)),
308
+ async closeSession(sessionId) {
309
+ const handle = sessions.get(sessionId);
310
+ if (!handle)
311
+ return;
312
+ handle.unsubscribe();
313
+ try {
314
+ handle.session.dispose();
315
+ }
316
+ catch {
317
+ // 已经关掉了
318
+ }
319
+ sessions.delete(sessionId);
320
+ },
321
+ /**
322
+ * 关会话:给了 id 就关那一个;**不给就是引擎级 dispose(切引擎时宿主就是这么调的)**,
323
+ * 把所有会话一起关掉。以前这个签名必填 sessionId,宿主 `dispose?.()` 传空 →
324
+ * `sessions.get(undefined)` 什么也没关,每切一次引擎就漏一批 SDK 会话与资源加载器。
325
+ */
326
+ async dispose(sessionId) {
327
+ const ids = sessionId ? [sessionId] : [...sessions.keys()];
328
+ for (const id of ids) {
329
+ const handle = sessions.get(id);
330
+ if (!handle)
331
+ continue;
332
+ handle.unsubscribe();
333
+ try {
334
+ handle.session.dispose();
335
+ }
336
+ catch {
337
+ // 已经关掉了
338
+ }
339
+ sessions.delete(id);
340
+ }
341
+ },
342
+ };
343
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 「这个会话持久化的是哪个模型」——读会话条目里**最后一条** `model_change`。
3
+ *
4
+ * 为什么不用 SDK 的 `session.model`:续聊历史会话时它常常还是进程默认模型,
5
+ * 界面就会显示成「别的模型」,看起来像「选了模型没生效」(真实踩过)。
6
+ * 为什么会话文件里是这个:用户在模型选择器上切换时 SDK 就写一条 `model_change`,
7
+ * 所以它既是「当前模型」也是「下次续聊用的模型」。
8
+ */
9
+ export function readSessionModel(handle) {
10
+ const entries = handle?.sessionManager?.getEntries?.() ?? [];
11
+ // 倒着找:追加式文件,最后一条就是当前生效的
12
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
13
+ const entry = entries[index];
14
+ if (entry?.type !== "model_change")
15
+ continue;
16
+ const modelId = typeof entry.modelId === "string" ? entry.modelId : "";
17
+ if (modelId === "")
18
+ continue;
19
+ const provider = typeof entry.provider === "string" ? entry.provider : "?";
20
+ return { provider, model: modelId };
21
+ }
22
+ return null;
23
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 思考强度(P4)——从 pi-sdk.ts 抽出来(400 行护栏)。
3
+ *
4
+ * 档位与 pi-ai 的 ThinkingLevelSchema 对齐;这个常量没对外导出,所以写死并在此注明。
5
+ * 模型不支持推理(model.reasoning !== true)时不提供档位 → 界面隐藏控件。
6
+ */
7
+ const THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"];
8
+ export function createPiThinking(sessions) {
9
+ const pick = (sessionId) => (sessionId ? sessions.get(sessionId) : [...sessions.values()].at(-1))?.session;
10
+ return {
11
+ async listThinkingLevels(sessionId) {
12
+ const session = pick(sessionId);
13
+ if (!session?.model?.reasoning)
14
+ return [];
15
+ return [...THINKING_LEVELS];
16
+ },
17
+ async getThinkingLevel(sessionId) {
18
+ const level = pick(sessionId)?.thinkingLevel;
19
+ return typeof level === "string" ? level : null;
20
+ },
21
+ async setThinkingLevel(sessionId, level) {
22
+ const session = pick(sessionId);
23
+ if (!session?.setThinkingLevel)
24
+ return false;
25
+ try {
26
+ session.setThinkingLevel(level);
27
+ return session.thinkingLevel === level;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ },
33
+ };
34
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * 把 pi 会话的条目映射成界面消息(ADR-0021 §2:事实源换成 pi 的会话存储之后,
3
+ * 「显示」这件事就落在这个纯函数上)。
4
+ *
5
+ * 输入是 SDK 的 `SessionEntry[]`(我们只认识其中两种:
6
+ * `{type:"message", id, timestamp, message}` 与其它状态条目——后者忽略)。
7
+ * 输出是我们的 `UiMessage[]`(text / tool / error 三类块)。
8
+ *
9
+ * 刻意**不 import SDK 类型**:只用最小结构描述(TS 里用 unknown + 收窄),
10
+ * 这样单测可以用合成条目直接覆盖,不需要拉起 SDK。
11
+ */
12
+ import { TOOL_EXCERPT_CHARS } from "../protocol.js";
13
+ import { fullArgs, summarizeArgs } from "./pi-events.js";
14
+ import { sanitizeHistoricalActions } from "../markers/actions.js";
15
+ import { parseUsage } from "../util/usage.js";
16
+ import { scheduledCost } from "../util/pricing-index.js";
17
+ function asRecord(value) {
18
+ return typeof value === "object" && value !== null ? value : {};
19
+ }
20
+ function textOfContent(content) {
21
+ if (typeof content === "string")
22
+ return content;
23
+ if (!Array.isArray(content))
24
+ return "";
25
+ return content
26
+ .map((part) => {
27
+ const record = asRecord(part);
28
+ return typeof record.text === "string" ? record.text : "";
29
+ })
30
+ .join("");
31
+ }
32
+ /** 工具结果里的退出码(不同工具的 details 形状不同,取不到就不给) */
33
+ function exitCodeOf(details) {
34
+ const record = asRecord(details);
35
+ const value = record.exitCode ?? asRecord(record.structuredContent).exitCode;
36
+ return typeof value === "number" ? value : undefined;
37
+ }
38
+ function truncate(text) {
39
+ if (text.length <= TOOL_EXCERPT_CHARS)
40
+ return { outputExcerpt: text, truncated: false };
41
+ return { outputExcerpt: text.slice(0, TOOL_EXCERPT_CHARS), truncated: true };
42
+ }
43
+ /**
44
+ * 把一条 SDK message 转成「消息 + 其中的工具块」。
45
+ *
46
+ * 助手消息只是**一次模型调用**:一轮里有几次工具轮次就有几条。调用方会把同一轮里的
47
+ * 助手消息**合并成一条界面消息**(见 entriesToUiMessagesRaw)—— 界面上一次提问只该有一个
48
+ * 回复气泡,费用与 token 也要按整轮累加。
49
+ */
50
+ function mapMessage(entryId, message, order) {
51
+ const record = asRecord(message);
52
+ const role = record.role;
53
+ const ts = typeof record.timestamp === "number" ? record.timestamp : Date.now();
54
+ if (role === "user") {
55
+ const text = textOfContent(record.content);
56
+ if (text.trim() === "")
57
+ return null;
58
+ return { id: entryId, role: "user", ts, blocks: [{ kind: "text", text }] };
59
+ }
60
+ if (role === "assistant") {
61
+ const usage = parseUsage(record.usage);
62
+ // 这条消息自己用的模型(历史按当时的模型显示,不跟当前选择变)
63
+ const provider = typeof record.provider === "string" ? record.provider : "";
64
+ const modelId = typeof record.model === "string" ? record.model : "";
65
+ const model = modelId ? (provider ? `${provider}/${modelId}` : modelId) : undefined;
66
+ const blocks = [];
67
+ const content = Array.isArray(record.content) ? record.content : [];
68
+ for (const part of content) {
69
+ const item = asRecord(part);
70
+ if (item.type === "text" && typeof item.text === "string" && item.text.trim() !== "") {
71
+ blocks.push({ kind: "text", text: item.text });
72
+ continue;
73
+ }
74
+ // 思考过程也回放出来(「过程」里要按发生顺序摆,历史里没有它就对不上)
75
+ if (item.type === "thinking" &&
76
+ typeof (item.thinking ?? item.text ?? item.reasoning) === "string" &&
77
+ String(item.thinking ?? item.text ?? item.reasoning).trim() !== "") {
78
+ blocks.push({ kind: "thinking", text: String(item.thinking ?? item.text ?? item.reasoning) });
79
+ continue;
80
+ }
81
+ if (item.type === "toolCall" || item.type === "tool_use") {
82
+ blocks.push({
83
+ kind: "tool",
84
+ toolCallId: String(item.id ?? `tool-${order}-${blocks.length}`),
85
+ name: String(item.name ?? "unknown"),
86
+ argsSummary: summarizeArgs(item.arguments),
87
+ // 摘要被截断时带一份完整的:历史里展开也要看得见整条命令
88
+ argsFull: fullArgs(item.arguments),
89
+ // 结果在后面的条目里回填;此刻只表明「调用过」
90
+ status: "ok",
91
+ outputExcerpt: "",
92
+ truncated: false,
93
+ startedAt: ts,
94
+ });
95
+ }
96
+ // 其它部件(images 等)不进界面
97
+ }
98
+ if (blocks.length === 0)
99
+ return null;
100
+ // 分时价格(ADR-0026 D 期):配了时段表就按这条消息的时间算钱,
101
+ // 不沿用会话里 pi 当时按全天价算出来的 cost(否则历史一会儿是旧价、一会儿是新价)
102
+ const priced = usage && model ? scheduledCost(model, usage, ts) : undefined;
103
+ const finalUsage = usage && priced !== undefined ? { ...usage, cost: priced } : usage;
104
+ return {
105
+ id: entryId,
106
+ role: "assistant",
107
+ ts,
108
+ blocks,
109
+ ...(finalUsage ? { usage: finalUsage } : {}),
110
+ ...(model ? { model } : {}),
111
+ };
112
+ }
113
+ return null; // system / toolResult 单独处理
114
+ }
115
+ /** 两段用量相加(一轮里多次模型调用 → 合计;cost 拿不到就不给,别编 0) */
116
+ function addUsage(base, extra) {
117
+ const cost = (base?.cost ?? 0) + (extra.cost ?? 0);
118
+ return {
119
+ input: (base?.input ?? 0) + extra.input,
120
+ output: (base?.output ?? 0) + extra.output,
121
+ cacheRead: (base?.cacheRead ?? 0) + extra.cacheRead,
122
+ cacheWrite: (base?.cacheWrite ?? 0) + extra.cacheWrite,
123
+ ...(base?.cost === undefined && extra.cost === undefined ? {} : { cost }),
124
+ };
125
+ }
126
+ /**
127
+ * 纯函数:条目 → 界面消息。
128
+ * 工具结果按 `toolCallId` 回填到最近的对应工具块(找不到就补一条工具块,保证结果不丢)。
129
+ */
130
+ export function entriesToUiMessages(entries) {
131
+ return sanitizeHistoricalActions(entriesToUiMessagesRaw(entries));
132
+ }
133
+ function entriesToUiMessagesRaw(entries) {
134
+ const messages = [];
135
+ /** toolCallId → 已经放进消息里的那个块(用于回填结果) */
136
+ const toolBlocks = new Map();
137
+ for (const [order, entry] of entries.entries()) {
138
+ const record = asRecord(entry);
139
+ if (record.type !== "message")
140
+ continue; // 状态类条目(model_change / thinking_level_change …)不进界面
141
+ const entryId = typeof record.id === "string" ? record.id : `entry-${order}`;
142
+ const message = asRecord(record.message);
143
+ if (message.role === "toolResult") {
144
+ const toolCallId = String(message.toolCallId ?? "");
145
+ const block = toolBlocks.get(toolCallId);
146
+ const { outputExcerpt, truncated } = truncate(textOfContent(message.content));
147
+ const failed = message.isError === true;
148
+ if (block) {
149
+ const exitCode = exitCodeOf(message.details);
150
+ block.status = failed ? "failed" : "ok";
151
+ block.outputExcerpt = outputExcerpt;
152
+ block.truncated = truncated;
153
+ block.endedAt = typeof message.timestamp === "number" ? message.timestamp : block.startedAt;
154
+ if (exitCode !== undefined)
155
+ block.exitCode = exitCode;
156
+ }
157
+ else {
158
+ // 找不到对应调用(老会话里可能有):补一条工具块,保证结果不丢
159
+ const exitCode = exitCodeOf(message.details);
160
+ const ts2 = typeof message.timestamp === "number" ? message.timestamp : Date.now();
161
+ messages.push({
162
+ id: entryId,
163
+ role: "assistant",
164
+ ts: ts2,
165
+ blocks: [
166
+ {
167
+ kind: "tool",
168
+ toolCallId,
169
+ name: String(message.toolName ?? "unknown"),
170
+ argsSummary: "",
171
+ status: failed ? "failed" : "ok",
172
+ outputExcerpt,
173
+ truncated,
174
+ startedAt: ts2,
175
+ endedAt: ts2,
176
+ ...(exitCode === undefined ? {} : { exitCode }),
177
+ },
178
+ ],
179
+ });
180
+ }
181
+ continue;
182
+ }
183
+ const mapped = mapMessage(entryId, message, order);
184
+ if (!mapped)
185
+ continue;
186
+ // 同一轮里的助手消息合并成一条(工具轮次会有很多条):界面上一次提问只有一个回复气泡,
187
+ // token / 费用也跟着累加 —— 否则历史里看到的是「每次模型调用各一条」,总数还得自己加。
188
+ const previous = messages.at(-1);
189
+ if (previous && previous.role === "assistant") {
190
+ previous.blocks = [...previous.blocks, ...mapped.blocks];
191
+ if (mapped.usage)
192
+ previous.usage = addUsage(previous.usage, mapped.usage);
193
+ if (mapped.model)
194
+ previous.model = mapped.model;
195
+ // 完成时间 = 这一轮最后一次模型调用的时间
196
+ previous.endedAt = mapped.ts;
197
+ for (const block of mapped.blocks) {
198
+ if (block.kind === "tool")
199
+ toolBlocks.set(block.toolCallId, block);
200
+ }
201
+ continue;
202
+ }
203
+ messages.push(mapped);
204
+ for (const block of mapped.blocks) {
205
+ if (block.kind === "tool")
206
+ toolBlocks.set(block.toolCallId, block);
207
+ }
208
+ }
209
+ return messages;
210
+ }
211
+ /** 从条目里取「首条用户消息」当标题(侧栏用;扫描器也有一份同口径实现) */
212
+ export function firstUserText(entries) {
213
+ for (const entry of entries) {
214
+ const record = asRecord(entry);
215
+ if (record.type !== "message")
216
+ continue;
217
+ const message = asRecord(record.message);
218
+ if (message.role !== "user")
219
+ continue;
220
+ const text = textOfContent(message.content).replace(/\s+/g, " ").trim();
221
+ if (text !== "")
222
+ return text;
223
+ }
224
+ return "";
225
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * pi SDK 的版本号(界面抬头显示 `pi v1.3.0`)。
3
+ *
4
+ * 用 SDK 自己导出的 `VERSION`,不去读它的 package.json:那个包只导出 `import`/`.`,
5
+ * `require.resolve("…/package.json")` 会报 ERR_PACKAGE_PATH_NOT_EXPORTED。
6
+ * 单独一个文件是为了守住 ADR-0003(只有 server/engine 能碰 SDK)与 400 行护栏。
7
+ */
8
+ import { VERSION } from "@earendil-works/pi-coding-agent";
9
+ export function piSdkVersion() {
10
+ return typeof VERSION === "string" && VERSION.trim() !== "" ? VERSION.trim() : null;
11
+ }
@@ -0,0 +1 @@
1
+ export {};