@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
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -29,7 +29,7 @@ import { DeepThinkingEngine, AgentCoordinator } from '@bolloon/constraint-runtim
|
|
|
29
29
|
import { WorkflowPivotLoop, createDefaultPivotConfig } from './workflow-pivot-loop.js';
|
|
30
30
|
import { initDocumentReceiver } from './p2p-document-tools.js';
|
|
31
31
|
import { startRun, recordStep, finishRun, readRun, budgetVerdict, recordDegradation, recordHarnessEvent, recordRecovery, setRunStatus, prepareResume, markRunRunning, buildResumeInstruction, argsDigestOf, repeatedFailureCount, classifyError as classifyRunError } from './run-store.js';
|
|
32
|
-
import { createGoal,
|
|
32
|
+
import { createGoal, attachRun, findActiveGoal } from './goal-store.js';
|
|
33
33
|
import { PiAgentHarness } from './pi-harness.js';
|
|
34
34
|
import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartbeat.js';
|
|
35
35
|
import { SkillRegistry } from '@bolloon/constraint-runtime';
|
|
@@ -757,6 +757,30 @@ export class PiAgentSession {
|
|
|
757
757
|
status: 'needs_human',
|
|
758
758
|
error: 'LLM 不可用: provider 未初始化或无 apiKey (fallback 不是执行结果, 需要配置或人工处理)',
|
|
759
759
|
});
|
|
760
|
+
// ★ M0: "工具/权限不可用"也是一条终止路径 → 同样进收尾漏斗 (规则 ④)。
|
|
761
|
+
const wiring = await import('./goal-flywheel-wiring.js');
|
|
762
|
+
const closed = await wiring.closeRunOnce({
|
|
763
|
+
goalId: boundGoalId,
|
|
764
|
+
runId: rec.runId,
|
|
765
|
+
caller: 'runner',
|
|
766
|
+
now: new Date().toISOString(),
|
|
767
|
+
finalReview: `LLM 不可用 → fallback 收尾 (不是执行结果): ${response}`,
|
|
768
|
+
});
|
|
769
|
+
if (!wiring.isRefusal(closed) && closed.outcome) {
|
|
770
|
+
const { reduceGoalState } = await import('./goal-state-reducer.js');
|
|
771
|
+
await reduceGoalState({
|
|
772
|
+
goalId: boundGoalId,
|
|
773
|
+
intent: 'closure_outcome',
|
|
774
|
+
now: new Date().toISOString(),
|
|
775
|
+
by: 'runner',
|
|
776
|
+
outcome: {
|
|
777
|
+
goalStatus: wiring.goalStatusFromDecision(closed.outcome.decision),
|
|
778
|
+
continuation: wiring.toGoalStoreContinuation(closed.outcome.continuation),
|
|
779
|
+
reason: closed.outcome.decision.reason,
|
|
780
|
+
},
|
|
781
|
+
runId: rec.runId,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
760
784
|
console.log(`[PiAgent] LLM 不可用 → 本次仍落 Run ${rec.runId} (needs_human), 不伪装成正常执行`);
|
|
761
785
|
}
|
|
762
786
|
catch (err) {
|
|
@@ -2341,30 +2365,61 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
|
|
|
2341
2365
|
await recordDegradation({ kind: 'core', op: 'pi-sdk.finishRun', runId: this.currentRunId, message }).catch(() => { });
|
|
2342
2366
|
onStream?.({ type: 'error', content: `⚠️ ${message}`, tool: 'harness' });
|
|
2343
2367
|
}
|
|
2344
|
-
// 2026-09-
|
|
2368
|
+
// ★ 2026-09-25 (M0 接线冻结, 规则 ③ + ④): Run 结束**必过收尾漏斗**。
|
|
2369
|
+
//
|
|
2370
|
+
// 旧写法在这里另起了一套 Goal 侧收尾 (读证据 → `evaluateGoalCompletion` →
|
|
2371
|
+
// `completeGoalIfEligible` / `setUnresolved`): 它**不收尾** —— 不写 Memory、不生成 Skill 候选、
|
|
2372
|
+
// 不写权威 continuation、不留决策记录。于是同一条 Run 在 Supervisor 那里还会再收一次 (或反过来
|
|
2373
|
+
// 这条路径根本不收), 全仓就有了两套"这一轮跑完意味着什么"。那一套已删除。
|
|
2374
|
+
//
|
|
2375
|
+
// 现在: `closeRunOnce` (幂等) → 9 步收尾 → 产物落盘; 再由 Goal reducer 把"下一步是什么"
|
|
2376
|
+
// 落进 Goal (没有 Supervisor 宿主时 Runner 也要留下 continuation, 否则下一次还是从零开始)。
|
|
2345
2377
|
if (this.currentGoalId) {
|
|
2346
2378
|
try {
|
|
2347
|
-
const
|
|
2348
|
-
const
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2379
|
+
const wiring = await import('./goal-flywheel-wiring.js');
|
|
2380
|
+
const { reduceGoalState } = await import('./goal-state-reducer.js');
|
|
2381
|
+
const nowIso = new Date().toISOString();
|
|
2382
|
+
const closed = await wiring.closeRunOnce({
|
|
2383
|
+
goalId: this.currentGoalId,
|
|
2384
|
+
runId: this.currentRunId,
|
|
2385
|
+
caller: 'runner',
|
|
2386
|
+
now: nowIso,
|
|
2387
|
+
finalReview: finalResponse ? String(finalResponse).slice(0, 2000) : '',
|
|
2388
|
+
});
|
|
2389
|
+
if (wiring.isRefusal(closed)) {
|
|
2390
|
+
onStream?.({ type: 'status', content: `⚠️ 收尾被拒 (不当作收过): ${closed.reason}`, tool: 'harness' });
|
|
2391
|
+
}
|
|
2392
|
+
else if (closed.outcome) {
|
|
2393
|
+
const view = closed.outcome;
|
|
2394
|
+
const applied = await reduceGoalState({
|
|
2395
|
+
goalId: this.currentGoalId,
|
|
2396
|
+
intent: 'closure_outcome',
|
|
2397
|
+
now: nowIso,
|
|
2398
|
+
by: 'runner',
|
|
2399
|
+
outcome: {
|
|
2400
|
+
goalStatus: wiring.goalStatusFromDecision(view.decision),
|
|
2401
|
+
continuation: wiring.toGoalStoreContinuation(view.continuation),
|
|
2402
|
+
reason: view.decision.reason,
|
|
2403
|
+
},
|
|
2404
|
+
runId: view.runId,
|
|
2405
|
+
});
|
|
2406
|
+
const line = view.decision.decision === 'complete'
|
|
2407
|
+
? `🎯 目标已达成 (仍要过完成门): ${view.decision.reason}`
|
|
2408
|
+
: `🎯 目标仍在进行 (未判完成): ${view.decision.reason}`;
|
|
2409
|
+
onStream?.({
|
|
2410
|
+
type: 'status',
|
|
2411
|
+
content: `${line}${applied.gateRejected ? ` [完成门拒绝: ${applied.gateRejected}]` : ''} `
|
|
2412
|
+
+ `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.currentGoalId})`,
|
|
2413
|
+
tool: 'harness',
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
else {
|
|
2417
|
+
// 幂等: 别处 (或上一次) 已经收过尾 —— 事实读不回来就如实说, 不重收也不假装
|
|
2418
|
+
onStream?.({ type: 'status', content: `🎯 ${closed.reason}`, tool: 'harness' });
|
|
2364
2419
|
}
|
|
2365
2420
|
}
|
|
2366
2421
|
catch (err) {
|
|
2367
|
-
await recordDegradation({ kind: 'observational', op: 'pi-sdk.
|
|
2422
|
+
await recordDegradation({ kind: 'observational', op: 'pi-sdk.runClosure', runId: this.currentRunId, message: String(err?.message || err).slice(0, 160) }).catch(() => { });
|
|
2368
2423
|
}
|
|
2369
2424
|
}
|
|
2370
2425
|
this.currentRunId = '';
|
package/dist/agents/run-store.js
CHANGED
|
@@ -24,14 +24,14 @@ export const RUN_TRANSITIONS = {
|
|
|
24
24
|
queued: ['running', 'aborted', 'interrupted'],
|
|
25
25
|
running: ['recovering', 'paused', 'awaiting_external', 'done', 'failed', 'aborted', 'interrupted', 'stalled', 'needs_human'],
|
|
26
26
|
recovering: ['running', 'failed', 'aborted', 'needs_human', 'interrupted', 'stalled'],
|
|
27
|
-
paused: ['running', 'aborted', 'interrupted'],
|
|
28
|
-
awaiting_external: ['running', 'failed', 'aborted', 'interrupted', 'stalled'],
|
|
27
|
+
paused: ['running', 'aborted', 'interrupted', 'recovering'],
|
|
28
|
+
awaiting_external: ['running', 'failed', 'aborted', 'interrupted', 'stalled', 'recovering'],
|
|
29
29
|
done: [],
|
|
30
30
|
failed: [],
|
|
31
31
|
aborted: [],
|
|
32
32
|
interrupted: ['recovering', 'aborted'], // 允许"从 checkpoint 恢复"
|
|
33
33
|
stalled: ['recovering', 'aborted', 'needs_human'],
|
|
34
|
-
needs_human: ['running', 'aborted'],
|
|
34
|
+
needs_human: ['running', 'aborted', 'recovering'],
|
|
35
35
|
};
|
|
36
36
|
export function canTransition(from, to) {
|
|
37
37
|
if (from === to)
|
|
@@ -572,6 +572,13 @@ function pidAlive(pid) {
|
|
|
572
572
|
return e?.code === 'EPERM';
|
|
573
573
|
}
|
|
574
574
|
}
|
|
575
|
+
let onRunTerminal = null;
|
|
576
|
+
export function setOnRunTerminal(hook) {
|
|
577
|
+
onRunTerminal = hook;
|
|
578
|
+
}
|
|
579
|
+
export function getOnRunTerminal() {
|
|
580
|
+
return onRunTerminal;
|
|
581
|
+
}
|
|
575
582
|
/**
|
|
576
583
|
* 孤儿对账: 启动时把 pid 已死的 running 记录改判 interrupted。
|
|
577
584
|
* 不做这一层的话, 重载后 UI/CLI 会显示"还在跑"的幽灵运行 —— 那是最典型的假状态。
|
|
@@ -588,11 +595,14 @@ export async function reconcileOrphans() {
|
|
|
588
595
|
continue;
|
|
589
596
|
}
|
|
590
597
|
try {
|
|
591
|
-
await finishRun(r.runId, {
|
|
598
|
+
const closed = await finishRun(r.runId, {
|
|
592
599
|
status: 'interrupted',
|
|
593
600
|
error: `进程 ${r.pid} 已不在 (刷新/重载/崩溃); 运行到此中断`,
|
|
594
601
|
});
|
|
595
602
|
interrupted.push(r.runId);
|
|
603
|
+
// 终止路径进唯一责任链 (拿不到 Goal 就不收, 不假装收过)
|
|
604
|
+
if (onRunTerminal)
|
|
605
|
+
await Promise.resolve(onRunTerminal(closed || r)).catch(() => null);
|
|
596
606
|
}
|
|
597
607
|
catch (err) {
|
|
598
608
|
failed.push(r.runId);
|
|
@@ -615,11 +625,14 @@ export async function superviseRuns(now = Date.now()) {
|
|
|
615
625
|
continue; // 交给 reconcileOrphans
|
|
616
626
|
if (now - Date.parse(r.updatedAt) > cfg.staleMs) {
|
|
617
627
|
try {
|
|
618
|
-
await finishRun(r.runId, {
|
|
628
|
+
const closed = await finishRun(r.runId, {
|
|
619
629
|
status: 'stalled',
|
|
620
630
|
error: `超过 ${Math.round(cfg.staleMs / 1000)}s 没有新进展 (可能是工具卡住或模型长时间无响应)`,
|
|
621
631
|
});
|
|
622
632
|
stalled.push(r.runId);
|
|
633
|
+
// 同上: 失速也是一条终止路径
|
|
634
|
+
if (onRunTerminal)
|
|
635
|
+
await Promise.resolve(onRunTerminal(closed || r)).catch(() => null);
|
|
623
636
|
}
|
|
624
637
|
catch (err) {
|
|
625
638
|
failed.push(r.runId);
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as os from 'os';
|
|
14
14
|
import { SkillsManager } from './skills-manager.js';
|
|
15
|
-
import { readGoal,
|
|
15
|
+
import { readGoal, updateGoal, addEvidence, } from './goal-store.js';
|
|
16
|
+
// 2026-09-25 (M0 接线冻结, 规则 ②): Goal 状态与 continuation 变更只有一个漏斗
|
|
17
|
+
import { reduceGoalState } from './goal-state-reducer.js';
|
|
16
18
|
import { recordDegradation } from './run-store.js';
|
|
17
19
|
export function parseSkillSpecs(list = []) {
|
|
18
20
|
const required = [];
|
|
@@ -109,15 +111,45 @@ export async function ensureGoalSkillsReady(goal, opts = {}) {
|
|
|
109
111
|
/** 门禁不过 → 写清事实并把 Goal 交给人 (不启动 Run, 不伪造失败) */
|
|
110
112
|
export async function blockGoalOnSkills(goalId, res) {
|
|
111
113
|
const reason = res.reason || '技能未就绪';
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
// 规则 ②: 状态与 continuation 都经 Goal reducer 写 (单一漏斗)
|
|
115
|
+
await reduceGoalState({
|
|
116
|
+
goalId,
|
|
117
|
+
intent: 'skill_gate_block',
|
|
118
|
+
now: new Date().toISOString(),
|
|
119
|
+
by: 'skill-readiness',
|
|
120
|
+
reason,
|
|
121
|
+
skillReadiness: {
|
|
122
|
+
ok: false, at: new Date().toISOString(), reason,
|
|
123
|
+
missing: res.missing, drift: res.drift, degradations: res.degradations,
|
|
124
|
+
},
|
|
125
|
+
}).catch(() => null);
|
|
118
126
|
for (const d of res.degradations)
|
|
119
127
|
await recordDegradation({ kind: 'observational', op: 'skill-readiness', message: d }).catch(() => { });
|
|
120
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* 只**记事实**、不改状态: 把本地技能就绪检查的结论写进 `continuation.skillReadiness`。
|
|
131
|
+
*
|
|
132
|
+
* ★ 2026-09-25 (P5 验收修复): 飞轮裁决 = `delegate` 时本地技能门禁**不适用** (能力由子 Agent 凭合同
|
|
133
|
+
* 交付), 所以不能调 `blockGoalOnSkills` (那会把 Goal 打成 needs_human)。但"本地缺什么"必须仍然
|
|
134
|
+
* 可查 —— 原来只有 `blockGoalOnSkills` 写这个字段, 门禁一放行就没有任何记录了。
|
|
135
|
+
*/
|
|
136
|
+
export async function recordSkillReadiness(goalId, res) {
|
|
137
|
+
// 规则 ②: continuation 也经 Goal reducer 写 (只记事实, 不改状态)
|
|
138
|
+
await reduceGoalState({
|
|
139
|
+
goalId,
|
|
140
|
+
intent: 'skill_gate_record',
|
|
141
|
+
now: new Date().toISOString(),
|
|
142
|
+
by: 'skill-readiness',
|
|
143
|
+
skillReadiness: {
|
|
144
|
+
ok: res.ok,
|
|
145
|
+
at: new Date().toISOString(),
|
|
146
|
+
reason: res.reason,
|
|
147
|
+
missing: res.missing,
|
|
148
|
+
drift: res.drift,
|
|
149
|
+
degradations: res.degradations,
|
|
150
|
+
},
|
|
151
|
+
}).catch(() => null);
|
|
152
|
+
}
|
|
121
153
|
/** 人工批准技能升级: 重新冻结快照 (显式动作, 不隐式切换) */
|
|
122
154
|
export async function approveSkillUpgrade(goalId, opts = {}) {
|
|
123
155
|
const goal = await readGoal(goalId);
|
|
@@ -126,8 +158,18 @@ export async function approveSkillUpgrade(goalId, opts = {}) {
|
|
|
126
158
|
const frozen = await freezeGoalSkills(goal, { ...opts, force: true });
|
|
127
159
|
if (!frozen.ok)
|
|
128
160
|
return { ok: false, reason: frozen.reason };
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
// ★ 规则 ⑥ (Skill 不许绕过 验证+快照+可回退 通道): 批准是**人**的动作, 但必须留下通道证据 ——
|
|
162
|
+
// 快照 (skillSnapshot, 上面 freezeGoalSkills 已写) + 版本 + 变更原因 + 可回退 (fromVersion 仍在
|
|
163
|
+
// Goal 上)。这里把"谁批的、为什么"连同新版本一起记进 Goal (走唯一漏斗, 规则 ②)。
|
|
164
|
+
const versions = (frozen.snapshot || []).map((s) => `${s.name}@${s.version}`);
|
|
165
|
+
const fromVersions = (goal.skillSnapshot || []).map((s) => `${s.name}@${s.version}`);
|
|
166
|
+
await reduceGoalState({
|
|
167
|
+
goalId,
|
|
168
|
+
intent: 'skill_upgrade_approved',
|
|
169
|
+
now: new Date().toISOString(),
|
|
170
|
+
by: 'human',
|
|
171
|
+
skillReadiness: { ok: true, at: new Date().toISOString(), reason: '人工批准技能升级' },
|
|
172
|
+
reason: `技能升级已被人工批准 (by=human; from=[${fromVersions.join(', ')}] → to=[${versions.join(', ')}]; 可回退: Goal.skillSnapshot 保留新版本快照, 内容哈希在 skills-manager 侧可核验)`,
|
|
173
|
+
}).catch(() => null);
|
|
132
174
|
return { ok: true, snapshot: frozen.snapshot };
|
|
133
175
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
// 2026-09-25 (M3 接缝): 换人必须重签合同的判据只有一份 (纯函数, 零 I/O)
|
|
4
|
+
import { childMatchesContract } from './goal-flywheel/wiring/contract.js';
|
|
3
5
|
const DEFAULT_STORAGE_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'agents');
|
|
4
6
|
const DEFAULT_TASK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
5
7
|
const DEFAULT_MAX_CONCURRENT_TASKS = 10;
|
|
@@ -160,10 +162,36 @@ export class SubAgentManager {
|
|
|
160
162
|
async assignTask(taskId, toAgentId) {
|
|
161
163
|
const task = this.tasks.get(taskId);
|
|
162
164
|
if (task) {
|
|
165
|
+
// 2026-09-25 (M3): 挂着工作合同的任务 **不许换人而不换合同**。
|
|
166
|
+
// 合同把 `childAgentId` 钉死了 (回报核验要求 report.childAgentId === contract.childAgentId),
|
|
167
|
+
// 把同一份合同指给另一个子 Agent ⇒ 新子的回报必然对不上合同, 合规判定整个失效。
|
|
168
|
+
// 换人的正确做法是**重新派遣** (父侧 signed 新合同) —— 本方法只维护指派关系, 无权签合同,
|
|
169
|
+
// 所以这里如实拒绝并留痕, 而不是"改了字段就当换成了"。
|
|
170
|
+
if (task.goalId && task.workId) {
|
|
171
|
+
const contract = (task.metadata?.workContract ?? null);
|
|
172
|
+
if (!contract) {
|
|
173
|
+
task.reassignRefused = {
|
|
174
|
+
code: 'contract_unreadable',
|
|
175
|
+
reason: `任务 ${taskId} 挂着 workId=${task.workId} 却读不到合同 → 无法判断新执行者是否与原合同一致, 拒绝换人 (先重签合同)`,
|
|
176
|
+
};
|
|
177
|
+
await this.saveTasks();
|
|
178
|
+
this.notifyTaskListeners(task);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const same = childMatchesContract(contract, toAgentId);
|
|
182
|
+
if (!same.ok) {
|
|
183
|
+
task.reassignRefused = { code: 'requires_new_contract', reason: same.reason };
|
|
184
|
+
await this.saveTasks();
|
|
185
|
+
this.notifyTaskListeners(task);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
task.reassignRefused = null;
|
|
189
|
+
}
|
|
163
190
|
task.assignedAgentId = toAgentId;
|
|
164
191
|
task.toAgentId = toAgentId;
|
|
165
192
|
task.status = 'assigned';
|
|
166
193
|
task.assignedAt = new Date().toISOString();
|
|
194
|
+
await this.heartbeatContractedTask(task, task.assignedAt);
|
|
167
195
|
await this.saveTasks();
|
|
168
196
|
this.notifyTaskListeners(task);
|
|
169
197
|
}
|
|
@@ -171,6 +199,37 @@ export class SubAgentManager {
|
|
|
171
199
|
async updateTaskStatus(taskId, status, result, error) {
|
|
172
200
|
const task = this.tasks.get(taskId);
|
|
173
201
|
if (task) {
|
|
202
|
+
// 2026-09-25 (M3 接缝): 挂了合同的子任务, 任何状态更新都是"子还活着"的证据 → 记一次心跳。
|
|
203
|
+
// 这是"执行中统一监控"的输入面: 没有心跳, 阻塞巡检只能看见"卡住", 看不见"在动"。
|
|
204
|
+
await this.heartbeatContractedTask(task);
|
|
205
|
+
// 2026-09-25 (飞轮接线 P2): 挂着工作合同的任务, "完成" 必须过回报核验 ——
|
|
206
|
+
// 回一段漂亮话 (没有逐条判据证据) **不算完成**: 任务留在 in_progress, 原因写清楚,
|
|
207
|
+
// 而 Goal 那边的 pendingReports 保持 (父仍然在等这份回报)。
|
|
208
|
+
//
|
|
209
|
+
// 2026-09-25 (M3): 判据从 `task.workId && task.goalId` 收紧成 `task.goalId` ——
|
|
210
|
+
// "派前必须有合同"的**反面**: 目标语境下的任务若**没有**合同 (签发失败 / 旧数据),
|
|
211
|
+
// 它同样不许自称完成 —— 没有合同的回报不核验也不接受 (handleChildReport 的 no_contract)。
|
|
212
|
+
if (status === 'completed' && task.goalId) {
|
|
213
|
+
if (!task.workId) {
|
|
214
|
+
task.status = 'in_progress';
|
|
215
|
+
task.error = '回报不接受为完成: 这条任务在目标语境下**没有工作合同** '
|
|
216
|
+
+ '(签合同失败或任务早于合同接线) —— 没有合同就没有判据, 无法核验, 不许当完成记账';
|
|
217
|
+
await this.saveTasks();
|
|
218
|
+
this.notifyTaskListeners(task);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const verdict = await this.validateTaskReport(task, result ?? '');
|
|
222
|
+
if (!verdict.accepted) {
|
|
223
|
+
task.status = 'in_progress';
|
|
224
|
+
if (result)
|
|
225
|
+
task.result = result;
|
|
226
|
+
task.error = `回报核验不过, 不接受为完成: ${verdict.reason}`
|
|
227
|
+
+ (verdict.missingEvidence.length ? ` [缺证据: ${verdict.missingEvidence.join('; ')}]` : '');
|
|
228
|
+
await this.saveTasks();
|
|
229
|
+
this.notifyTaskListeners(task);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
174
233
|
task.status = status;
|
|
175
234
|
if (result)
|
|
176
235
|
task.result = result;
|
|
@@ -183,6 +242,31 @@ export class SubAgentManager {
|
|
|
183
242
|
this.notifyTaskListeners(task);
|
|
184
243
|
}
|
|
185
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* 给挂着合同的任务记一次心跳 (P3 的输入面; 只有真写成盘才算数)。
|
|
247
|
+
*
|
|
248
|
+
* 为什么放在**真实派遣过程**里而不是让子 Agent 自己记得报: 心跳是"任务还活着"的事实,
|
|
249
|
+
* 而子 Agent 是最没有动力报心跳的一方 (它卡住了就不会报)。所以由派遣侧在每一次
|
|
250
|
+
* 真实状态变化 (派出去 / 换人 / 子更新状态) 时落一次 —— 没有这些落点, 阻塞巡检
|
|
251
|
+
* 只能看到"从来没心跳过", 分不清"卡住"与"刚派出"。
|
|
252
|
+
*
|
|
253
|
+
* 失败**不覆盖** `task.error` (那是子 Agent 的事实), 而是记进 `heartbeatError`:
|
|
254
|
+
* 心跳写不进去 = 监控会看到"无进展", 这件事必须留在任务上, 不许静默。
|
|
255
|
+
*/
|
|
256
|
+
async heartbeatContractedTask(task, at) {
|
|
257
|
+
if (!task.goalId || !task.workId)
|
|
258
|
+
return;
|
|
259
|
+
const now = at ?? new Date().toISOString();
|
|
260
|
+
try {
|
|
261
|
+
const { recordWorkHeartbeat } = await import('./goal-flywheel-wiring.js');
|
|
262
|
+
await recordWorkHeartbeat(task.goalId, task.workId, now);
|
|
263
|
+
task.lastHeartbeatAt = now;
|
|
264
|
+
task.heartbeatError = null;
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
task.heartbeatError = `心跳写入失败 (监控会看到"无进展"): ${String(e?.message || e).slice(0, 160)}`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
186
270
|
async getTask(taskId) {
|
|
187
271
|
return this.tasks.get(taskId);
|
|
188
272
|
}
|
|
@@ -300,14 +384,136 @@ export class SubAgentManager {
|
|
|
300
384
|
}
|
|
301
385
|
}
|
|
302
386
|
}
|
|
303
|
-
async delegateTask(fromAgentId, taskDescription, requiredCapabilities, priority = 'normal', input) {
|
|
387
|
+
async delegateTask(fromAgentId, taskDescription, requiredCapabilities, priority = 'normal', input, contractOptions) {
|
|
304
388
|
const agent = await this.findBestAgentForTask(requiredCapabilities, fromAgentId);
|
|
305
389
|
if (!agent) {
|
|
306
390
|
const task = await this.createTask('delegate', taskDescription.substring(0, 50), taskDescription, fromAgentId, undefined, priority, input);
|
|
307
|
-
|
|
391
|
+
const workContract = await this.issueWorkContractFor(fromAgentId, task, requiredCapabilities, contractOptions);
|
|
392
|
+
// 派前必须有工作合同 (M3): 目标语境下签不出合同 → **(没有收件人时本来也派不出去)**, 但要如实标注
|
|
393
|
+
if (!workContract && contractOptions?.goalId) {
|
|
394
|
+
this.refuseDispatchWithoutContract(task, contractOptions?.goalId);
|
|
395
|
+
await this.saveTasks();
|
|
396
|
+
return { task, agent: undefined, workContract: undefined };
|
|
397
|
+
}
|
|
398
|
+
if (workContract)
|
|
399
|
+
await this.heartbeatContractedTask(task);
|
|
400
|
+
await this.saveTasks();
|
|
401
|
+
return { task, agent: undefined, workContract };
|
|
308
402
|
}
|
|
309
403
|
const task = await this.createTask('delegate', taskDescription.substring(0, 50), taskDescription, fromAgentId, agent.id, priority, input);
|
|
310
|
-
|
|
404
|
+
// 2026-09-25 (P2): 派遣这件事**本身**就包含签合同 (不是事后再补一份文档);
|
|
405
|
+
// 有没有空闲子 Agent 不影响合同签发 (合同锁的是能力与判据, 不是某个人)。
|
|
406
|
+
//
|
|
407
|
+
// 2026-09-25 (M3): 签不出合同 ⇒ **不派出**。旧形状是"记个 error 照样把任务交出去",
|
|
408
|
+
// 那等于"派前必须有的合同"在失败路径上直接不存在 (子拿到一段任务描述就开始干,
|
|
409
|
+
// 回来只有一段文本, 父连判据都没有)。现在: 任务退回 `pending`、不绑执行者、
|
|
410
|
+
// 返回 `agent: undefined`, 让调用方**没有**可派遣的对象。
|
|
411
|
+
const workContract = await this.issueWorkContractFor(fromAgentId, task, requiredCapabilities, contractOptions);
|
|
412
|
+
if (!workContract && contractOptions?.goalId) {
|
|
413
|
+
this.refuseDispatchWithoutContract(task, contractOptions.goalId);
|
|
414
|
+
await this.saveTasks();
|
|
415
|
+
return { task, agent: undefined, workContract: undefined };
|
|
416
|
+
}
|
|
417
|
+
if (workContract)
|
|
418
|
+
await this.heartbeatContractedTask(task);
|
|
419
|
+
await this.saveTasks();
|
|
420
|
+
return { task, agent, workContract };
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* 目标语境下没有合同 ⇒ **不派出** (M3 的"派前必须有合同")。
|
|
424
|
+
*
|
|
425
|
+
* 落成事实: 任务退回 `pending` 且**不绑执行者** (没有 `assignedAt`),
|
|
426
|
+
* `contractRefused` 记下原因。于是:
|
|
427
|
+
* · 调用方拿到的 `agent` 是 `undefined` → 没有派遣对象;
|
|
428
|
+
* · 任务不会出现在 `getActiveTasks()` 里 (它没被派出去);
|
|
429
|
+
* · 原因留在任务上, 父能据此重签合同或交人 (不许静默当"没派过")。
|
|
430
|
+
*/
|
|
431
|
+
refuseDispatchWithoutContract(task, goalId) {
|
|
432
|
+
const reason = task.error
|
|
433
|
+
? `派前必须有工作合同: 合同没签成 → 不派出 (${task.error})`
|
|
434
|
+
: '派前必须有工作合同: 合同没签成 → 不派出';
|
|
435
|
+
task.status = 'pending';
|
|
436
|
+
task.assignedAgentId = undefined;
|
|
437
|
+
task.toAgentId = undefined;
|
|
438
|
+
task.assignedAt = undefined;
|
|
439
|
+
// Goal 上下文要留在任务上: ① 它说明这次派遣是**为哪个目标**尝试的;
|
|
440
|
+
// ② 于是完成门 (`updateTaskStatus` 的 `task.goalId` 判据) 也管得到这条没有合同的任务
|
|
441
|
+
// —— 没有合同就不许自称完成, 不留 goalId 的话它就绕过了那道门。
|
|
442
|
+
if (goalId)
|
|
443
|
+
task.goalId = goalId;
|
|
444
|
+
task.contractRefused = { code: 'contract_not_issued', reason };
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* 给这次派遣签工作合同 (P2)。只有调用方给了 `goalId` 才签 —— 没有长期目标就没有合同上下文,
|
|
448
|
+
* 这时行为与接线前完全一致 (只记任务)。
|
|
449
|
+
*/
|
|
450
|
+
async issueWorkContractFor(fromAgentId, task, requiredCapabilities, opts) {
|
|
451
|
+
if (!opts?.goalId)
|
|
452
|
+
return undefined;
|
|
453
|
+
try {
|
|
454
|
+
const { dispatchChildWork } = await import('./goal-flywheel-wiring.js');
|
|
455
|
+
const capability = (requiredCapabilities[0] || 'general').trim();
|
|
456
|
+
const issued = await dispatchChildWork({
|
|
457
|
+
goalId: opts.goalId,
|
|
458
|
+
parentRunId: opts.parentRunId ?? `subagent-task:${task.id}`,
|
|
459
|
+
childAgentId: task.assignedAgentId || task.toAgentId || `(unassigned:${fromAgentId})`,
|
|
460
|
+
capability,
|
|
461
|
+
objective: task.description,
|
|
462
|
+
inputs: { taskId: task.id, input: task.input ?? null },
|
|
463
|
+
allowedTools: opts.allowedTools ?? [],
|
|
464
|
+
budget: opts.budget ?? { maxSteps: null, maxDurationMs: this.config.taskTimeoutMs, maxAmount: null, currency: null },
|
|
465
|
+
deadline: opts.deadline ?? null,
|
|
466
|
+
successCriteria: (opts.successCriteria?.length ? opts.successCriteria : [task.description]).slice(0, 20),
|
|
467
|
+
issuedBy: `subagent-manager:${fromAgentId}`,
|
|
468
|
+
});
|
|
469
|
+
task.goalId = opts.goalId;
|
|
470
|
+
task.workId = issued.workId;
|
|
471
|
+
task.metadata = { ...(task.metadata ?? {}), workContract: issued };
|
|
472
|
+
await this.saveTasks();
|
|
473
|
+
return issued;
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
// 签合同失败 → 如实记在任务上 (不许悄悄降级成"没合同也能派")
|
|
477
|
+
task.error = `工作合同签发失败: ${String(err?.message || err).slice(0, 200)}`;
|
|
478
|
+
await this.saveTasks();
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* 核验挂着合同的子任务回报 (P2)。
|
|
484
|
+
* 报告可以是**结构化 JSON 文本** (推荐: 子按 `bolloon-work-report/1` 回), 也可以是纯文本 ——
|
|
485
|
+
* 纯文本没有逐条证据 → **不接受为完成** (这正是要堵的"回一段漂亮话就算完成")。
|
|
486
|
+
*/
|
|
487
|
+
async validateTaskReport(task, result) {
|
|
488
|
+
const { handleChildReport } = await import('./goal-flywheel-wiring.js');
|
|
489
|
+
const contract = (task.metadata?.workContract ?? null);
|
|
490
|
+
const raw = String(result ?? '');
|
|
491
|
+
let report = null;
|
|
492
|
+
try {
|
|
493
|
+
const parsed = JSON.parse(raw);
|
|
494
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.status === 'string') {
|
|
495
|
+
report = parsed;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch { /* 不是 JSON → 当纯文本处理 */ }
|
|
499
|
+
if (!report) {
|
|
500
|
+
report = {
|
|
501
|
+
workId: task.workId,
|
|
502
|
+
childAgentId: contract?.childAgentId ?? task.assignedAgentId ?? task.toAgentId ?? '(unknown)',
|
|
503
|
+
status: 'completed',
|
|
504
|
+
summary: raw.slice(0, 400),
|
|
505
|
+
evidence: [],
|
|
506
|
+
artifacts: [],
|
|
507
|
+
checks: [],
|
|
508
|
+
unresolvedItems: [],
|
|
509
|
+
blockReason: null,
|
|
510
|
+
nextRecommendation: '(未提供)',
|
|
511
|
+
durationMs: 0,
|
|
512
|
+
reportedAt: new Date().toISOString(),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const out = await handleChildReport({ goalId: task.goalId, workId: task.workId, report });
|
|
516
|
+
return { accepted: out.accepted, reason: out.reason, missingEvidence: out.missingEvidence };
|
|
311
517
|
}
|
|
312
518
|
async consultAgent(fromAgentId, toAgentId, query, taskId) {
|
|
313
519
|
return this.sendMessage(fromAgentId, toAgentId, query, 'query', taskId);
|
|
@@ -24,7 +24,7 @@ import { resolveTaskBudget } from './task-budget.js';
|
|
|
24
24
|
import { adviseResource } from './resource-advisor.js';
|
|
25
25
|
import { defaultRequestId, installBundle, deriveSkillInput, extractSources, extractConclusion } from './task-runner.js';
|
|
26
26
|
import { buildReportCard, renderReportCard } from './report-card.js';
|
|
27
|
-
import { loadResourceContract, executeContractSkill, validateResourceOutput, verifyInstallFidelity, } from '../x402/resource-contract.js';
|
|
27
|
+
import { loadResourceContract, executeContractSkill, validateResourceOutput, validateResourceInput, verifyInstallFidelity, } from '../x402/resource-contract.js';
|
|
28
28
|
import { beginTransaction, readTransaction, updateTransaction } from '../x402/transaction-store.js';
|
|
29
29
|
import { writeDeliveryContent } from '../x402/settlement-state.js';
|
|
30
30
|
import { computeContentHash } from '../x402/paid-info-protocol.js';
|
|
@@ -88,6 +88,23 @@ export async function runTaskOnchain(opts) {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
mark('acquire', `选中 ${chosen.name} (${(chosen.why || []).slice(0, 2).join(' / ') || '名字匹配'})`);
|
|
91
|
+
// ②.5 输入预检 (不花钱的前置门): 输入建不出来/不合 inputSchema → **绝不先注资**
|
|
92
|
+
// 教训: 先注资再执行, 执行不达标 → 钱卡在托管里 (只能 dispute+refund 才拿得回)。
|
|
93
|
+
// 契约的 inputSchema 在这里就能判, 所以把这道门放在任何链上写操作之前。
|
|
94
|
+
const preContract = chosen.contract || (await loadResourceContract(chosen.dir)).contract;
|
|
95
|
+
const preInput = opts.input ?? deriveSkillInput(preContract, opts.task);
|
|
96
|
+
const preChk = preContract ? validateResourceInput(preContract, preInput) : { ok: true, issues: [] };
|
|
97
|
+
if (!preChk.ok) {
|
|
98
|
+
const blocker = `输入不符合 inputSchema (${preChk.issues[0] || '未知'}) → 未开托管, 没花钱`;
|
|
99
|
+
const card = buildReportCard({ task: opts.task, executed: false, paid: false, evidenceRef: {}, blocker });
|
|
100
|
+
mark('report', '输入不完整 → 不开托管, 不花钱 (给 --input 或把数字写进任务文本)');
|
|
101
|
+
return {
|
|
102
|
+
ok: false, taskKey, requestId, transactionId: '', card, text: renderReportCard(card), budget,
|
|
103
|
+
skill: { name: chosen.name, version: chosen.version, dir: chosen.dir },
|
|
104
|
+
chain: { verified: false, reason: blocker, statuses: [] }, outputIssues: [...preChk.issues, blocker],
|
|
105
|
+
recovery: recoverOnchainTrade({ home: opts.home, taskKey }), stages,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
91
108
|
// ③ 金额 = 单次购买上限 (与 M1 硬约束一致); 转原子单位
|
|
92
109
|
const amountAtomic = BigInt(Math.round(budget.perPurchase * 10 ** decimals));
|
|
93
110
|
const amountChk = checkOnchainAmount({ amountAtomic, decimals, budget: { taskBudget: budget.taskBudget, perPurchase: budget.perPurchase } });
|
|
@@ -124,6 +141,7 @@ export async function runTaskOnchain(opts) {
|
|
|
124
141
|
network: opts.network || 'unknown', tokenDecimals: decimals,
|
|
125
142
|
buyerSigner: opts.buyerSigner, sellerSigner: opts.sellerSigner,
|
|
126
143
|
env: opts.env, persistState: opts.persistState, verifyOnChain: opts.verifyOnChain,
|
|
144
|
+
settleRetryAttempts: opts.settleRetryAttempts, settleRetryDelayMs: opts.settleRetryDelayMs,
|
|
127
145
|
};
|
|
128
146
|
// ⑤ 幂等/恢复: 已经链上走过的任务**不新开托管** (先读盘重建)
|
|
129
147
|
const before = recoverOnchainTrade({ home: opts.home, taskKey });
|