Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 49 additions & 5 deletions app/core/use_cases/retrieval/deterministic_graph_recall.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,12 @@ def build_deterministic_graph_pack(
)
)

memory_relations: dict[tuple[str, str, str], dict[str, Any]] = {}
structural_trace = _expand_structural_memory_relations(
request=request,
memory_candidates=memory_candidates,
uow=uow,
memory_relations=memory_relations,
)
concept_candidates = _discover_concepts(
request=request,
Expand All @@ -153,6 +155,12 @@ def build_deterministic_graph_pack(
request=request,
memory_candidates=memory_candidates,
)
selected_ids = {str(item["memory"].id) for item in selected_memories}
relations_payload = [
relation
for (subject, _, target), relation in sorted(memory_relations.items())
if subject in selected_ids and target in selected_ids
]
concepts_payload = [
_compact_concept_payload(entry["bundle"], why_selected=entry["why"])
for entry in selected_concepts
Expand All @@ -172,6 +180,7 @@ def build_deterministic_graph_pack(
for lane in lanes
],
"memories": [_memory_payload(item) for item in selected_memories],
"memory_relations": relations_payload,
"concepts": concepts_payload,
"relation_neighbors": neighbor_payload,
"anchors": anchors[:16],
Expand All @@ -186,6 +195,7 @@ def build_deterministic_graph_pack(
"pack_composition": _pack_composition(selected_memories, anchors),
"pack_budget": _pack_budget(
selected_memories=selected_memories,
memory_relations=relations_payload,
concepts=concepts_payload,
neighbors=neighbor_payload,
ranking_trace=ranking_trace,
Expand Down Expand Up @@ -254,11 +264,7 @@ def deterministic_brief_from_graph_pack(pack: dict[str, Any]) -> dict[str, Any]:
link_roles={"failed_tactic_for", "warns_about"},
)
known_traps.extend(_claim_texts(concept_items, {"failure_mode"}))
prior_cases = _brief_memory_texts(
memories,
kinds={"solution"},
link_roles={"solution_for", "example_of"},
)
prior_cases = _brief_case_texts(memories, pack["memory_relations"])
return {
"summary": _summary(memories=memories, concepts=concept_items),
"constraints": _truncate_list(constraints, 6),
Expand Down Expand Up @@ -458,6 +464,7 @@ def _expand_structural_memory_relations(
request: MemoryReadRequest,
memory_candidates: dict[str, dict[str, Any]],
uow: IUnitOfWork,
memory_relations: dict[tuple[str, str, str], dict[str, Any]],
) -> dict[str, Any]:
"""Add canonical structural memory-relation neighbors to recall candidates."""

Expand All @@ -476,6 +483,10 @@ def _expand_structural_memory_relations(
kinds=request.kinds or list(MATURE_MEMORY_KIND_VALUES),
predicates=predicates,
)
for row in rows:
key = (row["subject_memory_id"], row["predicate"], row["object_memory_id"])
memory_relations[key] = row | {"validated_at": _iso(row["validated_at"])}
del memory_relations[key]["visible_memory_ids"]
for neighbor in select_structural_memory_relation_neighbors(
rows, anchor_memory_id=anchor_memory_id
):
Expand Down Expand Up @@ -1127,12 +1138,14 @@ def _pack_composition(
def _pack_budget(
*,
selected_memories: Sequence[dict[str, Any]],
memory_relations: Sequence[dict[str, Any]],
concepts: Sequence[dict[str, Any]],
neighbors: Sequence[dict[str, Any]],
ranking_trace: dict[str, Any],
) -> dict[str, Any]:
payload = {
"memories": [_memory_payload(item) for item in selected_memories],
"memory_relations": memory_relations,
"concepts": concepts,
"relation_neighbors": neighbors,
}
Expand All @@ -1155,6 +1168,7 @@ def synthesis_pack_from_graph_pack(pack: dict[str, Any]) -> dict[str, Any]:
"strategy": pack.get("strategy"),
"request": pack.get("request") if isinstance(pack.get("request"), dict) else {},
"memories": _dict_items(pack.get("memories")),
"memory_relations": pack["memory_relations"],
"concepts": _dict_items(pack.get("concepts")),
"relation_neighbors": _dict_items(pack.get("relation_neighbors")),
"anchors": _dict_items(pack.get("anchors")),
Expand Down Expand Up @@ -1187,6 +1201,36 @@ def _brief_memory_texts(
return rendered


def _brief_case_texts(
memories: Sequence[dict[str, Any]], relations: Sequence[dict[str, Any]]
) -> list[str]:
"""Render cases in selected memory order, keeping outcomes with their sources."""

memories_by_id = {item["id"]: item for item in memories}
relations_by_target = defaultdict(list)
for relation in relations:
relations_by_target[relation["object_memory_id"]].append(relation)
cases = []
for target in memories:
linked = relations_by_target[target["id"]]
if not linked:
cases.extend(_brief_memory_texts(
[target], kinds={"solution"}, link_roles={"solution_for", "example_of"}
))
for relation in linked:
subject = memories_by_id[relation["subject_memory_id"]]
qualifiers = [relation["status"]]
for name in ("confidence", "validated_at"):
if relation[name] is not None:
qualifiers.append(f"{name}={relation[name]}")
cases.append(
f"{subject['kind']} ({subject['currentness']}): {_truncate(subject['text'], 300)} "
f"--{relation['predicate']} [{'; '.join(qualifiers)}]--> "
f"{target['kind']} ({target['currentness']}): {_truncate(target['text'], 300)}"
)
return cases


def _claim_texts(
concepts: Sequence[dict[str, Any]], claim_types: set[str]
) -> list[str]:
Expand Down
1 change: 1 addition & 0 deletions app/core/use_cases/retrieval/read/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def execute_read_memory(
"items": selected["concepts"],
},
"relation_neighbors": selected["relation_neighbors"],
"memory_relations": selected["memory_relations"],
"conflicts": selected["conflicts"],
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def list_structural_memory_relation_rows(
structural_memory_relations.c.subject_memory_id,
structural_memory_relations.c.predicate,
structural_memory_relations.c.object_memory_id,
structural_memory_relations.c.status,
structural_memory_relations.c.confidence,
structural_memory_relations.c.validated_at,
).where(
structural_memory_relations.c.repo_id == repo_id,
structural_memory_relations.c.predicate.in_(list(predicates)),
Expand All @@ -60,6 +63,9 @@ def list_structural_memory_relation_rows(
"subject_memory_id": str(row["subject_memory_id"]),
"predicate": str(row["predicate"]),
"object_memory_id": str(row["object_memory_id"]),
"status": str(row["status"]),
"confidence": row["confidence"],
"validated_at": row["validated_at"],
"visible_memory_ids": tuple(
sorted(
self._visible_memory_ids(
Expand Down
6 changes: 6 additions & 0 deletions app/infrastructure/host_apps/inner_agents/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
- `preference` records user or team guidance.
- `change` revises or replaces older knowledge.

`memory_relations` connect specific records by subject, predicate, and object.
`solved_by` and `failed_with` bind an outcome to its problem; keep that pairing and its stated conditions together.
`superseded_by` and `explained_by_change` bind old guidance to its replacement or explanation.
Preserve link direction and lifecycle qualifications when describing a case.
Do not transfer a solution or failure to another problem because their text is similar.

Concept claims give orientation:
- `definition` and `behavior` explain a concept.
- Active, relevant `invariant` and `usage_note` claims become constraints.
Expand Down
1 change: 1 addition & 0 deletions tests/operations/_shared/read_pipeline_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def stub_read_pipeline(monkeypatch: pytest.MonkeyPatch, *, zero_results: bool) -
"app.core.use_cases.retrieval.read.execute.build_deterministic_graph_pack",
lambda **kwargs: {
"memories": memories,
"memory_relations": [],
"concepts": [],
"relation_neighbors": [],
"conflicts": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def test_read_preserves_grouped_evidence_and_display_order(monkeypatch):
"implicit_related",
"concepts",
"relation_neighbors",
"memory_relations",
"conflicts",
]
assert pack["meta"] == {
Expand Down Expand Up @@ -51,3 +52,4 @@ def test_empty_read_keeps_evidence_envelope(monkeypatch):
).data["pack"]
assert sum(pack["meta"]["counts"].values()) == 0
assert pack["concepts"] == {"mode": "none", "items": []}
assert pack["memory_relations"] == []
2 changes: 2 additions & 0 deletions tests/operations/recall/execution/test_build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ def _graph_pack() -> dict:
return {
"strategy": "deterministic_graph",
"request": {"query": "migration timeout"},
"memory_relations": [],
"query_lanes": [{"lane": "original", "query": "migration timeout"}],
"memories": [
{
Expand Down Expand Up @@ -336,6 +337,7 @@ def _empty_graph_pack() -> dict:
return {
"strategy": "deterministic_graph",
"request": {"query": "nothing"},
"memory_relations": [],
"query_lanes": [],
"memories": [],
"concepts": [],
Expand Down
115 changes: 115 additions & 0 deletions tests/operations/recall/execution/test_deterministic_graph_recall.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from dataclasses import replace
from datetime import datetime, timezone

import pytest

Expand Down Expand Up @@ -31,7 +32,9 @@
_select_concepts,
_select_final_memories,
build_deterministic_graph_pack,
deterministic_brief_from_graph_pack,
source_items_from_graph_pack,
synthesis_pack_from_graph_pack,
)
from app.core.use_cases.retrieval.read.request import MemoryReadRequest
from app.core.use_cases.retrieval.recall.request import MemoryRecallRequest
Expand Down Expand Up @@ -144,6 +147,86 @@ def test_graph_pack_expands_canonical_structural_memory_relations() -> None:
assert sources_by_id["mem-change"]["input_section"] == "explicit_related"


@pytest.mark.parametrize("seed_both_endpoints", [False, True])
@pytest.mark.parametrize("excluded", [None, "hidden", "budget"])
def test_recall_preserves_case_pairings(monkeypatch, seed_both_endpoints, excluded):
uow = _FakeUow()
uow.concepts = _NoFakeConcepts()
uow.concept_semantic_retrieval = _NoFakeConceptSemanticRetrieval()
template = uow.memories._memories["mem-direct"]
uow.memories._memories = {
key: replace(template, id=MemoryId(key), kind=MemoryKind(kind), text=text)
for key, kind, text in (
("p1", "problem", "Writes time out while a lock is held."),
("p2", "problem", "Writes time out while the disk is full."),
("s1", "solution", "Release the lock."),
("s2", "solution", "Free disk space."),
("f1", "failed_tactic", "Increasing the timeout failed."),
)
}
if excluded == "hidden":
uow.memories._memories["s2"] = replace(
uow.memories._memories["s2"], repo_id=RepoId("other-repo")
)
relations = [
{
"subject_memory_id": subject,
"predicate": predicate,
"object_memory_id": target,
"status": status,
"confidence": 0.8,
"validated_at": datetime(2026, 9, 1, tzinfo=timezone.utc),
}
for subject, predicate, target, status in (
("p1", "solved_by", "s1", "active"),
("p2", "solved_by", "s2", "maybe_stale"),
("p1", "failed_with", "f1", "active"),
)
]
monkeypatch.setattr(
uow.read_policy,
"list_structural_memory_relation_rows",
lambda **kw: [
{
**row,
"visible_memory_ids": (row["subject_memory_id"], row["object_memory_id"]),
}
for row in relations
if kw["anchor_memory_id"] in (row["subject_memory_id"], row["object_memory_id"])
and row["predicate"] in kw["predicates"]
],
)
seeds = list(uow.memories._memories) if seed_both_endpoints else ["s1", "s2", "f1"]
monkeypatch.setattr(
uow.semantic_retrieval,
"query_semantic",
lambda **kw: [{"memory_id": key, "score": 0.92} for key in seeds],
)
request = _request(query="prior writes timeout")
if excluded == "budget":
request = request.model_copy(update={"limit": 1})
pack = build_deterministic_graph_pack(request=request, uow=uow)
expected = [
{**row, "validated_at": row["validated_at"].isoformat()}
for row in relations
if excluded != "budget" and (excluded != "hidden" or row["object_memory_id"] != "s2")
]
assert sorted(pack["memory_relations"], key=str) == sorted(expected, key=str)
synthesis = synthesis_pack_from_graph_pack(pack)
read = execute_read_memory(request, uow).data["pack"]
assert synthesis["memory_relations"] == read["memory_relations"] == pack["memory_relations"]
cases = deterministic_brief_from_graph_pack(pack)["prior_cases"]
for row in expected:
subject = uow.memories._memories[row["subject_memory_id"]].text
target = uow.memories._memories[row["object_memory_id"]].text
assert any(
subject in case and target in case and row["predicate"] in case
and row["status"] in case and row["validated_at"] in case
for case in cases
)
assert not any("lock is held" in case and "Free disk" in case for case in cases)


def _request(*, query: str) -> MemoryReadRequest:
return MemoryReadRequest.model_validate(
{
Expand All @@ -155,6 +238,35 @@ def _request(*, query: str) -> MemoryReadRequest:
)


@pytest.mark.parametrize("linked_solutions", [(0, 1, 2), (5,)])
def test_fast_brief_preserves_case_order_without_duplicate_solutions(linked_solutions):
memories = [
{"id": f"s{i}", "kind": "solution", "text": f"Fix {i}.", "currentness": "current"}
for i in range(6)
] + [
{"id": f"p{i}", "kind": "problem", "text": f"Problem {i}.", "currentness": "current"}
for i in linked_solutions
]
relations = [
{"subject_memory_id": f"p{i}", "predicate": "solved_by", "object_memory_id": f"s{i}",
"status": "active", "confidence": None, "validated_at": None}
for i in reversed(linked_solutions)
]
cases = deterministic_brief_from_graph_pack(
{"memories": memories, "memory_relations": relations}
)["prior_cases"]
assert len(cases) == 6
for i, case in enumerate(cases):
assert f"Fix {i}." in case
if i in linked_solutions:
assert f"Problem {i}." in case and "solved_by" in case


def test_synthesis_rejects_missing_memory_relations():
with pytest.raises(KeyError, match="memory_relations"):
synthesis_pack_from_graph_pack({"memories": []})


class _FakeVectorSearch:
model_name = "fake-embedding"

Expand Down Expand Up @@ -415,6 +527,9 @@ def list_structural_memory_relation_rows(self, **kwargs):
"subject_memory_id": "mem-direct",
"predicate": "explained_by_change",
"object_memory_id": "mem-change",
"status": "active",
"confidence": None,
"validated_at": None,
"visible_memory_ids": ("mem-direct", "mem-change"),
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ def _candidate_pack() -> dict:
return {
"strategy": "deterministic_graph",
"request": {"query": "recall telemetry"},
"memory_relations": [],
"query_lanes": [{"lane": "original", "query": "recall telemetry"}],
"memories": [
{
Expand Down Expand Up @@ -402,6 +403,7 @@ def _empty_pack() -> dict:
"request": {"query": "nothing matches"},
"query_lanes": [],
"memories": [],
"memory_relations": [],
"concepts": [],
"relation_neighbors": [],
"anchors": [],
Expand Down
Loading