Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 54 additions & 11 deletions packages/adapter-hermes/hermes-plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ def _scoped_session_id(raw_session_id: str, config: Optional[dict] = None) -> st
"type": "string",
"description": "Optional Working Memory owner address. Defaults to this node when view is working-memory.",
},
"sub_graph_name": {
"type": "string",
"description": "Optional sub-graph scope within context_graph_id. May be combined with view.",
},
"assertion_name": {
"type": "string",
"description": "Optional assertion name scope.",
Expand Down Expand Up @@ -804,6 +808,10 @@ def _scoped_session_id(raw_session_id: str, config: Optional[dict] = None) -> st
"query": {"type": "string", "description": "Free-text search query."},
"limit": {"type": "integer", "description": "Max results, default 20, capped at 100."},
"context_graph_id": {"type": "string", "description": "Optional context graph override. " + EXISTING_CONTEXT_GRAPH_ID_DESCRIPTION},
"sub_graph_name": {
"type": "string",
"description": "Optional project sub-graph scope. Requires a project context graph.",
},
},
"required": ["query"],
},
Expand Down Expand Up @@ -1390,10 +1398,14 @@ def _handle_query(self, args: Dict[str, Any]) -> str:
view = _first_text(args, "view")
if view and view not in ("working-memory", "shared-working-memory", "verified-memory"):
return tool_error('"view" must be one of: working-memory, shared-working-memory, verified-memory.')
if view and not _first_text(args, "context_graph_id"):
if view and not cg:
Comment thread
Jurij89 marked this conversation as resolved.
Outdated
return tool_error(f'"view: {view}" requires "context_graph_id".')
if view and _first_text(args, "sub_graph_name"):
return tool_error('"sub_graph_name" cannot be combined with view-based dkg_query routing.')
if args.get("sub_graph_name") is not None and not isinstance(args.get("sub_graph_name"), str):
return tool_error('"sub_graph_name" must be a string.')
if isinstance(args.get("sub_graph_name"), str) and not args.get("sub_graph_name", "").strip():
return tool_error('"sub_graph_name" must be a non-empty string.')
Comment thread
Jurij89 marked this conversation as resolved.
if _first_text(args, "sub_graph_name") and not cg:
return tool_error('"sub_graph_name" requires "context_graph_id" or a configured default context graph.')
if args.get("agent_address") is not None and not isinstance(args.get("agent_address"), str):
return tool_error('"agent_address" must be a string.')
if isinstance(args.get("agent_address"), str) and not args.get("agent_address", "").strip():
Expand Down Expand Up @@ -1423,6 +1435,16 @@ def _handle_memory_search(self, args: Dict[str, Any]) -> str:
if len(query) < 2:
return tool_error("query must be at least 2 characters.")
limit = _coerce_limit(args.get("limit"), default=20, maximum=100)
if "context_graph" in args:
return tool_error('"context_graph" is not a supported parameter on memory_search. Use "context_graph_id".')
project_context_graph = _first_text(args, "context_graph_id") or self._context_graph
if args.get("sub_graph_name") is not None and not isinstance(args.get("sub_graph_name"), str):
return tool_error('"sub_graph_name" must be a string.')
if isinstance(args.get("sub_graph_name"), str) and not args.get("sub_graph_name", "").strip():
return tool_error('"sub_graph_name" must be a non-empty string.')
project_sub_graph_name = _first_text(args, "sub_graph_name")
if project_sub_graph_name and (not project_context_graph or project_context_graph == "agent-context"):
return tool_error('"sub_graph_name" requires a project context graph for memory_search.')
if self._offline or not self._client:
return json.dumps(_cache_memory_search(query, self._cache, limit))

Expand All @@ -1432,9 +1454,6 @@ def _handle_memory_search(self, args: Dict[str, Any]) -> str:

sparql = _build_memory_search_sparql(keywords, limit)
agent_address = self._client._resolve_agent_address()
if "context_graph" in args:
return tool_error('"context_graph" is not a supported parameter on memory_search. Use "context_graph_id".')
project_context_graph = _first_text(args, "context_graph_id") or self._context_graph
context_graphs: List[str] = []
for cg in ("agent-context", project_context_graph):
if cg and cg not in context_graphs:
Expand All @@ -1450,13 +1469,37 @@ def _handle_memory_search(self, args: Dict[str, Any]) -> str:
):
if view == "working-memory" and not agent_address:
continue
result = self._client.query(
sparql,
cg,
view=view,
agent_address=agent_address if view == "working-memory" else None,
scoped_project_layer = bool(
project_sub_graph_name
and cg == project_context_graph
and cg != "agent-context"
)
query_kwargs = {
"view": view,
"agent_address": agent_address if view == "working-memory" else None,
}
if scoped_project_layer:
query_kwargs["sub_graph_name"] = project_sub_graph_name
try:
result = self._client.query(
sparql,
cg,
**query_kwargs,
)
except Exception as e:
if scoped_project_layer:
return tool_error(
f'memory_search sub_graph_name "{project_sub_graph_name}" failed for '
f'context_graph_id "{cg}" ({view}): {e}'
)
raise
if _client_result_failed(result):
if scoped_project_layer:
detail = result.get("error") if isinstance(result, dict) else "query failed"
return tool_error(
f'memory_search sub_graph_name "{project_sub_graph_name}" failed for '
f'context_graph_id "{cg}" ({view}): {detail}'
)
continue
successful_queries += 1
for binding in _extract_query_bindings(result):
Expand Down
83 changes: 80 additions & 3 deletions packages/adapter-hermes/test/hermes-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2307,9 +2307,10 @@ subscribe_schema = next(schema for schema in provider.get_tool_schemas() if sche
assert "include_shared_memory" in subscribe_schema["parameters"]["properties"], subscribe_schema
search_schema = next(schema for schema in provider.get_tool_schemas() if schema["name"] == "memory_search")
assert "context_graph_id" in search_schema["parameters"]["properties"], search_schema
assert "sub_graph_name" in search_schema["parameters"]["properties"], search_schema
assert "context_graph" not in search_schema["parameters"]["properties"], search_schema
query_schema = next(schema for schema in provider.get_tool_schemas() if schema["name"] == "dkg_query")
assert "sub_graph_name" not in query_schema["parameters"]["properties"], query_schema
assert "sub_graph_name" in query_schema["parameters"]["properties"], query_schema
share_schema = next(schema for schema in provider.get_tool_schemas() if schema["name"] == "dkg_share")
assert "context_graph_id" in share_schema["parameters"]["properties"], share_schema
assert "context_graph" not in share_schema["parameters"]["properties"], share_schema
Expand Down Expand Up @@ -2758,13 +2759,27 @@ for args, needle in [
({"sparql": "ASK {}", "include_shared_memory": True}, "include_shared_memory"),
({"sparql": "ASK {}", "context_graph": "old"}, "context_graph"),
({"sparql": "ASK {}", "context_graph_id": "cg:test", "view": "bad"}, "view"),
({"sparql": "ASK {}", "view": "working-memory"}, "context_graph_id"),
({"sparql": "ASK {}", "context_graph_id": "cg:test", "view": "shared-working-memory", "sub_graph_name": "scratch"}, "sub_graph_name"),
({"sparql": "ASK {}", "context_graph_id": "cg:test", "sub_graph_name": 42}, "sub_graph_name"),
({"sparql": "ASK {}", "context_graph_id": "cg:test", "view": "working-memory", "agent_address": " "}, "agent_address"),
]:
result = json.loads(provider.handle_tool_call("dkg_query", args))
assert needle in result["error"], (args, result)

provider_without_default = module.DKGMemoryProvider()
provider_without_default._offline = False
provider_without_default._context_graph = None
provider_without_default._client = QueryClient()
missing_view_cg = json.loads(provider_without_default.handle_tool_call("dkg_query", {
"sparql": "ASK {}",
"view": "working-memory",
}))
missing_sub_cg = json.loads(provider_without_default.handle_tool_call("dkg_query", {
"sparql": "ASK {}",
"sub_graph_name": "scratch",
}))
assert "context_graph_id" in missing_view_cg["error"], missing_view_cg
assert "sub_graph_name" in missing_sub_cg["error"] and "context_graph_id" in missing_sub_cg["error"], missing_sub_cg

result = json.loads(provider.handle_tool_call("dkg_query", {
"sparql": "ASK {}",
"context_graph_id": "cg:test",
Expand All @@ -2782,6 +2797,24 @@ result = json.loads(provider.handle_tool_call("dkg_query", {
assert result["ok"] is True, result
assert provider._client.queries[-1][2]["agent_address"] == "peer-default", provider._client.queries

result = json.loads(provider.handle_tool_call("dkg_query", {
"sparql": "ASK {}",
"context_graph_id": "cg:test",
"view": "shared-working-memory",
"sub_graph_name": "scratch",
}))
assert result["ok"] is True, result
assert provider._client.queries[-1][2]["sub_graph_name"] == "scratch", provider._client.queries

result = json.loads(provider.handle_tool_call("dkg_query", {
"sparql": "ASK {}",
"view": "shared-working-memory",
"sub_graph_name": "scratch",
}))
assert result["ok"] is True, result
assert provider._client.queries[-1][1] == "default-cg", provider._client.queries
assert provider._client.queries[-1][2]["sub_graph_name"] == "scratch", provider._client.queries

class ReadMarkdownClient:
def __init__(self):
self.calls = []
Expand Down Expand Up @@ -3108,12 +3141,15 @@ spec.loader.exec_module(module)
class FakeClient:
def __init__(self):
self.calls = []
self.fail_scoped_project = False

def _resolve_agent_address(self):
return "0xAgent"

def query(self, sparql, context_graph_id, **kwargs):
self.calls.append((context_graph_id, kwargs))
if self.fail_scoped_project and kwargs.get("sub_graph_name"):
return {"error": "Unknown sub-graph: skills"}
return {
"result": {
"bindings": [{
Expand Down Expand Up @@ -3156,6 +3192,47 @@ assert provider._client.calls == [
("project-cg", {"view": "shared-working-memory", "agent_address": None}),
("project-cg", {"view": "verified-memory", "agent_address": None}),
], provider._client.calls

provider._client.calls = []
scoped = json.loads(provider.handle_tool_call("memory_search", {
"query": "alpha beta",
"limit": 10,
"sub_graph_name": "skills",
}))
assert scoped["scope"] == "project-cg", scoped
assert provider._client.calls == [
("agent-context", {"view": "working-memory", "agent_address": "0xAgent"}),
("agent-context", {"view": "shared-working-memory", "agent_address": None}),
("agent-context", {"view": "verified-memory", "agent_address": None}),
("project-cg", {"view": "working-memory", "agent_address": "0xAgent", "sub_graph_name": "skills"}),
("project-cg", {"view": "shared-working-memory", "agent_address": None, "sub_graph_name": "skills"}),
("project-cg", {"view": "verified-memory", "agent_address": None, "sub_graph_name": "skills"}),
], provider._client.calls

provider._client.calls = []
provider._client.fail_scoped_project = True
failed_scoped = json.loads(provider.handle_tool_call("memory_search", {
"query": "alpha beta",
"limit": 10,
"sub_graph_name": "skills",
}))
assert "sub_graph_name" in failed_scoped["error"], failed_scoped
assert "Unknown sub-graph: skills" in failed_scoped["error"], failed_scoped
assert provider._client.calls == [
("agent-context", {"view": "working-memory", "agent_address": "0xAgent"}),
("agent-context", {"view": "shared-working-memory", "agent_address": None}),
("agent-context", {"view": "verified-memory", "agent_address": None}),
("project-cg", {"view": "working-memory", "agent_address": "0xAgent", "sub_graph_name": "skills"}),
], provider._client.calls
provider._client.fail_scoped_project = False

provider._context_graph = "agent-context"
missing_project = json.loads(provider.handle_tool_call("memory_search", {
"query": "alpha beta",
"sub_graph_name": "skills",
}))
assert "sub_graph_name" in missing_project["error"], missing_project
assert "project context graph" in missing_project["error"], missing_project
`;
const result = spawnSync('python', ['-B', '-c', script], {
cwd: process.cwd(),
Expand Down
29 changes: 26 additions & 3 deletions packages/adapter-openclaw/src/DkgMemoryPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@ export class DkgMemorySearchManager implements MemorySearchManager {
// inputs, so passing a raw peer ID through the resolver still works.
const agentAddress = rawAgentAddress ? toAgentPeerId(rawAgentAddress) : undefined;
const projectContextGraphId = session?.projectContextGraphId;
const projectSubGraphName = options?.projectSubGraphName;
if (projectSubGraphName && (!projectContextGraphId || projectContextGraphId === AGENT_CONTEXT_GRAPH)) {
Comment thread
Jurij89 marked this conversation as resolved.
this.deps.logger?.warn?.(
`[dkg-memory] DkgMemorySearchManager.search skipped project sub-graph scope ` +
`"${projectSubGraphName}" because no project context graph is selected.`,
);
throw new Error('projectSubGraphName requires a selected project context graph.');
}

// B28: Preflight the agent address BEFORE firing WM queries. The query
// engine at `packages/query/src/dkg-query-engine.ts:47-48` throws
Expand Down Expand Up @@ -273,16 +281,20 @@ export class DkgMemorySearchManager implements MemorySearchManager {
// there is no inherent trust advantage of agent-context over
// project-scoped memories at the same view tier.
//
// Per-query `.catch []` preserves partial-success semantics:
// Per-query `.catch -> []` preserves partial-success semantics:
// one failing (cg, view) pair emits exactly one warn and the
// surviving layers continue to contribute results.
// surviving layers continue to contribute results. The exception is
// explicit project sub-graph scope: if the caller supplied
// `projectSubGraphName`, daemon validation/routing failures must
// surface as caller errors rather than looking like "no memories".
interface LayerPlan {
layer: MemoryLayer;
source: MemorySource;
trustWeight: number;
contextGraphId: string;
view: 'working-memory' | 'shared-working-memory' | 'verified-memory';
sparql: string;
subGraphName?: string;
}
const plans: LayerPlan[] = [
{
Expand Down Expand Up @@ -319,6 +331,7 @@ export class DkgMemorySearchManager implements MemorySearchManager {
contextGraphId: projectContextGraphId,
view: 'working-memory',
sparql: permissiveSparql,
subGraphName: projectSubGraphName,
},
{
layer: 'project-swm',
Expand All @@ -327,6 +340,7 @@ export class DkgMemorySearchManager implements MemorySearchManager {
contextGraphId: projectContextGraphId,
view: 'shared-working-memory',
sparql: permissiveSparql,
subGraphName: projectSubGraphName,
},
{
layer: 'project-vm',
Expand All @@ -335,6 +349,7 @@ export class DkgMemorySearchManager implements MemorySearchManager {
contextGraphId: projectContextGraphId,
view: 'verified-memory',
sparql: permissiveSparql,
subGraphName: projectSubGraphName,
},
);
}
Expand All @@ -346,12 +361,20 @@ export class DkgMemorySearchManager implements MemorySearchManager {
contextGraphId: plan.contextGraphId,
view: plan.view,
agentAddress,
subGraphName: plan.subGraphName,
Comment thread
Jurij89 marked this conversation as resolved.
})
.then(r => ({ plan, bindings: extractBindings(r) }))
.catch(err => {
const message = errorMessage(err);
this.deps.logger?.warn?.(
`[dkg-memory] ${plan.layer} search failed (cg=${plan.contextGraphId}, view=${plan.view}): ${errorMessage(err)}`,
`[dkg-memory] ${plan.layer} search failed (cg=${plan.contextGraphId}, view=${plan.view}): ${message}`,
);
if (plan.subGraphName) {
Comment thread
Jurij89 marked this conversation as resolved.
Outdated
Comment thread
Jurij89 marked this conversation as resolved.
Outdated
throw new Error(
`memory_search sub_graph_name "${plan.subGraphName}" failed for ` +
`context graph "${plan.contextGraphId}" (${plan.view}): ${message}`,
);
}
return { plan, bindings: [] as any[] };
Comment thread
Jurij89 marked this conversation as resolved.
Outdated
}),
),
Expand Down
Loading
Loading