codespy-ai 1.2.1__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.1 → codespy_ai-1.2.3}/PKG-INFO +1 -1
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/pyproject.toml +1 -1
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/__init__.py +1 -1
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/context_safe.py +3 -3
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/__init__.py +7 -3
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/hippocampus.py +109 -152
- codespy_ai-1.2.3/src/codespy/agents/review/__init__.py +6 -0
- codespy_ai-1.2.3/src/codespy/agents/review/audit/__init__.py +1 -0
- codespy_ai-1.2.1/src/codespy/agents/reviewer/modules/auditor.py → codespy_ai-1.2.3/src/codespy/agents/review/audit/agent.py +11 -10
- codespy_ai-1.2.3/src/codespy/agents/review/code_review/__init__.py +1 -0
- codespy_ai-1.2.1/src/codespy/agents/reviewer/modules/code_reviewer.py → codespy_ai-1.2.3/src/codespy/agents/review/code_review/agent.py +13 -11
- codespy_ai-1.2.3/src/codespy/agents/review/doc/__init__.py +1 -0
- codespy_ai-1.2.1/src/codespy/agents/reviewer/modules/doc_reviewer.py → codespy_ai-1.2.3/src/codespy/agents/review/doc/agent.py +14 -12
- {codespy_ai-1.2.1/src/codespy/agents/reviewer/modules → codespy_ai-1.2.3/src/codespy/agents/review}/helpers.py +28 -4
- codespy_ai-1.2.3/src/codespy/agents/review/models.py +138 -0
- codespy_ai-1.2.3/src/codespy/agents/review/scope/__init__.py +2 -0
- codespy_ai-1.2.1/src/codespy/agents/reviewer/modules/scope_resolver.py → codespy_ai-1.2.3/src/codespy/agents/review/scope/agent.py +16 -42
- codespy_ai-1.2.3/src/codespy/agents/review/scope/models.py +91 -0
- codespy_ai-1.2.3/src/codespy/agents/review/summary/__init__.py +1 -0
- codespy_ai-1.2.1/src/codespy/agents/reviewer/modules/summarizer.py → codespy_ai-1.2.3/src/codespy/agents/review/summary/agent.py +12 -10
- codespy_ai-1.2.3/src/codespy/agents/review/supply_chain/__init__.py +1 -0
- codespy_ai-1.2.1/src/codespy/agents/reviewer/modules/supply_chain_auditor.py → codespy_ai-1.2.3/src/codespy/agents/review/supply_chain/agent.py +14 -12
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/cli_local.py +6 -6
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/cli_mcp_server.py +1 -1
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/cli_remote.py +3 -3
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config.py +2 -11
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config_dspy.py +0 -1
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config_memory.py +2 -4
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/patch_utils.py +1 -1
- codespy_ai-1.2.3/src/codespy/workflows/__init__.py +0 -0
- codespy_ai-1.2.3/src/codespy/workflows/review/__init__.py +7 -0
- {codespy_ai-1.2.1/src/codespy/agents/reviewer → codespy_ai-1.2.3/src/codespy/workflows/review}/models.py +32 -236
- codespy_ai-1.2.1/src/codespy/agents/reviewer/reviewer.py → codespy_ai-1.2.3/src/codespy/workflows/review/pipeline.py +11 -14
- codespy_ai-1.2.3/src/codespy/workflows/review/reporters/__init__.py +3 -0
- {codespy_ai-1.2.1/src/codespy/agents/reviewer → codespy_ai-1.2.3/src/codespy/workflows/review}/reporters/base.py +1 -1
- {codespy_ai-1.2.1/src/codespy/agents/reviewer → codespy_ai-1.2.3/src/codespy/workflows/review}/reporters/git.py +3 -2
- {codespy_ai-1.2.1/src/codespy/agents/reviewer → codespy_ai-1.2.3/src/codespy/workflows/review}/reporters/stdout.py +2 -2
- {codespy_ai-1.2.1/src/codespy/agents/reviewer → codespy_ai-1.2.3/src/codespy/workflows/review}/server.py +4 -4
- codespy_ai-1.2.1/src/codespy/agents/memory/hippocampus/modules/__init__.py +0 -13
- codespy_ai-1.2.1/src/codespy/agents/reviewer/__init__.py +0 -23
- codespy_ai-1.2.1/src/codespy/agents/reviewer/modules/__init__.py +0 -17
- codespy_ai-1.2.1/src/codespy/agents/reviewer/reporters/__init__.py +0 -11
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/LICENSE +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/README.md +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/cost_tracker.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/dspy_config.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/budget.py +0 -0
- {codespy_ai-1.2.1/src/codespy/agents/memory/hippocampus/modules → codespy_ai-1.2.3/src/codespy/agents/memory/hippocampus}/cartographer.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/context_memory.py +0 -0
- {codespy_ai-1.2.1/src/codespy/agents/memory/hippocampus/modules → codespy_ai-1.2.3/src/codespy/agents/memory/hippocampus}/distiller.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/episode.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/pg0_manager.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/postgres.py +0 -0
- {codespy_ai-1.2.1/src/codespy/agents/reviewer/modules → codespy_ai-1.2.3/src/codespy/agents/review/doc}/doc_extractor.py +0 -0
- {codespy_ai-1.2.1/src/codespy/agents/reviewer/modules → codespy_ai-1.2.3/src/codespy/agents/review/scope}/manifest_parser.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/cli.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config_git.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config_io.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config_llm.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config_utils.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/cyber/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/cyber/osv/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/cyber/osv/client.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/cyber/osv/models.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/cyber/osv/server.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/base.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/client.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/github_client.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/gitlab_client.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/local_diff.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/models.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/server.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/mcp_utils.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/bash.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/cpp.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/csharp.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/php.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/ruby.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/models.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/server.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/base.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/filesystem/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/filesystem/client.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/filesystem/server.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/models.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/s3/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/s3/client.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/s3/server.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/web/__init__.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/web/client.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/web/models.py +0 -0
- {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/web/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]:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from codespy.agents.review.audit import Auditor
|
|
2
|
+
from codespy.agents.review.code_review import CodeReviewer
|
|
3
|
+
from codespy.agents.review.doc import DocReviewer
|
|
4
|
+
from codespy.agents.review.scope import ScopeResolver
|
|
5
|
+
from codespy.agents.review.summary import Summarizer
|
|
6
|
+
from codespy.agents.review.supply_chain import SupplyChainAuditor
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from codespy.agents.review.audit.agent import Auditor
|
|
@@ -7,15 +7,15 @@ 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
11
|
from codespy.agents.memory.hippocampus.context_memory import Topic
|
|
12
|
-
from codespy.agents.
|
|
13
|
-
from codespy.agents.
|
|
12
|
+
from codespy.agents.review.models import Issue, ReviewContext
|
|
13
|
+
from codespy.agents.review.helpers import deepest_common_folder
|
|
14
14
|
from codespy.config import get_settings
|
|
15
15
|
from codespy.config_memory import get_episode_store
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
|
-
from codespy.agents.
|
|
18
|
+
from codespy.agents.review.scope.models import ScopeResult
|
|
19
19
|
|
|
20
20
|
logger = logging.getLogger(__name__)
|
|
21
21
|
|
|
@@ -94,21 +94,22 @@ class Auditor(dspy.Module):
|
|
|
94
94
|
if scope_topic:
|
|
95
95
|
scope_topics.append(scope_topic)
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
inject_context_memory(auditor)
|
|
98
|
+
hippo = Hippocampus(
|
|
99
|
+
task_name="audit",
|
|
99
100
|
budget=self._settings.get_memory_budget("audit"),
|
|
100
|
-
max_reflects=self._settings.get_memory_max_reflects("audit"),
|
|
101
101
|
question=question,
|
|
102
|
-
task_name="audit",
|
|
103
102
|
run_id=run_id,
|
|
104
103
|
initial_memory=initial_memory,
|
|
105
104
|
topics=scope_topics if scope_topics else None,
|
|
106
105
|
)
|
|
107
|
-
result =
|
|
106
|
+
result = auditor(
|
|
107
|
+
context_memory=hippo.context_memory,
|
|
108
108
|
pr_title=review_context.pr_context.pr_title,
|
|
109
109
|
summary=review_context.pr_context.summary,
|
|
110
110
|
all_issues=all_issues,
|
|
111
111
|
)
|
|
112
|
+
hippo.observe(result)
|
|
112
113
|
# Run episode save synchronously (auditor is the last module)
|
|
113
114
|
try:
|
|
114
115
|
_artifacts = {
|
|
@@ -117,7 +118,7 @@ class Auditor(dspy.Module):
|
|
|
117
118
|
f"## Recommendation\n\n{result.recommendation}\n"
|
|
118
119
|
)
|
|
119
120
|
}
|
|
120
|
-
|
|
121
|
+
hippo.end_episode(store, artifacts=_artifacts)
|
|
121
122
|
except Exception:
|
|
122
123
|
logger.warning("Audit episode save failed", exc_info=True)
|
|
123
124
|
else:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from codespy.agents.review.code_review.agent import CodeReviewer
|
|
@@ -11,10 +11,11 @@ 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
|
-
from codespy.agents.
|
|
17
|
-
from codespy.agents.
|
|
16
|
+
from codespy.agents.review.models import Issue, IssueCategory, ReviewContext
|
|
17
|
+
from codespy.agents.review.scope.models import ScopeResult
|
|
18
|
+
from codespy.agents.review.helpers import (
|
|
18
19
|
issues_to_markdown,
|
|
19
20
|
make_scope_relative,
|
|
20
21
|
resolve_scope_root,
|
|
@@ -288,7 +289,7 @@ class CodeReviewer(dspy.Module):
|
|
|
288
289
|
logger.info(
|
|
289
290
|
f" Code review: scope {scope.subroot} ({len(scope.changed_files)} files)"
|
|
290
291
|
)
|
|
291
|
-
|
|
292
|
+
hippo: Hippocampus | None = None
|
|
292
293
|
async with SignatureContext("code_review", self._cost_tracker):
|
|
293
294
|
# Load own prior "code_review" episode for this scope
|
|
294
295
|
scope_initial_memory: ContextMemory | None = None
|
|
@@ -314,20 +315,21 @@ class CodeReviewer(dspy.Module):
|
|
|
314
315
|
)
|
|
315
316
|
pr_ctx = review_context.pr_context
|
|
316
317
|
topics = [scope.topic(pr.repo_full_name), pr_ctx.to_topic()] if pr else []
|
|
317
|
-
|
|
318
|
-
|
|
318
|
+
inject_context_memory(agent)
|
|
319
|
+
hippo = Hippocampus(
|
|
320
|
+
task_name="code_review",
|
|
319
321
|
budget=self._settings.get_memory_budget("code_review"),
|
|
320
|
-
max_reflects=self._settings.get_memory_max_reflects("code_review"),
|
|
321
322
|
question=question,
|
|
322
|
-
task_name="code_review",
|
|
323
323
|
run_id=run_id,
|
|
324
324
|
initial_memory=scope_initial_memory,
|
|
325
325
|
topics=topics,
|
|
326
326
|
)
|
|
327
|
-
result = await
|
|
327
|
+
result = await agent.acall(
|
|
328
|
+
context_memory=hippo.context_memory,
|
|
328
329
|
scope=scoped,
|
|
329
330
|
categories=categories,
|
|
330
331
|
)
|
|
332
|
+
await hippo.aobserve(result)
|
|
331
333
|
issues = [
|
|
332
334
|
issue
|
|
333
335
|
for issue in (result.issues or [])
|
|
@@ -335,9 +337,9 @@ class CodeReviewer(dspy.Module):
|
|
|
335
337
|
]
|
|
336
338
|
# Fire-and-forget background episode save
|
|
337
339
|
_artifacts = {"review": issues_to_markdown(issues)}
|
|
338
|
-
def _persist(
|
|
340
|
+
def _persist(h=hippo, s=store, a=_artifacts):
|
|
339
341
|
try:
|
|
340
|
-
|
|
342
|
+
h.end_episode(s, artifacts=a)
|
|
341
343
|
except Exception:
|
|
342
344
|
logger.warning("Background code_review episode save failed", exc_info=True)
|
|
343
345
|
submit_episode_save(_persist, name="code-review-episode-save")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from codespy.agents.review.doc.agent import DocReviewer
|