更新 tool 的传入参数,指定传入关键字名称

This commit is contained in:
2026-05-11 18:14:57 +08:00
parent cbe13dd1df
commit 0dcb04ee89
3 changed files with 38 additions and 103 deletions
+2 -1
View File
@@ -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`
+16 -54
View File
@@ -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,
+20 -48
View File
@@ -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;
}