@pencil-agent/nano-pencil 2.0.1 → 2.0.2

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 (186) 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/model/custom-providers.js +1 -1
  7. package/dist/core/model-registry.js +5 -5
  8. package/dist/extensions/builtin/AGENT.md +115 -115
  9. package/dist/extensions/builtin/browser/AGENT.md +17 -17
  10. package/dist/extensions/builtin/browser/agent-workspace/agent_helpers.py +12 -12
  11. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/amazon/product-search.md +198 -198
  12. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/archive-org/scraping.md +341 -341
  13. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv/scraping.md +311 -311
  14. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv-bulk/scraping.md +333 -333
  15. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/atlas/overview.md +70 -70
  16. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/booking-com/scraping.md +578 -578
  17. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/capterra/scraping.md +440 -440
  18. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/centilebrain/generate-estimates.md +110 -110
  19. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coingecko/scraping.md +325 -325
  20. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coinmarketcap/scraping.md +463 -463
  21. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coursera/scraping.md +360 -360
  22. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/craigslist/scraping.md +390 -390
  23. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/crossref/scraping.md +568 -568
  24. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/dev-to/scraping.md +323 -323
  25. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/duckduckgo/scraping.md +349 -349
  26. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/ebay/scraping.md +435 -435
  27. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/etsy/scraping.md +506 -506
  28. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/eventbrite/scraping.md +363 -363
  29. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/expedia/automation.md +168 -168
  30. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/groups.md +236 -236
  31. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/pages.md +295 -295
  32. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/framer/editor.md +108 -108
  33. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/fred/scraping.md +493 -493
  34. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/g2/scraping.md +580 -580
  35. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/genius/scraping.md +511 -511
  36. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/repo-actions.md +65 -65
  37. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/scraping.md +184 -184
  38. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/glassdoor/scraping.md +543 -543
  39. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gmail/compose.md +122 -122
  40. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/goodreads/scraping.md +461 -461
  41. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gutenberg/scraping.md +383 -383
  42. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/hackernews/scraping.md +243 -243
  43. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/howlongtobeat/scraping.md +473 -473
  44. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/imdb/scraping.md +271 -271
  45. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/itch-io/scraping.md +436 -436
  46. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/job-boards/indeed-glassdoor.md +1021 -1021
  47. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/letterboxd/scraping.md +349 -349
  48. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/linkedin/invitation-manager.md +109 -109
  49. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/loom/folder-enumeration.md +170 -170
  50. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/macrotrends/scraping.md +537 -537
  51. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/article-hydration.md +120 -120
  52. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/scraping.md +414 -414
  53. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/metacritic/scraping.md +477 -477
  54. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/musicbrainz/scraping.md +478 -478
  55. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/nasa/scraping.md +339 -339
  56. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/news-aggregation/multi-source.md +205 -205
  57. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/open-library/scraping.md +472 -472
  58. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openalex/scraping.md +470 -470
  59. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openstreetmap/scraping.md +490 -490
  60. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/package-registries/npm-pypi.md +478 -478
  61. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/polymarket/scraping.md +234 -234
  62. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/producthunt/scraping.md +307 -307
  63. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/pubmed/scraping.md +421 -421
  64. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/quora/scraping.md +364 -364
  65. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rawg/scraping.md +352 -352
  66. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/reddit/scraping.md +124 -124
  67. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rest-countries/scraping.md +233 -233
  68. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/sec-edgar/scraping.md +361 -361
  69. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/README.md +36 -36
  70. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/embedded-apps.md +72 -72
  71. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/knowledge-base.md +109 -109
  72. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/polaris-inputs.md +137 -137
  73. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/soundcloud/scraping.md +362 -362
  74. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/spotify/scraping.md +339 -339
  75. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/stackoverflow/scraping.md +435 -435
  76. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/steam/scraping.md +575 -575
  77. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/substack/scraping.md +338 -338
  78. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/thetechgeeks/pricing.md +52 -52
  79. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tiktok/upload.md +107 -107
  80. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tradingview/scraping.md +309 -309
  81. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trello/boards-and-lists.md +88 -88
  82. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trustpilot/scraping.md +375 -375
  83. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/walmart/scraping.md +444 -444
  84. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wayback-machine/scraping.md +306 -306
  85. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/weather/scraping.md +398 -398
  86. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wellfound/scraping.md +596 -596
  87. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/world-bank/scraping.md +356 -356
  88. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/xiaohongshu/scraping.md +84 -84
  89. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/youtube/scraping.md +418 -418
  90. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/zillow/scraping.md +433 -433
  91. package/dist/extensions/builtin/browser/browser.md +73 -73
  92. package/dist/extensions/builtin/browser/install.md +142 -142
  93. package/dist/extensions/builtin/browser/interaction-skills/connection.md +48 -48
  94. package/dist/extensions/builtin/browser/interaction-skills/cookies.md +3 -3
  95. package/dist/extensions/builtin/browser/interaction-skills/cross-origin-iframes.md +3 -3
  96. package/dist/extensions/builtin/browser/interaction-skills/dialogs.md +64 -64
  97. package/dist/extensions/builtin/browser/interaction-skills/downloads.md +3 -3
  98. package/dist/extensions/builtin/browser/interaction-skills/drag-and-drop.md +3 -3
  99. package/dist/extensions/builtin/browser/interaction-skills/dropdowns.md +3 -3
  100. package/dist/extensions/builtin/browser/interaction-skills/iframes.md +3 -3
  101. package/dist/extensions/builtin/browser/interaction-skills/network-requests.md +3 -3
  102. package/dist/extensions/builtin/browser/interaction-skills/print-as-pdf.md +3 -3
  103. package/dist/extensions/builtin/browser/interaction-skills/profile-sync.md +90 -90
  104. package/dist/extensions/builtin/browser/interaction-skills/screenshots.md +17 -17
  105. package/dist/extensions/builtin/browser/interaction-skills/scrolling.md +3 -3
  106. package/dist/extensions/builtin/browser/interaction-skills/shadow-dom.md +3 -3
  107. package/dist/extensions/builtin/browser/interaction-skills/tabs.md +69 -69
  108. package/dist/extensions/builtin/browser/interaction-skills/uploads.md +1 -1
  109. package/dist/extensions/builtin/browser/interaction-skills/viewport.md +3 -3
  110. package/dist/extensions/builtin/browser/src/browser_harness/AGENT.md +15 -15
  111. package/dist/extensions/builtin/browser/src/browser_harness/__init__.py +8 -8
  112. package/dist/extensions/builtin/browser/src/browser_harness/_ipc.py +90 -90
  113. package/dist/extensions/builtin/browser/src/browser_harness/admin.py +722 -722
  114. package/dist/extensions/builtin/browser/src/browser_harness/daemon.py +328 -328
  115. package/dist/extensions/builtin/browser/src/browser_harness/helpers.py +396 -396
  116. package/dist/extensions/builtin/browser/src/browser_harness/run.py +103 -103
  117. package/dist/extensions/builtin/discipline/skills/brainstorming/SKILL.md +33 -33
  118. package/dist/extensions/builtin/discipline/skills/executing-plans/SKILL.md +25 -25
  119. package/dist/extensions/builtin/discipline/skills/finishing-development-branch/SKILL.md +25 -25
  120. package/dist/extensions/builtin/discipline/skills/receiving-code-review/SKILL.md +22 -22
  121. package/dist/extensions/builtin/discipline/skills/requesting-code-review/SKILL.md +31 -31
  122. package/dist/extensions/builtin/discipline/skills/systematic-debugging/SKILL.md +28 -28
  123. package/dist/extensions/builtin/discipline/skills/test-driven-development/SKILL.md +32 -32
  124. package/dist/extensions/builtin/discipline/skills/using-git-worktrees/SKILL.md +25 -25
  125. package/dist/extensions/builtin/discipline/skills/verification-before-completion/SKILL.md +27 -27
  126. package/dist/extensions/builtin/discipline/skills/writing-plans/SKILL.md +26 -26
  127. package/dist/extensions/builtin/goal/README.md +67 -67
  128. package/dist/extensions/builtin/grub/README.md +112 -112
  129. package/dist/extensions/builtin/link-world/agent-workspace/README.md +16 -16
  130. package/dist/extensions/builtin/link-world/internet-search/internet-search.md +65 -65
  131. package/dist/extensions/builtin/link-world/link-world-agent.md +82 -82
  132. package/dist/extensions/builtin/link-world/linkworld.md +313 -313
  133. package/dist/extensions/builtin/link-world/network-routing/network-routing.md +67 -67
  134. package/dist/extensions/builtin/loop/README.md +92 -92
  135. package/dist/extensions/builtin/mcp/figma-design.md +68 -68
  136. package/dist/extensions/builtin/mcp/mcp-management.md +85 -85
  137. package/dist/extensions/builtin/recap/AGENT.md +15 -15
  138. package/dist/extensions/builtin/sal/README.md +72 -72
  139. package/dist/extensions/builtin/security-audit/README.md +289 -289
  140. package/dist/extensions/builtin/team/AGENT.md +112 -112
  141. package/dist/extensions/builtin/team/TESTING.md +299 -299
  142. package/dist/extensions/builtin/token-save/README.md +56 -56
  143. package/dist/extensions/optional/AGENT.md +10 -10
  144. package/dist/modes/interactive/controllers/input-submit-controller.js +2 -2
  145. package/dist/modes/interactive/controllers/stream-render-controller.js +2 -2
  146. package/dist/modes/interactive/interactive-mode.js +19 -19
  147. package/dist/modes/interactive/theme/dark.json +85 -85
  148. package/dist/modes/interactive/theme/light.json +84 -84
  149. package/dist/modes/interactive/theme/theme-schema.json +335 -335
  150. package/dist/modes/interactive/theme/warm.json +81 -81
  151. package/dist/node_modules/@pencil-agent/ai/dist/cli.js +0 -0
  152. package/dist/node_modules/@pencil-agent/ai/dist/models.generated.js +1 -1
  153. 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
  154. package/docs/SDK-TESTING.md +364 -0
  155. package/docs/codex-goal-command-impl.md +1055 -1055
  156. package/docs/codex-goal-vs-grub.md +500 -500
  157. package/docs/custom-provider.md +27 -27
  158. package/docs/extensions.md +27 -27
  159. package/docs/keybindings.md +27 -27
  160. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +250 -250
  161. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +122 -122
  162. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -1222
  163. 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
  164. 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
  165. package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +320 -320
  166. package/docs/loop-usage-examples.md +214 -214
  167. package/docs/mem-core/346/212/200/346/234/257/346/226/207/346/241/243.md +593 -0
  168. package/docs/models.md +27 -27
  169. package/docs/packages.md +27 -27
  170. package/docs/pi-design-philosophy.md +457 -457
  171. package/docs/planmode.md +1987 -1987
  172. package/docs/prompt-templates.md +27 -27
  173. package/docs/providers.md +27 -27
  174. package/docs/sdk.md +27 -27
  175. package/docs/skills.md +27 -27
  176. package/docs/startup-performance-optimization.md +301 -0
  177. package/docs/themes.md +27 -27
  178. package/docs/tui.md +27 -27
  179. package/docs//350/256/244/347/237/245/345/234/260/345/233/276.md +47 -0
  180. package/package.json +190 -190
  181. package/docs/cc-agent-design.md +0 -1297
  182. package/docs/cc-tui-design.md +0 -1333
  183. package/docs/nanoPencil-/345/255/246/344/271/240/350/256/241/345/210/222.md +0 -170
  184. package/docs/scan-report.md +0 -3820
  185. package/docs//345/257/271/346/240/207Claude-Code.md +0 -1775
  186. package/docs//351/230/277/351/207/214/345/267/264/345/267/264/350/264/242/346/212/245/345/210/206/346/236/220/344/271/246.md +0 -261
@@ -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 处理器 |