@bolloon/bolloon-agent 0.3.42 → 0.3.43
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/skill-writer.js +70 -14
- package/package.json +1 -1
|
@@ -146,8 +146,26 @@ export async function writeSkillCandidate(c) {
|
|
|
146
146
|
const dir = getCandidateDir();
|
|
147
147
|
await fs.mkdir(dir, { recursive: true });
|
|
148
148
|
const safeName = sanitizeSkillName(c.name);
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
// 2026-08-08: 有 signature 的候选用固定文件名 (合并更新同一个), 无 signature 才带时间戳
|
|
150
|
+
const file = c.signature
|
|
151
|
+
? path.join(dir, `${safeName}.json`)
|
|
152
|
+
: path.join(dir, `${safeName}-${Date.now()}.json`);
|
|
153
|
+
// 追加式合并: 若同 signature 已存在, 累积 runs + 追加 body
|
|
154
|
+
let runs = c.runs ?? 1;
|
|
155
|
+
let body = c.body;
|
|
156
|
+
try {
|
|
157
|
+
const prev = JSON.parse(await fs.readFile(file, 'utf-8'));
|
|
158
|
+
if (prev && prev.runs)
|
|
159
|
+
runs = prev.runs + 1;
|
|
160
|
+
if (prev && prev.body && body !== prev.body && c.signature) {
|
|
161
|
+
// 同一 signature 重复运行 → 追加一条经验 (去重, 避免 body 膨胀)
|
|
162
|
+
const line = `- ${new Date().toISOString().slice(0, 16)} ${c.source}: ${c.description}`;
|
|
163
|
+
body = `${prev.body}\n${line}`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch { /* 新文件 */ }
|
|
167
|
+
const merged = { ...c, runs, body, timestamp: c.timestamp || new Date().toISOString() };
|
|
168
|
+
await fs.writeFile(file, JSON.stringify(merged, null, 2), 'utf-8');
|
|
151
169
|
return file;
|
|
152
170
|
}
|
|
153
171
|
export async function listSkillCandidates(home = os.homedir()) {
|
|
@@ -165,12 +183,37 @@ export async function listSkillCandidates(home = os.homedir()) {
|
|
|
165
183
|
const raw = await fs.readFile(path.join(dir, f), 'utf-8');
|
|
166
184
|
const c = JSON.parse(raw);
|
|
167
185
|
if (c.name && c.body)
|
|
168
|
-
out.push(c);
|
|
186
|
+
out.push({ ...c, file: path.join(dir, f) });
|
|
169
187
|
}
|
|
170
188
|
catch { /* 坏文件跳过 */ }
|
|
171
189
|
}
|
|
172
190
|
return out;
|
|
173
191
|
}
|
|
192
|
+
/** 按名字删除所有同名候选文件 (名可能与文件名前缀不完全一致) */
|
|
193
|
+
async function removeCandidateFiles(name, home) {
|
|
194
|
+
const safe = sanitizeSkillName(name);
|
|
195
|
+
const dir = getCandidateDir(home);
|
|
196
|
+
let files;
|
|
197
|
+
try {
|
|
198
|
+
files = await fs.readdir(dir);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
for (const f of files) {
|
|
204
|
+
if (!f.endsWith('.json'))
|
|
205
|
+
continue;
|
|
206
|
+
try {
|
|
207
|
+
const c = JSON.parse(await fs.readFile(path.join(dir, f), 'utf-8'));
|
|
208
|
+
if (sanitizeSkillName(c.name) === safe)
|
|
209
|
+
await fs.rm(path.join(dir, f), { force: true });
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
if (f.startsWith(safe))
|
|
213
|
+
await fs.rm(path.join(dir, f), { force: true });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
174
217
|
/** 把候选转正为正式 skill (可选: 转正后删除候选文件) */
|
|
175
218
|
export async function promoteCandidate(name, opts = {}, home = os.homedir()) {
|
|
176
219
|
const candidates = await listSkillCandidates(home);
|
|
@@ -179,18 +222,25 @@ export async function promoteCandidate(name, opts = {}, home = os.homedir()) {
|
|
|
179
222
|
return { ok: false, path: '', error: `候选 '${name}' 不存在` };
|
|
180
223
|
const r = await createSkill(c.name, c.description, c.body, opts);
|
|
181
224
|
if (r.ok) {
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
const dir = getCandidateDir(home);
|
|
185
|
-
for (const f of (await fs.readdir(dir))) {
|
|
186
|
-
if (f.startsWith(sanitizeSkillName(c.name) + '-'))
|
|
187
|
-
await fs.rm(path.join(dir, f), { force: true });
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
catch { /* 清理失败不阻塞 */ }
|
|
225
|
+
// 清理已转正的候选文件: 同 name 的所有候选
|
|
226
|
+
await removeCandidateFiles(c.name, home);
|
|
191
227
|
}
|
|
192
228
|
return r;
|
|
193
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* 从一轮成功的工具调用生成稳定签名 — 同一套工具序列 (有序去重, 最多 4 个) 视为同一经验.
|
|
232
|
+
* 用于跨运行合并: 第二次跑同样的工具 → 更新同一个候选, 而不是新建一个.
|
|
233
|
+
*/
|
|
234
|
+
export function toolSignature(okSteps) {
|
|
235
|
+
const seq = [];
|
|
236
|
+
for (const s of (okSteps || [])) {
|
|
237
|
+
if (s.name && !seq.includes(s.name))
|
|
238
|
+
seq.push(s.name);
|
|
239
|
+
if (seq.length >= 4)
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
return seq.join('_');
|
|
243
|
+
}
|
|
194
244
|
export async function writeRunEndSkillCandidates(steps, source, minOk = 2) {
|
|
195
245
|
const okSteps = (steps || []).filter((s) => s.status === 'ok' && s.name && s.name !== 'system' && s.name !== '?');
|
|
196
246
|
if (okSteps.length < minOk) {
|
|
@@ -200,13 +250,19 @@ export async function writeRunEndSkillCandidates(steps, source, minOk = 2) {
|
|
|
200
250
|
const body = `## 背景\n本轮对话连续成功调用了 ${okSteps.length} 个工具: ${toolNames}.\n\n` +
|
|
201
251
|
`## 流程\n${okSteps.map((s) => `1. 调用 ${s.name}${s.output ? ': ' + String(s.output).slice(0, 120) : ''}`).join('\n')}\n\n` +
|
|
202
252
|
`## 注意事项\n- 工具名以 list_skills / get_operation_logs 的实际注册名为准\n- 沉淀为正式 skill 前请人工确认流程可复用\n`;
|
|
203
|
-
|
|
253
|
+
// 2026-08-08: 稳定签名 + 固定文件名 → 同一套工具反复跑时合并更新到同一个候选 (runs++)
|
|
254
|
+
const signature = toolSignature(okSteps);
|
|
255
|
+
const candName = `auto-${signature}`;
|
|
256
|
+
const existing = (await listSkillCandidates()).find((x) => x.signature === signature || sanitizeSkillName(x.name) === sanitizeSkillName(candName));
|
|
204
257
|
const file = await writeSkillCandidate({
|
|
205
258
|
name: candName,
|
|
206
259
|
description: `自动候选: ${okSteps.length} 个工具连续成功 (${toolNames})`,
|
|
207
260
|
body,
|
|
208
261
|
source,
|
|
209
262
|
timestamp: new Date().toISOString(),
|
|
263
|
+
signature,
|
|
210
264
|
});
|
|
211
|
-
|
|
265
|
+
const merged = !!existing;
|
|
266
|
+
const runs = (existing?.runs ?? 0) + 1;
|
|
267
|
+
return { wrote: true, file, count: okSteps.length, names: toolNames, merged, runs };
|
|
212
268
|
}
|