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

This commit is contained in:
2026-05-11 18:14:57 +08:00
parent 59de5c672f
commit 0d5435022a
3 changed files with 38 additions and 103 deletions
+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;
}