codespy-ai 2.0.2__tar.gz → 2.1.1__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 (126) hide show
  1. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/PKG-INFO +2 -2
  2. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/pyproject.toml +2 -2
  3. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/__init__.py +1 -1
  4. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/cerebral/cerebral.py +75 -38
  5. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/cerebral/cost.py +12 -6
  6. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/cerebral/structured_output.py +7 -199
  7. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/postgres.py +15 -7
  8. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/prefrontal/format.py +1 -5
  9. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/prefrontal/prefrontal.py +1 -2
  10. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config_memory.py +136 -286
  11. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/pipeline.py +2 -2
  12. codespy_ai-2.0.2/src/codespy/agents/memory/cerebral/delta_ops_compat.py +0 -197
  13. codespy_ai-2.0.2/src/codespy/agents/memory/cerebral/routines.py +0 -230
  14. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/LICENSE +0 -0
  15. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/README.md +0 -0
  16. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/__init__.py +0 -0
  17. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/context_safe.py +0 -0
  18. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/cost_tracker.py +0 -0
  19. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/diagnostic_adapter.py +0 -0
  20. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/dspy_config.py +0 -0
  21. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/__init__.py +0 -0
  22. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/cerebral/__init__.py +0 -0
  23. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/cerebral/orphans.py +0 -0
  24. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/__init__.py +0 -0
  25. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/budget.py +0 -0
  26. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/cartographer.py +0 -0
  27. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/context_memory.py +0 -0
  28. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/distiller.py +0 -0
  29. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/episode.py +0 -0
  30. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/hippocampus.py +0 -0
  31. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/pg0_manager.py +0 -0
  32. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/prefrontal/__init__.py +0 -0
  33. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/prefrontal/facets.py +0 -0
  34. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/prefrontal/query.py +0 -0
  35. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/prefrontal/reach.py +0 -0
  36. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/recall.py +0 -0
  37. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/__init__.py +0 -0
  38. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/audit/__init__.py +0 -0
  39. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/audit/agent.py +0 -0
  40. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/code_review/__init__.py +0 -0
  41. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/code_review/agent.py +0 -0
  42. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/doc/__init__.py +0 -0
  43. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/doc/agent.py +0 -0
  44. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/doc/doc_extractor.py +0 -0
  45. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/helpers.py +0 -0
  46. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/models.py +0 -0
  47. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/scope/__init__.py +0 -0
  48. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/scope/agent.py +0 -0
  49. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/scope/manifest_parser.py +0 -0
  50. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/scope/models.py +0 -0
  51. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/summary/__init__.py +0 -0
  52. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/summary/agent.py +0 -0
  53. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/supply_chain/__init__.py +0 -0
  54. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/supply_chain/agent.py +0 -0
  55. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/rlm_tools.py +0 -0
  56. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/cli.py +0 -0
  57. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/cli_local.py +0 -0
  58. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/cli_mcp_server.py +0 -0
  59. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/cli_remote.py +0 -0
  60. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config.py +0 -0
  61. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config_dspy.py +0 -0
  62. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config_git.py +0 -0
  63. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config_io.py +0 -0
  64. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config_llm.py +0 -0
  65. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config_utils.py +0 -0
  66. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/__init__.py +0 -0
  67. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/cyber/__init__.py +0 -0
  68. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/cyber/osv/__init__.py +0 -0
  69. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/cyber/osv/client.py +0 -0
  70. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/cyber/osv/models.py +0 -0
  71. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/cyber/osv/server.py +0 -0
  72. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/__init__.py +0 -0
  73. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/base.py +0 -0
  74. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/client.py +0 -0
  75. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/github_client.py +0 -0
  76. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/gitlab_client.py +0 -0
  77. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/local_diff.py +0 -0
  78. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/models.py +0 -0
  79. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/server.py +0 -0
  80. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/mcp_utils.py +0 -0
  81. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/__init__.py +0 -0
  82. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
  83. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
  84. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
  85. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
  86. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
  87. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
  88. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/bash.py +0 -0
  89. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/cpp.py +0 -0
  90. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/csharp.py +0 -0
  91. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
  92. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
  93. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
  94. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
  95. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
  96. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/php.py +0 -0
  97. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
  98. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +0 -0
  99. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/ruby.py +0 -0
  100. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
  101. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
  102. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
  103. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/models.py +0 -0
  104. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
  105. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/server.py +0 -0
  106. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/__init__.py +0 -0
  107. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/base.py +0 -0
  108. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/filesystem/__init__.py +0 -0
  109. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/filesystem/client.py +0 -0
  110. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/filesystem/server.py +0 -0
  111. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/models.py +0 -0
  112. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/s3/__init__.py +0 -0
  113. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/s3/client.py +0 -0
  114. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/s3/server.py +0 -0
  115. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/web/__init__.py +0 -0
  116. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/web/client.py +0 -0
  117. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/web/models.py +0 -0
  118. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/web/server.py +0 -0
  119. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/__init__.py +0 -0
  120. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/__init__.py +0 -0
  121. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/models.py +0 -0
  122. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/reporters/__init__.py +0 -0
  123. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/reporters/base.py +0 -0
  124. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/reporters/git.py +0 -0
  125. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/reporters/stdout.py +0 -0
  126. {codespy_ai-2.0.2 → codespy_ai-2.1.1}/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: 2.0.2
3
+ Version: 2.1.1
4
4
  Summary: Code review agent powered by DSPy
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -23,7 +23,7 @@ Requires-Dist: cloudpickle (>=3.1.2,<4.0.0)
23
23
  Requires-Dist: ddgs (>=8.0.0)
24
24
  Requires-Dist: dspy[mcp] (>=3.3.0,<4.0.0)
25
25
  Requires-Dist: gitpython (>=3.1.42)
26
- Requires-Dist: hindsight-api-slim (>=0.10.1)
26
+ Requires-Dist: hindsight-api-slim (>=1.0.0,<2)
27
27
  Requires-Dist: httpx (>=0.28.0)
28
28
  Requires-Dist: json-repair (>=0.60.1)
29
29
  Requires-Dist: litellm (>=1.84.0,<2.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "codespy-ai"
3
- version = "2.0.2"
3
+ version = "2.1.1"
4
4
  description = "Code review agent powered by DSPy"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -53,7 +53,7 @@ beautifulsoup4 = ">=4.12.0"
53
53
  markdownify = ">=0.15.0"
54
54
  ddgs = ">=8.0.0"
55
55
  pg0-embedded = ">=0.15"
56
- hindsight-api-slim = ">=0.10.1"
56
+ hindsight-api-slim = ">=1.0.0,<2"
57
57
 
58
58
  [tool.poetry.group.dev.dependencies]
59
59
  pytest = ">=8.0.0"
@@ -1,3 +1,3 @@
1
1
  """codespy - Code review agent powered by DSPy."""
2
2
 
3
- __version__ = "2.0.2"
3
+ __version__ = "2.1.1"
@@ -33,7 +33,7 @@ HINDSIGHT_SCHEMA_DEFAULT = "semantic"
33
33
  # needs sentence-transformers and emits a misleading startup warning
34
34
  # (hindsight_api/config.py _validate). That config is built and cached at IMPORT
35
35
  # time — the `from hindsight_api import MemoryEngine` below pulls in
36
- # engine.llm_wrapper, whose module-level _get_raw_config() calls
36
+ # engine.llm_wrapper, which builds Hindsight's cached config via
37
37
  # HindsightConfig.from_env(). So these env vars MUST be set before that import
38
38
  # line, not in Cerebral.__init__ (too late). We inject our own LiteLLM-SDK
39
39
  # embeddings and an RRF-passthrough cross-encoder in Cerebral.__init__, so the
@@ -61,20 +61,12 @@ from hindsight_api.extensions.builtin.tenant import DefaultTenantExtension
61
61
  from hindsight_api.models import RequestContext
62
62
  from hindsight_api.worker.exceptions import DeferOperation
63
63
 
64
- # Install delta operations coercion shim for string-encoded operations
65
- # (see delta_ops_compat.py for details)
66
- from codespy.agents.memory.cerebral.delta_ops_compat import install_delta_ops_coercion
67
-
68
- install_delta_ops_coercion()
69
-
70
- # Install native structured output support and Bedrock schema sanitizer
64
+ # Register native structured output for Bedrock models litellm's map lacks the flag for
71
65
  from codespy.agents.memory.cerebral.structured_output import (
72
- install_bedrock_schema_sanitizer,
73
66
  register_native_structured_output_overrides,
74
67
  )
75
68
 
76
69
  register_native_structured_output_overrides()
77
- install_bedrock_schema_sanitizer()
78
70
 
79
71
 
80
72
  class _SchemaSyncTaskBackend(SyncTaskBackend):
@@ -233,7 +225,6 @@ from codespy.agents.memory.cerebral.cost import (
233
225
  MeteredLiteLLMSDKEmbeddings,
234
226
  register_cerebral_cost_recorder,
235
227
  )
236
- from codespy.agents.memory.cerebral.routines import ensure_maintenance_routines
237
228
  from codespy.agents.memory.prefrontal.reach import (
238
229
  TAG_ORG,
239
230
  TAG_PROJECT_SCOPE,
@@ -283,17 +274,23 @@ HINDSIGHT_SCHEMA = HINDSIGHT_SCHEMA_DEFAULT
283
274
  # - include_chunks=False: Observation text only (no file chunks)
284
275
  # - reflect_search_observations_max_tokens=3000: Down from 5000 default
285
276
  # - reflect_search_observations_include_entities=False: Exclude entity metadata
277
+ # - budget=low: a refresh gets reflect_max_iterations // 2 iterations, like Prefrontal
278
+ # reflect (Hindsight treats a null budget as MID for refreshes)
279
+ # - keep_trace=True: store the refresh trace with the briefing
280
+ # Key names must be exact: create_mental_model stores the dict without validating it.
286
281
  _BRIEFING_TRIGGER_STATIC = {
287
282
  "mode": "delta",
288
283
  "exclude_mental_models": True,
289
284
  "include_chunks": False,
290
285
  "reflect_search_observations_max_tokens": 3000,
291
286
  "reflect_search_observations_include_entities": False,
287
+ "budget": "low",
288
+ "keep_trace": True,
292
289
  }
293
290
 
294
- # Content Hindsight's HTTP API gives a mental model before its first refresh.
295
- # Prefrontal skips briefings that still hold it.
296
- MENTAL_MODEL_PLACEHOLDER = "Generating content..."
291
+ # Content pre-1.0 Hindsight gave a mental model before its first refresh.
292
+ # Only used to clear it from upgraded rows (see Cerebral._clear_legacy_placeholders).
293
+ _LEGACY_PLACEHOLDER = "Generating content..."
297
294
 
298
295
  # Generic (not code-review specific) question each briefing answers.
299
296
  MENTAL_MODEL_SOURCE_QUERY = (
@@ -465,19 +462,19 @@ class Cerebral:
465
462
  # Install two-step structured output fallback when extraction model is configured
466
463
  if extraction_llm_model:
467
464
  try:
468
- from hindsight_api.engine.llm_wrapper import LLMConfig
465
+ from hindsight_api.engine.llm_wrapper import LLMProvider
469
466
  from codespy.agents.memory.cerebral.structured_output import (
470
467
  install_two_step_structured_output,
471
468
  )
472
469
 
473
- extraction_cfg = LLMConfig(
470
+ extraction_llm = LLMProvider(
474
471
  provider="litellm",
475
472
  model=extraction_llm_model,
476
473
  api_key=extraction_llm_api_key,
477
474
  base_url=extraction_llm_base_url or "",
478
475
  reasoning_effort=None,
479
476
  )
480
- extraction_impl = extraction_cfg._provider_impl
477
+ extraction_impl = extraction_llm._provider_impl
481
478
  install_two_step_structured_output(self._engine, extraction_impl)
482
479
  except Exception:
483
480
  logger.warning(
@@ -488,11 +485,6 @@ class Cerebral:
488
485
 
489
486
  self._run_async(self._engine.initialize())
490
487
 
491
- # One-time repair: install maintenance routines if missing.
492
- # This is idempotent and only needed for databases that were migrated
493
- # before HINDSIGHT_API_DATABASE_SCHEMA was set to "semantic" (upstream #2638).
494
- self._run_async(ensure_maintenance_routines(self._engine, HINDSIGHT_SCHEMA))
495
-
496
488
  # Repair orphaned pending operations left by inline SyncTaskBackend
497
489
  from codespy.agents.memory.cerebral.orphans import repair_orphaned_operations
498
490
 
@@ -596,14 +588,11 @@ class Cerebral:
596
588
  # pending rows from blocking scoped submissions).
597
589
  "enable_auto_consolidation": False,
598
590
  }
599
- # Per-scope observation cap (not bank-wide; -1 means unlimited)
600
- if self._max_observations_per_scope >= 0:
601
- updates["observation_scope_limits"] = [
602
- {
603
- "scope": [f"{TAG_ORG}*", f"{TAG_REPO}*", f"{TAG_PROJECT_SCOPE}*"],
604
- "limit": self._max_observations_per_scope,
605
- }
606
- ]
591
+ # Per-scope observation cap (never the bank-wide max_observations_per_scope).
592
+ # Sent unconditionally so -1 (unlimited) lifts a previously stored cap.
593
+ updates["consolidation_strategies"] = self._consolidation_strategies()
594
+ # Tombstone the deprecated key (bank config merges with `config || $1`).
595
+ updates["observation_scope_limits"] = None
607
596
  self._run_async(
608
597
  self._engine.update_bank_config(
609
598
  self._bank_id,
@@ -611,10 +600,58 @@ class Cerebral:
611
600
  request_context=ctx,
612
601
  )
613
602
  )
603
+ self._clear_legacy_placeholders(ctx)
614
604
  except Exception:
615
605
  logger.warning("cerebral: failed to configure bank %s", self._bank_id, exc_info=True)
616
606
  self._bank_ensured = True
617
607
 
608
+ def _consolidation_strategies(self) -> list[dict[str, Any]]:
609
+ """``consolidation_strategies`` payload carrying the per-scope observation cap."""
610
+ return [
611
+ {
612
+ "scopes": [
613
+ {
614
+ "tags": [f"{TAG_ORG}*", f"{TAG_REPO}*", f"{TAG_PROJECT_SCOPE}*"],
615
+ "tags_match": "exact",
616
+ }
617
+ ],
618
+ "max_observations_per_scope": self._max_observations_per_scope,
619
+ }
620
+ ]
621
+
622
+ def _clear_legacy_placeholders(self, ctx: RequestContext) -> int:
623
+ """Empty briefings that still hold the pre-1.0 placeholder. Returns the count.
624
+
625
+ Pages through every mental model of the bank and rewrites legacy rows
626
+ with ``content=""`` (what Hindsight 1.0 creates pages with).
627
+ """
628
+ cleared = 0
629
+ offset = 0
630
+ while True:
631
+ page = self._run_async(
632
+ self._engine.list_mental_models(
633
+ self._bank_id,
634
+ detail="full",
635
+ limit=100,
636
+ offset=offset,
637
+ request_context=ctx,
638
+ )
639
+ )
640
+ for item in page.items:
641
+ if (item.get("content") or "").strip().startswith(_LEGACY_PLACEHOLDER):
642
+ self._run_async(
643
+ self._engine.update_mental_model(
644
+ self._bank_id, item["id"], content="", request_context=ctx
645
+ )
646
+ )
647
+ cleared += 1
648
+ offset += len(page.items)
649
+ if not page.items or offset >= page.total:
650
+ break
651
+ if cleared:
652
+ logger.info("cerebral: cleared legacy placeholder from %d briefing(s)", cleared)
653
+ return cleared
654
+
618
655
  @staticmethod
619
656
  def _mutation_lines(episode: Episode) -> list[str]:
620
657
  """Build observation lines from episode mutations.
@@ -943,7 +980,7 @@ class Cerebral:
943
980
  self._bank_id,
944
981
  self._mental_model_name(tags),
945
982
  self._mental_model_source_query(tags),
946
- MENTAL_MODEL_PLACEHOLDER,
983
+ "",
947
984
  mental_model_id=mm_id,
948
985
  tags=sorted(tags),
949
986
  max_tokens=self._max_mental_model_tokens,
@@ -965,12 +1002,12 @@ class Cerebral:
965
1002
  self._engine.get_mental_model(self._bank_id, mm_id, request_context=ctx)
966
1003
  )
967
1004
  else:
968
- # Existing briefing: update trigger if it differs
1005
+ # Existing briefing: update trigger if it differs. Stored triggers
1006
+ # are merged supersets (update_mental_model patches), so compare
1007
+ # only the keys codespy sends.
969
1008
  current_trigger = existing.get("trigger") or {}
970
- needs_update = (
971
- current_trigger.get("refresh_after_consolidation") != self._mental_models
972
- or current_trigger.get("exclude_mental_models") is not True
973
- or current_trigger.get("mode", "delta") != "delta"
1009
+ needs_update = any(
1010
+ current_trigger.get(k) != v for k, v in desired_trigger.items()
974
1011
  )
975
1012
  if needs_update:
976
1013
  self._run_async(
@@ -1280,10 +1317,10 @@ class Cerebral:
1280
1317
  summary.rewrite = True
1281
1318
 
1282
1319
  # Count iterations from agent_<n> scopes (excluding _err variants)
1283
- # +1 if there's any final scope (map, reduce, or rewrite)
1320
+ # +1 if there's any final scope (map, reduce, rewrite, or Hindsight 1.0's closing_done)
1284
1321
  agent_scopes = {s for s in scope_names if s.startswith("agent_") and not s.endswith("_err")}
1285
1322
  summary.iterations = len(agent_scopes)
1286
- if any(s.startswith("final") for s in scope_names):
1323
+ if any(s.startswith("final") or s == "closing_done" for s in scope_names):
1287
1324
  summary.iterations += 1
1288
1325
 
1289
1326
  # Tools from tool_trace (list of ToolCallTrace with output dict)
@@ -117,6 +117,7 @@ class CerebralCostRecorder:
117
117
  scope: str = "",
118
118
  input_tokens: int = 0,
119
119
  output_tokens: int = 0,
120
+ thoughts_tokens: int | None = 0,
120
121
  error: Exception | None = None,
121
122
  duration: float = 0.0,
122
123
  **_: Any,
@@ -127,7 +128,10 @@ class CerebralCostRecorder:
127
128
  model: Full litellm model string (e.g., "bedrock/converse/moonshotai.kimi-k2.5")
128
129
  scope: Hindsight scope (e.g., "retain_extract_facts", "consolidation")
129
130
  input_tokens: Input/prompt token count
130
- output_tokens: Output/completion token count
131
+ output_tokens: Visible output/completion token count
132
+ thoughts_tokens: Reasoning tokens. Hindsight >=0.10.2 reports them apart
133
+ from ``output_tokens``; they are billed as output, so they are
134
+ folded into the output count used for pricing and tracking.
131
135
  error: Exception if the call failed
132
136
  duration: Duration of the call in seconds
133
137
  **_: Ignored extra kwargs for forward compatibility
@@ -137,21 +141,23 @@ class CerebralCostRecorder:
137
141
  and errored calls that reached the provider (with billed usage).
138
142
  """
139
143
  try:
144
+ billed_output = output_tokens + (thoughts_tokens or 0)
145
+
140
146
  # Skip calls with no tokens (includes failed calls that never reached provider)
141
- if input_tokens == 0 and output_tokens == 0:
147
+ if input_tokens == 0 and billed_output == 0:
142
148
  return
143
149
 
144
150
  # Price the call using litellm
145
- prompt_cost, completion_cost = self._price_call_split(model, input_tokens, output_tokens)
151
+ prompt_cost, completion_cost = self._price_call_split(model, input_tokens, billed_output)
146
152
  cost = prompt_cost + completion_cost
147
- tokens = input_tokens + output_tokens
153
+ tokens = input_tokens + billed_output
148
154
 
149
155
  # Determine bucket: an active recall wins over the scope/task tag,
150
156
  # because Hindsight scopes cannot tell a reflect apart
151
157
  # from a mental-model refresh.
152
158
  usage = current_recall_usage.get()
153
159
  if usage is not None:
154
- usage.add_llm(model, input_tokens, output_tokens, prompt_cost, completion_cost)
160
+ usage.add_llm(model, input_tokens, billed_output, prompt_cost, completion_cost)
155
161
  bucket = BUCKET_MEMORY_RECALL
156
162
  else:
157
163
  bucket = _llm_bucket(scope)
@@ -166,7 +172,7 @@ class CerebralCostRecorder:
166
172
  tokens=tokens,
167
173
  calls=1,
168
174
  input_tokens=input_tokens,
169
- output_tokens=output_tokens,
175
+ output_tokens=billed_output,
170
176
  input_cost=prompt_cost,
171
177
  output_cost=completion_cost,
172
178
  duration=duration,
@@ -3,7 +3,6 @@
3
3
  Provides:
4
4
  - Native structured output registration for Bedrock models that support it
5
5
  but lack the flag in litellm's model map.
6
- - JSON schema sanitization for Bedrock's structured output constraints.
7
6
  - Two-step structured output fallback for models that don't enforce schemas.
8
7
 
9
8
  See: .kilo/plans/1790926015516-retain-facts-string-coercion.md
@@ -11,7 +10,6 @@ See: .kilo/plans/1790926015516-retain-facts-string-coercion.md
11
10
 
12
11
  from __future__ import annotations
13
12
 
14
- import copy
15
13
  import logging
16
14
  from typing import Any, Callable
17
15
 
@@ -25,10 +23,6 @@ _NATIVE_STRUCTURED_OUTPUT_OVERRIDES: dict[str, str] = {
25
23
  "nvidia.nemotron-super-3-120b": "bedrock_converse",
26
24
  }
27
25
 
28
- # Marker for idempotent wrapper installation
29
- _SCHEMA_SANITIZER_MARKER = "_codespy_schema_sanitizer"
30
-
31
-
32
26
  def register_native_structured_output_overrides() -> None:
33
27
  """Register native structured output capability for known-capable models.
34
28
 
@@ -89,198 +83,6 @@ def _verify_registration(model: str) -> None:
89
83
  logger.warning("Could not verify native structured output for %s: %s", model, e)
90
84
 
91
85
 
92
- def _strip_disallowed_keywords(schema: dict[str, Any]) -> dict[str, Any]:
93
- """Recursively strip JSON Schema keywords disallowed by Bedrock.
94
-
95
- Bedrock's structured output accepts a subset of Draft 2020-12.
96
- Disallowed keywords are silently rejected with a 400.
97
-
98
- Strips:
99
- - minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf
100
- - minLength, maxLength, pattern
101
- - maxItems
102
- - minItems with value > 1 (minItems: 0 or 1 is allowed)
103
- - format values outside: date-time, time, date, duration, email,
104
- hostname, uri, ipv4, ipv6, uuid
105
- - additionalProperties values other than false (forced to false)
106
-
107
- Recurses through: properties, items, $defs, definitions, anyOf, allOf, oneOf.
108
- Keys under properties/$defs are names, not keywords - never stripped.
109
-
110
- Returns a deep copy; input is never mutated.
111
- """
112
- if not isinstance(schema, dict):
113
- return schema
114
-
115
- # Deep copy to avoid mutating input
116
- result = copy.deepcopy(schema)
117
- return _strip_in_place(result)
118
-
119
-
120
- def _strip_in_place(schema: Any) -> Any:
121
- """In-place recursive strip. Internal helper."""
122
- if not isinstance(schema, dict):
123
- return schema
124
-
125
- allowed_formats = {
126
- "date-time",
127
- "time",
128
- "date",
129
- "duration",
130
- "email",
131
- "hostname",
132
- "uri",
133
- "ipv4",
134
- "ipv6",
135
- "uuid",
136
- }
137
-
138
- # Build list of keys to strip from this level
139
- to_strip: list[str] = []
140
- modified: list[str] = []
141
-
142
- for key, value in schema.items():
143
- if key in (
144
- "minimum",
145
- "maximum",
146
- "exclusiveMinimum",
147
- "exclusiveMaximum",
148
- "multipleOf",
149
- "minLength",
150
- "maxLength",
151
- "pattern",
152
- "maxItems",
153
- ):
154
- to_strip.append(key)
155
- modified.append(key)
156
- elif key == "minItems":
157
- # Only strip if value > 1
158
- if isinstance(value, int) and value > 1:
159
- to_strip.append(key)
160
- modified.append(key)
161
- elif key == "format":
162
- if value not in allowed_formats:
163
- to_strip.append(key)
164
- modified.append(f"format:{value}")
165
- elif key == "additionalProperties":
166
- # Force to false if not already false
167
- if value is not False:
168
- schema[key] = False
169
- modified.append(f"additionalProperties->{value}")
170
-
171
- # Strip disallowed keys
172
- for key in to_strip:
173
- del schema[key]
174
-
175
- # Log what was stripped at this level
176
- if modified:
177
- logger.debug("Bedrock schema sanitizer stripped: %s", modified)
178
-
179
- # Recurse into sub-schemas
180
- # Keys under properties/$defs are names, not keywords - preserve them
181
- for key in ("properties", "$defs", "definitions"):
182
- if key in schema and isinstance(schema[key], dict):
183
- for sub_schema in schema[key].values():
184
- _strip_in_place(sub_schema)
185
-
186
- # Array items
187
- if "items" in schema:
188
- _strip_in_place(schema["items"])
189
-
190
- # Combiners
191
- for combiner in ("anyOf", "allOf", "oneOf"):
192
- if combiner in schema and isinstance(schema[combiner], list):
193
- for sub_schema in schema[combiner]:
194
- _strip_in_place(sub_schema)
195
-
196
- # AdditionalProperties (if it's a schema, not just false)
197
- if isinstance(schema.get("additionalProperties"), dict):
198
- _strip_in_place(schema["additionalProperties"])
199
-
200
- # PropertyNames
201
- if isinstance(schema.get("propertyNames"), dict):
202
- _strip_in_place(schema["propertyNames"])
203
-
204
- return schema
205
-
206
-
207
- def sanitize_bedrock_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
208
- """Sanitize a JSON Schema for Bedrock's structured output constraints.
209
-
210
- Args:
211
- schema: The JSON Schema dict to sanitize.
212
-
213
- Returns:
214
- A deep copy with disallowed keywords stripped.
215
- """
216
- return _strip_disallowed_keywords(schema)
217
-
218
-
219
- def install_bedrock_schema_sanitizer() -> None:
220
- """Install the Bedrock JSON schema sanitizer.
221
-
222
- Patches litellm's AmazonConverseConfig._create_output_config_for_response_format
223
- to sanitize schemas before Bedrock receives them.
224
-
225
- Idempotent: calling multiple times has no additional effect.
226
- """
227
- import functools
228
-
229
- try:
230
- from litellm.llms.bedrock.chat.converse_transformation import (
231
- AmazonConverseConfig,
232
- )
233
- except ImportError:
234
- logger.debug("AmazonConverseConfig not found; skipping schema sanitizer")
235
- return
236
-
237
- # Get the raw descriptor from the class __dict__ to handle staticmethod correctly
238
- raw_descriptor = AmazonConverseConfig.__dict__.get(
239
- "_create_output_config_for_response_format"
240
- )
241
- if raw_descriptor is None:
242
- logger.warning(
243
- "AmazonConverseConfig._create_output_config_for_response_format not found"
244
- )
245
- return
246
-
247
- # Unwrap staticmethod to get the actual function
248
- if isinstance(raw_descriptor, staticmethod):
249
- original = raw_descriptor.__func__
250
- else:
251
- original = raw_descriptor
252
-
253
- # Check if already installed (check on the underlying function)
254
- if getattr(original, _SCHEMA_SANITIZER_MARKER, False):
255
- logger.debug("Bedrock schema sanitizer already installed")
256
- return
257
-
258
- # The original signature is: (json_schema=None, name=None, description=None)
259
- @functools.wraps(original)
260
- def wrapper(
261
- json_schema: dict[str, Any] | None = None,
262
- name: str | None = None,
263
- description: str | None = None,
264
- ) -> Any:
265
- """Wrapped staticmethod that sanitizes schemas before passing to Bedrock."""
266
- if json_schema is not None and isinstance(json_schema, dict):
267
- # Deep copy and sanitize the json_schema
268
- json_schema = _strip_disallowed_keywords(json_schema)
269
-
270
- return original(json_schema=json_schema, name=name, description=description)
271
-
272
- # Mark wrapper
273
- setattr(wrapper, _SCHEMA_SANITIZER_MARKER, True)
274
-
275
- # Install wrapper as staticmethod
276
- setattr(
277
- AmazonConverseConfig,
278
- "_create_output_config_for_response_format",
279
- staticmethod(wrapper),
280
- )
281
- logger.debug("Installed Bedrock JSON schema sanitizer")
282
-
283
-
284
86
  def enforces_schema(model: str | None) -> bool | None:
285
87
  """Check if a model enforces structured output schemas natively.
286
88
 
@@ -553,6 +355,12 @@ def install_two_step_structured_output(
553
355
  configs.add(id(cfg))
554
356
  llm_configs.append(cfg)
555
357
 
358
+ if not llm_configs:
359
+ logger.warning(
360
+ "Two-step structured output: no engine LLM config attribute resolved; "
361
+ "Hindsight internals may have changed"
362
+ )
363
+
556
364
  wrapped_count = 0
557
365
  for cfg in llm_configs:
558
366
  # Check if the config itself is a MultiLLMProvider (fix B2)
@@ -564,7 +372,7 @@ def install_two_step_structured_output(
564
372
  impl = getattr(cfg, "_provider_impl", None)
565
373
 
566
374
  if impl is None:
567
- logger.debug("No _provider_impl found on config")
375
+ logger.warning("No _provider_impl found on config %s", type(cfg).__name__)
568
376
  continue
569
377
 
570
378
  # Skip if the impl is a MultiLLMProvider
@@ -33,6 +33,7 @@ class EpisodeStore:
33
33
  schema: str | None = None,
34
34
  min_size: int = 1,
35
35
  max_size: int = 4,
36
+ persist_recalls: bool = False,
36
37
  ):
37
38
  """Create store with a psycopg ConnectionPool, scoped to a bank.
38
39
 
@@ -45,8 +46,12 @@ class EpisodeStore:
45
46
  (e.g. "episodic", "semantic"). None = server default (public).
46
47
  min_size: Minimum connections in pool
47
48
  max_size: Maximum connections in pool
49
+ persist_recalls: Write Prefrontal recall records to the ``recalls``
50
+ table. When False (default), ``save_episode`` skips them
51
+ and ``save_recalls`` is a no-op. The table is still created.
48
52
  """
49
53
  self.bank_id = bank_id
54
+ self.persist_recalls = persist_recalls
50
55
  self._schema = schema
51
56
  # Inject search_path into the connection string so every pooled
52
57
  # connection targets the right schema automatically.
@@ -567,19 +572,20 @@ class EpisodeStore:
567
572
  (self.bank_id, str(episode.id), name, content),
568
573
  )
569
574
 
570
- # 8. Insert recalls (Prefrontal monitoring) - now with episode_id set
571
- self._insert_recalls(
572
- cur, episode.recalls or [], run_id=episode.run_id, episode_id=episode.id, task=episode.task
573
- )
575
+ # 8. Insert recalls (Prefrontal monitoring, opt-in)
576
+ if self.persist_recalls:
577
+ self._insert_recalls(
578
+ cur, episode.recalls or [], run_id=episode.run_id, episode_id=episode.id, task=episode.task
579
+ )
574
580
 
575
581
  conn.commit()
576
582
 
577
583
  logger.info(
578
- "save_episode: persisted episode %s (bank=%s, task=%s, topics=%d, observations=%d, recalls=%d)",
584
+ "save_episode: persisted episode %s (bank=%s, task=%s, topics=%d, observations=%d, recalls=%s)",
579
585
  episode.id, self.bank_id, episode.task,
580
586
  len(episode.context_memory.topics),
581
587
  len(episode.context_memory.all_observations()),
582
- len(episode.recalls or []),
588
+ len(episode.recalls or []) if self.persist_recalls else "off",
583
589
  )
584
590
 
585
591
  def load_context(
@@ -836,8 +842,10 @@ class EpisodeStore:
836
842
  run_id: Run identifier
837
843
  episode_id: Optional episode UUID (None for run-level recalls)
838
844
  task: Task name (consumer task or "review" for run-level load)
845
+
846
+ No-op unless the store was built with ``persist_recalls=True``.
839
847
  """
840
- if not records:
848
+ if not self.persist_recalls or not records:
841
849
  return
842
850
 
843
851
  with self._pool.connection() as conn:
@@ -22,10 +22,6 @@ from codespy.agents.memory.prefrontal.reach import (
22
22
  tag_value,
23
23
  )
24
24
 
25
- # Content a mental model holds until its first refresh completes (kept in sync
26
- # with cerebral.MENTAL_MODEL_PLACEHOLDER; not imported to avoid loading Hindsight).
27
- MENTAL_MODEL_PLACEHOLDER = "Generating content..."
28
-
29
25
  SECTION_TITLES: dict[str, str] = {
30
26
  FACET_CONTEXT: "Around this work",
31
27
  FACET_SEEN_BEFORE: "Seen before",
@@ -158,7 +154,7 @@ def format_context_sections(
158
154
  # Briefing sections
159
155
  for mm in briefings or []:
160
156
  content = (mm.get("content") or "").strip()
161
- if not content or content == MENTAL_MODEL_PLACEHOLDER:
157
+ if not content:
162
158
  continue
163
159
  name = (mm.get("name") or "Briefing").strip()
164
160
  if not name.startswith("Briefing"):
@@ -27,7 +27,6 @@ import dspy # type: ignore[import-untyped]
27
27
 
28
28
  from codespy.agents.memory.prefrontal.facets import run_facets
29
29
  from codespy.agents.memory.prefrontal.format import (
30
- MENTAL_MODEL_PLACEHOLDER,
31
30
  estimate_tokens,
32
31
  fget,
33
32
  format_context_sections,
@@ -423,7 +422,7 @@ class Prefrontal:
423
422
  n_briefings = sum(
424
423
  1
425
424
  for mm in briefings
426
- if (mm.get("content") or "").strip() not in ("", MENTAL_MODEL_PLACEHOLDER)
425
+ if (mm.get("content") or "").strip()
427
426
  )
428
427
  facet_details: dict[str, dict[str, Any]] = {}
429
428
  for f in facets: