feat(mcp): serve the Experience tools from the server MCP endpoint and generalize usage attribution - #3866
Draft
t0saki wants to merge 6 commits into
Draft
feat(mcp): serve the Experience tools from the server MCP endpoint and generalize usage attribution#3866t0saki wants to merge 6 commits into
t0saki wants to merge 6 commits into
Conversation
The Experience attribution pipeline compared bare tool names and required a plain JSON-object tool_output. Only Codex records that shape: Claude Code namespaces MCP tools as mcp__openviking__* and records the result as an MCP content-block array, opencode uses openviking_*. On any other harness both recall/injection events and trajectory lineage silently came out empty. Move the Experience URI predicate, tool-name normalization, and tool-output envelope parsing into openviking/core/experience.py so both the usage extractor and the lineage collector share them.
search_experience / read_experience were piloted as a JS localToolProvider shim inside the Codex plugin, so only Codex had them. Register them on the server's /mcp endpoint instead: every proxy-based harness picks them up with no plugin change, and the fixed name + JSON payload contract that Agent Evolution attribution depends on is now enforced in one place. The port is not literal in three places: - no score_threshold, unlike the neighbouring find/search tools which default to 0.35 and would drop the weaker half of the matches - URI ownership goes through canonical_experience_uri, closing the JS fallback that accepted any tenant's experiences dir when user was empty - invalid input raises InvalidArgumentError rather than returning an error string, so a failed read is not recorded as an injection Delete the JS implementation and its Codex wiring. The shared proxy's localToolProvider hook stays as a generic escape hatch.
… a skill loader The MCP tools now reach every proxy-based harness on their own, but the skill that teaches an agent when to call them only reached Codex. Register it in OpenClaw's skills[] (the files were already packaged, just never declared) and add it to Claude Code and Cursor. The three byte-identical copies were maintained by hand; sync.mjs now generates them from examples/skills/ and sync.test.mjs fails on drift. Skills are copied verbatim — a GENERATED FROM banner ahead of the --- frontmatter would break every skill loader. Also wire capture-utils.test.mjs into pr.yml; it guards the tool_output truncation path and was not running in CI.
…attribution Drives mcp.call_tool and replays both the content-block array and FastMCP's structuredContent wrapper through the usage extractor and lineage collector, so the payload the tools actually emit is what attribution is proven against.
…sses A lifecycle-oriented skill for the core memory tools (recall/find/search/ grep, read, remember/add_resource/forget), synced byte-identically from examples/skills to the codex, claude-code, and cursor plugins. Converges cursor's minimal openviking-memory variant onto the shared source. The openclaw plugin keeps its REST-specific operator skill instead.
An agent whose plugin updated before the server would otherwise see the tools missing and improvise with generic find/read, which never counts as experience recall or injection.
t0saki
marked this pull request as draft
August 7, 2026 10:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Moves the Agent Evolution tool pair
search_experience/read_experiencefrom the Codex plugin's local JS shim onto the server's/mcpendpoint, so every MCP-connected harness (claude-code, codex, cursor, opencode, zcode, trae, pi) gets the same tools under a single server-side contract. Alongside the move, usage attribution is generalized so recall/injection statistics survive every harness's tool-name namespacing and tool-output envelope, the Experience Memory skill is distributed to every harness with a skill loader, and a newopenviking-memoryusage skill teaches agents the core memory-tool lifecycle.Root cause of the attribution fix. Usage attribution (
openviking/usage_reporter/extractors.py,openviking/session/memory/experience_lineage.py) compared bare tool names exactly (part.tool_name == "search_experience") and expectedtool_outputto parse as a plain JSON dict. This worked only because the tools were a Codex-only pilot: Codex rollouts record bare tool names and the raw JSON string. Once other harnesses have the tools, Claude Code recordsmcp__openviking__search_experienceand an MCP content-block array, opencode recordsopenviking_search_experience, and some clients record FastMCP'sstructuredContentenvelope{"result": "<json>"}. Each of those silently zeroes the recall/injection statistics and trajectory lineage — no error, just empty/api/v1/agent-evolutiondata. The mismatch was introduced with the pilot itself: the extraction pipeline was written against the Codex rollout shape, and nothing normalized names or unwrapped envelopes because no other shape existed yet.Human Involvement
Related Issue
N/A
Type of Change
Changes Made
openviking/core/experience.pyleaf module with the shared attribution primitives:is_experience_uri_for_user,normalize_experience_tool_name(stripsmcp__<server>__/<server>_prefixes), andload_tool_output_mapping(unwraps MCP content-block arrays,structuredContent, and{"result": "<json>"}envelopes).experience_lineage.pyandusage_reporter/extractors.pyboth consume it; the module lives undercore/rather thansession/memory/because a top-level import ofopenviking.session.memoryfromextractors.pybreaksimport openviking.usage_reporterthrough the pre-existingsession → storage → serviceimport cycle.openviking/server/mcp_endpoint.pyregisterssearch_experienceandread_experience. Search is pinned to the authenticated user'sviking://user/memories/experiences/, applies no score threshold (the neighbouringfinddefault of 0.35 would silently drop weaker matches), clampslimitto[1, 20]in the body (a pydantic constraint would turn out-of-range values into tool errors), dedupes canonicalized URIs (each result row becomes onememory.recalledevent), zeroes non-finite scores (bareNaNbreaksJSON.parseclient-side), and truncates snippets to 120 chars (keeps a full result set under harnesstool_outputcaps).read_experiencerequires the canonical URI verbatim and raisesInvalidArgumentErrorinstead of returning error strings, so a rejected call surfaces asisErrorand is never counted as an injection.examples/codex-memory-plugin/servers/experience-tools.mjs(+ its test) and thelocalToolProviderwiring inmcp-proxy.mjs; the sharedmcp-proxy-core.mjshook stays as a generic escape hatch. The Python tools' JSON output is byte-identical to the deleted JS implementation (verifiedjson.dumpsvsJSON.stringify, CJK included).sync.mjsgainsSKILL_TARGETSwith a byte-equality test insync.test.mjs;ov-experience-memoryis registered in openclaw'sskills[]and shipped to claude-code/cursor; the marketplace staging script's required list covers the new paths;SKILL.mdis rewritten for the server-side semantics (version 2026.8.7).examples/skills/openviking-memoryusage skill covering the session lifecycle (injected<openviking-context>first, retrieval-tool selection, deliberate writes, automatic capture/extraction,ovCLI pointer), synced to codex/claude-code/cursor; cursor's previous minimal variant converges onto the shared source, and openclaw keeps its REST-specificopenviking-context-databaseskill.capture-utils.test.mjsadded topr.yml(guards the tool-output truncation path attribution depends on).Testing
I have added tests that prove my fix is effective or that my feature works
New and existing unit tests pass locally with my changes
I have tested this on the following platforms:
tests/server/test_mcp_experience_tools.py: attribution parametrized across five recorded harness shapes (codex bare, claude-codemcp__+ content-block array, opencodeopenviking_, openclaw bare,structuredContent), envelope/name-normalization unit cases, and an end-to-end round trip that drives the realmcp.call_tooldispatch and replays its output throughMemoryUsageExtractorandcollect_read_experience_uris.tests/server/test_mcp_endpoint.py: target-URI pinning, absentscore_threshold, limit clamping, cross-user/sidecar/aliased-URI filtering, CJK percent-decoded titles, snippet fallback and truncation, non-finite score handling, and 12 rejected-URI cases asserting storage is never touched.pytest tests/server/green (128 passed); the 3 failures intests/unit/session/memory/test_extract_loop_match_text.pyreproduce identically onmain(pre-existing).node --testover the fullpr.ymlsuite: 249 passed. openclawvitest: 750 passed; the 4 failures inarchitecture-boundaries.test.tsalso fail onmainagainst untouched sources.ruff format --checkandruff checkclean on all changed Python files.Checklist
Screenshots (if applicable)
N/A
Additional Notes
benchmark/tau2/train/rollout_executor_vikingbot.pyregisters same-named tools with different parameters and output shapes (read_experience(experience_uri),search_experiencereturningcandidatesovermemories/cases). It produces no attribution statistics today and this PR does not change that; to be reconciled in a follow-up.examples/openclaw-plugin/plugin/openviking-experience-tools.tsisCanonicalExperienceUridegrades to accepting any tenant's experiences directory whenuseris empty — the same flaw the deleted JS shim had. Left for a follow-up since the openclaw REST path is out of scope here.