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.
Files changed (115) hide show
  1. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/PKG-INFO +1 -1
  2. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/pyproject.toml +1 -1
  3. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/__init__.py +1 -1
  4. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/context_safe.py +3 -3
  5. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/__init__.py +7 -3
  6. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/hippocampus.py +109 -152
  7. codespy_ai-1.2.3/src/codespy/agents/review/__init__.py +6 -0
  8. codespy_ai-1.2.3/src/codespy/agents/review/audit/__init__.py +1 -0
  9. 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
  10. codespy_ai-1.2.3/src/codespy/agents/review/code_review/__init__.py +1 -0
  11. 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
  12. codespy_ai-1.2.3/src/codespy/agents/review/doc/__init__.py +1 -0
  13. 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
  14. {codespy_ai-1.2.1/src/codespy/agents/reviewer/modules → codespy_ai-1.2.3/src/codespy/agents/review}/helpers.py +28 -4
  15. codespy_ai-1.2.3/src/codespy/agents/review/models.py +138 -0
  16. codespy_ai-1.2.3/src/codespy/agents/review/scope/__init__.py +2 -0
  17. 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
  18. codespy_ai-1.2.3/src/codespy/agents/review/scope/models.py +91 -0
  19. codespy_ai-1.2.3/src/codespy/agents/review/summary/__init__.py +1 -0
  20. 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
  21. codespy_ai-1.2.3/src/codespy/agents/review/supply_chain/__init__.py +1 -0
  22. 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
  23. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/cli_local.py +6 -6
  24. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/cli_mcp_server.py +1 -1
  25. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/cli_remote.py +3 -3
  26. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config.py +2 -11
  27. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config_dspy.py +0 -1
  28. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config_memory.py +2 -4
  29. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/patch_utils.py +1 -1
  30. codespy_ai-1.2.3/src/codespy/workflows/__init__.py +0 -0
  31. codespy_ai-1.2.3/src/codespy/workflows/review/__init__.py +7 -0
  32. {codespy_ai-1.2.1/src/codespy/agents/reviewer → codespy_ai-1.2.3/src/codespy/workflows/review}/models.py +32 -236
  33. codespy_ai-1.2.1/src/codespy/agents/reviewer/reviewer.py → codespy_ai-1.2.3/src/codespy/workflows/review/pipeline.py +11 -14
  34. codespy_ai-1.2.3/src/codespy/workflows/review/reporters/__init__.py +3 -0
  35. {codespy_ai-1.2.1/src/codespy/agents/reviewer → codespy_ai-1.2.3/src/codespy/workflows/review}/reporters/base.py +1 -1
  36. {codespy_ai-1.2.1/src/codespy/agents/reviewer → codespy_ai-1.2.3/src/codespy/workflows/review}/reporters/git.py +3 -2
  37. {codespy_ai-1.2.1/src/codespy/agents/reviewer → codespy_ai-1.2.3/src/codespy/workflows/review}/reporters/stdout.py +2 -2
  38. {codespy_ai-1.2.1/src/codespy/agents/reviewer → codespy_ai-1.2.3/src/codespy/workflows/review}/server.py +4 -4
  39. codespy_ai-1.2.1/src/codespy/agents/memory/hippocampus/modules/__init__.py +0 -13
  40. codespy_ai-1.2.1/src/codespy/agents/reviewer/__init__.py +0 -23
  41. codespy_ai-1.2.1/src/codespy/agents/reviewer/modules/__init__.py +0 -17
  42. codespy_ai-1.2.1/src/codespy/agents/reviewer/reporters/__init__.py +0 -11
  43. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/LICENSE +0 -0
  44. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/README.md +0 -0
  45. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/__init__.py +0 -0
  46. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/cost_tracker.py +0 -0
  47. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/dspy_config.py +0 -0
  48. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/__init__.py +0 -0
  49. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/budget.py +0 -0
  50. {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
  51. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/context_memory.py +0 -0
  52. {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
  53. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/hippocampus/episode.py +0 -0
  54. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/pg0_manager.py +0 -0
  55. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/agents/memory/postgres.py +0 -0
  56. {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
  57. {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
  58. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/cli.py +0 -0
  59. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config_git.py +0 -0
  60. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config_io.py +0 -0
  61. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config_llm.py +0 -0
  62. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/config_utils.py +0 -0
  63. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/__init__.py +0 -0
  64. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/cyber/__init__.py +0 -0
  65. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/cyber/osv/__init__.py +0 -0
  66. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/cyber/osv/client.py +0 -0
  67. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/cyber/osv/models.py +0 -0
  68. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/cyber/osv/server.py +0 -0
  69. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/__init__.py +0 -0
  70. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/base.py +0 -0
  71. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/client.py +0 -0
  72. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/github_client.py +0 -0
  73. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/gitlab_client.py +0 -0
  74. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/local_diff.py +0 -0
  75. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/models.py +0 -0
  76. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/git/server.py +0 -0
  77. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/mcp_utils.py +0 -0
  78. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/__init__.py +0 -0
  79. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
  80. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
  81. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
  82. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
  83. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
  84. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
  85. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/bash.py +0 -0
  86. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/cpp.py +0 -0
  87. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/csharp.py +0 -0
  88. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
  89. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
  90. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
  91. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
  92. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
  93. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/php.py +0 -0
  94. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
  95. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +0 -0
  96. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/ruby.py +0 -0
  97. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
  98. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
  99. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
  100. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/models.py +0 -0
  101. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
  102. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/parsers/treesitter/server.py +0 -0
  103. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/__init__.py +0 -0
  104. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/base.py +0 -0
  105. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/filesystem/__init__.py +0 -0
  106. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/filesystem/client.py +0 -0
  107. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/filesystem/server.py +0 -0
  108. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/models.py +0 -0
  109. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/s3/__init__.py +0 -0
  110. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/s3/client.py +0 -0
  111. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/storage/s3/server.py +0 -0
  112. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/web/__init__.py +0 -0
  113. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/web/client.py +0 -0
  114. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/web/models.py +0 -0
  115. {codespy_ai-1.2.1 → codespy_ai-1.2.3}/src/codespy/tools/web/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codespy-ai
3
- Version: 1.2.1
3
+ Version: 1.2.3
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.1"
3
+ version = "1.2.3"
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.1"
3
+ __version__ = "1.2.3"
@@ -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]:
@@ -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.reviewer.models import Issue, ReviewContext
13
- from codespy.agents.reviewer.modules.scope_resolver import _deepest_common_folder
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.reviewer.models import ScopeResult
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
- mem = Hippocampus(
98
- auditor,
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 = mem(
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
- mem.end_episode(store, artifacts=_artifacts)
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.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult
17
- from codespy.agents.reviewer.modules.helpers import (
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
- mem: Hippocampus | None = None
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
- mem = Hippocampus(
318
- agent,
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 mem.aforward(
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(m=mem, s=store, a=_artifacts):
340
+ def _persist(h=hippo, s=store, a=_artifacts):
339
341
  try:
340
- m.end_episode(s, artifacts=a)
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