codespy-ai 2.0.1__tar.gz → 2.0.2__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.1 → codespy_ai-2.0.2}/PKG-INFO +1 -1
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/pyproject.toml +1 -1
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/__init__.py +1 -1
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/cerebral.py +163 -13
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/prefrontal.py +2 -1
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/reach.py +46 -10
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/scope/agent.py +15 -29
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/LICENSE +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/README.md +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/context_safe.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/cost_tracker.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/diagnostic_adapter.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/dspy_config.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/cost.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/delta_ops_compat.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/orphans.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/routines.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/structured_output.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/budget.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/cartographer.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/context_memory.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/distiller.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/episode.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/hippocampus.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/pg0_manager.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/postgres.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/facets.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/format.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/query.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/recall.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/audit/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/audit/agent.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/code_review/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/code_review/agent.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/doc/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/doc/agent.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/doc/doc_extractor.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/helpers.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/scope/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/scope/manifest_parser.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/scope/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/summary/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/summary/agent.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/supply_chain/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/supply_chain/agent.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/rlm_tools.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/cli.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/cli_local.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/cli_mcp_server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/cli_remote.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config_dspy.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config_git.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config_io.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config_llm.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config_memory.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config_utils.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/cyber/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/cyber/osv/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/cyber/osv/client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/cyber/osv/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/cyber/osv/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/base.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/github_client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/gitlab_client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/local_diff.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/mcp_utils.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/bash.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/cpp.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/csharp.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/php.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/ruby.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/base.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/filesystem/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/filesystem/client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/filesystem/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/s3/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/s3/client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/s3/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/web/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/web/client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/web/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/web/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/pipeline.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/reporters/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/reporters/base.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/reporters/git.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/reporters/stdout.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/server.py +0 -0
|
@@ -238,8 +238,10 @@ from codespy.agents.memory.prefrontal.reach import (
|
|
|
238
238
|
TAG_ORG,
|
|
239
239
|
TAG_PROJECT_SCOPE,
|
|
240
240
|
TAG_REPO,
|
|
241
|
+
is_root_scope,
|
|
241
242
|
mental_model_id,
|
|
242
243
|
repo_tags,
|
|
244
|
+
root_scope_id,
|
|
243
245
|
scope_tags,
|
|
244
246
|
)
|
|
245
247
|
|
|
@@ -300,6 +302,26 @@ MENTAL_MODEL_SOURCE_QUERY = (
|
|
|
300
302
|
"dependencies and integrations, pitfalls and recurring problems, facts shown to be wrong."
|
|
301
303
|
)
|
|
302
304
|
|
|
305
|
+
# Question the repo-root briefing answers. The root scope of a monorepo sees
|
|
306
|
+
# facts from many scopes; this briefing must keep only what holds for all of them.
|
|
307
|
+
ROOT_MENTAL_MODEL_SOURCE_QUERY = (
|
|
308
|
+
"What should an agent starting work anywhere in this repository know, that holds for "
|
|
309
|
+
"every scope of it: repository layout and naming conventions, how scopes and manifests "
|
|
310
|
+
"are organized, shared tooling, build, CI and deployment, cross-scope integrations and "
|
|
311
|
+
"contracts, and repository-wide pitfalls or facts shown to be wrong. "
|
|
312
|
+
"Do not describe individual services, packages or libraries, their files, pull requests "
|
|
313
|
+
"or review findings; omit anything that is true for only one scope."
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# A delta refresh only adds content, so briefings grow past max_tokens. Past this
|
|
317
|
+
# factor of max_tokens, consolidate_run forces one full refresh (rewrite from scratch).
|
|
318
|
+
BRIEFING_FULL_REFRESH_FACTOR = 1.5
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _estimate_tokens(text: str) -> int:
|
|
322
|
+
"""Cheap token estimate (~4 chars per token), same heuristic as prefrontal.format."""
|
|
323
|
+
return (len(text or "") + 3) // 4
|
|
324
|
+
|
|
303
325
|
OBSERVATIONS_MISSION = (
|
|
304
326
|
"Merge facts about the same subject into one observation. Keep details that matter only "
|
|
305
327
|
"to one kind of work (e.g. documentation, security, dependencies) instead of generalizing "
|
|
@@ -823,16 +845,21 @@ class Cerebral:
|
|
|
823
845
|
if topic.type == "project_scope"
|
|
824
846
|
]
|
|
825
847
|
# Fallback to repo-root project scope instead of bare [org:, repo:]
|
|
826
|
-
return scopes or [scope_tags(repo_full_name, repo_full_name)]
|
|
848
|
+
return scopes or [scope_tags(repo_full_name, root_scope_id(repo_full_name))]
|
|
827
849
|
|
|
828
850
|
@staticmethod
|
|
829
851
|
def _mental_model_scopes(
|
|
830
852
|
scopes: Sequence[Sequence[str]], repo_full_name: str
|
|
831
853
|
) -> list[list[str]]:
|
|
832
|
-
"""Observation scopes
|
|
854
|
+
"""Observation scopes whose briefings are kept (deduped).
|
|
855
|
+
|
|
856
|
+
No bare ``[org:, repo:]`` scope: its briefing would read every fact of
|
|
857
|
+
the repo (all scopes of a monorepo). Repo-wide knowledge lives in the
|
|
858
|
+
repo-root scope ``[org:, repo:, project_scope:<repo>]`` instead.
|
|
859
|
+
"""
|
|
833
860
|
out: list[list[str]] = []
|
|
834
861
|
seen: set[tuple[str, ...]] = set()
|
|
835
|
-
for sc in
|
|
862
|
+
for sc in scopes:
|
|
836
863
|
key = tuple(sorted(sc))
|
|
837
864
|
if key not in seen:
|
|
838
865
|
seen.add(key)
|
|
@@ -840,15 +867,43 @@ class Cerebral:
|
|
|
840
867
|
return out
|
|
841
868
|
|
|
842
869
|
@staticmethod
|
|
843
|
-
def
|
|
870
|
+
def _scope_id_of(tags: Sequence[str]) -> str | None:
|
|
844
871
|
for tag in tags:
|
|
845
872
|
if tag.startswith(TAG_PROJECT_SCOPE):
|
|
846
|
-
return
|
|
873
|
+
return tag[len(TAG_PROJECT_SCOPE):]
|
|
874
|
+
return None
|
|
875
|
+
|
|
876
|
+
@staticmethod
|
|
877
|
+
def _repo_of(tags: Sequence[str]) -> str | None:
|
|
847
878
|
for tag in tags:
|
|
848
|
-
if tag.startswith(
|
|
849
|
-
return
|
|
879
|
+
if tag.startswith(TAG_REPO):
|
|
880
|
+
return tag[len(TAG_REPO):]
|
|
881
|
+
return None
|
|
882
|
+
|
|
883
|
+
@classmethod
|
|
884
|
+
def _is_root_briefing(cls, tags: Sequence[str]) -> bool:
|
|
885
|
+
scope_id = cls._scope_id_of(tags)
|
|
886
|
+
repo = cls._repo_of(tags)
|
|
887
|
+
return bool(scope_id and repo and is_root_scope(repo, scope_id))
|
|
888
|
+
|
|
889
|
+
@classmethod
|
|
890
|
+
def _mental_model_name(cls, tags: Sequence[str]) -> str:
|
|
891
|
+
scope_id = cls._scope_id_of(tags)
|
|
892
|
+
if scope_id:
|
|
893
|
+
if cls._is_root_briefing(tags):
|
|
894
|
+
return f"Briefing: {scope_id} (repo-wide)"
|
|
895
|
+
return f"Briefing: {scope_id}"
|
|
896
|
+
repo = cls._repo_of(tags)
|
|
897
|
+
if repo:
|
|
898
|
+
return f"Briefing: {repo}"
|
|
850
899
|
return "Briefing"
|
|
851
900
|
|
|
901
|
+
@classmethod
|
|
902
|
+
def _mental_model_source_query(cls, tags: Sequence[str]) -> str:
|
|
903
|
+
if cls._is_root_briefing(tags):
|
|
904
|
+
return ROOT_MENTAL_MODEL_SOURCE_QUERY
|
|
905
|
+
return MENTAL_MODEL_SOURCE_QUERY
|
|
906
|
+
|
|
852
907
|
def _sync_briefing_triggers(self, scopes: list[list[str]]) -> None:
|
|
853
908
|
"""Sync briefing triggers to match current settings before consolidation.
|
|
854
909
|
|
|
@@ -863,11 +918,7 @@ class Cerebral:
|
|
|
863
918
|
if not scopes:
|
|
864
919
|
return
|
|
865
920
|
|
|
866
|
-
desired_trigger =
|
|
867
|
-
**_BRIEFING_TRIGGER_STATIC,
|
|
868
|
-
"refresh_after_consolidation": self._mental_models,
|
|
869
|
-
"min_refresh_interval_seconds": self._min_mental_model_refresh_seconds,
|
|
870
|
-
}
|
|
921
|
+
desired_trigger = self._desired_trigger()
|
|
871
922
|
|
|
872
923
|
for tags in scopes:
|
|
873
924
|
mm_id = mental_model_id(tags)
|
|
@@ -891,7 +942,7 @@ class Cerebral:
|
|
|
891
942
|
self._engine.create_mental_model(
|
|
892
943
|
self._bank_id,
|
|
893
944
|
self._mental_model_name(tags),
|
|
894
|
-
|
|
945
|
+
self._mental_model_source_query(tags),
|
|
895
946
|
MENTAL_MODEL_PLACEHOLDER,
|
|
896
947
|
mental_model_id=mm_id,
|
|
897
948
|
tags=sorted(tags),
|
|
@@ -919,6 +970,7 @@ class Cerebral:
|
|
|
919
970
|
needs_update = (
|
|
920
971
|
current_trigger.get("refresh_after_consolidation") != self._mental_models
|
|
921
972
|
or current_trigger.get("exclude_mental_models") is not True
|
|
973
|
+
or current_trigger.get("mode", "delta") != "delta"
|
|
922
974
|
)
|
|
923
975
|
if needs_update:
|
|
924
976
|
self._run_async(
|
|
@@ -935,11 +987,103 @@ class Cerebral:
|
|
|
935
987
|
"on" if current_trigger.get("refresh_after_consolidation") else "off",
|
|
936
988
|
"on" if self._mental_models else "off",
|
|
937
989
|
)
|
|
990
|
+
# Name / source query drift (e.g. the repo-root briefing moved to
|
|
991
|
+
# its repo-wide question). A changed source query makes Hindsight's
|
|
992
|
+
# next delta refresh fall back to a full rewrite.
|
|
993
|
+
desired_name = self._mental_model_name(tags)
|
|
994
|
+
desired_query = self._mental_model_source_query(tags)
|
|
995
|
+
if (
|
|
996
|
+
existing.get("name") != desired_name
|
|
997
|
+
or existing.get("source_query") != desired_query
|
|
998
|
+
):
|
|
999
|
+
self._run_async(
|
|
1000
|
+
self._engine.update_mental_model(
|
|
1001
|
+
self._bank_id,
|
|
1002
|
+
mm_id,
|
|
1003
|
+
name=desired_name,
|
|
1004
|
+
source_query=desired_query,
|
|
1005
|
+
request_context=ctx,
|
|
1006
|
+
)
|
|
1007
|
+
)
|
|
1008
|
+
logger.info(
|
|
1009
|
+
"cerebral: briefing %s renamed/re-queried as %r (next refresh is full)",
|
|
1010
|
+
mm_id,
|
|
1011
|
+
desired_name,
|
|
1012
|
+
)
|
|
938
1013
|
|
|
939
1014
|
self._ensured_mental_models.add(mm_id)
|
|
940
1015
|
except Exception:
|
|
941
1016
|
logger.warning("cerebral: failed to sync briefing trigger for %s", mm_id, exc_info=True)
|
|
942
1017
|
|
|
1018
|
+
def _desired_trigger(self) -> dict[str, Any]:
|
|
1019
|
+
"""Briefing trigger for the current settings (delta mode)."""
|
|
1020
|
+
return {
|
|
1021
|
+
**_BRIEFING_TRIGGER_STATIC,
|
|
1022
|
+
"refresh_after_consolidation": self._mental_models,
|
|
1023
|
+
"min_refresh_interval_seconds": self._min_mental_model_refresh_seconds,
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
def _enforce_briefing_size(self, scopes: Sequence[Sequence[str]]) -> None:
|
|
1027
|
+
"""Force a full refresh of briefings grown past the size limit. Never raises.
|
|
1028
|
+
|
|
1029
|
+
Delta refreshes only add content, so a briefing drifts past
|
|
1030
|
+
``max_mental_model_tokens``. Above ``BRIEFING_FULL_REFRESH_FACTOR`` times
|
|
1031
|
+
that budget, the briefing is rewritten from scratch once: the trigger is
|
|
1032
|
+
switched to ``mode=full``, one explicit refresh runs inline (sync task
|
|
1033
|
+
backend), then the delta trigger is restored. Never deletes anything.
|
|
1034
|
+
"""
|
|
1035
|
+
if not self._mental_models or not scopes:
|
|
1036
|
+
return
|
|
1037
|
+
limit = int(self._max_mental_model_tokens * BRIEFING_FULL_REFRESH_FACTOR)
|
|
1038
|
+
for tags in scopes:
|
|
1039
|
+
mm_id = mental_model_id(tags)
|
|
1040
|
+
try:
|
|
1041
|
+
ctx = RequestContext()
|
|
1042
|
+
mm = self._run_async(
|
|
1043
|
+
self._engine.get_mental_model(self._bank_id, mm_id, request_context=ctx)
|
|
1044
|
+
)
|
|
1045
|
+
if not mm:
|
|
1046
|
+
continue
|
|
1047
|
+
before = _estimate_tokens((mm.get("content") or "").strip())
|
|
1048
|
+
if before <= limit:
|
|
1049
|
+
continue
|
|
1050
|
+
desired = self._desired_trigger()
|
|
1051
|
+
self._run_async(
|
|
1052
|
+
self._engine.update_mental_model(
|
|
1053
|
+
self._bank_id,
|
|
1054
|
+
mm_id,
|
|
1055
|
+
trigger={**desired, "mode": "full"},
|
|
1056
|
+
request_context=ctx,
|
|
1057
|
+
)
|
|
1058
|
+
)
|
|
1059
|
+
try:
|
|
1060
|
+
self._run_async(
|
|
1061
|
+
self._engine.submit_async_refresh_mental_model(
|
|
1062
|
+
self._bank_id, mm_id, request_context=ctx
|
|
1063
|
+
)
|
|
1064
|
+
)
|
|
1065
|
+
finally:
|
|
1066
|
+
self._run_async(
|
|
1067
|
+
self._engine.update_mental_model(
|
|
1068
|
+
self._bank_id, mm_id, trigger=desired, request_context=ctx
|
|
1069
|
+
)
|
|
1070
|
+
)
|
|
1071
|
+
after_mm = self._run_async(
|
|
1072
|
+
self._engine.get_mental_model(self._bank_id, mm_id, request_context=ctx)
|
|
1073
|
+
)
|
|
1074
|
+
after = _estimate_tokens(((after_mm or {}).get("content") or "").strip())
|
|
1075
|
+
logger.info(
|
|
1076
|
+
"cerebral: briefing %s (%s) over size (%d > %d tokens); "
|
|
1077
|
+
"full refresh -> %d tokens",
|
|
1078
|
+
mm_id,
|
|
1079
|
+
self._mental_model_name(tags),
|
|
1080
|
+
before,
|
|
1081
|
+
limit,
|
|
1082
|
+
after,
|
|
1083
|
+
)
|
|
1084
|
+
except Exception:
|
|
1085
|
+
logger.warning("cerebral: briefing size guard failed for %s", mm_id, exc_info=True)
|
|
1086
|
+
|
|
943
1087
|
def consolidate_run(self, run_id: str) -> None:
|
|
944
1088
|
"""Trigger one consolidation for all scopes touched during a run.
|
|
945
1089
|
|
|
@@ -986,6 +1130,8 @@ class Cerebral:
|
|
|
986
1130
|
observation_scopes=scopes,
|
|
987
1131
|
)
|
|
988
1132
|
)
|
|
1133
|
+
# Rewrite briefings that delta refreshes grew past their budget
|
|
1134
|
+
self._enforce_briefing_size(scopes)
|
|
989
1135
|
|
|
990
1136
|
delta = {b: _calls(b) - baseline[b] for b in buckets}
|
|
991
1137
|
logger.info(
|
|
@@ -1218,6 +1364,10 @@ class Cerebral:
|
|
|
1218
1364
|
if topic.type == "project_scope":
|
|
1219
1365
|
tags.append(f"{topic.type}:{topic.id}")
|
|
1220
1366
|
if repo_full_name:
|
|
1367
|
+
# No scope topic: tag with the repo-root scope, matching the
|
|
1368
|
+
# _observation_scopes fallback (recall reads project_scope:, not repo:).
|
|
1369
|
+
if not tags:
|
|
1370
|
+
tags.append(f"{TAG_PROJECT_SCOPE}{root_scope_id(repo_full_name)}")
|
|
1221
1371
|
tags.extend(repo_tags(repo_full_name))
|
|
1222
1372
|
tags.append(f"task:{episode.task}")
|
|
1223
1373
|
return tags
|
|
@@ -122,7 +122,8 @@ class Prefrontal:
|
|
|
122
122
|
task_name: Agent task (e.g. ``code_review``).
|
|
123
123
|
repo_full_name: ``owner/repo``.
|
|
124
124
|
scope_topic_ids: ``project_scope`` topic ids of the agent's scope(s).
|
|
125
|
-
include_repo: Also read repo-
|
|
125
|
+
include_repo: Also read the repo-root scope (``project_scope:<owner/repo>``):
|
|
126
|
+
repo-wide facts and the repo-wide briefing. Never every ``repo:`` fact.
|
|
126
127
|
"""
|
|
127
128
|
|
|
128
129
|
def __init__(
|
|
@@ -41,6 +41,32 @@ def scope_tags(repo_full_name: str, scope_topic_id: str) -> list[str]:
|
|
|
41
41
|
return [*repo_tags(repo_full_name), f"{TAG_PROJECT_SCOPE}{scope_topic_id}"]
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def root_scope_id(repo_full_name: str) -> str:
|
|
45
|
+
"""Topic id of the repo-root scope (same as ``make_topic_id(repo, '.')``).
|
|
46
|
+
|
|
47
|
+
The root scope holds repo-wide knowledge only (layout, conventions, shared
|
|
48
|
+
tooling); it is never a container of every scope's facts.
|
|
49
|
+
"""
|
|
50
|
+
return repo_full_name
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_root_scope(repo_full_name: str, scope_topic_id: str) -> bool:
|
|
54
|
+
"""True when ``scope_topic_id`` is the repo-root scope of ``repo_full_name``."""
|
|
55
|
+
return bool(repo_full_name) and scope_topic_id == root_scope_id(repo_full_name)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _with_root(
|
|
59
|
+
repo_full_name: str, scope_topic_ids: Sequence[str] | None, include_repo: bool
|
|
60
|
+
) -> list[str]:
|
|
61
|
+
"""Scope ids, plus the repo-root scope id when ``include_repo`` (deduped, order kept)."""
|
|
62
|
+
ids = [sid for sid in (scope_topic_ids or []) if sid]
|
|
63
|
+
if include_repo and repo_full_name:
|
|
64
|
+
root = root_scope_id(repo_full_name)
|
|
65
|
+
if root not in ids:
|
|
66
|
+
ids.append(root)
|
|
67
|
+
return ids
|
|
68
|
+
|
|
69
|
+
|
|
44
70
|
def mental_model_id(tags: Iterable[str]) -> str:
|
|
45
71
|
"""Deterministic mental-model id for a tag scope.
|
|
46
72
|
|
|
@@ -55,11 +81,16 @@ def mental_model_scopes(
|
|
|
55
81
|
scope_topic_ids: Sequence[str] | None,
|
|
56
82
|
include_repo: bool,
|
|
57
83
|
) -> list[list[str]]:
|
|
58
|
-
"""Tag scopes whose briefings an agent reads: its scopes
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
84
|
+
"""Tag scopes whose briefings an agent reads: its scopes, then the repo-root scope.
|
|
85
|
+
|
|
86
|
+
``include_repo`` adds the repo-root scope ``[org:, repo:, project_scope:<repo>]``
|
|
87
|
+
(repo-wide knowledge), never the bare ``[org:, repo:]`` scope, which would
|
|
88
|
+
cover every scope of a monorepo.
|
|
89
|
+
"""
|
|
90
|
+
return [
|
|
91
|
+
scope_tags(repo_full_name, sid)
|
|
92
|
+
for sid in _with_root(repo_full_name, scope_topic_ids, include_repo)
|
|
93
|
+
]
|
|
63
94
|
|
|
64
95
|
|
|
65
96
|
def mental_model_ids(
|
|
@@ -88,11 +119,16 @@ def local_tags(
|
|
|
88
119
|
scope_topic_ids: Sequence[str] | None,
|
|
89
120
|
include_repo: bool,
|
|
90
121
|
) -> list[str]:
|
|
91
|
-
"""Tags of the local pool (OR-ed): ``project_scope:`` ids, plus
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
122
|
+
"""Tags of the local pool (OR-ed): ``project_scope:`` ids, plus the repo-root
|
|
123
|
+
scope ``project_scope:<repo>`` if ``include_repo``.
|
|
124
|
+
|
|
125
|
+
Never ``repo:<repo>``: every fact of the repo carries it, so OR-ing it in
|
|
126
|
+
would pull every scope of a monorepo into the local pool.
|
|
127
|
+
"""
|
|
128
|
+
return [
|
|
129
|
+
f"{TAG_PROJECT_SCOPE}{sid}"
|
|
130
|
+
for sid in _with_root(repo_full_name, scope_topic_ids, include_repo)
|
|
131
|
+
]
|
|
96
132
|
|
|
97
133
|
|
|
98
134
|
def build_local_groups(
|
|
@@ -1037,8 +1037,8 @@ class ScopeResolver(dspy.Module):
|
|
|
1037
1037
|
from codespy.agents.memory.hippocampus import (
|
|
1038
1038
|
ContextMemory,
|
|
1039
1039
|
Topic,
|
|
1040
|
-
compute_common_ancestor_topic_id,
|
|
1041
1040
|
)
|
|
1041
|
+
from codespy.agents.memory.prefrontal.reach import root_scope_id
|
|
1042
1042
|
|
|
1043
1043
|
# Local bindings from review_context metadata
|
|
1044
1044
|
pr = review_context.metadata.pr
|
|
@@ -1145,38 +1145,24 @@ class ScopeResolver(dspy.Module):
|
|
|
1145
1145
|
if scope.subroot in boundary_descriptions:
|
|
1146
1146
|
scope.description = boundary_descriptions[scope.subroot]
|
|
1147
1147
|
|
|
1148
|
-
#
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
t = scope.topic(pr.repo_full_name)
|
|
1153
|
-
scope_topics.append(t)
|
|
1154
|
-
scope_topic_ids[scope.subroot] = t.id
|
|
1155
|
-
|
|
1156
|
-
# Compute common ancestor topic if >1 scope
|
|
1157
|
-
common_ancestor_topic_id = compute_common_ancestor_topic_id(
|
|
1158
|
-
pr.repo_full_name, [s.subroot for s in final_scopes]
|
|
1159
|
-
)
|
|
1160
|
-
if common_ancestor_topic_id:
|
|
1161
|
-
# Build description: "Common context for scopes: subroot1, subroot2, ..."
|
|
1162
|
-
subroot_list = ", ".join(s.subroot for s in final_scopes)
|
|
1163
|
-
common_desc = f"Common context for scopes: {subroot_list}"
|
|
1164
|
-
scope_topics.append(Topic(id=common_ancestor_topic_id, type="project_scope", description=common_desc))
|
|
1165
|
-
stamp_topic_ids = [common_ancestor_topic_id]
|
|
1166
|
-
elif scope_topics:
|
|
1167
|
-
# Single scope: stamp with its topic ID
|
|
1168
|
-
stamp_topic_ids = [scope_topics[0].id]
|
|
1169
|
-
else:
|
|
1170
|
-
stamp_topic_ids = []
|
|
1171
|
-
# Add PR URL topic (provides description; not a scope)
|
|
1148
|
+
# Scope-agent knowledge (repo layout, scope boundaries) is repo-wide:
|
|
1149
|
+
# bind it to the repo-root scope only. Binding every PR scope stamped
|
|
1150
|
+
# layout facts onto each service's briefing.
|
|
1151
|
+
root_topic_id = root_scope_id(pr.repo_full_name)
|
|
1172
1152
|
pr_ctx = review_context.pr_context
|
|
1173
|
-
scope_topics
|
|
1153
|
+
scope_topics: list[Topic] = [
|
|
1154
|
+
Topic(
|
|
1155
|
+
id=root_topic_id,
|
|
1156
|
+
type="project_scope",
|
|
1157
|
+
description="Repository root: knowledge shared by every scope",
|
|
1158
|
+
),
|
|
1159
|
+
pr_ctx.to_topic(),
|
|
1160
|
+
]
|
|
1174
1161
|
# Finalize scopes with hierarchical skills
|
|
1175
1162
|
self._finalize_scopes(final_scopes, review_context)
|
|
1176
1163
|
# Bind topics to hippocampus cmem for episode persistence
|
|
1177
|
-
if hippo is not None and
|
|
1178
|
-
|
|
1179
|
-
hippo.bind_topics(scope_topics, stamp_topic_ids)
|
|
1164
|
+
if hippo is not None and final_scopes:
|
|
1165
|
+
hippo.bind_topics(scope_topics, [root_topic_id, pr_ctx.pr_url])
|
|
1180
1166
|
|
|
1181
1167
|
# Fire-and-forget background episode save
|
|
1182
1168
|
cerebral = get_cerebral(self._settings)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/delta_ops_compat.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/structured_output.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/context_memory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/base_extractor.py
RENAMED
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/__init__.py
RENAMED
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/bash.py
RENAMED
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/cpp.py
RENAMED
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/csharp.py
RENAMED
|
File without changes
|
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/java.py
RENAMED
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/javascript.py
RENAMED
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py
RENAMED
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/objc.py
RENAMED
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/php.py
RENAMED
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/python.py
RENAMED
|
File without changes
|
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/ruby.py
RENAMED
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/rust.py
RENAMED
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/swift.py
RENAMED
|
File without changes
|
{codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/terraform.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|