@pencil-agent/nano-pencil 2.0.0-beta.8 → 2.0.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 (241) hide show
  1. package/README.md +267 -267
  2. package/dist/build-meta.json +3 -3
  3. package/dist/core/export-html/AGENT.md +11 -11
  4. package/dist/core/export-html/template.css +971 -971
  5. package/dist/core/export-html/template.html +54 -54
  6. package/dist/core/extensions-host/index.d.ts +1 -1
  7. package/dist/core/extensions-host/loader.js +1 -1
  8. package/dist/core/extensions-host/runner.d.ts +1 -0
  9. package/dist/core/extensions-host/runner.js +2 -2
  10. package/dist/core/extensions-host/types.d.ts +17 -22
  11. package/dist/core/lib/ai/src/types.d.ts +12 -2
  12. package/dist/core/persona/persona-manager.js +5 -2
  13. package/dist/core/runtime/agent-session.js +3 -3
  14. package/dist/core/runtime/extension-core-bindings.d.ts +1 -0
  15. package/dist/core/runtime/extension-core-bindings.js +2 -2
  16. package/dist/extensions/builtin/AGENT.md +115 -115
  17. package/dist/extensions/builtin/browser/AGENT.md +17 -17
  18. package/dist/extensions/builtin/browser/agent-workspace/agent_helpers.py +12 -12
  19. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/amazon/product-search.md +198 -198
  20. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/archive-org/scraping.md +341 -341
  21. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv/scraping.md +311 -311
  22. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv-bulk/scraping.md +333 -333
  23. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/atlas/overview.md +70 -70
  24. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/booking-com/scraping.md +578 -578
  25. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/capterra/scraping.md +440 -440
  26. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/centilebrain/generate-estimates.md +110 -110
  27. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coingecko/scraping.md +325 -325
  28. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coinmarketcap/scraping.md +463 -463
  29. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coursera/scraping.md +360 -360
  30. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/craigslist/scraping.md +390 -390
  31. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/crossref/scraping.md +568 -568
  32. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/dev-to/scraping.md +323 -323
  33. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/duckduckgo/scraping.md +349 -349
  34. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/ebay/scraping.md +435 -435
  35. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/etsy/scraping.md +506 -506
  36. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/eventbrite/scraping.md +363 -363
  37. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/expedia/automation.md +168 -168
  38. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/groups.md +236 -236
  39. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/pages.md +295 -295
  40. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/framer/editor.md +108 -108
  41. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/fred/scraping.md +493 -493
  42. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/g2/scraping.md +580 -580
  43. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/genius/scraping.md +511 -511
  44. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/repo-actions.md +65 -65
  45. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/scraping.md +184 -184
  46. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/glassdoor/scraping.md +543 -543
  47. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gmail/compose.md +122 -122
  48. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/goodreads/scraping.md +461 -461
  49. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gutenberg/scraping.md +383 -383
  50. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/hackernews/scraping.md +243 -243
  51. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/howlongtobeat/scraping.md +473 -473
  52. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/imdb/scraping.md +271 -271
  53. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/itch-io/scraping.md +436 -436
  54. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/job-boards/indeed-glassdoor.md +1021 -1021
  55. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/letterboxd/scraping.md +349 -349
  56. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/linkedin/invitation-manager.md +109 -109
  57. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/loom/folder-enumeration.md +170 -170
  58. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/macrotrends/scraping.md +537 -537
  59. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/article-hydration.md +120 -120
  60. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/scraping.md +414 -414
  61. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/metacritic/scraping.md +477 -477
  62. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/musicbrainz/scraping.md +478 -478
  63. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/nasa/scraping.md +339 -339
  64. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/news-aggregation/multi-source.md +205 -205
  65. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/open-library/scraping.md +472 -472
  66. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openalex/scraping.md +470 -470
  67. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openstreetmap/scraping.md +490 -490
  68. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/package-registries/npm-pypi.md +478 -478
  69. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/polymarket/scraping.md +234 -234
  70. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/producthunt/scraping.md +307 -307
  71. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/pubmed/scraping.md +421 -421
  72. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/quora/scraping.md +364 -364
  73. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rawg/scraping.md +352 -352
  74. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/reddit/scraping.md +124 -124
  75. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rest-countries/scraping.md +233 -233
  76. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/sec-edgar/scraping.md +361 -361
  77. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/README.md +36 -36
  78. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/embedded-apps.md +72 -72
  79. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/knowledge-base.md +109 -109
  80. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/polaris-inputs.md +137 -137
  81. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/soundcloud/scraping.md +362 -362
  82. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/spotify/scraping.md +339 -339
  83. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/stackoverflow/scraping.md +435 -435
  84. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/steam/scraping.md +575 -575
  85. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/substack/scraping.md +338 -338
  86. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/thetechgeeks/pricing.md +52 -52
  87. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tiktok/upload.md +107 -107
  88. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tradingview/scraping.md +309 -309
  89. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trello/boards-and-lists.md +88 -88
  90. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trustpilot/scraping.md +375 -375
  91. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/walmart/scraping.md +444 -444
  92. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wayback-machine/scraping.md +306 -306
  93. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/weather/scraping.md +398 -398
  94. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wellfound/scraping.md +596 -596
  95. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/world-bank/scraping.md +356 -356
  96. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/xiaohongshu/scraping.md +84 -84
  97. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/youtube/scraping.md +418 -418
  98. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/zillow/scraping.md +433 -433
  99. package/dist/extensions/builtin/browser/browser.md +73 -73
  100. package/dist/extensions/builtin/browser/install.md +142 -142
  101. package/dist/extensions/builtin/browser/interaction-skills/connection.md +48 -48
  102. package/dist/extensions/builtin/browser/interaction-skills/cookies.md +3 -3
  103. package/dist/extensions/builtin/browser/interaction-skills/cross-origin-iframes.md +3 -3
  104. package/dist/extensions/builtin/browser/interaction-skills/dialogs.md +64 -64
  105. package/dist/extensions/builtin/browser/interaction-skills/downloads.md +3 -3
  106. package/dist/extensions/builtin/browser/interaction-skills/drag-and-drop.md +3 -3
  107. package/dist/extensions/builtin/browser/interaction-skills/dropdowns.md +3 -3
  108. package/dist/extensions/builtin/browser/interaction-skills/iframes.md +3 -3
  109. package/dist/extensions/builtin/browser/interaction-skills/network-requests.md +3 -3
  110. package/dist/extensions/builtin/browser/interaction-skills/print-as-pdf.md +3 -3
  111. package/dist/extensions/builtin/browser/interaction-skills/profile-sync.md +90 -90
  112. package/dist/extensions/builtin/browser/interaction-skills/screenshots.md +17 -17
  113. package/dist/extensions/builtin/browser/interaction-skills/scrolling.md +3 -3
  114. package/dist/extensions/builtin/browser/interaction-skills/shadow-dom.md +3 -3
  115. package/dist/extensions/builtin/browser/interaction-skills/tabs.md +69 -69
  116. package/dist/extensions/builtin/browser/interaction-skills/uploads.md +1 -1
  117. package/dist/extensions/builtin/browser/interaction-skills/viewport.md +3 -3
  118. package/dist/extensions/builtin/browser/src/browser_harness/AGENT.md +15 -15
  119. package/dist/extensions/builtin/browser/src/browser_harness/__init__.py +8 -8
  120. package/dist/extensions/builtin/browser/src/browser_harness/_ipc.py +90 -90
  121. package/dist/extensions/builtin/browser/src/browser_harness/admin.py +722 -722
  122. package/dist/extensions/builtin/browser/src/browser_harness/daemon.py +328 -328
  123. package/dist/extensions/builtin/browser/src/browser_harness/helpers.py +396 -396
  124. package/dist/extensions/builtin/browser/src/browser_harness/run.py +103 -103
  125. package/dist/extensions/builtin/discipline/skills/brainstorming/SKILL.md +33 -33
  126. package/dist/extensions/builtin/discipline/skills/executing-plans/SKILL.md +25 -25
  127. package/dist/extensions/builtin/discipline/skills/finishing-development-branch/SKILL.md +25 -25
  128. package/dist/extensions/builtin/discipline/skills/receiving-code-review/SKILL.md +22 -22
  129. package/dist/extensions/builtin/discipline/skills/requesting-code-review/SKILL.md +31 -31
  130. package/dist/extensions/builtin/discipline/skills/systematic-debugging/SKILL.md +28 -28
  131. package/dist/extensions/builtin/discipline/skills/test-driven-development/SKILL.md +32 -32
  132. package/dist/extensions/builtin/discipline/skills/using-git-worktrees/SKILL.md +25 -25
  133. package/dist/extensions/builtin/discipline/skills/verification-before-completion/SKILL.md +27 -27
  134. package/dist/extensions/builtin/discipline/skills/writing-plans/SKILL.md +26 -26
  135. package/dist/extensions/builtin/goal/README.md +67 -67
  136. package/dist/extensions/builtin/goal/goal-controller.d.ts +39 -10
  137. package/dist/extensions/builtin/goal/goal-controller.js +1 -1
  138. package/dist/extensions/builtin/goal/goal-format.js +1 -1
  139. package/dist/extensions/builtin/goal/goal-prompts.d.ts +2 -0
  140. package/dist/extensions/builtin/goal/goal-prompts.js +5 -4
  141. package/dist/extensions/builtin/goal/goal-store.js +1 -1
  142. package/dist/extensions/builtin/goal/index.d.ts +1 -1
  143. package/dist/extensions/builtin/goal/index.js +10 -7
  144. package/dist/extensions/builtin/grub/README.md +112 -112
  145. package/dist/extensions/builtin/link-world/agent-workspace/README.md +16 -16
  146. package/dist/extensions/builtin/link-world/index.js +6 -6
  147. package/dist/extensions/builtin/link-world/internet-search/internet-search.md +65 -65
  148. package/dist/extensions/builtin/link-world/link-world-agent.md +82 -82
  149. package/dist/extensions/builtin/link-world/linkworld.md +313 -313
  150. package/dist/extensions/builtin/link-world/{network-routing.md → network-routing/network-routing.md} +67 -67
  151. package/dist/extensions/builtin/loop/README.md +92 -92
  152. package/dist/extensions/builtin/mcp/figma-design.md +68 -68
  153. package/dist/extensions/builtin/mcp/mcp-management.md +85 -85
  154. package/dist/extensions/builtin/plan/index.js +1 -1
  155. package/dist/extensions/builtin/recap/AGENT.md +15 -15
  156. package/dist/extensions/builtin/sal/README.md +72 -72
  157. package/dist/extensions/builtin/security-audit/README.md +289 -289
  158. package/dist/extensions/builtin/task/task-store.d.ts +4 -0
  159. package/dist/extensions/builtin/task/task-store.js +1 -1
  160. package/dist/extensions/builtin/team/AGENT.md +112 -112
  161. package/dist/extensions/builtin/team/TESTING.md +299 -299
  162. package/dist/extensions/builtin/token-save/README.md +56 -56
  163. package/dist/extensions/optional/AGENT.md +10 -10
  164. package/dist/index.d.ts +5 -30
  165. package/dist/index.js +1 -1
  166. package/dist/models.d.ts +7 -0
  167. package/dist/models.js +1 -0
  168. package/dist/modes/interactive/components/footer.js +1 -1
  169. package/dist/modes/interactive/components/task-status-panel.d.ts +36 -0
  170. package/dist/modes/interactive/components/task-status-panel.js +1 -0
  171. package/dist/modes/interactive/controllers/stream-render-controller.d.ts +7 -0
  172. package/dist/modes/interactive/controllers/stream-render-controller.js +2 -2
  173. package/dist/modes/interactive/interactive-mode.js +40 -40
  174. package/dist/modes/interactive/state/interactive-state.d.ts +2 -0
  175. package/dist/modes/interactive/state/interactive-state.js +1 -1
  176. package/dist/modes/interactive/theme/dark.json +85 -85
  177. package/dist/modes/interactive/theme/light.json +84 -84
  178. package/dist/modes/interactive/theme/theme-schema.json +335 -335
  179. package/dist/modes/interactive/theme/warm.json +81 -81
  180. package/dist/node_modules/@pencil-agent/ai/dist/cli.js +0 -0
  181. package/dist/node_modules/@pencil-agent/ai/dist/models.generated.js +1 -1
  182. package/dist/node_modules/@pencil-agent/ai/dist/providers/anthropic.js +2 -2
  183. package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-completions.js +5 -5
  184. package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-responses.js +1 -1
  185. package/dist/node_modules/@pencil-agent/ai/dist/stream.js +1 -1
  186. package/dist/packages/protocol/src/commands.d.ts +33 -0
  187. package/dist/packages/protocol/src/flags.d.ts +20 -0
  188. package/dist/packages/protocol/src/hooks.d.ts +17 -0
  189. package/dist/packages/protocol/src/hooks.js +0 -0
  190. package/dist/packages/{extension-sdk → protocol}/src/index.d.ts +7 -4
  191. package/dist/packages/protocol/src/index.js +1 -0
  192. package/dist/packages/{extension-sdk → protocol}/src/lifecycle.d.ts +15 -27
  193. package/dist/packages/protocol/src/lifecycle.js +0 -0
  194. package/dist/packages/{extension-sdk → protocol}/src/tools.d.ts +1 -1
  195. package/dist/packages/protocol/src/tools.js +0 -0
  196. package/dist/public-config.d.ts +12 -0
  197. package/dist/public-config.js +1 -0
  198. package/dist/runtime.d.ts +9 -0
  199. package/dist/runtime.js +1 -0
  200. package/dist/session-compaction.d.ts +7 -0
  201. package/dist/session-compaction.js +1 -0
  202. package/dist/session.d.ts +7 -0
  203. package/dist/session.js +1 -0
  204. package/dist/skills.d.ts +7 -0
  205. package/dist/skills.js +1 -0
  206. package/dist/tools.d.ts +7 -0
  207. package/dist/tools.js +1 -0
  208. package/docs/ACP/345/215/217/350/256/256/351/233/206/346/210/220/345/274/200/345/217/221/346/226/207/346/241/243.md +851 -0
  209. package/docs/SDK-TESTING.md +364 -0
  210. package/docs/codex-goal-command-impl.md +1055 -1055
  211. package/docs/codex-goal-vs-grub.md +500 -500
  212. package/docs/custom-provider.md +27 -27
  213. package/docs/extensions.md +27 -27
  214. package/docs/keybindings.md +27 -27
  215. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +250 -250
  216. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +122 -122
  217. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -1222
  218. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/256/236/347/216/260/346/212/245/345/221/212.md" +158 -158
  219. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/257/271/346/257/224/345/210/206/346/236/220.md" +128 -128
  220. package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +320 -320
  221. package/docs/loop-usage-examples.md +214 -214
  222. package/docs/mem-core/346/212/200/346/234/257/346/226/207/346/241/243.md +593 -0
  223. package/docs/models.md +27 -27
  224. package/docs/packages.md +27 -27
  225. package/docs/pi-design-philosophy.md +457 -457
  226. package/docs/planmode.md +1987 -1987
  227. package/docs/prompt-templates.md +27 -27
  228. package/docs/providers.md +27 -27
  229. package/docs/sdk.md +27 -27
  230. package/docs/skills.md +27 -27
  231. package/docs/startup-performance-optimization.md +301 -0
  232. package/docs/themes.md +27 -27
  233. package/docs/tui.md +27 -27
  234. package/docs//350/256/244/347/237/245/345/234/260/345/233/276.md +47 -0
  235. package/package.json +190 -162
  236. package/dist/packages/extension-sdk/src/index.js +0 -1
  237. package/docs/cc-agent-design.md +0 -1297
  238. package/docs/cc-tui-design.md +0 -1333
  239. package/docs//345/257/271/346/240/207Claude-Code.md +0 -1775
  240. /package/dist/packages/{extension-sdk/src/lifecycle.js → protocol/src/commands.js} +0 -0
  241. /package/dist/packages/{extension-sdk/src/tools.js → protocol/src/flags.js} +0 -0
@@ -1,1055 +1,1055 @@
1
- # Codex `/goal` 命令实现:抽丝剥骨的完整逻辑
2
-
3
- > 目标:一个笨模型读完本文档后,能在 TypeScript 下完整复刻 Codex 的 `/goal` 命令。
4
- > 所有逻辑均来自 OpenAI Codex CLI 源码(`codex-rs/`),逐行提取,零猜测。
5
-
6
- ---
7
-
8
- ## 一、Goal 是什么
9
-
10
- Goal 是 Codex 的**长期任务管理机制**。用户通过 `/goal <objective>` 设置一个目标,agent 会在 idle 时自动继续工作,直到:
11
- - 目标完成(agent 调用 `update_goal` 标记 `complete`)
12
- - token 预算耗尽(系统自动标记 `budget_limited`)
13
- - 被用户暂停(`/goal pause`)
14
- - agent 判断被阻塞(agent 调用 `update_goal` 标记 `blocked`)
15
- - 用量限制(系统标记 `usage_limited`)
16
-
17
- **关键特性**:Goal 持跨 turn 存活。一个 goal 可能驱动数十个 turn 的自动续作。
18
-
19
- ---
20
-
21
- ## 二、数据模型
22
-
23
- ### 2.1 状态枚举
24
-
25
- ```typescript
26
- type ThreadGoalStatus =
27
- | "active" // 正在执行
28
- | "paused" // 用户暂停
29
- | "blocked" // agent 判断阻塞(需 3 次连续阻塞 turn 才标记)
30
- | "usage_limited" // 系统用量限制
31
- | "budget_limited" // token 预算耗尽(终态)
32
- | "complete"; // 完成(终态)
33
- ```
34
-
35
- **分类**:
36
- - `is_active()`: `status === "active"`
37
- - `is_terminal()`: `status === "budget_limited" || status === "complete"`
38
-
39
- ### 2.2 Goal 实体
40
-
41
- ```typescript
42
- interface ThreadGoal {
43
- thread_id: string; // 所属会话 ID
44
- goal_id: string; // UUID v4,每次 replace/insert 生成新 ID
45
- objective: string; // 用户设定的目标描述
46
- status: ThreadGoalStatus;
47
- token_budget: number | null; // token 预算上限,null 表示无限
48
- tokens_used: number; // 已消耗 token 数
49
- time_used_seconds: number; // 已消耗时间(秒)
50
- created_at: number; // epoch 毫秒
51
- updated_at: number; // epoch 毫秒
52
- }
53
- ```
54
-
55
- ### 2.3 数据库表
56
-
57
- ```sql
58
- CREATE TABLE thread_goals (
59
- thread_id TEXT PRIMARY KEY NOT NULL,
60
- goal_id TEXT NOT NULL,
61
- objective TEXT NOT NULL,
62
- status TEXT NOT NULL CHECK(status IN (
63
- 'active', 'paused', 'blocked',
64
- 'usage_limited', 'budget_limited', 'complete'
65
- )),
66
- token_budget INTEGER, -- nullable
67
- tokens_used INTEGER NOT NULL DEFAULT 0,
68
- time_used_seconds INTEGER NOT NULL DEFAULT 0,
69
- created_at_ms INTEGER NOT NULL, -- epoch 毫秒
70
- updated_at_ms INTEGER NOT NULL -- epoch 毫秒
71
- );
72
- ```
73
-
74
- **关键约束**:`thread_id` 是主键,每个 thread 最多一个 goal。
75
-
76
- ---
77
-
78
- ## 三、命令解析
79
-
80
- ### 3.1 命令格式
81
-
82
- ```
83
- /goal → 显示当前 goal 摘要菜单
84
- /goal <objective> → 设置/替换 goal
85
- /goal clear → 清除 goal
86
- /goal edit → 打开编辑器修改 objective
87
- /goal pause → 暂停 goal
88
- /goal resume → 恢复 paused goal
89
- ```
90
-
91
- ### 3.2 解析逻辑(伪代码)
92
-
93
- ```typescript
94
- function dispatchGoalCommand(input: string, threadId: string | null): void {
95
- // 特性门禁检查
96
- if (!features.enabled("Goals")) return;
97
-
98
- const trimmed = input.trim();
99
-
100
- // 裸 /goal → 显示菜单
101
- if (trimmed === "") {
102
- if (threadId) {
103
- emit(AppEvent.OpenThreadGoalMenu, { threadId });
104
- } else {
105
- showInfo(GOAL_USAGE, "No goal is currently set.");
106
- }
107
- return;
108
- }
109
-
110
- // 子命令分派
111
- const lower = trimmed.toLowerCase();
112
-
113
- if (lower === "clear") {
114
- if (!threadId) { showUsage(); return; }
115
- emit(AppEvent.ClearThreadGoal, { threadId });
116
- return;
117
- }
118
-
119
- if (lower === "edit") {
120
- emit(AppEvent.OpenThreadGoalEditor, { threadId });
121
- return;
122
- }
123
-
124
- if (lower === "pause") {
125
- if (!threadId) { showUsage(); return; }
126
- emit(AppEvent.SetThreadGoalStatus, { threadId, status: "paused" });
127
- return;
128
- }
129
-
130
- if (lower === "resume") {
131
- if (!threadId) { showUsage(); return; }
132
- emit(AppEvent.SetThreadGoalStatus, { threadId, status: "active" });
133
- return;
134
- }
135
-
136
- // 其余文本 → 当作 objective
137
- const objective = trimmed;
138
- if (objective === "") {
139
- showError("Goal objective must not be empty.");
140
- return;
141
- }
142
-
143
- // 长度验证
144
- if (objective.length > MAX_THREAD_GOAL_OBJECTIVE_CHARS) {
145
- showError(`Goal objective is too long: ${objective.length} characters. Limit: ${MAX_THREAD_GOAL_OBJECTIVE_CHARS}.`);
146
- return;
147
- }
148
-
149
- if (!threadId) {
150
- // session 未启动,排队等待
151
- queueUserMessage(`/goal ${input}`, QueuedInputAction.ParseSlash);
152
- return;
153
- }
154
-
155
- emit(AppEvent.SetThreadGoalObjective, {
156
- threadId,
157
- objective,
158
- mode: "ConfirmIfExists",
159
- });
160
- }
161
- ```
162
-
163
- ---
164
-
165
- ## 四、核心操作流程
166
-
167
- ### 4.1 设置 Goal(`SetThreadGoalObjective`)
168
-
169
- 这是最复杂的操作。流程如下:
170
-
171
- ```
172
- 用户输入 /goal <objective>
173
-
174
- 解析为 SetThreadGoalObjective { threadId, objective, mode: "ConfirmIfExists" }
175
-
176
- set_thread_goal_objective()
177
-
178
- ┌─ mode === "ConfirmIfExists"?
179
- │ ├─ YES → 读取现有 goal
180
- │ │ ├─ 有 goal 且非 Complete → 弹出确认对话框 "Replace goal?"
181
- │ │ │ ├─ 用户选 "Replace" → 重新发送 SetThreadGoalObjective { mode: "ReplaceExisting" }
182
- │ │ │ └─ 用户选 "Cancel" → 结束
183
- │ │ ├─ 有 goal 且是 Complete → 直接 ReplaceExisting(不需确认)
184
- │ │ └─ 无 goal → 保持 ConfirmIfExists
185
- │ └─ NO → 继续
186
-
187
- ├─ mode === "ReplaceExisting"?
188
- │ ├─ YES → 先调用 thread_goal_clear() 删除旧 goal
189
- │ └─ NO → 继续
190
-
191
- ├─ 确定 status 和 token_budget:
192
- │ ├─ ConfirmIfExists / ReplaceExisting → (Active, null)
193
- │ └─ UpdateExisting → (status, token_budget)
194
-
195
- ├─ 调用 app_server.thread_goal_set(threadId, objective, status, token_budget)
196
- │ ↓
197
- │ GoalService.set_thread_goal()
198
- │ ↓
199
- │ 1. 验证 objective(trim + 长度检查)
200
- │ 2. 验证 token_budget(必须 > 0 如果提供)
201
- │ 3. 获取 runtime 的 goal_state_permit(防止 idle 续作冲突)
202
- │ 4. prepare_external_goal_mutation()
203
- │ ↓
204
- │ ┌─ 有 objective?
205
- │ │ ├─ 读取现有 goal
206
- │ │ │ ├─ 有 → update_thread_goal()(用 expected_goal_id 乐观锁)
207
- │ │ │ └─ 无 → replace_thread_goal()(INSERT OR REPLACE,重置 usage)
208
- │ │ └─ 无 objective?
209
- │ │ ├─ 读取现有 goal(必须存在)
210
- │ │ └─ update_thread_goal()(只改 status/budget)
211
- │ ↓
212
- │ 5. 如果改了 objective → fill_empty_thread_preview_if_possible()
213
- │ 6. 返回 GoalSetOutcome
214
-
215
- └─ 显示结果: "Goal active" + usage summary
216
- ```
217
-
218
- ### 4.2 清除 Goal(`ClearThreadGoal`)
219
-
220
- ```
221
- 用户输入 /goal clear
222
-
223
- clear_thread_goal()
224
-
225
- 1. 获取 goal_state_permit
226
- 2. prepare_external_goal_mutation()
227
- 3. delete_thread_goal(thread_id) → DELETE FROM thread_goals WHERE thread_id = ?
228
- 4. 释放 permit
229
- 5. apply_external_goal_clear() → 清除 runtime 状态
230
- 6. 返回 cleared: boolean
231
- ```
232
-
233
- ### 4.3 暂停/恢复(`SetThreadGoalStatus`)
234
-
235
- ```
236
- 用户输入 /goal pause 或 /goal resume
237
-
238
- set_thread_goal_status(thread_id, status)
239
-
240
- app_server.thread_goal_set(thread_id, null, status, null)
241
-
242
- GoalService.set_thread_goal() → 只更新 status 字段
243
- ```
244
-
245
- **暂停的特殊语义**:
246
- - `pause_active_thread_goal()` 只更新 `status = 'active'` 的行
247
- - 但如果目标是 `usage_limited`,也允许更新为 `paused`(覆盖 `budget_limited`)
248
-
249
- ### 4.4 编辑 Goal(`OpenThreadGoalEditor`)
250
-
251
- ```
252
- 用户输入 /goal edit
253
-
254
- 1. 读取现有 goal
255
- 2. 如果无 goal → 显示 "No goal is currently set."
256
- 3. 有 goal → 显示编辑器,预填当前 objective
257
- 4. 用户提交 → 发送 SetThreadGoalObjective { mode: "UpdateExisting", status, token_budget }
258
- ```
259
-
260
- **关键逻辑**:编辑时保留原始 status,除非原 status 是 `budget_limited` 或 `complete`,此时重置为 `active`:
261
-
262
- ```typescript
263
- function editedGoalStatus(status: ThreadGoalStatus): ThreadGoalStatus {
264
- switch (status) {
265
- case "active": return "active";
266
- case "paused":
267
- case "blocked":
268
- case "usage_limited": return status; // 保留
269
- case "budget_limited":
270
- case "complete": return "active"; // 重置
271
- }
272
- }
273
- ```
274
-
275
- ### 4.5 显示 Goal 摘要(`/goal` 裸命令)
276
-
277
- ```
278
- /goal
279
-
280
- open_thread_goal_menu()
281
-
282
- 1. app_server.thread_goal_get(thread_id)
283
- 2. 无 goal → 显示 GOAL_USAGE + "No goal is currently set."
284
- 3. 有 goal → show_goal_summary(goal):
285
- ┌─────────────────────────────────┐
286
- │ Goal │
287
- │ Status: active │
288
- │ Objective: Fix the bug in auth │
289
- │ Time used: 2m │
290
- │ Tokens used: 12.5K │
291
- │ Token budget: 50K │
292
- │ │
293
- │ Commands: /goal edit, │
294
- │ /goal pause, │
295
- │ /goal clear │
296
- └─────────────────────────────────┘
297
- ```
298
-
299
- ---
300
-
301
- ## 五、数据库操作详解
302
-
303
- ### 5.1 `replace_thread_goal` — 创建/完全替换
304
-
305
- ```typescript
306
- async function replace_thread_goal(
307
- threadId: string,
308
- objective: string,
309
- status: ThreadGoalStatus,
310
- tokenBudget: number | null
311
- ): Promise<ThreadGoal> {
312
- const goalId = uuid();
313
- const now = Date.now();
314
- // 如果 status 是 active 但预算已耗尽,立即降级
315
- status = statusAfterBudgetLimit(status, 0, tokenBudget);
316
-
317
- // INSERT OR REPLACE:重置所有 usage 计数
318
- await db.run(`
319
- INSERT INTO thread_goals (thread_id, goal_id, objective, status,
320
- token_budget, tokens_used, time_used_seconds, created_at_ms, updated_at_ms)
321
- VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?)
322
- ON CONFLICT(thread_id) DO UPDATE SET
323
- goal_id = excluded.goal_id,
324
- objective = excluded.objective,
325
- status = excluded.status,
326
- token_budget = excluded.token_budget,
327
- tokens_used = 0,
328
- time_used_seconds = 0,
329
- created_at_ms = excluded.created_at_ms,
330
- updated_at_ms = excluded.updated_at_ms
331
- `, [threadId, goalId, objective, status, tokenBudget, now, now]);
332
-
333
- return get_thread_goal(threadId);
334
- }
335
- ```
336
-
337
- ### 5.2 `insert_thread_goal` — 仅在 goal 已完成时替换
338
-
339
- ```typescript
340
- async function insert_thread_goal(
341
- threadId: string,
342
- objective: string,
343
- status: ThreadGoalStatus,
344
- tokenBudget: number | null
345
- ): Promise<ThreadGoal | null> {
346
- const goalId = uuid();
347
- const now = Date.now();
348
- status = statusAfterBudgetLimit(status, 0, tokenBudget);
349
-
350
- // 关键区别:WHERE thread_goals.status = 'complete'
351
- // 只有已完成的 goal 才会被替换
352
- const result = await db.run(`
353
- INSERT INTO thread_goals (...)
354
- VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?)
355
- ON CONFLICT(thread_id) DO UPDATE SET
356
- ...
357
- WHERE thread_goals.status = 'complete'
358
- `, [...]);
359
-
360
- // 如果 affected rows === 0,说明有未完成的 goal,返回 null
361
- return result.changes > 0 ? get_thread_goal(threadId) : null;
362
- }
363
- ```
364
-
365
- ### 5.3 `update_thread_goal` — 增量更新
366
-
367
- ```typescript
368
- async function update_thread_goal(
369
- threadId: string,
370
- update: {
371
- objective?: string;
372
- status?: ThreadGoalStatus;
373
- tokenBudget?: number | null; // null = 不改, Some(null) = 清除
374
- expectedGoalId?: string; // 乐观锁
375
- }
376
- ): Promise<ThreadGoal | null> {
377
- const now = Date.now();
378
-
379
- // 核心 SQL 逻辑(简化版):
380
- await db.run(`
381
- UPDATE thread_goals SET
382
- objective = COALESCE(?, objective),
383
- status = CASE
384
- -- 如果当前是 budget_limited 且新 status 是 paused/blocked,保留当前
385
- WHEN status = 'budget_limited' AND ? IN ('paused', 'blocked') THEN status
386
- -- 如果新 status 是 active 且预算已超,强制 budget_limited
387
- WHEN ? = 'active' AND token_budget IS NOT NULL AND tokens_used >= token_budget
388
- THEN 'budget_limited'
389
- ELSE ?
390
- END,
391
- token_budget = ?, -- 如果提供
392
- updated_at_ms = ?
393
- WHERE thread_id = ?
394
- AND (? IS NULL OR goal_id = ?) -- 乐观锁
395
- `, [...]);
396
-
397
- if (result.changes === 0) return null;
398
- return get_thread_goal(threadId);
399
- }
400
- ```
401
-
402
- ### 5.4 `account_thread_goal_usage` — 记账(最关键)
403
-
404
- 每次 turn 结束或 tool 完成时调用,累加 token 和时间。
405
-
406
- ```typescript
407
- async function account_thread_goal_usage(
408
- threadId: string,
409
- timeDeltaSeconds: number, // ≥ 0
410
- tokenDelta: number, // ≥ 0
411
- mode: GoalAccountingMode,
412
- expectedGoalId?: string
413
- ): Promise<GoalAccountingOutcome> {
414
- // 零增量直接返回
415
- if (timeDeltaSeconds === 0 && tokenDelta === 0) {
416
- return { kind: "Unchanged", goal: await get_thread_goal(threadId) };
417
- }
418
-
419
- // mode 决定哪些 status 的 goal 会被更新
420
- const statusFilter = {
421
- ActiveStatusOnly: "status = 'active'",
422
- ActiveOnly: "status IN ('active', 'budget_limited')",
423
- ActiveOrComplete: "status IN ('active', 'budget_limited', 'complete')",
424
- ActiveOrStopped: "status IN ('active', 'paused', 'blocked', 'usage_limited', 'budget_limited')",
425
- }[mode];
426
-
427
- // 预算限制检查的 status 范围
428
- const budgetCheckFilter = {
429
- ActiveStatusOnly: "status = 'active'",
430
- ActiveOnly: "status = 'active'",
431
- ActiveOrComplete: "status = 'active'",
432
- ActiveOrStopped: "status IN ('active', 'paused', 'blocked', 'usage_limited', 'budget_limited')",
433
- }[mode];
434
-
435
- const result = await db.run(`
436
- UPDATE thread_goals SET
437
- time_used_seconds = time_used_seconds + ?,
438
- tokens_used = tokens_used + ?,
439
- status = CASE
440
- WHEN ${budgetCheckFilter}
441
- AND token_budget IS NOT NULL
442
- AND tokens_used + ? >= token_budget
443
- THEN 'budget_limited'
444
- ELSE status
445
- END,
446
- updated_at_ms = ?
447
- WHERE thread_id = ? AND ${statusFilter}
448
- ${expectedGoalId ? "AND goal_id = ?" : ""}
449
- RETURNING *
450
- `, [...]);
451
-
452
- if (result.changes === 0) {
453
- return { kind: "Unchanged", goal: await get_thread_goal(threadId) };
454
- }
455
- return { kind: "Updated", goal: rowToGoal(result.row) };
456
- }
457
- ```
458
-
459
- ### 5.5 `statusAfterBudgetLimit` — 预算检查辅助
460
-
461
- ```typescript
462
- function statusAfterBudgetLimit(
463
- status: ThreadGoalStatus,
464
- tokensUsed: number,
465
- tokenBudget: number | null
466
- ): ThreadGoalStatus {
467
- if (status === "active" && tokenBudget !== null && tokensUsed >= tokenBudget) {
468
- return "budget_limited";
469
- }
470
- return status;
471
- }
472
- ```
473
-
474
- ---
475
-
476
- ## 六、自动续作机制(Idle Continuation)
477
-
478
- 这是 Goal 的核心魔法:当 agent 完成一个 turn 后 idle 时,如果有 active goal,会自动注入续作 prompt 继续工作。
479
-
480
- ### 6.1 触发链路
481
-
482
- ```
483
- Agent turn 结束
484
-
485
- on_thread_idle()
486
-
487
- runtime.continue_if_idle()
488
-
489
- 1. 检查 goal 是否 active
490
- 2. 读取 goal 状态
491
- 3. 注入 continuation prompt
492
- 4. 触发新 turn
493
- ```
494
-
495
- ### 6.2 续作 Prompt(continuation.md)
496
-
497
- ```
498
- Continue working toward the active thread goal.
499
-
500
- <objective>
501
- {{ objective }}
502
- </objective>
503
-
504
- Continuation behavior:
505
- - This goal persists across turns. Ending this turn does not require
506
- shrinking the objective to what fits now.
507
- - Keep the full objective intact. If it cannot be finished now, make
508
- concrete progress toward the real requested end state, leave the
509
- goal active, and do not redefine success around a smaller or easier task.
510
- - Temporary rough edges are acceptable while the work is moving in
511
- the right direction. Completion still requires the requested end state
512
- to be true and verified.
513
-
514
- Budget:
515
- - Tokens used: {{ tokens_used }}
516
- - Token budget: {{ token_budget }}
517
- - Tokens remaining: {{ remaining_tokens }}
518
-
519
- Work from evidence:
520
- Use the current worktree and external state as authoritative.
521
-
522
- Fidelity:
523
- - Optimize each turn for movement toward the requested end state.
524
- - Do not substitute a narrower, safer, smaller solution.
525
-
526
- Completion audit:
527
- Before deciding that the goal is achieved, treat completion as unproven
528
- and verify it against the actual current state:
529
- - Derive concrete requirements from the objective.
530
- - Preserve the original scope; do not redefine success.
531
- - For every explicit requirement, identify authoritative evidence.
532
- - Treat uncertain or indirect evidence as not achieved.
533
- - Mark complete only when current evidence proves every requirement.
534
-
535
- Blocked audit:
536
- - Do not call update_goal with "blocked" the first time a blocker appears.
537
- - Only use "blocked" when the same condition has repeated for 3+ consecutive turns.
538
- - Never use "blocked" merely because work is hard or slow.
539
- ```
540
-
541
- ### 6.3 Budget Limit Prompt(budget_limit.md)
542
-
543
- 当 token 预算耗尽时注入:
544
-
545
- ```
546
- The active thread goal has reached its token budget.
547
-
548
- <objective>
549
- {{ objective }}
550
- </objective>
551
-
552
- Budget:
553
- - Time spent: {{ time_used_seconds }} seconds
554
- - Tokens used: {{ tokens_used }}
555
- - Token budget: {{ token_budget }}
556
-
557
- The system has marked the goal as budget_limited, so do not start new
558
- substantive work for this goal. Wrap up this turn soon: summarize useful
559
- progress, identify remaining work or blockers, and leave the user with
560
- a clear next step.
561
-
562
- Do not call update_goal unless the goal is actually complete.
563
- ```
564
-
565
- ---
566
-
567
- ## 七、扩展系统集成(Extension)
568
-
569
- ### 7.1 工具注册
570
-
571
- Goal 扩展注册了 3 个 LLM 工具:
572
-
573
- | 工具名 | 用途 | 谁调用 |
574
- |--------|------|--------|
575
- | `get_goal` | 读取当前 goal | LLM |
576
- | `create_goal` | 创建新 goal | LLM(仅用户明确要求时) |
577
- | `update_goal` | 更新 goal status | LLM(仅 complete 或 blocked) |
578
-
579
- **关键约束**:LLM 只能标记 `complete` 或 `blocked`,不能 `pause`/`resume`/`budget_limited`。
580
-
581
- ### 7.2 工具 Schema
582
-
583
- ```typescript
584
- // get_goal: 无参数
585
- const getGoalTool = {
586
- name: "get_goal",
587
- description: "Get the current goal for this thread...",
588
- parameters: { type: "object", properties: {}, required: [] },
589
- };
590
-
591
- // create_goal
592
- const createGoalTool = {
593
- name: "create_goal",
594
- description: "Create a goal only when explicitly requested...",
595
- parameters: {
596
- type: "object",
597
- properties: {
598
- objective: { type: "string", description: "Required. The concrete objective..." },
599
- token_budget: { type: "integer", description: "Positive token budget..." },
600
- },
601
- required: ["objective"],
602
- },
603
- };
604
-
605
- // update_goal
606
- const updateGoalTool = {
607
- name: "update_goal",
608
- description: "Update the existing goal. Use only to mark complete or blocked...",
609
- parameters: {
610
- type: "object",
611
- properties: {
612
- status: {
613
- type: "string",
614
- enum: ["complete", "blocked"],
615
- description: "Set to 'complete' only when objective is achieved...",
616
- },
617
- },
618
- required: ["status"],
619
- },
620
- };
621
- ```
622
-
623
- ### 7.3 工具执行逻辑
624
-
625
- #### `create_goal` 执行
626
-
627
- ```typescript
628
- async function handleCreateGoal(args: { objective: string; token_budget?: number }) {
629
- args.objective = args.objective.trim();
630
- validateObjective(args.objective);
631
- validateBudget(args.token_budget);
632
-
633
- // insert_thread_goal:只有已完成的 goal 才会被替换
634
- const goal = await db.insert_thread_goal(
635
- threadId, args.objective, "active", args.token_budget
636
- );
637
-
638
- if (!goal) {
639
- throw new Error("cannot create a new goal because this thread has an unfinished goal");
640
- }
641
-
642
- // 设置线程预览(如果为空)
643
- await fillEmptyThreadPreview(threadId, goal.objective);
644
-
645
- // 标记当前 turn 的 goal 为活跃
646
- accounting.markCurrentTurnGoalActive(goal.goal_id);
647
-
648
- return goalResponse(goal);
649
- }
650
- ```
651
-
652
- #### `update_goal` 执行
653
-
654
- ```typescript
655
- async function handleUpdateGoal(args: { status: "complete" | "blocked" }) {
656
- // 先记账当前进度
657
- await accountActiveGoalProgress(
658
- args.status === "complete" ? "ActiveOrComplete" : "ActiveOrStopped",
659
- callId,
660
- BudgetLimitedGoalDisposition.ClearActive
661
- );
662
-
663
- // 更新 status
664
- const goal = await db.update_thread_goal(threadId, {
665
- status: args.status,
666
- expectedGoalId: null, // 不需要乐观锁
667
- });
668
-
669
- if (!goal) throw new Error("cannot update goal because this thread has no goal");
670
-
671
- // 清除当前 turn 的 goal 活跃标记
672
- accounting.clearCurrentTurnGoal();
673
-
674
- // complete 时附带 usage 报告
675
- return goalResponse(goal, args.status === "complete" ? "Include" : "Omit");
676
- }
677
- ```
678
-
679
- ### 7.4 生命周期钩子
680
-
681
- ```typescript
682
- // turn 开始时
683
- on_turn_start(turnId) {
684
- accounting.start_turn(turnId, mode, tokenUsageAtStart);
685
- if (mode === "Plan") {
686
- accounting.clearCurrentTurnGoal(); // Plan mode 不计 goal
687
- return;
688
- }
689
- const goal = await db.get_thread_goal(threadId);
690
- if (goal && (goal.status === "active" || goal.status === "budget_limited")) {
691
- accounting.markTurnGoalActive(turnId, goal.goal_id);
692
- }
693
- }
694
-
695
- // turn 结束时
696
- on_turn_stop(turnId) {
697
- await accountActiveGoalProgress(turnId, "ActiveOnly", ClearActive);
698
- accounting.finishTurn(turnId);
699
- }
700
-
701
- // token 使用时
702
- on_token_usage(tokenUsage) {
703
- accounting.recordTokenUsage(turnId, tokenUsage.total);
704
- }
705
-
706
- // tool 完成时
707
- on_tool_finish(toolName, outcome) {
708
- // 只有 Completed 和 Failed(handler_executed=true) 才计数
709
- // 跳过 update_goal 工具本身
710
- if (!shouldCount(outcome) || toolName === "update_goal") return;
711
-
712
- const progress = await accountActiveGoalProgress(turnId, "ActiveOnly", KeepActive);
713
-
714
- // 如果刚变为 budget_limited,注入 budget limit steering prompt
715
- if (progress.goal.status === "budget_limited") {
716
- if (accounting.markBudgetLimitReportedIfNew(progress.goal_id)) {
717
- injectSteeringItem(budgetLimitSteeringItem(progress.goal));
718
- }
719
- }
720
- }
721
-
722
- // thread idle 时(自动续作触发点)
723
- on_thread_idle() {
724
- await runtime.continue_if_idle();
725
- }
726
-
727
- // turn 出错时
728
- on_turn_error(error) {
729
- if (error === "UsageLimitExceeded") {
730
- await stopActiveGoal("UsageLimit"); // → usage_limited
731
- } else {
732
- await stopActiveGoal("TurnError"); // → blocked
733
- }
734
- }
735
- ```
736
-
737
- ---
738
-
739
- ## 八、Token 记账详解(Accounting)
740
-
741
- ### 8.1 记账状态
742
-
743
- ```typescript
744
- interface GoalAccountingState {
745
- currentTurnId: string | null;
746
- turns: Map<string, GoalTurnAccounting>;
747
- wallClock: GoalWallClockAccounting;
748
- budgetLimitReportedGoalId: string | null;
749
- }
750
-
751
- interface GoalTurnAccounting {
752
- currentTokenUsage: TokenUsage; // 当前累积
753
- lastAccountedTokenUsage: TokenUsage; // 上次记账时的快照
754
- activeGoalId: string | null;
755
- accountTokens: boolean;
756
- }
757
-
758
- interface GoalWallClockAccounting {
759
- lastAccountedAt: number; // Instant
760
- activeGoalId: string | null;
761
- }
762
- ```
763
-
764
- ### 8.2 记账流程
765
-
766
- ```
767
- Tool 完成 / Turn 结束
768
-
769
- accountActiveGoalProgress(turnId, mode, budgetDisposition)
770
-
771
- 1. 获取 progress_accounting_lock(Semaphore,防止并发记账)
772
- 2. progress_snapshot(turnId):
773
- - 计算 token_delta = currentTokenUsage.total - lastAccountedTokenUsage.total
774
- - 计算 time_delta = now - lastAccountedAt
775
- - 返回 { expectedGoalId, timeDelta, tokenDelta }
776
- 3. db.account_thread_goal_usage(threadId, timeDelta, tokenDelta, mode, expectedGoalId)
777
- 4. markProgressAccountedForStatus(turnId, snapshot, newStatus, disposition)
778
- - 更新 lastAccountedTokenUsage
779
- - 更新 lastAccountedAt
780
- - 如果 budget_limited 且 disposition === ClearActive → 清除 activeGoalId
781
- ```
782
-
783
- ### 8.3 记账模式
784
-
785
- | 模式 | 用途 | 影响的 status |
786
- |------|------|---------------|
787
- | `ActiveStatusOnly` | turn 中的 token 追踪 | 仅 `active` |
788
- | `ActiveOnly` | turn 结束/tool 完成 | `active`, `budget_limited` |
789
- | `ActiveOrComplete` | 标记 complete 时的最终记账 | `active`, `budget_limited`, `complete` |
790
- | `ActiveOrStopped` | 标记 blocked 时的记账 | 所有非终态 |
791
-
792
- ---
793
-
794
- ## 九、UI 显示逻辑
795
-
796
- ### 9.1 时间格式化
797
-
798
- ```typescript
799
- function formatGoalElapsedSeconds(seconds: number): string {
800
- seconds = Math.max(0, seconds);
801
- if (seconds < 60) return `${seconds}s`;
802
-
803
- const minutes = Math.floor(seconds / 60);
804
- if (minutes < 60) return `${minutes}m`;
805
-
806
- const hours = Math.floor(minutes / 60);
807
- const remainingMinutes = minutes % 60;
808
- if (hours >= 24) {
809
- const days = Math.floor(hours / 24);
810
- const remainingHours = hours % 24;
811
- return `${days}d ${remainingHours}h ${remainingMinutes}m`;
812
- }
813
-
814
- if (remainingMinutes === 0) return `${hours}h`;
815
- return `${hours}h ${remainingMinutes}m`;
816
- }
817
- ```
818
-
819
- ### 9.2 Status Line 指示器
820
-
821
- ```typescript
822
- function goalStatusIndicator(goal: GoalStatusState, now: Instant): GoalStatusIndicator | null {
823
- switch (goal.status) {
824
- case "active":
825
- // 如果有活跃 turn,加上当前 turn 的 elapsed time
826
- let displayGoal = { ...goal };
827
- if (activeTurnStartedAt) {
828
- const baseline = Math.max(goal.observedAt, activeTurnStartedAt);
829
- const activeSeconds = (now - baseline) / 1000;
830
- displayGoal.time_used_seconds += activeSeconds;
831
- }
832
- return {
833
- type: "Active",
834
- usage: goal.token_budget
835
- ? `${formatTokens(goal.tokens_used)} / ${formatTokens(goal.token_budget)}`
836
- : formatGoalElapsedSeconds(displayGoal.time_used_seconds),
837
- };
838
- case "paused": return { type: "Paused" };
839
- case "blocked": return { type: "Blocked" };
840
- case "usage_limited": return { type: "UsageLimited" };
841
- case "budget_limited":
842
- return {
843
- type: "BudgetLimited",
844
- usage: goal.token_budget
845
- ? `${formatTokens(goal.tokens_used)} / ${formatTokens(goal.token_budget)} tokens`
846
- : null,
847
- };
848
- case "complete":
849
- return {
850
- type: "Complete",
851
- usage: goal.token_budget
852
- ? `${formatTokens(goal.tokens_used)} tokens`
853
- : formatGoalElapsedSeconds(goal.time_used_seconds),
854
- };
855
- }
856
- }
857
- ```
858
-
859
- ### 9.3 替换确认对话框
860
-
861
- 当用户 `/goal <new objective>` 且已有未完成 goal 时:
862
-
863
- ```typescript
864
- function shouldConfirmBeforeReplacing(goal: ThreadGoal): boolean {
865
- // Complete 是终态,不需要确认
866
- if (goal.status === "complete") return false;
867
- // 其他所有状态都需要确认
868
- return true;
869
- }
870
-
871
- // 弹出选择:
872
- // ┌─────────────────────────────────┐
873
- // │ Replace goal? │
874
- // │ New objective: <new objective> │
875
- // │ │
876
- // │ > Replace current goal │
877
- // │ Set the new objective now │
878
- // │ Cancel │
879
- // │ Keep the current goal │
880
- // └─────────────────────────────────┘
881
- ```
882
-
883
- ---
884
-
885
- ## 十、完整状态机
886
-
887
- ```
888
- ┌──────────────────────────────┐
889
- │ 无 Goal │
890
- └──────────┬───────────────────┘
891
- │ /goal <objective>
892
- │ create_goal()
893
-
894
- ┌──────────────────────────────┐
895
- │ ACTIVE │
896
- │ (自动续作中) │
897
- └──┬────┬────┬────┬────┬───────┘
898
- │ │ │ │ │
899
- /goal pause│ │ │ │ │ update_goal(blocked)
900
- │ │ │ │ │ (3次连续阻塞后)
901
- ▼ │ │ │ ▼
902
- ┌────────┐ │ │ │ ┌─────────┐
903
- │ PAUSED │ │ │ │ │ BLOCKED │
904
- └───┬────┘ │ │ │ └────┬────┘
905
- │ │ │ │ │
906
- /goal resume │ │ │ │ /goal resume
907
- │ │ │ │ │
908
- ▼ │ │ │ ▼
909
- ┌──────────────────────────────┐
910
- │ ACTIVE │◄──── /goal resume
911
- └──┬────┬────┬────┬────┬───────┘ (从 paused/blocked/
912
- │ │ │ │ │ usage_limited 恢复)
913
- 超预算 │ │ │ │ │
914
- ▼ │ │ │ │
915
- ┌────────────┐│ │ │ │
916
- │ BUDGET_ ││ │ │ │
917
- │ LIMITED ││ │ │ │
918
- │ (终态) ││ │ │ │
919
- └────────────┘│ │ │ │
920
- │ │ │ │
921
- usage limit │ │ │ │
922
- ▼ │ │ │
923
- ┌──────────────┐│ │ │
924
- │ USAGE_ ││ │ │
925
- │ LIMITED ││ │ │
926
- └──────────────┘│ │ │
927
- │ │ │
928
- update_goal │ │ │
929
- (complete) │ │ │
930
- ▼ │ │
931
- ┌──────────────┐│ │
932
- │ COMPLETE ││ │
933
- │ (终态) ││ │
934
- └──────────────┘│ │
935
- │ │
936
- /goal clear │ │
937
- ▼ │
938
- ┌──────────────┐
939
- │ 无 Goal │
940
- └──────────────┘
941
- ```
942
-
943
- ---
944
-
945
- ## 十一、TypeScript 复刻清单
946
-
947
- 如果要从零实现,按以下顺序:
948
-
949
- ### 11.1 数据层
950
-
951
- - [ ] 定义 `ThreadGoalStatus` 类型(6 个值)
952
- - [ ] 定义 `ThreadGoal` 接口(9 个字段)
953
- - [ ] 建表 SQL(`thread_goals`,10 列)
954
- - [ ] 实现 `GoalStore` 类:
955
- - `get_goal(threadId)` → SELECT
956
- - `replace_goal(threadId, objective, status, tokenBudget)` → INSERT OR REPLACE(重置 usage)
957
- - `insert_goal(threadId, objective, status, tokenBudget)` → INSERT OR REPLACE(仅 status=complete 时覆盖)
958
- - `update_goal(threadId, update)` → UPDATE(带乐观锁 + 预算自动降级)
959
- - `delete_goal(threadId)` → DELETE
960
- - `account_usage(threadId, timeDelta, tokenDelta, mode, expectedGoalId)` → UPDATE(累加 + 预算检查)
961
- - `pause_active_goal(threadId)` → UPDATE status WHERE active
962
- - `usage_limit_active_goal(threadId)` → UPDATE status WHERE active OR budget_limited
963
-
964
- ### 11.2 命令层
965
-
966
- - [ ] 命令解析:`/goal [clear|edit|pause|resume|<objective>]`
967
- - [ ] 长度验证:objective 不超过 MAX_CHARS
968
- - [ ] 子命令分派:clear/edit/pause/resume/设置
969
- - [ ] ConfirmIfExists 逻辑:读取现有 → 非 Complete 弹确认
970
- - [ ] ReplaceExisting 逻辑:先 clear 再 set
971
-
972
- ### 11.3 工具层
973
-
974
- - [ ] `get_goal` 工具:无参数,返回当前 goal
975
- - [ ] `create_goal` 工具:参数 { objective, token_budget? }
976
- - 验证 objective 非空
977
- - 验证 budget > 0
978
- - 调用 insert_goal(未完成 goal 存在时报错)
979
- - 标记当前 turn goal 活跃
980
- - [ ] `update_goal` 工具:参数 { status: "complete"|"blocked" }
981
- - 先记账当前进度
982
- - 更新 status
983
- - 清除 turn goal 活跃标记
984
-
985
- ### 11.4 生命周期层
986
-
987
- - [ ] `on_turn_start`:读取 goal,标记活跃
988
- - [ ] `on_turn_stop`:记账 + 清除
989
- - [ ] `on_turn_abort`:记账 + 清除
990
- - [ ] `on_turn_error`:停止 goal(usage_limited 或 blocked)
991
- - [ ] `on_token_usage`:记录 token 增量
992
- - [ ] `on_tool_finish`:记账 + budget_limited 时注入 steering prompt
993
- - [ ] `on_thread_idle`:注入 continuation prompt,触发新 turn
994
-
995
- ### 11.5 Prompt 层
996
-
997
- - [ ] `continuation_prompt(goal)`:续作指令(含 objective、budget、completion audit、blocked audit)
998
- - [ ] `budget_limit_prompt(goal)`:预算耗尽收尾指令
999
- - [ ] `objective_updated_prompt(goal)`:objective 编辑后的新指令
1000
- - [ ] 三个模板的变量替换:`{{ objective }}`, `{{ tokens_used }}`, `{{ token_budget }}`, `{{ remaining_tokens }}`, `{{ time_used_seconds }}`
1001
-
1002
- ### 11.6 UI 层
1003
-
1004
- - [ ] `formatGoalElapsedSeconds(seconds)`:时间格式化(s/m/h/d)
1005
- - [ ] `goalStatusLabel(status)`:状态文本
1006
- - [ ] `goalUsageSummary(goal)`:一行摘要
1007
- - [ ] `goalSummaryLines(goal)`:多行详情 + 可用命令提示
1008
- - [ ] `goalStatusIndicator(goal, now, activeTurnStartedAt)`:status line 指示器
1009
- - [ ] `shouldConfirmBeforeReplacing(goal)`:替换确认判断
1010
- - [ ] `editedGoalStatus(status)`:编辑时的状态保留/重置逻辑
1011
- - [ ] 临时会话错误消息
1012
-
1013
- ### 11.7 边界条件
1014
-
1015
- - [ ] 临时会话(ephemeral)不支持 goal → 错误消息引导用户
1016
- - [ ] Plan mode 不计 goal
1017
- - [ ] Review subagent 不注册 goal 工具
1018
- - [ ] 并发记账用 Semaphore 保护
1019
- - [ ] `expected_goal_id` 乐观锁防止并发更新冲突
1020
- - [ ] `budget_limited` 是终态,不被 pause/blocked 覆盖(但可被 resume 从 paused/blocked 恢复)
1021
- - [ ] `usage_limited` 可以被 pause 覆盖
1022
- - [ ] turn 期间 idle continuation 不能与外部 goal mutation 冲突(goal_state_permit)
1023
-
1024
- ---
1025
-
1026
- ## 十二、源码文件索引
1027
-
1028
- | 文件 | 职责 |
1029
- |------|------|
1030
- | `state/src/model/thread_goal.rs` | 数据模型(ThreadGoal, ThreadGoalStatus, ThreadGoalRow) |
1031
- | `state/src/runtime/goals.rs` | 数据库操作(GoalStore, GoalUpdate, GoalAccountingMode) |
1032
- | `state/goals_migrations/0001_thread_goals.sql` | 建表 SQL |
1033
- | `ext/goal/src/lib.rs` | 模块导出 |
1034
- | `ext/goal/src/api.rs` | GoalService(TUI↔DB 桥梁) |
1035
- | `ext/goal/src/extension.rs` | 生命周期钩子注册(ThreadLifecycle, TurnLifecycle, TokenUsage, Tool) |
1036
- | `ext/goal/src/tool.rs` | LLM 工具执行(get/create/update_goal) |
1037
- | `ext/goal/src/spec.rs` | 工具 Schema 定义 |
1038
- | `ext/goal/src/accounting.rs` | Token/时间记账状态机 |
1039
- | `ext/goal/src/steering.rs` | Prompt 模板渲染 + steering 注入 |
1040
- | `ext/goal/src/runtime.rs` | GoalRuntimeHandle(线程级 runtime) |
1041
- | `ext/goal/src/events.rs` | 事件发射 |
1042
- | `ext/goal/src/analytics.rs` | 分析事件 |
1043
- | `ext/goal/src/metrics.rs` | 指标上报 |
1044
- | `prompts/templates/goals/continuation.md` | 续作 prompt 模板 |
1045
- | `prompts/templates/goals/budget_limit.md` | 预算耗尽 prompt 模板 |
1046
- | `prompts/templates/goals/objective_updated.md` | objective 编辑 prompt 模板 |
1047
- | `tui/src/slash_command.rs` | 命令注册(Goal 枚举变体) |
1048
- | `tui/src/chatwidget/slash_dispatch.rs` | 命令分派 |
1049
- | `tui/src/app_event.rs` | AppEvent 定义(5 个 goal 相关事件) |
1050
- | `tui/src/app/thread_goal_actions.rs` | TUI 层 goal 操作实现 |
1051
- | `tui/src/goal_display.rs` | 显示工具函数 |
1052
- | `tui/src/chatwidget/goal_menu.rs` | 摘要菜单 UI |
1053
- | `tui/src/chatwidget/goal_validation.rs` | objective 验证 |
1054
- | `tui/src/chatwidget/goal_status.rs` | Status line 指示器 |
1055
- | `app-server/src/request_processors/thread_goal_processor.rs` | JSON-RPC 处理器 |
1
+ # Codex `/goal` 命令实现:抽丝剥骨的完整逻辑
2
+
3
+ > 目标:一个笨模型读完本文档后,能在 TypeScript 下完整复刻 Codex 的 `/goal` 命令。
4
+ > 所有逻辑均来自 OpenAI Codex CLI 源码(`codex-rs/`),逐行提取,零猜测。
5
+
6
+ ---
7
+
8
+ ## 一、Goal 是什么
9
+
10
+ Goal 是 Codex 的**长期任务管理机制**。用户通过 `/goal <objective>` 设置一个目标,agent 会在 idle 时自动继续工作,直到:
11
+ - 目标完成(agent 调用 `update_goal` 标记 `complete`)
12
+ - token 预算耗尽(系统自动标记 `budget_limited`)
13
+ - 被用户暂停(`/goal pause`)
14
+ - agent 判断被阻塞(agent 调用 `update_goal` 标记 `blocked`)
15
+ - 用量限制(系统标记 `usage_limited`)
16
+
17
+ **关键特性**:Goal 持跨 turn 存活。一个 goal 可能驱动数十个 turn 的自动续作。
18
+
19
+ ---
20
+
21
+ ## 二、数据模型
22
+
23
+ ### 2.1 状态枚举
24
+
25
+ ```typescript
26
+ type ThreadGoalStatus =
27
+ | "active" // 正在执行
28
+ | "paused" // 用户暂停
29
+ | "blocked" // agent 判断阻塞(需 3 次连续阻塞 turn 才标记)
30
+ | "usage_limited" // 系统用量限制
31
+ | "budget_limited" // token 预算耗尽(终态)
32
+ | "complete"; // 完成(终态)
33
+ ```
34
+
35
+ **分类**:
36
+ - `is_active()`: `status === "active"`
37
+ - `is_terminal()`: `status === "budget_limited" || status === "complete"`
38
+
39
+ ### 2.2 Goal 实体
40
+
41
+ ```typescript
42
+ interface ThreadGoal {
43
+ thread_id: string; // 所属会话 ID
44
+ goal_id: string; // UUID v4,每次 replace/insert 生成新 ID
45
+ objective: string; // 用户设定的目标描述
46
+ status: ThreadGoalStatus;
47
+ token_budget: number | null; // token 预算上限,null 表示无限
48
+ tokens_used: number; // 已消耗 token 数
49
+ time_used_seconds: number; // 已消耗时间(秒)
50
+ created_at: number; // epoch 毫秒
51
+ updated_at: number; // epoch 毫秒
52
+ }
53
+ ```
54
+
55
+ ### 2.3 数据库表
56
+
57
+ ```sql
58
+ CREATE TABLE thread_goals (
59
+ thread_id TEXT PRIMARY KEY NOT NULL,
60
+ goal_id TEXT NOT NULL,
61
+ objective TEXT NOT NULL,
62
+ status TEXT NOT NULL CHECK(status IN (
63
+ 'active', 'paused', 'blocked',
64
+ 'usage_limited', 'budget_limited', 'complete'
65
+ )),
66
+ token_budget INTEGER, -- nullable
67
+ tokens_used INTEGER NOT NULL DEFAULT 0,
68
+ time_used_seconds INTEGER NOT NULL DEFAULT 0,
69
+ created_at_ms INTEGER NOT NULL, -- epoch 毫秒
70
+ updated_at_ms INTEGER NOT NULL -- epoch 毫秒
71
+ );
72
+ ```
73
+
74
+ **关键约束**:`thread_id` 是主键,每个 thread 最多一个 goal。
75
+
76
+ ---
77
+
78
+ ## 三、命令解析
79
+
80
+ ### 3.1 命令格式
81
+
82
+ ```
83
+ /goal → 显示当前 goal 摘要菜单
84
+ /goal <objective> → 设置/替换 goal
85
+ /goal clear → 清除 goal
86
+ /goal edit → 打开编辑器修改 objective
87
+ /goal pause → 暂停 goal
88
+ /goal resume → 恢复 paused goal
89
+ ```
90
+
91
+ ### 3.2 解析逻辑(伪代码)
92
+
93
+ ```typescript
94
+ function dispatchGoalCommand(input: string, threadId: string | null): void {
95
+ // 特性门禁检查
96
+ if (!features.enabled("Goals")) return;
97
+
98
+ const trimmed = input.trim();
99
+
100
+ // 裸 /goal → 显示菜单
101
+ if (trimmed === "") {
102
+ if (threadId) {
103
+ emit(AppEvent.OpenThreadGoalMenu, { threadId });
104
+ } else {
105
+ showInfo(GOAL_USAGE, "No goal is currently set.");
106
+ }
107
+ return;
108
+ }
109
+
110
+ // 子命令分派
111
+ const lower = trimmed.toLowerCase();
112
+
113
+ if (lower === "clear") {
114
+ if (!threadId) { showUsage(); return; }
115
+ emit(AppEvent.ClearThreadGoal, { threadId });
116
+ return;
117
+ }
118
+
119
+ if (lower === "edit") {
120
+ emit(AppEvent.OpenThreadGoalEditor, { threadId });
121
+ return;
122
+ }
123
+
124
+ if (lower === "pause") {
125
+ if (!threadId) { showUsage(); return; }
126
+ emit(AppEvent.SetThreadGoalStatus, { threadId, status: "paused" });
127
+ return;
128
+ }
129
+
130
+ if (lower === "resume") {
131
+ if (!threadId) { showUsage(); return; }
132
+ emit(AppEvent.SetThreadGoalStatus, { threadId, status: "active" });
133
+ return;
134
+ }
135
+
136
+ // 其余文本 → 当作 objective
137
+ const objective = trimmed;
138
+ if (objective === "") {
139
+ showError("Goal objective must not be empty.");
140
+ return;
141
+ }
142
+
143
+ // 长度验证
144
+ if (objective.length > MAX_THREAD_GOAL_OBJECTIVE_CHARS) {
145
+ showError(`Goal objective is too long: ${objective.length} characters. Limit: ${MAX_THREAD_GOAL_OBJECTIVE_CHARS}.`);
146
+ return;
147
+ }
148
+
149
+ if (!threadId) {
150
+ // session 未启动,排队等待
151
+ queueUserMessage(`/goal ${input}`, QueuedInputAction.ParseSlash);
152
+ return;
153
+ }
154
+
155
+ emit(AppEvent.SetThreadGoalObjective, {
156
+ threadId,
157
+ objective,
158
+ mode: "ConfirmIfExists",
159
+ });
160
+ }
161
+ ```
162
+
163
+ ---
164
+
165
+ ## 四、核心操作流程
166
+
167
+ ### 4.1 设置 Goal(`SetThreadGoalObjective`)
168
+
169
+ 这是最复杂的操作。流程如下:
170
+
171
+ ```
172
+ 用户输入 /goal <objective>
173
+
174
+ 解析为 SetThreadGoalObjective { threadId, objective, mode: "ConfirmIfExists" }
175
+
176
+ set_thread_goal_objective()
177
+
178
+ ┌─ mode === "ConfirmIfExists"?
179
+ │ ├─ YES → 读取现有 goal
180
+ │ │ ├─ 有 goal 且非 Complete → 弹出确认对话框 "Replace goal?"
181
+ │ │ │ ├─ 用户选 "Replace" → 重新发送 SetThreadGoalObjective { mode: "ReplaceExisting" }
182
+ │ │ │ └─ 用户选 "Cancel" → 结束
183
+ │ │ ├─ 有 goal 且是 Complete → 直接 ReplaceExisting(不需确认)
184
+ │ │ └─ 无 goal → 保持 ConfirmIfExists
185
+ │ └─ NO → 继续
186
+
187
+ ├─ mode === "ReplaceExisting"?
188
+ │ ├─ YES → 先调用 thread_goal_clear() 删除旧 goal
189
+ │ └─ NO → 继续
190
+
191
+ ├─ 确定 status 和 token_budget:
192
+ │ ├─ ConfirmIfExists / ReplaceExisting → (Active, null)
193
+ │ └─ UpdateExisting → (status, token_budget)
194
+
195
+ ├─ 调用 app_server.thread_goal_set(threadId, objective, status, token_budget)
196
+ │ ↓
197
+ │ GoalService.set_thread_goal()
198
+ │ ↓
199
+ │ 1. 验证 objective(trim + 长度检查)
200
+ │ 2. 验证 token_budget(必须 > 0 如果提供)
201
+ │ 3. 获取 runtime 的 goal_state_permit(防止 idle 续作冲突)
202
+ │ 4. prepare_external_goal_mutation()
203
+ │ ↓
204
+ │ ┌─ 有 objective?
205
+ │ │ ├─ 读取现有 goal
206
+ │ │ │ ├─ 有 → update_thread_goal()(用 expected_goal_id 乐观锁)
207
+ │ │ │ └─ 无 → replace_thread_goal()(INSERT OR REPLACE,重置 usage)
208
+ │ │ └─ 无 objective?
209
+ │ │ ├─ 读取现有 goal(必须存在)
210
+ │ │ └─ update_thread_goal()(只改 status/budget)
211
+ │ ↓
212
+ │ 5. 如果改了 objective → fill_empty_thread_preview_if_possible()
213
+ │ 6. 返回 GoalSetOutcome
214
+
215
+ └─ 显示结果: "Goal active" + usage summary
216
+ ```
217
+
218
+ ### 4.2 清除 Goal(`ClearThreadGoal`)
219
+
220
+ ```
221
+ 用户输入 /goal clear
222
+
223
+ clear_thread_goal()
224
+
225
+ 1. 获取 goal_state_permit
226
+ 2. prepare_external_goal_mutation()
227
+ 3. delete_thread_goal(thread_id) → DELETE FROM thread_goals WHERE thread_id = ?
228
+ 4. 释放 permit
229
+ 5. apply_external_goal_clear() → 清除 runtime 状态
230
+ 6. 返回 cleared: boolean
231
+ ```
232
+
233
+ ### 4.3 暂停/恢复(`SetThreadGoalStatus`)
234
+
235
+ ```
236
+ 用户输入 /goal pause 或 /goal resume
237
+
238
+ set_thread_goal_status(thread_id, status)
239
+
240
+ app_server.thread_goal_set(thread_id, null, status, null)
241
+
242
+ GoalService.set_thread_goal() → 只更新 status 字段
243
+ ```
244
+
245
+ **暂停的特殊语义**:
246
+ - `pause_active_thread_goal()` 只更新 `status = 'active'` 的行
247
+ - 但如果目标是 `usage_limited`,也允许更新为 `paused`(覆盖 `budget_limited`)
248
+
249
+ ### 4.4 编辑 Goal(`OpenThreadGoalEditor`)
250
+
251
+ ```
252
+ 用户输入 /goal edit
253
+
254
+ 1. 读取现有 goal
255
+ 2. 如果无 goal → 显示 "No goal is currently set."
256
+ 3. 有 goal → 显示编辑器,预填当前 objective
257
+ 4. 用户提交 → 发送 SetThreadGoalObjective { mode: "UpdateExisting", status, token_budget }
258
+ ```
259
+
260
+ **关键逻辑**:编辑时保留原始 status,除非原 status 是 `budget_limited` 或 `complete`,此时重置为 `active`:
261
+
262
+ ```typescript
263
+ function editedGoalStatus(status: ThreadGoalStatus): ThreadGoalStatus {
264
+ switch (status) {
265
+ case "active": return "active";
266
+ case "paused":
267
+ case "blocked":
268
+ case "usage_limited": return status; // 保留
269
+ case "budget_limited":
270
+ case "complete": return "active"; // 重置
271
+ }
272
+ }
273
+ ```
274
+
275
+ ### 4.5 显示 Goal 摘要(`/goal` 裸命令)
276
+
277
+ ```
278
+ /goal
279
+
280
+ open_thread_goal_menu()
281
+
282
+ 1. app_server.thread_goal_get(thread_id)
283
+ 2. 无 goal → 显示 GOAL_USAGE + "No goal is currently set."
284
+ 3. 有 goal → show_goal_summary(goal):
285
+ ┌─────────────────────────────────┐
286
+ │ Goal │
287
+ │ Status: active │
288
+ │ Objective: Fix the bug in auth │
289
+ │ Time used: 2m │
290
+ │ Tokens used: 12.5K │
291
+ │ Token budget: 50K │
292
+ │ │
293
+ │ Commands: /goal edit, │
294
+ │ /goal pause, │
295
+ │ /goal clear │
296
+ └─────────────────────────────────┘
297
+ ```
298
+
299
+ ---
300
+
301
+ ## 五、数据库操作详解
302
+
303
+ ### 5.1 `replace_thread_goal` — 创建/完全替换
304
+
305
+ ```typescript
306
+ async function replace_thread_goal(
307
+ threadId: string,
308
+ objective: string,
309
+ status: ThreadGoalStatus,
310
+ tokenBudget: number | null
311
+ ): Promise<ThreadGoal> {
312
+ const goalId = uuid();
313
+ const now = Date.now();
314
+ // 如果 status 是 active 但预算已耗尽,立即降级
315
+ status = statusAfterBudgetLimit(status, 0, tokenBudget);
316
+
317
+ // INSERT OR REPLACE:重置所有 usage 计数
318
+ await db.run(`
319
+ INSERT INTO thread_goals (thread_id, goal_id, objective, status,
320
+ token_budget, tokens_used, time_used_seconds, created_at_ms, updated_at_ms)
321
+ VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?)
322
+ ON CONFLICT(thread_id) DO UPDATE SET
323
+ goal_id = excluded.goal_id,
324
+ objective = excluded.objective,
325
+ status = excluded.status,
326
+ token_budget = excluded.token_budget,
327
+ tokens_used = 0,
328
+ time_used_seconds = 0,
329
+ created_at_ms = excluded.created_at_ms,
330
+ updated_at_ms = excluded.updated_at_ms
331
+ `, [threadId, goalId, objective, status, tokenBudget, now, now]);
332
+
333
+ return get_thread_goal(threadId);
334
+ }
335
+ ```
336
+
337
+ ### 5.2 `insert_thread_goal` — 仅在 goal 已完成时替换
338
+
339
+ ```typescript
340
+ async function insert_thread_goal(
341
+ threadId: string,
342
+ objective: string,
343
+ status: ThreadGoalStatus,
344
+ tokenBudget: number | null
345
+ ): Promise<ThreadGoal | null> {
346
+ const goalId = uuid();
347
+ const now = Date.now();
348
+ status = statusAfterBudgetLimit(status, 0, tokenBudget);
349
+
350
+ // 关键区别:WHERE thread_goals.status = 'complete'
351
+ // 只有已完成的 goal 才会被替换
352
+ const result = await db.run(`
353
+ INSERT INTO thread_goals (...)
354
+ VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?)
355
+ ON CONFLICT(thread_id) DO UPDATE SET
356
+ ...
357
+ WHERE thread_goals.status = 'complete'
358
+ `, [...]);
359
+
360
+ // 如果 affected rows === 0,说明有未完成的 goal,返回 null
361
+ return result.changes > 0 ? get_thread_goal(threadId) : null;
362
+ }
363
+ ```
364
+
365
+ ### 5.3 `update_thread_goal` — 增量更新
366
+
367
+ ```typescript
368
+ async function update_thread_goal(
369
+ threadId: string,
370
+ update: {
371
+ objective?: string;
372
+ status?: ThreadGoalStatus;
373
+ tokenBudget?: number | null; // null = 不改, Some(null) = 清除
374
+ expectedGoalId?: string; // 乐观锁
375
+ }
376
+ ): Promise<ThreadGoal | null> {
377
+ const now = Date.now();
378
+
379
+ // 核心 SQL 逻辑(简化版):
380
+ await db.run(`
381
+ UPDATE thread_goals SET
382
+ objective = COALESCE(?, objective),
383
+ status = CASE
384
+ -- 如果当前是 budget_limited 且新 status 是 paused/blocked,保留当前
385
+ WHEN status = 'budget_limited' AND ? IN ('paused', 'blocked') THEN status
386
+ -- 如果新 status 是 active 且预算已超,强制 budget_limited
387
+ WHEN ? = 'active' AND token_budget IS NOT NULL AND tokens_used >= token_budget
388
+ THEN 'budget_limited'
389
+ ELSE ?
390
+ END,
391
+ token_budget = ?, -- 如果提供
392
+ updated_at_ms = ?
393
+ WHERE thread_id = ?
394
+ AND (? IS NULL OR goal_id = ?) -- 乐观锁
395
+ `, [...]);
396
+
397
+ if (result.changes === 0) return null;
398
+ return get_thread_goal(threadId);
399
+ }
400
+ ```
401
+
402
+ ### 5.4 `account_thread_goal_usage` — 记账(最关键)
403
+
404
+ 每次 turn 结束或 tool 完成时调用,累加 token 和时间。
405
+
406
+ ```typescript
407
+ async function account_thread_goal_usage(
408
+ threadId: string,
409
+ timeDeltaSeconds: number, // ≥ 0
410
+ tokenDelta: number, // ≥ 0
411
+ mode: GoalAccountingMode,
412
+ expectedGoalId?: string
413
+ ): Promise<GoalAccountingOutcome> {
414
+ // 零增量直接返回
415
+ if (timeDeltaSeconds === 0 && tokenDelta === 0) {
416
+ return { kind: "Unchanged", goal: await get_thread_goal(threadId) };
417
+ }
418
+
419
+ // mode 决定哪些 status 的 goal 会被更新
420
+ const statusFilter = {
421
+ ActiveStatusOnly: "status = 'active'",
422
+ ActiveOnly: "status IN ('active', 'budget_limited')",
423
+ ActiveOrComplete: "status IN ('active', 'budget_limited', 'complete')",
424
+ ActiveOrStopped: "status IN ('active', 'paused', 'blocked', 'usage_limited', 'budget_limited')",
425
+ }[mode];
426
+
427
+ // 预算限制检查的 status 范围
428
+ const budgetCheckFilter = {
429
+ ActiveStatusOnly: "status = 'active'",
430
+ ActiveOnly: "status = 'active'",
431
+ ActiveOrComplete: "status = 'active'",
432
+ ActiveOrStopped: "status IN ('active', 'paused', 'blocked', 'usage_limited', 'budget_limited')",
433
+ }[mode];
434
+
435
+ const result = await db.run(`
436
+ UPDATE thread_goals SET
437
+ time_used_seconds = time_used_seconds + ?,
438
+ tokens_used = tokens_used + ?,
439
+ status = CASE
440
+ WHEN ${budgetCheckFilter}
441
+ AND token_budget IS NOT NULL
442
+ AND tokens_used + ? >= token_budget
443
+ THEN 'budget_limited'
444
+ ELSE status
445
+ END,
446
+ updated_at_ms = ?
447
+ WHERE thread_id = ? AND ${statusFilter}
448
+ ${expectedGoalId ? "AND goal_id = ?" : ""}
449
+ RETURNING *
450
+ `, [...]);
451
+
452
+ if (result.changes === 0) {
453
+ return { kind: "Unchanged", goal: await get_thread_goal(threadId) };
454
+ }
455
+ return { kind: "Updated", goal: rowToGoal(result.row) };
456
+ }
457
+ ```
458
+
459
+ ### 5.5 `statusAfterBudgetLimit` — 预算检查辅助
460
+
461
+ ```typescript
462
+ function statusAfterBudgetLimit(
463
+ status: ThreadGoalStatus,
464
+ tokensUsed: number,
465
+ tokenBudget: number | null
466
+ ): ThreadGoalStatus {
467
+ if (status === "active" && tokenBudget !== null && tokensUsed >= tokenBudget) {
468
+ return "budget_limited";
469
+ }
470
+ return status;
471
+ }
472
+ ```
473
+
474
+ ---
475
+
476
+ ## 六、自动续作机制(Idle Continuation)
477
+
478
+ 这是 Goal 的核心魔法:当 agent 完成一个 turn 后 idle 时,如果有 active goal,会自动注入续作 prompt 继续工作。
479
+
480
+ ### 6.1 触发链路
481
+
482
+ ```
483
+ Agent turn 结束
484
+
485
+ on_thread_idle()
486
+
487
+ runtime.continue_if_idle()
488
+
489
+ 1. 检查 goal 是否 active
490
+ 2. 读取 goal 状态
491
+ 3. 注入 continuation prompt
492
+ 4. 触发新 turn
493
+ ```
494
+
495
+ ### 6.2 续作 Prompt(continuation.md)
496
+
497
+ ```
498
+ Continue working toward the active thread goal.
499
+
500
+ <objective>
501
+ {{ objective }}
502
+ </objective>
503
+
504
+ Continuation behavior:
505
+ - This goal persists across turns. Ending this turn does not require
506
+ shrinking the objective to what fits now.
507
+ - Keep the full objective intact. If it cannot be finished now, make
508
+ concrete progress toward the real requested end state, leave the
509
+ goal active, and do not redefine success around a smaller or easier task.
510
+ - Temporary rough edges are acceptable while the work is moving in
511
+ the right direction. Completion still requires the requested end state
512
+ to be true and verified.
513
+
514
+ Budget:
515
+ - Tokens used: {{ tokens_used }}
516
+ - Token budget: {{ token_budget }}
517
+ - Tokens remaining: {{ remaining_tokens }}
518
+
519
+ Work from evidence:
520
+ Use the current worktree and external state as authoritative.
521
+
522
+ Fidelity:
523
+ - Optimize each turn for movement toward the requested end state.
524
+ - Do not substitute a narrower, safer, smaller solution.
525
+
526
+ Completion audit:
527
+ Before deciding that the goal is achieved, treat completion as unproven
528
+ and verify it against the actual current state:
529
+ - Derive concrete requirements from the objective.
530
+ - Preserve the original scope; do not redefine success.
531
+ - For every explicit requirement, identify authoritative evidence.
532
+ - Treat uncertain or indirect evidence as not achieved.
533
+ - Mark complete only when current evidence proves every requirement.
534
+
535
+ Blocked audit:
536
+ - Do not call update_goal with "blocked" the first time a blocker appears.
537
+ - Only use "blocked" when the same condition has repeated for 3+ consecutive turns.
538
+ - Never use "blocked" merely because work is hard or slow.
539
+ ```
540
+
541
+ ### 6.3 Budget Limit Prompt(budget_limit.md)
542
+
543
+ 当 token 预算耗尽时注入:
544
+
545
+ ```
546
+ The active thread goal has reached its token budget.
547
+
548
+ <objective>
549
+ {{ objective }}
550
+ </objective>
551
+
552
+ Budget:
553
+ - Time spent: {{ time_used_seconds }} seconds
554
+ - Tokens used: {{ tokens_used }}
555
+ - Token budget: {{ token_budget }}
556
+
557
+ The system has marked the goal as budget_limited, so do not start new
558
+ substantive work for this goal. Wrap up this turn soon: summarize useful
559
+ progress, identify remaining work or blockers, and leave the user with
560
+ a clear next step.
561
+
562
+ Do not call update_goal unless the goal is actually complete.
563
+ ```
564
+
565
+ ---
566
+
567
+ ## 七、扩展系统集成(Extension)
568
+
569
+ ### 7.1 工具注册
570
+
571
+ Goal 扩展注册了 3 个 LLM 工具:
572
+
573
+ | 工具名 | 用途 | 谁调用 |
574
+ |--------|------|--------|
575
+ | `get_goal` | 读取当前 goal | LLM |
576
+ | `create_goal` | 创建新 goal | LLM(仅用户明确要求时) |
577
+ | `update_goal` | 更新 goal status | LLM(仅 complete 或 blocked) |
578
+
579
+ **关键约束**:LLM 只能标记 `complete` 或 `blocked`,不能 `pause`/`resume`/`budget_limited`。
580
+
581
+ ### 7.2 工具 Schema
582
+
583
+ ```typescript
584
+ // get_goal: 无参数
585
+ const getGoalTool = {
586
+ name: "get_goal",
587
+ description: "Get the current goal for this thread...",
588
+ parameters: { type: "object", properties: {}, required: [] },
589
+ };
590
+
591
+ // create_goal
592
+ const createGoalTool = {
593
+ name: "create_goal",
594
+ description: "Create a goal only when explicitly requested...",
595
+ parameters: {
596
+ type: "object",
597
+ properties: {
598
+ objective: { type: "string", description: "Required. The concrete objective..." },
599
+ token_budget: { type: "integer", description: "Positive token budget..." },
600
+ },
601
+ required: ["objective"],
602
+ },
603
+ };
604
+
605
+ // update_goal
606
+ const updateGoalTool = {
607
+ name: "update_goal",
608
+ description: "Update the existing goal. Use only to mark complete or blocked...",
609
+ parameters: {
610
+ type: "object",
611
+ properties: {
612
+ status: {
613
+ type: "string",
614
+ enum: ["complete", "blocked"],
615
+ description: "Set to 'complete' only when objective is achieved...",
616
+ },
617
+ },
618
+ required: ["status"],
619
+ },
620
+ };
621
+ ```
622
+
623
+ ### 7.3 工具执行逻辑
624
+
625
+ #### `create_goal` 执行
626
+
627
+ ```typescript
628
+ async function handleCreateGoal(args: { objective: string; token_budget?: number }) {
629
+ args.objective = args.objective.trim();
630
+ validateObjective(args.objective);
631
+ validateBudget(args.token_budget);
632
+
633
+ // insert_thread_goal:只有已完成的 goal 才会被替换
634
+ const goal = await db.insert_thread_goal(
635
+ threadId, args.objective, "active", args.token_budget
636
+ );
637
+
638
+ if (!goal) {
639
+ throw new Error("cannot create a new goal because this thread has an unfinished goal");
640
+ }
641
+
642
+ // 设置线程预览(如果为空)
643
+ await fillEmptyThreadPreview(threadId, goal.objective);
644
+
645
+ // 标记当前 turn 的 goal 为活跃
646
+ accounting.markCurrentTurnGoalActive(goal.goal_id);
647
+
648
+ return goalResponse(goal);
649
+ }
650
+ ```
651
+
652
+ #### `update_goal` 执行
653
+
654
+ ```typescript
655
+ async function handleUpdateGoal(args: { status: "complete" | "blocked" }) {
656
+ // 先记账当前进度
657
+ await accountActiveGoalProgress(
658
+ args.status === "complete" ? "ActiveOrComplete" : "ActiveOrStopped",
659
+ callId,
660
+ BudgetLimitedGoalDisposition.ClearActive
661
+ );
662
+
663
+ // 更新 status
664
+ const goal = await db.update_thread_goal(threadId, {
665
+ status: args.status,
666
+ expectedGoalId: null, // 不需要乐观锁
667
+ });
668
+
669
+ if (!goal) throw new Error("cannot update goal because this thread has no goal");
670
+
671
+ // 清除当前 turn 的 goal 活跃标记
672
+ accounting.clearCurrentTurnGoal();
673
+
674
+ // complete 时附带 usage 报告
675
+ return goalResponse(goal, args.status === "complete" ? "Include" : "Omit");
676
+ }
677
+ ```
678
+
679
+ ### 7.4 生命周期钩子
680
+
681
+ ```typescript
682
+ // turn 开始时
683
+ on_turn_start(turnId) {
684
+ accounting.start_turn(turnId, mode, tokenUsageAtStart);
685
+ if (mode === "Plan") {
686
+ accounting.clearCurrentTurnGoal(); // Plan mode 不计 goal
687
+ return;
688
+ }
689
+ const goal = await db.get_thread_goal(threadId);
690
+ if (goal && (goal.status === "active" || goal.status === "budget_limited")) {
691
+ accounting.markTurnGoalActive(turnId, goal.goal_id);
692
+ }
693
+ }
694
+
695
+ // turn 结束时
696
+ on_turn_stop(turnId) {
697
+ await accountActiveGoalProgress(turnId, "ActiveOnly", ClearActive);
698
+ accounting.finishTurn(turnId);
699
+ }
700
+
701
+ // token 使用时
702
+ on_token_usage(tokenUsage) {
703
+ accounting.recordTokenUsage(turnId, tokenUsage.total);
704
+ }
705
+
706
+ // tool 完成时
707
+ on_tool_finish(toolName, outcome) {
708
+ // 只有 Completed 和 Failed(handler_executed=true) 才计数
709
+ // 跳过 update_goal 工具本身
710
+ if (!shouldCount(outcome) || toolName === "update_goal") return;
711
+
712
+ const progress = await accountActiveGoalProgress(turnId, "ActiveOnly", KeepActive);
713
+
714
+ // 如果刚变为 budget_limited,注入 budget limit steering prompt
715
+ if (progress.goal.status === "budget_limited") {
716
+ if (accounting.markBudgetLimitReportedIfNew(progress.goal_id)) {
717
+ injectSteeringItem(budgetLimitSteeringItem(progress.goal));
718
+ }
719
+ }
720
+ }
721
+
722
+ // thread idle 时(自动续作触发点)
723
+ on_thread_idle() {
724
+ await runtime.continue_if_idle();
725
+ }
726
+
727
+ // turn 出错时
728
+ on_turn_error(error) {
729
+ if (error === "UsageLimitExceeded") {
730
+ await stopActiveGoal("UsageLimit"); // → usage_limited
731
+ } else {
732
+ await stopActiveGoal("TurnError"); // → blocked
733
+ }
734
+ }
735
+ ```
736
+
737
+ ---
738
+
739
+ ## 八、Token 记账详解(Accounting)
740
+
741
+ ### 8.1 记账状态
742
+
743
+ ```typescript
744
+ interface GoalAccountingState {
745
+ currentTurnId: string | null;
746
+ turns: Map<string, GoalTurnAccounting>;
747
+ wallClock: GoalWallClockAccounting;
748
+ budgetLimitReportedGoalId: string | null;
749
+ }
750
+
751
+ interface GoalTurnAccounting {
752
+ currentTokenUsage: TokenUsage; // 当前累积
753
+ lastAccountedTokenUsage: TokenUsage; // 上次记账时的快照
754
+ activeGoalId: string | null;
755
+ accountTokens: boolean;
756
+ }
757
+
758
+ interface GoalWallClockAccounting {
759
+ lastAccountedAt: number; // Instant
760
+ activeGoalId: string | null;
761
+ }
762
+ ```
763
+
764
+ ### 8.2 记账流程
765
+
766
+ ```
767
+ Tool 完成 / Turn 结束
768
+
769
+ accountActiveGoalProgress(turnId, mode, budgetDisposition)
770
+
771
+ 1. 获取 progress_accounting_lock(Semaphore,防止并发记账)
772
+ 2. progress_snapshot(turnId):
773
+ - 计算 token_delta = currentTokenUsage.total - lastAccountedTokenUsage.total
774
+ - 计算 time_delta = now - lastAccountedAt
775
+ - 返回 { expectedGoalId, timeDelta, tokenDelta }
776
+ 3. db.account_thread_goal_usage(threadId, timeDelta, tokenDelta, mode, expectedGoalId)
777
+ 4. markProgressAccountedForStatus(turnId, snapshot, newStatus, disposition)
778
+ - 更新 lastAccountedTokenUsage
779
+ - 更新 lastAccountedAt
780
+ - 如果 budget_limited 且 disposition === ClearActive → 清除 activeGoalId
781
+ ```
782
+
783
+ ### 8.3 记账模式
784
+
785
+ | 模式 | 用途 | 影响的 status |
786
+ |------|------|---------------|
787
+ | `ActiveStatusOnly` | turn 中的 token 追踪 | 仅 `active` |
788
+ | `ActiveOnly` | turn 结束/tool 完成 | `active`, `budget_limited` |
789
+ | `ActiveOrComplete` | 标记 complete 时的最终记账 | `active`, `budget_limited`, `complete` |
790
+ | `ActiveOrStopped` | 标记 blocked 时的记账 | 所有非终态 |
791
+
792
+ ---
793
+
794
+ ## 九、UI 显示逻辑
795
+
796
+ ### 9.1 时间格式化
797
+
798
+ ```typescript
799
+ function formatGoalElapsedSeconds(seconds: number): string {
800
+ seconds = Math.max(0, seconds);
801
+ if (seconds < 60) return `${seconds}s`;
802
+
803
+ const minutes = Math.floor(seconds / 60);
804
+ if (minutes < 60) return `${minutes}m`;
805
+
806
+ const hours = Math.floor(minutes / 60);
807
+ const remainingMinutes = minutes % 60;
808
+ if (hours >= 24) {
809
+ const days = Math.floor(hours / 24);
810
+ const remainingHours = hours % 24;
811
+ return `${days}d ${remainingHours}h ${remainingMinutes}m`;
812
+ }
813
+
814
+ if (remainingMinutes === 0) return `${hours}h`;
815
+ return `${hours}h ${remainingMinutes}m`;
816
+ }
817
+ ```
818
+
819
+ ### 9.2 Status Line 指示器
820
+
821
+ ```typescript
822
+ function goalStatusIndicator(goal: GoalStatusState, now: Instant): GoalStatusIndicator | null {
823
+ switch (goal.status) {
824
+ case "active":
825
+ // 如果有活跃 turn,加上当前 turn 的 elapsed time
826
+ let displayGoal = { ...goal };
827
+ if (activeTurnStartedAt) {
828
+ const baseline = Math.max(goal.observedAt, activeTurnStartedAt);
829
+ const activeSeconds = (now - baseline) / 1000;
830
+ displayGoal.time_used_seconds += activeSeconds;
831
+ }
832
+ return {
833
+ type: "Active",
834
+ usage: goal.token_budget
835
+ ? `${formatTokens(goal.tokens_used)} / ${formatTokens(goal.token_budget)}`
836
+ : formatGoalElapsedSeconds(displayGoal.time_used_seconds),
837
+ };
838
+ case "paused": return { type: "Paused" };
839
+ case "blocked": return { type: "Blocked" };
840
+ case "usage_limited": return { type: "UsageLimited" };
841
+ case "budget_limited":
842
+ return {
843
+ type: "BudgetLimited",
844
+ usage: goal.token_budget
845
+ ? `${formatTokens(goal.tokens_used)} / ${formatTokens(goal.token_budget)} tokens`
846
+ : null,
847
+ };
848
+ case "complete":
849
+ return {
850
+ type: "Complete",
851
+ usage: goal.token_budget
852
+ ? `${formatTokens(goal.tokens_used)} tokens`
853
+ : formatGoalElapsedSeconds(goal.time_used_seconds),
854
+ };
855
+ }
856
+ }
857
+ ```
858
+
859
+ ### 9.3 替换确认对话框
860
+
861
+ 当用户 `/goal <new objective>` 且已有未完成 goal 时:
862
+
863
+ ```typescript
864
+ function shouldConfirmBeforeReplacing(goal: ThreadGoal): boolean {
865
+ // Complete 是终态,不需要确认
866
+ if (goal.status === "complete") return false;
867
+ // 其他所有状态都需要确认
868
+ return true;
869
+ }
870
+
871
+ // 弹出选择:
872
+ // ┌─────────────────────────────────┐
873
+ // │ Replace goal? │
874
+ // │ New objective: <new objective> │
875
+ // │ │
876
+ // │ > Replace current goal │
877
+ // │ Set the new objective now │
878
+ // │ Cancel │
879
+ // │ Keep the current goal │
880
+ // └─────────────────────────────────┘
881
+ ```
882
+
883
+ ---
884
+
885
+ ## 十、完整状态机
886
+
887
+ ```
888
+ ┌──────────────────────────────┐
889
+ │ 无 Goal │
890
+ └──────────┬───────────────────┘
891
+ │ /goal <objective>
892
+ │ create_goal()
893
+
894
+ ┌──────────────────────────────┐
895
+ │ ACTIVE │
896
+ │ (自动续作中) │
897
+ └──┬────┬────┬────┬────┬───────┘
898
+ │ │ │ │ │
899
+ /goal pause│ │ │ │ │ update_goal(blocked)
900
+ │ │ │ │ │ (3次连续阻塞后)
901
+ ▼ │ │ │ ▼
902
+ ┌────────┐ │ │ │ ┌─────────┐
903
+ │ PAUSED │ │ │ │ │ BLOCKED │
904
+ └───┬────┘ │ │ │ └────┬────┘
905
+ │ │ │ │ │
906
+ /goal resume │ │ │ │ /goal resume
907
+ │ │ │ │ │
908
+ ▼ │ │ │ ▼
909
+ ┌──────────────────────────────┐
910
+ │ ACTIVE │◄──── /goal resume
911
+ └──┬────┬────┬────┬────┬───────┘ (从 paused/blocked/
912
+ │ │ │ │ │ usage_limited 恢复)
913
+ 超预算 │ │ │ │ │
914
+ ▼ │ │ │ │
915
+ ┌────────────┐│ │ │ │
916
+ │ BUDGET_ ││ │ │ │
917
+ │ LIMITED ││ │ │ │
918
+ │ (终态) ││ │ │ │
919
+ └────────────┘│ │ │ │
920
+ │ │ │ │
921
+ usage limit │ │ │ │
922
+ ▼ │ │ │
923
+ ┌──────────────┐│ │ │
924
+ │ USAGE_ ││ │ │
925
+ │ LIMITED ││ │ │
926
+ └──────────────┘│ │ │
927
+ │ │ │
928
+ update_goal │ │ │
929
+ (complete) │ │ │
930
+ ▼ │ │
931
+ ┌──────────────┐│ │
932
+ │ COMPLETE ││ │
933
+ │ (终态) ││ │
934
+ └──────────────┘│ │
935
+ │ │
936
+ /goal clear │ │
937
+ ▼ │
938
+ ┌──────────────┐
939
+ │ 无 Goal │
940
+ └──────────────┘
941
+ ```
942
+
943
+ ---
944
+
945
+ ## 十一、TypeScript 复刻清单
946
+
947
+ 如果要从零实现,按以下顺序:
948
+
949
+ ### 11.1 数据层
950
+
951
+ - [ ] 定义 `ThreadGoalStatus` 类型(6 个值)
952
+ - [ ] 定义 `ThreadGoal` 接口(9 个字段)
953
+ - [ ] 建表 SQL(`thread_goals`,10 列)
954
+ - [ ] 实现 `GoalStore` 类:
955
+ - `get_goal(threadId)` → SELECT
956
+ - `replace_goal(threadId, objective, status, tokenBudget)` → INSERT OR REPLACE(重置 usage)
957
+ - `insert_goal(threadId, objective, status, tokenBudget)` → INSERT OR REPLACE(仅 status=complete 时覆盖)
958
+ - `update_goal(threadId, update)` → UPDATE(带乐观锁 + 预算自动降级)
959
+ - `delete_goal(threadId)` → DELETE
960
+ - `account_usage(threadId, timeDelta, tokenDelta, mode, expectedGoalId)` → UPDATE(累加 + 预算检查)
961
+ - `pause_active_goal(threadId)` → UPDATE status WHERE active
962
+ - `usage_limit_active_goal(threadId)` → UPDATE status WHERE active OR budget_limited
963
+
964
+ ### 11.2 命令层
965
+
966
+ - [ ] 命令解析:`/goal [clear|edit|pause|resume|<objective>]`
967
+ - [ ] 长度验证:objective 不超过 MAX_CHARS
968
+ - [ ] 子命令分派:clear/edit/pause/resume/设置
969
+ - [ ] ConfirmIfExists 逻辑:读取现有 → 非 Complete 弹确认
970
+ - [ ] ReplaceExisting 逻辑:先 clear 再 set
971
+
972
+ ### 11.3 工具层
973
+
974
+ - [ ] `get_goal` 工具:无参数,返回当前 goal
975
+ - [ ] `create_goal` 工具:参数 { objective, token_budget? }
976
+ - 验证 objective 非空
977
+ - 验证 budget > 0
978
+ - 调用 insert_goal(未完成 goal 存在时报错)
979
+ - 标记当前 turn goal 活跃
980
+ - [ ] `update_goal` 工具:参数 { status: "complete"|"blocked" }
981
+ - 先记账当前进度
982
+ - 更新 status
983
+ - 清除 turn goal 活跃标记
984
+
985
+ ### 11.4 生命周期层
986
+
987
+ - [ ] `on_turn_start`:读取 goal,标记活跃
988
+ - [ ] `on_turn_stop`:记账 + 清除
989
+ - [ ] `on_turn_abort`:记账 + 清除
990
+ - [ ] `on_turn_error`:停止 goal(usage_limited 或 blocked)
991
+ - [ ] `on_token_usage`:记录 token 增量
992
+ - [ ] `on_tool_finish`:记账 + budget_limited 时注入 steering prompt
993
+ - [ ] `on_thread_idle`:注入 continuation prompt,触发新 turn
994
+
995
+ ### 11.5 Prompt 层
996
+
997
+ - [ ] `continuation_prompt(goal)`:续作指令(含 objective、budget、completion audit、blocked audit)
998
+ - [ ] `budget_limit_prompt(goal)`:预算耗尽收尾指令
999
+ - [ ] `objective_updated_prompt(goal)`:objective 编辑后的新指令
1000
+ - [ ] 三个模板的变量替换:`{{ objective }}`, `{{ tokens_used }}`, `{{ token_budget }}`, `{{ remaining_tokens }}`, `{{ time_used_seconds }}`
1001
+
1002
+ ### 11.6 UI 层
1003
+
1004
+ - [ ] `formatGoalElapsedSeconds(seconds)`:时间格式化(s/m/h/d)
1005
+ - [ ] `goalStatusLabel(status)`:状态文本
1006
+ - [ ] `goalUsageSummary(goal)`:一行摘要
1007
+ - [ ] `goalSummaryLines(goal)`:多行详情 + 可用命令提示
1008
+ - [ ] `goalStatusIndicator(goal, now, activeTurnStartedAt)`:status line 指示器
1009
+ - [ ] `shouldConfirmBeforeReplacing(goal)`:替换确认判断
1010
+ - [ ] `editedGoalStatus(status)`:编辑时的状态保留/重置逻辑
1011
+ - [ ] 临时会话错误消息
1012
+
1013
+ ### 11.7 边界条件
1014
+
1015
+ - [ ] 临时会话(ephemeral)不支持 goal → 错误消息引导用户
1016
+ - [ ] Plan mode 不计 goal
1017
+ - [ ] Review subagent 不注册 goal 工具
1018
+ - [ ] 并发记账用 Semaphore 保护
1019
+ - [ ] `expected_goal_id` 乐观锁防止并发更新冲突
1020
+ - [ ] `budget_limited` 是终态,不被 pause/blocked 覆盖(但可被 resume 从 paused/blocked 恢复)
1021
+ - [ ] `usage_limited` 可以被 pause 覆盖
1022
+ - [ ] turn 期间 idle continuation 不能与外部 goal mutation 冲突(goal_state_permit)
1023
+
1024
+ ---
1025
+
1026
+ ## 十二、源码文件索引
1027
+
1028
+ | 文件 | 职责 |
1029
+ |------|------|
1030
+ | `state/src/model/thread_goal.rs` | 数据模型(ThreadGoal, ThreadGoalStatus, ThreadGoalRow) |
1031
+ | `state/src/runtime/goals.rs` | 数据库操作(GoalStore, GoalUpdate, GoalAccountingMode) |
1032
+ | `state/goals_migrations/0001_thread_goals.sql` | 建表 SQL |
1033
+ | `ext/goal/src/lib.rs` | 模块导出 |
1034
+ | `ext/goal/src/api.rs` | GoalService(TUI↔DB 桥梁) |
1035
+ | `ext/goal/src/extension.rs` | 生命周期钩子注册(ThreadLifecycle, TurnLifecycle, TokenUsage, Tool) |
1036
+ | `ext/goal/src/tool.rs` | LLM 工具执行(get/create/update_goal) |
1037
+ | `ext/goal/src/spec.rs` | 工具 Schema 定义 |
1038
+ | `ext/goal/src/accounting.rs` | Token/时间记账状态机 |
1039
+ | `ext/goal/src/steering.rs` | Prompt 模板渲染 + steering 注入 |
1040
+ | `ext/goal/src/runtime.rs` | GoalRuntimeHandle(线程级 runtime) |
1041
+ | `ext/goal/src/events.rs` | 事件发射 |
1042
+ | `ext/goal/src/analytics.rs` | 分析事件 |
1043
+ | `ext/goal/src/metrics.rs` | 指标上报 |
1044
+ | `prompts/templates/goals/continuation.md` | 续作 prompt 模板 |
1045
+ | `prompts/templates/goals/budget_limit.md` | 预算耗尽 prompt 模板 |
1046
+ | `prompts/templates/goals/objective_updated.md` | objective 编辑 prompt 模板 |
1047
+ | `tui/src/slash_command.rs` | 命令注册(Goal 枚举变体) |
1048
+ | `tui/src/chatwidget/slash_dispatch.rs` | 命令分派 |
1049
+ | `tui/src/app_event.rs` | AppEvent 定义(5 个 goal 相关事件) |
1050
+ | `tui/src/app/thread_goal_actions.rs` | TUI 层 goal 操作实现 |
1051
+ | `tui/src/goal_display.rs` | 显示工具函数 |
1052
+ | `tui/src/chatwidget/goal_menu.rs` | 摘要菜单 UI |
1053
+ | `tui/src/chatwidget/goal_validation.rs` | objective 验证 |
1054
+ | `tui/src/chatwidget/goal_status.rs` | Status line 指示器 |
1055
+ | `app-server/src/request_processors/thread_goal_processor.rs` | JSON-RPC 处理器 |