Skip to content

Commit 36b71c7

Browse files
committed
Refactor incremental planning, enhance observability, and improve project memory management
- Removed redundant state.objective assignment in IncrementalPlanner. - Updated Replanner to correctly reflect step statuses and objectives. - Integrated observability report rendering in Reporter class. - Enhanced AgentState to conditionally add timeline events based on observability settings. - Added graph impact recording in ApplyPatchTool and SearchReplaceTool. - Implemented tests for incremental planning, observability, project memory, and repository graph functionalities. - Improved error handling and logging for graph impact analysis.
1 parent 9d3ce5c commit 36b71c7

29 files changed

Lines changed: 1866 additions & 13 deletions

‎.env.example‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,24 @@ REPLAN_ON_FAILURE=true
156156
# against the leash with its own stop reason). 0 disables the cap.
157157
MAX_REPLANS=3
158158

159+
# --- Project Memory (Phase 12, local, human-readable) ------------------------
160+
# Maintains local markdown memory under the workspace and injects useful entries
161+
# into future planning. No embeddings, cloud calls, or external database.
162+
PROJECT_MEMORY_ENABLED=true
163+
PROJECT_MEMORY_DIR=.localcli/memory
164+
165+
# --- Repository Graph (Phase 13 MVP, local) ----------------------------------
166+
# Builds .localcli/graph.json with import/module edges and impact queries for
167+
# Python and JS/TS. Enhancement items such as call graph and export are not part
168+
# of the MVP.
169+
REPO_GRAPH_ENABLED=true
170+
GRAPH_DIR=.localcli
171+
172+
# --- Observability (Phase 14, AgentState-backed) -----------------------------
173+
# Collect lightweight timeline events and render the run dashboard/report panels.
174+
OBSERVABILITY_ENABLED=true
175+
VERBOSITY=normal # quiet | normal | verbose
176+
159177
# --- LLM retries -------------------------------------------------------------
160178
# Structured-output retry/repair attempts per LLM call (all providers).
161179
MAX_RETRIES=3

‎agent/cli.py‎

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030

3131
# Subcommands argparse owns; anything else as the first token is treated as a
3232
# bare-task shorthand for `run`.
33-
KNOWN_SUBCOMMANDS = {"run", "config", "models", "index", "search", "symbols", "context"}
33+
KNOWN_SUBCOMMANDS = {
34+
"run", "config", "models", "index", "search", "symbols", "context", "memory", "graph",
35+
}
3436

3537

3638
# --- Routing table -----------------------------------------------------------
@@ -152,18 +154,21 @@ async def cmd_run(args):
152154
if mode == "agent":
153155
from agent.state.agent_state import AgentState, TaskMetadata
154156
from agent.context import build_context_bundle
157+
from agent.memory.project_memory import ProjectMemoryManager
155158
state = AgentState(user_request=task_description,
156159
task=TaskMetadata(description=task_description))
157160
try:
158161
state.loaded_context = await build_context_bundle(settings.get_workspace_path())
159162
except Exception:
160163
pass
164+
project_memory = ProjectMemoryManager(settings.get_workspace_path())
165+
project_memory.load_into_state(state)
161166
if caps:
162167
state.capabilities = caps.model_dump()
163168
# Phase 11: incremental planning is on by default (settings); the agent
164169
# loop then runs plan -> execute step -> observe -> replan.
165170
engine = build_engine("agent", safety_mode=safety_mode,
166-
memory_manager=orchestrator.memory_manager,
171+
memory_manager=project_memory,
167172
incremental=settings.incremental_planning)
168173
state = await engine.execute(state)
169174
print(f"\nAgent run complete: status={state.final_outputs.status}, "
@@ -173,6 +178,10 @@ async def cmd_run(args):
173178
if settings.incremental_planning and state.plan.revisions:
174179
from agent.planning import render_plan_evolution
175180
print("\n" + render_plan_evolution(state))
181+
project_memory.update_from_state(state)
182+
if settings.observability_enabled and (args.observe or settings.verbosity == "verbose"):
183+
from agent.observability import render_dashboard
184+
print("\n" + render_dashboard(state))
176185
return 0
177186

178187
# Phase 11: pipeline strategy with step-wise execution + replanning when
@@ -183,6 +192,11 @@ async def cmd_run(args):
183192
else:
184193
report_path = await orchestrator.run(task_description)
185194
print(f"\nExecution complete. Report generated at: {report_path}")
195+
if settings.observability_enabled and (args.observe or settings.verbosity == "verbose"):
196+
state = getattr(orchestrator, "last_state", None)
197+
if state is not None:
198+
from agent.observability import render_dashboard
199+
print("\n" + render_dashboard(state))
186200
return 0
187201

188202

@@ -244,6 +258,49 @@ async def cmd_context(args):
244258
return 0
245259

246260

261+
def cmd_memory(args):
262+
"""Phase 12: view local project-memory markdown files."""
263+
from agent.memory.project_memory import ProjectMemoryManager
264+
265+
manager = ProjectMemoryManager(settings.get_workspace_path())
266+
if not manager.enabled:
267+
print("Project memory is disabled (PROJECT_MEMORY_ENABLED=false).")
268+
return 0
269+
bundle = manager.load()
270+
print(f"Project Memory: {manager.relative_dir}")
271+
if not bundle.files:
272+
print("(no project memory files)")
273+
return 0
274+
for rel in sorted(bundle.files):
275+
print("\n" + "=" * 52)
276+
print(rel)
277+
print("-" * 52)
278+
print(bundle.files[rel].rstrip())
279+
if bundle.recovered_files:
280+
print("\nRecovered/normalized:")
281+
for rel in bundle.recovered_files:
282+
print(f"- {rel}")
283+
return 0
284+
285+
286+
def cmd_graph(args):
287+
"""Phase 13: repository graph impact query."""
288+
if getattr(args, "graph_action", None) != "impact":
289+
print("Usage: localcli graph impact <file>")
290+
return 2
291+
from agent.graph import GraphBuilder, ImpactAnalyzer
292+
293+
if not settings.repo_graph_enabled:
294+
print("Repository graph is disabled (REPO_GRAPH_ENABLED=false).")
295+
return 0
296+
graph = GraphBuilder(settings.get_workspace_path()).build(force=args.refresh)
297+
dependents = ImpactAnalyzer(graph).dependents(args.file)
298+
print(f"Impact for {args.file}: {len(dependents)} dependent file(s)")
299+
for dep in dependents:
300+
print(f"- {dep}")
301+
return 0
302+
303+
247304
async def cmd_index(args):
248305
from agent.orchestrator import Orchestrator
249306
orchestrator = Orchestrator()
@@ -303,6 +360,7 @@ def build_parser():
303360
p_run = sub.add_parser("run", help="Run an end-to-end coding task")
304361
p_run.add_argument("task", nargs="*", help="The task description")
305362
p_run.add_argument("--reindex", action="store_true", help="Force full rebuild of the index first")
363+
p_run.add_argument("--observe", action="store_true", help="Print the Phase 14 AgentState dashboard after the run")
306364
_add_safety_flags(p_run)
307365

308366
p_index = sub.add_parser("index", help="Build/update the workspace index")
@@ -316,6 +374,14 @@ def build_parser():
316374
p_context = sub.add_parser("context", help="Generate/refresh the repository Context Bundle (Phase 9, local)")
317375
p_context.add_argument("--refresh", action="store_true", help="Force a full rescan, ignoring the cache")
318376

377+
sub.add_parser("memory", help="View local project memory markdown files (Phase 12)")
378+
379+
p_graph = sub.add_parser("graph", help="Repository graph queries (Phase 13 MVP)")
380+
graph_sub = p_graph.add_subparsers(dest="graph_action")
381+
p_impact = graph_sub.add_parser("impact", help="List files that depend on a file")
382+
p_impact.add_argument("file", help="Repository-relative file path")
383+
p_impact.add_argument("--refresh", action="store_true", help="Force graph regeneration first")
384+
319385
p_config = sub.add_parser("config", help="Configuration utilities")
320386
config_sub = p_config.add_subparsers(dest="config_action")
321387
config_sub.add_parser("check", help="Run preflight and print the resolved routing table + health")
@@ -351,6 +417,10 @@ async def _dispatch(args):
351417
return await cmd_symbols(args)
352418
if args.subcommand == "context":
353419
return await cmd_context(args)
420+
if args.subcommand == "memory":
421+
return cmd_memory(args)
422+
if args.subcommand == "graph":
423+
return cmd_graph(args)
354424
# default: run
355425
return await cmd_run(args)
356426

‎agent/config.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,22 @@ class AgentSettings(BaseSettings):
128128
replan_on_failure: bool = True
129129
max_replans: int = 3
130130

131+
# Phase 12: Project Memory. Local, human-readable markdown files that are
132+
# loaded at run start and updated only after successful runs. This is NOT
133+
# vector memory: no embeddings, cloud calls, or external database.
134+
project_memory_enabled: bool = True
135+
project_memory_dir: str = ".localcli/memory"
136+
137+
# Phase 13: Repository Graph MVP. Local graph.json with import/module edges
138+
# and impact queries for primary languages (py/js/ts). No call graph here.
139+
repo_graph_enabled: bool = True
140+
graph_dir: str = ".localcli"
141+
142+
# Phase 14: Observability. Timeline events live on AgentState and the
143+
# dashboard/report are read-only renderers over that state.
144+
observability_enabled: bool = True
145+
verbosity: str = "normal" # quiet | normal | verbose
146+
131147
# Logging
132148
log_level: str = "DEBUG"
133149

‎agent/context/engine.py‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,30 @@ async def build(self, *, use_cache: bool = True, force: bool = False,
7070

7171
logger.info("Context engine: building fresh context bundle.")
7272
scan = self.scanner.scan(self.workspace)
73+
dependency_graph = scan.dependency_graph
74+
repository_graph = {}
75+
if settings.repo_graph_enabled:
76+
try:
77+
from agent.graph.builder import GraphBuilder
78+
graph_builder = GraphBuilder(self.workspace, scanner=self.scanner)
79+
cached_graph = graph_builder.store.load()
80+
if cached_graph is not None and cached_graph.fingerprint == scan.fingerprint:
81+
graph = cached_graph
82+
else:
83+
graph = graph_builder.build_from_scan(scan)
84+
dependency_graph = dict(scan.dependency_graph)
85+
dependency_graph.update({
86+
path: module.dependencies
87+
for path, module in graph.modules.items()
88+
if module.dependencies
89+
})
90+
repository_graph = {
91+
"path": str(graph_builder.store.path),
92+
"modules": len(graph.modules),
93+
"edges": len(graph.edges),
94+
}
95+
except Exception as e:
96+
logger.warning(f"Context engine: repository graph unavailable, using scan map: {e}")
7397

7498
tech_stack = detectors.detect_tech_stack(scan.manifests)
7599
frameworks = detectors.detect_frameworks(tech_stack)
@@ -87,7 +111,8 @@ async def build(self, *, use_cache: bool = True, force: bool = False,
87111
tech_stack=tech_stack,
88112
entry_points=entry_points,
89113
conventions=conventions,
90-
dependency_graph=scan.dependency_graph,
114+
dependency_graph=dependency_graph,
115+
repository_graph=repository_graph,
91116
symbol_count=scan.symbol_count,
92117
)
93118

‎agent/context/schemas.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ class ContextBundle(BaseModel):
6161
entry_points: List[EntryPoint] = Field(default_factory=list)
6262
conventions: Conventions = Field(default_factory=Conventions)
6363
dependency_graph: Dict[str, List[str]] = Field(default_factory=dict)
64+
repository_graph: Dict[str, str | int] = Field(default_factory=dict) # Phase 13 graph metadata
6465
architecture_summary: Optional[str] = None # None when no LLM was available
6566
symbol_count: int = 0
67+
project_memory: Dict[str, str] = Field(default_factory=dict) # Phase 12 markdown memory
6668

6769
def to_planner_block(self) -> str:
6870
"""Render a compact, deterministic text block for the planner prompt.
@@ -84,6 +86,17 @@ def to_planner_block(self) -> str:
8486
if self.entry_points:
8587
eps = [f"{e.kind}:{e.target}" for e in self.entry_points[:8]]
8688
lines.append("- Entry points: " + ", ".join(eps))
89+
if self.repository_graph:
90+
modules = self.repository_graph.get("modules", 0)
91+
edges = self.repository_graph.get("edges", 0)
92+
lines.append(f"- Repository graph: {modules} modules, {edges} import edges")
93+
if self.dependency_graph:
94+
graph_bits = []
95+
for source, deps in sorted(self.dependency_graph.items())[:8]:
96+
if deps:
97+
graph_bits.append(f"{source} -> {', '.join(sorted(deps)[:4])}")
98+
if graph_bits:
99+
lines.append("- Import graph: " + " | ".join(graph_bits))
87100
conv = self.conventions
88101
if conv.primary_language or conv.test_layout or conv.lint_tools:
89102
bits = []
@@ -102,6 +115,18 @@ def to_planner_block(self) -> str:
102115
lines.append("- Architecture:")
103116
for ln in self.architecture_summary.strip().splitlines():
104117
lines.append(f" {ln}")
118+
if self.project_memory:
119+
lines.append("- Project Memory:")
120+
for rel, text in sorted(self.project_memory.items()):
121+
entries = []
122+
for line in text.splitlines():
123+
stripped = line.strip()
124+
if stripped.startswith("- "):
125+
entries.append(stripped[2:])
126+
if entries:
127+
lines.append(f" {rel}:")
128+
for entry in entries[:8]:
129+
lines.append(f" - {entry}")
105130
return "\n".join(lines)
106131

107132

‎agent/engine/agent_engine.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import asyncio
1919
import json
2020
from pathlib import Path
21-
from typing import Any, Dict, Optional
21+
from typing import TYPE_CHECKING, Any, Dict, Optional
2222

2323
from pydantic import BaseModel, Field
2424

@@ -36,6 +36,9 @@
3636
from agent.validation import BuildValidator, LintValidator, TestValidator
3737
from agent.retrieval import RipgrepSearch, SymbolIndex, TreeSitterIndexer
3838

39+
if TYPE_CHECKING:
40+
from agent.planning import StepOutcome
41+
3942

4043
class AgentAction(BaseModel):
4144
"""The structured action the policy model returns each turn."""

‎agent/graph/__init__.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Phase 13 - lightweight repository graph MVP."""
2+
3+
from agent.graph.builder import GraphBuilder
4+
from agent.graph.impact_analyzer import ImpactAnalyzer
5+
from agent.graph.queries import build_or_load_graph, impact
6+
from agent.graph.schemas import GraphEdge, GraphModule, RepositoryGraph
7+
from agent.graph.store import GraphStore, resolve_graph_dir
8+
9+
__all__ = [
10+
"GraphBuilder",
11+
"GraphEdge",
12+
"GraphModule",
13+
"GraphStore",
14+
"ImpactAnalyzer",
15+
"RepositoryGraph",
16+
"build_or_load_graph",
17+
"impact",
18+
"resolve_graph_dir",
19+
]

0 commit comments

Comments
 (0)