mirror of
https://github.com/volcengine/OpenViking.git
synced 2026-09-28 11:43:00 +08:00
feat(plugins): recall account-shared skills and flag skill entries
The server context face already mixes both skill roots into per-prompt recall, but the plugins' last-resort ranked find only searched viking://~/memories and viking://~/skills, so on servers without the context face a shared skill in viking://agent/skills could never be recalled. Add it as a third source, and name each skill hit by its directory rather than the .abstract.md it was indexed through, matching the context face and the session-start catalog. When an injected recall block carries a skill (a type="skills" entry, or a [skill] line from the fallback), its header gains one line telling the agent to read the skill's SKILL.md before following it. Turns without a skill are unchanged. The openclaw plugin's vendored recall-core copy is regenerated with it. Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28
This commit is contained in:
@@ -16,7 +16,10 @@ const USER_RESERVED_DIRS = new Set(["memories", "skills"]);
|
||||
const SOURCES = [
|
||||
{ type: "memory", uri: "viking://~/memories", bucket: "memories" },
|
||||
{ type: "skill", uri: "viking://~/skills", bucket: "skills" },
|
||||
{ type: "skill", uri: "viking://agent/skills", bucket: "skills" },
|
||||
];
|
||||
const SKILL_ENTRY_HINT =
|
||||
"Skill entries are OpenViking skills: read SKILL.md under the entry's URI before following one.";
|
||||
const DEFAULT_CONTEXT_LIMIT = 10;
|
||||
const DEFAULT_CONTEXT_MAX_TOKENS = 1600;
|
||||
const DEFAULT_REWRITE_MAX_BULLETS = 6;
|
||||
@@ -318,7 +321,15 @@ async function searchOneSource(fetchJSON, query, source, limit, actorPeerId = ""
|
||||
}, { actorPeerId });
|
||||
if (!res.ok) return [];
|
||||
const items = res.result?.[source.bucket] || [];
|
||||
return items.map((item) => ({ ...item, _sourceType: source.type }));
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
// A skill hit is its directory's .abstract.md; name the skill directory, as
|
||||
// the context face and the session-start catalog do.
|
||||
...(source.type === "skill" && typeof item.uri === "string"
|
||||
? { uri: item.uri.replace(/\/\.(?:abstract|overview)\.md$/, "") }
|
||||
: {}),
|
||||
_sourceType: source.type,
|
||||
}));
|
||||
}
|
||||
|
||||
async function searchAllSources(fetchJSON, query, perSourceLimit, actorPeerId = "", log = () => {}) {
|
||||
@@ -366,6 +377,7 @@ async function buildFallbackInjectionBlock(fetchJSON, items, cfg, actorPeerId =
|
||||
"<openviking-context>",
|
||||
"Relevant context from OpenViking. Use the read MCP tool to expand URIs.",
|
||||
];
|
||||
if (items.some((item) => item._sourceType === "skill")) lines.push(SKILL_ENTRY_HINT);
|
||||
let contentCount = 0;
|
||||
let hintCount = 0;
|
||||
|
||||
@@ -472,6 +484,7 @@ function wrapContext(body) {
|
||||
return [
|
||||
"<openviking-context>",
|
||||
"Relevant memory from OpenViking. Use the search/read MCP tools to expand URIs.",
|
||||
...(/\btype="skills"/.test(body) ? [SKILL_ENTRY_HINT] : []),
|
||||
body,
|
||||
"</openviking-context>",
|
||||
].join("\n");
|
||||
|
||||
+14
-1
@@ -16,7 +16,10 @@ const USER_RESERVED_DIRS = new Set(["memories", "skills"]);
|
||||
const SOURCES = [
|
||||
{ type: "memory", uri: "viking://~/memories", bucket: "memories" },
|
||||
{ type: "skill", uri: "viking://~/skills", bucket: "skills" },
|
||||
{ type: "skill", uri: "viking://agent/skills", bucket: "skills" },
|
||||
];
|
||||
const SKILL_ENTRY_HINT =
|
||||
"Skill entries are OpenViking skills: read SKILL.md under the entry's URI before following one.";
|
||||
const DEFAULT_CONTEXT_LIMIT = 10;
|
||||
const DEFAULT_CONTEXT_MAX_TOKENS = 1600;
|
||||
const DEFAULT_REWRITE_MAX_BULLETS = 6;
|
||||
@@ -318,7 +321,15 @@ async function searchOneSource(fetchJSON, query, source, limit, actorPeerId = ""
|
||||
}, { actorPeerId });
|
||||
if (!res.ok) return [];
|
||||
const items = res.result?.[source.bucket] || [];
|
||||
return items.map((item) => ({ ...item, _sourceType: source.type }));
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
// A skill hit is its directory's .abstract.md; name the skill directory, as
|
||||
// the context face and the session-start catalog do.
|
||||
...(source.type === "skill" && typeof item.uri === "string"
|
||||
? { uri: item.uri.replace(/\/\.(?:abstract|overview)\.md$/, "") }
|
||||
: {}),
|
||||
_sourceType: source.type,
|
||||
}));
|
||||
}
|
||||
|
||||
async function searchAllSources(fetchJSON, query, perSourceLimit, actorPeerId = "", log = () => {}) {
|
||||
@@ -366,6 +377,7 @@ async function buildFallbackInjectionBlock(fetchJSON, items, cfg, actorPeerId =
|
||||
"<openviking-context>",
|
||||
"Relevant context from OpenViking. Use the read MCP tool to expand URIs.",
|
||||
];
|
||||
if (items.some((item) => item._sourceType === "skill")) lines.push(SKILL_ENTRY_HINT);
|
||||
let contentCount = 0;
|
||||
let hintCount = 0;
|
||||
|
||||
@@ -472,6 +484,7 @@ function wrapContext(body) {
|
||||
return [
|
||||
"<openviking-context>",
|
||||
"Relevant memory from OpenViking. Use the search/read MCP tools to expand URIs.",
|
||||
...(/\btype="skills"/.test(body) ? [SKILL_ENTRY_HINT] : []),
|
||||
body,
|
||||
"</openviking-context>",
|
||||
].join("\n");
|
||||
|
||||
@@ -15,7 +15,10 @@ const USER_RESERVED_DIRS = new Set(["memories", "skills"]);
|
||||
const SOURCES = [
|
||||
{ type: "memory", uri: "viking://~/memories", bucket: "memories" },
|
||||
{ type: "skill", uri: "viking://~/skills", bucket: "skills" },
|
||||
{ type: "skill", uri: "viking://agent/skills", bucket: "skills" },
|
||||
];
|
||||
const SKILL_ENTRY_HINT =
|
||||
"Skill entries are OpenViking skills: read SKILL.md under the entry's URI before following one.";
|
||||
const DEFAULT_CONTEXT_LIMIT = 10;
|
||||
const DEFAULT_CONTEXT_MAX_TOKENS = 1600;
|
||||
const DEFAULT_REWRITE_MAX_BULLETS = 6;
|
||||
@@ -317,7 +320,15 @@ async function searchOneSource(fetchJSON, query, source, limit, actorPeerId = ""
|
||||
}, { actorPeerId });
|
||||
if (!res.ok) return [];
|
||||
const items = res.result?.[source.bucket] || [];
|
||||
return items.map((item) => ({ ...item, _sourceType: source.type }));
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
// A skill hit is its directory's .abstract.md; name the skill directory, as
|
||||
// the context face and the session-start catalog do.
|
||||
...(source.type === "skill" && typeof item.uri === "string"
|
||||
? { uri: item.uri.replace(/\/\.(?:abstract|overview)\.md$/, "") }
|
||||
: {}),
|
||||
_sourceType: source.type,
|
||||
}));
|
||||
}
|
||||
|
||||
async function searchAllSources(fetchJSON, query, perSourceLimit, actorPeerId = "", log = () => {}) {
|
||||
@@ -365,6 +376,7 @@ async function buildFallbackInjectionBlock(fetchJSON, items, cfg, actorPeerId =
|
||||
"<openviking-context>",
|
||||
"Relevant context from OpenViking. Use the read MCP tool to expand URIs.",
|
||||
];
|
||||
if (items.some((item) => item._sourceType === "skill")) lines.push(SKILL_ENTRY_HINT);
|
||||
let contentCount = 0;
|
||||
let hintCount = 0;
|
||||
|
||||
@@ -471,6 +483,7 @@ function wrapContext(body) {
|
||||
return [
|
||||
"<openviking-context>",
|
||||
"Relevant memory from OpenViking. Use the search/read MCP tools to expand URIs.",
|
||||
...(/\btype="skills"/.test(body) ? [SKILL_ENTRY_HINT] : []),
|
||||
body,
|
||||
"</openviking-context>",
|
||||
].join("\n");
|
||||
|
||||
@@ -469,3 +469,52 @@ test("an empty recall says whether the server had nothing or the threshold took
|
||||
assert.equal(belowThreshold.stage, "filtered_out");
|
||||
assert.equal(await buildRecallBlock(fallbackFetch([]), {}, "hello", { legacyCachePath }), null);
|
||||
});
|
||||
|
||||
test("fallback recall also searches the shared skill root and flags skill entries", async () => {
|
||||
const legacyCachePath = await tempPath("context-face.json");
|
||||
const targets = [];
|
||||
const fetchJSON = async (path, init) => {
|
||||
if (path === "/api/v1/search/search") return { ok: false, status: 503 };
|
||||
if (path === "/api/v1/search/recall") return { ok: false, status: 404 };
|
||||
if (path === "/api/v1/search/find") {
|
||||
const body = JSON.parse(init.body);
|
||||
targets.push(body.target_uri);
|
||||
const skills = body.target_uri === "viking://agent/skills"
|
||||
? [{ uri: "viking://agent/skills/deploy-runbook/.abstract.md", score: 0.8, abstract: "name: deploy-runbook", level: 0 }]
|
||||
: [];
|
||||
return { ok: true, result: { memories: [], skills } };
|
||||
}
|
||||
return { ok: false, status: 404 };
|
||||
};
|
||||
|
||||
const events = [];
|
||||
const block = await buildRecallBlock(fetchJSON, {
|
||||
recallLimit: 3,
|
||||
recallPreferAbstract: true,
|
||||
scoreThreshold: 0.35,
|
||||
}, "how do we deploy the payments service", {
|
||||
legacyCachePath,
|
||||
log: (event, data) => events.push({ event, data }),
|
||||
});
|
||||
|
||||
assert.deepEqual(targets.sort(), ["viking://agent/skills", "viking://~/memories", "viking://~/skills"]);
|
||||
assert.match(block, /\[skill 80%\] name: deploy-runbook/);
|
||||
assert.match(block, /Skill entries are OpenViking skills/);
|
||||
// The hit arrives as the skill's .abstract.md; the entry names the skill directory.
|
||||
const picked = events.find((e) => e.event === "recall_picked").data.items;
|
||||
assert.deepEqual(picked.map((item) => item.uri), ["viking://agent/skills/deploy-runbook"]);
|
||||
});
|
||||
|
||||
test("the skill hint appears only when the assembled context carries a skill", async () => {
|
||||
for (const [type, expected] of [["skills", true], ["events", false]]) {
|
||||
const block = await buildRecallBlock(async () => ({
|
||||
ok: true,
|
||||
result: {
|
||||
rendered: `<memory uri="viking://agent/skills/deploy-runbook" type="${type}">body</memory>`,
|
||||
entries: [],
|
||||
stats: { rewrite: "off" },
|
||||
},
|
||||
}), {}, "deploy the payments service", { legacyCachePath: await tempPath("context-face.json") });
|
||||
assert.equal(/Skill entries are OpenViking skills/.test(block), expected, type);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,7 +16,10 @@ const USER_RESERVED_DIRS = new Set(["memories", "skills"]);
|
||||
const SOURCES = [
|
||||
{ type: "memory", uri: "viking://~/memories", bucket: "memories" },
|
||||
{ type: "skill", uri: "viking://~/skills", bucket: "skills" },
|
||||
{ type: "skill", uri: "viking://agent/skills", bucket: "skills" },
|
||||
];
|
||||
const SKILL_ENTRY_HINT =
|
||||
"Skill entries are OpenViking skills: read SKILL.md under the entry's URI before following one.";
|
||||
const DEFAULT_CONTEXT_LIMIT = 10;
|
||||
const DEFAULT_CONTEXT_MAX_TOKENS = 1600;
|
||||
const DEFAULT_REWRITE_MAX_BULLETS = 6;
|
||||
@@ -318,7 +321,15 @@ async function searchOneSource(fetchJSON, query, source, limit, actorPeerId = ""
|
||||
}, { actorPeerId });
|
||||
if (!res.ok) return [];
|
||||
const items = res.result?.[source.bucket] || [];
|
||||
return items.map((item) => ({ ...item, _sourceType: source.type }));
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
// A skill hit is its directory's .abstract.md; name the skill directory, as
|
||||
// the context face and the session-start catalog do.
|
||||
...(source.type === "skill" && typeof item.uri === "string"
|
||||
? { uri: item.uri.replace(/\/\.(?:abstract|overview)\.md$/, "") }
|
||||
: {}),
|
||||
_sourceType: source.type,
|
||||
}));
|
||||
}
|
||||
|
||||
async function searchAllSources(fetchJSON, query, perSourceLimit, actorPeerId = "", log = () => {}) {
|
||||
@@ -366,6 +377,7 @@ async function buildFallbackInjectionBlock(fetchJSON, items, cfg, actorPeerId =
|
||||
"<openviking-context>",
|
||||
"Relevant context from OpenViking. Use the read MCP tool to expand URIs.",
|
||||
];
|
||||
if (items.some((item) => item._sourceType === "skill")) lines.push(SKILL_ENTRY_HINT);
|
||||
let contentCount = 0;
|
||||
let hintCount = 0;
|
||||
|
||||
@@ -472,6 +484,7 @@ function wrapContext(body) {
|
||||
return [
|
||||
"<openviking-context>",
|
||||
"Relevant memory from OpenViking. Use the search/read MCP tools to expand URIs.",
|
||||
...(/\btype="skills"/.test(body) ? [SKILL_ENTRY_HINT] : []),
|
||||
body,
|
||||
"</openviking-context>",
|
||||
].join("\n");
|
||||
|
||||
Reference in New Issue
Block a user