codespy-ai 1.2.2__tar.gz → 1.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/PKG-INFO +1 -1
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/pyproject.toml +1 -1
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/__init__.py +1 -1
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/context_safe.py +3 -3
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/__init__.py +7 -3
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/hippocampus.py +109 -152
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/audit/agent.py +9 -7
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/code_review/agent.py +10 -9
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/doc/agent.py +10 -9
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/scope/agent.py +12 -12
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/summary/agent.py +9 -8
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/supply_chain/agent.py +10 -9
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/config.py +2 -11
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/config_dspy.py +0 -1
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/config_memory.py +2 -4
- codespy_ai-1.2.2/src/codespy/agents/memory/hippocampus/modules/__init__.py +0 -13
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/LICENSE +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/README.md +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/cost_tracker.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/dspy_config.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/memory/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/budget.py +0 -0
- {codespy_ai-1.2.2/src/codespy/agents/memory/hippocampus/modules → codespy_ai-1.2.3/src/codespy/agents/memory/hippocampus}/cartographer.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/context_memory.py +0 -0
- {codespy_ai-1.2.2/src/codespy/agents/memory/hippocampus/modules → codespy_ai-1.2.3/src/codespy/agents/memory/hippocampus}/distiller.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/episode.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/memory/pg0_manager.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/memory/postgres.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/audit/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/code_review/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/doc/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/doc/doc_extractor.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/helpers.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/models.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/scope/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/scope/manifest_parser.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/scope/models.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/summary/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/agents/review/supply_chain/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/cli.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/cli_local.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/cli_mcp_server.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/cli_remote.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/config_git.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/config_io.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/config_llm.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/config_utils.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/cyber/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/cyber/osv/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/cyber/osv/client.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/cyber/osv/models.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/cyber/osv/server.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/git/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/git/base.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/git/client.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/git/github_client.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/git/gitlab_client.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/git/local_diff.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/git/models.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/git/patch_utils.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/git/server.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/mcp_utils.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/bash.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/cpp.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/csharp.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/php.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/ruby.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/models.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/server.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/storage/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/storage/base.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/storage/filesystem/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/storage/filesystem/client.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/storage/filesystem/server.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/storage/models.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/storage/s3/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/storage/s3/client.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/storage/s3/server.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/web/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/web/client.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/web/models.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/tools/web/server.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/workflows/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/workflows/review/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/workflows/review/models.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/workflows/review/pipeline.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/workflows/review/reporters/__init__.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/workflows/review/reporters/base.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/workflows/review/reporters/git.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/workflows/review/reporters/stdout.py +0 -0
- {codespy_ai-1.2.2 → codespy_ai-1.2.3}/src/codespy/workflows/review/server.py +0 -0
|
@@ -60,7 +60,7 @@ class ContextSafe(dspy.Module):
|
|
|
60
60
|
- Try/catch: if the inner module raises a context overflow error (unknown
|
|
61
61
|
models where pre-flight can't estimate), catches it and retries with RLM.
|
|
62
62
|
|
|
63
|
-
Transparent to
|
|
63
|
+
Transparent to inject_context_memory — delegates .signature to inner module.
|
|
64
64
|
"""
|
|
65
65
|
|
|
66
66
|
def __init__(
|
|
@@ -116,7 +116,7 @@ class ContextSafe(dspy.Module):
|
|
|
116
116
|
return self._create_rlm_fallback()(**kwargs)
|
|
117
117
|
|
|
118
118
|
async def aforward(self, **kwargs) -> dspy.Prediction:
|
|
119
|
-
"""Async path — used by code_review, scope, supply_chain via
|
|
119
|
+
"""Async path — used by code_review, scope, supply_chain, doc via agent.acall()."""
|
|
120
120
|
should_fallback, reason = self._should_use_rlm(kwargs)
|
|
121
121
|
if should_fallback:
|
|
122
122
|
logger.warning(
|
|
@@ -186,7 +186,7 @@ class ContextSafe(dspy.Module):
|
|
|
186
186
|
return False, ""
|
|
187
187
|
|
|
188
188
|
def _get_current_signature(self):
|
|
189
|
-
"""Get current signature (may include context_memory if
|
|
189
|
+
"""Get current signature (may include context_memory if inject_context_memory was called)."""
|
|
190
190
|
sig = getattr(self.module, "signature", None)
|
|
191
191
|
if sig is not None:
|
|
192
192
|
return sig
|
|
@@ -17,9 +17,12 @@ from codespy.agents.memory.hippocampus.episode import (
|
|
|
17
17
|
join_episode_saves,
|
|
18
18
|
submit_episode_save,
|
|
19
19
|
)
|
|
20
|
-
from codespy.agents.memory.hippocampus.hippocampus import
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
from codespy.agents.memory.hippocampus.hippocampus import (
|
|
21
|
+
Hippocampus,
|
|
22
|
+
inject_context_memory,
|
|
23
|
+
)
|
|
24
|
+
from codespy.agents.memory.hippocampus.cartographer import Cartographer, CartographerSig
|
|
25
|
+
from codespy.agents.memory.hippocampus.distiller import Distiller, DistillerSig
|
|
23
26
|
|
|
24
27
|
__all__ = [
|
|
25
28
|
"CacheCandidate",
|
|
@@ -39,6 +42,7 @@ __all__ = [
|
|
|
39
42
|
"SectionName",
|
|
40
43
|
"Topic",
|
|
41
44
|
"compute_common_ancestor_topic_id",
|
|
45
|
+
"inject_context_memory",
|
|
42
46
|
"join_episode_saves",
|
|
43
47
|
"make_topic_id",
|
|
44
48
|
"submit_episode_save",
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
import copy
|
|
7
6
|
import logging
|
|
8
7
|
import uuid
|
|
9
8
|
from datetime import UTC, datetime
|
|
@@ -16,7 +15,6 @@ from codespy.agents.memory.hippocampus.budget import (
|
|
|
16
15
|
_head_tail_text,
|
|
17
16
|
count_tokens,
|
|
18
17
|
evict,
|
|
19
|
-
format_inputs,
|
|
20
18
|
format_trajectory,
|
|
21
19
|
)
|
|
22
20
|
from codespy.agents.memory.hippocampus.context_memory import (
|
|
@@ -29,8 +27,8 @@ from codespy.agents.memory.hippocampus.context_memory import (
|
|
|
29
27
|
_PREFIX_TO_SECTION,
|
|
30
28
|
)
|
|
31
29
|
from codespy.agents.memory.hippocampus.episode import Episode
|
|
32
|
-
from codespy.agents.memory.hippocampus.
|
|
33
|
-
from codespy.agents.memory.hippocampus.
|
|
30
|
+
from codespy.agents.memory.hippocampus.distiller import Distiller
|
|
31
|
+
from codespy.agents.memory.hippocampus.cartographer import Cartographer
|
|
34
32
|
|
|
35
33
|
if TYPE_CHECKING:
|
|
36
34
|
from codespy.agents.memory.postgres import EpisodeStore
|
|
@@ -49,65 +47,76 @@ def prepend_context_memory(sig):
|
|
|
49
47
|
)
|
|
50
48
|
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
"""
|
|
50
|
+
def inject_context_memory(module: dspy.Module) -> dspy.Module:
|
|
51
|
+
"""Prepend context_memory input field to a dspy.Module's signatures.
|
|
54
52
|
|
|
55
|
-
|
|
53
|
+
Idempotent — skips predictors that already have context_memory.
|
|
54
|
+
Mutates the module in place and returns it for chaining.
|
|
55
|
+
Works through ContextSafe's signature delegation.
|
|
56
|
+
"""
|
|
57
|
+
top_sig = getattr(module, "signature", None)
|
|
58
|
+
if top_sig is not None:
|
|
59
|
+
module_inputs = set(top_sig.input_fields)
|
|
60
|
+
if "context_memory" not in top_sig.input_fields:
|
|
61
|
+
module.signature = prepend_context_memory(top_sig)
|
|
62
|
+
for _, pred in module.named_predictors():
|
|
63
|
+
if (
|
|
64
|
+
set(pred.signature.input_fields) & module_inputs
|
|
65
|
+
and "context_memory" not in pred.signature.input_fields
|
|
66
|
+
):
|
|
67
|
+
pred.signature = prepend_context_memory(pred.signature)
|
|
68
|
+
else:
|
|
69
|
+
for _, pred in module.named_predictors():
|
|
70
|
+
if "context_memory" not in pred.signature.input_fields:
|
|
71
|
+
pred.signature = prepend_context_memory(pred.signature)
|
|
72
|
+
return module
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Hippocampus:
|
|
76
|
+
"""Memory component that evolves via LLM-driven reflection.
|
|
77
|
+
|
|
78
|
+
The context memory is provided to agents so they start each run
|
|
56
79
|
with accumulated orientation knowledge (structure, entities, constants) about
|
|
57
80
|
the external context. After calls, the Distiller extracts transferable
|
|
58
81
|
understanding and the Cartographer edits the memory — "caching understanding,
|
|
59
82
|
not answers."
|
|
60
83
|
|
|
61
|
-
##
|
|
62
|
-
|
|
63
|
-
Reflection behaviour is governed by two orthogonal knobs:
|
|
84
|
+
## Lifecycle
|
|
64
85
|
|
|
65
|
-
|
|
66
|
-
(in-episode warm-up). ``None`` (default) = no limit, reflect after every call.
|
|
67
|
-
``0`` = never reflect online. ``N`` = reflect for the first N calls, buffer-only
|
|
68
|
-
thereafter.
|
|
86
|
+
Typical usage pattern::
|
|
69
87
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
``end_episode()`` is available regardless of the online setting.
|
|
88
|
+
# Construct Hippocampus with task name and optional memory budget
|
|
89
|
+
hippo = Hippocampus(task_name="...")
|
|
73
90
|
|
|
74
|
-
|
|
91
|
+
# Inject context memory into agent
|
|
92
|
+
inject_context_memory(agent)
|
|
75
93
|
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
pred = mem(task="…")
|
|
94
|
+
# Run agent with context_memory input
|
|
95
|
+
pred = agent(context_memory=hippo.context_memory, task="…")
|
|
79
96
|
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
for task in tasks:
|
|
83
|
-
pred = mem(task=task)
|
|
84
|
-
mem.end_episode()
|
|
97
|
+
# Observe the result (buffers trajectory)
|
|
98
|
+
hippo.observe(pred)
|
|
85
99
|
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
for task in tasks:
|
|
89
|
-
pred = mem(task=task)
|
|
90
|
-
mem.end_episode()
|
|
100
|
+
# Optional: bind topics before end_episode
|
|
101
|
+
hippo.bind_topics(topics, topic_ids)
|
|
91
102
|
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
pred = mem(task="…") # no end_episode() call
|
|
103
|
+
# End episode (consolidates and persists)
|
|
104
|
+
hippo.end_episode(store, artifacts={"key": "value"})
|
|
95
105
|
|
|
96
|
-
# Async
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await mem.aend_episode(store, dir)
|
|
106
|
+
# Async variant (for callers running inside an event loop)
|
|
107
|
+
pred = await agent.acall(context_memory=hippo.context_memory, task="…")
|
|
108
|
+
await hippo.aobserve(pred)
|
|
109
|
+
await hippo.aend_episode(store, artifacts={"key": "value"})
|
|
101
110
|
|
|
102
111
|
## Trajectory bounding (two-stage)
|
|
103
112
|
|
|
104
113
|
When ``budget.compact_trajectory`` is ``True`` and
|
|
105
114
|
``budget.max_trajectory_tokens`` is set:
|
|
106
115
|
|
|
107
|
-
- **Stage 1** (per
|
|
116
|
+
- **Stage 1** (per observation) — each trajectory is head+tail bounded at ``format_trajectory``
|
|
108
117
|
time. This keeps the buffer lightweight.
|
|
109
118
|
- **Stage 2** (``end_episode``) — the joined episode is head+tail bounded again, so
|
|
110
|
-
the combined result is guaranteed to fit the budget even if many
|
|
119
|
+
the combined result is guaranteed to fit the budget even if many observations are buffered.
|
|
111
120
|
|
|
112
121
|
With ``budget.compact_trajectory=False`` both stages are skipped and the
|
|
113
122
|
Distiller receives the full trajectory. The ContextSafe wrapper on the
|
|
@@ -125,41 +134,27 @@ class Hippocampus(dspy.Module):
|
|
|
125
134
|
|
|
126
135
|
def __init__(
|
|
127
136
|
self,
|
|
128
|
-
|
|
137
|
+
task_name: str,
|
|
129
138
|
budget: MemoryBudget | None = None,
|
|
130
|
-
max_reflects: int | None = None,
|
|
131
139
|
question: str | None = None,
|
|
132
|
-
task_name: str | None = None,
|
|
133
140
|
run_id: str | None = None,
|
|
134
141
|
initial_memory: ContextMemory | None = None,
|
|
135
142
|
topics: list[Topic] | None = None,
|
|
136
143
|
):
|
|
137
144
|
"""
|
|
138
145
|
Args:
|
|
139
|
-
module: Any dspy.Module (ReAct, RLM, Predict, …) to wrap.
|
|
140
146
|
budget: The four token budgets bounding memory, as a
|
|
141
147
|
:class:`MemoryBudget`. Defaults to ``MemoryBudget()`` — see that
|
|
142
148
|
class for per-field guidance. Resolve one from configuration with
|
|
143
149
|
``Settings.get_memory_budget(signature_name)``.
|
|
144
|
-
max_reflects: Maximum number of forward() calls that also reflect online.
|
|
145
|
-
None (default): no limit — reflect after every call (classic online learning).
|
|
146
|
-
0: never reflect online — pure buffering until end_episode().
|
|
147
|
-
N: reflect online for the first N calls, buffer-only afterwards.
|
|
148
|
-
Every call is always buffered regardless of this setting, so
|
|
149
|
-
end_episode() is always available.
|
|
150
150
|
question: Pre-computed question string for the reflection "question".
|
|
151
151
|
If set, this string is used directly as the Distiller question.
|
|
152
|
-
If None,
|
|
153
|
-
``budget.max_question_tokens``).
|
|
154
|
-
Set this when one field cleanly captures user intent.
|
|
152
|
+
If None, uses empty string (callers typically pass question at init).
|
|
155
153
|
task_name: Identity recorded in ``Episode.task`` and used in the episode
|
|
156
154
|
filename. Pass the signature's snake_case name (``"doc"``,
|
|
157
155
|
``"code_review"``, …) — the same key that drives config, LM
|
|
158
156
|
selection and cost attribution — so the episode path lines up with
|
|
159
|
-
the rest of the system.
|
|
160
|
-
``dspy.RLM``/``dspy.ReAct``-style modules expose ``.signature``,
|
|
161
|
-
``dspy.ChainOfThought`` does not, so the fallback would yield a
|
|
162
|
-
meaningless (and collision-prone) ``"ChainOfThought"``.
|
|
157
|
+
the rest of the system. Required parameter.
|
|
163
158
|
run_id: Identifier of the pipeline run this agent belongs to. Passed
|
|
164
159
|
down by the orchestrating ``ReviewPipeline`` so every module
|
|
165
160
|
invoked within the same review run shares the same identifier,
|
|
@@ -173,31 +168,9 @@ class Hippocampus(dspy.Module):
|
|
|
173
168
|
Topic IDs are auto-assigned to all new observations created during this episode.
|
|
174
169
|
Used for scope-aware memory organization.
|
|
175
170
|
"""
|
|
176
|
-
super().__init__()
|
|
177
|
-
|
|
178
|
-
module = copy.deepcopy(module)
|
|
179
|
-
|
|
180
|
-
# Prepend context_memory only to predictors that receive the module's own
|
|
181
|
-
# input fields.
|
|
182
|
-
top_sig = getattr(module, "signature", None)
|
|
183
|
-
if top_sig is not None:
|
|
184
|
-
module_inputs = set(top_sig.input_fields)
|
|
185
|
-
module.signature = prepend_context_memory(top_sig)
|
|
186
|
-
for _, pred in module.named_predictors():
|
|
187
|
-
if (
|
|
188
|
-
set(pred.signature.input_fields) & module_inputs
|
|
189
|
-
and "context_memory" not in pred.signature.input_fields
|
|
190
|
-
):
|
|
191
|
-
pred.signature = prepend_context_memory(pred.signature)
|
|
192
|
-
else:
|
|
193
|
-
for _, pred in module.named_predictors():
|
|
194
|
-
pred.signature = prepend_context_memory(pred.signature)
|
|
195
|
-
|
|
196
|
-
self.agent = module
|
|
197
171
|
self.distill = Distiller()
|
|
198
172
|
self.cartograph = Cartographer()
|
|
199
173
|
self.budget = budget or MemoryBudget()
|
|
200
|
-
self.max_reflects = max_reflects
|
|
201
174
|
self.question = question
|
|
202
175
|
self.cmem = initial_memory.model_copy(deep=True) if initial_memory else ContextMemory()
|
|
203
176
|
self._topic_ids: list[str] = []
|
|
@@ -209,33 +182,14 @@ class Hippocampus(dspy.Module):
|
|
|
209
182
|
self.cmem.topics.append(topic)
|
|
210
183
|
existing_ids.add(topic.id)
|
|
211
184
|
self.scores: dict[str, int] = {}
|
|
212
|
-
# Buffer of per-
|
|
185
|
+
# Buffer of per-observation bounded trajectory strings, cleared after end_episode().
|
|
213
186
|
self._episode_trajectories: list[str] = []
|
|
214
|
-
#
|
|
215
|
-
# Used to skip a redundant consolidation distill in the single-call case,
|
|
216
|
-
# and to detect "everything was reflected online" so end_episode() still
|
|
217
|
-
# persists a snapshot even when nothing remains to consolidate.
|
|
218
|
-
self._reflected_count: int = 0
|
|
219
|
-
# Question derived from the latest buffered call; used as Distiller consolidation input.
|
|
187
|
+
# Question derived from the latest buffered observation; used as Distiller consolidation input.
|
|
220
188
|
self._episode_question: str | None = None
|
|
221
189
|
|
|
222
|
-
# Identity
|
|
223
|
-
|
|
224
|
-
self.
|
|
225
|
-
if task_name:
|
|
226
|
-
self._task_name = task_name
|
|
227
|
-
elif top_sig is not None:
|
|
228
|
-
self._task_name = top_sig.__name__
|
|
229
|
-
else:
|
|
230
|
-
fallback = type(module).__name__
|
|
231
|
-
logger.warning(
|
|
232
|
-
"Hippocampus: task_name not provided and module %r has no .signature; "
|
|
233
|
-
"using collision-prone fallback %r. Pass task_name explicitly.",
|
|
234
|
-
module,
|
|
235
|
-
fallback,
|
|
236
|
-
)
|
|
237
|
-
self._task_name = fallback
|
|
238
|
-
self._module_name: str = type(module).__name__
|
|
190
|
+
# Identity for Episode metadata.
|
|
191
|
+
self._task_name = task_name
|
|
192
|
+
self._module_name = task_name # Default to task_name (more meaningful than "ContextSafe")
|
|
239
193
|
# Identifier of the pipeline run this agent belongs to (see run_id arg
|
|
240
194
|
# above). Falls back to a random UUID for standalone usage where no
|
|
241
195
|
# orchestrator provides one.
|
|
@@ -247,45 +201,58 @@ class Hippocampus(dspy.Module):
|
|
|
247
201
|
# Step counter incremented per _distill() call for mutation grouping.
|
|
248
202
|
self._distill_step: int = 0
|
|
249
203
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return pred
|
|
254
|
-
|
|
255
|
-
async def aforward(self, **kwargs) -> dspy.Prediction:
|
|
256
|
-
"""Async counterpart of :meth:`forward`.
|
|
204
|
+
@property
|
|
205
|
+
def context_memory(self) -> ContextMemory:
|
|
206
|
+
"""Read-only access to the current context memory.
|
|
257
207
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
event loop (e.g. reviewer modules using ``await agent.acall(...)``).
|
|
261
|
-
The Distiller/Cartographer reflection pass is still synchronous under
|
|
262
|
-
the hood but is offloaded to a thread so it never blocks the loop.
|
|
208
|
+
Returns the live reference (mutations through it like ``bind_topics`` work).
|
|
209
|
+
Assignment is blocked (read-only property).
|
|
263
210
|
"""
|
|
264
|
-
|
|
265
|
-
await asyncio.to_thread(self._buffer_and_distill, pred, kwargs)
|
|
266
|
-
return pred
|
|
211
|
+
return self.cmem
|
|
267
212
|
|
|
268
|
-
def
|
|
269
|
-
"""
|
|
213
|
+
def observe(self, result: dspy.Prediction | str, *, question: str | None = None) -> None:
|
|
214
|
+
"""Feed an observation (trajectory) to Hippocampus for buffering.
|
|
270
215
|
|
|
271
|
-
|
|
272
|
-
|
|
216
|
+
The trajectory is buffered and will be consolidated at ``end_episode()``.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
result: Either a ``dspy.Prediction`` (stage-1 trajectory bounding applies)
|
|
220
|
+
or a raw ``str`` (no bounding, caller controls input).
|
|
221
|
+
question: Optional per-observation question override. If not provided,
|
|
222
|
+
uses ``self.question`` or empty string.
|
|
273
223
|
"""
|
|
274
|
-
|
|
275
|
-
|
|
224
|
+
# Extract trajectory
|
|
225
|
+
if isinstance(result, str):
|
|
226
|
+
traj = result # raw string: no stage-1 bounding (caller controls input)
|
|
227
|
+
else:
|
|
228
|
+
max_traj = self.budget.max_trajectory_tokens if self.budget.compact_trajectory else None
|
|
229
|
+
traj = format_trajectory(result, max_traj) # stage-1 bounding for Predictions
|
|
230
|
+
|
|
231
|
+
# Buffer
|
|
276
232
|
self._episode_trajectories.append(traj)
|
|
277
|
-
self._episode_question = self.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
233
|
+
self._episode_question = question or self.question or ""
|
|
234
|
+
|
|
235
|
+
async def aobserve(self, result: dspy.Prediction | str, *, question: str | None = None) -> None:
|
|
236
|
+
"""Async counterpart of :meth:`observe`.
|
|
237
|
+
|
|
238
|
+
Offloads the observation processing to a thread so it never blocks
|
|
239
|
+
the caller's event loop.
|
|
240
|
+
"""
|
|
241
|
+
await asyncio.to_thread(self.observe, result, question=question)
|
|
242
|
+
|
|
243
|
+
def bind_topics(self, topics: list[Topic], topic_ids: list[str]) -> None:
|
|
244
|
+
"""Bind topics to the context memory for episode persistence.
|
|
245
|
+
|
|
246
|
+
Used by the scope agent post-call to bind scope-specific topics.
|
|
247
|
+
Replaces the previous pattern of direct ``_topic_ids`` access and
|
|
248
|
+
``cmem.bind_topics()`` calls.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
topics: List of Topic objects to register.
|
|
252
|
+
topic_ids: List of topic IDs to stamp on new observations.
|
|
253
|
+
"""
|
|
254
|
+
self._topic_ids = topic_ids
|
|
255
|
+
self.cmem.bind_topics(topics, topic_ids)
|
|
289
256
|
|
|
290
257
|
def _consolidate(self) -> str | None:
|
|
291
258
|
"""Join buffered trajectories (stage-2 bounded) and distill+apply once.
|
|
@@ -293,8 +260,7 @@ class Hippocampus(dspy.Module):
|
|
|
293
260
|
Returns the combined trajectory text used for consolidation, or
|
|
294
261
|
``None`` if the buffer is empty (no-op).
|
|
295
262
|
"""
|
|
296
|
-
|
|
297
|
-
if not self._episode_trajectories or skip_double_distill:
|
|
263
|
+
if not self._episode_trajectories:
|
|
298
264
|
return None
|
|
299
265
|
combined = "\n\n".join(
|
|
300
266
|
f"=== Call {i + 1} ===\n{t}" for i, t in enumerate(self._episode_trajectories)
|
|
@@ -332,7 +298,6 @@ class Hippocampus(dspy.Module):
|
|
|
332
298
|
)
|
|
333
299
|
self._episode_trajectories.clear()
|
|
334
300
|
self._episode_question = None
|
|
335
|
-
self._reflected_count = 0
|
|
336
301
|
self._mutations.clear()
|
|
337
302
|
self._distill_step = 0
|
|
338
303
|
|
|
@@ -345,9 +310,9 @@ class Hippocampus(dspy.Module):
|
|
|
345
310
|
|
|
346
311
|
A single Distiller pass sees all buffered trajectories joined with
|
|
347
312
|
``=== Call k ===`` headers. If ``budget.max_trajectory_tokens`` is set, the
|
|
348
|
-
combined text is head+tail bounded (stage 2) after per-
|
|
313
|
+
combined text is head+tail bounded (stage 2) after per-observation bounding
|
|
349
314
|
(stage 1) already applied at append time. The question is derived from
|
|
350
|
-
the first buffered
|
|
315
|
+
the first buffered observation. No-op if the buffer is empty.
|
|
351
316
|
|
|
352
317
|
After consolidation ``self.episode`` is set to a new :class:`Episode`
|
|
353
318
|
containing the task/module identity and a deep-copy snapshot of the
|
|
@@ -366,8 +331,7 @@ class Hippocampus(dspy.Module):
|
|
|
366
331
|
OSError: If persistence is requested and the write fails.
|
|
367
332
|
"""
|
|
368
333
|
combined = self._consolidate()
|
|
369
|
-
|
|
370
|
-
if not has_content:
|
|
334
|
+
if combined is None:
|
|
371
335
|
return
|
|
372
336
|
self._finalize_episode(artifacts)
|
|
373
337
|
if store is not None:
|
|
@@ -390,8 +354,7 @@ class Hippocampus(dspy.Module):
|
|
|
390
354
|
episode (e.g. ``{"review": "<markdown>"}``).
|
|
391
355
|
"""
|
|
392
356
|
combined = await asyncio.to_thread(self._consolidate)
|
|
393
|
-
|
|
394
|
-
if not has_content:
|
|
357
|
+
if combined is None:
|
|
395
358
|
return
|
|
396
359
|
await asyncio.to_thread(self._finalize_episode, artifacts)
|
|
397
360
|
if store is not None:
|
|
@@ -401,7 +364,6 @@ class Hippocampus(dspy.Module):
|
|
|
401
364
|
"""Discard the buffered trajectories without reflecting."""
|
|
402
365
|
self._episode_trajectories.clear()
|
|
403
366
|
self._episode_question = None
|
|
404
|
-
self._reflected_count = 0
|
|
405
367
|
self._mutations.clear()
|
|
406
368
|
self._distill_step = 0
|
|
407
369
|
|
|
@@ -409,11 +371,6 @@ class Hippocampus(dspy.Module):
|
|
|
409
371
|
# Internals
|
|
410
372
|
# ------------------------------------------------------------------
|
|
411
373
|
|
|
412
|
-
def _make_question(self, inputs: dict) -> str:
|
|
413
|
-
if self.question is not None:
|
|
414
|
-
return self.question
|
|
415
|
-
return format_inputs(inputs, self.budget.max_question_tokens)
|
|
416
|
-
|
|
417
374
|
def _record_mutations(
|
|
418
375
|
self, ops: list[Operation], new_ids: list[str], pre_memory: ContextMemory
|
|
419
376
|
) -> list[Mutation]:
|
|
@@ -7,7 +7,8 @@ import dspy
|
|
|
7
7
|
|
|
8
8
|
from codespy.agents import SignatureContext, get_cost_tracker
|
|
9
9
|
from codespy.agents.context_safe import ContextSafe
|
|
10
|
-
from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus
|
|
10
|
+
from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus, inject_context_memory
|
|
11
|
+
from codespy.agents.memory.hippocampus.context_memory import Topic
|
|
11
12
|
from codespy.agents.review.models import Issue, ReviewContext
|
|
12
13
|
from codespy.agents.review.helpers import deepest_common_folder
|
|
13
14
|
from codespy.config import get_settings
|
|
@@ -93,21 +94,22 @@ class Auditor(dspy.Module):
|
|
|
93
94
|
if scope_topic:
|
|
94
95
|
scope_topics.append(scope_topic)
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
inject_context_memory(auditor)
|
|
98
|
+
hippo = Hippocampus(
|
|
99
|
+
task_name="audit",
|
|
98
100
|
budget=self._settings.get_memory_budget("audit"),
|
|
99
|
-
max_reflects=self._settings.get_memory_max_reflects("audit"),
|
|
100
101
|
question=question,
|
|
101
|
-
task_name="audit",
|
|
102
102
|
run_id=run_id,
|
|
103
103
|
initial_memory=initial_memory,
|
|
104
104
|
topics=scope_topics if scope_topics else None,
|
|
105
105
|
)
|
|
106
|
-
result =
|
|
106
|
+
result = auditor(
|
|
107
|
+
context_memory=hippo.context_memory,
|
|
107
108
|
pr_title=review_context.pr_context.pr_title,
|
|
108
109
|
summary=review_context.pr_context.summary,
|
|
109
110
|
all_issues=all_issues,
|
|
110
111
|
)
|
|
112
|
+
hippo.observe(result)
|
|
111
113
|
# Run episode save synchronously (auditor is the last module)
|
|
112
114
|
try:
|
|
113
115
|
_artifacts = {
|
|
@@ -116,7 +118,7 @@ class Auditor(dspy.Module):
|
|
|
116
118
|
f"## Recommendation\n\n{result.recommendation}\n"
|
|
117
119
|
)
|
|
118
120
|
}
|
|
119
|
-
|
|
121
|
+
hippo.end_episode(store, artifacts=_artifacts)
|
|
120
122
|
except Exception:
|
|
121
123
|
logger.warning("Audit episode save failed", exc_info=True)
|
|
122
124
|
else:
|
|
@@ -11,7 +11,7 @@ import dspy # type: ignore[import-untyped]
|
|
|
11
11
|
|
|
12
12
|
from codespy.agents import SignatureContext, get_cost_tracker
|
|
13
13
|
from codespy.agents.context_safe import ContextSafe
|
|
14
|
-
from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus
|
|
14
|
+
from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus, inject_context_memory
|
|
15
15
|
from codespy.agents.memory.hippocampus.episode import submit_episode_save
|
|
16
16
|
from codespy.agents.review.models import Issue, IssueCategory, ReviewContext
|
|
17
17
|
from codespy.agents.review.scope.models import ScopeResult
|
|
@@ -289,7 +289,7 @@ class CodeReviewer(dspy.Module):
|
|
|
289
289
|
logger.info(
|
|
290
290
|
f" Code review: scope {scope.subroot} ({len(scope.changed_files)} files)"
|
|
291
291
|
)
|
|
292
|
-
|
|
292
|
+
hippo: Hippocampus | None = None
|
|
293
293
|
async with SignatureContext("code_review", self._cost_tracker):
|
|
294
294
|
# Load own prior "code_review" episode for this scope
|
|
295
295
|
scope_initial_memory: ContextMemory | None = None
|
|
@@ -315,20 +315,21 @@ class CodeReviewer(dspy.Module):
|
|
|
315
315
|
)
|
|
316
316
|
pr_ctx = review_context.pr_context
|
|
317
317
|
topics = [scope.topic(pr.repo_full_name), pr_ctx.to_topic()] if pr else []
|
|
318
|
-
|
|
319
|
-
|
|
318
|
+
inject_context_memory(agent)
|
|
319
|
+
hippo = Hippocampus(
|
|
320
|
+
task_name="code_review",
|
|
320
321
|
budget=self._settings.get_memory_budget("code_review"),
|
|
321
|
-
max_reflects=self._settings.get_memory_max_reflects("code_review"),
|
|
322
322
|
question=question,
|
|
323
|
-
task_name="code_review",
|
|
324
323
|
run_id=run_id,
|
|
325
324
|
initial_memory=scope_initial_memory,
|
|
326
325
|
topics=topics,
|
|
327
326
|
)
|
|
328
|
-
result = await
|
|
327
|
+
result = await agent.acall(
|
|
328
|
+
context_memory=hippo.context_memory,
|
|
329
329
|
scope=scoped,
|
|
330
330
|
categories=categories,
|
|
331
331
|
)
|
|
332
|
+
await hippo.aobserve(result)
|
|
332
333
|
issues = [
|
|
333
334
|
issue
|
|
334
335
|
for issue in (result.issues or [])
|
|
@@ -336,9 +337,9 @@ class CodeReviewer(dspy.Module):
|
|
|
336
337
|
]
|
|
337
338
|
# Fire-and-forget background episode save
|
|
338
339
|
_artifacts = {"review": issues_to_markdown(issues)}
|
|
339
|
-
def _persist(
|
|
340
|
+
def _persist(h=hippo, s=store, a=_artifacts):
|
|
340
341
|
try:
|
|
341
|
-
|
|
342
|
+
h.end_episode(s, artifacts=a)
|
|
342
343
|
except Exception:
|
|
343
344
|
logger.warning("Background code_review episode save failed", exc_info=True)
|
|
344
345
|
submit_episode_save(_persist, name="code-review-episode-save")
|
|
@@ -11,7 +11,7 @@ import dspy # type: ignore[import-untyped]
|
|
|
11
11
|
|
|
12
12
|
from codespy.agents import SignatureContext, get_cost_tracker
|
|
13
13
|
from codespy.agents.context_safe import ContextSafe
|
|
14
|
-
from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus
|
|
14
|
+
from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus, inject_context_memory
|
|
15
15
|
from codespy.agents.memory.hippocampus.episode import submit_episode_save
|
|
16
16
|
from codespy.agents.review.models import Issue, IssueCategory, ReviewContext
|
|
17
17
|
from codespy.agents.review.scope.models import ScopeResult
|
|
@@ -211,7 +211,7 @@ class DocReviewer(dspy.Module):
|
|
|
211
211
|
logger.info(
|
|
212
212
|
f" Doc review: scope {scope.subroot} ({len(scope.changed_files)} files)"
|
|
213
213
|
)
|
|
214
|
-
|
|
214
|
+
hippo: Hippocampus | None = None
|
|
215
215
|
async with SignatureContext("doc", self._cost_tracker):
|
|
216
216
|
# Load own prior "doc" episode for this scope
|
|
217
217
|
scope_initial_memory: ContextMemory | None = None
|
|
@@ -237,21 +237,22 @@ class DocReviewer(dspy.Module):
|
|
|
237
237
|
)
|
|
238
238
|
pr_ctx = review_context.pr_context
|
|
239
239
|
topics = [scope.topic(pr.repo_full_name), pr_ctx.to_topic()] if pr else []
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
inject_context_memory(reviewer)
|
|
241
|
+
hippo = Hippocampus(
|
|
242
|
+
task_name="doc",
|
|
242
243
|
budget=self._settings.get_memory_budget("doc"),
|
|
243
|
-
max_reflects=self._settings.get_memory_max_reflects("doc"),
|
|
244
244
|
question=question,
|
|
245
|
-
task_name="doc",
|
|
246
245
|
run_id=run_id,
|
|
247
246
|
initial_memory=scope_initial_memory,
|
|
248
247
|
topics=topics,
|
|
249
248
|
)
|
|
250
|
-
result = await
|
|
249
|
+
result = await reviewer.aforward(
|
|
250
|
+
context_memory=hippo.context_memory,
|
|
251
251
|
patches=patches,
|
|
252
252
|
documentation=documentation,
|
|
253
253
|
categories=[IssueCategory.DOCUMENTATION],
|
|
254
254
|
)
|
|
255
|
+
await hippo.aobserve(result)
|
|
255
256
|
issues = [
|
|
256
257
|
issue
|
|
257
258
|
for issue in (result.issues or [])
|
|
@@ -259,9 +260,9 @@ class DocReviewer(dspy.Module):
|
|
|
259
260
|
]
|
|
260
261
|
# Fire-and-forget background episode save
|
|
261
262
|
_artifacts = {"review": issues_to_markdown(issues)}
|
|
262
|
-
def _persist(
|
|
263
|
+
def _persist(h=hippo, s=store, a=_artifacts):
|
|
263
264
|
try:
|
|
264
|
-
|
|
265
|
+
h.end_episode(s, artifacts=a)
|
|
265
266
|
except Exception:
|
|
266
267
|
logger.warning("Background doc episode save failed", exc_info=True)
|
|
267
268
|
submit_episode_save(_persist, name="doc-episode-save")
|