refactor(memhub): recover writes from SDK outcomes
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
db11adff84
commit
1048959cba
2 files changed
+118
No files matched your search
@@ -866,6 +866,25 @@ class HyMemoryBridge:
|
||||
governance.
|
||||
"""
|
||||
client = await self._get_client()
|
||||
exact_inquiry = getattr(client, "get_write_outcome", None)
|
||||
if callable(exact_inquiry):
|
||||
try:
|
||||
outcome = await _joined_thread(exact_inquiry, request_id)
|
||||
except Exception as error:
|
||||
raise HyMemoryBridgeError("hy_memory_outcome_inquiry_failed") from error
|
||||
if outcome is None:
|
||||
return ()
|
||||
return await _outputs_from_write_outcome(
|
||||
client,
|
||||
outcome,
|
||||
request_id=request_id,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
evidence_ids=evidence_ids,
|
||||
)
|
||||
|
||||
# Compatibility for the pinned 2.0.4 wheel. Remove after the SDK-first
|
||||
# authority release is published and MemHub's exact pin is bumped.
|
||||
page_size = 500
|
||||
outputs: list[HyMemoryOutput] = []
|
||||
seen: set[str] = set()
|
||||
@@ -2061,6 +2080,69 @@ def _basic_info_memories(raw: object) -> tuple[Mapping[str, object], ...]:
|
||||
return tuple(memory for memory in memories if isinstance(memory, Mapping))
|
||||
|
||||
|
||||
async def _outputs_from_write_outcome(
|
||||
client: HyMemoryClientProtocol,
|
||||
outcome: object,
|
||||
*,
|
||||
request_id: str,
|
||||
user_id: str,
|
||||
agent_id: str,
|
||||
evidence_ids: tuple[str, ...],
|
||||
) -> tuple[HyMemoryOutput, ...]:
|
||||
if not isinstance(outcome, Mapping):
|
||||
raise HyMemoryBridgeError("hy_memory_outcome_shape_unknown")
|
||||
if outcome.get("request_id") not in {None, request_id}:
|
||||
raise HyMemoryBridgeError("hy_memory_outcome_identity_mismatch")
|
||||
for key, expected in (("user_id", user_id), ("agent_id", agent_id)):
|
||||
value = outcome.get(key)
|
||||
if value is not None and value != expected:
|
||||
raise HyMemoryBridgeError("hy_memory_outcome_identity_mismatch")
|
||||
status = outcome.get("status")
|
||||
if status == "failed":
|
||||
return ()
|
||||
if status != "succeeded":
|
||||
raise HyMemoryBridgeError("hy_memory_outcome_not_terminal")
|
||||
result = outcome.get("result")
|
||||
if not isinstance(result, Mapping):
|
||||
raise HyMemoryBridgeError("hy_memory_outcome_shape_unknown")
|
||||
candidates: list[str] = []
|
||||
for key in ("memory_ids", "experience_ids", "fact_ids"):
|
||||
values = result.get(key)
|
||||
if isinstance(values, list):
|
||||
candidates.extend(value for value in values if isinstance(value, str))
|
||||
if isinstance(result.get("memory_id"), str):
|
||||
candidates.append(cast(str, result["memory_id"]))
|
||||
|
||||
outputs: list[HyMemoryOutput] = []
|
||||
for memory_id in dict.fromkeys(candidates):
|
||||
try:
|
||||
record = await _joined_thread(client.get, memory_id)
|
||||
except Exception as error:
|
||||
raise HyMemoryBridgeError("hy_memory_outcome_inquiry_failed") from error
|
||||
# The singular add memory_id may be the hidden L1 raw node. It is not a
|
||||
# semantic memory output and public get correctly returns None for it.
|
||||
if not isinstance(record, Mapping):
|
||||
continue
|
||||
if record.get("user_id") != user_id or record.get("agent_id") != agent_id:
|
||||
raise HyMemoryBridgeError("hy_memory_outcome_identity_mismatch")
|
||||
content = record.get("content")
|
||||
if not isinstance(content, str) or not content.strip():
|
||||
continue
|
||||
layer = record.get("layer")
|
||||
owner = record.get("owner")
|
||||
outputs.append(
|
||||
HyMemoryOutput(
|
||||
memory_id=memory_id,
|
||||
content=content,
|
||||
layer=layer if isinstance(layer, str) else None,
|
||||
owner=owner if owner in {"user", "agent"} else None,
|
||||
tags=_string_tuple(record.get("tags")),
|
||||
evidence_ids=evidence_ids,
|
||||
)
|
||||
)
|
||||
return tuple(outputs)
|
||||
|
||||
|
||||
def _sdk_memory_record(raw: object) -> HySdkMemoryRecord:
|
||||
if not isinstance(raw, Mapping):
|
||||
raise HyMemoryBridgeError("hy_memory_record_shape_unknown")
|
||||
|
||||
@@ -644,6 +644,8 @@ def test_bridge_writes_evidence_refs_and_uses_source_as_provenance(tmp_path) ->
|
||||
|
||||
def test_bridge_recovers_only_outputs_with_the_exact_request_identity(tmp_path) -> None:
|
||||
client = FakeHyMemoryClient({"success": True})
|
||||
# Pinned SDK 2.0.4 compatibility path: no durable outcome API yet.
|
||||
client.get_write_outcome = None
|
||||
client.list_result = {
|
||||
"vdb": {
|
||||
"total": 3,
|
||||
@@ -699,6 +701,40 @@ def test_bridge_recovers_only_outputs_with_the_exact_request_identity(tmp_path)
|
||||
assert outputs[0].evidence_ids == ("evidence-1",)
|
||||
|
||||
|
||||
def test_bridge_prefers_exact_sdk_write_outcome_over_library_scan(tmp_path) -> None:
|
||||
client = FakeHyMemoryClient({"success": True})
|
||||
client.write_outcome = {
|
||||
"request_id": "request-1",
|
||||
"user_id": "local",
|
||||
"agent_id": "one-memory",
|
||||
"status": "succeeded",
|
||||
"result": {"success": True, "memory_ids": ["memory-1"]},
|
||||
}
|
||||
bridge = HyMemoryBridge(
|
||||
profile=snapshot(
|
||||
ProviderKind.OLLAMA,
|
||||
base_url="http://127.0.0.1:11434",
|
||||
has_api_key=False,
|
||||
),
|
||||
api_key=None,
|
||||
data_dir=tmp_path,
|
||||
client=client,
|
||||
)
|
||||
|
||||
outputs = asyncio.run(
|
||||
bridge.outputs_for_request(
|
||||
request_id="request-1",
|
||||
user_id="local",
|
||||
agent_id="one-memory",
|
||||
evidence_ids=("evidence-1",),
|
||||
)
|
||||
)
|
||||
|
||||
assert [output.memory_id for output in outputs] == ["memory-1"]
|
||||
assert outputs[0].evidence_ids == ("evidence-1",)
|
||||
assert not any("list_memories" in call for call in client.calls)
|
||||
|
||||
|
||||
def test_bridge_reads_sdk_memory_lifecycle_without_governance(tmp_path) -> None:
|
||||
client = FakeHyMemoryClient({"success": True})
|
||||
client.list_result = {
|
||||
|
||||
Reference in new issue
Block a user