更新 tool 的传入参数,指定传入关键字名称
This commit is contained in:
@@ -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`。
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user