feat(aggregation): Implement tier-level aggregation strategy tied to Personas

This commit is contained in:
2026-05-04 05:10:59 -04:00
parent a895b822d8
commit 36645f7f3e
5 changed files with 99 additions and 15 deletions
+12 -7
View File
@@ -197,15 +197,20 @@ def _build_files_section_from_items(file_items: list[dict[str, Any]]) -> str:
sections.append(f"### `{original}`\n\n```{lang}\n{content}\n```")
return "\n\n---\n\n".join(sections)
def build_markdown_from_items(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str], summary_only: bool = False) -> str:
def build_markdown_from_items(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str], summary_only: bool = False, aggregation_strategy: str = "auto") -> str:
"""Build markdown from pre-read file items instead of re-reading from disk."""
parts = []
# STATIC PREFIX: Files and Screenshots must go first to maximize Cache Hits
if file_items:
if summary_only:
if aggregation_strategy == "summarize":
parts.append("## Files (Summary)\n\n" + summarize.build_summary_markdown(file_items))
else:
elif aggregation_strategy == "full":
parts.append("## Files\n\n" + _build_files_section_from_items(file_items))
else: # auto
if summary_only:
parts.append("## Files (Summary)\n\n" + summarize.build_summary_markdown(file_items))
else:
parts.append("## Files\n\n" + _build_files_section_from_items(file_items))
if screenshots:
parts.append("## Screenshots\n\n" + build_screenshots_section(screenshot_base_dir, screenshots))
# DYNAMIC SUFFIX: History changes every turn, must go last
@@ -213,9 +218,9 @@ def build_markdown_from_items(file_items: list[dict[str, Any]], screenshot_base_
parts.append("## Discussion History\n\n" + build_discussion_section(history))
return "\n\n---\n\n".join(parts)
def build_markdown_no_history(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], summary_only: bool = False) -> str:
def build_markdown_no_history(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], summary_only: bool = False, aggregation_strategy: str = "auto") -> str:
"""Build markdown with only files + screenshots (no history). Used for stable caching."""
return build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history=[], summary_only=summary_only)
return build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history=[], summary_only=summary_only, aggregation_strategy=aggregation_strategy)
def build_discussion_text(history: list[str]) -> str:
"""Build just the discussion history section text. Returns empty string if no history."""
@@ -319,7 +324,7 @@ def build_markdown(base_dir: Path, files: list[str | dict[str, Any]], screenshot
parts.append("## Discussion History\n\n" + build_discussion_section(history))
return "\n\n---\n\n".join(parts)
def run(config: dict[str, Any]) -> tuple[str, Path, list[dict[str, Any]]]:
def run(config: dict[str, Any], aggregation_strategy: str = "auto") -> tuple[str, Path, list[dict[str, Any]]]:
namespace = config.get("project", {}).get("name")
if not namespace:
namespace = config.get("output", {}).get("namespace", "project")
@@ -336,7 +341,7 @@ def run(config: dict[str, Any]) -> tuple[str, Path, list[dict[str, Any]]]:
file_items = build_file_items(base_dir, files)
summary_only = config.get("project", {}).get("summary_only", False)
markdown = build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history,
summary_only=summary_only)
summary_only=summary_only, aggregation_strategy=aggregation_strategy)
output_file.write_text(markdown, encoding="utf-8")
return markdown, output_file, file_items
+7 -2
View File
@@ -148,6 +148,7 @@ class AppController:
self.project_paths: List[str] = []
self.active_discussion: str = "main"
self.disc_entries: List[Dict[str, Any]] = []
self.ui_active_persona: str = ""
self.disc_roles: List[str] = []
self.files: List[str] = []
self.screenshots: List[str] = []
@@ -2550,12 +2551,16 @@ class AppController:
models.save_config(self.config)
track_id = self.active_track.id if self.active_track else None
flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id)
full_md, path, file_items = aggregate.run(flat)
persona = self.personas.get(self.ui_active_persona)
strategy = persona.aggregation_strategy if persona else "auto"
full_md, path, file_items = aggregate.run(flat, aggregation_strategy=strategy)
# Build stable markdown (no history) for Gemini caching
screenshot_base_dir = Path(flat.get("screenshots", {}).get("base_dir", "."))
screenshots = flat.get("screenshots", {}).get("paths", [])
summary_only = flat.get("project", {}).get("summary_only", False)
stable_md = aggregate.build_markdown_no_history(file_items, screenshot_base_dir, screenshots, summary_only=summary_only)
stable_md = aggregate.build_markdown_no_history(file_items, screenshot_base_dir, screenshots, summary_only=summary_only, aggregation_strategy=strategy)
# Build discussion history text separately
history = flat.get("discussion", {}).get("history", [])
discussion_text = aggregate.build_discussion_text(history)
+4
View File
@@ -470,6 +470,7 @@ class Persona:
tool_preset: Optional[str] = None
bias_profile: Optional[str] = None
context_preset: Optional[str] = None
aggregation_strategy: Optional[str] = None
@property
def provider(self) -> Optional[str]:
@@ -514,6 +515,8 @@ class Persona:
res["bias_profile"] = self.bias_profile
if self.context_preset is not None:
res["context_preset"] = self.context_preset
if self.aggregation_strategy is not None:
res["aggregation_strategy"] = self.aggregation_strategy
return res
@classmethod
@@ -548,6 +551,7 @@ class Persona:
tool_preset=data.get("tool_preset"),
bias_profile=data.get("bias_profile"),
context_preset=data.get("context_preset"),
aggregation_strategy=data.get("aggregation_strategy"),
)
@dataclass
class MCPServerConfig:
+14 -6
View File
@@ -28,6 +28,7 @@ See Also:
- src/models.py for Ticket, Track, WorkerContext
"""
from src import ai_client
from src import summarize
import json
import threading
import time
@@ -401,6 +402,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
# Apply Persona if specified
preferred_models = []
persona_tool_preset = None
persona = None
if context.persona_id:
from src.personas import PersonaManager
from src import paths
@@ -443,6 +445,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
if context_files:
parser = ASTParser(language="python")
strategy = getattr(persona, "aggregation_strategy", "auto") if persona else "auto"
for i, file_path in enumerate(context_files):
try:
Path(file_path)
@@ -452,12 +455,17 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
tokens_before += _count_tokens(content)
if i == 0:
view = parser.get_curated_view(content, path=file_path)
elif ticket.target_file and Path(file_path).resolve() == Path(ticket.target_file).resolve() and ticket.target_symbols:
view = parser.get_targeted_view(content, ticket.target_symbols, path=file_path)
else:
view = parser.get_skeleton(content, path=file_path)
if strategy == "summarize":
view = summarize.summarise_file(Path(file_path), content)
elif strategy == "full":
view = content
else: # auto or skeleton
if i == 0:
view = parser.get_curated_view(content, path=file_path)
elif ticket.target_file and Path(file_path).resolve() == Path(ticket.target_file).resolve() and ticket.target_symbols:
view = parser.get_targeted_view(content, ticket.target_symbols, path=file_path)
else:
view = parser.get_skeleton(content, path=file_path)
tokens_after += _count_tokens(view)
context_injection += f"\nFile: {file_path}\n{view}\n"