codespy-ai 1.2.2__tar.gz → 1.2.4__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.
Files changed (112) hide show
  1. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/PKG-INFO +1 -1
  2. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/pyproject.toml +1 -1
  3. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/__init__.py +1 -1
  4. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/context_safe.py +3 -3
  5. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/memory/hippocampus/__init__.py +7 -3
  6. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/memory/hippocampus/hippocampus.py +109 -152
  7. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/audit/agent.py +35 -22
  8. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/code_review/agent.py +10 -9
  9. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/doc/agent.py +10 -9
  10. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/scope/agent.py +69 -21
  11. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/summary/agent.py +9 -8
  12. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/supply_chain/agent.py +10 -9
  13. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/config.py +14 -11
  14. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/config_dspy.py +1 -1
  15. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/config_memory.py +2 -4
  16. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/workflows/review/pipeline.py +4 -3
  17. codespy_ai-1.2.2/src/codespy/agents/memory/hippocampus/modules/__init__.py +0 -13
  18. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/LICENSE +0 -0
  19. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/README.md +0 -0
  20. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/__init__.py +0 -0
  21. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/cost_tracker.py +0 -0
  22. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/dspy_config.py +0 -0
  23. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/memory/__init__.py +0 -0
  24. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/memory/hippocampus/budget.py +0 -0
  25. {codespy_ai-1.2.2/src/codespy/agents/memory/hippocampus/modules → codespy_ai-1.2.4/src/codespy/agents/memory/hippocampus}/cartographer.py +0 -0
  26. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/memory/hippocampus/context_memory.py +0 -0
  27. {codespy_ai-1.2.2/src/codespy/agents/memory/hippocampus/modules → codespy_ai-1.2.4/src/codespy/agents/memory/hippocampus}/distiller.py +0 -0
  28. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/memory/hippocampus/episode.py +0 -0
  29. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/memory/pg0_manager.py +0 -0
  30. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/memory/postgres.py +0 -0
  31. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/__init__.py +0 -0
  32. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/audit/__init__.py +0 -0
  33. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/code_review/__init__.py +0 -0
  34. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/doc/__init__.py +0 -0
  35. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/doc/doc_extractor.py +0 -0
  36. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/helpers.py +0 -0
  37. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/models.py +0 -0
  38. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/scope/__init__.py +0 -0
  39. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/scope/manifest_parser.py +0 -0
  40. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/scope/models.py +0 -0
  41. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/summary/__init__.py +0 -0
  42. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/agents/review/supply_chain/__init__.py +0 -0
  43. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/cli.py +0 -0
  44. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/cli_local.py +0 -0
  45. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/cli_mcp_server.py +0 -0
  46. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/cli_remote.py +0 -0
  47. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/config_git.py +0 -0
  48. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/config_io.py +0 -0
  49. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/config_llm.py +0 -0
  50. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/config_utils.py +0 -0
  51. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/__init__.py +0 -0
  52. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/cyber/__init__.py +0 -0
  53. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/cyber/osv/__init__.py +0 -0
  54. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/cyber/osv/client.py +0 -0
  55. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/cyber/osv/models.py +0 -0
  56. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/cyber/osv/server.py +0 -0
  57. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/git/__init__.py +0 -0
  58. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/git/base.py +0 -0
  59. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/git/client.py +0 -0
  60. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/git/github_client.py +0 -0
  61. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/git/gitlab_client.py +0 -0
  62. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/git/local_diff.py +0 -0
  63. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/git/models.py +0 -0
  64. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/git/patch_utils.py +0 -0
  65. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/git/server.py +0 -0
  66. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/mcp_utils.py +0 -0
  67. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/__init__.py +0 -0
  68. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
  69. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
  70. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
  71. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
  72. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
  73. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
  74. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/bash.py +0 -0
  75. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/cpp.py +0 -0
  76. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/csharp.py +0 -0
  77. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
  78. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
  79. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
  80. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
  81. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
  82. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/php.py +0 -0
  83. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
  84. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +0 -0
  85. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/ruby.py +0 -0
  86. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
  87. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
  88. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
  89. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/models.py +0 -0
  90. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
  91. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/server.py +0 -0
  92. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/storage/__init__.py +0 -0
  93. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/storage/base.py +0 -0
  94. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/storage/filesystem/__init__.py +0 -0
  95. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/storage/filesystem/client.py +0 -0
  96. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/storage/filesystem/server.py +0 -0
  97. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/storage/models.py +0 -0
  98. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/storage/s3/__init__.py +0 -0
  99. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/storage/s3/client.py +0 -0
  100. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/storage/s3/server.py +0 -0
  101. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/web/__init__.py +0 -0
  102. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/web/client.py +0 -0
  103. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/web/models.py +0 -0
  104. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/tools/web/server.py +0 -0
  105. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/workflows/__init__.py +0 -0
  106. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/workflows/review/__init__.py +0 -0
  107. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/workflows/review/models.py +0 -0
  108. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/workflows/review/reporters/__init__.py +0 -0
  109. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/workflows/review/reporters/base.py +0 -0
  110. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/workflows/review/reporters/git.py +0 -0
  111. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/workflows/review/reporters/stdout.py +0 -0
  112. {codespy_ai-1.2.2 → codespy_ai-1.2.4}/src/codespy/workflows/review/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codespy-ai
3
- Version: 1.2.2
3
+ Version: 1.2.4
4
4
  Summary: Code review agent powered by DSPy
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "codespy-ai"
3
- version = "1.2.2"
3
+ version = "1.2.4"
4
4
  description = "Code review agent powered by DSPy"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """codespy - Code review agent powered by DSPy."""
2
2
 
3
- __version__ = "1.2.2"
3
+ __version__ = "1.2.4"
@@ -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 Hippocampus — delegates .signature to inner module.
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 Hippocampus.aforward."""
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 Hippocampus modified it)."""
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 Hippocampus
21
- from codespy.agents.memory.hippocampus.modules.cartographer import Cartographer, CartographerSig
22
- from codespy.agents.memory.hippocampus.modules.distiller import Distiller, DistillerSig
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.modules.distiller import Distiller
33
- from codespy.agents.memory.hippocampus.modules.cartographer import Cartographer
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
- class Hippocampus(dspy.Module):
53
- """Wraps a dspy.Module with a context memory that evolves via LLM-driven reflection.
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
- The context memory is prepended to every agent call so the agent starts each run
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
- ## Two independent controls
62
-
63
- Reflection behaviour is governed by two orthogonal knobs:
84
+ ## Lifecycle
64
85
 
65
- 1. **``max_reflects``** — maximum number of forward() calls that also reflect *online*
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
- 2. **Calling ``end_episode()``** (or not) — whether to consolidate the buffered
71
- episode into the memory at the end. Every call is *always* buffered so
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
- Common patterns::
91
+ # Inject context memory into agent
92
+ inject_context_memory(agent)
75
93
 
76
- # Classic per-call (default) — reflect after every call, no end consolidation
77
- mem = Hippocampus(agent)
78
- pred = mem(task="…")
94
+ # Run agent with context_memory input
95
+ pred = agent(context_memory=hippo.context_memory, task="…")
79
96
 
80
- # Pure batch — no online reflection, one holistic pass at the end
81
- mem = Hippocampus(agent, max_reflects=0)
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
- # Hybrid — warm up online for the first 3 calls, then holistic consolidation
87
- mem = Hippocampus(agent, max_reflects=3)
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
- # Read-only (memory never changes) — pure inference
93
- mem = Hippocampus(agent, max_reflects=0)
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 variants (for callers running inside an event loop, e.g.
97
- # reviewer modules using `await agent.acall(...)`)
98
- mem = Hippocampus(agent, max_reflects=0)
99
- pred = await mem.acall(task="…")
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 call) — each trajectory is head+tail bounded at ``format_trajectory``
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 calls are buffered.
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
- module: dspy.Module,
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, all input fields are serialized (bounded by
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. Inference is a last resort: only
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-call bounded trajectory strings, cleared after end_episode().
185
+ # Buffer of per-observation bounded trajectory strings, cleared after end_episode().
213
186
  self._episode_trajectories: list[str] = []
214
- # Count of buffered trajectories already distilled online (via max_reflects).
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 of the wrapped module/signature for Episode metadata. An explicit
223
- # task_name wins: inference only works for modules exposing .signature.
224
- self._task_name: str
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
- def forward(self, **kwargs) -> dspy.Prediction:
251
- pred = self.agent(context_memory=self.cmem, **kwargs)
252
- self._buffer_and_distill(pred, kwargs)
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
- Awaits the wrapped agent's ``acall`` instead of invoking it
259
- synchronously — required when the caller is already inside a running
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
- pred = await self.agent.acall(context_memory=self.cmem, **kwargs)
265
- await asyncio.to_thread(self._buffer_and_distill, pred, kwargs)
266
- return pred
211
+ return self.cmem
267
212
 
268
- def _buffer_and_distill(self, pred: dspy.Prediction, kwargs: dict) -> None:
269
- """Shared post-call work for both ``forward`` and ``aforward``.
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
- Buffers the (stage-1 bounded) trajectory and, depending on
272
- ``max_reflects``, runs an online distill+apply pass immediately.
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
- max_traj = self.budget.max_trajectory_tokens if self.budget.compact_trajectory else None
275
- traj = format_trajectory(pred, max_traj)
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._make_question(kwargs)
278
- # Online reflection: None = no limit (always); N = for the first N calls.
279
- if self.max_reflects is None or len(self._episode_trajectories) <= self.max_reflects:
280
- try:
281
- self._distill(traj, self._episode_question)
282
- self._reflected_count += 1
283
- except Exception:
284
- logger.warning(
285
- "Online reflection failed for %s; trajectory buffered for end_episode().",
286
- self._task_name,
287
- exc_info=True,
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
- skip_double_distill = len(self._episode_trajectories) == 1 and self._reflected_count > 0
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-call bounding
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 call. No-op if the buffer is empty.
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
- has_content = combined is not None or self._reflected_count > 0
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
- has_content = combined is not None or self._reflected_count > 0
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,9 @@ 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
12
+ from codespy.agents.memory.hippocampus.episode import submit_episode_save
11
13
  from codespy.agents.review.models import Issue, ReviewContext
12
14
  from codespy.agents.review.helpers import deepest_common_folder
13
15
  from codespy.config import get_settings
@@ -15,6 +17,7 @@ from codespy.config_memory import get_episode_store
15
17
 
16
18
  if TYPE_CHECKING:
17
19
  from codespy.agents.review.scope.models import ScopeResult
20
+ from codespy.agents.memory.hippocampus.episode import EpisodeStore
18
21
 
19
22
  logger = logging.getLogger(__name__)
20
23
 
@@ -56,8 +59,13 @@ class Auditor(dspy.Module):
56
59
  run_id: str | None,
57
60
  scopes: list["ScopeResult"] | None,
58
61
  topics: list["ScopeResult"] | None = None,
59
- ) -> dspy.Prediction:
60
- """Execute the auditor predictor (with or without Hippocampus memory)."""
62
+ ) -> tuple[dspy.Prediction, Hippocampus | None, "EpisodeStore | None", dict[str, str] | None]:
63
+ """Execute the auditor predictor (with or without Hippocampus memory).
64
+
65
+ Returns:
66
+ Tuple of (result, hippo, store, artifacts) where hippo/store/artifacts
67
+ are None when memory is disabled.
68
+ """
61
69
  question = (
62
70
  f"final audit of {review_context.pr_context.repo_slug}: "
63
71
  f"pull request {review_context.pr_context.pr_number} "
@@ -93,40 +101,36 @@ class Auditor(dspy.Module):
93
101
  if scope_topic:
94
102
  scope_topics.append(scope_topic)
95
103
 
96
- mem = Hippocampus(
97
- auditor,
104
+ inject_context_memory(auditor)
105
+ hippo = Hippocampus(
106
+ task_name="audit",
98
107
  budget=self._settings.get_memory_budget("audit"),
99
- max_reflects=self._settings.get_memory_max_reflects("audit"),
100
108
  question=question,
101
- task_name="audit",
102
109
  run_id=run_id,
103
110
  initial_memory=initial_memory,
104
111
  topics=scope_topics if scope_topics else None,
105
112
  )
106
- result = mem(
113
+ result = auditor(
114
+ context_memory=hippo.context_memory,
107
115
  pr_title=review_context.pr_context.pr_title,
108
116
  summary=review_context.pr_context.summary,
109
117
  all_issues=all_issues,
110
118
  )
111
- # Run episode save synchronously (auditor is the last module)
112
- try:
113
- _artifacts = {
114
- "audit": (
115
- f"## Quality Assessment\n\n{result.quality_assessment}\n\n"
116
- f"## Recommendation\n\n{result.recommendation}\n"
117
- )
118
- }
119
- mem.end_episode(store, artifacts=_artifacts)
120
- except Exception:
121
- logger.warning("Audit episode save failed", exc_info=True)
119
+ hippo.observe(result)
120
+ _artifacts = {
121
+ "audit": (
122
+ f"## Quality Assessment\n\n{result.quality_assessment}\n\n"
123
+ f"## Recommendation\n\n{result.recommendation}\n"
124
+ )
125
+ }
126
+ return result, hippo, store, _artifacts
122
127
  else:
123
128
  result = auditor(
124
129
  pr_title=review_context.pr_context.pr_title,
125
130
  summary=review_context.pr_context.summary,
126
131
  all_issues=all_issues,
127
132
  )
128
-
129
- return result
133
+ return result, None, None, None
130
134
 
131
135
  def forward(
132
136
  self,
@@ -166,7 +170,7 @@ class Auditor(dspy.Module):
166
170
  logger.info("Running audit...")
167
171
 
168
172
  with SignatureContext("audit", self._cost_tracker):
169
- result = self._call_auditor(
173
+ result, hippo, store, _artifacts = self._call_auditor(
170
174
  auditor,
171
175
  review_context,
172
176
  all_issues,
@@ -175,4 +179,13 @@ class Auditor(dspy.Module):
175
179
  topics,
176
180
  )
177
181
 
182
+ # Background episode save (after SignatureContext closes)
183
+ if hippo is not None and store is not None:
184
+ def _persist():
185
+ try:
186
+ hippo.end_episode(store, artifacts=_artifacts)
187
+ except Exception:
188
+ logger.warning("Audit episode save failed", exc_info=True)
189
+ submit_episode_save(_persist, name="audit-episode-save")
190
+
178
191
  return result.quality_assessment, result.recommendation
@@ -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
- mem: Hippocampus | None = None
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
- mem = Hippocampus(
319
- agent,
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 mem.aforward(
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(m=mem, s=store, a=_artifacts):
340
+ def _persist(h=hippo, s=store, a=_artifacts):
340
341
  try:
341
- m.end_episode(s, artifacts=a)
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")