@bolloon/bolloon-agent 0.4.33 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +493 -35
- package/dist/agents/external-events.js +35 -6
- package/dist/agents/gateway-group.js +100 -12
- package/dist/agents/goal-criteria.js +10 -3
- package/dist/agents/goal-flywheel/block-executor.js +346 -0
- package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
- package/dist/agents/goal-flywheel/goal-change.js +754 -0
- package/dist/agents/goal-flywheel/index.js +26 -0
- package/dist/agents/goal-flywheel/memory-layers.js +227 -0
- package/dist/agents/goal-flywheel/run-closure.js +809 -0
- package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
- package/dist/agents/goal-flywheel/types.js +329 -0
- package/dist/agents/goal-flywheel/wiring/change.js +288 -0
- package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
- package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
- package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
- package/dist/agents/goal-flywheel/wiring/index.js +26 -0
- package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
- package/dist/agents/goal-flywheel/work-contract.js +478 -0
- package/dist/agents/goal-flywheel/work-monitor.js +488 -0
- package/dist/agents/goal-flywheel-wiring.js +1870 -0
- package/dist/agents/goal-state-reducer.js +406 -0
- package/dist/agents/goal-store.js +35 -1
- package/dist/agents/mobile-flywheel-view.js +144 -0
- package/dist/agents/mobile-task-actions-verify.js +74 -0
- package/dist/agents/mobile-task-actions.js +178 -0
- package/dist/agents/mobile-task-views.js +199 -0
- package/dist/agents/network-pulse.js +304 -18
- package/dist/agents/pi-sdk.js +75 -20
- package/dist/agents/run-store.js +18 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-runner.js +9 -3
- package/dist/agents/task-group.js +8 -51
- package/dist/agents/task-public-text.js +89 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/tasks.js +350 -2
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/setup-wizard.js +71 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +12 -2
- package/dist/electron-build/agents/contacts/chain.js +26 -6
- package/dist/electron-build/agents/contacts/chain.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +492 -34
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/external-events.js +34 -5
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-criteria.js +10 -3
- package/dist/electron-build/agents/goal-criteria.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
- package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
- package/dist/electron-build/agents/goal-state-reducer.js +414 -0
- package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
- package/dist/electron-build/agents/goal-store.js +35 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/network-pulse.js +307 -19
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +74 -19
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +20 -5
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/skill-readiness.js +52 -9
- package/dist/electron-build/agents/skill-readiness.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +8 -2
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/orbitdb/cid-database.js +83 -17
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/utils/dual-source.js +471 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +331 -57
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +16 -1
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-info.js +82 -2
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +103 -12
- package/dist/ios/chain-explorer.js +617 -0
- package/dist/ios/explorer.html +163 -0
- package/dist/ios/index.html +96 -0
- package/dist/ios/mobile-agent.js +2 -2
- package/dist/ios/mobile-contacts.js +329 -0
- package/dist/ios/mobile-core.js +1379 -226
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +706 -7
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +762 -0
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/utils/dual-source.js +426 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +329 -57
- package/dist/utils/update-state.js +16 -1
- package/dist/utils/version-info.js +75 -1
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +1197 -513
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +531 -1
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/package.json +3 -1
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goal-change.ts — P4 「新要求注入」的分诊 (triage) 与生效 (apply) 规则 (2026-09-25)
|
|
3
|
+
*
|
|
4
|
+
* 问题: 用户在长期执行中途说的话, 最容易被"顺手直接改掉当前 Run 的判据" —— 于是
|
|
5
|
+
* 已发生的历史被悄悄改写, 子 Agent 还在按旧判据跑, 预算被 Agent 自己批大了。
|
|
6
|
+
*
|
|
7
|
+
* 本文件把「新要求」变成一条**可追溯的变更记录** + 一次**可审计的生效判定**:
|
|
8
|
+
* ① `ingestChange` —— 逐字记录原文与来源 (不解析, 不改写, 不读真实钟)
|
|
9
|
+
* ② `classifyChange` —— 确定性启发式分类 (8 类) + 影响面 + 是否需重规划
|
|
10
|
+
* ③ `applyChange` —— 决定"排入下一 Run / 待批准 / 不生效", 并给出下一 Run 指令
|
|
11
|
+
*
|
|
12
|
+
* 边界 (ownership, 见 docs/wiki/goal-continuation-flywheel.md §13–§14):
|
|
13
|
+
* · 纯函数: 不读真实钟 (`now` 一律注入) · 不做 I/O (不 import fs) · 不改写任何输入
|
|
14
|
+
* · **只**依赖冻结面 `./types.js` 的类型; 不 import 任何其它阶段的实现文件
|
|
15
|
+
* · 冻结的三个签名 (§14) 保持原样; 其余导出是本阶段新增的辅助面
|
|
16
|
+
* · 不写 store / 不改现有调用方: `applyChange` **只返回** 判定, 落库由接线层做
|
|
17
|
+
*
|
|
18
|
+
* 冻结面 5 条规则 (`GOAL_CHANGE_RULES`) 在本文件里的落点:
|
|
19
|
+
* 1 user_revocation_wins → 用户来源的 abort 在分类阶段**压过**同批其余意图;
|
|
20
|
+
* 生效阶段必排入下一 Run, 且不增判据版本
|
|
21
|
+
* 2 agent_cannot_approve_budget → 非用户来源的"扩大/方向不明"预算变更**永不**返回
|
|
22
|
+
* `next_run` (只有可证明的**收紧**才放行)
|
|
23
|
+
* 3 criteria_change_bumps_version → 只有「用户来源 + 完成判据变化」才 +1; 其余一律不动,
|
|
24
|
+
* 且已排入/已生效的请求不许二次 +1
|
|
25
|
+
* 4 history_not_rewritten → 每条 `nextRunDirective` 都自带"只影响后续 Run"声明;
|
|
26
|
+
* 纯函数不改写任何输入对象 (深冻结也能过)
|
|
27
|
+
* 5 children_receive_new_version → 每条 `nextRunDirective` 都带子 Agent 条款;
|
|
28
|
+
* `childChangeDirective` 给出逐 workId 的下发内容
|
|
29
|
+
*/
|
|
30
|
+
import { CHANGE_SOURCES } from './types.js';
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// §0. 规则 id —— 直接引用冻结面 GOAL_CHANGE_RULES 的取值
|
|
33
|
+
// (冻结面若改了枚举取值, 这里 `satisfies` 会编译失败, 而不是静默漂移)
|
|
34
|
+
// ============================================================================
|
|
35
|
+
const RULES = {
|
|
36
|
+
revocation: 'user_revocation_wins',
|
|
37
|
+
budget: 'agent_cannot_approve_budget',
|
|
38
|
+
criteria: 'criteria_change_bumps_version',
|
|
39
|
+
history: 'history_not_rewritten',
|
|
40
|
+
children: 'children_receive_new_version',
|
|
41
|
+
};
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// §1. 确定性关键词表 (与 goal-criteria.ts 同一风格: 不依赖 LLM, 失败不伪造成功)
|
|
44
|
+
// ============================================================================
|
|
45
|
+
/** 撤销必须**指向目标本身**才成立 —— "取消预算上限"指预算, 不是撤销 (否则误杀目标) */
|
|
46
|
+
const ABORT_PATTERNS = [
|
|
47
|
+
/(撤销|终止|放弃|停掉|停止|取消)(掉|执行)?(这个|该|当前的)?(目标|任务|工作|活儿|活|计划|需求)/,
|
|
48
|
+
/(别|不要|不用)(再)?继续(这个|该)?(目标|任务|工作|活儿)/,
|
|
49
|
+
/^(不做了|别做了|算了)[。.!!]?$/,
|
|
50
|
+
/\babort\b/,
|
|
51
|
+
/cancel the (goal|task|work)/,
|
|
52
|
+
];
|
|
53
|
+
const CRITERIA_PATTERNS = [
|
|
54
|
+
/判据|完成标准|完成条件|验收标准|验收条件|判定标准|完成的定义|算完成|criteria|definition of done/,
|
|
55
|
+
];
|
|
56
|
+
/** 判据"重申" (不是改判据) 的用词 —— 用于把 criteria 意图降级为 clarification */
|
|
57
|
+
const CRITERIA_RESTATE_PATTERNS = [
|
|
58
|
+
/照旧|不变|保持|重申|强调|记住|再确认|确认一下|仍然|还是(按|用|以)/,
|
|
59
|
+
];
|
|
60
|
+
const CRITERIA_CHANGE_PATTERNS = [
|
|
61
|
+
/改成|改为|变更|变成|换成|替换|调整为|增加|新增|加上|再加|删掉|删除|去掉|不再|取消|降低标准|提高标准/,
|
|
62
|
+
];
|
|
63
|
+
const BUDGET_PATTERNS = [
|
|
64
|
+
/预算|budget|成本上限|花费上限|开销上限|最大轮次|轮次上限|maxruns|时限|deadline|时间上限|耗时上限/,
|
|
65
|
+
];
|
|
66
|
+
const BUDGET_INCREASE_PATTERNS = [
|
|
67
|
+
/提高|增加|加大|扩大|放宽|上调|提升|更多|增至|提到|取消上限|去掉上限|不限预算|取消预算(限制|上限)|延长/,
|
|
68
|
+
];
|
|
69
|
+
const BUDGET_DECREASE_PATTERNS = [
|
|
70
|
+
/降低|减少|减小|收紧|缩减|下调|缩短|压缩/,
|
|
71
|
+
];
|
|
72
|
+
const PERMISSION_PATTERNS = [
|
|
73
|
+
/权限|授权|permission|允许访问|允许使用|允许调用|禁止访问|禁止使用|工具白名单|可以访问/,
|
|
74
|
+
];
|
|
75
|
+
const SCOPE_EXPANSION_PATTERNS = [
|
|
76
|
+
/顺便|另外(也|要|再|还有)|还要|也要(把|做|改)|再多做|多做一点|扩展范围|扩大范围|范围扩大|额外(也)?要|多做一个/,
|
|
77
|
+
];
|
|
78
|
+
const SCOPE_REDUCTION_PATTERNS = [
|
|
79
|
+
/只做|只保留|只提交|只改|缩小范围|范围缩小|砍掉|先不做|不用做|减少范围|减掉|不做(这个|那)?部分/,
|
|
80
|
+
];
|
|
81
|
+
const PRIORITY_PATTERNS = [
|
|
82
|
+
/优先级|优先|先做|后做|提前|插队|加急|priority/,
|
|
83
|
+
];
|
|
84
|
+
const KIND_LABELS = {
|
|
85
|
+
clarification: '补充说明',
|
|
86
|
+
priority_change: '优先级变化',
|
|
87
|
+
success_criteria_change: '完成判据变化',
|
|
88
|
+
budget_change: '预算变更',
|
|
89
|
+
permission_change: '权限变更',
|
|
90
|
+
scope_expansion: '范围扩大',
|
|
91
|
+
scope_reduction: '范围缩小',
|
|
92
|
+
abort: '撤销/终止',
|
|
93
|
+
};
|
|
94
|
+
/** 需要重规划的类别 (abort 是终止, 不重规划; clarification 不动计划) */
|
|
95
|
+
const REPLAN_KINDS = new Set([
|
|
96
|
+
'priority_change',
|
|
97
|
+
'success_criteria_change',
|
|
98
|
+
'budget_change',
|
|
99
|
+
'permission_change',
|
|
100
|
+
'scope_expansion',
|
|
101
|
+
'scope_reduction',
|
|
102
|
+
]);
|
|
103
|
+
/** 来源决定默认优先级 (用户最高; 撤销另有 user_revocation) */
|
|
104
|
+
const SOURCE_PRIORITY = {
|
|
105
|
+
user: 'high',
|
|
106
|
+
agent: 'normal',
|
|
107
|
+
external: 'normal',
|
|
108
|
+
system: 'low',
|
|
109
|
+
};
|
|
110
|
+
/** 生效判定的终态: 这三种状态再调 `applyChange` 一律拒绝 (幂等, 防重复增版本) */
|
|
111
|
+
const TERMINAL_STATUSES = ['applied', 'scheduled_next_run', 'superseded'];
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// §2. 小工具
|
|
114
|
+
// ============================================================================
|
|
115
|
+
function hasAny(text, pats) {
|
|
116
|
+
return pats.some((p) => p.test(text));
|
|
117
|
+
}
|
|
118
|
+
function lower(s) {
|
|
119
|
+
return String(s ?? '').toLowerCase();
|
|
120
|
+
}
|
|
121
|
+
/** FNV-1a 指纹 (纯函数, 不引 crypto/不读钟: 同一输入必得同一 id, 便于去重) */
|
|
122
|
+
function fingerprint(parts) {
|
|
123
|
+
const s = parts.join('\u0001');
|
|
124
|
+
let h = 0x811c9dc5;
|
|
125
|
+
for (let i = 0; i < s.length; i++) {
|
|
126
|
+
h ^= s.charCodeAt(i);
|
|
127
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
128
|
+
}
|
|
129
|
+
return h.toString(16).padStart(8, '0');
|
|
130
|
+
}
|
|
131
|
+
function head(s, n = 60) {
|
|
132
|
+
const t = String(s ?? '').trim();
|
|
133
|
+
return t.length > n ? `${t.slice(0, n)}…` : t;
|
|
134
|
+
}
|
|
135
|
+
/** 判据版本归一 (缺省/非法 → 1); **只做归一, 不做 +1** */
|
|
136
|
+
function normalizeVersion(v) {
|
|
137
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 1 ? Math.floor(v) : 1;
|
|
138
|
+
}
|
|
139
|
+
function budgetBaseline(b) {
|
|
140
|
+
if (typeof b === 'number' && Number.isFinite(b))
|
|
141
|
+
return b;
|
|
142
|
+
if (b && typeof b === 'object') {
|
|
143
|
+
const rec = b;
|
|
144
|
+
for (const k of ['maxRuns', 'deadlineMs', 'maxSteps', 'maxAmount']) {
|
|
145
|
+
const v = rec[k];
|
|
146
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
147
|
+
return v;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// §3. 意图探测 (导出: 接线层用它把一句话拆成多条变更)
|
|
154
|
+
// ============================================================================
|
|
155
|
+
/**
|
|
156
|
+
* 从原话里探测**所有**变更意图 (不去重以外不做裁剪, 交给 `classifyChange` 做消歧)。
|
|
157
|
+
* 返回空数组 = 没有可识别的变更意图 (即 `clarification`)。
|
|
158
|
+
*
|
|
159
|
+
* 刻意返回原文命中的**全部**意图: 一句话里出现两个不同领域的意图时,
|
|
160
|
+
* 一个 `kind` 字段装不下 —— 宁可让人拆开重提, 也不许静默丢掉一条要求。
|
|
161
|
+
*/
|
|
162
|
+
export function detectChangeIntents(instruction) {
|
|
163
|
+
const t = lower(instruction);
|
|
164
|
+
const out = [];
|
|
165
|
+
if (hasAny(t, ABORT_PATTERNS))
|
|
166
|
+
out.push('abort');
|
|
167
|
+
if (hasAny(t, CRITERIA_PATTERNS))
|
|
168
|
+
out.push('success_criteria_change');
|
|
169
|
+
if (hasAny(t, BUDGET_PATTERNS))
|
|
170
|
+
out.push('budget_change');
|
|
171
|
+
if (hasAny(t, PERMISSION_PATTERNS))
|
|
172
|
+
out.push('permission_change');
|
|
173
|
+
if (hasAny(t, SCOPE_EXPANSION_PATTERNS))
|
|
174
|
+
out.push('scope_expansion');
|
|
175
|
+
if (hasAny(t, SCOPE_REDUCTION_PATTERNS))
|
|
176
|
+
out.push('scope_reduction');
|
|
177
|
+
if (hasAny(t, PRIORITY_PATTERNS))
|
|
178
|
+
out.push('priority_change');
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
/** 判据相关的话是不是"重申既有判据"(而不是改判据) */
|
|
182
|
+
function isCriteriaRestatement(instruction, criteria) {
|
|
183
|
+
const t = lower(instruction);
|
|
184
|
+
if (hasAny(t, CRITERIA_CHANGE_PATTERNS))
|
|
185
|
+
return false;
|
|
186
|
+
if (hasAny(t, CRITERIA_RESTATE_PATTERNS))
|
|
187
|
+
return true;
|
|
188
|
+
return criteria.some((c) => c.length >= 2 && t.includes(lower(c)));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 判断预算变更是"扩大 / 收紧 / 判不出"。
|
|
192
|
+
* 数值优先 (有基准可比大小); 否则看方向用词; 两者都判不出 → `unspecified`。
|
|
193
|
+
* 注意: `applyChange` 拿不到 goal.budget, 因此生效阶段只能做**纯文本**判定 ——
|
|
194
|
+
* 判不出时按"不得自动批准"保守处理 (见 §6)。
|
|
195
|
+
*/
|
|
196
|
+
export function detectBudgetDirection(instruction, goalBudget) {
|
|
197
|
+
const t = lower(instruction);
|
|
198
|
+
const nums = (t.match(/\d+(\.\d+)?/g) ?? []).map(Number).filter((n) => Number.isFinite(n));
|
|
199
|
+
const last = nums.slice(-1)[0];
|
|
200
|
+
const to = typeof last === 'number' ? last : null;
|
|
201
|
+
const from = budgetBaseline(goalBudget);
|
|
202
|
+
const inc = hasAny(t, BUDGET_INCREASE_PATTERNS);
|
|
203
|
+
const dec = hasAny(t, BUDGET_DECREASE_PATTERNS);
|
|
204
|
+
if (from !== null && to !== null && from !== to) {
|
|
205
|
+
return {
|
|
206
|
+
direction: to > from ? 'increase' : 'decrease',
|
|
207
|
+
from,
|
|
208
|
+
to,
|
|
209
|
+
basis: `数值比较: ${from} → ${to}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (inc && !dec)
|
|
213
|
+
return { direction: 'increase', from, to, basis: '指令用词指向扩大' };
|
|
214
|
+
if (dec && !inc)
|
|
215
|
+
return { direction: 'decrease', from, to, basis: '指令用词指向收紧' };
|
|
216
|
+
if (inc && dec)
|
|
217
|
+
return { direction: 'unspecified', from, to, basis: '同时出现扩大与收紧用词 → 不猜' };
|
|
218
|
+
return { direction: 'unspecified', from, to, basis: '既无可比数值, 也无明确方向用词 → 不猜 (保守按需批准)' };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 这个变更是不是"放宽" —— 放宽只能由 principal (user) 决定。
|
|
222
|
+
* agent 可以收紧 (预算变小 / 范围变小), 不能放宽 (扩预算 / 扩范围 / 加权限 / 改写"什么算完成" / 撤销目标)。
|
|
223
|
+
*/
|
|
224
|
+
export function isWidening(kind, direction = 'unspecified') {
|
|
225
|
+
switch (kind) {
|
|
226
|
+
case 'abort':
|
|
227
|
+
case 'success_criteria_change':
|
|
228
|
+
case 'permission_change':
|
|
229
|
+
case 'scope_expansion':
|
|
230
|
+
return true;
|
|
231
|
+
case 'budget_change':
|
|
232
|
+
return direction !== 'decrease';
|
|
233
|
+
default:
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// ============================================================================
|
|
238
|
+
// §5. ① 记录 —— 原话逐字入档 (不解析)
|
|
239
|
+
// ============================================================================
|
|
240
|
+
/**
|
|
241
|
+
* 接收一条新要求: **逐字**记录原文 + 来源 + 记录者 + 时间。
|
|
242
|
+
* 不解析、不判断、不改写 —— 分类是 `classifyChange` 的事。
|
|
243
|
+
*
|
|
244
|
+
* `kind` 在未分诊时取最保守的 `clarification` (不改判据/不扩预算),
|
|
245
|
+
* 且 `status='received'` 会让 `applyChange` 直接拒绝 —— 未分诊的变更不可能生效。
|
|
246
|
+
*
|
|
247
|
+
* @throws 当 goalId / instruction / recordedBy / now 缺失, 或 source 不是合法来源。
|
|
248
|
+
*/
|
|
249
|
+
export function ingestChange(input) {
|
|
250
|
+
const goalId = String(input?.goalId ?? '').trim();
|
|
251
|
+
const instruction = typeof input?.instruction === 'string' ? input.instruction : '';
|
|
252
|
+
const recordedBy = String(input?.recordedBy ?? '').trim();
|
|
253
|
+
const now = String(input?.now ?? '').trim();
|
|
254
|
+
if (!goalId)
|
|
255
|
+
throw new Error('[goal-change] ingestChange: goalId 不能为空');
|
|
256
|
+
if (!instruction.trim())
|
|
257
|
+
throw new Error('[goal-change] ingestChange: instruction 不能为空 (用户原话必须逐字记录)');
|
|
258
|
+
if (!recordedBy)
|
|
259
|
+
throw new Error('[goal-change] ingestChange: recordedBy 不能为空 (变更必须可追溯)');
|
|
260
|
+
if (!now)
|
|
261
|
+
throw new Error('[goal-change] ingestChange: now 必须由调用方注入 (本模块不读真实钟)');
|
|
262
|
+
if (!CHANGE_SOURCES.includes(input.source)) {
|
|
263
|
+
throw new Error(`[goal-change] ingestChange: 非法 source "${String(input.source)}" (合法: ${CHANGE_SOURCES.join('/')})`);
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
changeId: `chg_${fingerprint([goalId, input.source, instruction, recordedBy, now])}`,
|
|
267
|
+
goalId,
|
|
268
|
+
source: input.source,
|
|
269
|
+
instruction, // 逐字: 不做 trim / 不做转义 (原文就是原文)
|
|
270
|
+
interpreted: null,
|
|
271
|
+
priority: SOURCE_PRIORITY[input.source],
|
|
272
|
+
kind: 'clarification', // 未分诊前的保守占位, 不代表真分类
|
|
273
|
+
scope: { kind: 'goal', targetIds: [goalId] },
|
|
274
|
+
impact: {
|
|
275
|
+
affectsObjective: false,
|
|
276
|
+
affectsCriteria: false,
|
|
277
|
+
affectsBudget: false,
|
|
278
|
+
affectsPermission: false,
|
|
279
|
+
affectedWorkIds: [],
|
|
280
|
+
},
|
|
281
|
+
effectiveAt: now, // 本飞轮语义: 最早生效点是"下一次 Run"
|
|
282
|
+
requiresReplan: false,
|
|
283
|
+
status: 'received',
|
|
284
|
+
recordedAt: now,
|
|
285
|
+
recordedBy,
|
|
286
|
+
appliesToFutureRunsOnly: true,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// ============================================================================
|
|
290
|
+
// §6. ② 分类 —— 8 类 + 影响面 + 是否需重规划
|
|
291
|
+
// ============================================================================
|
|
292
|
+
function impactFor(kind, scope) {
|
|
293
|
+
return {
|
|
294
|
+
affectsObjective: kind === 'scope_expansion' || kind === 'scope_reduction' || kind === 'abort',
|
|
295
|
+
affectsCriteria: kind === 'success_criteria_change',
|
|
296
|
+
affectsBudget: kind === 'budget_change',
|
|
297
|
+
affectsPermission: kind === 'permission_change',
|
|
298
|
+
affectedWorkIds: scope.kind === 'child_work' ? [...new Set(scope.targetIds)] : [],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const EMPTY_IMPACT = (scope) => impactFor('clarification', scope);
|
|
302
|
+
function describeKind(kind, r, dir) {
|
|
303
|
+
switch (kind) {
|
|
304
|
+
case 'abort':
|
|
305
|
+
return `撤销/终止: 在下一个 Run 边界终止该目标 (优先级最高, 压过同批其余变更)`;
|
|
306
|
+
case 'success_criteria_change':
|
|
307
|
+
return `完成判据变化: **必须增版本号** (批准时 criteriaVersion +1); 本文不改写已在执行的 Run 历史`;
|
|
308
|
+
case 'budget_change':
|
|
309
|
+
return `预算变更 (方向=${dir ? dir.direction : 'unspecified'}; ${dir ? dir.basis : '未判定'}): 非用户来源不得自动批准`;
|
|
310
|
+
case 'permission_change':
|
|
311
|
+
return `权限变更: 权限是安全面, 只有 principal (user) 能放宽`;
|
|
312
|
+
case 'scope_expansion':
|
|
313
|
+
return `范围扩大: 目标影响面变大 → 需重规划, 且只有 principal 能放宽`;
|
|
314
|
+
case 'scope_reduction':
|
|
315
|
+
return `范围缩小: 只收紧, 不改判据 → 可排入下一 Run`;
|
|
316
|
+
case 'priority_change':
|
|
317
|
+
return `优先级变化: 不改变完成判据, 只调整下一步顺序`;
|
|
318
|
+
default:
|
|
319
|
+
return `补充说明: 不改判据/预算/权限, 也不增判据版本`;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* 分类: 把原话收敛成**一个** `kind` + 影响面 + 是否需重规划 + 人类可读摘要。
|
|
324
|
+
*
|
|
325
|
+
* 消歧规则 (全部写死成规则, 不靠模型):
|
|
326
|
+
* · 判据"重申" (照旧/确认一下/原句重复) → `clarification`, **不**算改判据
|
|
327
|
+
* · 判据"变更" 出现时, 范围修饰词 (去掉/只保留…) 视为对判据本身的编辑, 不再重复算成范围变更
|
|
328
|
+
* · **用户撤销优先** (规则 1): 同批其余意图一律作废 → `kind='abort'`, `priority='user_revocation'`
|
|
329
|
+
* · 其余多意图 (一个 kind 装不下) → 退回 `status='received'` + `interpreted=null` (交人拆开重提)
|
|
330
|
+
*
|
|
331
|
+
* 已经是终态 (`applied`/`scheduled_next_run`/`superseded`) 的记录不许被重新分类
|
|
332
|
+
* (否则等于把已生效的变更又拉回可再次生效的状态)。
|
|
333
|
+
*/
|
|
334
|
+
export function classifyChange(r, goal) {
|
|
335
|
+
if (TERMINAL_STATUSES.includes(r.status))
|
|
336
|
+
return { ...r, appliesToFutureRunsOnly: true };
|
|
337
|
+
const t = lower(r.instruction);
|
|
338
|
+
const criteria = Array.isArray(goal?.successCriteria) ? goal.successCriteria.map(String) : [];
|
|
339
|
+
let intents = detectChangeIntents(r.instruction);
|
|
340
|
+
if (intents.includes('success_criteria_change') && isCriteriaRestatement(r.instruction, criteria)) {
|
|
341
|
+
intents = intents.filter((k) => k !== 'success_criteria_change');
|
|
342
|
+
}
|
|
343
|
+
if (intents.includes('success_criteria_change')) {
|
|
344
|
+
// "判据里去掉最后一条" 是对判据的编辑, 不是范围缩小
|
|
345
|
+
intents = intents.filter((k) => k !== 'scope_expansion' && k !== 'scope_reduction' && k !== 'priority_change');
|
|
346
|
+
}
|
|
347
|
+
const scope = r.scope ?? { kind: 'goal', targetIds: [r.goalId] };
|
|
348
|
+
const base = {
|
|
349
|
+
...r,
|
|
350
|
+
priority: SOURCE_PRIORITY[r.source],
|
|
351
|
+
scope,
|
|
352
|
+
impact: EMPTY_IMPACT(scope),
|
|
353
|
+
appliesToFutureRunsOnly: true,
|
|
354
|
+
};
|
|
355
|
+
// 规则 1: 用户明确撤销优先 —— 压过同批其余意图
|
|
356
|
+
if (r.source === 'user' && intents.includes('abort')) {
|
|
357
|
+
const superseded = intents.filter((k) => k !== 'abort');
|
|
358
|
+
return {
|
|
359
|
+
...base,
|
|
360
|
+
kind: 'abort',
|
|
361
|
+
priority: 'user_revocation',
|
|
362
|
+
status: 'triaged',
|
|
363
|
+
requiresReplan: false,
|
|
364
|
+
impact: impactFor('abort', scope),
|
|
365
|
+
interpreted: `${describeKind('abort', r, null)}; 原文: ${head(r.instruction)}` +
|
|
366
|
+
(superseded.length ? `; 同批的 ${superseded.join('/')} 一律作废 (superseded), 需单独重新提交` : ''),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
// 一个 kind 装不下多个意图 → 不猜, 交人
|
|
370
|
+
if (intents.length > 1) {
|
|
371
|
+
return {
|
|
372
|
+
...base,
|
|
373
|
+
kind: 'clarification',
|
|
374
|
+
status: 'received',
|
|
375
|
+
interpreted: null,
|
|
376
|
+
requiresReplan: false,
|
|
377
|
+
impact: EMPTY_IMPACT(scope),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const kind = intents.length === 1 ? intents[0] : 'clarification';
|
|
381
|
+
const dir = kind === 'budget_change' ? detectBudgetDirection(r.instruction, goal?.budget) : null;
|
|
382
|
+
const impact = impactFor(kind, scope);
|
|
383
|
+
return {
|
|
384
|
+
...base,
|
|
385
|
+
kind,
|
|
386
|
+
status: 'triaged',
|
|
387
|
+
requiresReplan: REPLAN_KINDS.has(kind),
|
|
388
|
+
impact,
|
|
389
|
+
interpreted: `${KIND_LABELS[kind]}: ${describeKind(kind, r, dir)}; 影响面: ${impactLabel(impact)}; 原文: ${head(r.instruction)}`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function impactLabel(i) {
|
|
393
|
+
const parts = [];
|
|
394
|
+
if (i.affectsObjective)
|
|
395
|
+
parts.push('目标');
|
|
396
|
+
if (i.affectsCriteria)
|
|
397
|
+
parts.push('判据');
|
|
398
|
+
if (i.affectsBudget)
|
|
399
|
+
parts.push('预算');
|
|
400
|
+
if (i.affectsPermission)
|
|
401
|
+
parts.push('权限');
|
|
402
|
+
if (i.affectedWorkIds.length)
|
|
403
|
+
parts.push(`子任务(${i.affectedWorkIds.length})`);
|
|
404
|
+
return parts.length ? parts.join('/') : '无';
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* 把变更范围收到具体子 Agent 工作上 (规则 5 的数据来源)。
|
|
408
|
+
* 冻结面 `classifyChange` 看不到在跑的 workId, 所以由接线层在这一步注入。
|
|
409
|
+
*/
|
|
410
|
+
export function scopeChangeToWork(r, workIds) {
|
|
411
|
+
const ids = [...new Set((workIds ?? []).map(String).filter(Boolean))];
|
|
412
|
+
const scope = { kind: ids.length ? 'child_work' : 'goal', targetIds: ids.length ? ids : [r.goalId] };
|
|
413
|
+
return { ...r, scope, impact: { ...r.impact, affectedWorkIds: ids }, appliesToFutureRunsOnly: true };
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* 注入状态后**必须**下发的条款 (规则 4): 新要求只影响后续 Run。
|
|
417
|
+
* 任何生效/待批准的变更都带这条 —— 不靠调用方记得写。
|
|
418
|
+
*/
|
|
419
|
+
function historyClause(v) {
|
|
420
|
+
return `[${RULES.history}] 本次变更只影响后续 Run (criteriaVersion=v${v}): 当前 Run 已发生的历史 / 证据 / 已完成步骤一律不改写、不回填。`;
|
|
421
|
+
}
|
|
422
|
+
/** 子 Agent 条款 (规则 5): 受影响子 Agent 必须先拿到变更版本 */
|
|
423
|
+
function childClause(r, v) {
|
|
424
|
+
const workIds = [...new Set(r.impact.affectedWorkIds)];
|
|
425
|
+
if (!workIds.length) {
|
|
426
|
+
return `[${RULES.children}] 本次变更无受影响子 Agent (impact.affectedWorkIds 为空); 若有在跑的子 Agent, 下发前必须先补全影响面 (scopeChangeToWork)。`;
|
|
427
|
+
}
|
|
428
|
+
const action = r.kind === 'abort' ? '停止并按父的 cancelPolicy 收尾' : '按变更版本继续下一步';
|
|
429
|
+
return `[${RULES.children}] 下列子 Agent 必须**先确认收到**变更版本 (criteriaVersion=v${v}) 再${action}; 未确认前不得按旧版本继续: ${workIds.join(', ')}。`;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* 原话条款 (P5 验收修复): 用户/外部**说的那句话本身**必须逐字进"下一 Run 的指令"。
|
|
433
|
+
*
|
|
434
|
+
* 原来只有 `parts` (由 kind 生成的摘要, 例如"补充说明: 只作为下一次 Run 的上下文…") 进指令 ——
|
|
435
|
+
* 人在界面上看不到自己提的要求, 只看到一句机器话 (`ingestGoalChange` 的返回与
|
|
436
|
+
* `continuation.nextAction` 都是这份指令, 网页"新要求"直接把返回打给人看)。
|
|
437
|
+
*
|
|
438
|
+
* 只对 `next_run` (真排入下一 Run) 的变更下发; 待批准/被拒的变更**不带**这条 ——
|
|
439
|
+
* 那两种情况下要求还没生效, 把它写进下一 Run 的指令会被执行器误读成"已批准"。
|
|
440
|
+
*/
|
|
441
|
+
function instructionClause(r) {
|
|
442
|
+
return `原话 (逐字入档, 不许改写/总结; 来源=${r.source}, 变更=${r.changeId}): ${r.instruction}`;
|
|
443
|
+
}
|
|
444
|
+
function compose(r, outcome, requiresReplan, criteriaVersion, reason, parts) {
|
|
445
|
+
const directive = [
|
|
446
|
+
...(outcome === 'next_run' ? [instructionClause(r)] : []),
|
|
447
|
+
...parts,
|
|
448
|
+
historyClause(criteriaVersion),
|
|
449
|
+
childClause(r, criteriaVersion),
|
|
450
|
+
].join('\n');
|
|
451
|
+
return { outcome, requiresReplan, nextRunDirective: directive, criteriaVersion, reason };
|
|
452
|
+
}
|
|
453
|
+
function reject(criteriaVersion, reason) {
|
|
454
|
+
return {
|
|
455
|
+
outcome: 'rejected',
|
|
456
|
+
requiresReplan: false,
|
|
457
|
+
nextRunDirective: `不做任何变更: ${reason}`,
|
|
458
|
+
criteriaVersion,
|
|
459
|
+
reason,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function pendingReason(r, dir) {
|
|
463
|
+
switch (r.kind) {
|
|
464
|
+
case 'abort':
|
|
465
|
+
return `[${RULES.revocation}] 撤销/终止只有用户 (principal) 能发起: 来源 ${r.source} 的 abort 不允许自动生效, 需人确认 (用户自己撤销时优先级最高)`;
|
|
466
|
+
case 'budget_change':
|
|
467
|
+
return `[${RULES.budget}] 来源 ${r.source} 的预算变更方向=${dir.direction} (${dir.basis}) —— 不是"可证明的收紧", Agent 不得自行批准扩大预算, 需人批准`;
|
|
468
|
+
case 'success_criteria_change':
|
|
469
|
+
return `[${RULES.criteria}] 来源 ${r.source} 不得改写"什么算完成": 待人批准后才 criteriaVersion +1`;
|
|
470
|
+
case 'permission_change':
|
|
471
|
+
return `[principal_only] 权限是安全面: 来源 ${r.source} 不得自行放宽权限, 需人批准`;
|
|
472
|
+
default:
|
|
473
|
+
return `[principal_only] 来源 ${r.source} 不得扩大目标范围 (范围扩大抬高风险面), 需人批准`;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function pendingEffect(r) {
|
|
477
|
+
switch (r.kind) {
|
|
478
|
+
case 'abort':
|
|
479
|
+
return `批准后: 在下一个 Run 边界终止该目标; 同批未生效的变更一并作废 (superseded)。`;
|
|
480
|
+
case 'budget_change':
|
|
481
|
+
return `批准后: 按用户原话调整预算上限并排入下一个 Run。`;
|
|
482
|
+
case 'success_criteria_change':
|
|
483
|
+
return `批准后: criteriaVersion +1, 下一次 Run 起按新判据判定 (旧证据保留, 不改写)。`;
|
|
484
|
+
case 'permission_change':
|
|
485
|
+
return `批准后: 只在下一个 Run 生效; 正在跑的子 Agent 按新权限继续。`;
|
|
486
|
+
default:
|
|
487
|
+
return `批准后: 下一个 Run 重新规划 (范围变大)。`;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* 生效判定 (纯函数, 不改写输入)。
|
|
492
|
+
*
|
|
493
|
+
* 判定顺序 (先安全后便利):
|
|
494
|
+
* 0. 未分诊 (`received` / `interpreted=null`) → 拒绝: 没理解的要求不许改变行为
|
|
495
|
+
* 1. 终态 (`scheduled_next_run`/`applied`/`superseded`) → 拒绝: 幂等, 防二次增判据版本
|
|
496
|
+
* 2. 用户撤销 (规则 1) → 排入下一 Run (且不增判据版本)
|
|
497
|
+
* 3. 非用户来源的"放宽" (扩预算/加权限/扩范围/改判据/撤销) → 待批准 (规则 2/3)
|
|
498
|
+
* 4. 其余 (补充说明 / 优先级 / 用户提出的变更 / 收紧类) → 排入下一 Run
|
|
499
|
+
*/
|
|
500
|
+
export function applyChange(r, goal) {
|
|
501
|
+
const base = normalizeVersion(goal?.criteriaVersion);
|
|
502
|
+
// 0. 未分诊
|
|
503
|
+
if (r.status === 'received' || !r.interpreted) {
|
|
504
|
+
const intents = detectChangeIntents(r.instruction);
|
|
505
|
+
const why = intents.length > 1
|
|
506
|
+
? `原话同时命中 ${intents.length} 个变更意图 (${intents.join('/')}), 一个 kind 装不下 —— 需拆成多条 GoalChangeRequest 重新提交`
|
|
507
|
+
: `还没有分类摘要 (status=${r.status}, interpreted=${r.interpreted === null ? 'null' : '有'}), 先 classifyChange 或交人分诊`;
|
|
508
|
+
return reject(base, `[未分诊] 变更 ${r.changeId} ${why}, 不许直接生效 (不改写历史/不改判据版本)`);
|
|
509
|
+
}
|
|
510
|
+
// 1. 终态幂等
|
|
511
|
+
if (TERMINAL_STATUSES.includes(r.status)) {
|
|
512
|
+
return reject(base, `[幂等] 变更 ${r.changeId} 状态已是 ${r.status}, 不重复生效 (尤其不许二次 +1 判据版本)`);
|
|
513
|
+
}
|
|
514
|
+
// 2. 规则 1: 用户明确撤销优先
|
|
515
|
+
if (r.kind === 'abort') {
|
|
516
|
+
if (r.source === 'user' && r.priority === 'user_revocation') {
|
|
517
|
+
return compose(r, 'next_run', false, base, `[${RULES.revocation}] 用户明确撤销优先级最高: 排入下一个 Run 终止该目标; 判据版本不变`, [
|
|
518
|
+
`用户明确撤销: 在下一个 Run 边界终止该目标, 不做任何补救式继续; 同批未生效的变更一并作废 (superseded)。`,
|
|
519
|
+
]);
|
|
520
|
+
}
|
|
521
|
+
return compose(r, 'pending_approval', true, base, pendingReason(r, { direction: 'unspecified', from: null, to: null, basis: '-' }), [
|
|
522
|
+
`待批准 (outcome=pending_approval): 批准前不动任何东西 —— 不终止目标、不排入下一 Run、判据版本不变。`,
|
|
523
|
+
pendingEffect(r),
|
|
524
|
+
`批准路径: 由人确认后以 source='user' 重新记录 (见 approvalAsUserChange), 原话逐字保留。`,
|
|
525
|
+
]);
|
|
526
|
+
}
|
|
527
|
+
const dir = r.kind === 'budget_change'
|
|
528
|
+
? detectBudgetDirection(r.instruction) // 生效阶段拿不到 goal.budget → 纯文本判定
|
|
529
|
+
: { direction: 'unspecified', from: null, to: null, basis: '-' };
|
|
530
|
+
// 3. 规则 2/3: 非用户来源不得"放宽"
|
|
531
|
+
if (isWidening(r.kind, dir.direction) && r.source !== 'user') {
|
|
532
|
+
return compose(r, 'pending_approval', true, base, pendingReason(r, dir), [
|
|
533
|
+
`待批准 (outcome=pending_approval): 批准前不动任何东西 —— 不排入下一 Run、判据版本不变、不改写历史。`,
|
|
534
|
+
pendingEffect(r),
|
|
535
|
+
`批准路径: 由人确认后以 source='user' 重新记录 (见 approvalAsUserChange), 原话逐字保留。`,
|
|
536
|
+
]);
|
|
537
|
+
}
|
|
538
|
+
// 4. 规则 3: 只有"用户来源 + 改判据"才 +1
|
|
539
|
+
if (r.kind === 'success_criteria_change') {
|
|
540
|
+
const next = base + 1;
|
|
541
|
+
return compose(r, 'next_run', true, next, `[${RULES.criteria}] 用户改完成判据: criteriaVersion ${base} → ${next}; 只有改判据才 +1`, [
|
|
542
|
+
`完成判据变更: criteriaVersion ${base} → ${next}; 下一次 Run 起按新判据判定 (旧判据下已产生的证据保留, 不改写)。`,
|
|
543
|
+
]);
|
|
544
|
+
}
|
|
545
|
+
// 5. 其余: 补充说明 / 优先级 / 收紧类 / 用户提出的放宽
|
|
546
|
+
const reason = r.kind === 'budget_change'
|
|
547
|
+
? r.source === 'user'
|
|
548
|
+
? `[${RULES.budget}] 预算变更来源是用户 (= 出资方本人), 视为已批准; 判据版本不变`
|
|
549
|
+
: `[${RULES.budget}] 非用户来源但方向可证明为收紧 (${dir.basis}), 只变小不扩大 → 允许排入下一 Run; 判据版本不变`
|
|
550
|
+
: `[无放宽] ${KIND_LABELS[r.kind]} 不扩大风险面 (来源 ${r.source}) → 排入下一 Run; 判据版本不变`;
|
|
551
|
+
return compose(r, 'next_run', REPLAN_KINDS.has(r.kind), base, reason, [effectFor(r, dir, base)]);
|
|
552
|
+
}
|
|
553
|
+
function effectFor(r, dir, v) {
|
|
554
|
+
switch (r.kind) {
|
|
555
|
+
case 'budget_change':
|
|
556
|
+
return r.source === 'user'
|
|
557
|
+
? `预算变更 (用户本人提出 = 已批准, 方向=${dir.direction}): 按用户原话调整预算上限, 排入下一个 Run。`
|
|
558
|
+
: `预算只收紧 (方向=decrease): 允许排入下一个 Run; 若收紧后已低于已消耗量, 接线层须在开跑前停下交人。`;
|
|
559
|
+
case 'permission_change':
|
|
560
|
+
return `权限变更 (用户本人决定): 只在下一个 Run 生效, 正在跑的子 Agent 按新权限继续 (旧产出不改写)。`;
|
|
561
|
+
case 'scope_expansion':
|
|
562
|
+
return `范围扩大 (用户本人提出 = 已批准): 下一个 Run 重新规划。`;
|
|
563
|
+
case 'scope_reduction':
|
|
564
|
+
return `范围缩小: 只收紧目标范围, 排入下一个 Run (判据版本不变)。`;
|
|
565
|
+
case 'priority_change':
|
|
566
|
+
return `优先级变化: 不改变完成判据, 下一个 Run 按新优先级排序。`;
|
|
567
|
+
default:
|
|
568
|
+
return `补充说明: 只作为下一次 Run 的上下文 (v${v}), 不改判据/预算/权限, 也不增判据版本。`;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// ============================================================================
|
|
572
|
+
// §8. 配套面: 状态推进 / 覆盖旧变更 / 人工批准 / 子 Agent 下发
|
|
573
|
+
// ============================================================================
|
|
574
|
+
/** 判定结果 → 变更记录该落到哪个状态 (接线层负责真的写) */
|
|
575
|
+
export function nextStatusFor(outcome) {
|
|
576
|
+
if (outcome === 'next_run')
|
|
577
|
+
return 'scheduled_next_run';
|
|
578
|
+
if (outcome === 'pending_approval')
|
|
579
|
+
return 'needs_approval';
|
|
580
|
+
return 'rejected';
|
|
581
|
+
}
|
|
582
|
+
/** 规则 1 的推论: 用户撤销压过一切 → 之前未生效的变更一律作废 */
|
|
583
|
+
export function shouldSupersedePending(r) {
|
|
584
|
+
return r.kind === 'abort' && r.source === 'user' && r.priority === 'user_revocation';
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* 人工批准路径 (规则 2/3 的出口)。
|
|
588
|
+
*
|
|
589
|
+
* 冻结的 `GoalChangeRequest` **没有**批准字段, 因此批准这件事在本模块里的表达是:
|
|
590
|
+
* **由人 (principal) 以 `source='user'` 重新记录同一条变更** —— Agent 的原始请求永远是
|
|
591
|
+
* `pending_approval`, 没有任何代码路径能让 Agent 自己把扩预算变成 `next_run`。
|
|
592
|
+
*/
|
|
593
|
+
export function approvalAsUserChange(r, approvedBy, now) {
|
|
594
|
+
const approved = ingestChange({ goalId: r.goalId, source: 'user', instruction: r.instruction, recordedBy: approvedBy, now });
|
|
595
|
+
const classified = classifyChange(approved, { successCriteria: [] });
|
|
596
|
+
if (classified.status !== 'triaged')
|
|
597
|
+
return classified; // 原话本身就有歧义 → 还是要人拆
|
|
598
|
+
return {
|
|
599
|
+
...classified,
|
|
600
|
+
interpreted: `人工批准 (${approvedBy}): 采纳来源 ${r.source} 的变更 ${r.changeId}; ${classified.interpreted}`,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
export function childChangeDirective(r, applied) {
|
|
604
|
+
const workIds = [...new Set(r.impact.affectedWorkIds)];
|
|
605
|
+
const v = normalizeVersion(applied?.criteriaVersion);
|
|
606
|
+
const action = r.kind === 'abort' ? '停止并按父的 cancelPolicy 收尾' : `按变更版本 (criteriaVersion=v${v}) 继续下一步`;
|
|
607
|
+
const directive = workIds.length
|
|
608
|
+
? `[${RULES.children}] 变更 ${r.changeId} 下发到 ${workIds.length} 个子 Agent (${workIds.join(', ')}): 每个子必须先确认收到 criteriaVersion=v${v} 再${action}; 未确认前不得按旧版本继续。`
|
|
609
|
+
: `[${RULES.children}] 变更 ${r.changeId} 没有受影响子 Agent (affectedWorkIds 为空): 若有在跑的子 Agent, 下发前必须先补全影响面。`;
|
|
610
|
+
return { workIds, criteriaVersion: v, directive, mustAckBeforeNextStep: true, rewritesHistory: false };
|
|
611
|
+
}
|
|
612
|
+
// ============================================================================
|
|
613
|
+
// §9. 把变更接进**正在运行的 Goal** (M4 接线用; 纯函数, 不读钟/不读盘)
|
|
614
|
+
// ============================================================================
|
|
615
|
+
//
|
|
616
|
+
// 上面 §7 的 `applyChange` 只回答"这条要求排不排得进下一 Run"。但用户是在**目标跑着的时候**
|
|
617
|
+
// 提要求的, 于是还差三个决定, 它们都属于同一个问题「变更对**正在跑的**这一轮意味着什么」:
|
|
618
|
+
//
|
|
619
|
+
// ① 在跑的 Run 怎么办? → `planChangeInjection().runBoundary`
|
|
620
|
+
// ② 在跑的子 Agent 怎么办? → `planChangeInjection().childDirectives` (规则 5)
|
|
621
|
+
// ③ 这两件事的判定依据是什么? → 全部写死成规则, 不靠模型也不靠调用方记得
|
|
622
|
+
//
|
|
623
|
+
// 判定纪律 (接缝照此执行, 不另立一套):
|
|
624
|
+
// · **只有撤销会打断在跑的 Run**。其余变更 (判据/预算/权限/范围/优先级/补充说明) 一律
|
|
625
|
+
// `let_finish` —— 让这一轮跑完, 新要求从下一个 Run 起生效 (规则 4: 当前 Run 的历史
|
|
626
|
+
// 不可被新要求改写; 中途换判据 = 把已发生的事实按新尺子重判, 正是要防的事)。
|
|
627
|
+
// · **没生效的变更不许影响在跑的 Run**: `pending_approval` / `rejected` 都 `let_finish` ——
|
|
628
|
+
// 一条还没批的要求若把在跑的 Run 停下来, 等于 Agent 用自己的"待批准"替人按了暂停键。
|
|
629
|
+
// · **拿不到事实就说拿不到** (`factsMissing`): 不许把"宿主没告诉我"当成"没有在跑的 Run"。
|
|
630
|
+
/** 在跑的 Run 状态 (只有这些会继续消耗预算/继续执行; 与执行器的运行态同域) */
|
|
631
|
+
export const IN_FLIGHT_RUN_STATUSES = ['queued', 'running', 'recovering'];
|
|
632
|
+
/** 这个 Run 状态是否还在跑 (未知取值一律 false: 不认识的态不当"在跑") */
|
|
633
|
+
export function isRunInFlight(status) {
|
|
634
|
+
return IN_FLIGHT_RUN_STATUSES.includes(String(status ?? ''));
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* 逐 workId 的下发内容 (规则 5)。
|
|
638
|
+
*
|
|
639
|
+
* 为什么每个子单独一份而不是一份带全部 workId: 下发是**逐个**发生的事实 ——
|
|
640
|
+
* 一份合并的指令没法回答"3 号子 Agent 到底确认了没有", 于是 ack 变成集体背书。
|
|
641
|
+
*/
|
|
642
|
+
export function childrenForChange(r, liveWorkIds, applied) {
|
|
643
|
+
const ids = [...new Set((liveWorkIds ?? []).map((x) => String(x ?? '').trim()).filter(Boolean))];
|
|
644
|
+
return ids.map((id) => {
|
|
645
|
+
const scoped = scopeChangeToWork(r, [id]);
|
|
646
|
+
return childChangeDirective(scoped, applied);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* 这句话是不是**只有人**能提的变更 (撤销 / 缩范围)? 返回命中的类别, 否则 null。
|
|
651
|
+
*
|
|
652
|
+
* 判据是**真实分诊** (`detectChangeIntents`), 不是关键词子串 —— 前者能认出
|
|
653
|
+
* "别继续做了 / abort this / 只做第一条", 后者只认得字面那几个词。
|
|
654
|
+
*/
|
|
655
|
+
export function userOnlyChangeKind(instruction) {
|
|
656
|
+
const intents = detectChangeIntents(instruction);
|
|
657
|
+
return intents.find((k) => k === 'abort' || k === 'scope_reduction') ?? null;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* 「变更对**正在跑的这一轮**意味着什么」—— 唯一的判定处。
|
|
661
|
+
*
|
|
662
|
+
* @param runningRun `{ runId, status }` = 宿主读到的事实; `null` = 明确没有在跑的 Run;
|
|
663
|
+
* `undefined` = 宿主不知道 (会如实标 `factsMissing`, 不猜)
|
|
664
|
+
*/
|
|
665
|
+
export function planChangeInjection(input) {
|
|
666
|
+
const { request: r, application: a } = input;
|
|
667
|
+
const liveWorkIds = [...new Set((input.liveWorkIds ?? []).map((x) => String(x ?? '').trim()).filter(Boolean))];
|
|
668
|
+
const factsMissing = input.runningRun === undefined;
|
|
669
|
+
const run = input.runningRun ?? null;
|
|
670
|
+
const inFlight = !!run && isRunInFlight(run.status);
|
|
671
|
+
const childDirectives = childrenForChange(r, liveWorkIds, { criteriaVersion: a.criteriaVersion });
|
|
672
|
+
const childScopeMissing = liveWorkIds.length > 0 && (r.impact?.affectedWorkIds?.length ?? 0) === 0;
|
|
673
|
+
let runBoundary;
|
|
674
|
+
if (factsMissing) {
|
|
675
|
+
runBoundary = {
|
|
676
|
+
action: 'let_finish',
|
|
677
|
+
runStatus: null,
|
|
678
|
+
runId: null,
|
|
679
|
+
factsMissing: true,
|
|
680
|
+
reason: '宿主没有提供"在跑的 Run"的事实 → 不断言它停或不停 (接缝不猜; 要拦住在跑的 Run 必须先把这份事实注入进来)',
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
else if (!inFlight) {
|
|
684
|
+
runBoundary = {
|
|
685
|
+
action: 'let_finish',
|
|
686
|
+
runStatus: null,
|
|
687
|
+
runId: run ? run.runId : null,
|
|
688
|
+
factsMissing: false,
|
|
689
|
+
reason: run
|
|
690
|
+
? `当前 Run ${run.runId} 状态=${run.status} (不在跑) → 没有需要打断的执行`
|
|
691
|
+
: '当前没有在跑的 Run → 变更只影响后续 Run',
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
else if (a.outcome === 'next_run' && r.kind === 'abort') {
|
|
695
|
+
runBoundary = {
|
|
696
|
+
action: 'stop_running_run',
|
|
697
|
+
runStatus: 'aborted',
|
|
698
|
+
runId: run.runId,
|
|
699
|
+
factsMissing: false,
|
|
700
|
+
reason: `[${RULES.revocation}] 用户明确撤销: 在跑的 Run ${run.runId} 必须停 (写 'aborted' 后执行器下一轮自行停下; `
|
|
701
|
+
+ '已发生的步骤/证据不改写 —— 停的是"继续", 不是"历史")',
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
else if (a.outcome !== 'next_run') {
|
|
705
|
+
runBoundary = {
|
|
706
|
+
action: 'let_finish',
|
|
707
|
+
runStatus: null,
|
|
708
|
+
runId: run.runId,
|
|
709
|
+
factsMissing: false,
|
|
710
|
+
reason: `变更 outcome=${a.outcome} (还没生效) → 不许用它打断在跑的 Run ${run.runId}`
|
|
711
|
+
+ ' (一条还没批准的要求把运行停下 = Agent 借"待批准"替人按暂停键)',
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
runBoundary = {
|
|
716
|
+
action: 'let_finish',
|
|
717
|
+
runStatus: null,
|
|
718
|
+
runId: run.runId,
|
|
719
|
+
factsMissing: false,
|
|
720
|
+
reason: `[${RULES.history}] 规则 4: 新要求只影响后续 Run —— 让在跑的 Run ${run.runId} 跑完`
|
|
721
|
+
+ ' (它的历史/证据不被改写), 新要求从下一个 Run 起生效',
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
changeId: r.changeId,
|
|
726
|
+
goalId: r.goalId,
|
|
727
|
+
kind: r.kind,
|
|
728
|
+
outcome: a.outcome,
|
|
729
|
+
criteriaVersion: a.criteriaVersion,
|
|
730
|
+
appliesToFutureRunsOnly: true,
|
|
731
|
+
runBoundary,
|
|
732
|
+
childDirectives,
|
|
733
|
+
childScopeMissing,
|
|
734
|
+
liveWorkIds,
|
|
735
|
+
nextRunDirective: a.nextRunDirective,
|
|
736
|
+
steps: changeFlowSteps({ request: r, application: a, liveWorkIds, childScopeMissing, runBoundary }),
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
/** 设计稿 §9 的流程逐条留痕 (接收→记录→判影响→摘要→评估当前 Run→必要时停→重规划→写入下一 Run) */
|
|
740
|
+
function changeFlowSteps(input) {
|
|
741
|
+
const { request: r, application: a, runBoundary } = input;
|
|
742
|
+
const impact = impactLabel(r.impact);
|
|
743
|
+
return [
|
|
744
|
+
`① 接收: 来源=${r.source} (原始原话逐字入档, ${r.instruction.length} 字)`,
|
|
745
|
+
`② 记录: ${r.changeId} 状态=${r.status} 优先级=${r.priority}`,
|
|
746
|
+
`③ 判影响面: ${impact}${a.requiresReplan ? ' → 需重规划' : ' → 不需重规划'}`,
|
|
747
|
+
`④ 生成变更摘要: ${r.interpreted ? '已生成 (interpreted 非空)' : '未生成 (未分诊 → 不许生效)'}`,
|
|
748
|
+
`⑤ 评估当前 Run: ${runBoundary.action === 'stop_running_run' ? `必须停 (${runBoundary.runId})` : '继续跑完 (不打断)'}`,
|
|
749
|
+
`⑥ 必要时暂停: ${runBoundary.action === 'stop_running_run' ? `写 Run 记录状态=${runBoundary.runStatus}` : '本轮不需要暂停动作'}`,
|
|
750
|
+
`⑦ 重规划: ${a.requiresReplan ? '下一个 Run 起重新规划' : '不需要 (不动计划)'}`,
|
|
751
|
+
`⑧ 写入下一 Run: outcome=${a.outcome}, criteriaVersion=v${a.criteriaVersion}; 在跑子 Agent ${input.liveWorkIds.length} 个`
|
|
752
|
+
+ (input.childScopeMissing ? ' (影响面未点明 → 下发前必须补全)' : ''),
|
|
753
|
+
];
|
|
754
|
+
}
|