feat(cli): wire project context CLI stubs to ACMS pipeline #534
@@ -2,6 +2,23 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Fixed `context inspect` to display project-scoped tier fragment counts instead of global
|
||||
counts. Added `ContextTierService.get_scoped_metrics()` which returns fragment population
|
||||
counts filtered to the target project while keeping hit/miss counters as global service
|
||||
metrics. (#499)
|
||||
- Fixed `context simulate --focus` to filter fragments by the specified focus URIs during
|
||||
dry-run assembly. Previously, focus URIs were passed to `ContextRequest` but not applied
|
||||
to fragment selection, making `--focus` a no-op. (#499)
|
||||
- Wired project context CLI stubs (`inspect`, `simulate`, `set`, `show`) to live ACMS pipeline
|
||||
services. `context inspect` queries `ContextTierService` for tier metrics and per-project
|
||||
fragments with optional filtering by strategy, focus area, breadth, and depth. `context
|
||||
simulate` performs dry-run context assembly using CRP models with configurable token budget
|
||||
and assembly strategies. `context set` gains 12 ACMS pipeline options (`hot_max_tokens`,
|
||||
`warm_max_decisions`, `cold_max_decisions`, `summary_max_tokens`, `temporal_scope`,
|
||||
`auto_refresh`, `focus_area`, `breadth`, `depth`, `assembly_strategy`, `retrieval_strategy`,
|
||||
`summary_strategy`). `context show` displays ACMS pipeline configuration alongside context
|
||||
policy. Includes 28 Behave BDD scenarios for wiring coverage, updated Robot Framework
|
||||
integration tests, and reference documentation. (#499)
|
||||
- Added hot/warm/cold context tiers with `ContextTier`, `ActorRole`, `TieredFragment`,
|
||||
`TierBudget`, `ActorContextView`, `TierMetrics`, and `ScopedBackendView` models.
|
||||
`ContextTierService` provides store/get, promotion/demotion with cold-tier summarisation
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Project Context CLI Reference
|
||||
|
||||
The `agents project context` commands manage per-project context policies
|
||||
that control what resources and files are visible during each ACMS phase.
|
||||
that control what resources and files are visible during each ACMS phase,
|
||||
and provide tools to inspect and simulate the ACMS pipeline state.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -20,7 +21,7 @@ An empty policy defaults to including everything.
|
||||
|
||||
## `agents project context set`
|
||||
|
||||
Persist a context policy view for a project.
|
||||
Persist a context policy view and ACMS pipeline configuration for a project.
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -36,6 +37,8 @@ agents project context set PROJECT [OPTIONS]
|
||||
|
||||
### Options
|
||||
|
||||
#### View Options
|
||||
|
||||
| Option | Type | Description |
|
||||
|----------------------|-----------|------------------------------------------|
|
||||
| `--view`, `-v` | `str` | Phase view: default, strategize, execute, apply (default: default) |
|
||||
@@ -46,7 +49,29 @@ agents project context set PROJECT [OPTIONS]
|
||||
| `--max-file-size` | `int` | Max file size in bytes (None = no limit) |
|
||||
| `--max-total-size` | `int` | Max total context size in bytes |
|
||||
| `--clear` | `flag` | Clear (reset) the view for the phase |
|
||||
| `--format`, `-f` | `str` | Output format (json, yaml, plain, table, rich) |
|
||||
|
||||
#### ACMS Pipeline Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|-------------------------|---------|----------|------------------------------------------------|
|
||||
| `--hot-max-tokens` | `int` | 8000 | Max tokens for the hot tier context window |
|
||||
| `--warm-max-decisions` | `int` | 500 | Max decisions for the warm tier |
|
||||
| `--cold-max-decisions` | `int` | 5000 | Max decisions for the cold tier |
|
||||
| `--query-limit` | `int` | None | Max number of fragments per query |
|
||||
| `--summarize/--no-summarize` | `bool` | True | Enable or disable context summarization |
|
||||
| `--summary-max-tokens` | `int` | None | Max tokens for context summaries |
|
||||
| `--strategy` | `str` | (none) | Preferred context strategy (repeatable) |
|
||||
| `--default-breadth` | `int` | 2 | Default breadth for context retrieval |
|
||||
| `--default-depth` | `str` | 3 | Default depth (integer or named level) |
|
||||
| `--skeleton-ratio` | `float` | 0.2 | Skeleton compression ratio (0.0-1.0) |
|
||||
| `--temporal-scope` | `str` | current | Temporal scope: current, recent, or all |
|
||||
| `--auto-refresh/--no-auto-refresh` | `bool` | True | Auto-refresh context on resource changes |
|
||||
|
||||
#### Output Options
|
||||
|
||||
| Option | Type | Description |
|
||||
|-----------------|-------|------------------------------------------|
|
||||
| `--format`, `-f`| `str` | Output format (json, yaml, plain, table, rich) |
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -70,13 +95,31 @@ agents project context set local/my-app \
|
||||
|
||||
# Clear execute view (inherit from strategize)
|
||||
agents project context set local/my-app --view execute --clear
|
||||
|
||||
# Configure ACMS pipeline options
|
||||
agents project context set local/my-app \
|
||||
--hot-max-tokens 4000 \
|
||||
--warm-max-decisions 200 \
|
||||
--temporal-scope recent \
|
||||
--no-auto-refresh
|
||||
|
||||
# Set preferred strategies
|
||||
agents project context set local/my-app \
|
||||
--strategy breadth_first \
|
||||
--strategy depth_first
|
||||
|
||||
# Adjust skeleton compression
|
||||
agents project context set local/my-app \
|
||||
--skeleton-ratio 0.5 \
|
||||
--default-breadth 5 \
|
||||
--default-depth 7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `agents project context show`
|
||||
|
||||
Display the context policy for a project.
|
||||
Display the context policy and ACMS pipeline configuration for a project.
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -97,16 +140,24 @@ agents project context show PROJECT [OPTIONS]
|
||||
| `--view`, `-v` | `str` | Show resolved view for a specific phase |
|
||||
| `--format`, `-f`| `str` | Output format (json, yaml, plain, table, rich) |
|
||||
|
||||
### Output
|
||||
|
||||
The `show` command displays:
|
||||
|
||||
- **View configuration** for each phase (or the resolved view for a specific phase)
|
||||
- **ACMS Pipeline Config** including hot/warm/cold tier budgets, summarization settings,
|
||||
strategies, temporal scope, and auto-refresh status
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Show all views
|
||||
# Show all views and ACMS config
|
||||
agents project context show local/my-app
|
||||
|
||||
# Show resolved view for execute phase
|
||||
agents project context show local/my-app --view execute
|
||||
|
||||
# JSON output
|
||||
# JSON output (includes acms_config key)
|
||||
agents project context show local/my-app --format json
|
||||
```
|
||||
|
||||
@@ -114,21 +165,109 @@ agents project context show local/my-app --format json
|
||||
|
||||
## `agents project context inspect`
|
||||
|
||||
> **Stub** — requires ACMS wiring (not yet implemented).
|
||||
Inspect the effective context state for a project by querying the ACMS
|
||||
tier service. Displays current fragment distribution across tiers, tier
|
||||
metrics, budget utilisation, and per-actor visibility.
|
||||
|
||||
Inspect the effective context for a project. This command will show
|
||||
which resources and files would be included/excluded for each phase
|
||||
based on the current policy and resource registry state.
|
||||
### Usage
|
||||
|
||||
Raises `NotImplementedError` with a descriptive message.
|
||||
```
|
||||
agents project context inspect PROJECT [OPTIONS]
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
| Argument | Description |
|
||||
|-----------|-------------------------|
|
||||
| `PROJECT` | Project namespaced name |
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Type | Description |
|
||||
|-------------------|-------|------------------------------------------------|
|
||||
| `--view`, `-v` | `str` | Phase view to inspect |
|
||||
| `--strategy` | `str` | Filter fragments by strategy name |
|
||||
| `--focus` | `str` | UKO URIs to focus on (repeatable) |
|
||||
| `--breadth` | `int` | Override breadth for context retrieval |
|
||||
| `--depth` | `str` | Override depth (integer or named level) |
|
||||
| `--format`, `-f` | `str` | Output format (json, yaml, plain, table, rich) |
|
||||
|
||||
### Output
|
||||
|
||||
The `inspect` command displays:
|
||||
|
||||
- **Tier Metrics** — project-scoped hot/warm/cold fragment counts, service-level hit/miss rates
|
||||
- **Tier Budget** — configured max tokens (hot) and max decisions (warm, cold)
|
||||
- **Project Fragments** — total count and breakdown by tier for the project
|
||||
- **Actor Visibility** — how many fragments each actor role (strategist, executor, reviewer) can see
|
||||
- **Fragment Details** — first 20 fragments with IDs, tiers, token counts, and access metadata
|
||||
- **ACMS Config** — the project's pipeline configuration
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Inspect all tiers for a project
|
||||
agents project context inspect local/my-app
|
||||
|
||||
# Inspect with JSON output
|
||||
agents project context inspect local/my-app --format json
|
||||
|
||||
# Inspect a specific phase view
|
||||
agents project context inspect local/my-app --view strategize
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `agents project context simulate`
|
||||
|
||||
> **Stub** — requires ACMS wiring (not yet implemented).
|
||||
Run a dry-run context window assembly for a project. Creates a simulated
|
||||
`AssembledContext` using the project's ACMS configuration and available
|
||||
tier fragments.
|
||||
|
||||
Simulate the context window for a project. This command will show
|
||||
the estimated token counts and context composition for each phase.
|
||||
### Usage
|
||||
|
||||
Raises `NotImplementedError` with a descriptive message.
|
||||
```
|
||||
agents project context simulate PROJECT [OPTIONS]
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
| Argument | Description |
|
||||
|-----------|-------------------------|
|
||||
| `PROJECT` | Project namespaced name |
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Type | Description |
|
||||
|-------------------|-------|------------------------------------------------|
|
||||
| `--view`, `-v` | `str` | Phase view for simulation |
|
||||
| `--budget` | `int` | Token budget override |
|
||||
| `--focus` | `str` | UKO URIs to focus on (repeatable) |
|
||||
| `--strategy` | `str` | Preferred strategies (repeatable) |
|
||||
| `--format`, `-f` | `str` | Output format (json, yaml, plain, table, rich) |
|
||||
|
||||
### Output
|
||||
|
||||
The `simulate` command displays:
|
||||
|
||||
- **Summary** — total tokens, budget usage percentage, strategies used, fragment count, context hash
|
||||
- **Fragment Table** — each assembled fragment with UKO node, token count, relevance score, depth, and strategy. When `--focus` is provided, only fragments matching the focus URIs are included.
|
||||
- **ACMS Config** — the project's pipeline configuration used for the simulation
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Simulate with default settings
|
||||
agents project context simulate local/my-app
|
||||
|
||||
# Simulate with custom budget
|
||||
agents project context simulate local/my-app --budget 4000
|
||||
|
||||
# Simulate for a specific phase with strategy hints
|
||||
agents project context simulate local/my-app \
|
||||
--view strategize \
|
||||
--strategy breadth_first
|
||||
|
||||
# JSON output for programmatic consumption
|
||||
agents project context simulate local/my-app --format json
|
||||
```
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
Feature: Context CLI ACMS pipeline wiring (B2.cli)
|
||||
As a CleverAgents user
|
||||
I want context inspect and simulate commands wired to the ACMS pipeline
|
||||
So that I can examine tier state and dry-run context assembly
|
||||
|
||||
Background:
|
||||
Given a context wiring in-memory database is initialized
|
||||
And a project "local/wire-app" exists for context wiring
|
||||
|
||||
# ── context set with ACMS options ──────────────────────────
|
||||
|
||||
Scenario: Set ACMS hot_max_tokens option
|
||||
When I run wired context set on "local/wire-app" with hot-max-tokens 4000
|
||||
Then the wired context set command should succeed
|
||||
And the stored ACMS config hot_max_tokens should be 4000
|
||||
|
||||
Scenario: Set ACMS warm_max_decisions option
|
||||
When I run wired context set on "local/wire-app" with warm-max-decisions 200
|
||||
Then the wired context set command should succeed
|
||||
And the stored ACMS config warm_max_decisions should be 200
|
||||
|
||||
Scenario: Set ACMS cold_max_decisions option
|
||||
When I run wired context set on "local/wire-app" with cold-max-decisions 3000
|
||||
Then the wired context set command should succeed
|
||||
And the stored ACMS config cold_max_decisions should be 3000
|
||||
|
||||
Scenario: Set ACMS temporal_scope option
|
||||
When I run wired context set on "local/wire-app" with temporal-scope "recent"
|
||||
Then the wired context set command should succeed
|
||||
And the stored ACMS config temporal_scope should be "recent"
|
||||
|
||||
Scenario: Set invalid temporal_scope fails
|
||||
When I run wired context set on "local/wire-app" with temporal-scope "invalid"
|
||||
Then the wired context set command should fail
|
||||
|
||||
Scenario: Set ACMS auto-refresh off
|
||||
When I run wired context set on "local/wire-app" with no-auto-refresh
|
||||
Then the wired context set command should succeed
|
||||
And the stored ACMS config auto_refresh should be false
|
||||
|
||||
Scenario: Set ACMS summarize off
|
||||
When I run wired context set on "local/wire-app" with no-summarize
|
||||
Then the wired context set command should succeed
|
||||
And the stored ACMS config summarize should be false
|
||||
|
||||
Scenario: Set ACMS strategy option
|
||||
When I run wired context set on "local/wire-app" with strategy "breadth_first"
|
||||
Then the wired context set command should succeed
|
||||
And the stored ACMS config strategies should contain "breadth_first"
|
||||
|
||||
Scenario: Set ACMS skeleton_ratio option
|
||||
When I run wired context set on "local/wire-app" with skeleton-ratio 0.5
|
||||
Then the wired context set command should succeed
|
||||
And the stored ACMS config skeleton_ratio should be 0.5
|
||||
|
||||
Scenario: Set ACMS default_breadth option
|
||||
When I run wired context set on "local/wire-app" with default-breadth 5
|
||||
Then the wired context set command should succeed
|
||||
And the stored ACMS config default_breadth should be 5
|
||||
|
||||
Scenario: Set ACMS default_depth option
|
||||
When I run wired context set on "local/wire-app" with default-depth 7
|
||||
Then the wired context set command should succeed
|
||||
And the stored ACMS config default_depth should be 7
|
||||
|
||||
Scenario: Set ACMS query_limit option
|
||||
When I run wired context set on "local/wire-app" with query-limit 50
|
||||
Then the wired context set command should succeed
|
||||
And the stored ACMS config query_limit should be 50
|
||||
|
||||
Scenario: Set ACMS summary_max_tokens option
|
||||
When I run wired context set on "local/wire-app" with summary-max-tokens 2000
|
||||
Then the wired context set command should succeed
|
||||
And the stored ACMS config summary_max_tokens should be 2000
|
||||
|
||||
# ── context show with ACMS config ──────────────────────────
|
||||
|
||||
Scenario: Show displays ACMS pipeline config
|
||||
Given I have set ACMS config on "local/wire-app" with hot-max-tokens 6000
|
||||
When I run wired context show on "local/wire-app" with format "json"
|
||||
Then the wired context show command should succeed
|
||||
And the wired output should be valid JSON
|
||||
And the wired JSON output should contain key "acms_config"
|
||||
|
||||
Scenario: Show with view displays ACMS config
|
||||
Given I have set ACMS config on "local/wire-app" with hot-max-tokens 6000
|
||||
When I run wired context show on "local/wire-app" with view "default" and format "json"
|
||||
Then the wired context show command should succeed
|
||||
And the wired output should be valid JSON
|
||||
And the wired JSON output should contain key "acms_config"
|
||||
|
||||
# ── context inspect wired to tier service ──────────────────
|
||||
|
||||
Scenario: Inspect returns tier metrics with no fragments
|
||||
When I run wired inspect for project "local/wire-app" using defaults
|
||||
Then the wired context inspect command should succeed
|
||||
|
||||
Scenario: Inspect with JSON format returns valid JSON
|
||||
When I run wired inspect for project "local/wire-app" using format "json"
|
||||
Then the wired context inspect command should succeed
|
||||
And the wired output should be valid JSON
|
||||
And the wired JSON output should contain key "tier_metrics"
|
||||
And the wired JSON output should contain key "project_fragments"
|
||||
And the wired JSON output should contain key "actor_visibility"
|
||||
|
||||
Scenario: Inspect with pre-populated fragments shows counts
|
||||
Given the tier service has a hot fragment "frag-1" for project "local/wire-app"
|
||||
And the tier service has a warm fragment "frag-2" for project "local/wire-app"
|
||||
When I run wired inspect for project "local/wire-app" using format "json"
|
||||
Then the wired context inspect command should succeed
|
||||
And the wired JSON project_fragments total should be 2
|
||||
|
||||
Scenario: Inspect shows project-scoped tier metrics not global counts
|
||||
Given the tier service has a hot fragment "scoped-a" for project "local/wire-app"
|
||||
And the tier service has a hot fragment "other-b" for project "local/other-proj"
|
||||
When I run wired inspect for project "local/wire-app" using format "json"
|
||||
Then the wired context inspect command should succeed
|
||||
And the wired JSON tier_metrics hot_count should be 1
|
||||
|
||||
Scenario: Inspect on nonexistent project fails
|
||||
When I run wired inspect for project "local/no-such" using defaults
|
||||
Then the wired context inspect command should fail
|
||||
|
||||
Scenario: Inspect with invalid view fails
|
||||
When I run wired inspect for project "local/wire-app" using view "bogus"
|
||||
Then the wired context inspect command should fail
|
||||
|
||||
# ── context simulate wired to tier service ─────────────────
|
||||
|
||||
Scenario: Simulate returns assembly result with no fragments
|
||||
When I run wired simulate for project "local/wire-app" using defaults
|
||||
Then the wired context simulate command should succeed
|
||||
|
||||
Scenario: Simulate with JSON format returns valid JSON
|
||||
When I run wired simulate for project "local/wire-app" using format "json"
|
||||
Then the wired context simulate command should succeed
|
||||
And the wired output should be valid JSON
|
||||
And the wired JSON output should contain key "total_tokens"
|
||||
And the wired JSON output should contain key "budget_used"
|
||||
And the wired JSON output should contain key "strategies_used"
|
||||
And the wired JSON output should contain key "context_hash"
|
||||
|
||||
Scenario: Simulate with budget override
|
||||
When I run wired simulate for project "local/wire-app" using budget 2000
|
||||
Then the wired context simulate command should succeed
|
||||
|
||||
Scenario: Simulate with pre-populated fragments assembles them
|
||||
Given the tier service has a hot fragment "sim-1" for project "local/wire-app" sized at 100 tokens
|
||||
When I run wired simulate for project "local/wire-app" using format "json"
|
||||
Then the wired context simulate command should succeed
|
||||
And the wired JSON total_tokens should be greater than 0
|
||||
|
||||
Scenario: Simulate on nonexistent project fails
|
||||
When I run wired simulate for project "local/no-such" using defaults
|
||||
Then the wired context simulate command should fail
|
||||
|
||||
Scenario: Simulate with invalid view fails
|
||||
When I run wired simulate for project "local/wire-app" using view "bogus"
|
||||
Then the wired context simulate command should fail
|
||||
|
||||
Scenario: Simulate with strategy hint
|
||||
When I run wired simulate for project "local/wire-app" using strategy "breadth_first"
|
||||
Then the wired context simulate command should succeed
|
||||
|
||||
Scenario: Simulate with focus filters fragments by resource URI
|
||||
Given a focused hot fragment "foc-1" with resource "uko:file/main.py" and 100 tokens exists for project "local/wire-app"
|
||||
And a focused hot fragment "foc-2" with resource "uko:file/test.py" and 80 tokens exists for project "local/wire-app"
|
||||
When I run wired simulate for project "local/wire-app" focusing on "uko:file/main.py" in format "json"
|
||||
Then the wired context simulate command should succeed
|
||||
And the wired JSON fragment_count should be 1
|
||||
And the wired JSON total_tokens should be 100
|
||||
|
||||
Scenario: Simulate with view option
|
||||
When I run wired simulate for project "local/wire-app" using view "strategize"
|
||||
Then the wired context simulate command should succeed
|
||||
@@ -115,13 +115,13 @@ Feature: M5 ACMS pipeline and large-project context smoke tests
|
||||
Then the m5 smoke project context show should succeed
|
||||
And the m5 smoke project context output should contain phase views
|
||||
|
||||
Scenario: M5 smoke project context inspect is not yet wired
|
||||
Scenario: M5 smoke project context inspect is wired to ACMS
|
||||
When I m5 smoke invoke project context inspect
|
||||
Then a m5 smoke NotImplementedError should be raised mentioning "ACMS"
|
||||
Then the m5 smoke project context inspect should succeed
|
||||
|
||||
Scenario: M5 smoke project context simulate is not yet wired
|
||||
Scenario: M5 smoke project context simulate is wired to ACMS
|
||||
When I m5 smoke invoke project context simulate
|
||||
Then a m5 smoke NotImplementedError should be raised mentioning "ACMS"
|
||||
Then the m5 smoke project context simulate should succeed
|
||||
|
||||
# --- Context analysis agent ---
|
||||
|
||||
|
||||
@@ -76,17 +76,17 @@ Feature: Project context CLI commands (B2.cli)
|
||||
When I run context show on "local/nonexistent-proj" without options
|
||||
Then the context show command should fail
|
||||
|
||||
# ── inspect raises NotImplementedError ──────────────────────
|
||||
# ── inspect now wired to ACMS tier service ─────────────────
|
||||
|
||||
Scenario: Inspect raises NotImplementedError with clear message
|
||||
Scenario: Inspect succeeds and shows tier state
|
||||
When I run context inspect on "local/ctx-app"
|
||||
Then a NotImplementedError should be raised with message containing "ACMS"
|
||||
Then the context inspect command should succeed
|
||||
|
||||
# ── simulate raises NotImplementedError ─────────────────────
|
||||
# ── simulate now wired to ACMS tier service ────────────────
|
||||
|
||||
Scenario: Simulate raises NotImplementedError with clear message
|
||||
Scenario: Simulate succeeds and shows assembly result
|
||||
When I run context simulate on "local/ctx-app"
|
||||
Then a NotImplementedError should be raised with message containing "ACMS"
|
||||
Then the context simulate command should succeed
|
||||
|
||||
# ── view inheritance ─────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -0,0 +1,646 @@
|
||||
"""Step definitions for context_cli_wiring.feature.
|
||||
|
||||
Tests the ``agents project context set/show/inspect/simulate``
|
||||
CLI commands wired to the ACMS pipeline using an in-memory
|
||||
SQLite database and a real ContextTierService instance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from io import StringIO
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from behave import given, then, when # type: ignore[import-untyped]
|
||||
from sqlalchemy import create_engine # type: ignore[import-untyped]
|
||||
from sqlalchemy.orm import Session, sessionmaker # type: ignore[import-untyped]
|
||||
|
||||
from cleveragents.application.services.context_tiers import ContextTierService
|
||||
from cleveragents.domain.models.acms.tiers import (
|
||||
ContextTier,
|
||||
TieredFragment,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Session wrapper (no-op close)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
class _UnclosableSession:
|
||||
"""Wraps a SQLAlchemy Session; ``close()`` is a no-op."""
|
||||
|
||||
def __init__(self, real_session: Session) -> None:
|
||||
object.__setattr__(self, "_real", real_session)
|
||||
|
||||
def close(self) -> None:
|
||||
"""No-op."""
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(object.__getattribute__(self, "_real"), name)
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
setattr(object.__getattribute__(self, "_real"), name, value)
|
||||
|
||||
|
||||
def _make_wiring_session_factory(
|
||||
context: Any,
|
||||
) -> tuple[Any, Any]:
|
||||
"""In-memory SQLite with all tables."""
|
||||
from cleveragents.infrastructure.database.models import Base
|
||||
|
||||
engine = create_engine(
|
||||
"sqlite:///:memory:",
|
||||
echo=False,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
Base.metadata.create_all(engine)
|
||||
real_session = sessionmaker(
|
||||
bind=engine,
|
||||
expire_on_commit=False,
|
||||
autoflush=True,
|
||||
autocommit=False,
|
||||
)()
|
||||
wrapper = _UnclosableSession(real_session)
|
||||
|
||||
def _factory() -> Any:
|
||||
return wrapper
|
||||
|
||||
return engine, _factory
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Background
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("a context wiring in-memory database is initialized")
|
||||
def step_init_wiring_db(context: Any) -> None:
|
||||
from cleveragents.infrastructure.database.repositories import (
|
||||
NamespacedProjectRepository,
|
||||
)
|
||||
|
||||
engine, session_factory = _make_wiring_session_factory(context)
|
||||
context.w_engine = engine
|
||||
context.w_session_factory = session_factory
|
||||
context.w_project_repo = NamespacedProjectRepository(
|
||||
session_factory=session_factory
|
||||
)
|
||||
context.w_tier_service = ContextTierService()
|
||||
context.w_output = ""
|
||||
context.w_exit_code = 0
|
||||
context.w_error = None
|
||||
|
||||
|
||||
@given('a project "{name}" exists for context wiring')
|
||||
def step_create_project_for_wiring(context: Any, name: str) -> None:
|
||||
from cleveragents.domain.models.core.project import (
|
||||
NamespacedProject,
|
||||
parse_namespaced_name,
|
||||
)
|
||||
|
||||
parsed = parse_namespaced_name(name)
|
||||
proj = NamespacedProject(
|
||||
name=parsed.name,
|
||||
namespace=parsed.namespace,
|
||||
)
|
||||
context.w_project_repo.create(proj)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def _mock_wiring_container(context: Any) -> MagicMock:
|
||||
"""Build a mock container wired to the test DB and tier service."""
|
||||
mock_container = MagicMock()
|
||||
mock_container.namespaced_project_repo.return_value = context.w_project_repo
|
||||
mock_container.session_factory.return_value = context.w_session_factory
|
||||
mock_container.context_tier_service.return_value = context.w_tier_service
|
||||
return mock_container
|
||||
|
||||
|
||||
def _run_wired(context: Any, func: Any, *args: Any, **kwargs: Any) -> None:
|
||||
"""Execute a CLI function with a mocked DI container."""
|
||||
import typer
|
||||
from rich.console import Console as RichConsole
|
||||
|
||||
mock_cont = _mock_wiring_container(context)
|
||||
buf = StringIO()
|
||||
test_console = RichConsole(
|
||||
file=buf,
|
||||
no_color=True,
|
||||
highlight=False,
|
||||
force_terminal=False,
|
||||
width=500,
|
||||
soft_wrap=True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"cleveragents.application.container.get_container",
|
||||
return_value=mock_cont,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.project_context.console",
|
||||
test_console,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.project_context.err_console",
|
||||
test_console,
|
||||
),
|
||||
):
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
context.w_exit_code = 0
|
||||
except typer.Exit as exc:
|
||||
context.w_exit_code = exc.exit_code
|
||||
except typer.Abort:
|
||||
context.w_exit_code = 1
|
||||
|
||||
context.w_output = buf.getvalue()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# context set with ACMS options
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I run wired context set on "{project}" with hot-max-tokens {val:d}')
|
||||
def step_wired_set_hot_max_tokens(context: Any, project: str, val: int) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, hot_max_tokens=val)
|
||||
|
||||
|
||||
@when('I run wired context set on "{project}" with warm-max-decisions {val:d}')
|
||||
def step_wired_set_warm_max_decisions(context: Any, project: str, val: int) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, warm_max_decisions=val)
|
||||
|
||||
|
||||
@when('I run wired context set on "{project}" with cold-max-decisions {val:d}')
|
||||
def step_wired_set_cold_max_decisions(context: Any, project: str, val: int) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, cold_max_decisions=val)
|
||||
|
||||
|
||||
@when('I run wired context set on "{project}" with temporal-scope "{scope}"')
|
||||
def step_wired_set_temporal_scope(context: Any, project: str, scope: str) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, temporal_scope=scope)
|
||||
|
||||
|
||||
@when('I run wired context set on "{project}" with no-auto-refresh')
|
||||
def step_wired_set_no_auto_refresh(context: Any, project: str) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, auto_refresh=False)
|
||||
|
||||
|
||||
@when('I run wired context set on "{project}" with no-summarize')
|
||||
def step_wired_set_no_summarize(context: Any, project: str) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, summarize=False)
|
||||
|
||||
|
||||
@when('I run wired context set on "{project}" with strategy "{strategy}"')
|
||||
def step_wired_set_strategy(context: Any, project: str, strategy: str) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, strategy=[strategy])
|
||||
|
||||
|
||||
@when('I run wired context set on "{project}" with skeleton-ratio {val:g}')
|
||||
def step_wired_set_skeleton_ratio(context: Any, project: str, val: float) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, skeleton_ratio=val)
|
||||
|
||||
|
||||
@when('I run wired context set on "{project}" with default-breadth {val:d}')
|
||||
def step_wired_set_default_breadth(context: Any, project: str, val: int) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, default_breadth=val)
|
||||
|
||||
|
||||
@when('I run wired context set on "{project}" with default-depth {val:d}')
|
||||
def step_wired_set_default_depth(context: Any, project: str, val: int) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, default_depth=str(val))
|
||||
|
||||
|
||||
@when('I run wired context set on "{project}" with query-limit {val:d}')
|
||||
def step_wired_set_query_limit(context: Any, project: str, val: int) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, query_limit=val)
|
||||
|
||||
|
||||
@when('I run wired context set on "{project}" with summary-max-tokens {val:d}')
|
||||
def step_wired_set_summary_max_tokens(context: Any, project: str, val: int) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, summary_max_tokens=val)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# context show with ACMS config
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@given('I have set ACMS config on "{project}" with hot-max-tokens {val:d}')
|
||||
def step_given_acms_config(context: Any, project: str, val: int) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_set
|
||||
|
||||
_run_wired(context, context_set, project=project, hot_max_tokens=val)
|
||||
|
||||
|
||||
@when('I run wired context show on "{project}" with format "{fmt}"')
|
||||
def step_wired_show_format(context: Any, project: str, fmt: str) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_show
|
||||
|
||||
_run_wired(context, context_show, project=project, output_format=fmt)
|
||||
|
||||
|
||||
@when('I run wired context show on "{project}" with view "{view}" and format "{fmt}"')
|
||||
def step_wired_show_view_format(
|
||||
context: Any, project: str, view: str, fmt: str
|
||||
) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_show
|
||||
|
||||
_run_wired(context, context_show, project=project, view=view, output_format=fmt)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# context inspect
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I run wired inspect for project "{project}" using defaults')
|
||||
def step_wired_inspect(context: Any, project: str) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_inspect
|
||||
|
||||
_run_wired(context, context_inspect, project=project)
|
||||
|
||||
|
||||
@when('I run wired inspect for project "{project}" using format "{fmt}"')
|
||||
def step_wired_inspect_format(context: Any, project: str, fmt: str) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_inspect
|
||||
|
||||
_run_wired(context, context_inspect, project=project, output_format=fmt)
|
||||
|
||||
|
||||
@when('I run wired inspect for project "{project}" using view "{view}"')
|
||||
def step_wired_inspect_view(context: Any, project: str, view: str) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_inspect
|
||||
|
||||
_run_wired(context, context_inspect, project=project, view=view)
|
||||
|
||||
|
||||
@given('the tier service has a hot fragment "{fid}" for project "{project}"')
|
||||
def step_given_hot_fragment(context: Any, fid: str, project: str) -> None:
|
||||
frag = TieredFragment(
|
||||
fragment_id=fid,
|
||||
content="test content for " + fid,
|
||||
tier=ContextTier.HOT,
|
||||
project_name=project,
|
||||
token_count=50,
|
||||
)
|
||||
context.w_tier_service.store(frag)
|
||||
|
||||
|
||||
@given('the tier service has a warm fragment "{fid}" for project "{project}"')
|
||||
def step_given_warm_fragment(context: Any, fid: str, project: str) -> None:
|
||||
frag = TieredFragment(
|
||||
fragment_id=fid,
|
||||
content="test content for " + fid,
|
||||
tier=ContextTier.WARM,
|
||||
project_name=project,
|
||||
token_count=30,
|
||||
)
|
||||
context.w_tier_service.store(frag)
|
||||
|
||||
|
||||
@given(
|
||||
'the tier service has a hot fragment "{fid}" for project'
|
||||
' "{project}" sized at {tokens:d} tokens'
|
||||
)
|
||||
def step_given_hot_fragment_tokens(
|
||||
context: Any, fid: str, project: str, tokens: int
|
||||
) -> None:
|
||||
frag = TieredFragment(
|
||||
fragment_id=fid,
|
||||
content="test content for simulation " + fid,
|
||||
tier=ContextTier.HOT,
|
||||
project_name=project,
|
||||
token_count=tokens,
|
||||
)
|
||||
context.w_tier_service.store(frag)
|
||||
|
||||
|
||||
@given(
|
||||
'a focused hot fragment "{fid}" with resource "{resource}"'
|
||||
" and {tokens:d} tokens exists"
|
||||
' for project "{project}"'
|
||||
)
|
||||
def step_given_focused_hot_fragment(
|
||||
context: Any, fid: str, resource: str, tokens: int, project: str
|
||||
) -> None:
|
||||
frag = TieredFragment(
|
||||
fragment_id=fid,
|
||||
content="test content for " + fid,
|
||||
tier=ContextTier.HOT,
|
||||
project_name=project,
|
||||
resource_id=resource,
|
||||
token_count=tokens,
|
||||
)
|
||||
context.w_tier_service.store(frag)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# context simulate
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I run wired simulate for project "{project}" using defaults')
|
||||
def step_wired_simulate(context: Any, project: str) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_simulate
|
||||
|
||||
_run_wired(context, context_simulate, project=project)
|
||||
|
||||
|
||||
@when('I run wired simulate for project "{project}" using format "{fmt}"')
|
||||
def step_wired_simulate_format(context: Any, project: str, fmt: str) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_simulate
|
||||
|
||||
_run_wired(context, context_simulate, project=project, output_format=fmt)
|
||||
|
||||
|
||||
@when('I run wired simulate for project "{project}" using budget {val:d}')
|
||||
def step_wired_simulate_budget(context: Any, project: str, val: int) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_simulate
|
||||
|
||||
_run_wired(context, context_simulate, project=project, budget=val)
|
||||
|
||||
|
||||
@when('I run wired simulate for project "{project}" using view "{view}"')
|
||||
def step_wired_simulate_view(context: Any, project: str, view: str) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_simulate
|
||||
|
||||
_run_wired(context, context_simulate, project=project, view=view)
|
||||
|
||||
|
||||
@when('I run wired simulate for project "{project}" using strategy "{strat}"')
|
||||
def step_wired_simulate_strategy(context: Any, project: str, strat: str) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_simulate
|
||||
|
||||
_run_wired(context, context_simulate, project=project, strategy_hint=[strat])
|
||||
|
||||
|
||||
@when(
|
||||
'I run wired simulate for project "{project}"'
|
||||
' focusing on "{focus}" in format "{fmt}"'
|
||||
)
|
||||
def step_wired_simulate_focus_format(
|
||||
context: Any, project: str, focus: str, fmt: str
|
||||
) -> None:
|
||||
from cleveragents.cli.commands.project_context import context_simulate
|
||||
|
||||
_run_wired(
|
||||
context,
|
||||
context_simulate,
|
||||
project=project,
|
||||
focus=[focus],
|
||||
output_format=fmt,
|
||||
)
|
||||
|
||||
|
||||
@then("the wired context set command should succeed")
|
||||
def step_wired_set_ok(context: Any) -> None:
|
||||
assert context.w_exit_code == 0, (
|
||||
f"Expected exit 0, got {context.w_exit_code}. Output: {context.w_output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the wired context set command should fail")
|
||||
def step_wired_set_fail(context: Any) -> None:
|
||||
assert context.w_exit_code != 0, (
|
||||
f"Expected non-zero exit, got {context.w_exit_code}"
|
||||
)
|
||||
|
||||
|
||||
@then("the wired context show command should succeed")
|
||||
def step_wired_show_ok(context: Any) -> None:
|
||||
assert context.w_exit_code == 0, (
|
||||
f"Expected exit 0, got {context.w_exit_code}. Output: {context.w_output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the wired context inspect command should succeed")
|
||||
def step_wired_inspect_ok(context: Any) -> None:
|
||||
assert context.w_exit_code == 0, (
|
||||
f"Expected exit 0, got {context.w_exit_code}. Output: {context.w_output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the wired context inspect command should fail")
|
||||
def step_wired_inspect_fail(context: Any) -> None:
|
||||
assert context.w_exit_code != 0, (
|
||||
f"Expected non-zero exit, got {context.w_exit_code}"
|
||||
)
|
||||
|
||||
|
||||
@then("the wired context simulate command should succeed")
|
||||
def step_wired_simulate_ok(context: Any) -> None:
|
||||
assert context.w_exit_code == 0, (
|
||||
f"Expected exit 0, got {context.w_exit_code}. Output: {context.w_output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the wired context simulate command should fail")
|
||||
def step_wired_simulate_fail(context: Any) -> None:
|
||||
assert context.w_exit_code != 0, (
|
||||
f"Expected non-zero exit, got {context.w_exit_code}"
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Then assertions — ACMS config
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def _read_stored_acms_config(context: Any) -> dict:
|
||||
"""Read the ACMS config from the test DB."""
|
||||
from cleveragents.cli.commands.project_context import _read_acms_config
|
||||
|
||||
return _read_acms_config(context.w_session_factory, "local/wire-app")
|
||||
|
||||
|
||||
@then("the stored ACMS config hot_max_tokens should be {val:d}")
|
||||
def step_acms_hot_max_tokens(context: Any, val: int) -> None:
|
||||
acms = _read_stored_acms_config(context)
|
||||
assert acms["hot_max_tokens"] == val, (
|
||||
f"Expected {val}, got {acms['hot_max_tokens']}"
|
||||
)
|
||||
|
||||
|
||||
@then("the stored ACMS config warm_max_decisions should be {val:d}")
|
||||
def step_acms_warm_max_decisions(context: Any, val: int) -> None:
|
||||
acms = _read_stored_acms_config(context)
|
||||
assert acms["warm_max_decisions"] == val, (
|
||||
f"Expected {val}, got {acms['warm_max_decisions']}"
|
||||
)
|
||||
|
||||
|
||||
@then("the stored ACMS config cold_max_decisions should be {val:d}")
|
||||
def step_acms_cold_max_decisions(context: Any, val: int) -> None:
|
||||
acms = _read_stored_acms_config(context)
|
||||
assert acms["cold_max_decisions"] == val, (
|
||||
f"Expected {val}, got {acms['cold_max_decisions']}"
|
||||
)
|
||||
|
||||
|
||||
@then('the stored ACMS config temporal_scope should be "{val}"')
|
||||
def step_acms_temporal_scope(context: Any, val: str) -> None:
|
||||
acms = _read_stored_acms_config(context)
|
||||
assert acms["temporal_scope"] == val, (
|
||||
f"Expected {val}, got {acms['temporal_scope']}"
|
||||
)
|
||||
|
||||
|
||||
@then("the stored ACMS config auto_refresh should be false")
|
||||
def step_acms_auto_refresh_false(context: Any) -> None:
|
||||
acms = _read_stored_acms_config(context)
|
||||
assert acms["auto_refresh"] is False, f"Expected False, got {acms['auto_refresh']}"
|
||||
|
||||
|
||||
@then("the stored ACMS config summarize should be false")
|
||||
def step_acms_summarize_false(context: Any) -> None:
|
||||
acms = _read_stored_acms_config(context)
|
||||
assert acms["summarize"] is False, f"Expected False, got {acms['summarize']}"
|
||||
|
||||
|
||||
@then('the stored ACMS config strategies should contain "{strat}"')
|
||||
def step_acms_strategies_contain(context: Any, strat: str) -> None:
|
||||
acms = _read_stored_acms_config(context)
|
||||
assert strat in acms.get("strategies", []), (
|
||||
f"Expected {strat} in {acms.get('strategies', [])}"
|
||||
)
|
||||
|
||||
|
||||
@then("the stored ACMS config skeleton_ratio should be {val:g}")
|
||||
def step_acms_skeleton_ratio(context: Any, val: float) -> None:
|
||||
acms = _read_stored_acms_config(context)
|
||||
assert abs(acms["skeleton_ratio"] - val) < 1e-9, (
|
||||
f"Expected {val}, got {acms['skeleton_ratio']}"
|
||||
)
|
||||
|
||||
|
||||
@then("the stored ACMS config default_breadth should be {val:d}")
|
||||
def step_acms_default_breadth(context: Any, val: int) -> None:
|
||||
acms = _read_stored_acms_config(context)
|
||||
assert acms["default_breadth"] == val, (
|
||||
f"Expected {val}, got {acms['default_breadth']}"
|
||||
)
|
||||
|
||||
|
||||
@then("the stored ACMS config default_depth should be {val:d}")
|
||||
def step_acms_default_depth(context: Any, val: int) -> None:
|
||||
acms = _read_stored_acms_config(context)
|
||||
assert acms["default_depth"] == val, f"Expected {val}, got {acms['default_depth']}"
|
||||
|
||||
|
||||
@then("the stored ACMS config query_limit should be {val:d}")
|
||||
def step_acms_query_limit(context: Any, val: int) -> None:
|
||||
acms = _read_stored_acms_config(context)
|
||||
assert acms["query_limit"] == val, f"Expected {val}, got {acms['query_limit']}"
|
||||
|
||||
|
||||
@then("the stored ACMS config summary_max_tokens should be {val:d}")
|
||||
def step_acms_summary_max_tokens(context: Any, val: int) -> None:
|
||||
acms = _read_stored_acms_config(context)
|
||||
assert acms["summary_max_tokens"] == val, (
|
||||
f"Expected {val}, got {acms['summary_max_tokens']}"
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Then assertions — JSON output
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the wired output should be valid JSON")
|
||||
def step_wired_output_valid_json(context: Any) -> None:
|
||||
try:
|
||||
context.w_parsed_json = json.loads(context.w_output.strip())
|
||||
except json.JSONDecodeError as exc:
|
||||
raise AssertionError(
|
||||
f"Output is not valid JSON: {exc}\nOutput: {context.w_output!r}"
|
||||
) from exc
|
||||
|
||||
|
||||
@then('the wired JSON output should contain key "{key}"')
|
||||
def step_wired_json_has_key(context: Any, key: str) -> None:
|
||||
data = getattr(context, "w_parsed_json", None)
|
||||
if data is None:
|
||||
data = json.loads(context.w_output.strip())
|
||||
context.w_parsed_json = data
|
||||
assert key in data, f"Key '{key}' not found in JSON output: {list(data.keys())}"
|
||||
|
||||
|
||||
@then("the wired JSON project_fragments total should be {val:d}")
|
||||
def step_wired_json_fragments_total(context: Any, val: int) -> None:
|
||||
data = getattr(context, "w_parsed_json", None)
|
||||
if data is None:
|
||||
data = json.loads(context.w_output.strip())
|
||||
context.w_parsed_json = data
|
||||
total = data.get("project_fragments", {}).get("total", 0)
|
||||
assert total == val, f"Expected {val} fragments, got {total}"
|
||||
|
||||
|
||||
@then("the wired JSON total_tokens should be greater than 0")
|
||||
def step_wired_json_total_tokens_positive(context: Any) -> None:
|
||||
data = getattr(context, "w_parsed_json", None)
|
||||
if data is None:
|
||||
data = json.loads(context.w_output.strip())
|
||||
context.w_parsed_json = data
|
||||
total = data.get("total_tokens", 0)
|
||||
assert total > 0, f"Expected total_tokens > 0, got {total}"
|
||||
|
||||
|
||||
@then("the wired JSON tier_metrics hot_count should be {val:d}")
|
||||
def step_wired_json_tier_metrics_hot_count(context: Any, val: int) -> None:
|
||||
data = getattr(context, "w_parsed_json", None)
|
||||
if data is None:
|
||||
data = json.loads(context.w_output.strip())
|
||||
context.w_parsed_json = data
|
||||
hot_count = data.get("tier_metrics", {}).get("hot_count", -1)
|
||||
assert hot_count == val, f"Expected tier_metrics.hot_count={val}, got {hot_count}"
|
||||
|
||||
|
||||
@then("the wired JSON fragment_count should be {val:d}")
|
||||
def step_wired_json_fragment_count(context: Any, val: int) -> None:
|
||||
data = getattr(context, "w_parsed_json", None)
|
||||
if data is None:
|
||||
data = json.loads(context.w_output.strip())
|
||||
context.w_parsed_json = data
|
||||
count = data.get("fragment_count", -1)
|
||||
assert count == val, f"Expected fragment_count={val}, got {count}"
|
||||
|
||||
|
||||
@then("the wired JSON total_tokens should be {val:d}")
|
||||
def step_wired_json_total_tokens_exact(context: Any, val: int) -> None:
|
||||
data = getattr(context, "w_parsed_json", None)
|
||||
if data is None:
|
||||
data = json.loads(context.w_output.strip())
|
||||
context.w_parsed_json = data
|
||||
total = data.get("total_tokens", -1)
|
||||
assert total == val, f"Expected total_tokens={val}, got {total}"
|
||||
@@ -16,10 +16,6 @@ from pydantic import ValidationError
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.commands.context import app as context_app
|
||||
from cleveragents.cli.commands.project_context import (
|
||||
context_inspect,
|
||||
context_simulate,
|
||||
)
|
||||
from cleveragents.domain.models.core.context import Context as ContextModel
|
||||
from cleveragents.domain.models.core.context_policy import (
|
||||
ContextView,
|
||||
@@ -407,6 +403,8 @@ def step_m5_project_with_policy(context: Context) -> None:
|
||||
|
||||
@when("I m5 smoke invoke project context show")
|
||||
def step_m5_invoke_project_context_show(context: Context) -> None:
|
||||
from cleveragents.cli.commands.project_context import _default_acms_config
|
||||
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.get.return_value = MagicMock() # project exists
|
||||
|
||||
@@ -426,6 +424,10 @@ def step_m5_invoke_project_context_show(context: Context) -> None:
|
||||
"cleveragents.cli.commands.project_context._read_policy",
|
||||
return_value=context.m5_saved_policy,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.project_context._read_acms_config",
|
||||
return_value=_default_acms_config(),
|
||||
),
|
||||
):
|
||||
from cleveragents.cli.commands.project_context import app as pc_app
|
||||
|
||||
@@ -448,28 +450,79 @@ def step_m5_project_context_has_phases(context: Context) -> None:
|
||||
|
||||
@when("I m5 smoke invoke project context inspect")
|
||||
def step_m5_invoke_context_inspect(context: Context) -> None:
|
||||
try:
|
||||
context_inspect(project="local/m5-proj")
|
||||
context.m5_error = None
|
||||
except NotImplementedError as exc:
|
||||
context.m5_error = exc
|
||||
from cleveragents.application.services.context_tiers import ContextTierService
|
||||
from cleveragents.cli.commands.project_context import (
|
||||
_default_acms_config,
|
||||
)
|
||||
from cleveragents.cli.commands.project_context import (
|
||||
app as pc_app,
|
||||
)
|
||||
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.get.return_value = MagicMock()
|
||||
|
||||
mock_container = MagicMock()
|
||||
mock_container.session_factory.return_value = MagicMock()
|
||||
mock_container.namespaced_project_repo.return_value = mock_repo
|
||||
mock_container.context_tier_service.return_value = ContextTierService()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"cleveragents.application.container.get_container",
|
||||
return_value=mock_container,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.project_context._read_acms_config",
|
||||
return_value=_default_acms_config(),
|
||||
),
|
||||
):
|
||||
result = context.runner.invoke(pc_app, ["inspect", "local/m5-proj"])
|
||||
context.m5_result = result
|
||||
|
||||
|
||||
@when("I m5 smoke invoke project context simulate")
|
||||
def step_m5_invoke_context_simulate(context: Context) -> None:
|
||||
try:
|
||||
context_simulate(project="local/m5-proj")
|
||||
context.m5_error = None
|
||||
except NotImplementedError as exc:
|
||||
context.m5_error = exc
|
||||
from cleveragents.application.services.context_tiers import ContextTierService
|
||||
from cleveragents.cli.commands.project_context import (
|
||||
_default_acms_config,
|
||||
)
|
||||
from cleveragents.cli.commands.project_context import (
|
||||
app as pc_app,
|
||||
)
|
||||
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.get.return_value = MagicMock()
|
||||
|
||||
mock_container = MagicMock()
|
||||
mock_container.session_factory.return_value = MagicMock()
|
||||
mock_container.namespaced_project_repo.return_value = mock_repo
|
||||
mock_container.context_tier_service.return_value = ContextTierService()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"cleveragents.application.container.get_container",
|
||||
return_value=mock_container,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.project_context._read_acms_config",
|
||||
return_value=_default_acms_config(),
|
||||
),
|
||||
):
|
||||
result = context.runner.invoke(pc_app, ["simulate", "local/m5-proj"])
|
||||
context.m5_result = result
|
||||
|
||||
|
||||
@then('a m5 smoke NotImplementedError should be raised mentioning "{text}"')
|
||||
def step_m5_not_implemented_mentioning(context: Context, text: str) -> None:
|
||||
assert context.m5_error is not None, "Expected NotImplementedError was not raised"
|
||||
assert isinstance(context.m5_error, NotImplementedError)
|
||||
assert text.lower() in str(context.m5_error).lower(), (
|
||||
f"Expected '{text}' in: {context.m5_error}"
|
||||
@then("the m5 smoke project context inspect should succeed")
|
||||
def step_m5_project_context_inspect_success(context: Context) -> None:
|
||||
assert context.m5_result.exit_code == 0, (
|
||||
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the m5 smoke project context simulate should succeed")
|
||||
def step_m5_project_context_simulate_success(context: Context) -> None:
|
||||
assert context.m5_result.exit_code == 0, (
|
||||
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -106,9 +106,17 @@ def step_create_project_for_ctx(context: Any, name: str) -> None:
|
||||
|
||||
def _mock_container(context: Any) -> MagicMock:
|
||||
"""Build a mock container wired to the test DB."""
|
||||
from cleveragents.application.services.context_tiers import (
|
||||
ContextTierService,
|
||||
)
|
||||
|
||||
mock_container = MagicMock()
|
||||
mock_container.namespaced_project_repo.return_value = context.ctx_project_repo
|
||||
mock_container.session_factory.return_value = context.ctx_session_factory
|
||||
# Provide a real (in-memory) ContextTierService for inspect/simulate
|
||||
if not hasattr(context, "ctx_tier_service"):
|
||||
context.ctx_tier_service = ContextTierService()
|
||||
mock_container.context_tier_service.return_value = context.ctx_tier_service
|
||||
return mock_container
|
||||
|
||||
|
||||
@@ -123,7 +131,12 @@ def _run_with_container(context: Any, func: Any, *args: Any, **kwargs: Any) -> N
|
||||
# ANSI escape codes when FORCE_COLOR is set or a real colour terminal
|
||||
# is detected.
|
||||
test_console = RichConsole(
|
||||
file=buf, no_color=True, highlight=False, force_terminal=False
|
||||
file=buf,
|
||||
no_color=True,
|
||||
highlight=False,
|
||||
force_terminal=False,
|
||||
width=500,
|
||||
soft_wrap=True,
|
||||
)
|
||||
|
||||
with (
|
||||
@@ -516,6 +529,20 @@ def step_policy_strat_none(context: Any) -> None:
|
||||
)
|
||||
|
||||
|
||||
@then("the context inspect command should succeed")
|
||||
def step_ctx_inspect_ok(context: Any) -> None:
|
||||
assert context.ctx_exit_code == 0, (
|
||||
f"Expected exit 0, got {context.ctx_exit_code}. Output: {context.ctx_output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the context simulate command should succeed")
|
||||
def step_ctx_simulate_ok(context: Any) -> None:
|
||||
assert context.ctx_exit_code == 0, (
|
||||
f"Expected exit 0, got {context.ctx_exit_code}. Output: {context.ctx_output}"
|
||||
)
|
||||
|
||||
|
||||
@then('a NotImplementedError should be raised with message containing "{msg}"')
|
||||
def step_not_implemented(context: Any, msg: str) -> None:
|
||||
assert context.ctx_error is not None, "No error was raised"
|
||||
|
||||
@@ -5,12 +5,20 @@ Usage: python robot/helper_project_context_cli.py <test-name>
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from io import StringIO
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from cleveragents.application.services.context_tiers import ContextTierService
|
||||
from cleveragents.domain.models.acms.tiers import (
|
||||
ContextTier,
|
||||
TieredFragment,
|
||||
)
|
||||
from cleveragents.domain.models.core.context_policy import (
|
||||
ContextView,
|
||||
ProjectContextPolicy,
|
||||
@@ -64,6 +72,68 @@ def _create_project(
|
||||
repo.create(proj)
|
||||
|
||||
|
||||
def _make_mock_container(
|
||||
repo: NamespacedProjectRepository,
|
||||
session_factory: Any,
|
||||
tier_service: ContextTierService | None = None,
|
||||
) -> MagicMock:
|
||||
"""Build a mock DI container wired to the test fixtures."""
|
||||
mock_container = MagicMock()
|
||||
mock_container.namespaced_project_repo.return_value = repo
|
||||
mock_container.session_factory.return_value = session_factory
|
||||
if tier_service is None:
|
||||
tier_service = ContextTierService()
|
||||
mock_container.context_tier_service.return_value = tier_service
|
||||
return mock_container
|
||||
|
||||
|
||||
def _run_cli_func(
|
||||
func: Any,
|
||||
repo: NamespacedProjectRepository,
|
||||
session_factory: Any,
|
||||
tier_service: ContextTierService | None = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[int, str]:
|
||||
"""Execute a CLI function with mocked DI and capture output."""
|
||||
import typer
|
||||
from rich.console import Console as RichConsole
|
||||
|
||||
mock_cont = _make_mock_container(repo, session_factory, tier_service)
|
||||
buf = StringIO()
|
||||
test_console = RichConsole(
|
||||
file=buf,
|
||||
no_color=True,
|
||||
highlight=False,
|
||||
force_terminal=False,
|
||||
width=500,
|
||||
soft_wrap=True,
|
||||
)
|
||||
|
||||
exit_code = 0
|
||||
with (
|
||||
patch(
|
||||
"cleveragents.application.container.get_container",
|
||||
return_value=mock_cont,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.project_context.console",
|
||||
test_console,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.project_context.err_console",
|
||||
test_console,
|
||||
),
|
||||
):
|
||||
try:
|
||||
func(**kwargs)
|
||||
except typer.Exit as exc:
|
||||
exit_code = exc.exit_code
|
||||
except typer.Abort:
|
||||
exit_code = 1
|
||||
|
||||
return exit_code, buf.getvalue()
|
||||
|
||||
|
||||
def test_context_set_show() -> None:
|
||||
"""Set a context policy and then read it back."""
|
||||
from cleveragents.cli.commands.project_context import (
|
||||
@@ -108,42 +178,173 @@ def test_context_set_show() -> None:
|
||||
print("context-set-show-ok")
|
||||
|
||||
|
||||
def test_context_inspect_stub() -> None:
|
||||
"""Verify inspect raises NotImplementedError."""
|
||||
from cleveragents.cli.commands.project_context import (
|
||||
def test_context_inspect_wired() -> None:
|
||||
"""Verify inspect is wired to ACMS and succeeds."""
|
||||
from cleveragents.cli.commands.project_context import context_inspect
|
||||
|
||||
repo, sf = _setup_db()
|
||||
_create_project(repo, "ctx-robot")
|
||||
|
||||
tier_svc = ContextTierService()
|
||||
# Pre-populate a fragment
|
||||
tier_svc.store(
|
||||
TieredFragment(
|
||||
fragment_id="robot-f1",
|
||||
content="test fragment",
|
||||
tier=ContextTier.HOT,
|
||||
project_name="local/ctx-robot",
|
||||
token_count=50,
|
||||
)
|
||||
)
|
||||
|
||||
exit_code, output = _run_cli_func(
|
||||
context_inspect,
|
||||
repo,
|
||||
sf,
|
||||
tier_service=tier_svc,
|
||||
project="local/ctx-robot",
|
||||
output_format="json",
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise AssertionError(f"inspect failed with exit {exit_code}: {output}")
|
||||
|
||||
data = json.loads(output.strip())
|
||||
if "tier_metrics" not in data:
|
||||
raise AssertionError(f"Missing tier_metrics in output: {data.keys()}")
|
||||
if "project_fragments" not in data:
|
||||
raise AssertionError(f"Missing project_fragments in output: {data.keys()}")
|
||||
if data["project_fragments"]["total"] != 1:
|
||||
raise AssertionError(
|
||||
f"Expected 1 fragment, got {data['project_fragments']['total']}"
|
||||
)
|
||||
|
||||
print("context-inspect-wired-ok")
|
||||
|
||||
|
||||
def test_context_simulate_wired() -> None:
|
||||
"""Verify simulate is wired to ACMS and succeeds."""
|
||||
from cleveragents.cli.commands.project_context import context_simulate
|
||||
|
||||
repo, sf = _setup_db()
|
||||
_create_project(repo, "ctx-robot")
|
||||
|
||||
tier_svc = ContextTierService()
|
||||
# Pre-populate a fragment for simulation
|
||||
tier_svc.store(
|
||||
TieredFragment(
|
||||
fragment_id="robot-sim1",
|
||||
content="simulation test fragment",
|
||||
tier=ContextTier.HOT,
|
||||
project_name="local/ctx-robot",
|
||||
token_count=100,
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
context_inspect(project="local/any")
|
||||
raise AssertionError("Should have raised")
|
||||
except NotImplementedError as exc:
|
||||
if "ACMS" not in str(exc):
|
||||
raise AssertionError(f"Missing ACMS message: {exc}") from exc
|
||||
|
||||
print("context-inspect-stub-ok")
|
||||
|
||||
|
||||
def test_context_simulate_stub() -> None:
|
||||
"""Verify simulate raises NotImplementedError."""
|
||||
from cleveragents.cli.commands.project_context import (
|
||||
exit_code, output = _run_cli_func(
|
||||
context_simulate,
|
||||
repo,
|
||||
sf,
|
||||
tier_service=tier_svc,
|
||||
project="local/ctx-robot",
|
||||
output_format="json",
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise AssertionError(f"simulate failed with exit {exit_code}: {output}")
|
||||
|
||||
data = json.loads(output.strip())
|
||||
if "total_tokens" not in data:
|
||||
raise AssertionError(f"Missing total_tokens in output: {data.keys()}")
|
||||
if data["total_tokens"] <= 0:
|
||||
raise AssertionError(
|
||||
f"Expected positive total_tokens, got {data['total_tokens']}"
|
||||
)
|
||||
if "strategies_used" not in data:
|
||||
raise AssertionError(f"Missing strategies_used in output: {data.keys()}")
|
||||
|
||||
print("context-simulate-wired-ok")
|
||||
|
||||
|
||||
def test_context_set_acms_options() -> None:
|
||||
"""Verify context set persists ACMS pipeline config."""
|
||||
from cleveragents.cli.commands.project_context import (
|
||||
_read_acms_config,
|
||||
context_set,
|
||||
)
|
||||
|
||||
try:
|
||||
context_simulate(project="local/any")
|
||||
raise AssertionError("Should have raised")
|
||||
except NotImplementedError as exc:
|
||||
if "ACMS" not in str(exc):
|
||||
raise AssertionError(f"Missing ACMS message: {exc}") from exc
|
||||
repo, sf = _setup_db()
|
||||
_create_project(repo, "ctx-robot")
|
||||
|
||||
print("context-simulate-stub-ok")
|
||||
exit_code, output = _run_cli_func(
|
||||
context_set,
|
||||
repo,
|
||||
sf,
|
||||
project="local/ctx-robot",
|
||||
hot_max_tokens=4000,
|
||||
warm_max_decisions=200,
|
||||
temporal_scope="recent",
|
||||
auto_refresh=False,
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise AssertionError(f"set failed with exit {exit_code}: {output}")
|
||||
|
||||
acms = _read_acms_config(sf, "local/ctx-robot")
|
||||
if acms["hot_max_tokens"] != 4000:
|
||||
raise AssertionError(f"hot_max_tokens mismatch: {acms['hot_max_tokens']}")
|
||||
if acms["warm_max_decisions"] != 200:
|
||||
raise AssertionError(
|
||||
f"warm_max_decisions mismatch: {acms['warm_max_decisions']}"
|
||||
)
|
||||
if acms["temporal_scope"] != "recent":
|
||||
raise AssertionError(f"temporal_scope mismatch: {acms['temporal_scope']}")
|
||||
if acms["auto_refresh"] is not False:
|
||||
raise AssertionError(f"auto_refresh mismatch: {acms['auto_refresh']}")
|
||||
|
||||
print("context-set-acms-ok")
|
||||
|
||||
|
||||
def test_context_show_acms_config() -> None:
|
||||
"""Verify context show includes ACMS config in JSON output."""
|
||||
from cleveragents.cli.commands.project_context import context_set, context_show
|
||||
|
||||
repo, sf = _setup_db()
|
||||
_create_project(repo, "ctx-robot")
|
||||
|
||||
# First set some ACMS config
|
||||
_run_cli_func(
|
||||
context_set,
|
||||
repo,
|
||||
sf,
|
||||
project="local/ctx-robot",
|
||||
hot_max_tokens=5000,
|
||||
)
|
||||
|
||||
exit_code, output = _run_cli_func(
|
||||
context_show,
|
||||
repo,
|
||||
sf,
|
||||
project="local/ctx-robot",
|
||||
output_format="json",
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise AssertionError(f"show failed with exit {exit_code}: {output}")
|
||||
|
||||
data = json.loads(output.strip())
|
||||
if "acms_config" not in data:
|
||||
raise AssertionError(f"Missing acms_config in output: {data.keys()}")
|
||||
if data["acms_config"]["hot_max_tokens"] != 5000:
|
||||
raise AssertionError(
|
||||
f"hot_max_tokens mismatch: {data['acms_config']['hot_max_tokens']}"
|
||||
)
|
||||
|
||||
print("context-show-acms-ok")
|
||||
|
||||
|
||||
TESTS: dict[str, Any] = {
|
||||
"set-show": test_context_set_show,
|
||||
"inspect-stub": test_context_inspect_stub,
|
||||
"simulate-stub": test_context_simulate_stub,
|
||||
"inspect-wired": test_context_inspect_wired,
|
||||
"simulate-wired": test_context_simulate_wired,
|
||||
"set-acms": test_context_set_acms_options,
|
||||
"show-acms": test_context_show_acms_config,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,17 +11,39 @@ ${HELPER_SCRIPT} robot/helper_project_context_cli.py
|
||||
Context Policy Set And Show Round Trip
|
||||
[Documentation] Set a context policy and verify it reads back correctly
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} set-show cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} context-set-show-ok
|
||||
|
||||
Context Inspect Raises NotImplementedError
|
||||
[Documentation] Verify inspect stub raises NotImplementedError
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} inspect-stub cwd=${WORKSPACE}
|
||||
Context Inspect Wired To ACMS
|
||||
[Documentation] Verify inspect queries the ACMS tier service and returns tier state
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} inspect-wired cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} context-inspect-stub-ok
|
||||
Should Contain ${result.stdout} context-inspect-wired-ok
|
||||
|
||||
Context Simulate Raises NotImplementedError
|
||||
[Documentation] Verify simulate stub raises NotImplementedError
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} simulate-stub cwd=${WORKSPACE}
|
||||
Context Simulate Wired To ACMS
|
||||
[Documentation] Verify simulate runs dry-run context assembly via ACMS
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} simulate-wired cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} context-simulate-stub-ok
|
||||
Should Contain ${result.stdout} context-simulate-wired-ok
|
||||
|
||||
Context Set ACMS Pipeline Options
|
||||
[Documentation] Verify context set persists ACMS pipeline configuration
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} set-acms cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} context-set-acms-ok
|
||||
|
||||
Context Show ACMS Pipeline Config
|
||||
[Documentation] Verify context show displays ACMS pipeline configuration
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} show-acms cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} context-show-acms-ok
|
||||
|
||||
@@ -266,6 +266,33 @@ class ContextTierService:
|
||||
warm_miss_count=self._warm_miss,
|
||||
)
|
||||
|
||||
def get_scoped_metrics(self, project_names: list[str]) -> TierMetrics:
|
||||
"""Return metrics scoped to the given project names.
|
||||
|
||||
Fragment counts (``hot_count``, ``warm_count``, ``cold_count``)
|
||||
reflect only fragments belonging to the specified projects.
|
||||
Hit/miss counters remain global as they are service-level cache
|
||||
performance metrics that are not tracked per-project.
|
||||
|
||||
Args:
|
||||
project_names: Project names to scope fragment counts to.
|
||||
|
||||
Raises:
|
||||
ValueError: If *project_names* is empty.
|
||||
"""
|
||||
if not project_names:
|
||||
raise ValueError("project_names must be non-empty")
|
||||
scoped = ScopedBackendView(allowed_projects=project_names)
|
||||
return TierMetrics(
|
||||
hot_count=sum(1 for f in self._hot.values() if scoped.is_visible(f)),
|
||||
warm_count=sum(1 for f in self._warm.values() if scoped.is_visible(f)),
|
||||
cold_count=sum(1 for f in self._cold.values() if scoped.is_visible(f)),
|
||||
hot_hit_count=self._hot_hit,
|
||||
hot_miss_count=self._hot_miss,
|
||||
warm_hit_count=self._warm_hit,
|
||||
warm_miss_count=self._warm_miss,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Budget
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
"""Project context policy CLI commands.
|
||||
|
||||
Implements ``agents project context set/show/inspect/simulate`` to
|
||||
manage per-project context policies with view inheritance.
|
||||
manage per-project context policies with view inheritance and to
|
||||
query the ACMS pipeline for context state and dry-run assembly.
|
||||
|
||||
Commands:
|
||||
- ``agents project context set`` — persist a context policy view
|
||||
and ACMS pipeline configuration overrides
|
||||
- ``agents project context show`` — display the active context policy
|
||||
- ``agents project context inspect`` — (stub) inspect effective context
|
||||
- ``agents project context simulate`` — (stub) simulate context window
|
||||
including effective ACMS pipeline config
|
||||
- ``agents project context inspect`` — inspect effective context state
|
||||
from the ACMS tier service for a project
|
||||
- ``agents project context simulate`` — dry-run context assembly
|
||||
showing fragments, budget usage, and strategy selection
|
||||
|
||||
Based on ADR-014 (Context Management / ACMS) and the
|
||||
``ProjectContextPolicy`` domain model.
|
||||
@@ -15,19 +20,34 @@ Based on ADR-014 (Context Management / ACMS) and the
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json as _json
|
||||
from typing import Annotated, Any
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
from cleveragents.cli.formatting import OutputFormat, format_output
|
||||
from cleveragents.domain.models.acms.crp import (
|
||||
AssembledContext,
|
||||
ContextFragment,
|
||||
ContextRequest,
|
||||
FragmentProvenance,
|
||||
)
|
||||
from cleveragents.domain.models.acms.tiers import (
|
||||
ActorRole,
|
||||
ContextTier,
|
||||
TieredFragment,
|
||||
TierMetrics,
|
||||
)
|
||||
from cleveragents.domain.models.core.context_policy import (
|
||||
VALID_PHASES,
|
||||
ContextView,
|
||||
ProjectContextPolicy,
|
||||
)
|
||||
from cleveragents.domain.models.core.project import TemporalScope
|
||||
|
||||
app = typer.Typer(help="Manage project context policies")
|
||||
console = Console()
|
||||
@@ -35,6 +55,15 @@ err_console = Console(stderr=True)
|
||||
|
||||
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"
|
||||
|
||||
# Default ACMS pipeline configuration values
|
||||
_DEFAULT_HOT_MAX_TOKENS = 8000
|
||||
_DEFAULT_WARM_MAX_DECISIONS = 500
|
||||
_DEFAULT_COLD_MAX_DECISIONS = 5000
|
||||
_DEFAULT_BREADTH = 2
|
||||
_DEFAULT_DEPTH = 3
|
||||
_DEFAULT_SKELETON_RATIO = 0.2
|
||||
_DEFAULT_BUDGET_TOKENS = 8000
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
@@ -49,6 +78,14 @@ def _get_namespaced_project_repo() -> Any:
|
||||
return container.namespaced_project_repo()
|
||||
|
||||
|
||||
def _get_context_tier_service() -> Any:
|
||||
"""Return the ContextTierService from the DI container."""
|
||||
from cleveragents.application.container import get_container
|
||||
|
||||
container = get_container()
|
||||
return container.context_tier_service()
|
||||
|
||||
|
||||
def _load_policy_json(
|
||||
session_factory: Any,
|
||||
namespaced_name: str,
|
||||
@@ -112,17 +149,57 @@ def _read_policy(
|
||||
return ProjectContextPolicy()
|
||||
|
||||
|
||||
def _read_acms_config(
|
||||
session_factory: Any,
|
||||
namespaced_name: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Read the ACMS pipeline config from the stored policy JSON.
|
||||
|
||||
The ACMS config is stored as an ``acms_config`` key in the
|
||||
same JSON blob as the context policy. Returns defaults when
|
||||
not present.
|
||||
"""
|
||||
raw = _load_policy_json(session_factory, namespaced_name)
|
||||
if raw is None:
|
||||
return _default_acms_config()
|
||||
return raw.get("acms_config", _default_acms_config())
|
||||
|
||||
|
||||
def _default_acms_config() -> dict[str, Any]:
|
||||
"""Return the default ACMS pipeline configuration."""
|
||||
return {
|
||||
"hot_max_tokens": _DEFAULT_HOT_MAX_TOKENS,
|
||||
"warm_max_decisions": _DEFAULT_WARM_MAX_DECISIONS,
|
||||
"cold_max_decisions": _DEFAULT_COLD_MAX_DECISIONS,
|
||||
"query_limit": None,
|
||||
"summarize": True,
|
||||
"summary_max_tokens": None,
|
||||
"strategies": [],
|
||||
"default_breadth": _DEFAULT_BREADTH,
|
||||
"default_depth": _DEFAULT_DEPTH,
|
||||
"depth_gradient": {},
|
||||
"skeleton_ratio": _DEFAULT_SKELETON_RATIO,
|
||||
"temporal_scope": "current",
|
||||
"auto_refresh": True,
|
||||
}
|
||||
|
||||
|
||||
def _write_policy(
|
||||
session_factory: Any,
|
||||
namespaced_name: str,
|
||||
policy: ProjectContextPolicy,
|
||||
acms_config: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Serialise and persist the policy."""
|
||||
_save_policy_json(
|
||||
session_factory,
|
||||
namespaced_name,
|
||||
policy.model_dump(mode="json"),
|
||||
)
|
||||
"""Serialise and persist the policy with optional ACMS config."""
|
||||
data = policy.model_dump(mode="json")
|
||||
if acms_config is not None:
|
||||
data["acms_config"] = acms_config
|
||||
else:
|
||||
# Preserve existing ACMS config
|
||||
existing = _load_policy_json(session_factory, namespaced_name)
|
||||
if existing is not None and "acms_config" in existing:
|
||||
data["acms_config"] = existing["acms_config"]
|
||||
_save_policy_json(session_factory, namespaced_name, data)
|
||||
|
||||
|
||||
def _policy_to_dict(policy: ProjectContextPolicy) -> dict[str, Any]:
|
||||
@@ -143,6 +220,160 @@ def _policy_to_dict(policy: ProjectContextPolicy) -> dict[str, Any]:
|
||||
return result
|
||||
|
||||
|
||||
def _tier_metrics_to_dict(metrics: TierMetrics) -> dict[str, Any]:
|
||||
"""Convert tier metrics to a display-friendly dict."""
|
||||
return {
|
||||
"hot_count": metrics.hot_count,
|
||||
"warm_count": metrics.warm_count,
|
||||
"cold_count": metrics.cold_count,
|
||||
"hot_hit_count": metrics.hot_hit_count,
|
||||
"hot_miss_count": metrics.hot_miss_count,
|
||||
"warm_hit_count": metrics.warm_hit_count,
|
||||
"warm_miss_count": metrics.warm_miss_count,
|
||||
}
|
||||
|
||||
|
||||
def _fragment_to_dict(fragment: TieredFragment) -> dict[str, Any]:
|
||||
"""Convert a tiered fragment to a display-friendly dict."""
|
||||
return {
|
||||
"fragment_id": fragment.fragment_id,
|
||||
"tier": fragment.tier.value,
|
||||
"resource_id": fragment.resource_id,
|
||||
"project_name": fragment.project_name,
|
||||
"token_count": fragment.token_count,
|
||||
"access_count": fragment.access_count,
|
||||
"last_accessed": fragment.last_accessed.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def _assembled_to_dict(assembled: AssembledContext) -> dict[str, Any]:
|
||||
"""Convert an AssembledContext to a display-friendly dict."""
|
||||
return {
|
||||
"total_tokens": assembled.total_tokens,
|
||||
"budget_used": assembled.budget_used,
|
||||
"strategies_used": assembled.strategies_used,
|
||||
"context_hash": assembled.context_hash,
|
||||
"preamble": assembled.preamble,
|
||||
"fragment_count": len(assembled.fragments),
|
||||
"fragments": [
|
||||
{
|
||||
"uko_node": f.uko_node,
|
||||
"detail_depth": f.detail_depth,
|
||||
"token_count": f.token_count,
|
||||
"relevance_score": f.relevance_score,
|
||||
"provenance": {
|
||||
"resource_uri": f.provenance.resource_uri,
|
||||
"location": f.provenance.location,
|
||||
"strategy": f.provenance.strategy,
|
||||
},
|
||||
}
|
||||
for f in assembled.fragments
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _simulate_context_assembly(
|
||||
project_name: str,
|
||||
acms_config: dict[str, Any],
|
||||
budget_tokens: int | None = None,
|
||||
focus_uris: list[str] | None = None,
|
||||
strategy_hints: list[str] | None = None,
|
||||
view: str = "default",
|
||||
) -> AssembledContext:
|
||||
"""Run a dry-run context assembly using the CRP models.
|
||||
|
||||
This creates a simulated AssembledContext based on available
|
||||
backend data and the project's ACMS configuration. When the
|
||||
full ACMS pipeline is wired, this will delegate to the live
|
||||
pipeline instead.
|
||||
"""
|
||||
effective_budget = budget_tokens or acms_config.get(
|
||||
"hot_max_tokens", _DEFAULT_BUDGET_TOKENS
|
||||
)
|
||||
effective_strategies = strategy_hints or acms_config.get("strategies", [])
|
||||
default_breadth = acms_config.get("default_breadth", _DEFAULT_BREADTH)
|
||||
default_depth = acms_config.get("default_depth", _DEFAULT_DEPTH)
|
||||
temporal_str = acms_config.get("temporal_scope", "current")
|
||||
|
||||
# Resolve temporal scope
|
||||
temporal = TemporalScope.CURRENT
|
||||
if temporal_str == "recent":
|
||||
temporal = TemporalScope.RECENT
|
||||
elif temporal_str == "all":
|
||||
temporal = TemporalScope.ALL
|
||||
|
||||
# Build the context request
|
||||
_request = ContextRequest(
|
||||
query=f"Context assembly for project {project_name} (view: {view})",
|
||||
focus=focus_uris or [],
|
||||
breadth=default_breadth,
|
||||
depth=default_depth,
|
||||
temporal=temporal,
|
||||
max_tokens=effective_budget,
|
||||
preferred_strategies=effective_strategies,
|
||||
purpose=f"dry-run simulate for {project_name}",
|
||||
)
|
||||
|
||||
# Query the tier service for existing fragments
|
||||
tier_service = _get_context_tier_service()
|
||||
project_fragments = tier_service.get_scoped_view([project_name])
|
||||
|
||||
# Apply focus URI filtering if specified
|
||||
if focus_uris:
|
||||
project_fragments = [
|
||||
f
|
||||
for f in project_fragments
|
||||
if f.resource_id in focus_uris
|
||||
or any(u in (f.resource_id or "") for u in focus_uris)
|
||||
]
|
||||
|
||||
# Build simulated context fragments from tier data
|
||||
fragments: list[ContextFragment] = []
|
||||
total_tokens = 0
|
||||
for tf in project_fragments:
|
||||
if total_tokens + tf.token_count > effective_budget:
|
||||
break
|
||||
fragments.append(
|
||||
ContextFragment(
|
||||
uko_node=tf.resource_id or f"uko:{tf.fragment_id}",
|
||||
content=tf.content[:100] + "..."
|
||||
if len(tf.content) > 100
|
||||
else tf.content,
|
||||
detail_depth=default_depth if isinstance(default_depth, int) else 3,
|
||||
token_count=tf.token_count,
|
||||
relevance_score=min(1.0, tf.access_count / 10.0)
|
||||
if tf.access_count > 0
|
||||
else 0.5,
|
||||
provenance=FragmentProvenance(
|
||||
resource_uri=tf.resource_id or "unknown",
|
||||
location="",
|
||||
strategy="tier_retrieval",
|
||||
),
|
||||
)
|
||||
)
|
||||
total_tokens += tf.token_count
|
||||
|
||||
budget_used = total_tokens / effective_budget if effective_budget > 0 else 0.0
|
||||
content_for_hash = f"{project_name}:{view}:{total_tokens}"
|
||||
context_hash = hashlib.sha256(content_for_hash.encode()).hexdigest()[:16]
|
||||
|
||||
strategies_used = (
|
||||
effective_strategies if effective_strategies else ["tier_retrieval"]
|
||||
)
|
||||
|
||||
return AssembledContext(
|
||||
fragments=fragments,
|
||||
total_tokens=total_tokens,
|
||||
budget_used=min(budget_used, 1.0),
|
||||
strategies_used=strategies_used,
|
||||
context_hash=context_hash,
|
||||
preamble=(
|
||||
f"Simulated context for {project_name}"
|
||||
f" (view: {view}, budget: {effective_budget} tokens)"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Commands
|
||||
# ------------------------------------------------------------------
|
||||
@@ -213,6 +444,90 @@ def context_set(
|
||||
help="Default execution environment for this project (host or container)",
|
||||
),
|
||||
] = None,
|
||||
hot_max_tokens: Annotated[
|
||||
int | None,
|
||||
typer.Option(
|
||||
"--hot-max-tokens",
|
||||
help="Max tokens for the hot tier context window",
|
||||
),
|
||||
] = None,
|
||||
warm_max_decisions: Annotated[
|
||||
int | None,
|
||||
typer.Option(
|
||||
"--warm-max-decisions",
|
||||
help="Max decisions for the warm tier",
|
||||
),
|
||||
] = None,
|
||||
cold_max_decisions: Annotated[
|
||||
int | None,
|
||||
typer.Option(
|
||||
"--cold-max-decisions",
|
||||
help="Max decisions for the cold tier",
|
||||
),
|
||||
] = None,
|
||||
query_limit: Annotated[
|
||||
int | None,
|
||||
typer.Option(
|
||||
"--query-limit",
|
||||
help="Max number of fragments per query",
|
||||
),
|
||||
] = None,
|
||||
summarize: Annotated[
|
||||
bool | None,
|
||||
typer.Option(
|
||||
"--summarize/--no-summarize",
|
||||
help="Enable or disable context summarization",
|
||||
),
|
||||
] = None,
|
||||
summary_max_tokens: Annotated[
|
||||
int | None,
|
||||
typer.Option(
|
||||
"--summary-max-tokens",
|
||||
help="Max tokens for context summaries",
|
||||
),
|
||||
] = None,
|
||||
strategy: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
"--strategy",
|
||||
help="Preferred context strategy (repeatable)",
|
||||
),
|
||||
] = None,
|
||||
default_breadth: Annotated[
|
||||
int | None,
|
||||
typer.Option(
|
||||
"--default-breadth",
|
||||
help="Default breadth for context retrieval",
|
||||
),
|
||||
] = None,
|
||||
default_depth: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--default-depth",
|
||||
help="Default depth (integer or named level)",
|
||||
),
|
||||
] = None,
|
||||
skeleton_ratio: Annotated[
|
||||
float | None,
|
||||
typer.Option(
|
||||
"--skeleton-ratio",
|
||||
help="Skeleton compression ratio (0.0-1.0)",
|
||||
),
|
||||
] = None,
|
||||
temporal_scope: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--temporal-scope",
|
||||
help="Temporal scope: current, recent, or all",
|
||||
),
|
||||
] = None,
|
||||
auto_refresh: Annotated[
|
||||
bool | None,
|
||||
typer.Option(
|
||||
"--auto-refresh/--no-auto-refresh",
|
||||
help="Auto-refresh context on resource changes",
|
||||
),
|
||||
] = None,
|
||||
clear: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
@@ -225,7 +540,12 @@ def context_set(
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Set context policy for a project phase view."""
|
||||
"""Set context policy for a project phase view.
|
||||
|
||||
Configures both the context view filtering (resource/path
|
||||
include/exclude) and the ACMS pipeline parameters (tier budgets,
|
||||
strategies, temporal scope, etc.) for a project.
|
||||
"""
|
||||
if view not in VALID_PHASES:
|
||||
err_console.print(
|
||||
f"[red]Invalid view '{view}': must be one of {PHASE_NAMES}[/red]"
|
||||
@@ -246,6 +566,7 @@ def context_set(
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
policy = _read_policy(session_factory, project)
|
||||
acms = _read_acms_config(session_factory, project)
|
||||
|
||||
if clear:
|
||||
# Reset the view to None (inherit from parent)
|
||||
@@ -264,7 +585,43 @@ def context_set(
|
||||
)
|
||||
policy = policy.model_copy(update={f"{view}_view": new_view})
|
||||
|
||||
_write_policy(session_factory, project, policy)
|
||||
# Update ACMS config with any provided overrides
|
||||
if hot_max_tokens is not None:
|
||||
acms["hot_max_tokens"] = hot_max_tokens
|
||||
if warm_max_decisions is not None:
|
||||
acms["warm_max_decisions"] = warm_max_decisions
|
||||
if cold_max_decisions is not None:
|
||||
acms["cold_max_decisions"] = cold_max_decisions
|
||||
if query_limit is not None:
|
||||
acms["query_limit"] = query_limit
|
||||
if summarize is not None:
|
||||
acms["summarize"] = summarize
|
||||
if summary_max_tokens is not None:
|
||||
acms["summary_max_tokens"] = summary_max_tokens
|
||||
if strategy is not None:
|
||||
acms["strategies"] = strategy
|
||||
if default_breadth is not None:
|
||||
acms["default_breadth"] = default_breadth
|
||||
if default_depth is not None:
|
||||
# Parse as int if possible, otherwise keep as string
|
||||
try:
|
||||
acms["default_depth"] = int(default_depth)
|
||||
except ValueError:
|
||||
acms["default_depth"] = default_depth
|
||||
if skeleton_ratio is not None:
|
||||
acms["skeleton_ratio"] = skeleton_ratio
|
||||
if temporal_scope is not None:
|
||||
if temporal_scope not in ("current", "recent", "all"):
|
||||
err_console.print(
|
||||
f"[red]Invalid temporal scope '{temporal_scope}': "
|
||||
f"must be current, recent, or all[/red]"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
acms["temporal_scope"] = temporal_scope
|
||||
if auto_refresh is not None:
|
||||
acms["auto_refresh"] = auto_refresh
|
||||
|
||||
_write_policy(session_factory, project, policy, acms)
|
||||
|
||||
# Handle execution_environment at the project level
|
||||
if execution_environment is not None:
|
||||
@@ -285,11 +642,13 @@ def context_set(
|
||||
project,
|
||||
{
|
||||
**policy.model_dump(mode="json"),
|
||||
"acms_config": acms,
|
||||
"execution_environment": execution_environment.lower(),
|
||||
},
|
||||
)
|
||||
|
||||
data = _policy_to_dict(policy)
|
||||
data["acms_config"] = acms
|
||||
if output_format.lower() == OutputFormat.RICH:
|
||||
console.print(
|
||||
Panel(
|
||||
@@ -323,7 +682,7 @@ def context_show(
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Show the context policy for a project."""
|
||||
"""Show the context policy and ACMS pipeline config for a project."""
|
||||
from cleveragents.application.container import get_container
|
||||
|
||||
container = get_container()
|
||||
@@ -338,6 +697,7 @@ def context_show(
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
policy = _read_policy(session_factory, project)
|
||||
acms = _read_acms_config(session_factory, project)
|
||||
|
||||
if view is not None:
|
||||
if view not in VALID_PHASES:
|
||||
@@ -349,9 +709,11 @@ def context_show(
|
||||
data: dict[str, Any] = {
|
||||
"phase": view,
|
||||
"resolved_view": resolved.model_dump(mode="json"),
|
||||
"acms_config": acms,
|
||||
}
|
||||
else:
|
||||
data = _policy_to_dict(policy)
|
||||
data["acms_config"] = acms
|
||||
|
||||
if output_format.lower() == OutputFormat.RICH:
|
||||
title = f"Context Policy: {project}"
|
||||
@@ -387,6 +749,34 @@ def context_show(
|
||||
lines.append(f"[bold]{phase}:[/bold] configured")
|
||||
else:
|
||||
lines.append(f"[bold]{phase}:[/bold] (inherits)")
|
||||
# ACMS pipeline config section
|
||||
lines.append("")
|
||||
lines.append("[bold]ACMS Pipeline Config:[/bold]")
|
||||
lines.append(
|
||||
f" Hot max tokens: {acms.get('hot_max_tokens', _DEFAULT_HOT_MAX_TOKENS)}"
|
||||
)
|
||||
lines.append(
|
||||
f" Warm max decisions: "
|
||||
f"{acms.get('warm_max_decisions', _DEFAULT_WARM_MAX_DECISIONS)}"
|
||||
)
|
||||
lines.append(
|
||||
f" Cold max decisions: "
|
||||
f"{acms.get('cold_max_decisions', _DEFAULT_COLD_MAX_DECISIONS)}"
|
||||
)
|
||||
lines.append(f" Summarize: {acms.get('summarize', True)}")
|
||||
lines.append(f" Temporal scope: {acms.get('temporal_scope', 'current')}")
|
||||
lines.append(f" Auto-refresh: {acms.get('auto_refresh', True)}")
|
||||
strategies = acms.get("strategies", [])
|
||||
lines.append(
|
||||
f" Strategies: {', '.join(strategies) if strategies else '(default)'}"
|
||||
)
|
||||
lines.append(
|
||||
f" Default breadth: {acms.get('default_breadth', _DEFAULT_BREADTH)}"
|
||||
)
|
||||
lines.append(f" Default depth: {acms.get('default_depth', _DEFAULT_DEPTH)}")
|
||||
lines.append(
|
||||
f" Skeleton ratio: {acms.get('skeleton_ratio', _DEFAULT_SKELETON_RATIO)}"
|
||||
)
|
||||
console.print(
|
||||
Panel(
|
||||
"\n".join(lines),
|
||||
@@ -404,17 +794,192 @@ def context_inspect(
|
||||
str,
|
||||
typer.Argument(help="Project namespaced name"),
|
||||
],
|
||||
view: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--view",
|
||||
"-v",
|
||||
help="Phase view to inspect: default, strategize, execute, or apply",
|
||||
),
|
||||
] = None,
|
||||
strategy_filter: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--strategy",
|
||||
help="Filter fragments by strategy name",
|
||||
),
|
||||
] = None,
|
||||
focus: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
"--focus",
|
||||
help="UKO URIs to focus on (repeatable)",
|
||||
),
|
||||
] = None,
|
||||
breadth: Annotated[
|
||||
int | None,
|
||||
typer.Option(
|
||||
"--breadth",
|
||||
help="Override breadth for context retrieval",
|
||||
),
|
||||
] = None,
|
||||
depth: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--depth",
|
||||
help="Override depth (integer or named level)",
|
||||
),
|
||||
] = None,
|
||||
output_format: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Inspect effective context for a project.
|
||||
"""Inspect effective context state for a project.
|
||||
|
||||
NOTE: This command requires ACMS wiring and is not yet
|
||||
implemented.
|
||||
Queries the ACMS tier service to display the current context
|
||||
fragments across hot/warm/cold tiers, tier metrics, budget
|
||||
utilisation, and the effective ACMS pipeline configuration.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"'agents project context inspect' requires ACMS "
|
||||
"wiring and is not yet implemented. "
|
||||
"See ADR-014 for the planned integration."
|
||||
)
|
||||
if view is not None and view not in VALID_PHASES:
|
||||
err_console.print(
|
||||
f"[red]Invalid view '{view}': must be one of {PHASE_NAMES}[/red]"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
from cleveragents.application.container import get_container
|
||||
|
||||
container = get_container()
|
||||
session_factory = container.session_factory()
|
||||
|
||||
# Validate project exists
|
||||
repo = _get_namespaced_project_repo()
|
||||
try:
|
||||
repo.get(project)
|
||||
except Exception as exc:
|
||||
err_console.print(f"[red]Project not found:[/red] {project}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
# Query the tier service (project-scoped fragment counts)
|
||||
tier_service = _get_context_tier_service()
|
||||
metrics: TierMetrics = tier_service.get_scoped_metrics([project])
|
||||
budget = tier_service.budget
|
||||
|
||||
# Get project-scoped fragments
|
||||
project_fragments: list[TieredFragment] = tier_service.get_scoped_view([project])
|
||||
|
||||
# Apply optional filters
|
||||
if strategy_filter is not None:
|
||||
# Filter fragments whose metadata or resource_id relates to the strategy
|
||||
project_fragments = [
|
||||
f
|
||||
for f in project_fragments
|
||||
if f.metadata.get("strategy") == strategy_filter
|
||||
or strategy_filter in (f.resource_id or "")
|
||||
]
|
||||
if focus is not None:
|
||||
# Filter fragments whose resource_id matches any focus URI
|
||||
project_fragments = [
|
||||
f
|
||||
for f in project_fragments
|
||||
if f.resource_id in focus or any(u in (f.resource_id or "") for u in focus)
|
||||
]
|
||||
|
||||
# Get actor views for each role
|
||||
actor_views: dict[str, int] = {}
|
||||
for role in ActorRole:
|
||||
role_frags = tier_service.get_for_actor(role, [project])
|
||||
actor_views[role.value] = len(role_frags)
|
||||
|
||||
# Read ACMS config and apply overrides
|
||||
acms = _read_acms_config(session_factory, project)
|
||||
if breadth is not None:
|
||||
acms["default_breadth"] = breadth
|
||||
if depth is not None:
|
||||
try:
|
||||
acms["default_depth"] = int(depth)
|
||||
except ValueError:
|
||||
acms["default_depth"] = depth
|
||||
|
||||
# Build result
|
||||
data: dict[str, Any] = {
|
||||
"project": project,
|
||||
"view": view or "all",
|
||||
"tier_metrics": _tier_metrics_to_dict(metrics),
|
||||
"tier_budget": {
|
||||
"max_tokens_hot": budget.max_tokens_hot,
|
||||
"max_decisions_warm": budget.max_decisions_warm,
|
||||
"max_decisions_cold": budget.max_decisions_cold,
|
||||
},
|
||||
"project_fragments": {
|
||||
"total": len(project_fragments),
|
||||
"by_tier": {
|
||||
"hot": len([f for f in project_fragments if f.tier == ContextTier.HOT]),
|
||||
"warm": len(
|
||||
[f for f in project_fragments if f.tier == ContextTier.WARM]
|
||||
),
|
||||
"cold": len(
|
||||
[f for f in project_fragments if f.tier == ContextTier.COLD]
|
||||
),
|
||||
},
|
||||
},
|
||||
"actor_visibility": actor_views,
|
||||
"acms_config": acms,
|
||||
}
|
||||
|
||||
# Add fragment details (limited to first 20)
|
||||
fragment_details = [_fragment_to_dict(f) for f in project_fragments[:20]]
|
||||
data["fragments"] = fragment_details
|
||||
|
||||
if output_format.lower() == OutputFormat.RICH:
|
||||
title = f"Context State: {project}"
|
||||
if view:
|
||||
title += f" (phase: {view})"
|
||||
|
||||
# Metrics table
|
||||
metrics_table = Table(title="Tier Metrics", expand=False)
|
||||
metrics_table.add_column("Metric", style="bold")
|
||||
metrics_table.add_column("Value", justify="right")
|
||||
metrics_table.add_row("Hot fragments", str(metrics.hot_count))
|
||||
metrics_table.add_row("Warm fragments", str(metrics.warm_count))
|
||||
metrics_table.add_row("Cold fragments", str(metrics.cold_count))
|
||||
metrics_table.add_row("Hot hits", str(metrics.hot_hit_count))
|
||||
metrics_table.add_row("Hot misses", str(metrics.hot_miss_count))
|
||||
metrics_table.add_row("Warm hits", str(metrics.warm_hit_count))
|
||||
metrics_table.add_row("Warm misses", str(metrics.warm_miss_count))
|
||||
console.print(Panel(metrics_table, title=title, expand=False))
|
||||
|
||||
# Budget info
|
||||
budget_lines: list[str] = []
|
||||
budget_lines.append(f"[bold]Hot max tokens:[/bold] {budget.max_tokens_hot}")
|
||||
budget_lines.append(
|
||||
f"[bold]Warm max decisions:[/bold] {budget.max_decisions_warm}"
|
||||
)
|
||||
budget_lines.append(
|
||||
f"[bold]Cold max decisions:[/bold] {budget.max_decisions_cold}"
|
||||
)
|
||||
console.print(Panel("\n".join(budget_lines), title="Tier Budget", expand=False))
|
||||
|
||||
# Project fragments summary
|
||||
by_tier = data["project_fragments"]["by_tier"]
|
||||
console.print(
|
||||
Panel(
|
||||
f"Total: {data['project_fragments']['total']} | "
|
||||
f"Hot: {by_tier['hot']} | "
|
||||
f"Warm: {by_tier['warm']} | "
|
||||
f"Cold: {by_tier['cold']}",
|
||||
title=f"Project Fragments: {project}",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
# Actor visibility
|
||||
av_lines = [f"[bold]{r}:[/bold] {c} fragments" for r, c in actor_views.items()]
|
||||
console.print(
|
||||
Panel("\n".join(av_lines), title="Actor Visibility", expand=False)
|
||||
)
|
||||
else:
|
||||
console.print(format_output(data, output_format))
|
||||
|
||||
|
||||
@app.command(name="simulate")
|
||||
@@ -423,14 +988,116 @@ def context_simulate(
|
||||
str,
|
||||
typer.Argument(help="Project namespaced name"),
|
||||
],
|
||||
view: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--view",
|
||||
"-v",
|
||||
help="Phase view for simulation: default, strategize, execute, or apply",
|
||||
),
|
||||
] = None,
|
||||
budget: Annotated[
|
||||
int | None,
|
||||
typer.Option(
|
||||
"--budget",
|
||||
help="Token budget for simulation (overrides project config)",
|
||||
),
|
||||
] = None,
|
||||
focus: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
"--focus",
|
||||
help="UKO URIs to focus on (repeatable)",
|
||||
),
|
||||
] = None,
|
||||
strategy_hint: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
"--strategy",
|
||||
help="Preferred strategies for simulation (repeatable)",
|
||||
),
|
||||
] = None,
|
||||
output_format: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Simulate context window for a project.
|
||||
"""Simulate context window assembly for a project.
|
||||
|
||||
NOTE: This command requires ACMS wiring and is not yet
|
||||
implemented.
|
||||
Runs a dry-run context assembly using the project's ACMS
|
||||
configuration and displays the assembled fragments, budget
|
||||
usage, and strategy selection.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"'agents project context simulate' requires ACMS "
|
||||
"wiring and is not yet implemented. "
|
||||
"See ADR-014 for the planned integration."
|
||||
effective_view = view or "default"
|
||||
if effective_view not in VALID_PHASES:
|
||||
err_console.print(
|
||||
f"[red]Invalid view '{effective_view}': must be one of {PHASE_NAMES}[/red]"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
from cleveragents.application.container import get_container
|
||||
|
||||
container = get_container()
|
||||
session_factory = container.session_factory()
|
||||
|
||||
# Validate project exists
|
||||
repo = _get_namespaced_project_repo()
|
||||
try:
|
||||
repo.get(project)
|
||||
except Exception as exc:
|
||||
err_console.print(f"[red]Project not found:[/red] {project}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
acms = _read_acms_config(session_factory, project)
|
||||
assembled = _simulate_context_assembly(
|
||||
project_name=project,
|
||||
acms_config=acms,
|
||||
budget_tokens=budget,
|
||||
focus_uris=focus,
|
||||
strategy_hints=strategy_hint,
|
||||
view=effective_view,
|
||||
)
|
||||
|
||||
data = _assembled_to_dict(assembled)
|
||||
data["project"] = project
|
||||
data["view"] = effective_view
|
||||
data["acms_config"] = acms
|
||||
|
||||
if output_format.lower() == OutputFormat.RICH:
|
||||
title = f"Context Simulation: {project} (view: {effective_view})"
|
||||
|
||||
# Summary panel
|
||||
summary_lines: list[str] = []
|
||||
summary_lines.append(f"[bold]Total tokens:[/bold] {assembled.total_tokens}")
|
||||
summary_lines.append(f"[bold]Budget used:[/bold] {assembled.budget_used:.1%}")
|
||||
summary_lines.append(
|
||||
f"[bold]Strategies:[/bold] "
|
||||
f"{', '.join(assembled.strategies_used) or '(none)'}"
|
||||
)
|
||||
summary_lines.append(f"[bold]Fragment count:[/bold] {len(assembled.fragments)}")
|
||||
summary_lines.append(f"[bold]Context hash:[/bold] {assembled.context_hash}")
|
||||
if assembled.preamble:
|
||||
summary_lines.append(f"[bold]Preamble:[/bold] {assembled.preamble}")
|
||||
console.print(Panel("\n".join(summary_lines), title=title, expand=False))
|
||||
|
||||
# Fragment table
|
||||
if assembled.fragments:
|
||||
frag_table = Table(title="Assembled Fragments", expand=False)
|
||||
frag_table.add_column("#", style="dim", justify="right")
|
||||
frag_table.add_column("UKO Node")
|
||||
frag_table.add_column("Tokens", justify="right")
|
||||
frag_table.add_column("Relevance", justify="right")
|
||||
frag_table.add_column("Depth", justify="right")
|
||||
frag_table.add_column("Strategy")
|
||||
for idx, frag in enumerate(assembled.fragments, 1):
|
||||
frag_table.add_row(
|
||||
str(idx),
|
||||
frag.uko_node,
|
||||
str(frag.token_count),
|
||||
f"{frag.relevance_score:.2f}",
|
||||
str(frag.detail_depth),
|
||||
frag.provenance.strategy,
|
||||
)
|
||||
console.print(frag_table)
|
||||
else:
|
||||
console.print(format_output(data, output_format))
|
||||
|
||||
@@ -48,6 +48,8 @@ _FALSE_POSITIVE_KEYS: set[str] = {
|
||||
"prompt_tokens",
|
||||
"completion_tokens",
|
||||
"token_estimate",
|
||||
"hot_max_tokens",
|
||||
"summary_max_tokens",
|
||||
"auth_method",
|
||||
"auth_type",
|
||||
"auth_enabled",
|
||||
|
||||
Reference in New Issue
Block a user