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.
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/PKG-INFO +2 -2
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/pyproject.toml +2 -2
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/__init__.py +1 -1
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/cerebral/cerebral.py +75 -38
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/cerebral/cost.py +12 -6
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/cerebral/structured_output.py +7 -199
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/postgres.py +15 -7
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/prefrontal/format.py +1 -5
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/prefrontal/prefrontal.py +1 -2
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config_memory.py +136 -286
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/pipeline.py +2 -2
- codespy_ai-2.0.2/src/codespy/agents/memory/cerebral/delta_ops_compat.py +0 -197
- codespy_ai-2.0.2/src/codespy/agents/memory/cerebral/routines.py +0 -230
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/LICENSE +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/README.md +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/context_safe.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/cost_tracker.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/diagnostic_adapter.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/dspy_config.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/cerebral/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/cerebral/orphans.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/budget.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/cartographer.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/context_memory.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/distiller.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/episode.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/hippocampus/hippocampus.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/pg0_manager.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/prefrontal/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/prefrontal/facets.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/prefrontal/query.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/prefrontal/reach.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/recall.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/audit/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/audit/agent.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/code_review/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/code_review/agent.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/doc/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/doc/agent.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/doc/doc_extractor.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/helpers.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/models.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/scope/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/scope/agent.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/scope/manifest_parser.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/scope/models.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/summary/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/summary/agent.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/supply_chain/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/review/supply_chain/agent.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/rlm_tools.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/cli.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/cli_local.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/cli_mcp_server.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/cli_remote.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config_dspy.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config_git.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config_io.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config_llm.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/config_utils.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/cyber/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/cyber/osv/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/cyber/osv/client.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/cyber/osv/models.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/cyber/osv/server.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/base.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/client.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/github_client.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/gitlab_client.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/local_diff.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/models.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/git/server.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/mcp_utils.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/bash.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/cpp.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/csharp.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/php.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/ruby.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/models.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/parsers/treesitter/server.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/base.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/filesystem/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/filesystem/client.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/filesystem/server.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/models.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/s3/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/s3/client.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/storage/s3/server.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/web/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/web/client.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/web/models.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/tools/web/server.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/models.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/reporters/__init__.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/reporters/base.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/reporters/git.py +0 -0
- {codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/workflows/review/reporters/stdout.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
56
|
+
hindsight-api-slim = ">=1.0.0,<2"
|
|
57
57
|
|
|
58
58
|
[tool.poetry.group.dev.dependencies]
|
|
59
59
|
pytest = ">=8.0.0"
|
|
@@ -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,
|
|
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
|
-
#
|
|
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
|
|
295
|
-
#
|
|
296
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
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,
|
|
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 +
|
|
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,
|
|
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=
|
|
175
|
+
output_tokens=billed_output,
|
|
170
176
|
input_cost=prompt_cost,
|
|
171
177
|
output_cost=completion_cost,
|
|
172
178
|
duration=duration,
|
{codespy_ai-2.0.2 → codespy_ai-2.1.1}/src/codespy/agents/memory/cerebral/structured_output.py
RENAMED
|
@@ -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.
|
|
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
|
|
571
|
-
self.
|
|
572
|
-
|
|
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=%
|
|
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
|
|
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()
|
|
425
|
+
if (mm.get("content") or "").strip()
|
|
427
426
|
)
|
|
428
427
|
facet_details: dict[str, dict[str, Any]] = {}
|
|
429
428
|
for f in facets:
|