Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdbb61fe31 |
@@ -420,6 +420,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
message listing available built-in profiles. The resolved profile name is also
|
||||
logged at debug level for observability.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Configurable Agent Limits** (#9246): Replaced hardcoded `deps[:10]` in
|
||||
``ContextAnalysisAgent`` and ``contexts[:5]` in ``PlanGenerationGraph`` with
|
||||
configurable constructor parameters ``max_dependencies`` (default: 10) and
|
||||
``max_context_files`` (default: 5). Non-positive values raise ``ValueError``.
|
||||
All existing call sites remain backward-compatible via default arguments.
|
||||
|
||||
### Added
|
||||
|
||||
- **ACMS Index Data Model and File Traversal Engine** (#9579): Implements the
|
||||
|
||||
@@ -16,6 +16,7 @@ Below are some of the specific details of various contributions.
|
||||
* Brent E. Edwards has contributed quality assurance, test coverage, and CI pipeline improvements.
|
||||
* HAL 9000 has contributed automated implementation, bug fixes, and feature development as part of the CleverAgents automation pool.
|
||||
* HAL 9000 has contributed concurrency safety improvements, including thread-safe context tier management (issue #7547) for parallel plan execution.
|
||||
* HAL 9000 has contributed the configurable agent limits refactor (#9246/#9050): replaced hardcoded ``deps[:10]`` in ``ContextAnalysisAgent`` and ``contexts[:5]`` in ``PlanGenerationGraph`` with validated constructor parameters ``max_dependencies`` (default: 10) and ``max_context_files`` (default: 5), including 12 BDD scenarios covering defaults, custom values, edge cases, and invalid-input error handling.
|
||||
* HAL 9000 has contributed the plan concurrency race-condition fix (#7989): wired `LockService` into the plan lifecycle, guarding `execute_plan()` and `apply_plan()` with plan-level advisory locks and unique per-invocation owner identities to prevent silent concurrent state corruption.
|
||||
* HAL 9000 has contributed the bug-hunt-pool-supervisor non-blocking tracking fix: updated step 5 to be best-effort and added rule 9 to prevent the automation-tracking-manager call from blocking the main supervisor loop.
|
||||
* HAL 9000 has contributed the plugin entry point security hardening fix (#7476): enforced entry point allowlist validation before importing plugin modules to prevent malicious plugin loading.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
Feature: Configurable limits in context analysis and plan generation
|
||||
As a developer using the agents module
|
||||
I want to configure the maximum number of dependencies and context files
|
||||
So that I can tune the agents for different project sizes and resource constraints
|
||||
|
||||
Background:
|
||||
Given the configurable limits module is importable
|
||||
|
||||
# ContextAnalysisAgent.max_dependencies tests
|
||||
|
||||
@configurable_limits
|
||||
Scenario: ContextAnalysisAgent uses default max_dependencies of 10
|
||||
Given a ContextAnalysisAgent with default settings
|
||||
When I parse dependencies from a string with 15 items
|
||||
Then the result should contain exactly 10 dependencies
|
||||
|
||||
@configurable_limits
|
||||
Scenario: ContextAnalysisAgent respects custom max_dependencies
|
||||
Given a ContextAnalysisAgent with max_dependencies set to 3
|
||||
When I parse dependencies from a string with 15 items
|
||||
Then the result should contain exactly 3 dependencies
|
||||
|
||||
@configurable_limits
|
||||
Scenario: ContextAnalysisAgent raises ValueError for non-positive max_dependencies
|
||||
When I create a ContextAnalysisAgent with max_dependencies set to 0
|
||||
Then a ValueError should be raised about max_dependencies
|
||||
|
||||
@configurable_limits
|
||||
Scenario: ContextAnalysisAgent raises ValueError for negative max_dependencies
|
||||
When I create a ContextAnalysisAgent with max_dependencies set to -5
|
||||
Then a ValueError should be raised about max_dependencies
|
||||
|
||||
@configurable_limits
|
||||
Scenario: ContextAnalysisAgent max_dependencies of 1 returns single dependency
|
||||
Given a ContextAnalysisAgent with max_dependencies set to 1
|
||||
When I parse dependencies from a string with 15 items
|
||||
Then the result should contain exactly 1 dependencies
|
||||
|
||||
# PlanGenerationGraph.max_context_files tests
|
||||
|
||||
@configurable_limits
|
||||
Scenario: PlanGenerationGraph uses default max_context_files of 5
|
||||
Given a PlanGenerationGraph with default settings
|
||||
When I format a context summary with 8 context files
|
||||
Then the summary should contain exactly 5 file entries
|
||||
And the summary should mention 3 more files
|
||||
|
||||
@configurable_limits
|
||||
Scenario: PlanGenerationGraph respects custom max_context_files
|
||||
Given a PlanGenerationGraph with max_context_files set to 2
|
||||
When I format a context summary with 8 context files
|
||||
Then the summary should contain exactly 2 file entries
|
||||
And the summary should mention 6 more files
|
||||
|
||||
@configurable_limits
|
||||
Scenario: PlanGenerationGraph raises ValueError for non-positive max_context_files
|
||||
When I create a PlanGenerationGraph with max_context_files set to 0
|
||||
Then a ValueError should be raised about max_context_files
|
||||
|
||||
@configurable_limits
|
||||
Scenario: PlanGenerationGraph raises ValueError for negative max_context_files
|
||||
When I create a PlanGenerationGraph with max_context_files set to -1
|
||||
Then a ValueError should be raised about max_context_files
|
||||
|
||||
@configurable_limits
|
||||
Scenario: PlanGenerationGraph with fewer contexts than max_context_files shows all
|
||||
Given a PlanGenerationGraph with max_context_files set to 10
|
||||
When I format a context summary with 3 context files
|
||||
Then the summary should contain exactly 3 file entries
|
||||
And the summary should not mention more files
|
||||
|
||||
@configurable_limits
|
||||
Scenario: PlanGenerationGraph with exactly max_context_files contexts shows all
|
||||
Given a PlanGenerationGraph with default settings
|
||||
When I format a context summary with 5 context files
|
||||
Then the summary should contain exactly 5 file entries
|
||||
And the summary should not mention more files
|
||||
|
||||
@configurable_limits
|
||||
Scenario: PlanGenerationGraph with empty contexts returns no context message
|
||||
Given a PlanGenerationGraph with default settings
|
||||
When I format a context summary with 0 context files
|
||||
Then the summary should be the no-context message
|
||||
@@ -0,0 +1,191 @@
|
||||
"""Step definitions for agent_configurable_limits.feature.
|
||||
|
||||
These steps test the configurable ``max_dependencies`` parameter on
|
||||
``ContextAnalysisAgent`` and the configurable ``max_context_files`` parameter
|
||||
on ``PlanGenerationGraph``, covering:
|
||||
|
||||
- Default values preserve existing behaviour (10 / 5 respectively)
|
||||
- Custom values are respected
|
||||
- Non-positive values raise ``ValueError``
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from behave import given, then, when
|
||||
from langchain_community.llms import FakeListLLM
|
||||
|
||||
from cleveragents.agents.graphs.context_analysis import ContextAnalysisAgent
|
||||
from cleveragents.agents.graphs.plan_generation import PlanGenerationGraph
|
||||
from cleveragents.domain.models.core import Context
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_DEFAULT_RESPONSES = [
|
||||
"Dependencies: ['os', 'sys', 'pathlib']",
|
||||
"Relevance: 0.8 - core module",
|
||||
"Summary: Generated context overview",
|
||||
] * 30
|
||||
|
||||
|
||||
def _make_llm() -> FakeListLLM:
|
||||
return FakeListLLM(responses=list(_DEFAULT_RESPONSES))
|
||||
|
||||
|
||||
def _make_dep_string(count: int) -> str:
|
||||
"""Return an LLM-style dependency string with *count* items."""
|
||||
items = ", ".join(f"dep_{i}" for i in range(count))
|
||||
return f"[{items}]"
|
||||
|
||||
|
||||
def _make_contexts(count: int) -> list[Context]:
|
||||
"""Return a list of *count* dummy Context objects."""
|
||||
return [
|
||||
Context(plan_id=1, path=f"src/file_{i}.py", content=f"content of file {i}")
|
||||
for i in range(count)
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Background
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("the configurable limits module is importable")
|
||||
def step_module_importable(context: Any) -> None:
|
||||
assert ContextAnalysisAgent is not None
|
||||
assert PlanGenerationGraph is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ContextAnalysisAgent — default max_dependencies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("a ContextAnalysisAgent with default settings")
|
||||
def step_create_agent_default(context: Any) -> None:
|
||||
context.agent = ContextAnalysisAgent(llm=_make_llm())
|
||||
|
||||
|
||||
@given("a ContextAnalysisAgent with max_dependencies set to {limit:d}")
|
||||
def step_create_agent_custom_max_deps(context: Any, limit: int) -> None:
|
||||
context.agent = ContextAnalysisAgent(llm=_make_llm(), max_dependencies=limit)
|
||||
|
||||
|
||||
@when("I parse dependencies from a string with {count:d} items")
|
||||
def step_parse_deps_with_count(context: Any, count: int) -> None:
|
||||
dep_string = _make_dep_string(count)
|
||||
context.parsed_deps = context.agent._parse_dependencies(dep_string)
|
||||
|
||||
|
||||
@then("the result should contain exactly {expected:d} dependencies")
|
||||
def step_check_dep_count(context: Any, expected: int) -> None:
|
||||
actual = len(context.parsed_deps)
|
||||
assert actual == expected, (
|
||||
f"Expected {expected} dependencies, got {actual}: {context.parsed_deps!r}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ContextAnalysisAgent — ValueError for invalid max_dependencies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I create a ContextAnalysisAgent with max_dependencies set to {limit:d}")
|
||||
def step_create_agent_invalid_max_deps(context: Any, limit: int) -> None:
|
||||
context.raised_error = None
|
||||
try:
|
||||
ContextAnalysisAgent(llm=_make_llm(), max_dependencies=limit)
|
||||
except ValueError as exc:
|
||||
context.raised_error = exc
|
||||
|
||||
|
||||
@then("a ValueError should be raised about max_dependencies")
|
||||
def step_check_value_error_max_deps(context: Any) -> None:
|
||||
assert context.raised_error is not None, (
|
||||
"Expected ValueError but no error was raised"
|
||||
)
|
||||
assert "max_dependencies" in str(context.raised_error), (
|
||||
f"Expected 'max_dependencies' in error message, got: {context.raised_error!r}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PlanGenerationGraph — default max_context_files
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("a PlanGenerationGraph with default settings")
|
||||
def step_create_graph_default(context: Any) -> None:
|
||||
context.graph = PlanGenerationGraph(llm=_make_llm())
|
||||
|
||||
|
||||
@given("a PlanGenerationGraph with max_context_files set to {limit:d}")
|
||||
def step_create_graph_custom_max_ctx(context: Any, limit: int) -> None:
|
||||
context.graph = PlanGenerationGraph(llm=_make_llm(), max_context_files=limit)
|
||||
|
||||
|
||||
@when("I format a context summary with {count:d} context files")
|
||||
def step_format_context_summary(context: Any, count: int) -> None:
|
||||
contexts = _make_contexts(count)
|
||||
context.summary = context.graph._format_context_summary(contexts)
|
||||
context.context_count = count
|
||||
|
||||
|
||||
@then("the summary should contain exactly {expected:d} file entries")
|
||||
def step_check_file_entries(context: Any, expected: int) -> None:
|
||||
# Each file entry starts with "File: "
|
||||
file_count = context.summary.count("File: ")
|
||||
assert file_count == expected, (
|
||||
f"Expected {expected} 'File:' entries in summary, got {file_count}.\n"
|
||||
f"Summary:\n{context.summary!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the summary should mention {extra:d} more files")
|
||||
def step_check_more_files_mention(context: Any, extra: int) -> None:
|
||||
assert f"... and {extra} more files" in context.summary, (
|
||||
f"Expected '... and {extra} more files' in summary.\n"
|
||||
f"Summary:\n{context.summary!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the summary should not mention more files")
|
||||
def step_check_no_more_files(context: Any) -> None:
|
||||
assert "more files" not in context.summary, (
|
||||
f"Expected no 'more files' mention in summary.\nSummary:\n{context.summary!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the summary should be the no-context message")
|
||||
def step_check_no_context_message(context: Any) -> None:
|
||||
assert context.summary == "No context files provided", (
|
||||
f"Expected 'No context files provided', got: {context.summary!r}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PlanGenerationGraph — ValueError for invalid max_context_files
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I create a PlanGenerationGraph with max_context_files set to {limit:d}")
|
||||
def step_create_graph_invalid_max_ctx(context: Any, limit: int) -> None:
|
||||
context.raised_error = None
|
||||
try:
|
||||
PlanGenerationGraph(llm=_make_llm(), max_context_files=limit)
|
||||
except ValueError as exc:
|
||||
context.raised_error = exc
|
||||
|
||||
|
||||
@then("a ValueError should be raised about max_context_files")
|
||||
def step_check_value_error_max_ctx(context: Any) -> None:
|
||||
assert context.raised_error is not None, (
|
||||
"Expected ValueError but no error was raised"
|
||||
)
|
||||
assert "max_context_files" in str(context.raised_error), (
|
||||
f"Expected 'max_context_files' in error message, got: {context.raised_error!r}"
|
||||
)
|
||||
@@ -97,6 +97,7 @@ class ContextAnalysisAgent:
|
||||
chunk_size: int = 2000,
|
||||
chunk_overlap: int = 200,
|
||||
retry_attempts: int = 3,
|
||||
max_dependencies: int = 10,
|
||||
):
|
||||
"""Initialize the context analysis agent.
|
||||
|
||||
@@ -105,10 +106,21 @@ class ContextAnalysisAgent:
|
||||
chunk_size: Maximum size of each chunk in characters
|
||||
chunk_overlap: Overlap between chunks in characters
|
||||
retry_attempts: Number of times to retry transient LLM failures
|
||||
max_dependencies: Maximum number of dependencies returned by
|
||||
``_parse_dependencies``. Must be a positive integer.
|
||||
Defaults to ``10``.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``max_dependencies`` is not a positive integer.
|
||||
"""
|
||||
if max_dependencies <= 0:
|
||||
raise ValueError(
|
||||
f"max_dependencies must be a positive integer, got {max_dependencies!r}"
|
||||
)
|
||||
self.chunk_size = chunk_size
|
||||
self.chunk_overlap = chunk_overlap
|
||||
self.retry_attempts = max(1, retry_attempts)
|
||||
self.max_dependencies = max_dependencies
|
||||
|
||||
# Initialize LLM - an LLM must be provided explicitly
|
||||
if llm is None:
|
||||
@@ -291,7 +303,7 @@ class ContextAnalysisAgent:
|
||||
.split(",")
|
||||
)
|
||||
deps.extend(part.strip() for part in parts if part.strip())
|
||||
return deps[:10]
|
||||
return deps[: self.max_dependencies]
|
||||
|
||||
def _chunk_documents(self, state: ContextAnalysisState) -> dict[str, Any]:
|
||||
"""Chunk large documents for processing."""
|
||||
|
||||
@@ -165,6 +165,7 @@ class PlanGenerationGraph:
|
||||
max_retries: int = 3,
|
||||
context_llm: BaseLanguageModel | None = None,
|
||||
checkpoint_limit: int = 2,
|
||||
max_context_files: int = 5,
|
||||
):
|
||||
"""Initialize the plan generation graph.
|
||||
|
||||
@@ -173,7 +174,19 @@ class PlanGenerationGraph:
|
||||
max_retries: Maximum number of retry attempts
|
||||
context_llm: Optional language model dedicated to context analysis
|
||||
checkpoint_limit: Maximum checkpoints to retain per thread
|
||||
max_context_files: Maximum number of context files included in
|
||||
``_format_context_summary``. Must be a positive integer.
|
||||
Defaults to ``5``.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``max_context_files`` is not a positive integer.
|
||||
"""
|
||||
if max_context_files <= 0:
|
||||
raise ValueError(
|
||||
"max_context_files must be a positive integer, "
|
||||
f"got {max_context_files!r}"
|
||||
)
|
||||
self.max_context_files = max_context_files
|
||||
self.max_retries = max(1, max_retries)
|
||||
|
||||
# Initialize LLMs - an LLM must be provided explicitly
|
||||
@@ -578,12 +591,14 @@ class PlanGenerationGraph:
|
||||
return "No context files provided"
|
||||
|
||||
summary_parts: list[str] = []
|
||||
for ctx in contexts[:5]: # Limit to first 5 files
|
||||
for ctx in contexts[: self.max_context_files]:
|
||||
content_preview = ctx.content[:300] if ctx.content else ""
|
||||
summary_parts.append(f"File: {ctx.path}\nPreview: {content_preview}...\n")
|
||||
|
||||
if len(contexts) > 5:
|
||||
summary_parts.append(f"... and {len(contexts) - 5} more files")
|
||||
if len(contexts) > self.max_context_files:
|
||||
summary_parts.append(
|
||||
f"... and {len(contexts) - self.max_context_files} more files"
|
||||
)
|
||||
|
||||
return "\n".join(summary_parts)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user