feat(acms): plan execution leverages ACMS context for LLM calls #1163
@@ -167,6 +167,27 @@ Feature: LLM Actors Coverage
|
||||
When I call execute actor with plan_id "EXEC05" and no stream callback
|
||||
Then the execute result should have zero entries
|
||||
|
||||
Scenario: LLMExecuteActor injects assembled execute-phase context into prompt
|
||||
Given a valid LLMExecuteActor with assembled execute-phase context
|
||||
And the LLM returns file blocks for execute
|
||||
When I call execute actor with plan_id "EXEC06" and no stream callback
|
||||
Then the execute prompt should contain "ACMS Execute-Phase Context"
|
||||
And the execute prompt should contain "src/main.py"
|
||||
|
||||
Scenario: LLMExecuteActor falls back when context assembly fails
|
||||
Given a valid LLMExecuteActor with failing context assembly
|
||||
And the LLM returns file blocks for execute
|
||||
When I call execute actor with plan_id "EXEC07" and no stream callback
|
||||
Then the execute result should contain a changeset
|
||||
And the execute prompt should not contain "ACMS Execute-Phase Context"
|
||||
|
||||
Scenario: LLMExecuteActor handles empty assembled context
|
||||
Given a valid LLMExecuteActor with empty assembled context
|
||||
And the LLM returns file blocks for execute
|
||||
When I call execute actor with plan_id "EXEC08" and no stream callback
|
||||
Then the execute result should contain a changeset
|
||||
And the execute prompt should not contain "ACMS Execute-Phase Context"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# LLMExecuteActor._parse_file_blocks
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
@@ -30,6 +30,11 @@ from cleveragents.application.services.plan_executor import (
|
||||
StrategyDecision,
|
||||
)
|
||||
from cleveragents.core.exceptions import ValidationError
|
||||
from cleveragents.domain.models.acms.crp import (
|
||||
AssembledContext,
|
||||
ContextFragment,
|
||||
FragmentProvenance,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import InvariantSource, PlanInvariant
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -72,7 +77,8 @@ def _make_mock_registry(llm_response):
|
||||
"""Create a mock ProviderRegistry that returns a mock LLM."""
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.invoke.return_value = llm_response
|
||||
registry = SimpleNamespace(create_llm=MagicMock(return_value=mock_llm))
|
||||
registry = MagicMock()
|
||||
registry.create_llm.return_value = mock_llm
|
||||
return registry
|
||||
|
||||
|
||||
@@ -108,6 +114,19 @@ def _sample_decisions():
|
||||
]
|
||||
|
||||
|
||||
class _StubContextAssembler:
|
||||
"""Simple execute-context assembler test double."""
|
||||
|
||||
def __init__(self, assembled: AssembledContext | None = None, fail: bool = False):
|
||||
self._assembled = assembled
|
||||
self._fail = fail
|
||||
|
||||
def assemble(self, plan):
|
||||
if self._fail:
|
||||
raise RuntimeError("context assembly failed")
|
||||
return self._assembled
|
||||
|
||||
|
||||
LLM_NUMBERED_RESPONSE = "1. Create the module\n2. Write unit tests\n3. Update docs"
|
||||
|
||||
LLM_FILE_BLOCKS_RESPONSE = (
|
||||
@@ -234,9 +253,8 @@ def step_llm_returns_no_content(context):
|
||||
response = _make_mock_llm_response_no_content("1. Fallback step text")
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.invoke.return_value = response
|
||||
context.mock_registry = SimpleNamespace(
|
||||
create_llm=MagicMock(return_value=mock_llm),
|
||||
)
|
||||
context.mock_registry = MagicMock()
|
||||
context.mock_registry.create_llm.return_value = mock_llm
|
||||
context.mock_lifecycle = _make_mock_lifecycle(strategy_actor="openai/gpt-4")
|
||||
context.strategize_actor = LLMStrategizeActor(
|
||||
provider_registry=context.mock_registry,
|
||||
@@ -450,14 +468,76 @@ def step_setup_valid_execute_actor(context):
|
||||
context.sandbox_dir = None
|
||||
|
||||
|
||||
@given("a valid LLMExecuteActor with assembled execute-phase context")
|
||||
def step_setup_execute_actor_with_context(context):
|
||||
llm_response = _make_mock_llm_response(LLM_FILE_BLOCKS_RESPONSE)
|
||||
context.mock_registry = _make_mock_registry(llm_response)
|
||||
context.mock_lifecycle = _make_mock_lifecycle(execution_actor="openai/gpt-4")
|
||||
assembled = AssembledContext(
|
||||
fragments=(
|
||||
ContextFragment(
|
||||
uko_node="resource://src/main.py",
|
||||
content="def run():\n return 'ok'",
|
||||
detail_depth=2,
|
||||
token_count=12,
|
||||
relevance_score=0.9,
|
||||
provenance=FragmentProvenance(
|
||||
resource_uri="local/large-repo",
|
||||
location="src/main.py",
|
||||
strategy="execute_phase_context",
|
||||
),
|
||||
),
|
||||
),
|
||||
total_tokens=12,
|
||||
budget_used=0.1,
|
||||
strategies_used=("relevance",),
|
||||
context_hash="ctxhash123",
|
||||
preamble="execute phase context",
|
||||
provenance_map={},
|
||||
)
|
||||
context.execute_actor = LLMExecuteActor(
|
||||
provider_registry=context.mock_registry,
|
||||
lifecycle_service=context.mock_lifecycle,
|
||||
context_assembler=_StubContextAssembler(assembled=assembled),
|
||||
)
|
||||
|
||||
|
||||
@given("a valid LLMExecuteActor with failing context assembly")
|
||||
def step_setup_execute_actor_with_failing_context(context):
|
||||
llm_response = _make_mock_llm_response(LLM_FILE_BLOCKS_RESPONSE)
|
||||
context.mock_registry = _make_mock_registry(llm_response)
|
||||
context.mock_lifecycle = _make_mock_lifecycle(execution_actor="openai/gpt-4")
|
||||
context.execute_actor = LLMExecuteActor(
|
||||
provider_registry=context.mock_registry,
|
||||
lifecycle_service=context.mock_lifecycle,
|
||||
context_assembler=_StubContextAssembler(fail=True),
|
||||
)
|
||||
|
||||
|
||||
@given("a valid LLMExecuteActor with empty assembled context")
|
||||
def step_setup_execute_actor_with_empty_context(context):
|
||||
llm_response = _make_mock_llm_response(LLM_FILE_BLOCKS_RESPONSE)
|
||||
context.mock_registry = _make_mock_registry(llm_response)
|
||||
context.mock_lifecycle = _make_mock_lifecycle(execution_actor="openai/gpt-4")
|
||||
context.execute_actor = LLMExecuteActor(
|
||||
provider_registry=context.mock_registry,
|
||||
lifecycle_service=context.mock_lifecycle,
|
||||
context_assembler=_StubContextAssembler(assembled=None),
|
||||
)
|
||||
|
||||
|
||||
@given("the LLM returns file blocks for execute")
|
||||
def step_llm_returns_file_blocks(context):
|
||||
llm_response = _make_mock_llm_response(LLM_FILE_BLOCKS_RESPONSE)
|
||||
context.mock_registry = _make_mock_registry(llm_response)
|
||||
context.mock_lifecycle = _make_mock_lifecycle(execution_actor="anthropic/claude-3")
|
||||
context_assembler = None
|
||||
if hasattr(context, "execute_actor") and context.execute_actor is not None:
|
||||
context_assembler = getattr(context.execute_actor, "_context_assembler", None)
|
||||
context.execute_actor = LLMExecuteActor(
|
||||
provider_registry=context.mock_registry,
|
||||
lifecycle_service=context.mock_lifecycle,
|
||||
context_assembler=context_assembler,
|
||||
)
|
||||
|
||||
|
||||
@@ -502,6 +582,10 @@ def step_execute_actor_no_callback(context, pid):
|
||||
decisions=_sample_decisions(),
|
||||
stream_callback=None,
|
||||
)
|
||||
llm = context.mock_registry.create_llm.return_value
|
||||
call_args = llm.invoke.call_args
|
||||
assert call_args is not None
|
||||
context.execute_prompt_text = call_args.args[0][0].content
|
||||
|
||||
|
||||
@when('I call execute actor with plan_id "{pid}" and a sandbox root')
|
||||
@@ -576,6 +660,20 @@ def step_verify_zero_entries(context):
|
||||
assert context.execute_result.tool_calls_count == 0
|
||||
|
||||
|
||||
@then('the execute prompt should contain "{needle}"')
|
||||
def step_execute_prompt_contains(context, needle):
|
||||
assert needle in context.execute_prompt_text, (
|
||||
f"Expected prompt to contain '{needle}', got: {context.execute_prompt_text}"
|
||||
)
|
||||
|
||||
|
||||
@then('the execute prompt should not contain "{needle}"')
|
||||
def step_execute_prompt_not_contains(context, needle):
|
||||
assert needle not in context.execute_prompt_text, (
|
||||
f"Expected prompt not to contain '{needle}', got: {context.execute_prompt_text}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LLMExecuteActor._parse_file_blocks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -23,7 +23,9 @@ import sys
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, NoReturn, cast
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
@@ -36,6 +38,18 @@ if _SRC not in sys.path:
|
||||
from cleveragents.application.services.context_phase_analysis import ( # noqa: E402
|
||||
analyze_phase_summaries,
|
||||
)
|
||||
from cleveragents.application.services.context_tiers import ( # noqa: E402
|
||||
ContextTierService,
|
||||
)
|
||||
from cleveragents.application.services.execute_phase_context_assembler import ( # noqa: E402
|
||||
ACMSExecutePhaseContextAssembler,
|
||||
)
|
||||
from cleveragents.application.services.llm_actors import ( # noqa: E402
|
||||
LLMExecuteActor,
|
||||
)
|
||||
from cleveragents.application.services.plan_executor import ( # noqa: E402
|
||||
StrategyDecision,
|
||||
)
|
||||
from cleveragents.application.services.resource_registry_service import ( # noqa: E402
|
||||
ResourceRegistryService,
|
||||
)
|
||||
@@ -43,6 +57,7 @@ from cleveragents.cli.commands.project_context import ( # noqa: E402
|
||||
_read_policy,
|
||||
_write_policy,
|
||||
)
|
||||
from cleveragents.config.settings import Settings # noqa: E402
|
||||
from cleveragents.domain.models.acms.tiers import ( # noqa: E402
|
||||
ContextTier,
|
||||
TieredFragment,
|
||||
@@ -765,6 +780,106 @@ def context_view_validation() -> None:
|
||||
print("m5-context-view-validation-ok")
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Subcommand: llm-execute-acms-context
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
|
||||
def llm_execute_acms_context() -> None:
|
||||
"""Verify execute-phase LLM prompt uses ACMS assembled execute context."""
|
||||
proj_repo, _link_repo, _svc, sf = _setup_db()
|
||||
_create_project(proj_repo, "local/exec-ctx")
|
||||
|
||||
policy = ProjectContextPolicy(
|
||||
default_view=ContextView(include_paths=["**/*"]),
|
||||
strategize_view=ContextView(include_paths=["docs/**"]),
|
||||
execute_view=ContextView(
|
||||
include_paths=["src/**"],
|
||||
max_file_size=100_000,
|
||||
max_total_size=100_000,
|
||||
),
|
||||
)
|
||||
_write_policy(sf, "local/exec-ctx", policy)
|
||||
|
||||
tier_service = ContextTierService(settings=Settings())
|
||||
tier_service.store(
|
||||
TieredFragment(
|
||||
fragment_id="frag-docs",
|
||||
content="documentation fragment",
|
||||
tier=ContextTier.HOT,
|
||||
resource_id="local/large-repo",
|
||||
project_name="local/exec-ctx",
|
||||
token_count=20,
|
||||
metadata={"path": "docs/readme.md", "relevance_score": "0.6"},
|
||||
)
|
||||
)
|
||||
tier_service.store(
|
||||
TieredFragment(
|
||||
fragment_id="frag-src",
|
||||
content="def run():\n return 'ok'",
|
||||
tier=ContextTier.HOT,
|
||||
resource_id="local/large-repo",
|
||||
project_name="local/exec-ctx",
|
||||
token_count=16,
|
||||
metadata={"path": "src/main.py", "relevance_score": "0.9"},
|
||||
)
|
||||
)
|
||||
|
||||
assembler = ACMSExecutePhaseContextAssembler(
|
||||
context_tier_service=tier_service,
|
||||
project_repository=proj_repo,
|
||||
hot_max_tokens=1024,
|
||||
)
|
||||
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.invoke.return_value = SimpleNamespace(
|
||||
content="FILE: src/out.py\n```python\nprint('ok')\n```\n"
|
||||
)
|
||||
registry = cast(Any, SimpleNamespace(create_llm=MagicMock(return_value=mock_llm)))
|
||||
|
||||
plan = SimpleNamespace(
|
||||
identity=SimpleNamespace(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
|
||||
action_name="local/action",
|
||||
project_links=[SimpleNamespace(project_name="local/exec-ctx")],
|
||||
)
|
||||
action = SimpleNamespace(execution_actor="openai/gpt-4")
|
||||
lifecycle = SimpleNamespace(
|
||||
get_plan=MagicMock(return_value=plan),
|
||||
get_action=MagicMock(return_value=action),
|
||||
)
|
||||
|
||||
actor = LLMExecuteActor(
|
||||
provider_registry=registry,
|
||||
lifecycle_service=lifecycle,
|
||||
context_assembler=assembler,
|
||||
)
|
||||
actor.execute(
|
||||
plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
||||
decisions=[
|
||||
StrategyDecision(
|
||||
decision_id="d1",
|
||||
step_text="Implement feature",
|
||||
sequence=0,
|
||||
parent_id=None,
|
||||
)
|
||||
],
|
||||
stream_callback=None,
|
||||
)
|
||||
|
||||
call_args = mock_llm.invoke.call_args
|
||||
if call_args is None:
|
||||
_fail("LLM invoke was not called")
|
||||
prompt = call_args.args[0][0].content
|
||||
if "ACMS Execute-Phase Context" not in prompt:
|
||||
_fail("prompt missing ACMS execute-phase context section")
|
||||
if "src/main.py" not in prompt:
|
||||
_fail("prompt missing execute-view source fragment")
|
||||
if "docs/readme.md" in prompt:
|
||||
_fail("prompt should exclude strategize-only docs fragment in execute view")
|
||||
|
||||
print("m5-llm-execute-acms-context-ok")
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Dispatcher
|
||||
# -------------------------------------------------------------------
|
||||
@@ -779,6 +894,7 @@ _COMMANDS: dict[str, Callable[[], None]] = {
|
||||
"acms-scoped-context": acms_scoped_context,
|
||||
"context-policy-clear": context_policy_clear,
|
||||
"context-view-validation": context_view_validation,
|
||||
"llm-execute-acms-context": llm_execute_acms_context,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -329,6 +329,17 @@ ACMS Scoped Context Output Per Phase
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m5-acms-scoped-context-ok
|
||||
|
||||
Execute Phase LLM Uses ACMS Context
|
||||
[Documentation] Verify execute-phase plan prompting injects
|
||||
... ACMS assembled context using execute-view
|
||||
... policy (not strategize view) and excludes
|
||||
... out-of-view fragments.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} llm-execute-acms-context cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m5-llm-execute-acms-context-ok
|
||||
|
||||
Context Policy Clear And Inheritance Fallback
|
||||
[Documentation] Verify that clearing a phase view causes
|
||||
... resolve_view to fall back to the parent phase.
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"""Execute-phase ACMS context assembly for LLM execution prompts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
from pathlib import PurePath
|
||||
from typing import Any, Protocol
|
||||
|
||||
import structlog
|
||||
|
||||
from cleveragents.application.services.acms_service import ACMSPipeline
|
||||
from cleveragents.domain.models.acms.crp import AssembledContext, ContextRequest
|
||||
from cleveragents.domain.models.acms.tiers import TieredFragment
|
||||
from cleveragents.domain.models.core.context_fragment import (
|
||||
ContextBudget as CoreContextBudget,
|
||||
)
|
||||
from cleveragents.domain.models.core.context_fragment import (
|
||||
ContextFragment as CoreContextFragment,
|
||||
)
|
||||
from cleveragents.domain.models.core.context_fragment import (
|
||||
FragmentProvenance as CoreFragmentProvenance,
|
||||
)
|
||||
from cleveragents.domain.models.core.context_policy import ProjectContextPolicy
|
||||
from cleveragents.infrastructure.database.repositories import (
|
||||
NamespacedProjectRepository,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class ExecutePhaseContextAssembler(Protocol):
|
||||
"""Protocol for assembling execute-phase ACMS context."""
|
||||
|
||||
def assemble(self, plan: Any) -> AssembledContext | None:
|
||||
"""Return assembled context for *plan* or ``None`` when unavailable."""
|
||||
...
|
||||
|
||||
|
||||
class ACMSExecutePhaseContextAssembler(ExecutePhaseContextAssembler):
|
||||
"""Assemble execute-phase ACMS context from tier fragments and project policy."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context_tier_service: Any,
|
||||
project_repository: NamespacedProjectRepository,
|
||||
acms_pipeline: ACMSPipeline | None = None,
|
||||
hot_max_tokens: int = 4096,
|
||||
) -> None:
|
||||
self._tier = context_tier_service
|
||||
self._project_repository = project_repository
|
||||
self._pipeline = acms_pipeline or ACMSPipeline()
|
||||
self._hot_max_tokens = hot_max_tokens
|
||||
self._logger = logger.bind(component="execute_phase_context_assembler")
|
||||
|
||||
def _resolve_execute_view(self, project_name: str) -> Any:
|
||||
"""Resolve the effective execute-phase view for *project_name*."""
|
||||
try:
|
||||
policy = self._project_repository.get_context_policy(project_name)
|
||||
except Exception:
|
||||
self._logger.warning(
|
||||
"context_policy_lookup_failed",
|
||||
project_name=project_name,
|
||||
exc_info=True,
|
||||
)
|
||||
return ProjectContextPolicy().resolve_view("execute")
|
||||
return policy.resolve_view("execute")
|
||||
|
||||
@staticmethod
|
||||
def _path_matches(path: str, include: list[str], exclude: list[str]) -> bool:
|
||||
"""Return whether *path* passes include/exclude path globs."""
|
||||
pure_path = PurePath(path)
|
||||
if include and not any(pure_path.full_match(pattern) for pattern in include):
|
||||
return False
|
||||
return not (
|
||||
exclude and any(pure_path.full_match(pattern) for pattern in exclude)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resource_matches(
|
||||
resource_id: str, include: list[str], exclude: list[str]
|
||||
) -> bool:
|
||||
"""Return whether *resource_id* passes include/exclude resource globs."""
|
||||
if include and not any(
|
||||
fnmatch.fnmatch(resource_id, pattern) for pattern in include
|
||||
):
|
||||
return False
|
||||
return not (
|
||||
exclude
|
||||
and any(fnmatch.fnmatch(resource_id, pattern) for pattern in exclude)
|
||||
)
|
||||
|
||||
def _to_context_fragment(
|
||||
self,
|
||||
fragment: TieredFragment,
|
||||
*,
|
||||
strategy: str,
|
||||
) -> CoreContextFragment:
|
||||
"""Convert a ``TieredFragment`` into a CRP ``ContextFragment``."""
|
||||
metadata = dict(fragment.metadata)
|
||||
detail_depth_raw = metadata.get("detail_depth", 1)
|
||||
detail_depth = detail_depth_raw if isinstance(detail_depth_raw, int) else 1
|
||||
score = metadata.get("relevance_score")
|
||||
relevance = float(score) if isinstance(score, (int, float)) else 0.5
|
||||
bounded_relevance = min(1.0, max(0.0, relevance))
|
||||
location = metadata.get("path", "")
|
||||
return CoreContextFragment(
|
||||
uko_node=fragment.resource_id or f"uko:{fragment.fragment_id}",
|
||||
content=fragment.content,
|
||||
detail_depth=detail_depth,
|
||||
token_count=fragment.token_count,
|
||||
relevance_score=bounded_relevance,
|
||||
provenance=CoreFragmentProvenance(
|
||||
resource_uri=fragment.resource_id or "unknown",
|
||||
location=location if isinstance(location, str) else "",
|
||||
strategy=strategy,
|
||||
),
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
def assemble(self, plan: Any) -> AssembledContext | None:
|
||||
"""Assemble execute-phase context for *plan* from scoped tier fragments."""
|
||||
project_names = [
|
||||
project.project_name for project in getattr(plan, "project_links", [])
|
||||
]
|
||||
if not project_names:
|
||||
return None
|
||||
|
||||
views = {name: self._resolve_execute_view(name) for name in project_names}
|
||||
scoped = self._tier.get_scoped_view(project_names)
|
||||
if not scoped:
|
||||
return None
|
||||
|
||||
filtered: list[CoreContextFragment] = []
|
||||
excluded_resource_or_scope = 0
|
||||
excluded_path_rules = 0
|
||||
excluded_max_file = 0
|
||||
excluded_max_total = 0
|
||||
per_project_bytes: dict[str, int] = {name: 0 for name in project_names}
|
||||
|
||||
for fragment in scoped:
|
||||
view = views.get(fragment.project_name)
|
||||
if view is None:
|
||||
excluded_resource_or_scope += 1
|
||||
continue
|
||||
|
||||
resource_id = fragment.resource_id or ""
|
||||
if not self._resource_matches(
|
||||
resource_id,
|
||||
view.include_resources,
|
||||
view.exclude_resources,
|
||||
):
|
||||
excluded_resource_or_scope += 1
|
||||
continue
|
||||
|
||||
path_value = (
|
||||
fragment.metadata.get("path")
|
||||
if isinstance(fragment.metadata, dict)
|
||||
else None
|
||||
)
|
||||
path_text = path_value if isinstance(path_value, str) else ""
|
||||
if path_text and not self._path_matches(
|
||||
path_text,
|
||||
view.include_paths,
|
||||
view.exclude_paths,
|
||||
):
|
||||
excluded_path_rules += 1
|
||||
continue
|
||||
if view.include_paths and not path_text:
|
||||
excluded_path_rules += 1
|
||||
continue
|
||||
|
||||
byte_size = len(fragment.content.encode("utf-8"))
|
||||
if view.max_file_size is not None and byte_size > view.max_file_size:
|
||||
excluded_max_file += 1
|
||||
continue
|
||||
|
||||
current_bytes = per_project_bytes.get(fragment.project_name, 0)
|
||||
if (
|
||||
view.max_total_size is not None
|
||||
and current_bytes + byte_size > view.max_total_size
|
||||
):
|
||||
excluded_max_total += 1
|
||||
continue
|
||||
per_project_bytes[fragment.project_name] = current_bytes + byte_size
|
||||
|
||||
filtered.append(
|
||||
self._to_context_fragment(
|
||||
fragment,
|
||||
strategy="execute_phase_context",
|
||||
)
|
||||
)
|
||||
|
||||
if not filtered:
|
||||
self._logger.info(
|
||||
"execute_context_empty_after_filtering",
|
||||
plan_id=plan.identity.plan_id,
|
||||
project_names=project_names,
|
||||
excluded_resource_or_scope=excluded_resource_or_scope,
|
||||
excluded_path_rules=excluded_path_rules,
|
||||
excluded_max_file_size=excluded_max_file,
|
||||
excluded_max_total_size=excluded_max_total,
|
||||
)
|
||||
return None
|
||||
|
||||
budget = CoreContextBudget(max_tokens=self._hot_max_tokens, reserved_tokens=0)
|
||||
request = ContextRequest(
|
||||
query=(
|
||||
f"Execute-phase context for plan {plan.identity.plan_id} "
|
||||
f"({', '.join(project_names)})"
|
||||
),
|
||||
purpose="llm_execute_phase_prompt",
|
||||
max_tokens=self._hot_max_tokens,
|
||||
)
|
||||
payload = self._pipeline.assemble(
|
||||
plan_id=plan.identity.plan_id,
|
||||
fragments=filtered,
|
||||
budget=budget,
|
||||
strategy="relevance",
|
||||
request=request,
|
||||
)
|
||||
self._logger.info(
|
||||
"execute_context_assembled",
|
||||
plan_id=plan.identity.plan_id,
|
||||
project_names=project_names,
|
||||
included_fragments=len(payload.fragments),
|
||||
total_tokens=payload.total_tokens,
|
||||
budget_used=payload.budget_used,
|
||||
excluded_resource_or_scope=excluded_resource_or_scope,
|
||||
excluded_path_rules=excluded_path_rules,
|
||||
excluded_max_file_size=excluded_max_file,
|
||||
excluded_max_total_size=excluded_max_total,
|
||||
)
|
||||
return AssembledContext(
|
||||
fragments=payload.fragments,
|
||||
total_tokens=payload.total_tokens,
|
||||
budget_used=payload.budget_used,
|
||||
strategies_used=payload.strategies_used,
|
||||
context_hash=payload.context_hash,
|
||||
preamble=payload.preamble,
|
||||
provenance_map=payload.provenance_map,
|
||||
)
|
||||
@@ -15,6 +15,9 @@ from typing import TYPE_CHECKING, Any
|
||||
import structlog
|
||||
from ulid import ULID
|
||||
|
||||
from cleveragents.application.services.execute_phase_context_assembler import (
|
||||
ExecutePhaseContextAssembler,
|
||||
)
|
||||
from cleveragents.application.services.plan_executor import (
|
||||
ExecuteResult,
|
||||
StrategizeResult,
|
||||
@@ -22,6 +25,7 @@ from cleveragents.application.services.plan_executor import (
|
||||
StreamCallback,
|
||||
)
|
||||
from cleveragents.core.exceptions import ValidationError
|
||||
from cleveragents.domain.models.acms.crp import AssembledContext
|
||||
from cleveragents.domain.models.core.plan import PlanInvariant
|
||||
from cleveragents.tool.builtins.changeset import ChangeSet, ChangeSetEntry
|
||||
|
||||
@@ -61,7 +65,7 @@ class LLMStrategizeActor:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
provider_registry: ProviderRegistry,
|
||||
provider_registry: ProviderRegistry | None,
|
||||
lifecycle_service: Any,
|
||||
) -> None:
|
||||
if provider_registry is None:
|
||||
@@ -214,8 +218,9 @@ class LLMExecuteActor:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
provider_registry: ProviderRegistry,
|
||||
provider_registry: ProviderRegistry | None,
|
||||
lifecycle_service: Any,
|
||||
context_assembler: ExecutePhaseContextAssembler | None = None,
|
||||
) -> None:
|
||||
if provider_registry is None:
|
||||
raise ValidationError("provider_registry must not be None")
|
||||
@@ -223,8 +228,35 @@ class LLMExecuteActor:
|
||||
raise ValidationError("lifecycle_service must not be None")
|
||||
self._registry = provider_registry
|
||||
self._lifecycle = lifecycle_service
|
||||
self._context_assembler = context_assembler
|
||||
self._logger = logger.bind(actor="llm_execute")
|
||||
|
||||
@staticmethod
|
||||
def _format_context_for_prompt(assembled: AssembledContext) -> str:
|
||||
"""Render assembled ACMS context into a deterministic prompt section."""
|
||||
lines = [
|
||||
"ACMS Execute-Phase Context:",
|
||||
f"- total_tokens: {assembled.total_tokens}",
|
||||
f"- budget_used: {assembled.budget_used:.4f}",
|
||||
f"- context_hash: {assembled.context_hash}",
|
||||
f"- strategies_used: {', '.join(assembled.strategies_used) or '(none)'}",
|
||||
]
|
||||
if assembled.preamble:
|
||||
lines.append(f"- preamble: {assembled.preamble}")
|
||||
lines.append("- fragments:")
|
||||
for idx, fragment in enumerate(assembled.fragments, start=1):
|
||||
lines.append(
|
||||
" "
|
||||
f"{idx}. {fragment.provenance.resource_uri} "
|
||||
f"({fragment.token_count} tokens, depth={fragment.detail_depth})"
|
||||
)
|
||||
lines.append(
|
||||
f" location: {fragment.provenance.location or '(unknown)'}"
|
||||
)
|
||||
lines.append(" content:")
|
||||
lines.append(f"{fragment.content}")
|
||||
return "\n".join(lines)
|
||||
|
||||
def execute(
|
||||
self,
|
||||
plan_id: str,
|
||||
@@ -258,12 +290,31 @@ class LLMExecuteActor:
|
||||
|
||||
llm = self._registry.create_llm(provider_type=provider_type, model_id=model_id)
|
||||
|
||||
assembled_context: AssembledContext | None = None
|
||||
if self._context_assembler is not None:
|
||||
try:
|
||||
assembled_context = self._context_assembler.assemble(plan)
|
||||
except Exception:
|
||||
self._logger.warning(
|
||||
"execute_context_assembly_failed",
|
||||
plan_id=plan_id,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# Build a prompt summarising the decisions
|
||||
steps_text = "\n".join(f"{d.sequence + 1}. {d.step_text}" for d in decisions)
|
||||
context_section = ""
|
||||
if assembled_context is not None:
|
||||
context_section = (
|
||||
"\n\n"
|
||||
f"{self._format_context_for_prompt(assembled_context)}"
|
||||
"\n\nUse this assembled context instead of requesting full raw files."
|
||||
)
|
||||
prompt = (
|
||||
"You are an expert software engineer. "
|
||||
"Implement the following steps by producing file changes.\n\n"
|
||||
f"Steps:\n{steps_text}\n\n"
|
||||
f"{context_section}"
|
||||
"For each file you create or modify, output a block:\n"
|
||||
"FILE: <path>\n```\n<full file content>\n```\n\n"
|
||||
"Only output file blocks. Do not add commentary."
|
||||
|
||||
@@ -1277,6 +1277,9 @@ def _get_plan_executor(lifecycle_service: PlanLifecycleService | None = None) ->
|
||||
bugs caused by the DI ``Factory`` creating independent instances.
|
||||
"""
|
||||
from cleveragents.application.container import get_container
|
||||
from cleveragents.application.services.execute_phase_context_assembler import (
|
||||
ACMSExecutePhaseContextAssembler,
|
||||
)
|
||||
from cleveragents.application.services.llm_actors import (
|
||||
LLMExecuteActor,
|
||||
LLMStrategizeActor,
|
||||
@@ -1292,9 +1295,15 @@ def _get_plan_executor(lifecycle_service: PlanLifecycleService | None = None) ->
|
||||
provider_registry=registry,
|
||||
lifecycle_service=lifecycle_service,
|
||||
)
|
||||
context_assembler = ACMSExecutePhaseContextAssembler(
|
||||
context_tier_service=container.context_tier_service(),
|
||||
project_repository=container.namespaced_project_repo(),
|
||||
hot_max_tokens=container.settings().context_max_tokens_hot,
|
||||
)
|
||||
execute_actor = LLMExecuteActor(
|
||||
provider_registry=registry,
|
||||
lifecycle_service=lifecycle_service,
|
||||
context_assembler=context_assembler,
|
||||
)
|
||||
|
||||
return PlanExecutor(
|
||||
|
||||
@@ -2976,6 +2976,43 @@ class NamespacedProjectRepository:
|
||||
except (OperationalError, SQLAlchemyDatabaseError) as exc:
|
||||
raise DatabaseError(f"Failed to list projects: {exc}") from exc
|
||||
|
||||
@database_retry
|
||||
def get_context_policy(self, namespaced_name: str) -> Any:
|
||||
"""Return a project's stored ``ProjectContextPolicy`` or the default."""
|
||||
from cleveragents.domain.models.core.context_policy import ProjectContextPolicy
|
||||
|
||||
session = self._session()
|
||||
try:
|
||||
row = (
|
||||
session.query(NamespacedProjectModel)
|
||||
.filter_by(namespaced_name=namespaced_name)
|
||||
.first()
|
||||
)
|
||||
if row is None or row.context_policy_json is None:
|
||||
return ProjectContextPolicy()
|
||||
|
||||
payload = json.loads(cast(str, row.context_policy_json))
|
||||
if not isinstance(payload, dict):
|
||||
_log.warning(
|
||||
"project_context_policy_payload_invalid",
|
||||
project_name=namespaced_name,
|
||||
)
|
||||
return ProjectContextPolicy()
|
||||
return ProjectContextPolicy.model_validate(payload)
|
||||
except (ValueError, TypeError):
|
||||
_log.warning(
|
||||
"project_context_policy_invalid",
|
||||
project_name=namespaced_name,
|
||||
exc_info=True,
|
||||
)
|
||||
return ProjectContextPolicy()
|
||||
except (OperationalError, SQLAlchemyDatabaseError) as exc:
|
||||
raise DatabaseError(
|
||||
f"Failed to load project context policy for '{namespaced_name}': {exc}"
|
||||
) from exc
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@database_retry
|
||||
def update(self, project: Any) -> Any:
|
||||
"""Update mutable fields of an existing project.
|
||||
|
||||
Reference in New Issue
Block a user