@bolloon/bolloon-agent 0.4.32 → 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/chain/explorer.js +52 -0
- package/dist/agents/chain/index.js +2 -0
- package/dist/agents/chain/onchain-trade.js +15 -1
- 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 +525 -27
- 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-onchain-runner.js +19 -1
- package/dist/agents/task/task-runner.js +36 -6
- package/dist/agents/task-board.js +702 -0
- package/dist/agents/task-group.js +524 -0
- package/dist/agents/task-public-text.js +89 -0
- package/dist/cli/commands/index.js +3 -0
- package/dist/cli/commands/tasks.js +977 -8
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/protocol-envelope.js +4 -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/chain/explorer.js +59 -0
- package/dist/electron-build/agents/chain/explorer.js.map +1 -0
- 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/local-signer.js +101 -0
- package/dist/electron-build/agents/local-signer.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +531 -28
- 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 +35 -5
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/task-board.js +761 -0
- package/dist/electron-build/agents/task-board.js.map +1 -0
- package/dist/electron-build/agents/task-contract.js +382 -0
- package/dist/electron-build/agents/task-contract.js.map +1 -0
- 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,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goal-flywheel/work-contract.ts — P2 「子 Agent 工作合同」(独立模块, **未接线**)
|
|
3
|
+
*
|
|
4
|
+
* 解决的问题 (设计 §7 / §14 P2): 现在子 Agent "回一段文本也能算数"。
|
|
5
|
+
* 这一层给出**唯一**的派遣前契约 (`AgentWorkContract`) 与**结构化回报**的核验,
|
|
6
|
+
* 让父能明确回答三件事:
|
|
7
|
+
* ① 这份合同签得合不合法 —— 缺目标 / 缺判据 / deadline 早于 now / 金额没币种 → **拒签** (抛错)
|
|
8
|
+
* ② 这份回报够不够格 —— 缺字段 / 缺判据证据 / 越界 (`ChildProhibition`)
|
|
9
|
+
* ③ 能不能算「完成」 —— 只有: 状态 `completed` + 报告完整 + 逐条判据有证据 + 没越界 + 没超期
|
|
10
|
+
*
|
|
11
|
+
* 边界 (刻意不做, 与设计 §13 一致):
|
|
12
|
+
* - **不做 I/O, 不读真实钟**: 时间一律由 `now` / `deadline` / `reportedAt` 注入;
|
|
13
|
+
* 本文件不 import `fs` / `node:*` / 任何阶段的实现文件, 只 import `./types.js` 的类型与枚举。
|
|
14
|
+
* - **不改现有调用方**: `execution-supervisor.ts` / `goal-store.ts` / `pi-sdk.ts` / watchdog 一律没碰。
|
|
15
|
+
* 派遣时签合同、收到回报时核验 —— 那是接线 (单一所有者) 后续的事。
|
|
16
|
+
* - **不做监控**: "多久没心跳 / 要不要接管" 是 P3 (`work-monitor.ts`) 的事;
|
|
17
|
+
* 本文件只核验报告内容, 不判断时间流逝 (因此 `acceptsAsComplete` 没有 `now` 参数)。
|
|
18
|
+
*
|
|
19
|
+
* 回报协议 (`bolloon-work-report/1`, 即合同里的 `reportSchema`):
|
|
20
|
+
* - 每条 `successCriteria` 都要有**两件**东西:
|
|
21
|
+
* ① 一条证据: `WorkEvidence.kind` 或 `.ref` === `criterion:<判据原文>`
|
|
22
|
+
* ② 一条检查: `WorkCheck.name` === `criterion:<判据原文>` 且 `verdict === 'pass'`
|
|
23
|
+
* - `issueWorkContract` 把这条约定**写进合同的 `requiredEvidence`**, 子不需要猜。
|
|
24
|
+
* - 对自称 `completed` 的报告: 上述任意一条缺失 → 判越界 `mark_unverified_as_complete`
|
|
25
|
+
* (设计 §10 强负例 7: 漂亮但无证据的结果 → 父 Goal **不完成**)。
|
|
26
|
+
* - 非 `completed` 的报告 (`partial` / `blocked` / `failed` / `cancelled`) **允许**判据未满足
|
|
27
|
+
* (被卡住本来就拿不到证据, 硬要证据只会逼出假证据), 但仍然必须带**至少一条**证据
|
|
28
|
+
* (禁"只回一段文本"), 且 `blocked` 必须给结构化 `BlockRecord` (禁写成一句"有问题")。
|
|
29
|
+
*
|
|
30
|
+
* 无法从回报核验的两条禁则 (`mutate_parent_goal_state` / `spawn_unbounded_subtasks`):
|
|
31
|
+
* 回报里**没有对应字段**, 本文件**不假装**能查 —— 见 `NOT_CHECKABLE_FROM_REPORT`,
|
|
32
|
+
* 由监控层 / 接线层拦。
|
|
33
|
+
*/
|
|
34
|
+
import { CHILD_PROHIBITIONS, CHECK_VERDICTS, WORK_REPORT_STATUSES, } from './types.js';
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// §0. 常量与默认值 (合同里没有入参的字段, 默认值在这里一份写死)
|
|
37
|
+
// ============================================================================
|
|
38
|
+
/** 回报协议名 (写进合同的 `reportSchema`) */
|
|
39
|
+
export const WORK_REPORT_SCHEMA = 'bolloon-work-report/1';
|
|
40
|
+
/** 判据引用前缀: `criterion:<判据原文>` —— 证据与检查都按这个名字对齐 */
|
|
41
|
+
export const CRITERION_REF_PREFIX = 'criterion:';
|
|
42
|
+
/** 心跳默认间隔 (没有预算/期限约束时) */
|
|
43
|
+
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
44
|
+
/** 心跳下限 (再短就是噪音) */
|
|
45
|
+
export const MIN_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
46
|
+
/** 心跳上限 (再长就等于没有心跳) */
|
|
47
|
+
export const MAX_HEARTBEAT_INTERVAL_MS = 60_000;
|
|
48
|
+
/** 判据引用名 (证据与检查共用) */
|
|
49
|
+
export function criterionRef(criterion) {
|
|
50
|
+
return `${CRITERION_REF_PREFIX}${criterion}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 默认失败处置 (设计 §7 职责划分 + §8 处理规则):
|
|
54
|
+
* - 心跳没了: 先 `stall` (标停), **不是**自动接管 —— 接管要先查 lease (P3)
|
|
55
|
+
* - 预算用尽: `stop_and_report` (硬底线, 不许自行扩预算)
|
|
56
|
+
* - 工具被拒: `report` (强调**不自动绕过 Harness**)
|
|
57
|
+
* - 反复失败: `escalate` (升级给父, 不许自己重试到天荒地老)
|
|
58
|
+
*/
|
|
59
|
+
export const DEFAULT_FAILURE_POLICY = Object.freeze({
|
|
60
|
+
onHeartbeatMiss: 'stall',
|
|
61
|
+
onBudgetExhausted: 'stop_and_report',
|
|
62
|
+
onToolDenied: 'report',
|
|
63
|
+
onRepeatedFailure: 'escalate',
|
|
64
|
+
});
|
|
65
|
+
/** 默认取消策略: 宽限收尾 + 保留工件 (取消不该销毁已产出的东西) */
|
|
66
|
+
export const DEFAULT_CANCEL_POLICY = Object.freeze({
|
|
67
|
+
onParentCancel: 'graceful',
|
|
68
|
+
graceMs: 3_000,
|
|
69
|
+
preserveArtifacts: true,
|
|
70
|
+
onParentGoalClosed: 'finish_current_step',
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* 能从**回报本身**核验的禁则 (本文件真的实现并测了)。
|
|
74
|
+
* 固定优先级 = `CHILD_PROHIBITIONS` 的声明顺序 (同一份报告只回第一条, 结果可复现)。
|
|
75
|
+
*/
|
|
76
|
+
export const REPORT_CHECKABLE_PROHIBITIONS = [
|
|
77
|
+
'expand_own_budget',
|
|
78
|
+
'mark_unverified_as_complete',
|
|
79
|
+
'rewrite_success_criteria',
|
|
80
|
+
];
|
|
81
|
+
/**
|
|
82
|
+
* 回报里**没有字段**可供核验的禁则 —— 本文件不假装能查, 由监控层 / 接线层拦。
|
|
83
|
+
* (`AgentWorkReport` 里既没有"父 Goal 状态的镜像", 也没有"派生了几条子任务"的记录。)
|
|
84
|
+
*/
|
|
85
|
+
export const NOT_CHECKABLE_FROM_REPORT = [
|
|
86
|
+
'mutate_parent_goal_state',
|
|
87
|
+
'spawn_unbounded_subtasks',
|
|
88
|
+
];
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// §1. 工具 (纯函数: 无 I/O, 无全局状态, 不读真实钟)
|
|
91
|
+
// ============================================================================
|
|
92
|
+
/** 拒签合同的错误 —— 带上**结构化**原因清单 (不许只给一句人话) */
|
|
93
|
+
export class WorkContractError extends Error {
|
|
94
|
+
issues;
|
|
95
|
+
constructor(issues) {
|
|
96
|
+
super(`拒绝签发工作合同: ${issues.join('; ')}`);
|
|
97
|
+
this.name = 'WorkContractError';
|
|
98
|
+
this.issues = issues;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function isNonEmptyString(v) {
|
|
102
|
+
return typeof v === 'string' && v.trim().length > 0;
|
|
103
|
+
}
|
|
104
|
+
function isFiniteNumber(v) {
|
|
105
|
+
return typeof v === 'number' && Number.isFinite(v);
|
|
106
|
+
}
|
|
107
|
+
function isPlainObject(v) {
|
|
108
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
109
|
+
}
|
|
110
|
+
/** ISO 时间戳宽松判定: 必须是 `YYYY-MM-DD…` 且 `Date.parse` 认得出 (宽松是为兼容全仓既有写法) */
|
|
111
|
+
function isIsoLike(v) {
|
|
112
|
+
return typeof v === 'string' && /^\d{4}-\d{2}-\d{2}/.test(v) && !Number.isNaN(Date.parse(v));
|
|
113
|
+
}
|
|
114
|
+
function isWorkReportStatus(v) {
|
|
115
|
+
return WORK_REPORT_STATUSES.includes(v);
|
|
116
|
+
}
|
|
117
|
+
/** 去重但保留首次出现顺序 */
|
|
118
|
+
function dedupe(values) {
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const v of values) {
|
|
122
|
+
if (seen.has(v))
|
|
123
|
+
continue;
|
|
124
|
+
seen.add(v);
|
|
125
|
+
out.push(v);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/** FNV-1a 32 位 (两次不同种子拼成 16 位十六进制) —— 让 workId 可复现, 不引入随机数/时钟依赖 */
|
|
130
|
+
function fnv1a32(input, seed) {
|
|
131
|
+
let h = seed >>> 0;
|
|
132
|
+
for (let i = 0; i < input.length; i++) {
|
|
133
|
+
h ^= input.charCodeAt(i);
|
|
134
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
135
|
+
}
|
|
136
|
+
return h.toString(16).padStart(8, '0');
|
|
137
|
+
}
|
|
138
|
+
/** 稳定哈希 (同一输入 → 同一结果; 不读时钟, 不用随机数) */
|
|
139
|
+
export function stableHash(input) {
|
|
140
|
+
return fnv1a32(input, 0x811c9dc5) + fnv1a32(`${input}\u0000#2`, 0x01000193);
|
|
141
|
+
}
|
|
142
|
+
/** 深冻结 (合同是只读的) */
|
|
143
|
+
function deepFreeze(value) {
|
|
144
|
+
if (value !== null && typeof value === 'object') {
|
|
145
|
+
for (const key of Object.keys(value)) {
|
|
146
|
+
deepFreeze(value[key]);
|
|
147
|
+
}
|
|
148
|
+
Object.freeze(value);
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 拷贝 `inputs` 再冻结 —— **不动调用方的对象** (只冻结我们自己的副本)。
|
|
154
|
+
* `structuredClone` 拿不到的类型 (函数/Promise 等) 退化为浅拷贝, 但绝不抛。
|
|
155
|
+
*/
|
|
156
|
+
function cloneInputs(inputs) {
|
|
157
|
+
try {
|
|
158
|
+
return structuredClone(inputs);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return { ...inputs };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 心跳间隔推导 (合同里没有这个入参, 只能推):
|
|
166
|
+
* 取 min(默认 30s, 预算时长/6, 到 deadline 剩余/3), 再夹到 [5s, 60s]。
|
|
167
|
+
* 理由: 心跳至少要能在"预算用尽 / 超期"之前发出几次, 长预算也不许把心跳拉长到没意义。
|
|
168
|
+
*/
|
|
169
|
+
function deriveHeartbeatIntervalMs(budget, deadline, now) {
|
|
170
|
+
const candidates = [DEFAULT_HEARTBEAT_INTERVAL_MS];
|
|
171
|
+
if (isFiniteNumber(budget?.maxDurationMs) && budget.maxDurationMs > 0) {
|
|
172
|
+
candidates.push(Math.floor(budget.maxDurationMs / 6));
|
|
173
|
+
}
|
|
174
|
+
if (deadline !== null && isIsoLike(deadline) && isIsoLike(now)) {
|
|
175
|
+
const remaining = Date.parse(deadline) - Date.parse(now);
|
|
176
|
+
if (remaining > 0)
|
|
177
|
+
candidates.push(Math.floor(remaining / 3));
|
|
178
|
+
}
|
|
179
|
+
const raw = Math.min(...candidates);
|
|
180
|
+
return Math.min(Math.max(raw, MIN_HEARTBEAT_INTERVAL_MS), MAX_HEARTBEAT_INTERVAL_MS);
|
|
181
|
+
}
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// §2. 签发合同 (issueWorkContract)
|
|
184
|
+
// ============================================================================
|
|
185
|
+
/**
|
|
186
|
+
* 签发子 Agent 工作合同 (设计 §14 冻结签名)。
|
|
187
|
+
*
|
|
188
|
+
* 合同不合法就**不签** —— 抛 `WorkContractError` (带 `issues` 结构化原因), 不做"差不多就行"的宽容。
|
|
189
|
+
* 返回的对象是**深冻结**的: 子拿到的合同不能被就地改写 (失败/取消策略、判据、预算都不可改)。
|
|
190
|
+
*
|
|
191
|
+
* `workId` 由 (goalId, parentRunId, childAgentId, capability, objective, now) 稳定哈希得出 ——
|
|
192
|
+
* 同输入同 id (可复现、可幂等), 不读时钟也不用随机数。
|
|
193
|
+
*/
|
|
194
|
+
export function issueWorkContract(input) {
|
|
195
|
+
const issues = [];
|
|
196
|
+
const named = [
|
|
197
|
+
['goalId', input.goalId],
|
|
198
|
+
['parentRunId', input.parentRunId],
|
|
199
|
+
['childAgentId', input.childAgentId],
|
|
200
|
+
['capability', input.capability],
|
|
201
|
+
['objective', input.objective],
|
|
202
|
+
['issuedBy', input.issuedBy],
|
|
203
|
+
];
|
|
204
|
+
for (const [name, value] of named) {
|
|
205
|
+
if (!isNonEmptyString(value))
|
|
206
|
+
issues.push(`${name} 为空/缺失 (合同必须有明确的 ${name})`);
|
|
207
|
+
}
|
|
208
|
+
if (!isIsoLike(input.now))
|
|
209
|
+
issues.push('now 不是合法 ISO 时间戳');
|
|
210
|
+
if (input.deadline !== null) {
|
|
211
|
+
if (!isIsoLike(input.deadline)) {
|
|
212
|
+
issues.push('deadline 不是合法 ISO 时间戳 (null = 不设期限)');
|
|
213
|
+
}
|
|
214
|
+
else if (isIsoLike(input.now) && Date.parse(input.deadline) < Date.parse(input.now)) {
|
|
215
|
+
issues.push('deadline 早于签发时间 now (还没开始就超期的合同不许签)');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!isPlainObject(input.inputs))
|
|
219
|
+
issues.push('inputs 必须是结构化对象 (Record<string, unknown>)');
|
|
220
|
+
const rawCriteria = Array.isArray(input.successCriteria) ? input.successCriteria : [];
|
|
221
|
+
const criteriaTrimmed = rawCriteria.map((c) => (typeof c === 'string' ? c.trim() : ''));
|
|
222
|
+
if (criteriaTrimmed.some((c) => c.length === 0))
|
|
223
|
+
issues.push('successCriteria 含空条目');
|
|
224
|
+
const successCriteria = dedupe(criteriaTrimmed.filter((c) => c.length > 0));
|
|
225
|
+
if (successCriteria.length === 0) {
|
|
226
|
+
issues.push('successCriteria 为空 (没有成功判据 = 没有"完成"的定义, 这种合同一定会扯皮)');
|
|
227
|
+
}
|
|
228
|
+
const rawTools = Array.isArray(input.allowedTools) ? input.allowedTools : [];
|
|
229
|
+
const toolsTrimmed = rawTools.map((t) => (typeof t === 'string' ? t.trim() : ''));
|
|
230
|
+
if (toolsTrimmed.some((t) => t.length === 0))
|
|
231
|
+
issues.push('allowedTools 含空条目');
|
|
232
|
+
// 空数组是合法的: "无工具授权" (只能基于 inputs 推理) —— 但不许出现空字符串这种脏数据
|
|
233
|
+
const allowedTools = dedupe(toolsTrimmed.filter((t) => t.length > 0));
|
|
234
|
+
const budget = input.budget;
|
|
235
|
+
if (!isPlainObject(budget)) {
|
|
236
|
+
issues.push('budget 缺失 (子不许自行扩预算, 所以父必须把上限写清)');
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
for (const field of ['maxSteps', 'maxDurationMs', 'maxAmount']) {
|
|
240
|
+
const v = budget[field];
|
|
241
|
+
if (v === null || v === undefined)
|
|
242
|
+
continue;
|
|
243
|
+
if (!isFiniteNumber(v) || v <= 0) {
|
|
244
|
+
issues.push(`budget.${field} 必须是 null 或正数 (现在: ${String(v)})`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (field === 'maxSteps' && !Number.isInteger(v))
|
|
248
|
+
issues.push('budget.maxSteps 必须是整数');
|
|
249
|
+
}
|
|
250
|
+
if (budget.maxAmount !== null && budget.maxAmount !== undefined && !isNonEmptyString(budget.currency)) {
|
|
251
|
+
issues.push('budget.maxAmount 有值但 budget.currency 为空 (金额没有币种 = 无法核验)');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (issues.length > 0)
|
|
255
|
+
throw new WorkContractError(issues);
|
|
256
|
+
const workId = `work-${stableHash([input.goalId, input.parentRunId, input.childAgentId, input.capability, input.objective, input.now].join('\u0000'))}`;
|
|
257
|
+
const contract = {
|
|
258
|
+
workId,
|
|
259
|
+
goalId: input.goalId,
|
|
260
|
+
parentRunId: input.parentRunId,
|
|
261
|
+
childAgentId: input.childAgentId,
|
|
262
|
+
capability: input.capability,
|
|
263
|
+
objective: input.objective,
|
|
264
|
+
inputs: cloneInputs(input.inputs),
|
|
265
|
+
allowedTools,
|
|
266
|
+
budget: {
|
|
267
|
+
maxSteps: budget.maxSteps ?? null,
|
|
268
|
+
maxDurationMs: budget.maxDurationMs ?? null,
|
|
269
|
+
maxAmount: budget.maxAmount ?? null,
|
|
270
|
+
currency: budget.currency ?? null,
|
|
271
|
+
},
|
|
272
|
+
deadline: input.deadline,
|
|
273
|
+
successCriteria,
|
|
274
|
+
reportSchema: WORK_REPORT_SCHEMA,
|
|
275
|
+
heartbeatIntervalMs: deriveHeartbeatIntervalMs(budget, input.deadline, input.now),
|
|
276
|
+
failurePolicy: { ...DEFAULT_FAILURE_POLICY },
|
|
277
|
+
cancelPolicy: { ...DEFAULT_CANCEL_POLICY },
|
|
278
|
+
// 约定写进合同: 每条判据一条证据 (子不需要猜要交什么)
|
|
279
|
+
requiredEvidence: successCriteria.map((c) => criterionRef(c)),
|
|
280
|
+
issuedAt: input.now,
|
|
281
|
+
issuedBy: input.issuedBy,
|
|
282
|
+
};
|
|
283
|
+
return deepFreeze(contract);
|
|
284
|
+
}
|
|
285
|
+
// ============================================================================
|
|
286
|
+
// §3. 核验回报 (validateChildReport)
|
|
287
|
+
// ============================================================================
|
|
288
|
+
/**
|
|
289
|
+
* `BlockRecord` 在回报里必须自带这些事实 (缺一项 = 说不清卡在哪, 设计 §8):
|
|
290
|
+
* 必带键 + 必须非空的键。`note` 也算"说清", 空说明等于没报 (禁"有问题"式回报)。
|
|
291
|
+
*/
|
|
292
|
+
const BLOCK_RECORD_KEYS = [
|
|
293
|
+
'kind',
|
|
294
|
+
'blockedAt',
|
|
295
|
+
'lastProgressAt',
|
|
296
|
+
'owner',
|
|
297
|
+
'dependency',
|
|
298
|
+
'suggestedAction',
|
|
299
|
+
'note',
|
|
300
|
+
];
|
|
301
|
+
/** 阻塞记录是否说得清 (返回问题清单; 空 = 合格) */
|
|
302
|
+
function blockRecordIssues(block) {
|
|
303
|
+
if (!isPlainObject(block))
|
|
304
|
+
return ['blockReason 不是结构化对象 (不许写成一句"有问题")'];
|
|
305
|
+
const issues = [];
|
|
306
|
+
for (const key of BLOCK_RECORD_KEYS) {
|
|
307
|
+
if (!(key in block))
|
|
308
|
+
issues.push(`blockReason 缺字段 ${key}`);
|
|
309
|
+
}
|
|
310
|
+
for (const key of ['kind', 'blockedAt', 'lastProgressAt', 'owner', 'suggestedAction', 'note']) {
|
|
311
|
+
if (key in block && !isNonEmptyString(block[key]))
|
|
312
|
+
issues.push(`blockReason.${key} 为空`);
|
|
313
|
+
}
|
|
314
|
+
return issues;
|
|
315
|
+
}
|
|
316
|
+
/** 报告里的检查是否满足某条判据 (`criterion:<原文>` 且 verdict='pass') */
|
|
317
|
+
function verdictOfCriterion(report, criterion) {
|
|
318
|
+
const checks = Array.isArray(report?.checks) ? report.checks : [];
|
|
319
|
+
const hit = checks.find((c) => isPlainObject(c) && c.name === criterionRef(criterion));
|
|
320
|
+
if (!hit || !isPlainObject(hit))
|
|
321
|
+
return null;
|
|
322
|
+
const verdict = hit.verdict;
|
|
323
|
+
return CHECK_VERDICTS.includes(verdict) ? verdict : null;
|
|
324
|
+
}
|
|
325
|
+
/** 某条证据是否引用了合同要求的证据名 (kind 或 ref 任一对上 —— 精确匹配, 不许模糊) */
|
|
326
|
+
function citesRequirement(evidence, requirement) {
|
|
327
|
+
if (!isPlainObject(evidence))
|
|
328
|
+
return false;
|
|
329
|
+
const e = evidence;
|
|
330
|
+
return e.kind === requirement || e.ref === requirement;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 核验子 Agent 回报 (设计 §14 冻结签名)。
|
|
334
|
+
*
|
|
335
|
+
* 三个桶都**返回原因**, 不只是 boolean:
|
|
336
|
+
* - `missingFields` —— 字段级不合格: 缺失 / 空 / 格式错 / **与合同不一致**
|
|
337
|
+
* (例如回报里的 `workId` 不是本合同签发的那个)。`evidence` 为空数组、`checks` 为空数组、
|
|
338
|
+
* `status='blocked'` 却没给结构化 `blockReason` 都算这里。
|
|
339
|
+
* - `missingEvidence` —— **自称 `completed`** 时, 合同的 `requiredEvidence` 里查不到对应证据的条目。
|
|
340
|
+
* (非完成状态本来就允许判据未满足 → 这一桶为空, 但"至少一条证据"仍由 `missingFields` 兜着。)
|
|
341
|
+
* - `violation` —— 越界, 取值见 `REPORT_CHECKABLE_PROHIBITIONS`; 多条命中时按
|
|
342
|
+
* `CHILD_PROHIBITIONS` 声明顺序取第一条 (结果可复现)。
|
|
343
|
+
*
|
|
344
|
+
* 本函数**不改**合同与回报 (测试里用深冻结输入钉住这一点)。
|
|
345
|
+
*/
|
|
346
|
+
export function validateChildReport(contract, report) {
|
|
347
|
+
// 报告可能根本不是它自称的那个形状 (子只回了文本 / 少字段), 所以按 unknown 读
|
|
348
|
+
const r = (report ?? {});
|
|
349
|
+
const missingFields = [];
|
|
350
|
+
if (!isNonEmptyString(r.workId) || r.workId !== contract.workId)
|
|
351
|
+
missingFields.push('workId');
|
|
352
|
+
if (!isNonEmptyString(r.childAgentId) || r.childAgentId !== contract.childAgentId)
|
|
353
|
+
missingFields.push('childAgentId');
|
|
354
|
+
if (!isWorkReportStatus(r.status))
|
|
355
|
+
missingFields.push('status');
|
|
356
|
+
if (!isNonEmptyString(r.summary))
|
|
357
|
+
missingFields.push('summary');
|
|
358
|
+
if (!Array.isArray(r.evidence) || r.evidence.length === 0)
|
|
359
|
+
missingFields.push('evidence');
|
|
360
|
+
if (!Array.isArray(r.artifacts))
|
|
361
|
+
missingFields.push('artifacts');
|
|
362
|
+
if (!Array.isArray(r.checks) || r.checks.length === 0)
|
|
363
|
+
missingFields.push('checks');
|
|
364
|
+
if (!Array.isArray(r.unresolvedItems))
|
|
365
|
+
missingFields.push('unresolvedItems');
|
|
366
|
+
if (!isNonEmptyString(r.nextRecommendation))
|
|
367
|
+
missingFields.push('nextRecommendation');
|
|
368
|
+
if (!isFiniteNumber(r.durationMs) || r.durationMs < 0)
|
|
369
|
+
missingFields.push('durationMs');
|
|
370
|
+
if (!isIsoLike(r.reportedAt))
|
|
371
|
+
missingFields.push('reportedAt');
|
|
372
|
+
// 被阻塞 → 必须有能说清卡在哪的阻塞记录 (null 或结构不全都算不合格)
|
|
373
|
+
if (r.status === 'blocked' && !isPlainObject(r.blockReason)) {
|
|
374
|
+
missingFields.push('blockReason');
|
|
375
|
+
}
|
|
376
|
+
else if (r.blockReason !== null && r.blockReason !== undefined && blockRecordIssues(r.blockReason).length > 0) {
|
|
377
|
+
missingFields.push('blockReason');
|
|
378
|
+
}
|
|
379
|
+
// 判据证据: 只对"自称完成"的报告要求 (非完成状态不许逼出自证)
|
|
380
|
+
const missingEvidence = [];
|
|
381
|
+
if (r.status === 'completed') {
|
|
382
|
+
const evidence = Array.isArray(r.evidence) ? r.evidence : [];
|
|
383
|
+
for (const requirement of contract.requiredEvidence) {
|
|
384
|
+
if (!evidence.some((e) => citesRequirement(e, requirement)))
|
|
385
|
+
missingEvidence.push(requirement);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const violation = detectViolation(contract, report, missingEvidence);
|
|
389
|
+
const ok = missingFields.length === 0 && missingEvidence.length === 0 && violation === null;
|
|
390
|
+
return { ok, missingFields, missingEvidence, violation };
|
|
391
|
+
}
|
|
392
|
+
/** 越界判定 (纯函数; 固定优先级见 `REPORT_CHECKABLE_PROHIBITIONS`) */
|
|
393
|
+
function detectViolation(contract, report, missingEvidence) {
|
|
394
|
+
const r = (report ?? {});
|
|
395
|
+
// ① 超预算: 报告里唯一能核验的预算是耗时 (maxSteps / maxAmount 回报里没有对应字段 → 不假装能查)
|
|
396
|
+
const maxDurationMs = contract.budget?.maxDurationMs ?? null;
|
|
397
|
+
const budgetOverrun = maxDurationMs !== null && isFiniteNumber(r.durationMs) && r.durationMs > maxDurationMs;
|
|
398
|
+
// ② 把未验证的结果标完成
|
|
399
|
+
const evidenceEmpty = !Array.isArray(r.evidence) || r.evidence.length === 0;
|
|
400
|
+
const unresolvedLeft = Array.isArray(r.unresolvedItems) && r.unresolvedItems.length > 0;
|
|
401
|
+
const criteriaWithoutPass = contract.successCriteria.filter((c) => verdictOfCriterion(report, c) !== 'pass');
|
|
402
|
+
const unverifiedComplete = r.status === 'completed' &&
|
|
403
|
+
(evidenceEmpty || missingEvidence.length > 0 || criteriaWithoutPass.length > 0 || unresolvedLeft);
|
|
404
|
+
// ③ 私改成功判据: 报告里对合同**没有**的判据给了检查 (自己另立了一套判据)
|
|
405
|
+
const checks = Array.isArray(r.checks) ? r.checks : [];
|
|
406
|
+
const forgedCriteria = checks.some((c) => {
|
|
407
|
+
if (!isPlainObject(c))
|
|
408
|
+
return false;
|
|
409
|
+
const name = c.name;
|
|
410
|
+
if (!isNonEmptyString(name) || !name.startsWith(CRITERION_REF_PREFIX))
|
|
411
|
+
return false;
|
|
412
|
+
return !contract.successCriteria.includes(name.slice(CRITERION_REF_PREFIX.length));
|
|
413
|
+
});
|
|
414
|
+
const hit = [];
|
|
415
|
+
if (budgetOverrun)
|
|
416
|
+
hit.push('expand_own_budget');
|
|
417
|
+
if (unverifiedComplete)
|
|
418
|
+
hit.push('mark_unverified_as_complete');
|
|
419
|
+
if (forgedCriteria)
|
|
420
|
+
hit.push('rewrite_success_criteria');
|
|
421
|
+
return CHILD_PROHIBITIONS.find((p) => hit.includes(p)) ?? null;
|
|
422
|
+
}
|
|
423
|
+
// ============================================================================
|
|
424
|
+
// §4. 是否接受为完成 (acceptsAsComplete)
|
|
425
|
+
// ============================================================================
|
|
426
|
+
/**
|
|
427
|
+
* 能不能把这份回报**接受为完成** (设计 §14 冻结签名)。这是父的判定, 不是子的自评。
|
|
428
|
+
*
|
|
429
|
+
* 拒绝原因按固定优先级给第一条 (越早发现越该先说):
|
|
430
|
+
* ① 自报状态不是 `completed`
|
|
431
|
+
* ② 报告不完整 (`missingFields`) —— 证据/检查为空的报告一律不接受
|
|
432
|
+
* ③ 判据缺证据 (`missingEvidence`)
|
|
433
|
+
* ④ 越界 (`violation`, 含 `mark_unverified_as_complete`)
|
|
434
|
+
* ⑤ 超期 (`reportedAt > deadline`) —— 超期是**交付时间**问题, 不是报告质量问题:
|
|
435
|
+
* 此时 `validateChildReport` 仍可能 `ok=true` (报告本身合格), 但要父决定是否重签合同。
|
|
436
|
+
*
|
|
437
|
+
* 通过 = 上面五条全不成立 (判据逐条有证据 + 逐条 `pass` 检查 + 无未解决项)。
|
|
438
|
+
*/
|
|
439
|
+
export function acceptsAsComplete(contract, report) {
|
|
440
|
+
const r = (report ?? {});
|
|
441
|
+
const validation = validateChildReport(contract, report);
|
|
442
|
+
if (r.status !== 'completed') {
|
|
443
|
+
return {
|
|
444
|
+
accepted: false,
|
|
445
|
+
reason: `子 Agent 自报状态 '${String(r.status)}', 不是 'completed' —— 不算完成 (未完成的工作可以继续等, 但不许当完成记账)`,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (validation.missingFields.length > 0) {
|
|
449
|
+
return {
|
|
450
|
+
accepted: false,
|
|
451
|
+
reason: `报告不完整, 缺/不合格字段: ${validation.missingFields.join(', ')} (证据与检查为空的报告一律不接受为完成)`,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (validation.missingEvidence.length > 0) {
|
|
455
|
+
return {
|
|
456
|
+
accepted: false,
|
|
457
|
+
reason: `判据缺证据: ${validation.missingEvidence.join(', ')} —— 逐条判据没有证据就不算完成`,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (validation.violation !== null) {
|
|
461
|
+
return {
|
|
462
|
+
accepted: false,
|
|
463
|
+
reason: `子 Agent 越界 (${validation.violation}) —— 该回报不能被接受为完成, 需要父/监控层处理`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
if (contract.deadline !== null && isIsoLike(r.reportedAt) && Date.parse(r.reportedAt) > Date.parse(contract.deadline)) {
|
|
467
|
+
return {
|
|
468
|
+
accepted: false,
|
|
469
|
+
reason: `回报时间 ${String(r.reportedAt)} 超过合同 deadline ${contract.deadline} —— 超期交付由父决定是否重签合同, 这里不判完成`,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const evidenceCount = Array.isArray(r.evidence) ? r.evidence.length : 0;
|
|
473
|
+
const unresolvedCount = Array.isArray(r.unresolvedItems) ? r.unresolvedItems.length : 0;
|
|
474
|
+
return {
|
|
475
|
+
accepted: true,
|
|
476
|
+
reason: `${contract.successCriteria.length} 条判据逐条有证据且检查 pass, 证据 ${evidenceCount} 条, 未解决项 ${unresolvedCount} 条 (workId ${contract.workId})`,
|
|
477
|
+
};
|
|
478
|
+
}
|