@bolloon/bolloon-agent 0.4.26 → 0.4.28
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/execution-supervisor.js +51 -0
- package/dist/agents/p2p-info.js +175 -0
- package/dist/agents/run-store.js +15 -0
- package/dist/agents/task/local-seller.js +149 -0
- package/dist/agents/task/report-card.js +146 -0
- package/dist/agents/task/resource-advisor.js +182 -0
- package/dist/agents/task/task-budget.js +103 -0
- package/dist/agents/task/task-runner.js +618 -0
- package/dist/agents/trace-export.js +125 -0
- package/dist/agents/write-staging.js +12 -4
- package/dist/agents/x402/goal-run-bridge.js +109 -0
- package/dist/agents/x402/milestone-settlement.js +150 -0
- package/dist/agents/x402/paid-info-store.js +66 -12
- package/dist/agents/x402/payment-recovery.js +290 -0
- package/dist/agents/x402/resource-contract.js +484 -0
- package/dist/agents/x402/settlement-state.js +378 -0
- package/dist/agents/x402/trade.js +257 -0
- package/dist/agents/x402/transaction-protocol.js +99 -0
- package/dist/agents/x402/transaction-store.js +350 -0
- package/dist/cli-entry.js +168 -0
- package/dist/index.js +103 -0
- package/dist/web/routes-x402-info.js +1 -0
- package/dist/web/server.js +82 -0
- package/package.json +1 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resource-advisor.ts — M1 薄层 ②: 资源顾问
|
|
3
|
+
*
|
|
4
|
+
* 只回答三个问题:
|
|
5
|
+
* ① 当前任务是否缺外部能力?
|
|
6
|
+
* ② 本地 Registry 里哪个可执行 Skill 满足契约?
|
|
7
|
+
* ③ 为什么选它?
|
|
8
|
+
*
|
|
9
|
+
* M1 明确**不做**: 语义搜索 / 向量检索 / P2P 发现 / 竞价 / 推荐系统。
|
|
10
|
+
* 这里只有: 关键词确定性匹配 + 契约完整性检查 + 价格来源(报价)检查, 同分用名字排序 (可复现)。
|
|
11
|
+
*
|
|
12
|
+
* "本地 Registry" = 已装技能目录 (`defaultSkillPaths` + `~/.bolloon/skills`)
|
|
13
|
+
* × 本机 x402 报价 (`listInfo`), 两者按约定关联 (见 linkListing)。
|
|
14
|
+
*/
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { defaultSkillPaths, loadSkillsDir } from '../skill-loader.js';
|
|
18
|
+
import { listInfo } from '../x402/paid-info-store.js';
|
|
19
|
+
import { parseResourceContract } from '../x402/resource-contract.js';
|
|
20
|
+
/** 中文按 bigram、英文/数字按词切; 去停用词。确定性, 无随机。 */
|
|
21
|
+
export function tokenizeTask(task) {
|
|
22
|
+
const text = String(task || '').toLowerCase();
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const m of text.matchAll(/[a-z0-9][a-z0-9._-]+/g))
|
|
25
|
+
out.push(m[0]);
|
|
26
|
+
const cjk = text.replace(/[^\u4e00-\u9fff]+/g, ' ');
|
|
27
|
+
for (const seg of cjk.split(/\s+/)) {
|
|
28
|
+
if (!seg)
|
|
29
|
+
continue;
|
|
30
|
+
if (seg.length <= 2) {
|
|
31
|
+
out.push(seg);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
for (let i = 0; i + 2 <= seg.length; i++)
|
|
35
|
+
out.push(seg.slice(i, i + 2));
|
|
36
|
+
}
|
|
37
|
+
const stop = new Set(['这个', '那个', '一个', '是否', '请问', '帮我', 'the', 'and', 'for', 'with', 'task']);
|
|
38
|
+
return Array.from(new Set(out.filter((t) => t.length >= 2 && !stop.has(t))));
|
|
39
|
+
}
|
|
40
|
+
/** 关键词命中打分: 只在名字 / 描述 / triggers / capability 文本里找, 命中即记 why。 */
|
|
41
|
+
export function scoreSkill(args) {
|
|
42
|
+
const fields = [
|
|
43
|
+
['name', args.name || ''],
|
|
44
|
+
['description', args.description || ''],
|
|
45
|
+
['triggers', (args.triggers || []).join(' ')],
|
|
46
|
+
['capability', args.capability || ''],
|
|
47
|
+
['guarantees', (args.guarantees || []).join(' ')],
|
|
48
|
+
];
|
|
49
|
+
const lowered = fields.map(([k, v]) => [k, v.toLowerCase()]);
|
|
50
|
+
const why = [];
|
|
51
|
+
let score = 0;
|
|
52
|
+
for (const tok of args.tokens) {
|
|
53
|
+
for (const [field, text] of lowered) {
|
|
54
|
+
if (!text)
|
|
55
|
+
continue;
|
|
56
|
+
if (text.includes(tok)) {
|
|
57
|
+
const w = field === 'name' ? 3 : field === 'capability' ? 2 : 1;
|
|
58
|
+
score += w;
|
|
59
|
+
why.push(`命中 ${field}: "${tok}" (+${w})`);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { score, why };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 报价 ↔ 技能 的关联约定 (M1 确定性规则, 不做模糊匹配):
|
|
68
|
+
* ① item.id === skill 名 ② item.source.note 含 `skill=<name>` ③ item.title 含技能名
|
|
69
|
+
*/
|
|
70
|
+
export function linkListing(skillName, items) {
|
|
71
|
+
const lower = skillName.toLowerCase();
|
|
72
|
+
const hit = items.find((i) => {
|
|
73
|
+
if (String(i.id).toLowerCase() === lower)
|
|
74
|
+
return true;
|
|
75
|
+
const note = String(i.source?.note || '').toLowerCase();
|
|
76
|
+
if (note.includes(`skill=${lower}`))
|
|
77
|
+
return true;
|
|
78
|
+
return String(i.title || '').toLowerCase().includes(lower);
|
|
79
|
+
});
|
|
80
|
+
if (!hit)
|
|
81
|
+
return undefined;
|
|
82
|
+
return {
|
|
83
|
+
itemId: hit.id,
|
|
84
|
+
title: String(hit.title || hit.id),
|
|
85
|
+
amount: String(hit.price?.amount ?? '0'),
|
|
86
|
+
currency: String(hit.price?.currency ?? 'USDC'),
|
|
87
|
+
network: String(hit.price?.network ?? 'base-sepolia'),
|
|
88
|
+
payTo: String(hit.price?.payTo ?? ''),
|
|
89
|
+
skillName: String(hit.source?.note || '').includes('skill=') ? skillName : undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 顾问主函数: 扫本地 Registry → 过滤"有契约且可执行"的候选 → 确定性打分排序。
|
|
94
|
+
* 不猜: 一个候选都没有 → needed:false 并说明原因。
|
|
95
|
+
*/
|
|
96
|
+
export async function adviseResource(opts) {
|
|
97
|
+
const home = opts.home ?? os.homedir();
|
|
98
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
99
|
+
const notes = [];
|
|
100
|
+
const paths = (opts.skillPaths ?? [...defaultSkillPaths(home, cwd), path.join(home, '.bolloon', 'skills')]);
|
|
101
|
+
const merged = [];
|
|
102
|
+
for (const p of paths)
|
|
103
|
+
if (!merged.includes(p))
|
|
104
|
+
merged.push(p);
|
|
105
|
+
const skillDirs = [];
|
|
106
|
+
for (const p of merged) {
|
|
107
|
+
let metas = [];
|
|
108
|
+
try {
|
|
109
|
+
metas = await loadSkillsDir(p);
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
notes.push(`技能目录不可读 (跳过): ${p} — ${String(e?.message || e).slice(0, 80)}`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
for (const meta of metas) {
|
|
116
|
+
const fm = (meta.frontmatter || {});
|
|
117
|
+
const parsed = parseResourceContract(fm, { skillName: String(meta.name), skillVersion: String(fm.version || '') });
|
|
118
|
+
if (!parsed.ok || !parsed.contract)
|
|
119
|
+
continue; // 非法契约直接不算候选
|
|
120
|
+
const c = parsed.contract;
|
|
121
|
+
if (!c.execution?.entrypoint)
|
|
122
|
+
continue; // 不可执行 → 不是 M1 要的资源
|
|
123
|
+
if (!c.inputSchema || !c.outputSchema)
|
|
124
|
+
continue; // 无输入/输出契约 → 不买
|
|
125
|
+
skillDirs.push({
|
|
126
|
+
name: String(meta.name),
|
|
127
|
+
version: String(fm.version || (c.version ?? '')),
|
|
128
|
+
dir: path.dirname(String(meta.sourcePath)),
|
|
129
|
+
description: String(meta.description || ''),
|
|
130
|
+
triggers: (meta.triggers || []),
|
|
131
|
+
contract: parsed.contract,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (skillDirs.length === 0) {
|
|
136
|
+
return { needed: false, reason: `本地 Registry 里没有"可执行且契约完整"的 Skill (已扫 ${merged.length} 个目录)`, candidates: [], notes };
|
|
137
|
+
}
|
|
138
|
+
const items = await listInfo(home);
|
|
139
|
+
const tokens = tokenizeTask(opts.task);
|
|
140
|
+
const candidates = skillDirs.map((s) => {
|
|
141
|
+
const c = s.contract;
|
|
142
|
+
const scored = scoreSkill({
|
|
143
|
+
tokens,
|
|
144
|
+
name: s.name,
|
|
145
|
+
description: s.description,
|
|
146
|
+
triggers: s.triggers,
|
|
147
|
+
capability: c.capability,
|
|
148
|
+
guarantees: c.guarantees,
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
name: s.name,
|
|
152
|
+
version: s.version,
|
|
153
|
+
dir: s.dir,
|
|
154
|
+
contract: s.contract,
|
|
155
|
+
score: scored.score,
|
|
156
|
+
why: scored.why,
|
|
157
|
+
listing: linkListing(s.name, items),
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
// 确定性排序: 分数降序 → 名字升序
|
|
161
|
+
candidates.sort((a, b) => (b.score - a.score) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
162
|
+
const minScore = opts.minScore ?? 1;
|
|
163
|
+
const top = candidates[0];
|
|
164
|
+
if (!top || top.score < minScore) {
|
|
165
|
+
return {
|
|
166
|
+
needed: false,
|
|
167
|
+
reason: `任务关键词与本地可执行 Skill 都不匹配 (最高分 ${top?.score ?? 0} < ${minScore}) — 不买不该买的东西`,
|
|
168
|
+
candidates,
|
|
169
|
+
notes,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (!top.listing) {
|
|
173
|
+
notes.push(`候选 Skill "${top.name}" 没有本机报价 (listInfo 里没有对应 item) → M1 买不到, 会如实告诉你`);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
needed: true,
|
|
177
|
+
reason: `本地 Registry 命中 "${top.name}" (分数 ${top.score}: ${top.why.slice(0, 3).join(' / ') || '名字匹配'})`,
|
|
178
|
+
candidates,
|
|
179
|
+
chosen: top,
|
|
180
|
+
notes,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-budget.ts — M1 预算闸 (leo 2026-09-18 冻结规则 ③)
|
|
3
|
+
*
|
|
4
|
+
* 单任务 0.05 USDC / 单次购买 0.02 USDC / 单日测试 0.10 USDC, **所有层取最小值**。
|
|
5
|
+
* 用户只在开始时给一次; 执行过程中**不许被 Agent 自动扩大** (assertNoExpansion 是那条红线)。
|
|
6
|
+
*
|
|
7
|
+
* 与 policy 的关系: `economic-policy.ts` 的 `单笔 $1 / 每日 $10` 是系统级闸门,
|
|
8
|
+
* 这里是**任务级**的第二道闸, 两层都过才放行 (trade.ts 的 taskBudget 参数即为此闸)。
|
|
9
|
+
*/
|
|
10
|
+
/** M1 硬上限 (leo 定) */
|
|
11
|
+
export const M1_BUDGET_LIMITS = { task: 0.05, perPurchase: 0.02, daily: 0.10 };
|
|
12
|
+
function num(v) {
|
|
13
|
+
if (v === undefined || v === null || v === '')
|
|
14
|
+
return undefined;
|
|
15
|
+
const n = typeof v === 'number' ? v : Number(String(v));
|
|
16
|
+
if (!Number.isFinite(n))
|
|
17
|
+
return undefined;
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 解析任务预算。缺省 → M1 默认值; 给多了 → **取 min 并显式记录被收紧** (不静默改成别的数)。
|
|
22
|
+
* 非法输入 → ok:false (不猜)。
|
|
23
|
+
*/
|
|
24
|
+
export function resolveTaskBudget(opts = {}) {
|
|
25
|
+
const lim = opts.limits ?? M1_BUDGET_LIMITS;
|
|
26
|
+
const reqTask = num(opts.taskBudget);
|
|
27
|
+
const reqPer = num(opts.perPurchase);
|
|
28
|
+
const reqDaily = num(opts.daily);
|
|
29
|
+
for (const [k, v] of [['taskBudget', opts.taskBudget], ['perPurchase', opts.perPurchase], ['daily', opts.daily]]) {
|
|
30
|
+
if (v !== undefined && v !== null && String(v) !== '' && num(v) === undefined) {
|
|
31
|
+
return { ok: false, error: `${k} 不是合法数字: ${String(v)}` };
|
|
32
|
+
}
|
|
33
|
+
const n = num(v);
|
|
34
|
+
if (n !== undefined && n <= 0)
|
|
35
|
+
return { ok: false, error: `${k} 必须为正数: ${String(v)}` };
|
|
36
|
+
}
|
|
37
|
+
const taskBudget = Math.min(reqTask ?? lim.task, lim.task);
|
|
38
|
+
const perPurchase = Math.min(reqPer ?? lim.perPurchase, lim.perPurchase, taskBudget);
|
|
39
|
+
const daily = Math.min(reqDaily ?? lim.daily, lim.daily);
|
|
40
|
+
const why = [];
|
|
41
|
+
why.push(`任务预算 ${taskBudget} USDC (M1 硬上限 ${lim.task}${reqTask !== undefined ? `, 你给的 ${reqTask}` : ''})`);
|
|
42
|
+
why.push(`单次购买上限 ${perPurchase} USDC (M1 硬上限 ${lim.perPurchase}${reqPer !== undefined ? `, 你给的 ${reqPer}` : ''})`);
|
|
43
|
+
why.push(`当日预算 ${daily} USDC (M1 硬上限 ${lim.daily}${reqDaily !== undefined ? `, 你给的 ${reqDaily}` : ''})`);
|
|
44
|
+
const clamped = {
|
|
45
|
+
task: reqTask !== undefined && reqTask > lim.task,
|
|
46
|
+
perPurchase: reqPer !== undefined && reqPer > Math.min(lim.perPurchase, taskBudget),
|
|
47
|
+
daily: reqDaily !== undefined && reqDaily > lim.daily,
|
|
48
|
+
};
|
|
49
|
+
if (clamped.task)
|
|
50
|
+
why.push(`⚠️ 你给的任务预算 ${reqTask} 超过 M1 上限 ${lim.task} → 实际按 ${taskBudget} 执行 (不是 Agent 改的, 是 M1 规则)`);
|
|
51
|
+
if (clamped.perPurchase)
|
|
52
|
+
why.push(`⚠️ 你给的购买上限 ${reqPer} 被收紧到 ${perPurchase}`);
|
|
53
|
+
if (clamped.daily)
|
|
54
|
+
why.push(`⚠️ 你给的当日预算 ${reqDaily} 被收紧到 ${daily}`);
|
|
55
|
+
return { ok: true, plan: { taskBudget, perPurchase, daily, requested: { task: reqTask, perPurchase: reqPer, daily: reqDaily }, clamped, why } };
|
|
56
|
+
}
|
|
57
|
+
/** 逐层检查一次购买; 哪一层拦住就说清哪一层 (绝不合并成"预算不足"这种糊话)。 */
|
|
58
|
+
export function checkPurchaseAllowed(opts) {
|
|
59
|
+
const amount = num(opts.amount);
|
|
60
|
+
if (amount === undefined)
|
|
61
|
+
return { allowed: false, layer: undefined, reason: `金额不是合法数字: ${String(opts.amount)}` };
|
|
62
|
+
const spentInTask = opts.spentInTask ?? 0;
|
|
63
|
+
const spentToday = opts.spentToday ?? 0;
|
|
64
|
+
const remainingTask = round6(opts.plan.taskBudget - spentInTask);
|
|
65
|
+
const remainingDaily = round6(opts.plan.daily - spentToday);
|
|
66
|
+
if (amount > opts.plan.perPurchase) {
|
|
67
|
+
const capSource = opts.plan.perPurchase < opts.plan.requested.perPurchase
|
|
68
|
+
? `来自任务预算 ${opts.plan.taskBudget}`
|
|
69
|
+
: opts.plan.clamped.perPurchase ? '被 M1 上限收紧后的值' : '你给的上限';
|
|
70
|
+
return { allowed: false, layer: 'perPurchase', reason: `单次购买上限 ${opts.plan.perPurchase} USDC (${capSource}); 这次要 ${amount} USDC`, remainingTask, remainingDaily };
|
|
71
|
+
}
|
|
72
|
+
if (amount > remainingTask) {
|
|
73
|
+
return { allowed: false, layer: 'taskBudget', reason: `任务预算只剩 ${remainingTask} USDC; 这次要 ${amount} USDC`, remainingTask, remainingDaily };
|
|
74
|
+
}
|
|
75
|
+
if (amount > remainingDaily) {
|
|
76
|
+
return { allowed: false, layer: 'daily', reason: `今日预算只剩 ${remainingDaily} USDC; 这次要 ${amount} USDC`, remainingTask, remainingDaily };
|
|
77
|
+
}
|
|
78
|
+
return { allowed: true, remainingTask, remainingDaily };
|
|
79
|
+
}
|
|
80
|
+
function round6(n) {
|
|
81
|
+
return Math.round(n * 1e6) / 1e6;
|
|
82
|
+
}
|
|
83
|
+
/** 购买前的"价格与预算影响"预览 (M1 验收第 5 项: 付款前必须看得到) */
|
|
84
|
+
export function previewPurchaseImpact(plan, amount, spentToday = 0) {
|
|
85
|
+
const a = num(amount);
|
|
86
|
+
if (a === undefined)
|
|
87
|
+
return [`金额非法: ${String(amount)}`];
|
|
88
|
+
const pctTask = plan.taskBudget > 0 ? Math.round((a / plan.taskBudget) * 100) : 100;
|
|
89
|
+
return [
|
|
90
|
+
`价格 ${a} USDC · 占任务预算 ${pctTask}%`,
|
|
91
|
+
`购买后: 任务剩余 ${round6(plan.taskBudget - a)} USDC · 今日剩余 ${round6(plan.daily - spentToday - a)} USDC`,
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
/** 执行中不许扩大预算 (M1 红线: 用户只给一次) */
|
|
95
|
+
export function assertNoExpansion(prev, next) {
|
|
96
|
+
if (next.taskBudget > prev.taskBudget + 1e-9)
|
|
97
|
+
return { ok: false, reason: `任务预算被扩大: ${prev.taskBudget} → ${next.taskBudget} (M1 不允许)` };
|
|
98
|
+
if (next.perPurchase > prev.perPurchase + 1e-9)
|
|
99
|
+
return { ok: false, reason: `单次上限被扩大: ${prev.perPurchase} → ${next.perPurchase} (M1 不允许)` };
|
|
100
|
+
if (next.daily > prev.daily + 1e-9)
|
|
101
|
+
return { ok: false, reason: `当日预算被扩大: ${prev.daily} → ${next.daily} (M1 不允许)` };
|
|
102
|
+
return { ok: true };
|
|
103
|
+
}
|