From 0dcb04ee890558abc10df926826b7c30393d5d10 Mon Sep 17 00:00:00 2001 From: Huarch Date: Mon, 11 May 2026 18:14:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20tool=20=E7=9A=84=E4=BC=A0?= =?UTF-8?q?=E5=85=A5=E5=8F=82=E6=95=B0=EF=BC=8C=E6=8C=87=E5=AE=9A=E4=BC=A0?= =?UTF-8?q?=E5=85=A5=E5=85=B3=E9=94=AE=E5=AD=97=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .opencode/agents/instruction.md | 3 +- .opencode/tools/memory_manager.ts | 70 +++++++------------------------ .opencode/tools/skill_manager.ts | 68 +++++++++--------------------- 3 files changed, 38 insertions(+), 103 deletions(-) diff --git a/.opencode/agents/instruction.md b/.opencode/agents/instruction.md index 5ab752f..98ee4bf 100644 --- a/.opencode/agents/instruction.md +++ b/.opencode/agents/instruction.md @@ -18,7 +18,8 @@ temperature: 0.2 8. 每次按需加载技能(skills)前,先明确说明加载理由,并只加载与当前任务直接相关的最小技能集合。 9. 当 `dynamic_http_call` 返回 `result_mode = referenced` 和 `result_ref` 时,说明当前只拿到了预览;如果后续推理仍需要完整结果,必须调用 `fetch_result_ref` 回读,不能把 preview 当成完整数据。 10. 当且仅当出现**长期有效且高价值**的信号时,才允许调用在线学习工具: - - `memory_manager`:用户明确长期偏好/约束,或当前项目/环境的稳定事实 + - `memory_manager`:用户明确长期偏好/约束,或当前项目/环境的稳定事实。参数规范优先使用: + - `scope`: `user` 或 `workspace` - `skill_manager`:已经被证明有效且可复用的 workflow / 方法模式;由您自己判断应写入 `.opencode/skills` 树中的哪个 skill 位置 11. 不要把一次性问题、临时上下文、未经验证的猜测写入任何学习工具。 12. 严禁把 token、password、secret、API key、system prompt、隐私数据写入 `memory_manager` 或 `skill_manager`。 diff --git a/.opencode/tools/memory_manager.ts b/.opencode/tools/memory_manager.ts index b52644b..213f051 100644 --- a/.opencode/tools/memory_manager.ts +++ b/.opencode/tools/memory_manager.ts @@ -9,50 +9,23 @@ const initializePromise = Promise.all([ toolContextStore.initialize(), ]); -const MEMORY_SIGNAL_TYPES = new Set([ - "user_preference", - "user_constraint", - "project_fact", - "environment_fact", - "agent_correction", -]); - -const isSignalAllowedForScope = (scope: string, signalType: string) => { - if (!MEMORY_SIGNAL_TYPES.has(signalType)) { - return false; - } - if (scope === "user") { - return signalType === "user_preference" || signalType === "user_constraint"; - } - if (scope === "workspace") { - return ( - signalType === "project_fact" || - signalType === "environment_fact" || - signalType === "agent_correction" - ); - } - return false; -}; - export default tool({ description: - "将高置信度、长期有效的用户偏好或项目事实写入持久 memory。禁止写入 token、password、secret、system prompt 或一次性上下文。", + "将长期有效的用户偏好或项目事实写入持久 memory。禁止写入 token、password、secret、system prompt 或一次性上下文。scope 仅允许 'user' 或 'workspace'。", args: { reason: tool.schema .string() .describe("Why this memory should be persisted for future requests."), scope: tool.schema .string() - .describe("Target memory scope: 'user' for user preferences, 'workspace' for project/environment facts."), - signal_type: tool.schema - .string() - .describe("Signal type, e.g. user_preference, user_constraint, project_fact, environment_fact."), - confidence: tool.schema - .number() - .describe("Confidence between 0 and 1. Only high-confidence memories should be persisted."), + .describe( + "Required exact keyword. Use only 'user' for user-level durable preferences/constraints, or 'workspace' for project/environment durable facts. Do not use 'project', Chinese labels, or any alias.", + ), content: tool.schema .string() - .describe("The durable fact or preference to remember, written as one concise sentence."), + .describe( + "The durable fact or preference to remember, written as one concise sentence.", + ), }, async execute(args, context) { await initializePromise; @@ -60,34 +33,23 @@ export default tool({ if (!sessionContext) { throw new Error(`session context not found for ${context.sessionID}`); } - if (!isSignalAllowedForScope(args.scope, args.signal_type)) { - return JSON.stringify({ - ok: true, - kind: "memory", - decision: "rejected", - detail: `signal_type ${args.signal_type} is not allowed for scope ${args.scope}`, - }); - } - if (args.confidence < 0.8) { - return JSON.stringify({ - ok: true, - kind: "memory", - decision: "rejected", - detail: "confidence below memory threshold", - }); - } - - const scope = args.scope === "user" ? "user" : args.scope === "workspace" ? "workspace" : null; + const scope = + args.scope === "user" + ? "user" + : args.scope === "workspace" + ? "workspace" + : null; if (!scope) { return JSON.stringify({ ok: true, kind: "memory", decision: "rejected", - detail: `unsupported scope: ${args.scope}`, + detail: `unsupported scope: ${args.scope}; use exact keyword 'user' or 'workspace'`, }); } - const scopeKey = scope === "user" ? sessionContext.actorKey : sessionContext.projectKey; + const scopeKey = + scope === "user" ? sessionContext.actorKey : sessionContext.projectKey; const result = await memoryStore.upsert(scope, scopeKey, { content: args.content, sessionId: context.sessionID, diff --git a/.opencode/tools/skill_manager.ts b/.opencode/tools/skill_manager.ts index 9b181aa..e81554a 100644 --- a/.opencode/tools/skill_manager.ts +++ b/.opencode/tools/skill_manager.ts @@ -2,7 +2,6 @@ import { tool } from "@opencode-ai/plugin"; import { join, posix } from "node:path"; import { config } from "../../src/config.js"; -import { ResultReferenceStore } from "../../src/results/store.js"; import { ToolSessionContextStore } from "../../src/session/toolContextStore.js"; import { atomicWriteFileWithHistory, @@ -11,12 +10,8 @@ import { } from "../../src/utils/fileStore.js"; import { sanitizePersistentLine } from "../../src/utils/persistencePolicy.js"; -const resultStore = new ResultReferenceStore(); const toolContextStore = new ToolSessionContextStore(); -const initializePromise = Promise.all([ - resultStore.initialize(), - toolContextStore.initialize(), -]); +const initializePromise = toolContextStore.initialize(); const SKILLS_ROOT_DIR = ".opencode/skills"; // learned skill 与正式技能树同路径组织,但历史版本单独落到 data/history/skills 下。 const SKILLS_HISTORY_DIR = join(config.PERSISTENCE_HISTORY_DIR, "skills"); @@ -29,23 +24,14 @@ export default tool({ args: { reason: tool.schema .string() - .describe("Why this workflow or method should be learned for future reuse."), + .describe( + "The reusable workflow or method pattern to persist for future reuse, written as one concise sentence.", + ), skill_path: tool.schema .string() - .describe("Target skill directory path relative to .opencode/skills, for example analytics/simulation-analysis/leakage or platform/governance-observability/meta."), - pattern: tool.schema - .string() - .describe("A reusable workflow pattern written as one concise bullet-like sentence."), - signal_type: tool.schema - .string() - .describe("Signal type, e.g. validated_workflow, successful_complex_convergence, analysis_method, tool_recovery_pattern."), - confidence: tool.schema - .number() - .describe("Confidence between 0 and 1. Only very high-confidence patterns are stored as learned skills."), - result_refs: tool.schema - .array(tool.schema.string()) - .optional() - .describe("Optional authorized result_ref list used only for evidence validation before persisting the skill."), + .describe( + "Target skill directory path relative to .opencode/skills, for example analytics/simulation-analysis/leakage or platform/governance-observability/meta.", + ), }, async execute(args, context) { await initializePromise; @@ -59,39 +45,19 @@ export default tool({ ok: true, kind: "skill", decision: "rejected", - detail: "invalid skill_path; expected a relative path under .opencode/skills", + detail: + "invalid skill_path; expected a relative path under .opencode/skills", }); } - const pattern = sanitizePersistentLine(args.pattern, 320); + const pattern = sanitizePersistentLine(args.reason, 320); if (!pattern) { return JSON.stringify({ ok: true, kind: "skill", decision: "rejected", - detail: "pattern rejected by persistence policy", + detail: "reason rejected by persistence policy", }); } - if (args.confidence < 0.85) { - return JSON.stringify({ - ok: true, - kind: "skill", - decision: "rejected", - detail: "only very high-confidence patterns can be stored as skills", - }); - } - if (args.result_refs?.length) { - await Promise.all( - args.result_refs.map(async (resultRef) => { - const record = await resultStore.peekAuthorized(resultRef, { - actorKey: sessionContext.actorKey, - projectId: sessionContext.projectId, - }); - if (!record) { - throw new Error(`unauthorized or missing result_ref: ${resultRef}`); - } - }), - ); - } const result = await appendLearnedSkillPattern(skillPath, pattern); return JSON.stringify({ @@ -104,10 +70,14 @@ export default tool({ }, }); -const appendLearnedSkillPattern = async (skillPath: string, pattern: string) => { +const appendLearnedSkillPattern = async ( + skillPath: string, + pattern: string, +) => { return serializeWrite(async () => { const target = join(SKILLS_ROOT_DIR, skillPath, "SKILL.md"); - const current = (await readTextFile(target)) ?? defaultLearnedSkill(skillPath); + const current = + (await readTextFile(target)) ?? defaultLearnedSkill(skillPath); const existingPatterns = extractLearnedPatterns(current); if (existingPatterns.includes(pattern)) { return { changed: false, target }; @@ -155,7 +125,9 @@ version: 1.0.0 `; const normalizeSkillPath = (rawSkillPath: string) => { - const normalized = posix.normalize(rawSkillPath.trim().replace(/^\/+|\/+$/g, "")); + const normalized = posix.normalize( + rawSkillPath.trim().replace(/^\/+|\/+$/g, ""), + ); if (!normalized || normalized === "." || normalized.startsWith("..")) { return null; }