@rexvane/piweb 0.3.14 → 0.3.16

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 (262) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +12 -9
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +6 -6
  5. package/.next/react-loadable-manifest.json +16 -4
  6. package/.next/required-server-files.js +3 -1
  7. package/.next/required-server-files.json +3 -1
  8. package/.next/routes-manifest.json +20 -0
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +7 -4
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +7 -4
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -2
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +5 -2
  23. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  24. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  28. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  29. package/.next/server/app/api/agent/new/route.js +1 -1
  30. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  31. package/.next/server/app/api/config/route.js +1 -1
  32. package/.next/server/app/api/config/route.js.nft.json +1 -1
  33. package/.next/server/app/api/files/route.js +1 -1
  34. package/.next/server/app/api/files/route.js.nft.json +1 -1
  35. package/.next/server/app/api/git/route.js +2 -2
  36. package/.next/server/app/api/git/route.js.nft.json +1 -1
  37. package/.next/server/app/api/growth/route.js +1 -1
  38. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  39. package/.next/server/app/api/health/route.js +1 -1
  40. package/.next/server/app/api/models/route.js +1 -1
  41. package/.next/server/app/api/models/route.js.nft.json +1 -1
  42. package/.next/server/app/api/pi-settings/route.js +1 -1
  43. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  44. package/.next/server/app/api/plugins/route.js +1 -1
  45. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  46. package/.next/server/app/api/prompts/route.js +1 -1
  47. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  48. package/.next/server/app/api/security/route.js +1 -1
  49. package/.next/server/app/api/security/route.js.nft.json +1 -1
  50. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  51. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  52. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  53. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  54. package/.next/server/app/api/sessions/import/route.js +3 -3
  55. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  56. package/.next/server/app/api/sessions/route.js +1 -1
  57. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  58. package/.next/server/app/api/skills/route.js +2 -2
  59. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  60. package/.next/server/app/api/swarm/[id]/route.js +1 -0
  61. package/.next/server/app/api/swarm/[id]/route.js.nft.json +1 -0
  62. package/.next/server/app/api/swarm/[id]/route_client-reference-manifest.js +1 -0
  63. package/.next/server/app/api/swarm/route.js +1 -0
  64. package/.next/server/app/api/swarm/route.js.nft.json +1 -0
  65. package/.next/server/app/api/swarm/route_client-reference-manifest.js +1 -0
  66. package/.next/server/app/api/tests/route.js +2 -0
  67. package/.next/server/app/api/tests/route.js.nft.json +1 -0
  68. package/.next/server/app/api/tests/route_client-reference-manifest.js +1 -0
  69. package/.next/server/app/api/update/route.js +2 -2
  70. package/.next/server/app/api/update/route.js.nft.json +1 -1
  71. package/.next/server/app/api/version/route.js +1 -1
  72. package/.next/server/app/api/version/route.js.nft.json +1 -1
  73. package/.next/server/app/api/web-auth/route.js +1 -1
  74. package/.next/server/app/api/workspaces/route.js +1 -1
  75. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  76. package/.next/server/app/icon.svg/route.js +1 -1
  77. package/.next/server/app/index.html +1 -1
  78. package/.next/server/app/index.rsc +9 -6
  79. package/.next/server/app/index.segments/__PAGE__.segment.rsc +7 -4
  80. package/.next/server/app/index.segments/_full.segment.rsc +9 -6
  81. package/.next/server/app/index.segments/_tree.segment.rsc +5 -2
  82. package/.next/server/app/login/page.js +1 -1
  83. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/login.html +1 -1
  85. package/.next/server/app/login.rsc +8 -5
  86. package/.next/server/app/login.segments/_full.segment.rsc +8 -5
  87. package/.next/server/app/login.segments/_tree.segment.rsc +5 -2
  88. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +6 -3
  89. package/.next/server/app/page.js +1 -1
  90. package/.next/server/app/page_client-reference-manifest.js +1 -1
  91. package/.next/server/app-paths-manifest.json +12 -9
  92. package/.next/server/chunks/496.js +1 -1
  93. package/.next/server/chunks/532.js +1 -1
  94. package/.next/server/chunks/686.js +6 -2
  95. package/.next/server/chunks/757.js +1 -0
  96. package/.next/server/chunks/775.js +3 -0
  97. package/.next/server/middleware-build-manifest.js +1 -1
  98. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  99. package/.next/server/pages/404.html +1 -1
  100. package/.next/server/pages/500.html +1 -1
  101. package/.next/server/server-reference-manifest.json +1 -1
  102. package/.next/static/0t1jBUAuVaTFTXxRyIo3q/_buildManifest.js +1 -0
  103. package/.next/static/chunks/2608.43c58236c9cce8e6.js +1 -0
  104. package/.next/static/chunks/{2716.a9e808190a4e2424.js → 2716.4ec42df1d51354c9.js} +1 -1
  105. package/.next/static/chunks/4632.62139b565b657aac.js +1 -0
  106. package/.next/static/chunks/{4673.d3d0a7f2599080fb.js → 4673.7796104395deb8d7.js} +1 -1
  107. package/.next/static/chunks/4686-17f21a006739a596.js +1 -0
  108. package/.next/static/chunks/7577.10af9bb661652da8.js +1 -0
  109. package/.next/static/chunks/8866.38ec4ac4ee5a1b74.js +1 -0
  110. package/.next/static/chunks/app/_global-error/page-5148173b3f55e415.js +1 -0
  111. package/.next/static/chunks/app/api/agent/[id]/events/route-5148173b3f55e415.js +1 -0
  112. package/.next/static/chunks/app/api/agent/[id]/route-5148173b3f55e415.js +1 -0
  113. package/.next/static/chunks/app/api/agent/[id]/tree/route-5148173b3f55e415.js +1 -0
  114. package/.next/static/chunks/app/api/agent/new/route-5148173b3f55e415.js +1 -0
  115. package/.next/static/chunks/app/api/config/route-5148173b3f55e415.js +1 -0
  116. package/.next/static/chunks/app/api/files/route-5148173b3f55e415.js +1 -0
  117. package/.next/static/chunks/app/api/git/route-5148173b3f55e415.js +1 -0
  118. package/.next/static/chunks/app/api/growth/route-5148173b3f55e415.js +1 -0
  119. package/.next/static/chunks/app/api/health/route-5148173b3f55e415.js +1 -0
  120. package/.next/static/chunks/app/api/models/route-5148173b3f55e415.js +1 -0
  121. package/.next/static/chunks/app/api/pi-settings/route-5148173b3f55e415.js +1 -0
  122. package/.next/static/chunks/app/api/plugins/route-5148173b3f55e415.js +1 -0
  123. package/.next/static/chunks/app/api/prompts/route-5148173b3f55e415.js +1 -0
  124. package/.next/static/chunks/app/api/security/route-5148173b3f55e415.js +1 -0
  125. package/.next/static/chunks/app/api/sessions/[id]/export/route-5148173b3f55e415.js +1 -0
  126. package/.next/static/chunks/app/api/sessions/[id]/route-5148173b3f55e415.js +1 -0
  127. package/.next/static/chunks/app/api/sessions/import/route-5148173b3f55e415.js +1 -0
  128. package/.next/static/chunks/app/api/sessions/route-5148173b3f55e415.js +1 -0
  129. package/.next/static/chunks/app/api/skills/route-5148173b3f55e415.js +1 -0
  130. package/.next/static/chunks/app/api/swarm/[id]/route-5148173b3f55e415.js +1 -0
  131. package/.next/static/chunks/app/api/swarm/route-5148173b3f55e415.js +1 -0
  132. package/.next/static/chunks/app/api/tests/route-5148173b3f55e415.js +1 -0
  133. package/.next/static/chunks/app/api/update/route-5148173b3f55e415.js +1 -0
  134. package/.next/static/chunks/app/api/version/route-5148173b3f55e415.js +1 -0
  135. package/.next/static/chunks/app/api/web-auth/route-5148173b3f55e415.js +1 -0
  136. package/.next/static/chunks/app/api/workspaces/route-5148173b3f55e415.js +1 -0
  137. package/.next/static/chunks/app/page-14379b8060d5cf07.js +1 -0
  138. package/.next/static/chunks/next/dist/client/components/builtin/app-error-5148173b3f55e415.js +1 -0
  139. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-5148173b3f55e415.js +1 -0
  140. package/.next/static/chunks/next/dist/client/components/builtin/not-found-5148173b3f55e415.js +1 -0
  141. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-5148173b3f55e415.js +1 -0
  142. package/.next/static/chunks/webpack-d1a69ad9edb38b11.js +1 -0
  143. package/.next/static/css/d0a0162cb481abe8.css +1 -0
  144. package/.next/types/app/api/swarm/[id]/route.ts +353 -0
  145. package/.next/types/app/api/swarm/route.ts +353 -0
  146. package/.next/types/app/api/tests/route.ts +353 -0
  147. package/.next/types/routes.d.ts +4 -1
  148. package/.next/types/validator.ts +27 -0
  149. package/README.md +61 -7
  150. package/assets/archive.png +0 -0
  151. package/assets/growth-1.png +0 -0
  152. package/assets/growth-2.png +0 -0
  153. package/assets/import.png +0 -0
  154. package/assets/main.png +0 -0
  155. package/assets/outline.png +0 -0
  156. package/assets/prompts-1.png +0 -0
  157. package/assets/prompts-2.png +0 -0
  158. package/assets/providers-1.png +0 -0
  159. package/assets/providers-2.png +0 -0
  160. package/assets/providers-3.png +0 -0
  161. package/assets/trace-1.png +0 -0
  162. package/assets/trace-2.png +0 -0
  163. package/assets/trace-3.png +0 -0
  164. package/assets/trace-4.png +0 -0
  165. package/assets/viewer-1.png +0 -0
  166. package/assets/viewer-2.png +0 -0
  167. package/next.config.ts +6 -2
  168. package/package.json +10 -5
  169. package/scripts/launcher.mjs +1 -1
  170. package/scripts/managed-niubash-assets.mjs +146 -0
  171. package/scripts/pick-folder.ps1 +11 -2
  172. package/scripts/prepare-niubash-packages.mjs +182 -0
  173. package/scripts/release.mjs +11 -31
  174. package/scripts/run-test-file.mjs +34 -0
  175. package/scripts/smoke-managed-niubash.mjs +62 -0
  176. package/scripts/verify-package.mjs +53 -0
  177. package/scripts/verify-release-config.mjs +24 -0
  178. package/src/app/api/config/route.ts +4 -1
  179. package/src/app/api/security/route.ts +8 -3
  180. package/src/app/api/swarm/[id]/route.ts +28 -0
  181. package/src/app/api/swarm/route.ts +24 -0
  182. package/src/app/api/tests/route.ts +27 -0
  183. package/src/app/api/update/route.ts +8 -2
  184. package/src/app/globals.css +777 -0
  185. package/src/app/layout.tsx +13 -6
  186. package/src/components/AppShell.tsx +505 -199
  187. package/src/components/ChatInput.tsx +153 -26
  188. package/src/components/ChatWindow.tsx +46 -40
  189. package/src/components/ContextMeter.tsx +11 -17
  190. package/src/components/FileViewer.tsx +1 -1
  191. package/src/components/GitPanel.tsx +13 -11
  192. package/src/components/ModelSelector.tsx +19 -6
  193. package/src/components/PiMark.tsx +5 -38
  194. package/src/components/ProjectPanel.tsx +1 -0
  195. package/src/components/PromptPanel.tsx +132 -51
  196. package/src/components/SessionImportSection.tsx +53 -37
  197. package/src/components/SessionSidebar.tsx +159 -57
  198. package/src/components/SettingsPanel.tsx +330 -193
  199. package/src/components/SwarmCoordinatorPod.tsx +1021 -0
  200. package/src/components/TestInspectorPod.tsx +549 -0
  201. package/src/components/icons.tsx +23 -0
  202. package/src/hooks/usePiWeb.ts +112 -8
  203. package/src/i18n.tsx +59 -3
  204. package/src/lib/agent-manager.ts +182 -11
  205. package/src/lib/command-validation.ts +9 -1
  206. package/src/lib/delegation.ts +93 -0
  207. package/src/lib/managed-niubash.ts +231 -0
  208. package/src/lib/pi.ts +3 -2
  209. package/src/lib/runtime-activity.ts +27 -0
  210. package/src/lib/session-import/claude.ts +13 -2
  211. package/src/lib/session-import/codex.ts +44 -8
  212. package/src/lib/session-import/files.ts +14 -0
  213. package/src/lib/session-import/grok.ts +54 -24
  214. package/src/lib/session-import/index.ts +5 -0
  215. package/src/lib/session-import/lock.ts +25 -0
  216. package/src/lib/session-import/types.ts +12 -0
  217. package/src/lib/session-import/writer.ts +8 -3
  218. package/src/lib/skills-service.ts +16 -9
  219. package/src/lib/swarm-service.ts +371 -0
  220. package/src/lib/test-run-service.ts +122 -0
  221. package/src/lib/theme.ts +68 -1
  222. package/src/lib/trajectory.ts +1 -0
  223. package/src/lib/types.ts +14 -0
  224. package/src/lib/workflow.ts +55 -0
  225. package/.next/static/LOFl9bqqMV2goBV8erESO/_buildManifest.js +0 -1
  226. package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
  227. package/.next/static/chunks/4632.112d2e893d48d188.js +0 -1
  228. package/.next/static/chunks/4686-7d694def920ff093.js +0 -1
  229. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +0 -1
  230. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +0 -1
  231. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +0 -1
  232. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +0 -1
  233. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +0 -1
  234. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +0 -1
  235. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +0 -1
  236. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +0 -1
  237. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +0 -1
  238. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +0 -1
  239. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +0 -1
  240. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +0 -1
  241. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +0 -1
  242. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +0 -1
  243. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +0 -1
  244. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +0 -1
  245. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +0 -1
  246. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +0 -1
  247. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +0 -1
  248. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +0 -1
  249. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +0 -1
  250. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +0 -1
  251. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +0 -1
  252. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +0 -1
  253. package/.next/static/chunks/app/page-9a5440047c5a415c.js +0 -1
  254. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +0 -1
  255. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +0 -1
  256. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +0 -1
  257. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +0 -1
  258. package/.next/static/chunks/webpack-d28e132e1b8c100c.js +0 -1
  259. package/.next/static/css/b2d35abc828d4713.css +0 -1
  260. package/assets/showcase.png +0 -0
  261. package/bin/pi.js +0 -50
  262. /package/.next/static/{LOFl9bqqMV2goBV8erESO → 0t1jBUAuVaTFTXxRyIo3q}/_ssgManifest.js +0 -0
@@ -33,8 +33,12 @@ import { TrajLedger, buildTrajectoryFromEntries, toTrajTokens } from "./trajecto
33
33
  import { startArchivedSessionRetention } from "./session-retention";
34
34
  import { sanitizeToolOutput } from "./text-sanitize";
35
35
  import { BoundaryError } from "./path-security";
36
+ import { beginActivity } from "./runtime-activity";
36
37
  import { estimateTokensOf } from "./process-format";
37
38
  import { getPiSettings } from "./pi-settings";
39
+ import { createManagedNiubashTools } from "./managed-niubash";
40
+ import { createDelegationTool } from "./delegation";
41
+ import { awaitingGoalMessage, isWorkflowMode, planCanRun, readWorkflow, saveWorkflow, workflowInstruction, type WorkflowState } from "./workflow";
38
42
  import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
39
43
  import type {
40
44
  AgentCommand,
@@ -65,6 +69,7 @@ interface Managed {
65
69
  ledger: TrajLedger;
66
70
  lastActive: number;
67
71
  toolPreset: ToolPreset;
72
+ workflow: WorkflowState;
68
73
  /** 显式逐项选择优先于 preset;保留失效名称以便资源重新启用后恢复用户选择。 */
69
74
  customActiveTools?: string[];
70
75
  resourceVersion: number;
@@ -526,6 +531,20 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
526
531
  break;
527
532
  case "agent_settled":
528
533
  m.runActive = false;
534
+ if (m.workflow.mode === "plan") {
535
+ if (m.workflow.planStatus === "planning") {
536
+ const answer = [...(m.session?.messages ?? [])].reverse().find((message) => message.role === "assistant");
537
+ const ready = answer?.role === "assistant" && answer.stopReason === "stop" && answer.content.some((part) => part.type === "text" && part.text.trim());
538
+ const entry = ready ? m.sm.getBranch().slice().reverse().find((item) => item.type === "message" && item.message.role === "assistant" && item.message.stopReason === "stop") : undefined;
539
+ setWorkflow(m, { ...m.workflow, planStatus: entry ? "ready" : "idle", planId: entry?.id });
540
+ } else if (m.workflow.planStatus === "executing") {
541
+ // 执行报错或被中止时方案仍待执行:以当前叶节点为新的批准锚点,可以直接再次执行。
542
+ const answer = [...(m.session?.messages ?? [])].reverse().find((message) => message.role === "assistant");
543
+ const failed = answer?.role === "assistant" && (answer.stopReason === "error" || answer.stopReason === "aborted");
544
+ const anchor = failed ? m.sm.getLeafId() : null;
545
+ setWorkflow(m, anchor ? { ...m.workflow, planStatus: "ready", planId: anchor } : { ...m.workflow, planStatus: "idle", planId: undefined });
546
+ }
547
+ }
529
548
  publish(m, { type: "status", isStreaming: false, state: "idle", ts: now });
530
549
  publishUsage(m);
531
550
  if (m.resourceReloadPending) queueMicrotask(() => void reloadManagedResources(m));
@@ -635,6 +654,7 @@ function registerManaged(sessionPath: string, cwd: string, sm: SessionManager):
635
654
  ledger: new TrajLedger(initialTrajectory),
636
655
  lastActive: Date.now(),
637
656
  toolPreset: "readonly",
657
+ workflow: readWorkflow(sm),
638
658
  resourceVersion: 0,
639
659
  resourceReloadPending: false,
640
660
  resourceReloading: false,
@@ -783,12 +803,33 @@ function publishEnvironmentTrajectory(m: Managed, session: AgentSession): void {
783
803
  }
784
804
 
785
805
  export async function ensureSession(m: Managed): Promise<AgentSession> {
806
+ const release = beginActivity("session");
807
+ try { return await ensureSessionActive(m); }
808
+ finally { release(); }
809
+ }
810
+
811
+ async function ensureSessionActive(m: Managed): Promise<AgentSession> {
786
812
  if (m.disposed) throw new Error("session is disposed");
787
813
  if (m.creating) return m.creating;
788
814
  if (m.session) return m.session;
789
815
  m.creating = (async () => {
790
816
  await resourceLoaderReady(m.cwd);
791
- const loader = await createSessionResourceLoader(m.cwd);
817
+ const loader = await createSessionResourceLoader(m.cwd, [{
818
+ name: "piweb-workflow",
819
+ hidden: true,
820
+ factory: (pi) => {
821
+ pi.on("before_agent_start", (event) => {
822
+ const instruction = workflowInstruction(m.workflow);
823
+ if (instruction) {
824
+ if (event.systemPromptOptions.forceSystemPrompt !== undefined) event.systemPromptOptions.forceSystemPrompt += `\n\n${instruction}`;
825
+ else event.systemPromptOptions.sections.piweb_workflow = instruction;
826
+ } else delete event.systemPromptOptions.sections.piweb_workflow;
827
+ if (m.workflow.mode === "plan" && m.workflow.planStatus !== "executing") {
828
+ event.systemPromptOptions.selectedTools = event.systemPromptOptions.selectedTools.filter((name) => [...TOOL_PRESETS.readonly, "piweb_delegate"].includes(name));
829
+ }
830
+ });
831
+ },
832
+ }]);
792
833
  let created: AgentSession | undefined;
793
834
  try {
794
835
  if (m.disposed) throw new Error("session is disposed");
@@ -799,6 +840,8 @@ export async function ensureSession(m: Managed): Promise<AgentSession> {
799
840
  modelRuntime: await getModelRuntime(),
800
841
  resourceLoader: loader,
801
842
  settingsManager: getSettingsManager(m.cwd),
843
+ // Windows: 同名 bash 覆盖 SDK 内置工具,指向托管 niubash;其它平台返回 [],走 Pi 默认。
844
+ customTools: [...createManagedNiubashTools(m.cwd), createDelegationTool(m.cwd, () => m.session)],
802
845
  });
803
846
  created = session;
804
847
  if (m.disposed) throw new Error("session is disposed");
@@ -845,7 +888,9 @@ function isManagedBusy(m: Managed): boolean {
845
888
 
846
889
  function allowedToolNames(m: Managed, session: AgentSession): string[] {
847
890
  const allowed = m.customActiveTools ?? (m.toolPreset === "full" ? undefined : TOOL_PRESETS[m.toolPreset]);
848
- return listAllToolNames(session).filter((name) => !allowed || allowed.includes(name));
891
+ const effective = allowed ? [...allowed, ...(m.customActiveTools || m.toolPreset === "readonly" ? [] : ["piweb_delegate"])] : undefined;
892
+ const planning = m.workflow.mode === "plan" && m.workflow.planStatus !== "executing";
893
+ return listAllToolNames(session).filter((name) => (!effective || effective.includes(name)) && (!planning || [...TOOL_PRESETS.readonly, "piweb_delegate"].includes(name)));
849
894
  }
850
895
 
851
896
  function applyToolPolicy(m: Managed, session: AgentSession): void {
@@ -856,6 +901,20 @@ function publishTools(m: Managed, session: AgentSession): void {
856
901
  publish(m, { type: "tools", active: listActiveTools(session), all: listAllToolNames(session), toolPreset: m.toolPreset, customActiveTools: m.customActiveTools ?? null, ts: Date.now() });
857
902
  }
858
903
 
904
+ function setWorkflow(m: Managed, next: WorkflowState): void {
905
+ saveWorkflow(m.sm, next);
906
+ m.workflow = next;
907
+ publishWorkflow(m);
908
+ }
909
+
910
+ function publishWorkflow(m: Managed): void {
911
+ if (m.session && !m.disposed) {
912
+ applyToolPolicy(m, m.session);
913
+ publishTools(m, m.session);
914
+ }
915
+ publish(m, { type: "workflow", workflow: m.workflow, ts: Date.now() });
916
+ }
917
+
859
918
  /** SDK reload 自动启用扩展工具,注册新工具也会刷新全集;统一在激活边界收敛到服务端选择。 */
860
919
  function installSessionPolicy(m: Managed, session: AgentSession): void {
861
920
  const setActive = session.setActiveToolsByName.bind(session);
@@ -1171,12 +1230,12 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
1171
1230
  }
1172
1231
  for (let i = messages.length - 1; i >= 0 && !compactedSinceLastAssistant; i--) {
1173
1232
  const msg = messages[i];
1174
- if (msg.role === "assistant" && msg.usage) {
1175
- lastTurnTokens =
1233
+ if (msg.role === "assistant" && msg.usage && msg.stopReason !== "error" && msg.stopReason !== "aborted") {
1234
+ lastTurnTokens = Number(msg.usage.totalTokens ?? 0) || (
1176
1235
  Number(msg.usage.input ?? 0) +
1177
1236
  Number(msg.usage.cacheRead ?? 0) +
1178
1237
  Number(msg.usage.cacheWrite ?? 0) +
1179
- Number(msg.usage.output ?? 0);
1238
+ Number(msg.usage.output ?? 0));
1180
1239
  break;
1181
1240
  }
1182
1241
  }
@@ -1241,6 +1300,7 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
1241
1300
  thinkingLevel,
1242
1301
  thinkingLevels,
1243
1302
  toolPreset: m.toolPreset,
1303
+ workflow: m.workflow,
1244
1304
  customActiveTools: m.customActiveTools ?? null,
1245
1305
  extensionUiRequests: m.ui?.getPendingRequests() ?? [],
1246
1306
  tools,
@@ -1294,12 +1354,52 @@ export interface CommandResult {
1294
1354
  }
1295
1355
 
1296
1356
  export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandResult> {
1357
+ let release: (() => void) | undefined;
1358
+ try {
1359
+ if (!["abort", "clearQueue", "extensionUiResponse"].includes(cmd.cmd)) release = beginActivity("session");
1360
+ return await executeCommand(m, cmd);
1361
+ } catch (error) {
1362
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
1363
+ } finally {
1364
+ release?.();
1365
+ }
1366
+ }
1367
+
1368
+ function trackedPrompt(session: AgentSession, ...args: Parameters<AgentSession["prompt"]>): ReturnType<AgentSession["prompt"]> {
1369
+ const release = beginActivity("session");
1370
+ try { return session.prompt(...args).finally(release); }
1371
+ catch (error) { release(); throw error; }
1372
+ }
1373
+
1374
+ async function executeCommand(m: Managed, cmd: AgentCommand): Promise<CommandResult> {
1297
1375
  touch(m);
1298
1376
  try {
1299
1377
  switch (cmd.cmd) {
1300
1378
  case "prepare":
1301
1379
  await ensureSession(m);
1302
1380
  return { ok: true };
1381
+ case "setWorkflowMode": {
1382
+ if (!isWorkflowMode(cmd.mode)) return { ok: false, error: "invalid workflow mode" };
1383
+ if (isManagedBusy(m)) return { ok: false, error: "cannot change workflow while the agent is running" };
1384
+ // 重选当前模式不重置:否则会清掉 Goal 目标或待执行的方案,并多写一条记录。
1385
+ if (cmd.mode === m.workflow.mode) return { ok: true };
1386
+ setWorkflow(m, { mode: cmd.mode, planStatus: "idle", goal: "" });
1387
+ return { ok: true };
1388
+ }
1389
+ case "approvePlan": {
1390
+ if (!planCanRun(m.workflow)) return { ok: false, error: "there is no plan awaiting approval" };
1391
+ if (isManagedBusy(m)) return { ok: false, error: "session is busy" };
1392
+ const branch = m.sm.getBranch();
1393
+ const planIndex = branch.findIndex((entry) => entry.id === m.workflow.planId);
1394
+ if (planIndex < 0 || branch.slice(planIndex + 1).some((entry) => entry.type === "message")) {
1395
+ return { ok: false, error: "the plan is no longer the latest conversation; request a new plan" };
1396
+ }
1397
+ const ready = m.workflow;
1398
+ setWorkflow(m, { ...ready, planStatus: "executing" });
1399
+ const result = await execute(m, { cmd: "prompt", text: "Execute the plan you just proposed. Verify the changes and report any unfinished work." });
1400
+ if (!result.ok) setWorkflow(m, ready);
1401
+ return result;
1402
+ }
1303
1403
  case "prompt": {
1304
1404
  const text = (cmd.text ?? "").trim();
1305
1405
  if (!text && !cmd.images?.length) return { ok: false, error: "empty prompt" };
@@ -1311,6 +1411,7 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
1311
1411
  if (m.disposed) throw new Error("session is disposed");
1312
1412
  // 只有用户显式选择 steer/followUp 才排队;旧的普通提交不能悄悄成为 steering。
1313
1413
  if ((session.isStreaming || m.runActive) && !cmd.behavior) return { ok: false, error: "session is busy; choose steer or followUp" };
1414
+ if (m.workflow.mode === "plan" && (session.isStreaming || m.runActive)) return { ok: false, error: "wait for the current plan or execution to finish" };
1314
1415
  if (!session.isStreaming) {
1315
1416
  // 基线快照不能无限阻塞首条 prompt(git 不可用时每次探测至多 30s 超时):
1316
1417
  // 5s 内没完成就放行 prompt,快照后台补拍(工具结束后照常记步,外部修改兜底)。
@@ -1320,11 +1421,18 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
1320
1421
  ]);
1321
1422
  }
1322
1423
  if (m.disposed) throw new Error("session is disposed");
1323
- return await new Promise<CommandResult>((resolve) => {
1424
+ const previousWorkflow = m.workflow;
1425
+ if (m.workflow.mode === "plan" && m.workflow.planStatus !== "executing") {
1426
+ setWorkflow(m, { ...m.workflow, planStatus: "planning", planId: undefined });
1427
+ } else if (m.workflow.mode === "goal" && !m.workflow.goal && text) {
1428
+ setWorkflow(m, { ...m.workflow, goal: text.slice(0, 8_000) });
1429
+ }
1430
+ const result = await new Promise<CommandResult>((resolve) => {
1324
1431
  let accepted = false;
1325
- const run = session.prompt(text, {
1432
+ const run = trackedPrompt(session, text, {
1326
1433
  images: cmd.images,
1327
1434
  streamingBehavior: cmd.behavior,
1435
+ expandPromptTemplates: m.workflow.mode !== "plan" || m.workflow.planStatus === "executing",
1328
1436
  source: "rpc",
1329
1437
  preflightResult: (success) => {
1330
1438
  if (success) {
@@ -1341,7 +1449,9 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
1341
1449
  if (accepted) publish(m, { type: "error", message, ts: Date.now() });
1342
1450
  else resolve({ ok: false, error: message });
1343
1451
  });
1344
- });
1452
+ });
1453
+ if (!result.ok && m.workflow !== previousWorkflow) setWorkflow(m, previousWorkflow);
1454
+ return result;
1345
1455
  } finally {
1346
1456
  m.promptSubmitting = false;
1347
1457
  if (!m.disposed) publish(m, { type: "status", isStreaming: isManagedBusy(m), state: isManagedBusy(m) ? "running" : "idle", ts: Date.now() });
@@ -1350,6 +1460,7 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
1350
1460
  }
1351
1461
  case "steer":
1352
1462
  case "followUp": {
1463
+ if (m.workflow.mode === "plan") return { ok: false, error: "Plan mode does not queue prompts" };
1353
1464
  if (m.promptSubmitting || m.resourceReloading) return { ok: false, error: "session is busy accepting another command" };
1354
1465
  const session = await ensureSession(m);
1355
1466
  if (m.promptSubmitting || m.resourceReloading || !session.isStreaming) return { ok: false, error: "session is not accepting queued messages" };
@@ -1438,6 +1549,10 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
1438
1549
  const entryId = String((cmd as any).entryId ?? "");
1439
1550
  if (!entryId) return { ok: false, error: "missing entryId" };
1440
1551
  const r = await session.navigateTree(entryId);
1552
+ if (!r?.cancelled) {
1553
+ m.workflow = readWorkflow(m.sm);
1554
+ publishWorkflow(m);
1555
+ }
1441
1556
  return { ok: !r?.cancelled, data: { cancelled: r?.cancelled === true, editorText: r?.editorText } };
1442
1557
  }
1443
1558
  case "editAndResend": {
@@ -1447,6 +1562,9 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
1447
1562
  const text = (cmd.text ?? "").trim();
1448
1563
  if (!entryId) return { ok: false, error: "missing entryId" };
1449
1564
  if (!text) return { ok: false, error: "empty prompt" };
1565
+ const target = m.sm.getEntry(entryId);
1566
+ if (target?.type !== "message" || target.message.role !== "user") return { ok: false, error: "message is not editable" };
1567
+ const images = Array.isArray(target.message.content) ? target.message.content.filter((part) => part.type === "image") : [];
1450
1568
  if (m.promptSubmitting || m.resourceReloading) return { ok: false, error: "session is busy accepting another command" };
1451
1569
  m.promptSubmitting = true;
1452
1570
  try {
@@ -1460,11 +1578,64 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
1460
1578
  }
1461
1579
  }
1462
1580
  if (m.disposed) throw new Error("session is disposed");
1463
- const moved = await session.navigateTree(entryId);
1581
+ // SDK 对当前叶节点直接返回 no-op;先移开叶节点,才能把最后一条用户消息也原位重发。
1582
+ const originalLeafId = m.sm.getLeafId();
1583
+ const leafWasTarget = originalLeafId === entryId;
1584
+ if (leafWasTarget) {
1585
+ if (target.parentId) m.sm.branch(target.parentId);
1586
+ else m.sm.resetLeaf();
1587
+ }
1588
+ let moved;
1589
+ try {
1590
+ moved = await session.navigateTree(entryId);
1591
+ } catch (error) {
1592
+ if (leafWasTarget) m.sm.branch(entryId);
1593
+ throw error;
1594
+ }
1595
+ if (moved?.cancelled && leafWasTarget) m.sm.branch(entryId);
1464
1596
  if (moved?.cancelled) return { ok: false, error: "edit cancelled" };
1597
+ m.workflow = readWorkflow(m.sm);
1598
+ publishWorkflow(m);
1465
1599
  await growthOf(m).prepare().catch(() => undefined);
1466
- await session.prompt(text);
1467
- return { ok: true };
1600
+ const previousWorkflow = m.workflow;
1601
+ if (m.workflow.mode === "plan") setWorkflow(m, { ...m.workflow, planStatus: "planning", planId: undefined });
1602
+ // 编辑的正是定下目标的那条消息时,目标跟着改;旧目标记录是它的祖先,回退分支不会带走它。
1603
+ else if (m.workflow.mode === "goal" && awaitingGoalMessage(m.sm) && m.workflow.goal !== text.slice(0, 8_000)) {
1604
+ setWorkflow(m, { ...m.workflow, goal: text.slice(0, 8_000) });
1605
+ }
1606
+ const options = { images, expandPromptTemplates: m.workflow.mode !== "plan" };
1607
+ return await new Promise<CommandResult>((resolve) => {
1608
+ let accepted = false;
1609
+ const run = trackedPrompt(session, text, {
1610
+ ...options,
1611
+ preflightResult: (success) => {
1612
+ if (success) {
1613
+ accepted = true;
1614
+ resolve({ ok: true, data: { accepted: true } });
1615
+ }
1616
+ },
1617
+ });
1618
+ void run.then(() => {
1619
+ if (!accepted) resolve({ ok: true });
1620
+ }, (error) => {
1621
+ const message = String(error?.message ?? error);
1622
+ if (accepted) {
1623
+ publish(m, { type: "error", message, ts: Date.now() });
1624
+ return;
1625
+ }
1626
+ // Rejected preflight never wrote the edited turn; restore the old branch before the client resyncs.
1627
+ try {
1628
+ if (originalLeafId) m.sm.branch(originalLeafId);
1629
+ else m.sm.resetLeaf();
1630
+ session.refreshContext();
1631
+ m.workflow = readWorkflow(m.sm);
1632
+ publishWorkflow(m);
1633
+ } catch {
1634
+ if (m.workflow !== previousWorkflow) setWorkflow(m, previousWorkflow);
1635
+ }
1636
+ resolve({ ok: false, error: message });
1637
+ });
1638
+ });
1468
1639
  } finally {
1469
1640
  m.promptSubmitting = false;
1470
1641
  }
@@ -1,4 +1,4 @@
1
- import type { AgentCommand, ImageAttachment, ToolPreset } from "./types";
1
+ import type { AgentCommand, ImageAttachment, ToolPreset, WorkflowMode } from "./types";
2
2
 
3
3
  const COMMANDS = new Set<AgentCommand["cmd"]>([
4
4
  "prepare",
@@ -10,6 +10,8 @@ const COMMANDS = new Set<AgentCommand["cmd"]>([
10
10
  "setModel",
11
11
  "setThinkingLevel",
12
12
  "setToolPreset",
13
+ "setWorkflowMode",
14
+ "approvePlan",
13
15
  "setActiveTools",
14
16
  "rename",
15
17
  "fork",
@@ -21,6 +23,7 @@ const COMMANDS = new Set<AgentCommand["cmd"]>([
21
23
  "reload",
22
24
  ]);
23
25
  const TOOL_PRESETS = new Set<ToolPreset>(["readonly", "standard", "full"]);
26
+ const WORKFLOW_MODES = new Set<WorkflowMode>(["agent", "plan", "goal"]);
24
27
  const STREAMING_BEHAVIORS = new Set(["steer", "followUp"]);
25
28
  const IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
26
29
  const MAX_TEXT = 1_000_000;
@@ -78,6 +81,10 @@ export function parseAgentCommand(value: unknown): CommandParseResult {
78
81
  if (typeof raw.preset !== "string" || !TOOL_PRESETS.has(raw.preset as ToolPreset)) throw new Error("invalid tool preset");
79
82
  command.preset = raw.preset as ToolPreset;
80
83
  }
84
+ if (raw.mode !== undefined) {
85
+ if (typeof raw.mode !== "string" || !WORKFLOW_MODES.has(raw.mode as WorkflowMode)) throw new Error("invalid workflow mode");
86
+ command.mode = raw.mode as WorkflowMode;
87
+ }
81
88
  if (raw.behavior !== undefined) {
82
89
  if (typeof raw.behavior !== "string" || !STREAMING_BEHAVIORS.has(raw.behavior)) throw new Error("invalid streaming behavior");
83
90
  command.behavior = raw.behavior as "steer" | "followUp";
@@ -108,6 +115,7 @@ export function parseAgentCommand(value: unknown): CommandParseResult {
108
115
  if (command.cmd === "setModel" && (!command.provider || !command.modelId)) throw new Error("provider and modelId are required");
109
116
  if (command.cmd === "setThinkingLevel" && !command.level) throw new Error("level is required");
110
117
  if (command.cmd === "setToolPreset" && !command.preset) throw new Error("preset is required");
118
+ if (command.cmd === "setWorkflowMode" && !command.mode) throw new Error("mode is required");
111
119
  if (command.cmd === "setActiveTools" && !command.names) throw new Error("names are required");
112
120
  if (command.cmd === "navigate" && !command.entryId) throw new Error("entryId is required");
113
121
  return { ok: true, command };
@@ -0,0 +1,93 @@
1
+ import { DefaultResourceLoader, SessionManager, createAgentSession, defineTool, type AgentSession, type ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { getAgentDir, getModelRuntime, getSettingsManager, TOOL_PRESETS, withResourceLock } from "./pi";
4
+
5
+ const MAX_TASKS = 3;
6
+ const TASK_TIMEOUT_MS = 120_000;
7
+ const MAX_REPORT_CHARS = 12_000;
8
+
9
+ const parameters = Type.Object({
10
+ tasks: Type.Array(Type.Object({
11
+ title: Type.String({ minLength: 1, maxLength: 80 }),
12
+ instruction: Type.String({ minLength: 1, maxLength: 4_000 }),
13
+ }), { minItems: 1, maxItems: MAX_TASKS }),
14
+ });
15
+
16
+ function reportOf(session: AgentSession): string {
17
+ const answer = [...session.messages].reverse().find((message) => message.role === "assistant");
18
+ if (!answer || answer.role !== "assistant") return "The subagent returned no assistant answer.";
19
+ return answer.content.filter((part) => part.type === "text").map((part) => part.text).join("\n").slice(0, MAX_REPORT_CHARS) || "The subagent returned no text answer.";
20
+ }
21
+
22
+ async function runSubagent(cwd: string, parent: AgentSession, instruction: string, signal?: AbortSignal): Promise<string> {
23
+ if (signal?.aborted) throw new Error("Delegation cancelled");
24
+ const loader = new DefaultResourceLoader({
25
+ cwd,
26
+ agentDir: getAgentDir(),
27
+ settingsManager: getSettingsManager(cwd),
28
+ noExtensions: true,
29
+ noPromptTemplates: true,
30
+ noThemes: true,
31
+ appendSystemPrompt: ["You are a read-only PiWeb subagent. Investigate the assigned task and return a concise, evidence-based report. Do not modify files or run commands. Do not delegate further."],
32
+ });
33
+ let child: AgentSession | undefined;
34
+ let timer: ReturnType<typeof setTimeout> | undefined;
35
+ let timedOut = false;
36
+ const abort = () => { void child?.abort().catch(() => undefined); };
37
+ try {
38
+ await withResourceLock(() => loader.reload());
39
+ if (signal?.aborted) throw new Error("Delegation cancelled");
40
+ const created = await createAgentSession({
41
+ cwd,
42
+ agentDir: getAgentDir(),
43
+ sessionManager: SessionManager.inMemory(cwd),
44
+ modelRuntime: await getModelRuntime(),
45
+ model: parent.model ?? undefined,
46
+ thinkingLevel: parent.thinkingLevel,
47
+ settingsManager: getSettingsManager(cwd),
48
+ resourceLoader: loader,
49
+ tools: TOOL_PRESETS.readonly,
50
+ });
51
+ child = created.session;
52
+ if (signal?.aborted) throw new Error("Delegation cancelled");
53
+ signal?.addEventListener("abort", abort, { once: true });
54
+ timer = setTimeout(() => { timedOut = true; abort(); }, TASK_TIMEOUT_MS);
55
+ await child.prompt(instruction, { source: "rpc", expandPromptTemplates: false });
56
+ if (timedOut) throw new Error("Subagent timed out after two minutes");
57
+ if (signal?.aborted) throw new Error("Delegation cancelled");
58
+ return reportOf(child);
59
+ } finally {
60
+ if (timer) clearTimeout(timer);
61
+ signal?.removeEventListener("abort", abort);
62
+ child?.dispose();
63
+ loader.getExtensions().runtime.invalidate();
64
+ }
65
+ }
66
+
67
+ export function createDelegationTool(cwd: string, parent: () => AgentSession | null): ToolDefinition {
68
+ return defineTool({
69
+ name: "piweb_delegate",
70
+ label: "Delegate research",
71
+ description: "Delegate one to three independent, read-only research or review tasks to temporary Pi subagents. They cannot edit files, run shell commands, or delegate further. Use only when parallel investigation is useful; summarize their findings yourself.",
72
+ promptSnippet: "Delegate bounded, read-only research tasks to temporary subagents.",
73
+ parameters,
74
+ executionMode: "sequential",
75
+ async execute(_id, { tasks }, signal, onUpdate) {
76
+ const leader = parent();
77
+ if (!leader) throw new Error("Parent session is unavailable");
78
+ const reports: string[] = new Array(tasks.length);
79
+ await Promise.all(tasks.map(async (task, index) => {
80
+ try {
81
+ const report = await runSubagent(cwd, leader, task.instruction, signal);
82
+ reports[index] = `## ${task.title}\n${report}`;
83
+ } catch (error) {
84
+ reports[index] = `## ${task.title}\nFailed: ${String((error as Error)?.message ?? error)}`;
85
+ }
86
+ onUpdate?.({ content: [{ type: "text", text: reports.filter(Boolean).join("\n\n") }], details: undefined });
87
+ }));
88
+ if (signal?.aborted) throw new Error("Delegation cancelled");
89
+ const text = reports.join("\n\n");
90
+ return { content: [{ type: "text", text }], details: undefined };
91
+ },
92
+ });
93
+ }