@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.
- package/README.md +267 -267
- package/dist/build-meta.json +3 -3
- package/dist/core/export-html/AGENT.md +11 -11
- package/dist/core/export-html/template.css +971 -971
- package/dist/core/export-html/template.html +54 -54
- package/dist/core/model/custom-providers.js +1 -1
- package/dist/core/model-registry.js +5 -5
- package/dist/extensions/builtin/AGENT.md +115 -115
- package/dist/extensions/builtin/browser/AGENT.md +17 -17
- package/dist/extensions/builtin/browser/agent-workspace/agent_helpers.py +12 -12
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/amazon/product-search.md +198 -198
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/archive-org/scraping.md +341 -341
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv/scraping.md +311 -311
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv-bulk/scraping.md +333 -333
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/atlas/overview.md +70 -70
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/booking-com/scraping.md +578 -578
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/capterra/scraping.md +440 -440
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/centilebrain/generate-estimates.md +110 -110
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coingecko/scraping.md +325 -325
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coinmarketcap/scraping.md +463 -463
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coursera/scraping.md +360 -360
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/craigslist/scraping.md +390 -390
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/crossref/scraping.md +568 -568
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/dev-to/scraping.md +323 -323
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/duckduckgo/scraping.md +349 -349
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/ebay/scraping.md +435 -435
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/etsy/scraping.md +506 -506
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/eventbrite/scraping.md +363 -363
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/expedia/automation.md +168 -168
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/groups.md +236 -236
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/pages.md +295 -295
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/framer/editor.md +108 -108
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/fred/scraping.md +493 -493
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/g2/scraping.md +580 -580
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/genius/scraping.md +511 -511
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/repo-actions.md +65 -65
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/scraping.md +184 -184
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/glassdoor/scraping.md +543 -543
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gmail/compose.md +122 -122
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/goodreads/scraping.md +461 -461
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gutenberg/scraping.md +383 -383
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/hackernews/scraping.md +243 -243
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/howlongtobeat/scraping.md +473 -473
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/imdb/scraping.md +271 -271
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/itch-io/scraping.md +436 -436
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/job-boards/indeed-glassdoor.md +1021 -1021
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/letterboxd/scraping.md +349 -349
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/linkedin/invitation-manager.md +109 -109
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/loom/folder-enumeration.md +170 -170
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/macrotrends/scraping.md +537 -537
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/article-hydration.md +120 -120
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/scraping.md +414 -414
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/metacritic/scraping.md +477 -477
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/musicbrainz/scraping.md +478 -478
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/nasa/scraping.md +339 -339
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/news-aggregation/multi-source.md +205 -205
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/open-library/scraping.md +472 -472
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openalex/scraping.md +470 -470
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openstreetmap/scraping.md +490 -490
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/package-registries/npm-pypi.md +478 -478
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/polymarket/scraping.md +234 -234
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/producthunt/scraping.md +307 -307
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/pubmed/scraping.md +421 -421
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/quora/scraping.md +364 -364
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rawg/scraping.md +352 -352
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/reddit/scraping.md +124 -124
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rest-countries/scraping.md +233 -233
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/sec-edgar/scraping.md +361 -361
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/README.md +36 -36
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/embedded-apps.md +72 -72
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/knowledge-base.md +109 -109
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/polaris-inputs.md +137 -137
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/soundcloud/scraping.md +362 -362
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/spotify/scraping.md +339 -339
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/stackoverflow/scraping.md +435 -435
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/steam/scraping.md +575 -575
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/substack/scraping.md +338 -338
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/thetechgeeks/pricing.md +52 -52
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tiktok/upload.md +107 -107
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tradingview/scraping.md +309 -309
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trello/boards-and-lists.md +88 -88
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trustpilot/scraping.md +375 -375
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/walmart/scraping.md +444 -444
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wayback-machine/scraping.md +306 -306
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/weather/scraping.md +398 -398
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wellfound/scraping.md +596 -596
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/world-bank/scraping.md +356 -356
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/xiaohongshu/scraping.md +84 -84
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/youtube/scraping.md +418 -418
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/zillow/scraping.md +433 -433
- package/dist/extensions/builtin/browser/browser.md +73 -73
- package/dist/extensions/builtin/browser/install.md +142 -142
- package/dist/extensions/builtin/browser/interaction-skills/connection.md +48 -48
- package/dist/extensions/builtin/browser/interaction-skills/cookies.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/cross-origin-iframes.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/dialogs.md +64 -64
- package/dist/extensions/builtin/browser/interaction-skills/downloads.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/drag-and-drop.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/dropdowns.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/iframes.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/network-requests.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/print-as-pdf.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/profile-sync.md +90 -90
- package/dist/extensions/builtin/browser/interaction-skills/screenshots.md +17 -17
- package/dist/extensions/builtin/browser/interaction-skills/scrolling.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/shadow-dom.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/tabs.md +69 -69
- package/dist/extensions/builtin/browser/interaction-skills/uploads.md +1 -1
- package/dist/extensions/builtin/browser/interaction-skills/viewport.md +3 -3
- package/dist/extensions/builtin/browser/src/browser_harness/AGENT.md +15 -15
- package/dist/extensions/builtin/browser/src/browser_harness/__init__.py +8 -8
- package/dist/extensions/builtin/browser/src/browser_harness/_ipc.py +90 -90
- package/dist/extensions/builtin/browser/src/browser_harness/admin.py +722 -722
- package/dist/extensions/builtin/browser/src/browser_harness/daemon.py +328 -328
- package/dist/extensions/builtin/browser/src/browser_harness/helpers.py +396 -396
- package/dist/extensions/builtin/browser/src/browser_harness/run.py +103 -103
- package/dist/extensions/builtin/discipline/skills/brainstorming/SKILL.md +33 -33
- package/dist/extensions/builtin/discipline/skills/executing-plans/SKILL.md +25 -25
- package/dist/extensions/builtin/discipline/skills/finishing-development-branch/SKILL.md +25 -25
- package/dist/extensions/builtin/discipline/skills/receiving-code-review/SKILL.md +22 -22
- package/dist/extensions/builtin/discipline/skills/requesting-code-review/SKILL.md +31 -31
- package/dist/extensions/builtin/discipline/skills/systematic-debugging/SKILL.md +28 -28
- package/dist/extensions/builtin/discipline/skills/test-driven-development/SKILL.md +32 -32
- package/dist/extensions/builtin/discipline/skills/using-git-worktrees/SKILL.md +25 -25
- package/dist/extensions/builtin/discipline/skills/verification-before-completion/SKILL.md +27 -27
- package/dist/extensions/builtin/discipline/skills/writing-plans/SKILL.md +26 -26
- package/dist/extensions/builtin/goal/README.md +67 -67
- package/dist/extensions/builtin/grub/README.md +112 -112
- package/dist/extensions/builtin/link-world/agent-workspace/README.md +16 -16
- package/dist/extensions/builtin/link-world/internet-search/internet-search.md +65 -65
- package/dist/extensions/builtin/link-world/link-world-agent.md +82 -82
- package/dist/extensions/builtin/link-world/linkworld.md +313 -313
- package/dist/extensions/builtin/link-world/network-routing/network-routing.md +67 -67
- package/dist/extensions/builtin/loop/README.md +92 -92
- package/dist/extensions/builtin/mcp/figma-design.md +68 -68
- package/dist/extensions/builtin/mcp/mcp-management.md +85 -85
- package/dist/extensions/builtin/recap/AGENT.md +15 -15
- package/dist/extensions/builtin/sal/README.md +72 -72
- package/dist/extensions/builtin/security-audit/README.md +289 -289
- package/dist/extensions/builtin/team/AGENT.md +112 -112
- package/dist/extensions/builtin/team/TESTING.md +299 -299
- package/dist/extensions/builtin/token-save/README.md +56 -56
- package/dist/extensions/optional/AGENT.md +10 -10
- package/dist/modes/interactive/controllers/input-submit-controller.js +2 -2
- package/dist/modes/interactive/controllers/stream-render-controller.js +2 -2
- package/dist/modes/interactive/interactive-mode.js +19 -19
- package/dist/modes/interactive/theme/dark.json +85 -85
- package/dist/modes/interactive/theme/light.json +84 -84
- package/dist/modes/interactive/theme/theme-schema.json +335 -335
- package/dist/modes/interactive/theme/warm.json +81 -81
- package/dist/node_modules/@pencil-agent/ai/dist/cli.js +0 -0
- package/dist/node_modules/@pencil-agent/ai/dist/models.generated.js +1 -1
- 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
- package/docs/SDK-TESTING.md +364 -0
- package/docs/codex-goal-command-impl.md +1055 -1055
- package/docs/codex-goal-vs-grub.md +500 -500
- package/docs/custom-provider.md +27 -27
- package/docs/extensions.md +27 -27
- package/docs/keybindings.md +27 -27
- package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +250 -250
- package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +122 -122
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -1222
- 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
- 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
- package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +320 -320
- package/docs/loop-usage-examples.md +214 -214
- package/docs/mem-core/346/212/200/346/234/257/346/226/207/346/241/243.md +593 -0
- package/docs/models.md +27 -27
- package/docs/packages.md +27 -27
- package/docs/pi-design-philosophy.md +457 -457
- package/docs/planmode.md +1987 -1987
- package/docs/prompt-templates.md +27 -27
- package/docs/providers.md +27 -27
- package/docs/sdk.md +27 -27
- package/docs/skills.md +27 -27
- package/docs/startup-performance-optimization.md +301 -0
- package/docs/themes.md +27 -27
- package/docs/tui.md +27 -27
- package/docs//350/256/244/347/237/245/345/234/260/345/233/276.md +47 -0
- package/package.json +190 -190
- package/docs/cc-agent-design.md +0 -1297
- package/docs/cc-tui-design.md +0 -1333
- package/docs/nanoPencil-/345/255/246/344/271/240/350/256/241/345/210/222.md +0 -170
- package/docs/scan-report.md +0 -3820
- package/docs//345/257/271/346/240/207Claude-Code.md +0 -1775
- 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 处理器 |
|