Files
temp/docs/specification/core-concepts.md

693 KiB
Raw Permalink Blame History

Core Concepts

Plan

!!! adr "Architecture Decision" The plan lifecycle, phase transitions, and plan hierarchy are defined in ADR-006: Plan Lifecycle.

A plan is the fundamental unit of orchestration and traceability.

Plan Lifecycle Phases

A plan progresses through four phases:

Action → Strategize → Execute → Apply

In this spec:

  • Action is the first phase. An action serves as a reusable plan template — it defines the work to be done (description, definition of done, actors, arguments, invariants) without being bound to any project. No significant processing occurs during the Action phase. When an action is used (via agents plan use), it spawns a new plan entity that enters the Strategize phase. By default, the original action remains available for reuse; single-use actions (created with reusable: false) are deleted after first use. Although Action is formally a phase in the lifecycle, it is unique in that it functions primarily as a template from which plans are instantiated.
  • Strategize is the second phase and the first where active processing occurs (the output is a strategy).
  • Execute is the third phase (the output is a changeset).
  • Apply is the fourth and final phase. Apply merges the sandbox changeset into real project resources. Upon completion, the plan reaches one of several terminal states: applied (success — changes committed), constrained (cannot complete within the current strategy's constraints — may trigger reversion to Strategize), errored (failed), or cancelled.

The normal phase progression is forward, but both Execute and Apply may revert to Strategize when the current strategy's constraints are too restrictive (see Phase Reversion below).

Phase Transition Verbs (CLI / UX Contract)

Verbs that trigger phase transitions. CleverAgents should standardize these verbs as the public API (CLI, TUI, web):

Current Phase Command Verb Next Phase
(none) create Action
Action use Strategize
Strategize execute Execute
Execute apply Apply

!!! note "Important behavioral rules" * CleverAgents uses automation profiles to control which of these phase transitions happen automatically. The profile determines whether each transition requires explicit user action or proceeds autonomously, but the verbs remain the conceptual contract. * There is no strategize command. The phase transition verbs are used in CLI commands — not the phase names. The use verb (as in agents plan use) is the command that transitions a plan from the Action phase into Strategize. This is by design: ==use describes the user's intent== (using an action template on a project), and the system responds by entering the Strategize phase.

Phase Reversion (→ Strategize)

!!! adr "Architecture Decision" Phase reversion rules and the decision tree model are detailed in ADR-006: Plan Lifecycle and ADR-007: Decision Tree and Correction.

While the normal phase progression is forward (Action → Strategize → Execute → Apply), both the Execute and Apply phases may revert to Strategize under specific conditions. In all cases, the reversion target is always Strategize — there is no reversion to Execute or any other intermediate phase.

Execute → Strategize
  • Trigger: During execution, the execution actor discovers that constraints established during Strategize are too restrictive to complete the work. For example, a strategy decision may have mandated an approach that proves infeasible once actual resources are examined, dependencies are resolved, or implementation details are encountered.
  • Behavior: Rather than overriding or silently violating Strategize-phase decisions, the plan reverts to the Strategize phase so the strategy actor can adjust the decision tree. Execute-phase decisions must always be constrained by Strategize-phase decisions — if those constraints cannot be honored, the correct response is reversion, not violation.
  • After reversion: The strategy actor receives the execution actor's findings (the reason for reversion, the specific constraints that were too restrictive) and produces an updated decision tree. The plan then re-enters the Execute phase with the revised strategy.
  • Automation: Controlled by the delete_content automation profile flag.
Apply → Strategize
  • Trigger: During the Apply phase, the system determines that the changeset cannot be successfully applied within the current strategy's constraints. Examples include: merge conflicts that violate invariants, validation failures at apply time that cannot be resolved within the strategy's scope, or resource state drift that contradicts strategy assumptions.
  • Behavior: The plan enters the constrained terminal state within the Apply phase. From this state, the plan may revert to Strategize either automatically (if the access_network automation profile flag allows it) or after manual user approval.
  • After reversion: The strategy actor receives the Apply phase's findings (the constraint violations, the specific issues encountered) and produces an updated decision tree. The plan then progresses forward again through Execute and Apply with the revised strategy.
  • Automation: Controlled by the access_network automation profile flag (separate from delete_content, which governs Execute → Strategize reversion).

Plan States (Per Phase)

A plan's ==phase== indicates "what step of the lifecycle it is in." Separately, the plan has a ==processing state== indicating "what is happening right now."

=== "Action Phase"

| State | Description |
| :---- | :---------- |
| `available` | Action exists and can be used |
| `archived` | Soft-deleted or hidden (optional) |

=== "Strategize / Execute Phases"

| State | Terminal? | Description |
| :---- | :-------: | :---------- |
| `queued` | No | Waiting for compute/worker |
| `processing` | No | Currently running |
| `errored` | Yes | Failed; includes error metadata |
| `complete` | Yes | Finished successfully |
| `cancelled` | Yes | User/system cancelled; safe terminal |

=== "Apply Phase"

| State | Terminal? | Description |
| :---- | :-------: | :---------- |
| `queued` | No | Waiting for compute/worker |
| `processing` | No | Currently running — diff review, conflict resolution, validation |
| `errored` | Yes | Failed; includes error metadata |
| `applied` | Yes | ==Changes successfully committed== to real resources |
| `constrained` | Yes | Cannot complete within current strategy's constraints; may trigger reversion to Strategize |
| `cancelled` | Yes | User/system cancelled; safe terminal |

Plan Identity and Traceability

Every plan should have:

Field Type Description
plan_id ULID ==Unique, immutable== identifier
parent_plan_id ULID? Nullable; present for child plans
root_plan_id ULID The top-most plan in the tree
attempt Integer Attempt counter (increments on phase re-run)
created_at Timestamp When the plan was created
updated_at Timestamp Last modification time
completed_at Timestamp? When the plan reached a terminal state
created_by Identity User or session identity

Plan Hierarchy and Parallelism

!!! adr "Architecture Decision" Plan hierarchy, child plan spawning, and parallel execution are covered in ADR-006: Plan Lifecycle.

A single plan should usually represent the smallest "complete" unit of work (similar to what would fit in one git commit). However:

  • Plans are hierarchical. A child plan is simply a Plan with a parent — it follows the same lifecycle, has the same data model, and is functionally identical to a root plan except that it has a parent_plan_id.
  • Decisions about child plans are made during Strategize (as subplan_spawn decision types). Multiple child plans that should execute concurrently are grouped under a subplan_parallel_spawn decision.
  • Child plans are actually spawned during Execute (based on those decisions).
  • Child plans can run in parallel (via subplan_parallel_spawn) or sequentially (via individual subplan_spawn decisions). Without a subplan_parallel_spawn wrapper, each subplan_spawn decision results in sequential execution.
  • Applicable invariants are enforced during Strategize by adding invariant_enforced decisions to the tree, which constrain downstream decisions and child plans.
  • The parent plan is responsible for merging results.

This is core to the long-term objective: tackling large tasks while only recomputing parts of the decision tree when corrected.

Hierarchical Decomposition for Scale

??? example "Example: Converting Firefox to Rust" When handling massive tasks, the system uses hierarchical decomposition. Each level spawns child plans (which are themselves full Plans with their own decision trees):

**1. Root Plan** — High-level architectural decisions and invariants

:   - "Convert Firefox Renderer to Rust"
    - Invariant: "Maintain API compatibility with existing C++ callers"
    - Decision: "Start with leaf modules, work inward"
    - Context: Module dependency graph (2,847 modules)

**2. Subsystem-Level Plans** — Major component decisions (`subplan_parallel_spawn`)

:   - "Phase 1: Convert utility libraries (no external deps)"
    - Each subsystem plan gets its own bounded context and inherits parent invariants

**3. Module-Level Plans** — Individual module conversions

:   - "Convert string_utils module"
    - Context: Only the 47 functions and 12 dependent files
    - Decision: "Use Rust's String type"

**4. File-Level Plans** — Specific file changes

:   - Actual code transformations
    - Minimal context needed

!!! tip
    At each level, only the ==relevant context== is loaded. The persistent decision graph means we can always reconstruct why we're converting a particular module and what constraints apply from higher-level decisions.
Child Plan Spawning Mechanism

In the actor definition for the execution actor, tool nodes can directly invoke registered tools to trigger child plans. The local/create-subplan tool is independently registered and referenced by name in the actor graph node. During execution, subplan_spawn decisions are realized as actual child plans, and subplan_parallel_spawn groups trigger concurrent spawning of all enclosed child plans.


# Example: Execution actor with subplan spawning capability.
# The graph uses a tool node referencing a named registered tool.
actors:
  code_executor:
    type: graph
    config:
      actor: anthropic/claude-3-opus
    skills:
      - local/plan-tools          # Skill containing create_subplan for LLM tool-calling
      - local/file-ops
    routes:
      execute_workflow:
        nodes:
          - name: spawn_test_subplan
            type: tool
            tool: local/create-subplan # Named tool from Tool Registry

The local/create-subplan tool is independently registered via its own YAML configuration file:


# File: tools/create-subplan.yaml
cleveragents:
  version: "3.0"

tool: name: local/create-subplan description: "Spawn a subplan for a given action" source: custom

input_schema: type: object properties: action: { type: string } target_files: { type: array, items: { type: string } } required: [action]

capability: writes: true checkpointable: false side_effects: [spawn_subplan]

code: | subplan = ctx.spawn_subplan( action=params["action"], target_files=params.get("target_files", []) ) return {"subplan_id": subplan.id}

The local/plan-tools skill references this tool (and others) by name:


# File: skills/plan-tools.yaml
skill:
  name: local/plan-tools
  description: "Tools for spawning and managing subplans"
  tools:
    - local/create-subplan
Child Plan Execution Modes

=== "Sequential"

Individual `subplan_spawn` decisions without a `subplan_parallel_spawn` wrapper execute one after another. If one fails, subsequent child plans are ==not started==.

=== "Parallel"

Multiple `subplan_spawn` decisions grouped under a `subplan_parallel_spawn` decision execute ==concurrently==. If one fails, others can continue. The `subplan_parallel_spawn` decision acts as a container that signals the system to spawn all enclosed child plans simultaneously.

Parallel execution is bounded by `SubplanConfig.max_parallel` (default: `5`, range: 150). This cap prevents runaway resource consumption when a large number of child plans are spawned simultaneously. The runtime uses a `ThreadPoolExecutor` with `min(max_parallel, len(subplans))` workers. The `SubplanConfig` model also controls `merge_strategy` (default: `git_three_way`), `fail_fast` (default: `false`), `timeout_per_subplan_seconds` (default: `null`), `retry_failed` (default: `true`), and `max_retries` (default: `2`).
Child Plan Failure Handling

!!! note "Failure Semantics" | Execution Mode | On Failure | Behavior | | :------------- | :--------- | :------- | | Parallel | One child fails | Other child plans ==continue== | | Sequential | One child fails | Subsequent child plans ==not started== |

!!! tip
    An "error" only occurs if an exception is thrown by the application (a bug). Plan failures (e.g., tests don't pass) are handled within the plan's logic, not as application errors.
Child Plan Result Merging

The way child plan results are merged depends on the resource type:

  • Git-compatible resources (source code, text files): Git-style merge
  • Databases: Transaction coordination or sequential application
  • Other resources: Pluggable merge strategies based on resource type
  • Non-mergeable resources: May require sequential execution only

The Plan "Decision Tree" and Visualization

!!! adr "Architecture Decision" The decision tree's dual structure (structural tree and influence DAG), versioning model, and historical reconstruction are defined in ADR-034: Decision Tree Versioning and History.

CleverAgents records enough information to render:

  • an ASCII tree in the TUI, and
  • optionally a GUI tree via visualization tools (D3/Cytoscape) once the data exists.

This implies each plan should persist:

  • decisions made,
  • the rationale (or at least the prompt/context snapshot that produced it),
  • dependencies ("this decision influenced these child plans").

This is required for "correcting plans" (see Behavior section).

Dual Structure: Structural Tree and Influence DAG

A plan's decisions form two overlapping structures that serve different purposes:

Structural Tree (parent_decision_id): Defines the hierarchical rendering order. Each decision has at most one parent. The prompt_definition decision is the root. This tree determines how agents plan tree renders the hierarchy.

Influence DAG (decision_dependencies table): Captures which decisions constrained or influenced which other decisions. A single decision may be influenced by multiple upstream decisions. For example, an implementation_choice might be influenced by both a strategy_choice (which set the approach) and a resource_selection (which determined the files to modify).

Structure Storage Purpose Used For
Structural tree parent_decision_id column Rendering, navigation, tree visualization agents plan tree, agents plan explain
Influence DAG decision_dependencies table Dependency tracking, impact analysis agents plan correct (affected subtree computation)

A decision's parent in the structural tree may differ from its dependencies in the influence DAG. The tree answers "what is this decision nested under?"; the DAG answers "what decisions caused this one to exist?"

Current Tree vs. Superseded Branches

The current tree for a plan is the set of active, non-superseded decisions:


SELECT * FROM decisions
WHERE plan_id = :plan_id
  AND superseded_by IS NULL
ORDER BY sequence_number;

When a decision is corrected, the original and all its descendants are marked superseded_by = <new_decision_id>. Superseded decisions are never deleted — they remain in the database as permanent records, forming an append-only history. agents plan tree --show-superseded renders both current and historical branches, with superseded decisions displayed dimmed and annotated.

Structural Invariants

The current tree always satisfies:

  1. Single root: Exactly one prompt_definition decision with parent_decision_id IS NULL.
  2. Reachability: Every non-root decision is reachable from the root via parent_decision_id links.
  3. Acyclicity: No circular references in parent_decision_id chains.
  4. Monotonic ordering: sequence_number is monotonically increasing per plan. New decisions (including corrections) always receive higher sequence numbers than existing ones. Gaps in the current tree's sequence numbers indicate where superseded decisions once existed.

Decision Data Model

!!! adr "Architecture Decision" The decision data model, decision types, and decision tree semantics are defined in ADR-007: Decision Tree and Correction.

Relationship Between Plan Description and Decisions

Each plan has a description field (inherited from the action's description, potentially with argument substitutions). This description acts as the primary component of the prompt fed to the strategy actor during the Strategize phase.

Decisions are choices that are NOT explicitly defined by the plan description. They represent the gaps, ambiguities, or implementation details that must be resolved to execute the plan.

For example:

  • Plan description: "Increase test coverage to 85%"
  • Decisions that emerge:
    • "Which modules should be prioritized?" (not specified in description)
    • "Should we use mocks or integration tests for the database layer?" (not specified)
    • "Should we refactor the auth module to make it more testable, or write tests around it as-is?" (not specified)
Decision Making Based on Autonomy Level

Who makes decisions depends on the plan's automation profile:

Profile Flag Who Makes Decisions
edit_code = 1.0 (always manual) User is prompted for each decision point during Strategize
edit_code = 0.0 (always automatic) Strategy actor makes decisions autonomously, records reasoning
execute_command = 1.0 (always manual) User is prompted for each decision point during Execute
execute_command = 0.0 (always automatic) Execution actor makes decisions autonomously

Intermediate thresholds (e.g., edit_code = 0.7) cause the system to auto-decide only when the Semantic Escalation confidence score meets or exceeds the threshold; otherwise the user is prompted.

When automation allows automatic decisions, the strategy actor uses its best judgment based on context, and records its reasoning in the decision's rationale field.

When user input is required, the system pauses and prompts the user:


Decision required: Which modules should be prioritized for test coverage?

Options identified by the strategy actor:

  1. auth module (currently 45% coverage, high risk)
  2. payment module (currently 52% coverage, high risk)
  3. user module (currently 71% coverage, medium risk)

Your choice (or provide custom guidance): _

The Prompt as the Root Decision

The prompt passed to the strategize actor is itself a decision node in the decision tree—specifically, it's the root decision of type prompt_definition.

This is important because:

  1. Every plan has its own prompt: The root plan's prompt comes from the action description + user arguments. Child plan prompts are created by parent plans during their execution.

  2. Parent plans create child plan prompts: When a parent plan spawns a child plan, it decides what prompt to give that child plan. This is recorded as a prompt_definition decision in the parent's tree, and becomes the root decision of the child plan's tree.

  3. Invariants flow into the decision tree: During Strategize, applicable invariants (from global, project, action, and plan scopes) are reconciled via the Invariant Reconciliation Actor and recorded as invariant_enforced decisions, making them explicit constraints that influence downstream decisions and child plans.

  4. Unified correction mechanism: Since the prompt, invariants, and all other decisions are part of the same tree, correcting any of them uses the same agents plan correct command.


Plan: 01KH29QDEE6DZTXKWNKCV8VP0F
├── [prompt_definition] "Increase test coverage to 85% for the whole project."
├── [invariant_enforced] "Prioritize all functionality related to financial transactions and user management"
├── [invariant_enforced] "All API calls over TCP must be mocked"
├── [strategy_choice] "Prioritize auth and payment modules, implement them in parallel before the rest"
├── [subplan_parallel_spawn] "Implement auth and payment modules, in parallel"
│   ├── [subplan_spawn] "Write tests for auth module"
│   │   └── Plan: 01KH29R8WPKPBHRY7Q0NA9XW86
│   │       ├── [prompt_definition] "Write unit tests for auth module using mocks for the remote API calls"
│   │       ├── [implementation_choice] "Test login flow first"
│   │       └── ...
│   └── [subplan_spawn] "Write tests for payment module"
│       └── Plan: 01KH29RN2YKSXMTBDG82AKRHRA
│           ├── [prompt_definition] "Write unit tests for payment module using"
│           └── ...
└── [subplan_parallel_spawn] "Write tests for all modules except the auth and payment modules"
    └──  ...
Correcting Decisions (Including Prompts)

All corrections use the same unified command:


agents plan correct <decision_id> --mode=<mode> --guidance "<corrected decision text>"

Parameters:

  • <decision_id>: The ULID of the decision to correct
  • --mode: Either revert (rollback and re-run) or append (add fix at end)
  • --guidance: Free-form text specifying what the correct decision should be

Examples:


# Correct a strategy choice
agents plan correct 01ARZ3NDEKTSV4RRFFQ69G5FAV --mode=revert \
  --guidance "Prioritize the payment module first, not auth, due to upcoming deadline"

# Correct the root prompt to be more specific agents plan tree <plan_id> # Shows: [prompt_definition] id=01ARZ3NDEKTSV4RRFFQ69G5FAV "Increase test coverage to 85%"

agents plan correct 01ARZ3NDEKTSV4RRFFQ69G5FAV --mode=revert </span> --guidance "Increase test coverage to 85%, prioritizing auth and payment modules. Use mocks for database tests, not integration tests."

# Correct a subplan's prompt (originally created by parent plan) agents plan correct 01BRZ4PDFLUTW5SSGR70H6GBW --mode=revert </span> --guidance "Write unit tests for auth module, focusing on edge cases for token expiration"

# Append a fix rather than rewriting history agents plan correct 01ARZ3NDEKTSV4RRFFQ69G5FAV --mode=append </span> --guidance "The previous approach missed error handling tests - add comprehensive error path coverage"

# Remove an invariant that shouldn't apply agents plan correct 01CRZ5QEHMVUX6TTHR81I7HCX --mode=revert </span> --guidance "Remove this invariant - TCP mocking is not needed for this module since it has no network calls"

# Add a missing invariant to the plan agents invariant add --plan 01HXM8C2ZK4Q7C2B3F2R4VYV6J "All database queries must use parameterized statements"

Note: CLI commands should not require interactive input. The --guidance parameter provides the correction inline.

When to correct the prompt vs. a specific decision:

Situation Correction Approach
Original request was too vague Correct the prompt_definition decision
Strategy actor made a bad choice on a specific question Correct that specific decision
Parent plan gave a child plan a bad prompt Correct the child plan's prompt_definition
An invariant should not apply to this plan Correct (remove) the invariant_enforced decision
A missing constraint should be added Add a new invariant_enforced decision via agents invariant add --plan or correct the plan's strategy
Sequential child plans should run in parallel Correct the relevant decisions to use a subplan_parallel_spawn grouping

Because the prompt is part of the decision tree, the system automatically knows that correcting it invalidates all downstream decisions in that plan (and its child plans).

Decisions are created during both the Strategize and Execute phases. The Strategize phase produces the initial decision tree — strategy choices, invariant enforcement, resource selections, and child plan blueprints. The Execute phase may create additional decisions constrained by those made during Strategize, reflecting discoveries and adjustments that arise during execution. If Execute finds that Strategize-phase constraints are too restrictive to proceed, the plan reverts to Strategize for adjustment (see Phase Reversion below). The decision tree captures what choices were made and why, enabling correction and replay.

Decision Recording Protocol

!!! adr "Architecture Decision" The decision recording protocol, tool-based recording mechanism, and context snapshot capture are defined in ADR-033: Decision Recording Protocol. Decision-aligned checkpointing during Execute is defined in ADR-035: Decision Tree Rollback and Replay.

Actors record decisions by calling a built-in record_decision tool. This tool is automatically included in every actor's skill set for plan-related invocations. Decision types are partitioned into three creation categories:

Category Decision Types Creator Mechanism
System-created prompt_definition, user_intervention Plan lifecycle engine Created automatically at plan instantiation (prompt) and when user provides guidance (intervention)
Reconciliation-actor-recorded invariant_enforced Invariant Reconciliation Actor The actor evaluates applicable invariants, resolves conflicts using precedence rules, and calls record_decision for each enforced invariant
Actor-recorded strategy_choice, resource_selection, subplan_spawn, subplan_parallel_spawn, implementation_choice, tool_invocation, error_recovery, validation_response Strategy or Execution actor Actor identifies a choice point and calls record_decision

The record_decision tool accepts the decision type, question, chosen option, alternatives considered, confidence score, and rationale. The system automatically captures the context snapshot at the moment of the call — the hot context hash, resource references, and a LangGraph checkpoint of the actor's complete state (actor_state_ref). This snapshot enables replay from the exact decision point during correction.

Strategize-phase recording loop: The strategy actor's system prompt instructs it to identify ambiguities and choice points in the plan description. For each choice point, the actor gathers context, evaluates options, and calls record_decision. Each recorded decision becomes part of the actor's context for subsequent reasoning.


# Pseudocode: Strategy actor decision recording loop
while unresolved_ambiguities_remain(plan, context):
    choice_point = analyze_context_for_ambiguity(context)
    options = generate_and_evaluate_options(choice_point, context, invariants)
<span style="opacity: 0.7;"># Record via tool call — system auto-captures context snapshot</span>
decision_id = record_decision(
    decision_type = choice_point.type,
    question = choice_point.question,
    chosen_option = best_option.description,
    alternatives_considered = [o.description <span style="color: magenta; font-weight: 600;">for</span> o <span style="color: magenta; font-weight: 600;">in</span> rejected_options],
    confidence_score = best_option.confidence,
    rationale = best_option.reasoning
)

context.add_decision(decision_id)  <span style="opacity: 0.7;"># Decision informs subsequent reasoning</span>

Execute-phase recording pattern: The execution actor sees Strategize-phase decisions as read-only constraints. When it encounters a choice point not already resolved by the strategy (implementation detail, error handling, tool selection), it calls record_decision. Execute-phase decisions must be consistent with Strategize-phase constraints — if they cannot be, the plan reverts to Strategize rather than recording a contradictory decision.

!!! warning "Execute-Phase Checkpoint Trigger" In the Execute phase, record_decision is treated as a write operation for checkpoint purposes. The system automatically creates a coordinated checkpoint group — one checkpoint per modified sandbox resource — at every Execute-phase decision point. This creates a 1:1 mapping between decisions and resource states, enabling fine-grained rollback to any Execute-phase decision (see Correcting Plans in the Behavior section).

Automation profile integration: When record_decision is called, the system checks the confidence score against the applicable threshold (edit_code or execute_command). If confidence is below the threshold, execution pauses and the decision is presented to the user for approval or override.

Decision Record Structure

Decision:
  # Identity
  decision_id: ULID # Unique identifier
  plan_id: ULID # Parent plan this decision belongs to
  parent_decision_id: ULID | null # Parent decision (for tree structure)
  sequence_number: int # Order within the plan's decisions

# Classification decision_type: enum - prompt_definition # The prompt/description for this plan (root decision) - invariant_enforced # An invariant (from global, project, action, or plan scope) applicable to this plan, added as a constraint - strategy_choice # High-level approach decision during Strategize - implementation_choice # How to implement a specific task - resource_selection # Which resources to read/modify - subplan_spawn # Decision to create a child plan (spawned later in Execute) - subplan_parallel_spawn # Decision to spawn a group of child plans in parallel (contains subplan_spawn children) - tool_invocation # Which skill/tool to use - error_recovery # How to handle a failure - validation_response # Response to validation failure - user_intervention # User provided guidance/correction

# The Decision Itself question: str # What question was being answered chosen_option: str # What was decided alternatives_considered: list[str] # Other options that were evaluated confidence_score: float | null # 0.0-1.0 if the actor provided confidence

# Context Snapshot (for replay) context_snapshot: hot_context_hash: str # Cryptographic hash of the exact context hot_context_ref: str # Pointer to the full stored snapshot relevant_resources: list[ResourceRef] # Every file/symbol that influenced this decision actor_state_ref: str # Complete LangGraph checkpoint

# When the system decides "refactor the authentication module to use async patterns," # it permanently records: # - Which files were examined to make that decision # - What symbols and dependencies were traced # - The exact code state that was analyzed # - The reasoning chain that led to this choice # - Alternative approaches that were considered but rejected

# Rationale rationale: str # Why this option was chosen actor_reasoning: str | null # Raw LLM reasoning if available

# Downstream Impact (populated during Execute phase) downstream_decision_ids: list[ULID] # Decisions that depend on this one downstream_plan_ids: list[ULID] # Child plans spawned because of this decision artifacts_produced: list[ArtifactRef] # Files/outputs created under this decision

# Timestamps created_at: datetime

# Correction Metadata is_correction: bool # Was this decision a correction of another? corrects_decision_id: ULID | null # If correction, which decision was replaced correction_reason: str | null # Why the correction was made superseded_by: ULID | null # If this decision was later corrected

Decision Timing
Phase Decision Activity
Strategize Initial decisions are created, including invariant_enforced decisions for applicable invariants, strategy_choice decisions, subplan_spawn decisions, and subplan_parallel_spawn decisions grouping parallel child plans. downstream_plan_ids is empty. The system aims to make reasonable, comprehensive decisions at this stage to guide execution.
Execute Child plans are spawned and downstream_plan_ids is populated based on subplan_spawn decisions. Additional decisions may be created during execution — these are constrained by Strategize-phase decisions and arise from discoveries made during execution (e.g., implementation_choice, resource_selection, tool_invocation, error_recovery). If Execute determines that Strategize-phase constraints are too restrictive, the plan reverts to Strategize for adjustment rather than overriding those constraints.
Apply No new decisions. History can be flagged for cleanup after successful apply.
Decision Tree Storage Schema

-- Core decision table
CREATE TABLE decisions (
    decision_id TEXT PRIMARY KEY,      -- ULID
    plan_id TEXT NOT NULL,
    parent_decision_id TEXT,
    sequence_number INTEGER NOT NULL,
    decision_type TEXT NOT NULL,        -- prompt_definition, invariant_enforced, strategy_choice,
                                       -- implementation_choice, resource_selection, subplan_spawn,
                                       -- subplan_parallel_spawn, tool_invocation, error_recovery,
                                       -- validation_response, user_intervention
    question TEXT,
    chosen_option TEXT NOT NULL,
    alternatives_considered TEXT,      -- JSON array
    confidence_score REAL,
    rationale TEXT,
    actor_reasoning TEXT,
    context_snapshot TEXT NOT NULL,    -- JSON blob
    is_correction BOOLEAN DEFAULT FALSE,
    corrects_decision_id TEXT,
    correction_reason TEXT,
    superseded_by TEXT,
    created_at TEXT NOT NULL,
<span style="color: #5599ff; font-weight: 600;">FOREIGN</span> <span style="color: #5599ff; font-weight: 600;">KEY</span> (plan_id) <span style="color: #5599ff; font-weight: 600;">REFERENCES</span> plans(plan_id),
<span style="color: #5599ff; font-weight: 600;">FOREIGN</span> <span style="color: #5599ff; font-weight: 600;">KEY</span> (parent_decision_id) <span style="color: #5599ff; font-weight: 600;">REFERENCES</span> decisions(decision_id),
<span style="color: #5599ff; font-weight: 600;">FOREIGN</span> <span style="color: #5599ff; font-weight: 600;">KEY</span> (corrects_decision_id) <span style="color: #5599ff; font-weight: 600;">REFERENCES</span> decisions(decision_id),
<span style="color: #5599ff; font-weight: 600;">FOREIGN</span> <span style="color: #5599ff; font-weight: 600;">KEY</span> (superseded_by) <span style="color: #5599ff; font-weight: 600;">REFERENCES</span> decisions(decision_id)

);

-- Downstream relationships (many-to-many for DAG) CREATE TABLE decision_dependencies ( upstream_decision_id TEXT NOT NULL, downstream_decision_id TEXT NOT NULL, dependency_type TEXT NOT NULL, -- 'decision', 'plan', 'artifact' downstream_ref TEXT NOT NULL, -- The actual ID of decision/plan/artifact

<span style="color: #5599ff; font-weight: 600;">PRIMARY</span> <span style="color: #5599ff; font-weight: 600;">KEY</span> (upstream_decision_id, downstream_decision_id, downstream_ref),
<span style="color: #5599ff; font-weight: 600;">FOREIGN</span> <span style="color: #5599ff; font-weight: 600;">KEY</span> (upstream_decision_id) <span style="color: #5599ff; font-weight: 600;">REFERENCES</span> decisions(decision_id)

);

-- Correction history CREATE TABLE correction_attempts ( attempt_id TEXT PRIMARY KEY, -- ULID plan_id TEXT NOT NULL, original_decision_id TEXT NOT NULL, new_decision_id TEXT, original_subtree_snapshot TEXT, -- Reference to archived state correction_reason TEXT, status TEXT NOT NULL, -- 'pending', 'executing', 'completed', 'failed' created_at TEXT NOT NULL, completed_at TEXT,

<span style="color: #5599ff; font-weight: 600;">FOREIGN</span> <span style="color: #5599ff; font-weight: 600;">KEY</span> (plan_id) <span style="color: #5599ff; font-weight: 600;">REFERENCES</span> plans(plan_id),
<span style="color: #5599ff; font-weight: 600;">FOREIGN</span> <span style="color: #5599ff; font-weight: 600;">KEY</span> (original_decision_id) <span style="color: #5599ff; font-weight: 600;">REFERENCES</span> decisions(decision_id),
<span style="color: #5599ff; font-weight: 600;">FOREIGN</span> <span style="color: #5599ff; font-weight: 600;">KEY</span> (new_decision_id) <span style="color: #5599ff; font-weight: 600;">REFERENCES</span> decisions(decision_id)

);

Action

!!! adr "Architecture Decision" Actions as reusable plan templates and the action-to-plan transition are defined in ADR-006: Plan Lifecycle.

What an Action Is

An action is a reusable plan template that is not associated with any projects yet.

Actions are created via CLI commands with a required --config YAML file that fully defines the action. Any CLI options supplied alongside the config file act as optional overrides for values in the YAML.

Examples:

  • "Increase test coverage to 80%"
  • "Refactor module X to be async-safe"
  • "Write an RFC for feature Y"
  • "Provision an infra cluster and validate access" (non-code)

Actions are intentionally project-agnostic so they can be reused across projects.

An action is the first stage of a plan — before it is used. Because of this, invariants can be attached to actions. When an action is used (via agents plan use), any invariants attached to the action are carried forward as plan-level invariants on the resulting plan. This allows teams to bake constraints directly into reusable templates. Invariants can also be added to a plan after the action is used, via agents invariant add --plan or --invariant flags on agents plan use.

Action Creation (CLI)

Actions are created using the CLI. The action is fully defined by the YAML configuration file:


agents action create --config ./actions/code-coverage.yaml

The YAML file provides the complete action definition, including the name, strategy-actor, execution-actor, definition-of-done, arguments, invariants, and all other properties.

Required parameters:

  • --config: YAML configuration file that fully defines the action (name, strategy-actor, execution-actor, definition-of-done, etc.)

Arguments defined in the YAML's args section are values that will be:

  • Injected into the description and/or definition of done (via templating)
  • Passed into the context of the actors
  • Required when using the action on projects

Action Data Model (Expanded)

A plan in the Action phase has:

1) name (namespaced)

Format:

  • [server:][namespace/]<name>

Rules:

  • If server is omitted, default server is assumed unless namespace is local.
  • If namespace is omitted, default is local.
  • Names should be stable identifiers (kebab-case recommended).

Examples:

  • local/code-coverage
  • myusername/code-coverage
  • myorgname/code-coverage
  • prod:myorgname/code-coverage (server-qualified)
2) short_description

Optional at creation; auto-filled if blank.

3) long_description

Optional but recommended for reusable actions.

4) definition_of_done (DoD)

Required. Must be explicit and testable.

5) actors

Two actors minimum, two optional:

  • strategy_actor (planner/architect) — required
  • execution_actor (builder/implementer) — required
  • estimation_actor (cost/risk estimator) — optional
  • invariant_actor (Invariant Reconciliation Actor) — optional; resolves invariant conflicts when the plan enters Strategize. Lookup falls back to project, then global config.

Actors can be:

  • an LLM agent (built-in),
  • a graph (custom yaml, or built-in),

Note: graphs are hierarchical allowing them to reference other actors as nodes.

Actor abstraction is central: an actor may be a single agent or an entire graph.

6) reusable (boolean)
  • Default: true.
  • If true: using the action creates a new plan in Strategize while leaving the action available.
  • If false: action self-deletes (or auto-archives) after first use.
7) read_only (boolean)
  • Default: false.
  • If true: the plan must only use read-only tools (tools with read_only: true in their capability metadata) and must never modify resources (even in sandbox).
  • Read-only actions are still useful for "investigation reports," architecture reviews, or dry-run planning.

To make actions genuinely reusable, actions should declare their inputs:

  • required args (e.g., target coverage percent),
  • optional args (e.g., test framework),
  • validation rules (types, bounds).

Example:

  • target_coverage_percent: integer 0100
9) automation_profile

The resolved automation profile name for this plan (e.g., trusted, auto, local/careful-auto). Determined at plan use time using the profile precedence rules (plan > action > project > global). Once set, it is locked to the plan.

Strategy (Strategize Phase)

!!! adr "Architecture Decision" The Strategize phase, strategy actors, and decision generation are defined in ADR-006: Plan Lifecycle.

Using an Action (Transition to Strategize)

The use command transitions an Action into the Strategize phase by applying it to one or more projects:


# Basic usage
agents plan use local/code-coverage my-api-service

# Multiple projects agents plan use local/schema-update </span> api-service </span> web-frontend </span> mobile-app

# With action arguments agents plan use local/code-coverage </span> my-api-service </span> --arg target_coverage_percent=85 </span> --arg test_framework=pytest

# With explicit automation profile agents plan use local/deploy-action </span> staging-env </span> --automation-profile manual

# With invariants attached at use time agents plan use local/code-coverage </span> my-api-service </span> --arg target_coverage_percent=85 </span> --invariant "All API calls over TCP must be mocked" </span> --invariant "Do not modify the payments module"

Parameters:

  • <PROJECT>: Project to apply the action to (positional, can be repeated for multi-project plans)
  • --arg: Action argument values (format: name=value)
  • --automation-profile: Override automation profile for this plan
  • --invariant: Invariant to attach to the created plan (can be repeated). These are added as plan-level invariants in addition to any inherited from the action, project, or global scope.

When the action is used:

  1. A new plan is created with a unique ULID
  2. The plan's automation profile is resolved (plan > action > project > global precedence)
  3. Any invariants from the action are carried forward as plan-level invariants, combined with any --invariant flags provided
  4. The plan enters the Strategize phase
  5. The Invariant Reconciliation Actor computes the effective invariant view (resolving conflicts using plan > project > global precedence)
  6. The strategy_actor begins analyzing the project(s)

What Strategize Does

When an action is used on projects, it becomes a plan in Strategize.

Strategize is:

  • read-only, producing a plan of attack,
  • responsible for gathering context from project resources,
  • responsible for collecting applicable invariants (from global, project, action, and plan scopes), computing the effective invariant view via the Invariant Reconciliation Actor (applying plan > project > global precedence), and recording them as invariant_enforced decisions,
  • responsible for generating a strategy and child plan blueprint (using subplan_spawn and subplan_parallel_spawn decisions),
  • not allowed to execute child plans or modify resources.

This "architect vs coder" separation is explicitly described as a core motivation.

Resource-aware dependency analysis: During the Strategize phase, the strategy actor employs specialized mechanisms to compute precise dependency closures:


# Pseudocode of what happens inside a strategy actor
def compute_closure_for_refactoring(target_module):
    closure = ResourceClosure()
<span style="opacity: 0.7;"># Direct file dependencies</span>
closure.add_files(find_imports(target_module))
closure.add_files(find_includes(target_module))

<span style="opacity: 0.7;"># Symbol dependencies</span>
<span style="color: magenta; font-weight: 600;">for</span> symbol <span style="color: magenta; font-weight: 600;">in</span> extract_exported_symbols(target_module):
    closure.add_files(find_symbol_usage(symbol, scope=<span style="color: #66cc66;">&#x27;project&#x27;</span>))

<span style="opacity: 0.7;"># Test dependencies</span>
closure.add_files(find_tests_for_module(target_module))

<span style="opacity: 0.7;"># Build system dependencies</span>
closure.add_files(find_build_references(target_module))

<span style="color: magenta; font-weight: 600;">return</span> closure

The system leverages several key insights:

  • Modular boundaries exist: Even in legacy codebases, there are natural boundaries
  • Changes are incremental: We don't convert 50,000 files atomically
  • Dependencies are sparse: Most modules depend on a small fraction of the codebase
  • Interfaces are narrow: Public APIs are much smaller than implementations

Strategize Data Model

A plan in Strategize contains all Action fields plus:

1) projects

A list of projects the plan is used on.

Important: A strategy plan may target multiple projects. Multi-project work in one "window" is considered a major usability advantage over tools that require being run from a single directory.

2) strategy_context

A structured object describing:

  • what resources were considered,
  • how they were retrieved,
  • what filtering/limits were applied,
  • what the actor saw.

This matters because a plan must be debuggable and correctable later.

Recommended fields:

  • resource_refs: IDs of resources used
  • queries: search queries performed
  • selected_chunks: chunk IDs + sources + reasons
  • constraints: context window limits, file ignore patterns
  • generated_summaries: if summarization occurred
3) strategy

The output plan:

  • steps (ordered and/or DAG),
  • conditions/branches ("if tests fail, do X"),
  • child plans to spawn (including which action templates to use, and whether they should run in parallel via subplan_parallel_spawn),
  • evaluation criteria (how to know success),
  • risk assessment.

Strategize should output not only narrative text but also a machine-usable blueprint:

  • list of tasks,
  • required skills,
  • expected outputs,
  • dependencies between tasks.

This blueprint becomes the input to Execute.

5) cost_estimate and risk_estimate (optional)

Cost and risk estimation is optional but recommended for production use.

When enabled, a specialized estimation actor analyzes:

  • The initial prompt/request
  • The strategy produced by the Strategize phase
  • Historical data from similar plans (if available)

And produces estimates for:

  • LLM tokens/cost range
  • Number of steps/child plans expected
  • Expected risk of rollbacks
  • Estimated execution time

Implementation: Similar to how there's a strategy_actor and execution_actor for each action, there can be an optional estimation_actor whose entire job is cost/risk estimation. This actor runs after Strategize completes (before Execute) and its output is informational only. Estimation failures are logged but never block the Execute transition.

Estimation actor resolution follows a 4-level fallback chain (highest priority first):

  1. CLI --estimation-actor override (applied post-creation by the CLI layer)
  2. Action YAML estimation_actor field
  3. (project-scoped key — reserved for future use)
  4. actor.default.estimation global config key (CLEVERAGENTS_DEFAULT_ESTIMATION_ACTOR)

When the resolved actor is set, PlanLifecycleService.complete_strategize() invokes _run_estimation() and emits a PLAN_ESTIMATION_COMPLETE event on success. The result is stored on the plan and plan.cost_estimate_usd is populated from the midpoint of the estimated cost range.


# Example: Action with estimation actor (defined in the YAML config file)
agents action create --config ./actions/expensive-refactor.yaml

This becomes critical in server/multi-user usage and cost controls.

Execution (Execute Phase)

!!! adr "Architecture Decision" The Execute phase, sandbox model, and execution safety mechanisms are defined in ADR-006: Plan Lifecycle and ADR-015: Sandbox and Checkpoint.

What Execute Does

Execute is where the plan actually performs work, but in a sandboxed environment that can later be reviewed and applied.

Key properties:

  1. Work happens in a sandbox All file modifications, generated artifacts, and intermediate outputs live in an isolated "execution workspace" until Apply.

  2. Execute may spawn child plans Child plan spawning is a first-class behavior of Execute: a parent plan can distribute work to child plans (sequentially via subplan_spawn or concurrently via subplan_parallel_spawn) and merge results.

  3. Execute must support checkpointing / rollback (when enabled) Checkpointable tools allow rolling back to a checkpoint ID to recover from partial failure or wrong turns.

  4. Execute produces a "reviewable diff" Diff review sandbox is described as a differentiating feature: users can inspect changes before applying.

Execution Workspace / Sandbox Model (Detailed)

!!! adr "Architecture Decision" Sandbox isolation, lazy sandboxing, and resource-type-specific sandbox strategies are detailed in ADR-015: Sandbox and Checkpoint.

A sandbox isolates plan execution from the real project resources until Apply.

!!! tip "Key Sandbox Principles" - [x] Lazy Sandboxing — Resources are sandboxed ==only when accessed==, not upfront - [x] Per-Plan Sandboxes — Each plan and child plan has its own isolated sandbox - [x] Resource-Defined Strategy — The sandbox strategy is defined on each resource, not globally - [x] Cleanup Behavior — Automatic cleanup on exit, crash recovery on next run, retention-based archival

??? info "Why Lazy Sandboxing?" A project may have many resources (git repo + 10 databases + cloud accounts), but a plan may only modify one resource. Only accessed resources are sandboxed, making this efficient for large projects with many linked resources.

Sandbox Implementation Strategies

Different resource types require different sandbox strategies:

Resource Type Strategy Rollback Mechanism
git-checkout git_worktree Git reset/checkout
git none N/A (represents a repo instance — not directly sandboxable)
fs-mount copy_on_write or overlay Restore from snapshot
fs-directory copy_on_write Restore from snapshot
Custom database types transaction_rollback Transaction rollback
Custom API types none Often not sandboxable

1. Git worktree / branch sandbox (preferred for code)

  • Create a worktree or temporary branch
  • All modifications are commits or staged changes
  • Apply merges/cherry-picks

Pros: natural rollback, diff support, efficient Cons: requires git

2. Filesystem copy sandbox

  • Copy project directory to a sandbox directory
  • Execute modifies sandbox copy
  • Apply syncs diff back

Pros: simple Cons: expensive for huge repos

3. Overlay filesystem sandbox

  • Use overlayfs-style "copy-on-write" to avoid full copies

Pros: efficient Cons: more complex, OS-dependent

4. Transaction-based sandbox (for databases)

  • Begin transaction at sandbox creation
  • All operations within transaction
  • Rollback on failure, commit on apply

Pros: native to databases Cons: long-running transactions can cause issues

5. No sandbox (for non-sandboxable resources)

  • Some resources cannot be sandboxed (certain APIs, cloud services)
  • User proceeds at their own risk
  • Plan should warn about non-sandboxable resources
Multi-Resource Sandboxing

When a plan accesses multiple resources:

  • Each resource gets its own sandbox (based on its defined strategy)
  • Sandboxes are independent
  • Apply commits each sandbox separately
  • If any sandbox Apply fails, others may still succeed (partial apply)

Complete isolation during execution prevents compound errors: Each plan executes in its own sandbox, which means:


Plan A (refactoring auth module):
- Sandbox A1: Contains only auth/*.cpp, auth_tests/*.cpp
- Cannot see Plan B's intermediate states
- Cannot accidentally depend on Plan B's half-done work

Plan B (updating API endpoints):

  • Sandbox B1: Contains only api/.cpp, api_tests/.cpp
  • Makes changes assuming current auth interface
  • Protected from Plan A's intermediate refactoring

Hierarchical merge resolution: When child plans complete, the parent plan performs intelligent merging:


def merge_subplan_results(subplan_results):
    # Group by resource type
    by_resource = group_by_resource_type(subplan_results)
<span style="opacity: 0.7;"># Apply resource-specific merge strategies</span>
<span style="color: magenta; font-weight: 600;">for</span> resource_type, changes <span style="color: magenta; font-weight: 600;">in</span> by_resource:
    <span style="color: magenta; font-weight: 600;">if</span> resource_type == <span style="color: #66cc66;">&#x27;git-checkout&#x27;</span>:
        merge_git_changes(changes)  <span style="opacity: 0.7;"># Three-way merge</span>
    <span style="color: magenta; font-weight: 600;">elif</span> resource_type == <span style="color: #66cc66;">&#x27;fs-mount&#x27;</span>:
        merge_fs_changes(changes)   <span style="opacity: 0.7;"># Copy-on-write reconciliation</span>
    <span style="color: magenta; font-weight: 600;">elif</span> resource_type.startswith(<span style="color: #66cc66;">&#x27;database&#x27;</span>):
        merge_db_changes(changes)   <span style="opacity: 0.7;"># Sequential application</span>

<span style="opacity: 0.7;"># Validate merged state</span>
run_integration_tests()

Execution Data Model

A plan in Execute contains:

1) execution_context

The context used for execution (often smaller/more tactical than strategy context).

2) execution_log

Structured timeline of:

  • tool calls (with parent skill noted),
  • actor calls,
  • outputs,
  • errors and retries,
  • checkpoints created.

This log is essential for debugging.

3) artifacts

Outputs produced:

  • changed files,
  • generated files,
  • reports,
  • diagrams,
  • test outputs,
  • diffs.
4) sandbox_ref

Pointer to the sandbox location/state:

  • path, branch name, workspace ID, container ID, etc.
5) checkpoint_graph (if enabled)

A record of checkpoints:

  • checkpoint ID
  • timestamp
  • tool responsible (and its parent skill)
  • resources affected
  • rollback instructions / metadata

Checkpointing in Execute (Core Safety Mechanism)

!!! adr "Architecture Decision" Checkpointing, rollback, and transaction safety are defined in ADR-015: Sandbox and Checkpoint.

The intended user-level behavior is:

  • "Give me a checkpoint ID."
  • Perform additional operations.
  • "Roll back to checkpoint X."

Not all tools can support this; checkpointing must be declared per tool (in its capability metadata).

Tool-level checkpointability

Each tool declares (via its capability metadata):

  • checkpointable: true|false
  • checkpoint_scope: what granularity of rollback is supported (file, transaction, commit, snapshot)
  • rollback_mechanism: how rollback occurs

Examples:

  • File tools (from a file-ops skill): snapshot file states pre-modification
  • Git tools (from a git-ops skill): create commit or stash; rollback is reset/checkout
  • Shell/CLI tools (running inside a container): rollback by restoring filesystem snapshot or reloading base image state

It should be noted that checkpointing is easier when tool scope is constrained (e.g., "only files within a docker image + git").

Plan-level rollback policy

Plans should have an option:

  • rollback_enabled: true|false

If disabled, the plan may use more generic/unsafe tools with fewer restrictions (useful for low-stakes tasks).

CheckpointService Operations

The CheckpointService provides the following key operations for checkpoint lifecycle management:

  • create_checkpoint(plan_id, sandbox_ref, reason, source_tool, phase, decision_id, checkpoint_type, size_bytes) — Creates a standard checkpoint record. The sandbox_ref is a git commit hash of the current HEAD.

  • create_workspace_snapshot(plan_id, sandbox_ref, decision_id, *, reason, phase) — Creates a diff-based workspace snapshot before decision execution. Captures only files that differ from the previous checkpoint (or the initial sandbox state when no prior checkpoints exist). The diff manifest is stored in checkpoint metadata under "diff_paths", "diff_based", and "diff_hash" keys. Returns the new Checkpoint.

  • selective_rollback(plan_id, checkpoint_id) — Rolls back to a specific checkpoint with atomic (all-or-nothing) semantics. If the rollback fails partway through, a best-effort recovery attempt restores the sandbox to its pre-rollback HEAD. Raises BusinessRuleViolation if recovery also fails. Raises ResourceNotFoundError if the checkpoint does not exist.

  • archive_artifacts(sandbox_path, artifact_paths, archive_dir) — Physically moves artifact files to an archive directory, preserving them outside the sandbox for later retrieval.

The service supports both database-backed persistence (via CheckpointRepository) and in-memory fallback for tests. Guard checks (whether a plan has been applied, whether a sandbox exists) are resolved via PlanLifecycleService when wired, querying persistent plan state rather than relying on in-memory flags.

Execution should be treated like a transactional pipeline:

  • Each step either:

    • commits a checkpoint on success, or
    • rolls back to the previous checkpoint on failure.

This is explicitly motivated by "partial failure leaves codebase inconsistent" and the need for transaction rollback.

Execution Environment Routing

!!! adr "Architecture Decision" Execution environment routing, devcontainer integration, and the 6-level precedence chain are defined in ADR-043: Devcontainer Integration. Container resource types are defined in ADR-039: Container Resource Types.

When a plan enters the Execute phase, the runtime must determine where each tool invocation runs: on the host, or inside a container. This decision is called execution environment routing. The router evaluates a 6-level precedence chain and selects the first matching environment.

Precedence Chain (highest → lowest):

Priority Source Condition
1 Plan-level execution_environment with priority: override Always wins when set. Configured via agents plan use --execution-environment … --execution-env-priority override.
2 Project-level execution_environment with priority: override Wins unless a plan-level override exists. Configured via agents project context set --execution-environment … --execution-env-priority override.
3 Nearest-ancestor devcontainer Auto-detected devcontainer-instance resource that is a child (or descendant) of a resource linked to the project. Lazy-built on first access.
4 Plan-level execution_environment with priority: fallback Used only when no devcontainer is detected and no override exists above.
5 Project-level execution_environment with priority: fallback Used only when no closer-scoped environment exists.
6 Host The local operating system. Default when no container environment is configured or detected.

Routing Algorithm:

function resolve_execution_environment(plan, project):
    # Level 1: plan override
    if plan.execution_environment AND plan.execution_env_priority == "override":
        return plan.execution_environment

    # Level 2: project override
    if project.execution_environment AND project.execution_env_priority == "override":
        return project.execution_environment

    # Level 3: nearest-ancestor devcontainer
    for resource in project.linked_resources (ordered by DAG depth, shallowest first):
        devcontainer = find_child_of_type(resource, "devcontainer-instance")
        if devcontainer:
            if devcontainer.state == "detected":
                build_and_start(devcontainer)   # lazy activation
            return devcontainer

    # Level 4: plan fallback
    if plan.execution_environment AND plan.execution_env_priority == "fallback":
        return plan.execution_environment

    # Level 5: project fallback
    if project.execution_environment AND project.execution_env_priority == "fallback":
        return project.execution_environment

    # Level 6: host
    return HOST_ENVIRONMENT

Tool-Level Environment Preferences:

Individual tools may declare environment preferences via the environment field in their capability metadata (see §Tool Capability Metadata):

  • environment.required: Tool must run in this environment type (container or host). If the resolved environment doesn't match, the tool invocation fails with an error rather than silently running in the wrong environment.
  • environment.preferred: Tool prefers this environment type but will run wherever the router places it.
  • environment.specific: Tool targets a specific named resource (e.g., local/api-dev). If the named resource is available and running, the tool is routed there regardless of the general precedence chain.

When a tool declares environment.required: container but the router resolves to host, the runtime MUST raise an error. The operator can then either: (a) add a container resource to the project, or (b) change the tool's environment requirement.

Lazy Activation:

Devcontainers detected during auto-discovery are created in detected (not built) state. They are built and started only when the execution environment router first selects them. This avoids unnecessary container builds when the user never executes a plan that needs container isolation. Once built, the container remains available for subsequent plan executions until explicitly stopped or the resource is removed.

Tool-Based Resource Modification (Modern Architecture)

!!! adr "Architecture Decision" The tool-based resource modification approach and tool execution model are defined in ADR-011: Tool System.

IMPORTANT: CleverAgents does NOT parse LLM output to extract code. Instead, it uses the modern tool-based approach pioneered by Claude Code, Cursor, and Aider where:

  1. LLMs call tools directly (edit_file(), write_file(), delete_file(), etc.) — tools provided by referenced skills
  2. Tools operate on the sandbox - each tool invocation modifies sandbox state directly
  3. ChangeSet is built from tool invocations - not by parsing LLM text output
  4. Validation runs on sandbox state - after tools execute, not on parsed output

This architecture provides:

  • Atomic operations: Each tool call is a discrete, trackable change
  • No parsing ambiguity: Tools have structured parameters (path, content, etc.)
  • Resource-agnostic: Same pattern works for files, databases, APIs, any resource type
  • Safety by design: Tools run in sandbox with defined capabilities and restrictions
  • MCP compatibility: Tools from MCP-based skills map directly to MCP tools for external integrations
How It Works
sequenceDiagram
    participant LLM as LLM Agent
    participant Router as Tool Router
    participant Sandbox as Sandbox
    participant CS as ChangeSet
    participant Val as Validator

    LLM->>Router: Tool call (with parameters)
    Router->>Router: Validate parameters
    Router->>Router: Enforce capability restrictions
    Router->>Sandbox: Execute tool in sandbox
    Sandbox->>Sandbox: Operate on sandboxed state
    Sandbox->>Sandbox: Record invocation
    Sandbox->>Sandbox: Create checkpoint (if needed)
    Sandbox->>CS: Emit Change record
    CS->>CS: Accumulate into ChangeSet
    CS->>Val: Submit for validation
    Val->>Val: Run validators on sandbox state
    Val->>Val: Generate diff from ChangeSet
    Val-->>LLM: Present for review before Apply
Built-in Resource Tools

CleverAgents provides these core tools (via built-in skills) for resource manipulation:

Tool Description Creates Change?
read_file(path) Read file contents No
write_file(path, content) Create/overwrite file Yes
edit_file(path, changes) Apply targeted edits Yes
delete_file(path) Remove file Yes
move_file(src, dst) Rename/move file Yes
create_directory(path) Create directory Yes
list_files(pattern) List files matching glob No
search_files(pattern, content) Search file contents No
get_file_info(path) Get file metadata No

Each built-in tool automatically:

  • Operates within sandbox boundaries
  • Records changes to the ChangeSet
  • Validates parameters against project configuration
  • Enforces deny-list patterns (.git/, node_modules/, etc.)
Why Not Parse LLM Output?

The obsolete approach of parsing markdown code fences has fundamental problems:

  1. Ambiguity: Is text explanation or code? Where does one file end and another begin?
  2. Fragility: Models output varying formats; regex parsing is brittle
  3. Loss of semantics: You lose the intent (create vs modify vs delete)
  4. No atomicity: Can't rollback individual operations
  5. Resource-limited: Only works for files, not databases or other resources

The tool-based approach solves all of these by making each operation explicit, typed, and trackable.

Semantic Error Prevention

!!! adr "Architecture Decision" The multi-layer error prevention strategy and guardrail architecture are defined in ADR-018: Semantic Error Prevention.

CleverAgents provides multiple layers of proactive error prevention that catch semantic errors before they can propagate through the system.

Layer 1: Decision-time Validation During Strategize

Every decision includes semantic validation:


Decision: Refactor payment module to async
alternatives_considered:
  - "Convert to async/await patterns" (chosen)
  - "Use thread pool with channels" (rejected: doesn't integrate with async ecosystem)
  - "Keep synchronous with timeout" (rejected: doesn't solve core latency issue)
confidence_score: 0.85
validation_performed:
  - Checked all payment API consumers can handle async
  - Verified database driver supports async operations
  - Confirmed no regulatory requirement for sync processing

Layer 2: Execution-time Semantic Guards

The execution actor uses a tool node that references the independently registered local/validate-api-compat tool:


# Actor graph uses a named tool node for semantic validation
actors:
  code_executor:
    type: graph
    skills:
      - local/semantic-validators    # Skill containing validation tools for LLM tool-calling
    nodes:
      - name: semantic_validator
        type: tool
        tool: local/validate-api-compat # Named tool from Tool Registry

The local/validate-api-compat tool is independently registered via its own YAML:


# File: tools/validate-api-compat.yaml
cleveragents:
  version: "3.0"

tool: name: local/validate-api-compat description: "Check for breaking API changes and attempt auto-migration" source: custom

capability: writes: true checkpointable: true

code: | # Not just syntax checking - semantic validation old_api = extract_api_signature(previous_version) new_api = extract_api_signature(current_version)

breaking_changes = find_breaking_changes(old_api, new_api)
if breaking_changes:
    affected_consumers = find_api_consumers(breaking_changes)
    migration_plan = generate_migration(breaking_changes)

    if can_auto_migrate(affected_consumers, migration_plan):
        apply_migration(migration_plan)
    <span style="color: cyan; font-weight: 600;">else</span>:
        raise SemanticError(
            &quot;Breaking API changes require manual review&quot;,
            changes=breaking_changes,
            affected=affected_consumers
        )

The local/semantic-validators skill references this tool by name:


# File: skills/semantic-validators.yaml
skill:
  name: local/semantic-validators
  description: "Semantic validation tools for API compatibility and code invariants"
  tools:
    - local/validate-api-compat

Layer 3: Invariant Enforcement

!!! adr "Architecture Decision" Invariant scoping, enforcement mechanisms, and inheritance rules are defined in ADR-016: Invariant System.

Invariants are named constraints that guide and constrain plan execution. They can be attached at four scopes, all managed through the unified agents invariant command:

  • Global invariants: Apply to all plans across all projects. Added via agents invariant add --global.
  • Project invariants: Apply to all plans targeting a specific project. Added via agents invariant add --project. Can also be attached at creation time via agents project create --invariant.
  • Action invariants: Attached to an action and carried forward as plan-level invariants when the action is used. Added via agents invariant add --action. Can also be attached at creation time via agents action create --invariant.
  • Plan invariants: Apply to a specific plan and its child plans. Added via agents invariant add --plan. Can also be attached at creation time via agents plan use --invariant.

Precedence and conflict resolution: When invariants from different scopes conflict, narrower scopes override broader scopes:

  • Plan-level invariants override project-level and global-level invariants.
  • Project-level invariants override global-level invariants.

Note: Action invariants are carried forward as plan-level invariants when the action is used (see Action invariants above), so they participate in precedence at the plan tier — there is no separate action tier in the precedence chain.

Conflict resolution is performed by the Invariant Reconciliation Actor — a dedicated actor responsible for comparing invariants across scopes, identifying conflicts, and producing the final effective invariant view for a plan. The Invariant Reconciliation Actor is set at three levels via --invariant-actor:

  1. Global config: Set via agents config set actor.default.invariant <ACTOR>. Defines the default Invariant Reconciliation Actor used when neither the project nor the plan specifies one.
  2. Project-level: Set via --invariant-actor on agents project create. If a project defines an Invariant Reconciliation Actor, it is used to reconcile that project's invariants against global invariants.
  3. Plan-level: Set via --invariant-actor on agents action create (carried forward when the action is used) or agents plan use (which overrides whatever was set on the action). If a plan has an Invariant Reconciliation Actor, it reconciles invariants from all scopes (plan, project, and global) and produces the final effective view for that plan.

The lookup order is: plan → project → global config. The first Invariant Reconciliation Actor found is used.

Invariant view calculation: When an action is used and a plan enters the Strategize phase, the Invariant Reconciliation Actor computes the effective invariant view by:

  1. Collecting all invariants from global, project, plan, and action scopes.
  2. Identifying conflicts (invariants from different scopes that contradict each other).
  3. Applying precedence rules (plan > project > global) to resolve conflicts.
  4. Producing the final set of effective invariants.

Each effective invariant is then recorded as an invariant_enforced decision in the plan's decision tree. This makes invariants visible, auditable, and correctable through the standard decision correction mechanism.

Child plan inheritance: When a top-level plan spawns child plans, the parent's effective invariant view (already reconciled) is passed down to each child plan. Child plans do not re-run reconciliation — they inherit the parent's resolved view.

Correcting invariants: The correction mechanism for invariant_enforced decisions supports two operations:

  • Remove: Remove an existing invariant from the plan's decision tree (the invariant remains defined at its scope but is no longer enforced for this plan).
  • Add: Add a new invariant to the plan. When adding, the user can select from invariants already accessible to the plan (those defined at the plan, action, project, or global scope), or provide free-form text to create a new ad-hoc invariant.

# Add invariants at different scopes
agents invariant add --global "All public APIs must maintain backward compatibility"
agents invariant add --global "Payment processing must be idempotent"
agents invariant add --project local/api-service \
  "Database transactions must complete within 5 seconds"
agents invariant add --project local/api-service \
  "Authentication must always use OAuth2"
agents invariant add --plan <PLAN_ID> "All API calls over TCP must be mocked"
agents invariant add --action local/code-coverage "Test files must not import production secrets"

# List invariants agents invariant list --global agents invariant list --project local/api-service agents invariant list --plan <PLAN_ID> --effective # Shows reconciled view

# Remove an invariant agents invariant remove <INVARIANT_ID>

# Attach invariants at creation time (convenience) agents project create --invariant "All endpoints must validate auth tokens" local/api-service agents action create --config ./actions/code-coverage.yaml agents plan use local/code-coverage local/api-service --invariant "Mock all network calls"

# Correct an invariant decision (remove or replace via standard correction) agents plan correct <DECISION_ID> --mode=revert </span> --guidance "Remove this invariant - it does not apply to this module"

The system collects, reconciles, and checks invariants:


class InvariantEnforcer:
    def compute_effective_invariants(self, plan):
        """Compute the effective invariant view for a plan using the Invariant Reconciliation Actor."""
        # 1. Collect raw invariants from all scopes
        raw = self.collect_all_invariants(plan)
    <span style="opacity: 0.7;"># 2. Find the Invariant Reconciliation Actor (plan -&gt; project -&gt; global config)</span>
    reconciler = (
        self.get_plan_invariant_actor(plan)
        <span style="color: magenta; font-weight: 600;">or</span> self.get_project_invariant_actor(plan)
        <span style="color: magenta; font-weight: 600;">or</span> self.get_global_invariant_actor()
    )

    <span style="opacity: 0.7;"># 3. Reconcile: apply precedence (plan &gt; project &gt; global), resolve conflicts</span>
    effective = reconciler.reconcile(raw, precedence=[<span style="color: #66cc66;">&#x27;plan&#x27;</span>, <span style="color: #66cc66;">&#x27;project&#x27;</span>, <span style="color: #66cc66;">&#x27;global&#x27;</span>])
    <span style="color: magenta; font-weight: 600;">return</span> effective

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">collect_all_invariants</span>(self, plan):
    <span style="color: #66cc66;">&quot;&quot;&quot;Collect invariants from all scopes accessible to this plan.&quot;&quot;&quot;</span>
    invariants = []
    invariants.extend(self.get_global_invariants())
    <span style="color: magenta; font-weight: 600;">for</span> project <span style="color: magenta; font-weight: 600;">in</span> plan.projects:
        invariants.extend(self.get_project_invariants(project))
    invariants.extend(self.get_action_invariants(plan.action))
    invariants.extend(self.get_plan_invariants(plan))
    <span style="color: magenta; font-weight: 600;">return</span> invariants

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">check_invariant_preservation</span>(self, changes, enforced_invariants):
    <span style="color: #66cc66;">&quot;&quot;&quot;Check that changes respect all enforced invariants.&quot;&quot;&quot;</span>
    <span style="color: magenta; font-weight: 600;">for</span> invariant <span style="color: magenta; font-weight: 600;">in</span> enforced_invariants:
        <span style="color: magenta; font-weight: 600;">if</span> <span style="color: magenta; font-weight: 600;">not</span> self.verify_invariant(invariant, changes):
            <span style="color: magenta; font-weight: 600;">return</span> InvariantViolation(invariant, changes)
    <span style="color: magenta; font-weight: 600;">return</span> Success()

Layer 4: Predictive Error Prevention

The system learns from past failures:


Error Pattern Database:
  - pattern: "Async conversion in payment module"
    historical_failures:
      - "Race condition in payment confirmation"
      - "Timeout handling breaks idempotency"
    preventive_checks:
      - "Add explicit transaction boundaries"
      - "Verify idempotency keys are preserved"
      - "Check distributed lock acquisition"

Apply Phase

!!! adr "Architecture Decision" The Apply phase, conflict resolution, and apply strategies are defined in ADR-006: Plan Lifecycle.

What Apply Does

Apply takes the sandboxed work product and makes it "real" in the project.

Core properties:

  1. Apply is a controlled commit step Apply exists specifically to separate "generated work" from "committed work," enabling review and safer automation.

  2. Apply is often the highest-risk step It changes real systems. This is where approvals and checks matter most.

  3. Apply produces a terminal 'applied' state After successful apply, the plan reaches the applied terminal state within the Apply phase.

Apply should perform (configurable) validations before committing:

  • Diff review gate

    • If the select_tool threshold is not met (i.e., confidence < threshold, or threshold is 1.0) in the automation profile, show:

      • changed files summary,
      • full diff,
      • risk warnings.
  • Conflict resolution

    • If applying to a git repo, handle rebase/merge conflicts safely.
  • Audit log

    • Record who applied, what changed, when, and why.

Validation in Apply

Validation runs during Execute, not during Apply. By the time a plan reaches the Apply phase, all required validations have already passed. Apply commits the sandbox changes to the real resources and does not re-run validations.

For full details on validation — including the Validation type system, modes, attachment scoping, failure handling, fix-then-revalidate loops, and data model — see the Validation section under Core Concepts.

Apply Data Model

A plan in Apply includes:

  • apply_summary
  • applied_artifacts (final commit hash, merged PR link, file list)
  • final_validation_results (per-validation tool invocation results: passed, message, and data for each)
  • approval_record (if human approvals are required)
  • deployment_record (optional, if apply triggers deploy)

Apply Terminal States

Apply is the final phase. It does not transition to another phase — instead, the plan reaches one of several terminal states within the Apply phase:

When Apply succeeds:

  • plan.phase = apply
  • plan.state = applied
  • the sandbox may be cleaned up or archived depending on retention policy
  • changes have been committed to real project resources

When Apply cannot complete within constraints:

  • plan.phase = apply
  • plan.state = constrained
  • the Apply phase has determined that the current strategy's constraints prevent successful application (e.g., merge conflicts that violate invariants, validation failures that cannot be resolved within the strategy's scope, resource state drift that contradicts strategy assumptions)
  • depending on the access_network automation profile flag, this may automatically trigger a reversion to Strategize, or the system may pause for user decision
  • sandbox remains intact pending reversion or user action

When Apply fails:

  • plan.phase = apply
  • plan.state = errored
  • sandbox remains intact for inspection/retry

When Apply is cancelled:

  • plan.phase = apply
  • plan.state = cancelled
  • sandbox remains intact for inspection

Project

!!! adr "Architecture Decision" The project concept, resource linking, and project-level configuration are defined in ADR-009: Project Model.

A project is the boundary that answers:

  • "Where is the work happening?"
  • "What can this plan read and write?"
  • "What skills (and their tools) can this plan use?"
  • "What context is available?"

A project is a collection of linked resources and configuration. Projects link to independently registered resources from the Resource Registry — they do not define resources inline. A resource can be linked to multiple projects, enabling shared resources across teams and workflows.

Important: Projects are created via CLI commands, NOT YAML configuration files.

Project Types: Local vs Remote

Projects are classified based on their resources:

Type Definition Where Plans Can Execute
Local Contains at least one local-only resource Client only
Remote All resources are remotely accessible Client or Server

This distinction matters for server mode: the server can only execute plans on remote projects because it needs network access to all resources.

Project Creation (CLI)


# Step 1: Register resources independently
agents resource add git-checkout local/api-repo \
  --path /repos/api-service \
  --branch main

agents resource add local/database local/staging-db </span> --connection-string "postgresql://staging.example.com/mydb" </span> --read-only

# Step 2: Create the project agents project create "my-api-service"

# Step 3: Link resources to the project agents project link-resource "my-api-service" local/api-repo

agents project link-resource "my-api-service" local/staging-db --read-only

Resources are registered once and can be linked to multiple projects. The resource's type, sandbox strategy, and capabilities are defined by its resource type in the Resource Registry — not by the project.

Project Data Model

A project includes:

1) Identity
  • name — the namespaced project name (e.g., local/api-service). This serves as the project's globally unique identifier; no separate ULID or ID is generated. The namespacing scheme ([[server:]namespace/]name) ensures global uniqueness across systems.
  • namespace (follows same rules as actors: local/, <username>/, <orgname>/)
  • is_remote (boolean, derived from resources)
2) Linked Resources

Resources are the "things you can act on." Projects link to independently registered resources from the Resource Registry rather than defining them inline. This means:

  • A resource can be linked to multiple projects (shared resources).
  • The resource's type, capabilities, sandbox strategy, and DAG relationships are defined by the resource itself — not by the project.
  • Projects can apply project-level overrides when linking (e.g., marking a writable resource as read-only within a specific project context).

Each linked resource reference has:

  • resource_id (ULID reference to a Resource Registry entry). Resources can be referenced by name or ULID in CLI commands; name is resolved to resource_id at command time.
  • project_read_only (boolean — project-level read-only override)
  • alias (optional short name for referencing within the project)

The full resource details (type, location, sandbox strategy, capabilities, parent/child DAG, etc.) are stored in the Resource Registry. See the Resources section for details.

3) Context configuration

Project-level defaults:

  • ignore patterns (like .gitignore semantics)
  • max file size
  • indexing strategy
  • preferred chunking/summarization policy (even if evolving)
  • context retention policy
4) Execution Environment

!!! adr "Architecture Decision" Execution environment routing is defined in ADR-043: Devcontainer Integration and Container-Project Association.

Projects can configure a default execution environment — a container in which tools execute instead of on the host. This is set via agents project context set:


execution_environment:
  default: local/dev-container        # Resource name of the default container
  priority: fallback                 # fallback | override

Priority semantics:

  • fallback (default): Use this execution environment only when no devcontainer is auto-detected for the current resource or its ancestors. If a resource (or ancestor) has a .devcontainer/, the devcontainer wins.
  • override: Always use this execution environment, ignoring any auto-detected devcontainers.

When a project has multiple containers linked as resources, the execution_environment.default field specifies which one to use. Without this setting, the system relies on auto-detected devcontainers or falls back to host execution.

Plans can override the project-level execution environment via --execution-environment on agents plan use (see Execution Environment Routing).

Multi-Project Operations

A single plan may target multiple projects (e.g., updating shared schemas across services). This is considered a key UX advantage over "run in one directory" systems. Because resources are independently registered and can be linked to multiple projects, shared resources across projects are a natural part of the architecture.

In multi-project execution:

  • Strategize must clarify which steps affect which projects.
  • Execution must isolate sandboxes per project OR define a composite sandbox. When multiple projects share the same resource, a single sandbox for that resource is used.
  • Apply must commit changes to each project separately, with separate approval records if necessary.
  • Tool resource bindings are resolved per-project — the same tool may bind to different resources depending on which project context it runs in.

Namespaces

!!! adr "Architecture Decision" The namespace system, resolution rules, and ownership model are defined in ADR-002: Namespace System.

Namespaces define ownership, scoping, and discoverability of actors, tools, skills, resources, resource types, actions, projects, and plans.

All named entities use the format <namespace>/<name>.

Namespace Types

Namespace Scope Storage Examples
local/ Current machine only Local database local/my-reviewer, local/test-action
<username>/ Personal server namespace Server database freemo/code-analyzer, jsmith/deploy-script
<orgname>/ Organization namespace Server database cleverthis/standard-review, acme/deploy-action
openai/, anthropic/, etc. Built-in LLM actors N/A (built-in) openai/gpt-4, anthropic/claude-3-opus

Namespace Rules

  • local/

    • Reserved namespace for local-only items
    • Exists only on the current machine
    • Stored in local database
    • Fast iteration, no sharing
    • Default namespace when none specified
  • <username>/ (e.g., freemo/, jsmith/)

    • Personal namespace on the server
    • Created when user registers an account
    • Stored on server, synced when connected
    • Used for reusable entities (actions, actors, tools, skills, resources) a user wants across machines
    • Only the owning user can create/modify items
  • <orgname>/ (e.g., cleverthis/, acme/)

    • Organization namespace on the server
    • Created when organization is registered
    • Shared across team members
    • All namespaced entities (actions, actors, tools, skills, resources, projects) can be centrally managed
  • Built-in Provider Namespaces (openai/, anthropic/, google/, etc.)

    • Reserved for built-in LLM actors
    • Automatically available when API keys are configured
    • In server mode: available if logged in and server has keys
    • In local mode: requires environment variables or app configuration
    • Cannot be used for custom actors

Server-qualified Names

To disambiguate between servers (when connected to multiple):

  • dev:freemo/code-coverage (personal namespace on dev server)
  • prod:cleverthis/deploy-action (org namespace on prod server)

This enables a pattern where:

  • local machine runs a lightweight client
  • server stores canonical definitions
  • multiple servers can coexist

Actor

!!! adr "Architecture Decision" The actor abstraction, actor types, and actor graph composition are defined in ADR-010: Actor and Agent Architecture.

What an Actor Is

!!! adr "Architecture Decision" The canonical definition of an actor — anything conversational, actor-as-graph principle, hierarchical composition, and the actor/agent distinction — is formalized in ADR-031: Actor Abstraction Definition.

An actor is the abstraction that generalizes "agent" into "anything conversational."

!!! abstract "Actor at a Glance" * It can be as small as a ==single LLM agent==. * It can also be an ==entire graph== that itself calls other actors/tools. * Actors can be nested/hierarchical, enabling "orchestrator of orchestrators."

**Every custom actor IS a graph** (a LangGraph defined via YAML configuration). Even a simple actor wrapping a single LLM is technically a graph with one node.

Actor Naming

Actors are always named using <namespace>/<name> format:

??? example "Naming Examples" | Name | Namespace | Description | | :--- | :-------- | :---------- | | local/my-reviewer | local | Local actor on this machine | | freemo/code-analyzer | freemo | Personal server actor | | cleverthis/deploy-specialist | cleverthis | Organization actor | | openai/gpt-4 | openai | Built-in LLM actor |

Actor Definition (YAML Configuration)

Actors are defined via YAML configuration files. Tools and skills are also defined via their own YAML configuration files. YAML configuration is used for actors, tools, and skills (not for actions or projects).

Example actor configuration (see examples/ directory for full examples):


name: local/my-workflow

cleveragents: version: "3.0" default_actor: workflow_controller

actors: # Simple LLM actor with skills referenced by name my_assistant: type: llm config: actor: openai/gpt-4 # Reference to built-in actor temperature: 0.7 system_prompt: | You are a helpful assistant. Current task: {{ context.task_description }} skills: - local/file-ops # Grants access to all tools in this skill - local/git-ops

# LLM actor with a composite skill (includes many sub-skills) data_processor: type: llm config: actor: anthropic/claude-3-opus system_prompt: | You are a data processing assistant. skills: - local/data-toolkit # A skill containing analysis + transformation tools

# Actor referencing another actor reviewer: type: llm config: actor: local/code-reviewer # Reference to another custom actor memory_enabled: true max_history: 20 skills: - local/file-ops - local/git-ops

routes: main_workflow: type: graph entry_point: start nodes: - name: analyze type: agent agent: my_assistant - name: process type: agent agent: data_processor edges: - source: start target: analyze - source: analyze target: process - source: process target: end

context: global: task_description: "Default task"

Jinja2 Template Preprocessing

!!! adr "Architecture Decision" The two-phase Jinja2 + environment variable preprocessing pipeline for actor YAML files is defined in ADR-032: Jinja2 YAML Template Preprocessing.

Actor configuration YAML files are processed through a two-phase pipeline before the resulting data structure is validated and loaded:

  1. Phase 1 — Jinja2 Template Rendering: The raw file content is run through a sandboxed Jinja2 template engine, resolving {{ }} expressions, {% %} control structures, and {# #} comments into static YAML text.
  2. Phase 2 — Environment Variable Interpolation: After YAML parsing, all string values matching ${VAR} or ${VAR:default} are recursively replaced with OS environment variable values, with automatic type coercion.

!!! warning "Execution Order" Jinja2 templates are evaluated before YAML parsing. Environment variables are interpolated after YAML parsing. The two phases are never interleaved.

Jinja2 Template Syntax

The engine uses standard Jinja2 delimiters inside the YAML file:

Delimiter Purpose Example
{{ ... }} Variable expression {{ context.paper_details.topic }}
{% ... %} Block statement (if, for, block, etc.) {% if context.auto_finish_active %}
{# ... #} Comment (stripped from output) {# This is ignored #}

Variable expressions are used to inject dynamic values into prompts and configuration fields:

system_prompt: |
  You are writing a paper on {{ context.paper_details.topic | tojson }}.
  Target length: {{ context.paper_details.length | tojson }} words.
  Audience: {{ context.paper_details.audience | tojson }}.

Conditional blocks enable a single configuration to define behavior for multiple modes:

system_prompt: |
  You are a research assistant.
  {% if context.auto_finish_active %}
  Auto-finish mode is active. Do not ask questions. Proceed autonomously.
  {% else %}
  Engage in interactive conversation to refine the output.
  {% endif %}

For loops generate repetitive YAML structure from data:

system_prompt: |
  Available sections:
  {% for section in context.section_paths %}
  - {{ section }}
  {% endfor %}
Sandboxed Execution

All template rendering uses jinja2.sandbox.SandboxedEnvironment, which prevents templates from:

  • Executing arbitrary Python code
  • Accessing the filesystem
  • Calling os.system, eval, exec, or similar unsafe operations
  • Accessing private attributes of objects
Custom Jinja2 Filters

The engine registers four custom filters in addition to all standard Jinja2 built-in filters:

Filter Purpose Example
yaml Serializes any value to a YAML-formatted string {{ my_dict | yaml }}
indent Indents text by N spaces (default 2) {{ content | indent(4) }}
sum Sums a numeric iterable {{ values | sum }}
selectattr Selects a named attribute from each item in a sequence {{ items | selectattr('name') }}

All standard Jinja2 built-in filters are also available, including: tojson, default, lower, upper, trim, join, replace, length, first, last, sort, unique, map, reject, select, batch, slice, int, float, string, list, dictsort, escape, safe, truncate, wordwrap, center, format, title, capitalize, striptags, urlencode, abs, round, pprint, groupby, max, min, random, filesizeformat, wordcount, reverse.

Exposed Built-in Functions

The following safe Python built-in functions are exposed in the template context and can be called directly in expressions:

Function Purpose Example
range() Generates integer sequences {% for i in range(5) %}
abs() Absolute value {{ abs(score) }}
round() Rounds a number {{ round(value, 2) }}
len() Length of a collection {{ len(items) }}
min() Minimum value {{ min(scores) }}
max() Maximum value {{ max(scores) }}
sum() Sum of values {{ sum(counts) }}
Template Context Resolution

Template variables are resolved from a context dictionary. The context supports a nested context key convention:

  • {{ context.paper_details.topic }} → resolves context["paper_details"]["topic"]
  • {{ context.brainstorming_summary }} → resolves context["brainstorming_summary"]

The global_context top-level key in the actor configuration file populates this dictionary at load time. At runtime, the actor invocation context, plan context, and session context are merged with the following precedence: runtime context > plan context > session context > global_context from YAML.

Template Detection and Bypass

The engine detects Jinja2 content by scanning for {% or {{ markers in the raw text. Files without these markers bypass Jinja2 processing entirely and are parsed as plain YAML with zero template overhead.

Deferred Rendering

When Jinja2 markers are present but no context is available at load time, the engine performs deferred rendering: it parses the YAML with template markers intact as literal strings. Templates are preserved in the parsed structure and rendered later when runtime context becomes available. This is the mechanism by which system_prompt fields retain their Jinja2 templates for runtime evaluation.

Template Protection for system_prompt Fields

A special protection mechanism ensures Jinja2 syntax inside system_prompt fields survives the YAML loading process:

  1. During loading, all Jinja2 delimiters in the raw file are temporarily replaced with sentinel markers:

    • {{<<<TEMPLATE_START>>>
    • }}<<<TEMPLATE_END>>>
    • {%<<<BLOCK_START>>>
    • %}<<<BLOCK_END>>>
  2. The protected text is parsed as YAML.

  3. After parsing, the sentinels are restored to Jinja2 syntax only within system_prompt fields (recursively through all nested dictionaries and lists).

This allows system prompts to contain Jinja2 templates that are evaluated at runtime (when the actor's full context is available) rather than at file-load time.

YAML Post-Processing

After Jinja2 rendering, the engine applies automatic post-processing to fix common issues:

  1. Blank line cleanup: Removes extraneous blank lines generated by {% %} block tags between YAML keys.
  2. Multi-colon line splitting: Detects and splits lines where template expansion produces multiple key: value pairs on a single line.
  3. Indentation correction: Inserts indentation hints for {% for %} loops to ensure generated YAML maintains correct structure.
Environment Variable Interpolation

After YAML parsing, all string values are recursively scanned for environment variable references:

Pattern Behavior
${VAR} Replaced with os.environ["VAR"]. Raises ValueError if not set.
${VAR:default} Replaced with os.environ.get("VAR", "default"). Uses default if not set.

Automatic type coercion is applied after substitution:

Substituted Value Coerced Type Example
"true" / "false" (case-insensitive) bool True / False
Digits only (with optional leading -) int 42, -7
Digits with single . float 3.14, -0.5
Anything else str Unchanged

This enables configurations like:

env_vars:
  WORK_DIR: ${HOME}/workspace       # String: /home/user/workspace
  LOG_LEVEL: ${LOG_LEVEL:info}      # String: info (default)
  MAX_RETRIES: ${MAX_RETRIES:3}     # Integer: 3 (coerced from default)
  DEBUG: ${DEBUG_MODE:false}        # Boolean: False (coerced from default)

Environment variable interpolation is applied recursively to all nested dictionaries and lists, ensuring resolution at any depth.

Actor Arguments

All actors can receive arguments when invoked, including built-in actors. Arguments are passed when:

  1. An action is used on projects (arguments flow to strategy/execution actors)
  2. An actor is directly invoked

Arguments are injected into the actor's context and can be used in Jinja2 templates within prompts.

For built-in actors (like openai/gpt-4), common arguments include:

  • temperature
  • max_tokens
  • system_prompt

Actor Composition (Hierarchical References)

Actors can reference other actors by name:

actors:
  complex_workflow:
    type: llm
    config:
      actor: local/base-analyzer  # References another registered actor

Load order matters: Referenced actors must be loaded/defined before actors that depend on them.

This enables hierarchical composition where:

  • Actor A's graph can include nodes that call Actor B (by name)
  • Actor B itself is a graph that might call Actor C
  • And so on (no depth limit)

Circular actor references are prohibited and detected at registration time.

Actor vs Agent (Relationship)

!!! adr "Architecture Decision" The formal actor/agent distinction — actors as the general abstraction, agents as a specialized subset — is defined in ADR-031: Actor Abstraction Definition.

  • Agent: an actor that is specifically an LLM with tools and reasoning behaviors.

  • Actor: may be an agent, but may also be:

    • a composite workflow,
    • a multi-step graph,
    • a wrapper around a third-party system (as long as it's "text in → text out" conversationally).

Actor Definition Fields (Complete Reference)

The complete set of fields available in an actor definition. For the formal JSON Schema and additional annotated examples, see Actor Configuration Files in the Configuration section.

Field Type Required Description
name string Yes Namespaced actor name in <namespace>/<name> format. Used as the registered identity.
type string Yes Actor type: llm (language model), tool (tool collection), or graph (multi-node workflow).
description string Yes Human-readable description of what the actor does.
version string No Schema version (default: "1.0").
model string Yes (LLM/GRAPH) LLM model identifier (e.g., gpt-4, claude-3.5-sonnet).
system_prompt string No System prompt text. Supports Jinja2 templates for dynamic content.
tools list Yes (TOOL) List of tool references (strings) and/or inline tool definitions.
context_view string No Role-based context filtering: strategist, executor, reviewer, or full.
memory object No Conversation history settings (see Memory Configuration below).
context object No File inclusion and context window settings (see Context Configuration below).
route object Yes (GRAPH) Graph topology (see Route Configuration below).
env_vars object No Environment variable key-value mappings.
skills list[string] No List of skill names this actor can use. Each entry is a namespaced skill name (e.g., local/file-ops). Skills provide tool capabilities to the actor. See Actor References to Skills and Tools.
lsp list|object No LSP server bindings for language intelligence. Can be a list of namespaced LSP server names (explicit binding), an object with languages: list (language-based binding), or an object with auto: true (resource-auto binding). See LSP Integration.
lsp_capabilities list[string]|"all" No Controls which LSP capabilities are exposed as tools. When omitted or "all", all capabilities from bound servers are available. When a list, only the named capabilities are exposed (e.g., [diagnostics, hover, definitions]).
lsp_context_enrichment object No Controls automatic LSP context enrichment. Keys: diagnostics (bool, default true), type_annotations (bool, default false), max_diagnostics_per_file (int, default 50).

Additional fields available in the v2/runtime actor definition format (within the config: block of actors defined inside the actors: top-level key):

Field Type Required Description
config.provider string Yes (LLM) LLM provider identifier: openai, anthropic, google, azure, openrouter, etc.
config.model string Yes (LLM) Model identifier within the provider.
config.actor string No Combined provider/model format (alternative to separate provider + model).
config.system_prompt string No System prompt text with Jinja2 template support.
config.temperature float No Sampling temperature (0.0 to 2.0). Lower = more deterministic.
config.max_tokens integer No Maximum tokens in the generated response.
config.memory_enabled boolean No Enable conversation memory (default: false).
config.max_history integer No Maximum conversation turns retained in memory (default: 50).
config.unsafe boolean No Allow this actor to perform unsafe operations (default: false).
config.options object No Provider-specific options passed through to the underlying LLM API.
config.tools list Yes (tool) List of inline tool definitions (each with name and code).
config.response_format object No JSON schema for structured output from the LLM.
Memory Configuration
Field Type Default Description
enabled boolean true Whether to maintain conversation history.
max_messages integer null (unlimited) Maximum number of messages to retain.
max_tokens integer null (unlimited) Maximum tokens in retained history.
summarize_old boolean false Whether to summarize old messages instead of discarding them.
Context Configuration
Field Type Default Description
include_files list[string] [] File paths to include in the actor's context.
include_dirs list[string] [] Directory paths to include in the actor's context.
exclude_patterns list[string] [] Glob patterns to exclude from context (e.g., "**/__pycache__/**").
max_context_tokens integer null (model default) Maximum size of the context window in tokens.
Context View

The context_view field controls role-based context filtering:

Value Purpose Includes
strategist High-level planning view Project structure, goals, constraints, architectural summaries
executor Implementation view Source code, file contents, specific task details
reviewer Validation view Changes, diffs, test results, review criteria
full Complete view (use sparingly) All available context from all categories
Type-Specific Requirements
Actor Type Required Fields Optional Fields
llm model system_prompt, tools, context_view, memory, context, env_vars, lsp, lsp_capabilities, lsp_context_enrichment
tool tools (at least one) context_view, env_vars
graph model, route system_prompt, tools, context_view, memory, context, env_vars, lsp, lsp_capabilities, lsp_context_enrichment
Tool Definitions (Inline)

Tools in an actor can be either string references to registered tools or inline definitions:

tools:
  # String reference to a registered tool
  - files/read_file
  - files/list_directory

  # Inline tool definition
  - name: utils/count_lines
    description: Count the number of lines in a file
    parameters:
      - name: file_path
        type: str
        description: Path to the file
        required: true
        default: null
    code: |
      def count_lines(file_path: str) -> int:
          with open(file_path, 'r', encoding='utf-8') as f:
              return len(f.readlines())

Each inline tool parameter supports:

Field Type Required Description
name string Yes Parameter name (must be a valid Python identifier).
type string Yes Python type annotation as string (e.g., str, int, list[str]).
description string Yes Human-readable description.
required boolean No Whether the parameter must be provided (default: true).
default any No Default value if not provided (only for optional parameters).

Inline tool name must follow the namespace/tool_name format. The code field contains Python source code that defines a callable function.

Route Configuration (Graph Topology)

For type: graph actors, the route field defines the graph structure:

Field Type Required Description
nodes list[NodeDefinition] Yes All nodes in the graph.
edges list[EdgeDefinition] Yes All edges connecting nodes.
entry_node string Yes ID of the starting node.
exit_nodes list[string] Yes IDs of terminal nodes.

Node Definition:

Field Type Required Description
id string Yes Unique node identifier (alphanumeric with underscores/hyphens).
type string Yes Node type: agent, tool, conditional, or subgraph.
name string Yes Human-readable node name.
description string Yes Node purpose and behavior.
config object No Type-specific configuration (see below).

Node type-specific config:

Node Type Config Fields Description
agent model, prompt, tools LLM agent with optional tools
tool tool_name, parameters Deterministic tool execution
conditional conditions[].check, conditions[].route_to Routes based on state conditions (Python expressions)
subgraph actor_path Embeds another actor as a nested workflow

Edge Definition:

Field Type Required Description
from_node string Yes Source node ID.
to_node string Yes Target node ID.
condition string No Python expression for conditional routing.
priority integer No Edge priority for multiple outgoing edges (higher = evaluated first, default: 0).

Graph Validation:

  • All node IDs must be unique within the graph.
  • The entry_node must reference an existing node ID.
  • All exit_nodes must reference existing node IDs.
  • All edge from_node and to_node must reference existing node IDs.
  • The graph must be acyclic — cycles are detected via DFS and rejected at validation time.
  • All nodes must be reachable from the entry node.

Actor Composition and Graphs

!!! adr "Architecture Decision" Hierarchical actor composition, the actor-as-graph principle, and graph node types are defined in ADR-031: Actor Abstraction Definition.

Actors can reference:

  • other actors (by namespaced name)
  • skills (by namespaced name — all tools from referenced skills become available)
  • subgraphs

This is central to enabling both:

  • multi-agent orchestration, and
  • modular reuse of workflows.

Nodes in the Graph: Actors and Tools

!!! adr "Architecture Decision" The two graph node types (actor nodes and tool nodes) and their relationship to the skill and tool systems are defined in ADR-031: Actor Abstraction Definition and ADR-030: Skill Abstraction Definition.

Graph nodes can be any of:

  • an actor (another LLM agent or composite workflow, referenced by name),
  • a tool node — a deterministic, non-LLM step that directly invokes a tool. Tool nodes can:
    • Reference a named registered tool by its fully-qualified name (e.g., tool: local/run-migrations). The tool must be registered in the Tool Registry via agents tool add. Metadata can optionally be overridden at the point of use.
    • Define an anonymous inline tool using the same format as a tool YAML body (with anonymous: true). This is useful for one-off, workflow-specific operations that don't warrant separate registration.

This is a powerful simplification: actors provide intelligence, tools provide capability (both as graph nodes and through skills for LLM tool-calling), and skills organize tools into reusable collections. Everything participates in the same graph.

Tool node with named tool reference:


nodes:
  - name: run_db_migrate
    type: tool
    tool: local/run-migrations # Named tool from Tool Registry
    override:                           # Optional metadata override
      capability:
        human_approval_required: true
  • name: spawn_tests type: tool tool: local/create-subplan # Another named tool

Tool node with anonymous inline tool:


nodes:
  - name: custom_validation
    type: tool
    anonymous: true
    description: "Validate output format before proceeding"
    input_schema:
      type: object
      properties:
        data: { type: object }
    capability:
      read_only: true
    code: |
      # Inline Python — same format as a named tool YAML body
      if not params["data"].get("status"):
          raise ValueError("Missing status field")
      return {"valid": True}

LSP Integration

!!! adr "Architecture Decision" The Language Server Protocol integration architecture — LSP Registry, actor LSP binding, capability exposure, and server lifecycle — is defined in ADR-027: Language Server Protocol (LSP) Integration.

Actors that perform software development tasks — writing code, refactoring, reviewing, debugging — benefit from language intelligence: the ability to understand types, navigate symbol definitions, discover references, identify compilation errors, and comprehend the structural relationships within a codebase. The Language Server Protocol (LSP) is the standard for this kind of intelligence, and CleverAgents integrates LSP servers directly into the actor runtime so that agents gain the same semantic code understanding that human developers enjoy in their IDEs.

!!! abstract "LSP at a Glance" * LSP servers are registered in a global LSP Registry (namespaced like tools, skills, and actors). * Actors bind LSP servers via their YAML configuration — explicitly by name, by language, or automatically based on project resources. * Different nodes in an actor's graph can have different LSP bindings. * LSP capabilities are exposed as tools (via LSPToolAdapter) and as context enrichment (diagnostics/type info injected into ACMS hot context). * The LSP Runtime in the Infrastructure layer manages server lifecycle, workspace mapping, and file synchronization.

LSP Registry

Language servers are registered as first-class entities in the global LSP Registry, following the same patterns as the Tool Registry, Skill Registry, and Actor Registry. Each entry defines a language server — its command, the languages it covers, its capabilities, and any initialization options.

LSP entries are:

  • Namespaced as [[server:]namespace/]name (e.g., local/pyright, local/clangd, cleverthis/rust-analyzer).
  • Defined via YAML configuration and registered with agents lsp add --config <file>.
  • Managed via agents lsp list, agents lsp show, agents lsp remove.

Example LSP configuration:


name: local/pyright
description: "Pyright language server for Python type checking and intelligence"

command: pyright-langserver args: ["--stdio"] transport: stdio

languages:

  • python

capabilities:

  • diagnostics
  • hover
  • completions
  • definitions
  • references
  • rename
  • code_actions
  • formatting
  • signature_help
  • document_symbols
  • workspace_symbols

initialization: python: pythonPath: "${PYTHON_PATH:/usr/bin/python3}" analysis: typeCheckingMode: "basic" autoSearchPaths: true

LSP configuration fields:

Field Type Required Description
name string Yes Namespaced identifier (<namespace>/<name>).
description string No Human-readable description of the language server.
command string Yes Executable command to launch the language server process.
args list[string] No Command-line arguments for the server process.
transport string No Communication transport: stdio (default) or tcp.
languages list[string] Yes Programming languages this server provides intelligence for (e.g., python, typescript, rust).
capabilities list[string] No Explicit list of LSP capabilities this server provides. When omitted, capabilities are auto-discovered from the server's initialize response.
initialization object No LSP initializationOptions sent during the initialize handshake. Supports ${ENV_VAR} interpolation.
workspace_settings object No LSP workspace configuration sent via workspace/didChangeConfiguration.
Resource Language Discovery

Resources attached to projects represent codebases, file trees, and repositories that contain source code in one or more programming languages. The system determines what languages a resource contains through a layered discovery process:

  1. File extension analysis — File extensions (.py, .ts, .rs, .go, .cpp, etc.) are mapped to language identifiers using a built-in extension-to-language table. This is the fastest and most common method.
  2. Content analysis — For files without extensions or with ambiguous extensions, content markers are inspected: shebang lines (#!/usr/bin/env python3), magic comments, and structural patterns.
  3. UKO classification — The Universal Knowledge Ontology classifies resources at the technology-specific layer (Python, TypeScript, Rust, etc.), providing semantic language identification that persists across sessions.
  4. Explicit project-level declaration — Projects may explicitly declare their languages via configuration, overriding or supplementing automatic discovery.

Language discovery results are cached per resource and invalidated when content changes.

Actor LSP Binding

Actors declare LSP dependencies in their YAML configuration via the lsp: field. Three binding modes are supported, from fully explicit to fully automatic:

Explicit binding (by server name):


actors:
  code_analyst:
    type: llm
    config:
      actor: anthropic/claude-3-opus
      system_prompt: |
        You are a Python code analyst. Use LSP tools to understand
        type information and identify issues in the codebase.
    skills:
      - local/file-ops
    lsp:
      - local/pyright
      - local/ruff-lsp

Language-based binding (runtime resolves servers from registry):


actors:
  polyglot_reviewer:
    type: llm
    config:
      actor: openai/gpt-4
    lsp:
      languages:
        - python
        - typescript
        - rust

Resource-auto binding (auto-discover from project resources):


actors:
  universal_developer:
    type: llm
    config:
      actor: anthropic/claude-3-opus
      system_prompt: |
        You are a software developer.
    lsp:
      auto: true

When auto: true is set, the runtime inspects the project's bound resources via language discovery, determines which languages are present, and binds the appropriate LSP servers automatically. This is the most reusable pattern — an actor configured with lsp: { auto: true } works correctly regardless of the language of the project it is assigned to.

Jinja2 dynamic binding (template-driven resolution):


actors:
  dynamic_analyst:
    type: llm
    config:
      actor: openai/gpt-4
    lsp:
      {% for lang in context.project_languages %}
      - {{ lsp_registry.for_language(lang) | first }}
      {% endfor %}

Template variables available during LSP resolution:

Variable Type Description
lsp_registry object The LSP Registry. Supports .for_language(lang) returning matching server names and .all() returning all entries.
context.project_languages list[string] Languages detected in the current project's resources.
context.resource_languages list[string] Languages detected in the specific resource being processed.
Per-Node LSP Binding

Different nodes in an actor's graph can have different LSP configurations. This allows fine-grained control — for example, giving a strategy actor read-only diagnostics while giving an execution actor full LSP capabilities:


actors:
  strategy_planner:
    type: llm
    config:
      actor: openai/gpt-4
      system_prompt: |
        Plan the implementation strategy. Use LSP diagnostics
        to assess codebase health before proposing changes.
    lsp:
      - local/pyright
    lsp_capabilities:           # Restrict to read-only
      - diagnostics
      - hover
      - definitions
      - references

code_implementer: type: llm config: actor: anthropic/claude-3-opus system_prompt: | Implement code changes. Use full LSP capabilities for navigation, diagnostics, and refactoring. lsp: auto: true lsp_capabilities: all # Full capabilities (default)

routes: dev_workflow: type: graph entry_point: plan nodes: - name: plan type: agent agent: strategy_planner - name: implement type: agent agent: code_implementer edges: - source: plan target: implement - source: implement target: end

The lsp_capabilities field controls which LSP features are exposed to the actor as tools. When omitted or set to all, every capability declared by the bound LSP servers is available.

LSP Capability Exposure

LSP capabilities reach actors through two complementary mechanisms:

As Tools (via LSPToolAdapter)

The LSPToolAdapter is an Infrastructure-layer adapter (analogous to MCPToolAdapter for MCP) that translates LSP server capabilities into CleverAgents tools. When an actor with LSP bindings activates, the adapter generates tool definitions for each exposed capability and injects them into the actor's tool surface alongside skill-provided tools.

Available LSP tools:

Capability LSP Method Tool Name Description
diagnostics textDocument/publishDiagnostics lsp/diagnostics Retrieve compilation errors, warnings, and hints for a file
hover textDocument/hover lsp/hover Get type information and documentation at a position
completions textDocument/completion lsp/completions Get code completion suggestions at a position
definitions textDocument/definition lsp/definition Go to the definition of a symbol
references textDocument/references lsp/references Find all references to a symbol
rename textDocument/rename lsp/rename Compute a rename refactoring across files
code_actions textDocument/codeAction lsp/code-actions Get available code actions (quick fixes, refactors)
formatting textDocument/formatting lsp/format Format a document according to language conventions
signature_help textDocument/signatureHelp lsp/signature Get function signature information at a call site
document_symbols textDocument/documentSymbol lsp/symbols List all symbols (classes, functions, variables) in a file
workspace_symbols workspace/symbol lsp/workspace-symbols Search for symbols across the entire workspace

When multiple LSP servers are bound to an actor (e.g., local/pyright for Python and local/typescript-lsp for TypeScript), the tool adapter routes requests to the appropriate server based on the file's detected language. The tool names remain the same — routing is transparent to the actor.

As Context Enrichment

In addition to explicit tool calls, LSP servers can automatically enrich the actor's context:

  • Diagnostic injection: When a source file enters the actor's hot context, the LSP server's diagnostics for that file are appended as structured annotations. The actor "sees" type errors, unused imports, and other issues without explicitly calling lsp/diagnostics.
  • Type annotation overlay: Hover information for key symbols (function signatures, class hierarchies, variable types) can be pre-fetched and included as context metadata.

Context enrichment is controlled per actor:


actors:
  enriched_reviewer:
    type: llm
    config:
      actor: openai/gpt-4
    lsp:
      auto: true
    lsp_context_enrichment:
      diagnostics: true                # Auto-inject diagnostics (default: true)
      type_annotations: false           # Auto-inject type info (default: false)
      max_diagnostics_per_file: 50     # Limit to avoid context bloat
LSP Server Lifecycle

LSP servers are managed by the LSP Runtime in the Infrastructure layer:

  1. Startup — When an actor with LSP bindings activates (for a plan phase or agents actor run), the LSP Runtime starts the required language server processes. Each server is initialized with the initialize LSP handshake, passing initializationOptions from the registry entry and workspace root paths mapped from bound resources.

  2. Workspace Mapping — LSP workspace roots correspond to registered resource physical paths. When the actor operates within a sandbox (Execute phase), the sandbox's working directory is used as the workspace root, ensuring the language server analyzes sandboxed code — not the original.

  3. File Synchronization — As the actor reads and modifies files (via tools), the LSP Runtime sends textDocument/didOpen, textDocument/didChange, and textDocument/didClose notifications to keep the language server's view synchronized with the actor's mutations.

  4. Shared Instances — If multiple actors in the same plan or session require the same language server for the same workspace, the LSP Runtime shares a single server instance. Reference counting ensures the server stays alive as long as at least one actor needs it.

  5. Shutdown — When the actor deactivates (plan phase completes, session ends), the LSP Runtime sends shutdown and exit lifecycle messages. Shared instances shut down only when the last referencing actor deactivates.

  6. Crash Recovery — If a language server process crashes, the LSP Runtime restarts it automatically, re-sends the initialize handshake, re-opens tracked documents, and resumes operations without disrupting the actor's execution.

LSP in the Plan Lifecycle

LSP integration enhances each phase of the plan lifecycle:

Phase LSP Role Example Use
Strategize Read-only intelligence — diagnostics, type information, and symbol navigation inform strategy decisions Strategy actor calls lsp/diagnostics to assess codebase health, lsp/symbols to understand module structure, lsp/references to gauge impact of proposed changes
Execute Full intelligence — all capabilities available during code generation and modification Execution actor calls lsp/definition to understand APIs before using them, lsp/diagnostics to verify changes compile, lsp/rename to perform safe refactorings
Apply Validation intelligence — diagnostics confirm the final changeset is clean Apply-phase validations invoke lsp/diagnostics on all modified files to ensure no regressions before merge
Comparison with MCP

LSP and MCP are both standard protocols for external capability servers integrated into the actor runtime:

Aspect MCP (Model Context Protocol) LSP (Language Server Protocol)
Purpose General-purpose tool discovery and invocation Language-specific code intelligence
Scope Any callable operation (file I/O, API calls, data queries) Code analysis capabilities (diagnostics, navigation, completions)
Registry Tool Registry (via MCPToolAdapter) LSP Registry (via LSPToolAdapter)
Binding Skills compose MCP tools; actors acquire via skill references Actors bind LSP servers directly via lsp: configuration
Tool generation Tools pre-defined by MCP server, registered at discovery Tools generated dynamically from LSP server capabilities at activation
Lifecycle Managed by MCP SDK connection lifecycle Managed by LSP Runtime with workspace mapping and file synchronization

Both adapters follow the same architectural pattern: a standard protocol server in the Infrastructure layer is bridged into the actor's tool surface through a typed adapter. The key difference is that MCP tools are general-purpose and explicitly authored, while LSP tools are language-specific and automatically derived from the protocol's capability model.

Multi-Actor Configuration File (Complete Structure)

A single actor configuration YAML file can define an entire multi-actor system — multiple actors, graph routing, stream processing, context sharing, and message routing — all in one file. This is the format used for complex workflows like the scientific paper writer.

Top-Level Keys
Key Type Required Description
name string Yes Namespaced actor name (<namespace>/<name>).
cleveragents object No Metadata: version, logging, template engine, safety, default actor.
actors (or agents) object Yes Map of actor names to definitions. Both key names are accepted.
routes object No Map of route names to stream or graph topology definitions.
merges list No Stream merge operations combining multiple sources into one target.
splits list No Stream split operations dividing one source into multiple targets.
publications list No Output stream names (e.g., ["__output__"]).
templates object No Reusable template definitions for Jinja2 inheritance.
instances object No Instantiated templates with bound parameters.
global_context object No Key-value pairs accessible to all actors via {{ context.key }}.
context object No Alternative context block with global: sub-key.
prompts object No Named prompt templates referenceable by actors.
pipelines object No Hybrid pipeline definitions combining stream and graph stages.
cleveragents Metadata Block
cleveragents:
  version: "3.0"             # Schema version (default: "3.0")
  logging:
    level: "INFO"            # DEBUG, INFO, WARNING, ERROR (default: INFO)
  template_engine: "JINJA2"  # JINJA2 or NONE (default: JINJA2)
  unsafe: false              # Allow unsafe operations (default: false)
  default_actor: my_actor    # Default actor when multiple defined
Route Definitions

Routes connect actors via stream or graph topologies:

Stream Routes — reactive processing pipelines:

routes:
  chat_stream:
    type: stream
    stream_type: cold          # cold (default), hot, or replay
    operators:
      - type: map              # map or graph_execute
        params:
          agent: chat_agent    # Actor name for map operators
    publications:
      - __output__             # Output stream name
    subscriptions:
      - __input__              # Input stream name
    buffer_size: 10            # Stream buffer size (default: 10)
    initial_value: null        # Initial value (optional)

Graph Routes — LangGraph-based directed graph workflows:

routes:
  main:
    type: graph
    entry_point: start         # Entry node name (required)
    nodes:
      start:
        type: START            # Special start node
      end:
        type: END              # Special end node
      router:
        type: message_router   # Message-based routing node
        rules:                 # Routing rules (see below)
          - ...
      my_actor_node:
        type: agent            # Actor-backed node
        agent: my_actor        # References actor by name
        metadata: {}           # Optional metadata
    edges:
      - source: start
        target: router
      - source: router
        target: my_actor_node
        condition:
          context_value: next_node
          equals: my_actor_node
      - source: my_actor_node
        target: end
    checkpointing: false       # Enable checkpointing (default: false)
    checkpoint_dir: null       # Checkpoint storage directory
    enable_time_travel: false  # Enable time travel debugging (default: false)
    parallel_execution: false  # Allow parallel node execution (default: false)
    state_class: null          # Custom state class name
Graph Node Types
Type Purpose Key Fields
agent Node backed by an actor agent: <actor_name>
tool Node invoking tools tools: [<tool_ref>, ...]
function Node backed by a Python function function: <function_name>
conditional Branching node condition: { ... }
subgraph Delegates to another graph route subgraph: <route_name>
start / START Explicit start node (none)
end / END Terminal node (none)
message_router Content-based message routing rules: [...]
Message Router Node

The message_router node type routes messages to different actors based on message content. It uses a rules-based system:

router:
  type: message_router
  rules:
    # Prefix-based routing
    - type: prefix
      match: "GOTO_BRAINSTORMING"
      target: brainstorming
      strip_match: true        # Remove the prefix before forwarding

    # Contains-based routing
    - type: contains
      match: "SET_TOPIC:"
      target: discovery

    # Suffix-based routing (catch-all)
    - type: suffix
      match: ""                # Empty string matches everything
      target: workflow_controller

Each rule specifies:

Field Type Description
type string Match type: prefix, contains, or suffix.
match string Pattern to match against the message content.
target string Node name to route the message to.
strip_match boolean Whether to strip the matched pattern from the message (default: false).

Rules are evaluated in order; the first matching rule determines the target node.

Routing Prefixes (Inter-Actor Communication)

Actors communicate with each other and the routing system via routing prefixes — special string prefixes prepended to output text that the message router interprets:

Prefix Pattern Purpose Example
GOTO_<NODE>: Route to a specific node GOTO_BRAINSTORMING:Start the brainstorm
SET_<FIELD>: Set a context field value SET_TOPIC:Quantum computing
ROUTE_<TARGET>: Route to a sub-target ROUTE_ASK_TOPIC:What topic?
COMMAND_OUTPUT: Display output directly to user COMMAND_OUTPUT:Help text here
DISCOVERY_RESPONSE: Response from discovery stage DISCOVERY_RESPONSE:Topic set

Tool actors return these prefixes as their result variable. The message router parses the prefix and routes accordingly. The message content after the colon is forwarded to the target node.

Conditional Edges

Graph edges can include conditions that control routing based on graph state:

edges:
  # Unconditional edge
  - source: start
    target: router

  # Conditional edge — routes based on context value
  - source: router
    target: brainstorming
    condition:
      context_value: next_node
      equals: brainstorming

  # Conditional edge — routes based on boolean flag
  - source: passthrough
    target: auto_driver
    condition:
      context_value: auto_finish_active
      equals: true
Merges and Splits

Merges combine multiple input streams into a single stream:

merges:
  - sources: [__input__]    # Special __input__ = user input
    target: main            # Route name to send merged input to

Splits divide a single stream into multiple output streams:

splits:
  - source: main_output
    targets: [log_stream, display_stream]

Special stream names:

  • __input__ — the user's input
  • __output__ — the final output displayed to the user
Inline Tool Code Model

Tool-type actors define their behavior entirely in inline Python code within the code: field:

my_tool_actor:
  type: tool
  config:
    tools:
      - name: my_tool
        code: |
          import sys
          # Available variables:
          #   input_data  — the input text/message passed to this tool
          #   context     — shared mutable context dictionary
          #   result      — set this variable to define the tool's output

          msg = input_data or ''
          context['last_input'] = msg

          if msg.startswith('!help'):
              result = "COMMAND_OUTPUT:Available commands: !help, !next"
          else:
              result = f"GOTO_PROCESSOR:{msg}"

          print(f"DEBUG: {result}", file=sys.stderr)

The inline code execution model provides three implicit variables:

Variable Type Description
input_data string The input text/message passed to the tool.
context dict Shared mutable context dictionary. Changes persist across invocations.
result string Set this variable to define the tool's output.

The context dictionary is the primary mechanism for inter-actor state sharing. All actors in the same configuration share the same context, enabling data flow between stages.

Context Sharing

The context dictionary (accessible in tool code and Jinja2 templates) serves as shared state:

# Set via global_context in YAML:
global_context:
  writing_stage: intro
  paper_details:
    topic: null
    length: null
    audience: null

# Or via context.global:
context:
  global:
    conversation_mode: true
    default_actor: openai/gpt-4

At runtime, tool actors read and modify context freely:

# In tool code:
context['writing_stage'] = 'brainstorming'  # Update stage
topic = context.get('paper_details', {}).get('topic')  # Read nested value
context.setdefault('history', []).append(msg)  # Append to list

In Jinja2 templates (system prompts):

system_prompt: |
  Paper topic: {{ context.paper_details.topic | tojson }}
  {% if context.auto_finish_active %}
  Proceed autonomously.
  {% endif %}
Stream-to-Graph Bridge

Routes can include bridge configuration for dynamic topology changes:

routes:
  adaptive:
    type: stream
    bridge:
      upgrade_conditions:
        message_count_threshold: 5
      downgrade_conditions:
        idle_timeout: 30
      state_extractor: "extract_graph_state"
      state_flattener: "flatten_to_stream"
      preserve_subscriptions: true
      preserve_checkpointing: true
Publications

The publications key defines output streams at the route or top level:

# Route-level publications
routes:
  chat_stream:
    type: stream
    publications:
      - __output__

# Top-level publications
publications:
  - __output__

Actor Configuration File Loading

Actor configuration files can be loaded from either JSON or YAML format:

  1. The loader first attempts JSON parsing (json.loads)
  2. If JSON parsing fails, it falls back to the YAML pipeline (Jinja2 preprocessing + yaml.safe_load + environment variable interpolation)

The resolution order for provider and model values when loading:

  1. CLI override (--provider, --model)
  2. Top-level provider / model keys
  3. Top-level provider_type / model_id aliases
  4. v2-extracted values from actors.<name>.config.provider / .model

For unsafe flag: the result is true if any of the following is true: the top-level unsafe key, the v2-extracted unsafe flag, or the CLI --unsafe flag.

Agent

!!! adr "Architecture Decision" The agent specialization and its relationship to the actor abstraction are defined in ADR-010: Actor and Agent Architecture.

Agent Definition

In CleverAgents, an agent is a specialized actor with:

  • a conversational interface,
  • tool-calling capability,
  • potentially memory, planning heuristics, and role identity.

Examples of agent roles:

  • planner/architect (strategy actor)
  • coder/implementer (execution actor)
  • reviewer/qa agent
  • release/apply agent

The transcript explicitly discusses role separation like planner/coder/reviewer in context views/memory proposals.

Agent Behavior Configuration

Agents should be configurable without code changes:

  • prompt templates
  • tool sets
  • safety constraints
  • style constraints (verbosity, code style)
  • reliability controls (self-checks, validations)

A design goal is user empowerment: "users customize LLM behavior without modifying core code."

Tools

!!! adr "Architecture Decision" The tool system, tool adapter layer, and tool lifecycle are defined in ADR-011: Tool System.

What a Tool Is

A tool is a namespaced, independently registered, callable operation. It is the ==atomic unit of execution== in CleverAgents — the smallest piece of functionality that can read, write, or transform resources. Tools are defined in their own YAML configuration files, managed through the agents tool CLI commands, and registered in the Tool Registry.

Tools follow the same <namespace>/<name> naming convention as actors, skills, and other entities (e.g., local/run-migrations, cleverthis/validate-api, local/create-subplan). They support optional server-qualified prefixes for multi-server disambiguation (e.g., dev:freemo/custom-analysis).

Validation as a Tool subtype: A Validation is a specialized subtype of Tool that extends the Tool class with validation-specific metadata (mode: required/informational) and a structured JSON return format ({ "passed": bool, "message": string, "data": object }). Because Validation extends Tool via standard class inheritance, it inherits all base properties and behaviors — registration, resource bindings, lifecycle hooks, capability metadata, source types (custom, MCP, agent_skill, builtin, and the validation-specific wrapped source for Validations that delegate to an existing Tool), and the ability to exist as a tool node in an actor graph. A Validation can also wrap an existing plain Tool via the wraps field, reusing the Tool's implementation and interpreting its output through a transform function — see the Tool Wrapping subsection under Core Concepts > Validation for details.

!!! note "Key Constraints on Validations" - Always read-only: Validations observe and report but ==never modify resources==. writes is always false and checkpointable is always false. - Shared namespace: Validations and plain Tools share the same naming namespace in the Tool Registry. A name conflict results in an error. - Superset/subset semantics: A Validation can be used anywhere a Tool is expected (since it IS a Tool), but not vice versa. - Unified management: Listed via agents tool list --type validation, inspected via agents tool show, removed via agents tool remove. Only add, attach, and detach have validation-specific CLI commands.

Validations can be attached to resources directly, or to resources through projects or plans. The same mechanisms used to determine what tools can operate on what resources carry over to determining what validations apply to a particular resource. See the Validation section (immediately following this Tools section) for full details on the Validation type system, modes, attachment scoping, failure handling, and data model.

The Dual Role of Tools

Tools serve two distinct roles in CleverAgents:

  1. As components of a Skill: A skill references tools by name to assemble a reusable capability collection. When an actor references a skill, all of that skill's tools (including those from included child skills) become available to the actor's LLM agent for tool-calling.

  2. As tool nodes in an Actor graph: An actor's graph definition can include type: tool nodes that directly invoke a specific tool. This is used for deterministic, non-LLM steps in a workflow — e.g., spawning a child plan, running validation, or executing a migration. The tool node either references a named registered tool or defines an anonymous inline tool.

block-beta
    columns 3
    space:3
    block:header:3
        A["Tool: Dual Role"]
    end
    space:3
    block:role1:1
        B["Role 1: In a Skill"]
        C["Skill: local/devops"]
        D["tools:"]
        E["  - local/run-migrations"]
        F["  - local/validate-schema"]
        G["(tool-calling by LLM)"]
    end
    space:1
    block:role2:1
        H["Role 2: In an Actor Graph"]
        I["Actor Graph node:"]
        J["  name: run_db"]
        K["  type: tool"]
        L["  tool: local/run-migrations"]
        M["(deterministic invoke)"]
    end

Tool Configuration (YAML)

Tools are defined in their own YAML configuration files, separate from skills and actors. A tool YAML file declares the tool's identity, schema, capability metadata, and implementation:


# File: tools/run-migrations.yaml
cleveragents:
  version: "3.0"

tool: name: local/run-migrations description: "Run database migrations for the API service"

source: custom # mcp | agent_skill | builtin | custom

# Resource bindings — what resources this tool needs access to resources: db: type: local/database access: read_write required: true description: "Target database for migrations"

input_schema: type: object properties: direction: type: string enum: [up, down] count: type: integer default: 1 required: [direction]

capability: writes: true write_scope: resource_slots: [db] # References the "db" resource slot checkpointable: true checkpoint_scope: transaction side_effects: [schema_mutation]

code: | import subprocess direction = params["direction"] count = params.get("count", 1) db = ctx.resources["db"] # Access the bound database resource result = subprocess.run( ["alembic", direction, str(count)], capture_output=True, text=True, cwd=db.sandbox.root ) return {"stdout": result.stdout, "returncode": result.returncode}

Another example — a tool that wraps an MCP server endpoint:


# File: tools/create-github-issue.yaml
cleveragents:
  version: "3.0"

tool: name: local/create-github-issue description: "Create a GitHub issue via MCP"

source: mcp mcp_server: command: "npx @anthropic/mcp-github" env: GITHUB_TOKEN: "${GITHUB_TOKEN}" tool_name: create_issue # The tool name as exposed by the MCP server

capability: writes: true write_scope: [github:issues] checkpointable: false

And an Agent Skill tool:


# File: tools/deploy-staging.yaml
cleveragents:
  version: "3.0"

tool: name: local/deploy-staging description: "Deploy the current branch to the staging environment"

source: agent_skill agent_skill: path: ./skills/deploy-to-staging sandbox_policy: container allowed_tools: ["Bash(docker:)", "Bash(kubectl:)", "Read"]

capability: writes: true checkpointable: false side_effects: [deploy, infrastructure]

Tool Registration and Management

Tools are managed through the agents tool CLI commands:


# Register a new tool from its YAML configuration
agents tool add --config ./tools/run-migrations.yaml

# Update an existing tool (re-reads the config file, overwrites registration) agents tool add --config ./tools/run-migrations.yaml --update

# List all registered tools agents tool list

# Show details for a tool (schema, capability, references) agents tool show local/run-migrations

# Remove a tool agents tool remove local/run-migrations

Once registered, a tool is available to be referenced by skills (in their tools list) and by actor graphs (as type: tool nodes). Tools persist in the database (local or server) and follow the same namespace rules as actors and skills.

Anonymous Tools

An anonymous tool is an inline tool definition that appears directly in a skill YAML or an actor graph node. Anonymous tools use the same format as a named tool's YAML definition (same input_schema, capability, and code fields) but lack a namespaced name. They are:

  • Not registered in the Tool Registry
  • Not reusable — they exist only within the YAML file where they are defined
  • Useful for one-off operations that are too specific to warrant separate registration

Anonymous tools in a skill YAML:


skill:
  name: local/my-skill
  tools:
    - local/run-migrations          # Named tool reference
    - local/validate-api-compat     # Named tool reference

anonymous_tools: # Inline definitions, same format as tool YAML - description: "One-off data cleanup for this project" input_schema: type: object properties: table: { type: string } capability: writes: true checkpointable: true code: | # ... Python code ... return {"cleaned": count}

Anonymous tools in an actor graph node:


nodes:
  - name: custom_step
    type: tool
    anonymous: true
    description: "Inline validation specific to this workflow"
    input_schema:
      type: object
      properties:
        data: { type: object }
    capability:
      read_only: true
    code: |
      # ... Python code ...
      return {"valid": True}

The anonymous tool format is intentionally identical to the body of a named tool YAML — this means promoting an anonymous tool to a named, registered tool is a simple copy-paste into its own YAML file and agents tool add.

Metadata Overrides

When referencing a named tool in a skill or actor graph, its registered metadata can optionally be overridden at the point of use. This allows context-specific adjustments without modifying the tool's global registration.

Overriding tool metadata in a skill:


skill:
  name: local/strict-devops
  tools:
    - name: local/run-migrations
      override:
        capability:
          human_approval_required: true # Override: require approval in this skill
          write_scope: [database:staging] # Override: restrict scope for this context
- local/validate-api-compat            # No overrides, use as registered

Overriding tool metadata in an actor graph node:


nodes:
  - name: safe_migrate
    type: tool
    tool: local/run-migrations
    override:
      capability:
        human_approval_required: true

Overriding tool metadata when including a sub-skill:

When a skill includes another skill (importing all its tools), individual tools from the included skill can have their metadata overridden:


skill:
  name: local/production-ops
  includes:
    - name: local/devops-toolkit
      tool_overrides:
        - tool: local/run-migrations
          override:
            capability:
              human_approval_required: true # In this context, require human approval
              write_scope: [database:production]
    - <span style="color: cyan;">tool</span>: local/create-github-issue
      <span style="color: cyan; font-weight: 600;">override</span>:
        <span style="color: cyan; font-weight: 600;">capability</span>:
          <span style="color: cyan; font-weight: 600;">human_approval_required</span>: <span style="color: magenta; font-weight: 600;">true</span> <span style="opacity: 0.7;"># Require approval in this context</span>

Override rules:

  1. Overrides are shallow-merged — only the specified fields are replaced; unspecified fields retain their registered values.
  2. Overrides never persist back to the Tool Registry — they apply only at the point of use.
  3. Built-in tool metadata cannot be overridden (it is authoritative from the implementation).
  4. The override scope is limited to capability and description fields. Schema (input_schema, output_schema) cannot be overridden because it would break callers' expectations.

Resource Bindings

!!! adr "Architecture Decision" Resource binding resolution, slot declarations, and project-specific binding are defined in ADR-008: Resource System and ADR-011: Tool System.

Tools operate on resources — git repositories, filesystems, databases, and more. The resource binding system declares and resolves the relationship between a tool and the resources it needs access to.

Resource Slots

A tool declares one or more resource slots in its YAML configuration. Each slot is a typed placeholder that specifies:

  • Slot name: A logical name used to reference the resource within the tool's code and parameters.
  • Resource type: The resource type required (e.g., git, fs-mount, local/database). The bound resource must be of this type (or a compatible subtype).
  • Access mode: read_only, write_only, or read_write.
  • Description: Human-readable explanation of what the tool uses this resource for.
  • Required/optional: Whether the slot must be bound for the tool to function.

Example tool YAML with resource slots:


tool:
  name: local/run-migrations
  description: "Run database migrations"
  source: custom

resources: db: type: local/database access: read_write required: true description: "Target database for migrations"

capability: writes: true write_scope: [db:migrations] # References the "db" slot checkpointable: true

code: | direction = params["direction"] db_resource = ctx.resources["db"] # Access the bound resource result = db_resource.handler.execute_migration(direction, db_resource.sandbox) return {"status": "ok"}

A tool that works with multiple resources:


tool:
  name: local/cross-repo-diff
  description: "Compare files across two git repositories"
  source: custom

resources: source_repo: type: git-checkout access: read_only required: true description: "Source repository to compare from" target_repo: type: git-checkout access: read_only required: true description: "Target repository to compare against"

capability: read_only: true

code: | source = ctx.resources["source_repo"] target = ctx.resources["target_repo"] # ... compare files across repos ...

Three Binding Modes

Resource slots are resolved to actual resources through one of three binding modes:

1. Contextual Binding (default)

The slot declares a resource type requirement, and the system resolves it from the plan's project context at activation time. This is the most common mode — the tool says "I need a git-checkout resource" and the system finds one among the project's linked resources.


resources:
  repo:
    type: git-checkout
    access: read_write
    # No `bind` field → contextual binding

Resolution rules for contextual binding:

  • The system searches the plan's project for linked resources matching the slot's type.
  • If exactly one resource of the right type exists, it is automatically bound.
  • If multiple resources match, the system uses the slot name as a hint (e.g., a slot named repo prefers a resource with alias repo). If ambiguous, the plan execution raises an error requiring explicit binding.
  • If no resource matches, the tool cannot be activated for this plan (a validation error is raised during plan creation).

2. Static Binding

The slot is hardcoded to a specific registered resource by name. This is useful for tools that always operate on the same resource, regardless of project context.


resources:
  docs:
    type: fs-mount
    access: read_only
    bind: local/company-docs # Static: always this resource
    description: "Company documentation corpus"

Static bindings are resolved at registration time and validated — the named resource must exist and be of the correct type.

3. Parameter Binding

The resource reference is passed as a tool argument at invocation time. This is useful for tools that operate on user-specified resources.


resources:
  target:
    type: git-checkout
    access: read_only
    from_param: repository # Bound from the "repository" input parameter
    description: "Repository to analyze"

input_schema: type: object properties: repository: type: string description: "Name of the registered resource to analyze" required: [repository]

The from_param field links a resource slot to an input parameter. At invocation time, the system resolves the parameter value as a resource name from the Resource Registry and validates type compatibility.

Binding Resolution Flow
stateDiagram-v2
    [*] --> ToolActivation: Actor references skill or tool node

    state "For Each Resource Slot" as ForEach {
        state binding_check <<choice>>
        [*] --> binding_check: Check binding type

        binding_check --> StaticBinding: has bind field
        binding_check --> ContextualBinding: no bind, no from_param
        binding_check --> ParameterBinding: has from_param

        state "Static Binding" as StaticBinding {
            [*] --> ResolveByName: Resolve from Resource Registry
            ResolveByName --> ValidateType: Validate type compatibility
        }

        state "Contextual Binding" as ContextualBinding {
            [*] --> SearchProject: Search plan's project resources
            SearchProject --> FilterByType: Filter by resource type
            state match_check <<choice>>
            FilterByType --> match_check
            match_check --> AutoBind: One match
            match_check --> TryAlias: Multiple matches
            match_check --> ValidationError: No matches
            TryAlias --> AliasMatch: Try alias/name match
            AliasMatch --> AutoBind: Match found
            AliasMatch --> ValidationError: No match
        }

        state "Parameter Binding" as ParameterBinding {
            [*] --> DeferBinding: Defer to invocation time
        }

        StaticBinding --> StoreBindings
        AutoBind --> StoreBindings
        ParameterBinding --> StoreBindings
        state "Store in ToolActivationContext" as StoreBindings
    }

    state "Tool Invocation" as Invocation {
        [*] --> ResolveParams: Resolve parameter-bound slots\nfrom invocation params
        [*] --> ValidateAccess: Ensure sandbox exists\nValidate access mode
        ResolveParams --> Execute
        ValidateAccess --> Execute
        state "Execute tool with bound resources" as Execute
    }

    ForEach --> Invocation
    Invocation --> [*]
Built-in Tool Resource Bindings

Built-in tools have implicit resource slots that do not need to be declared in YAML (they are hardcoded in the implementation):

Built-in Tool Group Implicit Slot Slot Type Access
file_operations (read_file, write_file, edit_file, etc.) directory fs-directory or git-checkout read_write
directory_operations (create_directory, list_directory, etc.) directory fs-directory or git-checkout read_write
search_operations (search_files, find_definition, etc.) directory fs-directory or git-checkout read_only
git_operations (git_status, git_diff, git_log, etc.) repo git-checkout read_only

Built-in tools accept both fs-directory and git-checkout types for file operations because a git-checkout resource's worktree root is an fs-directory. When binding to a git-checkout, the resource router automatically resolves to the worktree root fs-directory child for file operations. A standalone fs-mount resource also works — the router resolves through the fs-mount → root fs-directory chain.

Resource Discovery via Bindings

The binding system enables powerful resource discovery queries:

  • "What tools can modify this resource?" → Find all tools with resource slots matching the resource's type and access: read_write or access: write_only.
  • "What tools can read this virtual file?" → Find the virtual file's physical children, then find tools with slots matching each physical resource's type.
  • "What resources does this tool need?" → Inspect the tool's declared resource slots.
  • "Is this tool compatible with this project?" → Check if the project's linked resources can satisfy all of the tool's required resource slots.
Transitive Reachability

!!! adr "Architecture Decision" Tool reachability, access projection, and read/write routing are defined in ADR-037: Tool Reachability and Access Projection.

Tool reachability extends beyond direct binding. A tool bound to a git-checkout can transitively reach every descendant fs-file through the DAG's contains edges.

Forward reachability: Given a tool T bound to resource R, the set of all resources T can access is {R} {all descendants of R via contains edges}.

Inverse reachability: Given a resource r, the set of tools that can reach it is found by walking up the containment hierarchy from r, collecting all ancestors, and finding tools with resource slots compatible with any ancestor's type.

Cross-equivalence reachability: If resource r has a virtual parent, the system also finds tools that can reach any sibling physical manifestation of the same virtual resource. This answers: "What tools can reach the same logical resource through any physical path?"

Example: fs-file at /repo/src/main.py is reachable by:

  • write_file (bound to git-checkout ancestor → forward reach)
  • lsp_hover (bound to lsp-workspace → reaches equivalent lsp-document sibling)
  • docker_exec (bound to container-instance → reaches equivalent fs-file in container mount)
Access Projection

When a tool bound to ancestor resource R accesses descendant resource d, the access projection computes how d is identified within R's access space. Each resource type handler implements a project_access method that returns an AccessProjection:

  • access_path: The path in the binding resource's namespace (e.g., src/main.py for filesystem, file:///repo/src/main.py for LSP).
  • protocol: The access mechanism (filesystem, lsp-textdocument, container-exec, sql, etc.).
  • crosses_sandbox: Whether this projection crosses a sandbox boundary (important: an LSP server reading from the real filesystem crosses the sandbox and sees pre-sandbox content).
  • read_richness: A score indicating how much information this access path provides (LSP: 10, filesystem: 1). Used for read routing.
Read/Write Routing

When a virtual resource has multiple physical manifestations reachable through different tools, the system routes reads and writes through different paths:

For writes: Route to the canonical write target — the physical manifestation in the strongest sandbox domain (preference: git_worktree > snapshot > copy_on_write > transaction_rollback). This ensures writes are sandbox-tracked and checkpointable.

For reads: Route to the richest available source, ranked by read_richness:

Source Richness Provides
lsp-document 10 Type info, symbols, diagnostics, go-to-def, references, completions
Semantic index 5 Pre-computed symbol index, dependency graph
fs-file via git-checkout 1 Raw file content, git history
fs-file via container mount 1 Raw file content
git-tree-entry 1 File content at specific commit

The routing algorithm selects the highest-richness source that is available, current (coherence checks pass), and compatible with the query type.

Tool Registry

CleverAgents maintains a Tool Registry — a persistent catalog of all independently registered tools:

@startuml
skinparam classAttributeIconSize 0
skinparam classFontSize 13
skinparam noteFontSize 11
skinparam defaultFontSize 12

class ToolRegistry {
  - toolIndex : Map<String, ToolRecord>
  --
  + add(config_path) : ToolRecord
  + update(name, config_path) : ToolRecord
  + remove(name) : void
  + lookup(name) : ToolRecord
  + list(filters) : ToolRecord[]
}

class ToolRecord {
  + name : String
  + description : String
  + source : String {mcp|agent_skill|builtin|custom}
  + config_path : String
  + input_schema : JSONSchema
  + output_schema : JSONSchema
  + capability_metadata : CapabilityMetadata
  + resource_slots : List<ResourceSlot>
  + code : String
}

ToolRegistry "1" *-- "0..*" ToolRecord : indexes >

note right of ToolRegistry
  **Populated by:**
  - agents tool add CLI command
  - Dynamic refresh on MCP notifications

  **Consumed by:**
  - Skill registration
  - Actor graph construction
  - Resource binding resolution
  - Plan validation
end note
@enduml

The Tool Registry works alongside the Skill Registry (described in the Skills section). Skills reference tools by name from the Tool Registry; the Skill Registry's flattened tool sets are composed from Tool Registry entries plus any anonymous inline tools.

Tool Interface and Architecture

Each individual tool — whether independently registered or defined as an anonymous inline tool — conforms to a uniform interface regardless of its source:

@startuml
skinparam classAttributeIconSize 0
skinparam classFontSize 13
skinparam defaultFontSize 12
skinparam linetype ortho

class Tool {
}

class Identity {
  + name : String
  + qualified_name : String
  + source : ToolSource
}

class Schema {
  + input_schema : JSONSchema
  + output_schema : JSONSchema
}

class CapabilityMetadata {
  + read_only : Boolean
  + writes : Boolean
  + write_scope : String
  + idempotent : Boolean
  + checkpointable : Boolean
  + side_effects : List<String>
  + cost_profile : String
  + human_approval_required : Boolean
}

class ResourceBindings {
  + slots : Map<String, ResourceSlot>
}

class ResourceSlot {
  + type : String
  + access : String
  + required : Boolean
  + bind : String
  + from_param : String
  + description : String
}

interface Lifecycle <<interface>> {
  + discover() : ToolDescriptor
  + activate() : void
  + execute(params, ctx) : Result
  + deactivate() : void
}

class ExecutionContext {
  + sandbox : Sandbox
  + plan : Plan
  + changes : List<Change>
  + resources : Map<String, BoundResource>
}

enum ToolSource {
  mcp
  agent_skill
  builtin
  custom
}

Tool *-- Identity
Tool *-- Schema
Tool *-- CapabilityMetadata
Tool *-- ResourceBindings
Tool *-- Lifecycle
Tool o-- ExecutionContext : uses at runtime >
ResourceBindings *-- "0..*" ResourceSlot
Identity --> ToolSource
@enduml

Every tool implements the same four lifecycle methods. The tool adapter layer is responsible for translating source-specific behavior into these methods.

Tool Adapter Layer

!!! adr "Architecture Decision" The adapter pattern for tool sources and the uniform tool interface are defined in ADR-011: Tool System.

Each tool source has a corresponding adapter that translates source-specific protocols into the uniform tool interface:

@startuml
skinparam classAttributeIconSize 0
skinparam classFontSize 13
skinparam defaultFontSize 12

interface "ToolInterface" as UTI <<interface>> {
  + discover() : ToolDescriptor
  + activate() : void
  + execute(params, ctx) : Result
  + deactivate() : void
}

class MCPToolAdapter {
  + discover() : ToolDescriptor
  .. tools/list RPC → descriptors ..
  + activate() : void
  .. spawn server, init JSON-RPC ..
  + execute(params, ctx) : Result
  .. tools/call RPC → result ..
  + deactivate() : void
  .. shutdown server ..
}

class AgentSkillAdapter {
  + discover() : ToolDescriptor
  .. parse SKILL.md frontmatter ..
  + activate() : void
  .. load SKILL.md body into agent context ..
  + execute(params, ctx) : Result
  .. agent follows instructions, runs scripts ..
  + deactivate() : void
  .. remove from context ..
}

class BuiltinAdapter {
  + discover() : ToolDescriptor
  .. return hardcoded descriptors ..
  + activate() : void
  .. no-op ..
  + execute(params, ctx) : Result
  .. call native Python impl ..
  + deactivate() : void
  .. no-op ..
}

MCPToolAdapter .up.|> UTI
AgentSkillAdapter .up.|> UTI
BuiltinAdapter .up.|> UTI
@enduml
MCPToolAdapter

Bridges external MCP servers into the tool model:

  1. discover(): Spawns the MCP server process (or connects to a remote Streamable HTTP endpoint), performs the MCP initialize handshake, negotiates capabilities, then calls tools/list to enumerate available tools. Each MCP tool becomes a separate ToolDescriptor with its inputSchema and inferred capability metadata.

  2. activate(): Ensures the MCP server process is running and the JSON-RPC connection is healthy. For remote servers, validates the authentication token. Registers for notifications/tools/list_changed so CleverAgents can dynamically update available tools.

  3. execute(): Translates a Tool.execute(params, ctx) call into an MCP tools/call JSON-RPC request. Before dispatching:

    • Rewrites file paths to sandbox-relative paths
    • Validates params against inputSchema
    • Checks capability metadata against plan access policy
    • Creates a checkpoint if the tool is marked checkpointable

    After the MCP tool returns its content[] response, the adapter:

    • Parses the result into the CleverAgents Result format
    • Records any resource mutations as Change objects in the plan's ChangeSet
  4. deactivate(): Sends a clean shutdown to the MCP server process and closes the JSON-RPC connection.

Capability inference: MCP tools expose limited metadata (name, description, inputSchema). The adapter infers extended capability metadata using heuristics:

  • Tools whose names contain read, get, list, search, findread_only: true
  • Tools whose names contain write, create, update, delete, setwrites: true
  • All inferences can be overridden via the overrides block in tool or skill YAML
AgentSkillAdapter

Bridges Agent Skills Standard (SKILL.md folders) into the tool model. Agent Skills are fundamentally different from MCP tools — they are instruction-driven rather than schema-driven. An Agent Skill is not a single function call; it is a bundle of procedural knowledge that an LLM agent loads into its context and follows.

  1. discover(): Scans the configured skill directory for a SKILL.md file. Parses only the YAML frontmatter (name, description, optional compatibility, metadata, allowed-tools) to produce a lightweight ToolDescriptor. This metadata is injected into the agent's system prompt in a structured format so the LLM can decide when the skill is relevant:

    
    <style="color: cyan; font-weight: 600;">available_agent_skills>
      <style="color: cyan; font-weight: 600;">agent_skill>
        <style="color: cyan; font-weight: 600;">name>deploy-to-staging</style="color: cyan; font-weight: 600;">name>
        <style="color: cyan; font-weight: 600;">description>Deploy the current branch to the staging environment.</style="color: cyan; font-weight: 600;">description>
        <style="color: cyan; font-weight: 600;">tool>local/deploy-staging</style="color: cyan; font-weight: 600;">tool>
      </style="color: cyan; font-weight: 600;">agent_skill>
    </style="color: cyan; font-weight: 600;">available_agent_skills>
    

    Discovery is low-cost — only ~50100 tokens per Agent Skill for metadata. The full instructions are not loaded until activation.

  2. activate(): When the LLM agent determines (or is instructed) that a task matches the skill's description, the adapter loads the full SKILL.md Markdown body into the agent's active context. This injects step-by-step instructions, examples, edge cases, and references to bundled scripts. The tool is now "active" — the agent has the procedural knowledge to execute it.

    If the skill references additional files (references/*.md, scripts/*.py, assets/*), these are made available on demand — the agent can read them as needed, following the Agent Skills Standard's progressive disclosure model.

  3. execute(): Unlike MCP tools (which are single RPC calls), Agent Skill execution is agent-mediated. The LLM agent follows the loaded instructions, potentially:

    • Running bundled scripts via shell execution (sandboxed)
    • Reading reference files for additional context
    • Using other available tools (e.g., built-in file operations) as part of the procedure
    • Making multiple tool calls in sequence to accomplish the workflow

    The adapter wraps this execution in a tool execution context so that all mutations are tracked, sandboxed, and checkpointable. Script execution respects the allowed_tools and sandbox_policy declared in the tool's YAML.

  4. deactivate(): Removes the skill's instructions from the agent's active context to free up token budget. The skill's metadata remains available for re-activation.

Key design principle: Agent Skills extend the agent's knowledge, not just its toolset. An Agent Skill can teach an agent a multi-step workflow that involves calling multiple other tools, making decisions based on intermediate results, and following domain-specific best practices — something a single MCP tool call cannot express.

BuiltinToolAdapter

Wraps CleverAgents' native resource operations as tools:

  1. discover(): Returns hardcoded ToolDescriptor objects for each built-in operation. These descriptors have fully specified capability metadata since the implementation is first-party.

  2. activate(): No-op. Built-in tools are always available.

  3. execute(): Calls the native Python implementation directly. Built-in tools operate through the resource abstraction layer, automatically integrating with sandbox path mapping, change tracking, and checkpointing.

  4. deactivate(): No-op.

Built-in tool groups:

File Operations (file_operations):


read_file(path: str) -> str
write_file(path: str, content: str) -> None
edit_file(path: str, edits: list[Edit]) -> None
delete_file(path: str) -> None
move_file(source: str, destination: str) -> None
copy_file(source: str, destination: str) -> None

Directory Operations (directory_operations):


create_directory(path: str) -> None
list_directory(path: str, pattern: str = "*") -> list[str]
delete_directory(path: str, recursive: bool = False) -> None

Search Operations (search_operations):


search_files(pattern: str, content_pattern: str = None) -> list[Match]
find_definition(symbol: str) -> list[Location]
find_references(symbol: str) -> list[Location]

Git Operations (git_operations, when resource is a git repository):


git_status() -> GitStatus
git_diff(path: str = None) -> str
git_log(count: int = 10) -> list[Commit]
git_blame(path: str) -> list[BlameLine]

Each built-in tool:

  • Has fully defined capability metadata
  • Operates through the resource abstraction layer
  • Automatically tracks changes to the ChangeSet
  • Respects sandbox boundaries and deny-lists

Tool Capability Metadata (Critical for Safety)

MCP's metadata is not sufficient (read-only/idempotent is not enough; write scope is unclear). CleverAgents extends every tool — regardless of source — with a uniform capability metadata schema:


capability:
  read_only: bool # Whether tool only performs read operations
  writes: bool # Whether tool can modify resources
  write_scope:             # What the tool is allowed to mutate
    - file_paths: ["src/**", "tests/**"]     # Path patterns within bound resources
    - resource_slots: ["repo", "db"]         # Resource slot names (from resource bindings)
    - environment:             # Execution environment compatibility
        required: container | host | any  # Where the tool CAN run (default: any)
        preferred: container | host        # Where the tool PREFERS to run (optional)
        specific: <resource-name>           # A specific container required (optional)
  idempotent: bool # Whether repeated calls produce same result
  checkpointable: bool # Whether tool supports checkpoint/rollback
  checkpoint_scope: str # What can be rolled back (file, transaction, commit, snapshot)
  side_effects:            # Non-reversible effects
    - install_packages
    - mutate_infra
    - send_email
  cost_profile:            # Usage constraints
    rate_limit: "10/min"
    estimated_cost: "$0.01/call"
  human_approval_required: bool # Whether a human must approve invocation

Where metadata comes from per source:

Source Metadata origin Override mechanism
Built-in Hardcoded in implementation Not overridable (authoritative)
MCP Inferred from tool name/description + MCP annotations overrides block in tool or skill YAML; override at skill/actor reference point
Agent Skill Declared in SKILL.md frontmatter metadata + inferred from allowed-tools Tool YAML capability block; override at skill/actor reference point
Custom Manually declared in tool YAML capability block override at skill/actor reference point (author is the source of truth for registered values)

Read-Only Actions and Tool Access Control

When an action is marked read_only: true, it can only use tools that have read_only: true in their capability metadata. This is enforced at runtime by the tool execution context — any attempt to invoke a tool with writes: true from a read-only plan raises a AccessDeniedError.

Tool Execution Flow

When an LLM agent decides to use a tool (regardless of source), the following flow occurs through the unified execution pipeline:


1. LLM generates tool call
   e.g., edit_file(path="src/main.py", changes=[...])
   or:   local/github.create_issue(title="Bug fix", body="...")
   or:   (activates Agent Skill "deploy-to-staging" via instructions)
                                ↓
2. Tool Router receives call
   - Resolves tool by name from the Tool Registry or actor's skill tool sets
   - Validates parameters against inputSchema
    - Checks capability metadata against plan's access policy:
     • Is this tool in allowed skill categories?
     • Does the plan allow writes?
     • Is human approval required?
   - If denied → return AccessDeniedError to LLM
                                ↓
3. Resource Binding Resolution & Sandbox Context
   - Resolve resource bindings for this tool:
     • Static bindings: already resolved at registration
     • Contextual bindings: resolve from plan's project resources
     • Parameter bindings: resolve from invocation arguments
   - Validate resource type compatibility for each slot
   - Validate access mode (e.g., read_write tool on read_only resource → error)
   - Ensure sandbox exists for each bound resource (lazy sandboxing)
   - Maps logical paths to sandbox-relative paths via bound resource handlers
   - Inject bound resources into ctx.resources[slot_name]
   - If tool is checkpointable → create pre-execution checkpoint
                                ↓
4. Adapter-Specific Execution
   - MCP: sends tools/call JSON-RPC to server process
   - Agent Skill: agent follows loaded SKILL.md instructions,
     running scripts and tools in sandboxed shell
   - Built-in: calls native Python implementation directly
   - Custom: executes inline code with sandboxed context
                                ↓
5. Change Recording
   - If tool modified resources → create Change record(s)
   - Append Change(s) to plan's ChangeSet
   - Update sandbox state
   - If checkpointable → record checkpoint for rollback
                                ↓
6. Result Return
   - Normalize result to uniform Result type
   - Return to LLM agent for continued reasoning

Change Tracking from Tool Invocations

Critical Architecture Point: The ChangeSet is NOT built by parsing LLM output. It is built by recording the effects of tool invocations:


class ToolExecutionContext:
    """Context provided to every tool execution, regardless of source."""
<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">__init__</span>(self, plan: Plan, sandbox: Sandbox,
             resources: <span style="color: cyan;">dict</span>[<span style="color: cyan;">str</span>, BoundResource]):
    self.plan = plan
    self.sandbox = sandbox
    self.resources = resources    <span style="opacity: 0.7;"># slot_name → BoundResource</span>
    self.changes: <span style="color: cyan;">list</span>[Change] = []

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">record_change</span>(self, change: Change) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Record a change made by a tool.&quot;&quot;&quot;</span>
    self.changes.append(change)
    self.plan.changeset.add_change(change)

class WriteFileTool: """Example: built-in tool for writing files."""

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">execute</span>(self, path: <span style="color: cyan;">str</span>, content: <span style="color: cyan;">str</span>, ctx: ToolExecutionContext) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    handler = ctx.sandbox.get_handler(path)
    change = handler.write(path, content, ctx.sandbox)
    ctx.record_change(change)

This approach means:

  • Every resource modification is explicit and tracked
  • The ChangeSet accurately reflects what was done, not what was said
  • Rollback is precise (replay inverse of recorded changes)
  • Audit logs show exactly which tool invocation produced each change

MCP Integration Details

!!! adr "Architecture Decision" MCP adoption, tool-to-skill mapping, and actor-graph usage are defined in ADR-029: Model Context Protocol (MCP) Adoption.

CleverAgents integrates with the Model Context Protocol (MCP) to discover and invoke external tools, then normalizes them into the Tool Registry with extended capability metadata and sandbox-aware execution. MCP tools are composed into skills and appear as tool nodes in actor graphs, giving them the same lifecycle, change-tracking, and checkpoint semantics as built-in tools.

MCP Concepts Mapping
MCP Concept CleverAgents Equivalent Extension
Tool Independently registered Tool (source: mcp), referenced by skills Extended capability metadata (write_scope, checkpointable, side_effects)
Resource Resource Extended to support both read AND write operations
Prompt Action template Full plan lifecycle (Strategize → Execute → Apply)
Server MCP Server connection (declared in tool or skill YAML) Managed by MCPToolAdapter with lifecycle and reconnection
JSON-RPC Internal adapter protocol Abstracted behind Tool.execute()
MCP Server Lifecycle Management

CleverAgents manages MCP server processes as part of the tool/skill/actor lifecycle:

sequenceDiagram
    participant CLI as CLI
    participant Reg as Registry
    participant ActorRT as Actor Runtime
    participant Adapter as MCPToolAdapter
    participant MCP as MCP Server

    note over CLI,Reg: Phase 1 - Registration
    CLI->>Reg: agents tool add or skill add
    Reg->>Reg: Validate server command or endpoint
    Reg->>Reg: Store server config in record

    note over ActorRT,MCP: Phase 2 - Actor Activation
    ActorRT->>Adapter: Activate (for each MCP server)
    Adapter->>MCP: Spawn process (stdio) or connect (HTTP)
    Adapter->>MCP: MCP initialize handshake
    MCP-->>Adapter: Capabilities
    Adapter->>MCP: tools list
    MCP-->>Adapter: Tool descriptors
    Adapter->>MCP: Subscribe notifications tools list_changed

    note over ActorRT,MCP: Phase 3 - Execution
    ActorRT->>Adapter: LLM generates tool call
    Adapter->>MCP: tools call (JSON-RPC)
    MCP-->>Adapter: Result
    Adapter->>ActorRT: Result + Change tracking

    note over ActorRT,MCP: Phase 4 - Deactivation
    ActorRT->>Adapter: Deactivate
    Adapter->>MCP: Clean shutdown
Sandbox Path Rewriting for MCP Tools

MCP servers operate on real filesystem paths, but CleverAgents executes plans in sandboxes. The MCPToolAdapter transparently rewrites paths:


# Before sending to MCP server:
# Logical path: "src/main.py"
# Sandbox path: "/tmp/sandbox-01HXM/worktree/src/main.py"
# The adapter rewrites the tool arguments so the MCP server
# operates on sandboxed state without knowing about the sandbox.

This ensures MCP tools respect sandbox boundaries even though they have no awareness of the CleverAgents sandbox model.

Agent Skills Integration Details

!!! adr "Architecture Decision" The Agent Skills standard integration is defined in ADR-028: Agent Skills Standard (AgentSkills.io).

Agent Skills follow the Agent Skills standard from https://AgentSkills.io, which defines the SKILL.md structure and the progressive disclosure model used by the runtime. Within actor graphs, Agent Skills appear as tool nodes — the actor runtime loads the skill's instructions into the LLM context when the node activates, enabling the agent to follow multi-step procedures that may invoke MCP tools, built-in tools, or other Agent Skills during execution.

Discovery and Progressive Disclosure

Agent Skills follow a three-tier progressive disclosure model that maps directly to the tool lifecycle:

Tier What loads When Token cost
Metadata name + description from SKILL.md frontmatter Tool registration / actor activation ~50100 tokens per Agent Skill
Instructions Full SKILL.md Markdown body When LLM determines task matches the skill (activate phase) Recommended < 5000 tokens
Resources scripts/, references/, assets/ On demand during execution Variable

This means an actor can have dozens of Agent Skill tools available but only pay the token cost for their metadata at startup. Full instructions load only when relevant.

Agent Skills vs. MCP Tools: When to Use Which
Dimension MCP Tools Agent Skills
Interaction model Single function call with JSON params/result Multi-step procedure with instructions the agent follows
Knowledge type "Here is a function you can call" "Here is how to accomplish a complex task"
Statefulness Stateless per-call Stateful across multiple tool calls within a procedure
Authoring Implement an MCP server (code) Write a SKILL.md file (prose + optional scripts)
Portability Any MCP-compatible host Any Agent Skills-compatible agent
Best for Atomic operations (CRUD, queries, API calls) Complex workflows (code review processes, deployment procedures, data analysis pipelines)

Both can coexist in the same skill. A common pattern is an Agent Skill tool that teaches the agent a workflow which involves calling multiple MCP tools:


Agent Skill Tool: "local/deploy-staging"
  SKILL.md instructions:
    1. Run tests using the built-in shell tool
    2. Create a PR using the GitHub MCP tool (create_pull_request)
    3. Wait for CI using the GitHub MCP tool (get_check_runs)
    4. Deploy using the AWS MCP tool (ecs_update_service)
    5. Verify deployment using the HTTP MCP tool (fetch_url)

Tool-Level Checkpointability

Each tool declares whether it supports checkpoint/rollback via its capability metadata. Checkpointing behavior varies by source:

Source Checkpoint mechanism
Built-in file ops Snapshot file state pre-modification; rollback restores file content
Built-in git ops Create commit or stash; rollback via git reset / git checkout
MCP tools Adapter-created checkpoints of affected resources; rollback replays inverse operations where possible
Agent Skills Composite — each sub-tool-call within the skill's execution is individually checkpointed
Custom (containerized) Filesystem snapshot or container image layer; rollback restores snapshot

Checkpointing is easier when tool scope is constrained (e.g., "only files within a sandbox worktree + git"). The capability metadata's checkpoint_scope field communicates what granularity of rollback is supported.

When require_checkpoints is enabled on a plan, the plan may only use tools that have checkpointable: true. If disabled, the plan may use more generic/unsafe tools with fewer restrictions (useful for low-stakes tasks).

Validation

!!! adr "Architecture Decision" The validation system, gate types, and validation lifecycle are defined in ADR-013: Validation Abstraction.

What a Validation Is

!!! abstract "Validation in Brief" A Validation is a specialized subtype of Tool designed to verify that work performed during plan execution meets specified quality, correctness, or compliance criteria. A Validation observes the current state of resources, evaluates a condition, and returns a structured ==pass/fail result==. It never modifies resources — it only reads and reports.

Because Validation extends Tool via standard class inheritance, it is not a separate concept bolted onto the system — it is a tool, with additional constraints and metadata. This means validation benefits from the entire tool infrastructure: source types, resource bindings, lifecycle hooks, input/output schemas, registry management, skill composition, and actor graph integration. No parallel system is required.

!!! success "Safety Guarantee" Validations are ==always safe to run==. They cannot cause side effects, corrupt state, or require rollback. This enables aggressive parallelization, speculative validation, and retry without concern.

Relationship to Tool

A Validation inherits all base Tool properties:

Inherited from Tool Additional in Validation
name (namespaced) mode (required or informational)
description Structured return format enforcement
source (custom, mcp, agent_skill, builtin, wrapped) Read-only enforcement (writes: false, checkpointable: false)
input_schema (JSON Schema) Attachment scoping (resource-centric, with optional project/plan scope)
output_schema (JSON Schema) Attachment ULID (system-assigned per attachment)
capability metadata wraps (optional reference to an existing Tool)
resource_slots (resource bindings) transform (Python function to convert wrapped Tool output to validation format)
lifecycle hooks (discover/activate/execute/deactivate)
code (inline implementation)
mcp_server / mcp_tool_name (MCP source)
timeout
idempotent

Shared namespace: Validations and plain Tools share the same naming namespace in the Tool Registry. A name like local/run-tests can refer to either a Tool or a Validation — but never both simultaneously. Attempting to register a Validation with a name already taken by a plain Tool (or vice versa) results in a conflict error. This shared namespace enables unified management: agents tool list shows both types, agents tool show works for both, and agents tool remove removes either type.

Superset/subset semantics: Because Validation extends Tool, a Validation can be used anywhere a Tool is expected — it can appear in a skill's tool list, serve as a tool node in an actor graph, or be invoked directly by an LLM agent. However, a plain Tool cannot be used where a Validation is specifically required. For example, agents validation attach rejects a name that resolves to a plain Tool rather than a Validation. This distinction is enforced by the type discriminator stored in the Tool Registry: each entry is tagged as either tool or validation.

Read-Only Enforcement

!!! danger "Hard Constraint — Not a Convention" Validations are always read-only:

| Property | Value | Reason |
| :------- | :---: | :----- |
| `writes` | `false` | Must never modify, create, or delete resources |
| `checkpointable` | `false` | No state changes → nothing to checkpoint |
| `read_only` | `true` | Implicit inverse of `writes` |

This constraint is enforced at ==multiple levels==:

  1. Registration time: The agents validation add command does not accept --writes/--no-writes or --checkpointable/--no-checkpointable flags. Any such values in the validation YAML are silently overridden to false.
  2. Runtime enforcement: The execution runtime treats validation invocations as read-only operations. Resource bindings are resolved with access: read_only semantics. If a validation's implementation attempts to write, the sandbox enforces the constraint and the invocation fails.
  3. Sandbox interaction: When a validation executes within a plan's sandbox, it reads the sandbox state but cannot modify it — verifying work-in-progress without risk of corruption.

The read-only guarantee has an important architectural consequence: validations are always safe to run. They cannot cause side effects, corrupt state, or require rollback. This enables aggressive parallelization, speculative validation (running validations before all work is complete to provide early feedback), and retry without concern for accumulated state changes.

Validation Mode

Each Validation has a mode that determines how the system responds to failure:

=== "Required (mode: required)"

!!! danger "Hard Gate"
    Required validations ==must pass== for execution to proceed to the Apply phase. They define the "definition of done" for a plan.

- When a required validation fails, the execution actor attempts to **fix the underlying issue** (e.g., fix failing tests, correct lint errors) within the bounds of the current strategy, then re-runs the validation.
- This fix-then-revalidate loop continues up to the configured retry limit.
- If self-fix is exhausted, the actor may request a strategy revision or escalate to the user, depending on the automation profile.

=== "Informational (mode: informational)"

!!! info "Advisory Check"
    Informational validations are ==recorded but do not block== execution. Useful for advisory checks where failure is noteworthy but not blocking.

- When an informational validation fails, the result (including `message` and `data`) is included in the plan's validation summary for human review, but the plan continues normally.
- Use cases: bundle size reports, code complexity metrics, deprecation warnings, performance benchmarks.

??? tip "Gradual Promotion Pattern" The distinction between required and informational allows teams to introduce new validations gradually — start as ==informational== to measure impact, then promote to ==required== once the codebase is clean.

The mode is set at registration time (in the validation YAML or via --required/--informational on agents validation add) and applies globally to that validation. The mode cannot be overridden per-attachment — if different scopes need different enforcement levels for the same check, register separate validations with different names and modes.

Structured Return Format

Every Validation must return a JSON object conforming to this structure:

{
  "passed": true,
  "message": "All 247 tests passed, 94% coverage",
  "data": {
    "tests_run": 247,
    "tests_passed": 247,
    "tests_failed": 0,
    "coverage_percent": 94.2,
    "coverage_threshold": 80,
    "duration_seconds": 12.4
  }
}
Field Type Required Description
passed boolean Yes Whether the validation passed (true) or failed (false). This is the only field that drives system behavior (gating for required, recording for informational).
message string No Human-readable summary of the result. Displayed in CLI output, plan summaries, and used by the execution actor to understand failures. Should be concise but actionable (e.g., "3 lint errors in src/api/handler.py" rather than just "failed").
data object No Arbitrary structured data in any format the validation chooses. This is the validation's primary output channel for rich, machine-readable results. Examples: test coverage reports, lint error lists with file/line/column, security vulnerability details, performance measurement data. The data field has no required schema — each validation defines its own structure.

Return format enforcement: If a validation returns output that is not valid JSON, or valid JSON that lacks the passed boolean field, the system treats the invocation as an error (distinct from a validation failure). The validation result is recorded as "passed": false with a system-generated message indicating the malformed return, and the original output is preserved in data.raw_output for debugging.

The structured return format serves multiple consumers:

  • The execution actor reads message and data to understand failures and attempt fixes. A well-structured data field (e.g., with file paths and line numbers) enables more targeted fix attempts.
  • The plan summary records all validation results for human review and auditing.
  • Downstream automation (CI scripts, dashboards) can parse data programmatically via agents --format json plan show.

Attachment Scoping

Validations are not globally active — they must be explicitly attached to one or more scopes to take effect. This attachment model provides fine-grained control over which validations run for which work.

Attachment Model

A validation is always attached to a resource. The optional --project or --plan flag controls the scope under which the validation is active for that resource:

  1. Direct attachment (agents validation attach <RESOURCE> <VALIDATION> [args...])

    The validation is always active when that resource is accessed by any plan, regardless of which project or plan is involved. This scope is for invariants that must hold for a resource in all contexts — e.g., a lint check that must pass for a repository no matter what project is using it.

    Use cases:

    • Repository-wide lint/format checks
    • Schema validation on database resources
    • Security scanning on any codebase resource
    • License compliance checks
  2. Project-scoped attachment (agents validation attach --project <PROJECT> <RESOURCE> <VALIDATION> [args...])

    The validation is active only for that resource when it is being interacted with through the specified project. Different projects using the same resource can have different validation requirements. This is the most common attachment scope.

    Use cases:

    • Unit test suites specific to a project
    • Type checking for a TypeScript project (not relevant for a Python project using the same repo)
    • API compatibility checks for a microservice project
    • Bundle size checks for a frontend project
  3. Plan-scoped attachment (agents validation attach --plan <PLAN_ID> <RESOURCE> <VALIDATION> [args...])

    The validation is active only for that resource when it is being interacted with through the specified plan. This is the most targeted scope, useful for one-off or experimental validations.

    Use cases:

    • Temporary extra checks during a risky migration
    • One-time regression validation for a specific bug fix
    • Experimental validations being tested before promoting to project scope
    • User-added ad hoc checks during an interactive session

Since validations accept arguments, the same validation can be attached to the same resource multiple times with different arguments. For example, local/run-tests might be attached to local/api-repo through project local/api-service with --coverage-threshold 90 and also through project local/staging with --coverage-threshold 70.

Attachment Resolution

When a plan executes, the system collects all applicable validations for each resource the plan accesses. The resolution algorithm:

  1. Collect direct validations: For every resource the plan accesses, collect all validations directly attached to that resource (no scope flag).
  2. Collect project-scoped validations: For every resource the plan accesses, collect all validations attached to that resource with a --project scope matching the plan's target project.
  3. Collect plan-scoped validations: For every resource the plan accesses, collect all validations attached to that resource with a --plan scope matching the plan itself.
  4. Union: Take the union of all collected validations per resource. If the same validation name appears from multiple scopes for the same resource (e.g., attached both directly and through a project), each attachment is treated as a distinct invocation — they may have different arguments and thus produce different results. When the same validation name appears from multiple scopes with identical arguments, the most specific scope wins (plan > project > direct) and the validation runs once.
  5. Child plan inheritance: Child plans (spawned via subplan_spawn or subplan_parallel_spawn) inherit their parent plan's project-scoped and plan-scoped resource/validation associations by default. Direct validations are collected independently based on which resources the child plan accesses (which may differ from the parent).
Attachment Identity

Each attachment is identified by a system-assigned ULID. This ULID is:

  • Returned by agents validation attach when the attachment is created.
  • Displayed by agents tool show <VALIDATION> in the "Attached To" section and by agents project show in the validations list.
  • Required by agents validation detach <ATTACHMENT_ID> to remove a specific attachment.

The ULID is necessary because the same validation can be attached to the same resource multiple times — directly, through different projects, through different plans, and even to the same resource and scope with different arguments. The ULID uniquely identifies which specific attachment to remove.

Attachment Arguments

When attaching a validation, optional arguments can be provided after the validation name:


$ agents validation attach --project local/api-service local/api-repo local/run-tests --coverage-threshold 90
$ agents validation attach --project local/staging-api local/api-repo local/run-tests --coverage-threshold 70

These arguments are stored with the attachment and passed to the validation tool's input_schema at execution time. This enables a single validation definition to be reused across scopes with different thresholds or configurations. If no arguments are provided, the validation uses its input_schema defaults.

Validation Lifecycle in Plan Execution

Validation participates in the plan lifecycle at well-defined points:

During Strategize

Validations are not executed during Strategize. However, the strategy actor is aware of which validations are attached (via the attachment resolution described above) and factors them into the strategy:

  • The strategy may allocate time/tokens for validation fix-up loops.
  • The strategy may note that certain validations are informational and thus non-blocking.
  • The decision tree may include validation_checkpoint decisions that mark points where validation should be run.
During Execute

Validation is the final step of the Execute phase. The execution actor's workflow proceeds as follows:

  1. Perform work: The actor executes the strategy — writing code, modifying resources, running tools — all within the sandbox.
  2. Collect applicable validations: The system resolves all validations from the resource-direct, project, and plan scopes (as described in Attachment Resolution above).
  3. Execute validations: Each applicable validation is invoked as a standard tool call. The validation reads the sandbox state and returns its structured result. Validations may be run in parallel since they are read-only and cannot interfere with each other.
  4. Process results:
    • All required validations pass: Execution is complete. The plan transitions to the review/Apply phase.
    • Any required validation fails: The execution actor enters the fix-then-revalidate loop (see Validation Failure Handling below).
    • Informational validations fail: Results are recorded in the plan's validation summary. No fix attempts are made.
Validation is Not Run During Apply

!!! warning "Apply is the Point of No Return" Validation runs during Execute, not during Apply. By the time a plan reaches Apply, all required validations have already passed. Apply commits the sandbox changes to the real resources.

If post-apply verification is needed (e.g., integration tests against the real system), implement it as a separate plan or external CI pipeline — ==not as a validation attachment==.

**Rationale**: Introducing validation during Apply would create situations where committed changes might need to be rolled back, defeating the purpose of the sandbox model.

Validation Failure Handling

Because validations are tools, their execution follows the standard tool invocation flow. Each validation returns a structured JSON result with { "passed": true/false, "message": "...", "data": {...} }. The execution actor interprets these results based on the validation's mode.

Required Validation Failure

When a required validation returns "passed": false during Execute:

  1. Diagnosis: The execution actor examines the validation's message and data fields to understand the nature of the failure. A well-structured data field (e.g., with specific file paths, line numbers, error messages) enables more targeted fix attempts.

  2. Self-fix attempt: The execution actor attempts to fix the issue within the bounds of the current strategy. For example:

    • A failing test: the actor reads the test output, identifies the broken assertion, and fixes the code.
    • A lint error: the actor reads the lint report and corrects the formatting or style violation.
    • A type error: the actor reads the type checker output and fixes the type mismatch.
  3. Re-validation: After each fix attempt, the actor re-invokes the failing validation. If it passes, the loop ends. If it still fails, the actor examines the new message and data for the updated failure state.

  4. Retry limit: The fix-then-revalidate loop runs up to the configured retry limit (default: 3 attempts, configurable per plan or in the automation profile). After the limit is reached, self-fix stops.

  5. Strategy revision: If the actor determines that the failure cannot be resolved within the current strategy's constraints (e.g., the strategy says "modify only handler.py" but the test failure requires changes to model.py), it may request a strategy revision. This triggers a re-run of the Strategize phase for the affected subtree of the decision tree. Whether this happens automatically or requires user approval depends on the automation profile's delete_content flag.

  6. Escalation: If strategy revision also fails, or if the automation profile requires human approval for strategy changes, the plan pauses and requests user guidance via agents plan prompt:

    
    agents plan prompt <plan_id> "Try using mock objects for the database tests"
    
  7. Terminal failure: If the user does not intervene (or explicitly cancels), the plan fails with state: failed. The sandbox is preserved for inspection.

The flow can be summarized as:


validatefailfixre-validatefailfixre-validate → ... → retry limitrequest strategy revisionre-strategizere-executevalidatestill failingescalate to useruser provides guidanceresumestill failingplan fails
Informational Validation Failure

When an informational validation returns "passed": false:

  1. The result (including message and data) is recorded in the plan's validation_summary.
  2. Execution continues normally — no fix attempts, no blocking, no escalation.
  3. The informational failure is visible in agents plan show, plan summaries, and any rendering format.
  4. Informational failures may trigger notifications (if configured) but never block plan progression.
Validation Error vs. Validation Failure

!!! note "Important Distinction" | Condition | Meaning | Handling | | :-------- | :------ | :------- | | Validation failure (passed: false) | Validation ran successfully, condition not met | Mode-specific (fix loop for required, record for informational) | | Validation error | Validation itself failed to execute (runtime exception, timeout, malformed return) | ==Always treated as required failure== regardless of mode |

When a validation error occurs:

  • The result is recorded with passed: false, a system-generated message describing the error, and the raw error details in data.
  • If the validation is required, the fix-then-revalidate loop attempts to resolve the error (e.g., by fixing a broken test configuration).
  • If the validation is informational, the error is still recorded but does not block.
  • Repeated validation errors (e.g., the validation tool itself is misconfigured) are surfaced prominently in the plan summary.

Validation in Actor Graphs

Because Validations are Tools, they can appear as tool nodes in actor graphs. This enables explicit, deterministic validation steps within workflows:


# Actor graph with explicit validation nodes
graph:
  nodes:
    - name: implement
      type: agent
      actor: local/code-writer
    - name: run_tests
      type: tool
      tool: local/run-tests       # This is a Validation (subtype of Tool)
    - name: lint_check
      type: tool
      tool: local/lint-check      # Also a Validation
    - name: fix_issues
      type: agent
      actor: local/code-fixer
  edges:
    - [implement, run_tests]
    - [implement, lint_check]
    - condition: "not run_tests.passed or not lint_check.passed"
      from: [run_tests, lint_check]
      to: fix_issues
    - [fix_issues, run_tests]    # Retry loop
    - [fix_issues, lint_check]

When a Validation appears as a tool node, its return value is available to downstream nodes via the standard graph data flow. The passed, message, and data fields can be used in edge conditions, passed as input to subsequent nodes, or logged for the plan summary.

This graph-based approach complements the attachment model: attached validations run automatically at the end of Execute (the system handles collection and invocation), while graph-embedded validations run at explicitly defined points in the workflow (the actor author controls placement). Both approaches can coexist — a plan may have attached validations that run at the end plus graph-embedded validations that run at intermediate checkpoints.

Validation and Skills

Validations can be included in skills just like any other tool. Since a Validation is a Tool, a skill YAML can reference it by name:


# Skill: local/python-quality
skill:
  name: local/python-quality
  description: "Python code quality tools and validations"
  tools:
    - local/run-tests         # Validation (required)
    - local/lint-check        # Validation (required)
    - local/type-check        # Validation (required)
    - local/format-code       # Plain Tool (writes)
    - local/run-benchmarks    # Validation (informational)

When an actor references this skill, the Validations are available as tools that the LLM agent can call. This is distinct from validation attachment — being included in a skill makes the validation callable by the actor's LLM, but does not make it a required gate. Attachment determines gating; skill inclusion determines availability.

A common pattern: a team's skill includes their validations for on-demand use by the LLM (the actor can run tests proactively during development), while the same validations are also attached to the project (ensuring they run as a mandatory gate at the end of Execute even if the LLM forgot to run them).

Tool Wrapping

A Validation can wrap an existing Tool, reusing its implementation without duplicating code. This is the recommended approach when a plain Tool already performs the work a Validation needs (e.g., running tests, generating a report, scanning for vulnerabilities) and the Validation only needs to interpret the Tool's output as pass/fail.

The Problem

Consider a team that already has a registered Tool called local/run-tests — it runs the test suite and returns a structured report (test counts, coverage data, output logs). Now they want a Validation that gates execution on all tests passing. Without wrapping, they would need to duplicate the entire test-running implementation inside a new Validation YAML, differing only in the final pass/fail interpretation. This duplication creates maintenance burden: if the test runner configuration changes, both the Tool and the Validation must be updated.

The Solution: wraps + transform

A Validation YAML can declare a wraps field that references an existing registered Tool by name. At execution time, the system invokes the wrapped Tool, captures its output, and passes that output through a transform function that produces the Validation's structured return format ({ "passed", "message", "data" }).


# validations/tests-pass.yaml
# Wraps the existing local/run-tests tool — no test logic duplicated

name: local/tests-pass description: "Validate that all unit tests pass (wraps local/run-tests)"

wraps: local/run-tests

transform: | def transform(tool_output): passed = tool_output.get("returncode") == 0 return { "passed": passed, "message": "All tests passed" if passed else f"Tests failed (exit {tool_output.get('returncode')})", "data": tool_output }

validation: mode: required

timeout: 600

When local/tests-pass is invoked (either by attachment or direct call):

  1. The system resolves wraps: local/run-tests to the registered Tool.
  2. The Validation's input arguments are mapped to the wrapped Tool's input arguments using the argument_mapping (see below). If no argument_mapping is defined, the Validation's input arguments are passed through to the wrapped Tool as-is.
  3. The wrapped Tool is invoked with the mapped arguments.
  4. The wrapped Tool's output is captured (whatever it returns — JSON, text, etc.).
  5. The transform function is called with the captured output.
  6. The transform function returns the Validation's structured result ({ "passed", "message", "data" }).

This is opaque to the consumer — the caller sees only a standard Validation with its structured return format. The wrapping is an implementation detail.

What Gets Inherited

When a Validation uses wraps, the following properties are inherited from the wrapped Tool unless explicitly overridden in the Validation YAML:

Property Inherited? Override behavior
input_schema Yes If the Validation YAML defines input_schema, it replaces the wrapped Tool's schema entirely. If omitted, the Validation accepts the same inputs as the wrapped Tool. When a custom input_schema is defined, an argument_mapping should also be provided to map the Validation's inputs to the wrapped Tool's expected inputs.
argument_mapping No Not inherited. When present, maps the Validation's input arguments to the wrapped Tool's input arguments. When absent, input arguments are passed through unchanged. See the Argument Mapping section.
resource_slots Yes If the Validation YAML defines resource_slots, they replace the wrapped Tool's slots. If omitted, the Validation inherits the same resource bindings.
timeout Yes If the Validation YAML specifies timeout, it overrides. If omitted, the wrapped Tool's timeout is used.
description No The Validation must provide its own description (it serves a different purpose than the wrapped Tool).
source No Ignored — the source is implicitly wrapped. The Validation does not declare source or code.
capability No Overridden — writes and checkpointable are forced to false regardless of the wrapped Tool's values.
The transform Function

The transform field contains a Python function that converts the wrapped Tool's output to the Validation's return format. The function signature is:


def transform(tool_output) -> dict:
    """
    Args:
        tool_output: The wrapped Tool's return value. The type depends on the
                     Tool's implementation — typically a dict (for JSON-returning
                     tools) but may be a string or other type.
    Returns:
        A dict with at minimum {"passed": bool}. May also include
        "message" (str) and "data" (any).
    """

The transform function runs in a sandboxed Python environment with no write access to the filesystem or network. It is a pure data transformation — it receives the Tool's output and produces the Validation's result. If the transform function raises an exception, the Validation is treated as an error (see Validation Error vs. Validation Failure).

If transform is omitted, the system applies a default transform that expects the wrapped Tool's output to already contain a passed field:

  • If the output is a dict with a passed boolean, it is used as-is (pass-through).
  • If the output is a dict without passed, the validation errors with a message indicating the wrapped tool's output is not in validation format and a transform function is required.
  • If the output is not a dict, the validation errors similarly.
Argument Mapping (argument_mapping)

When a Validation wraps a Tool, the Validation may accept different input arguments than the wrapped Tool. The argument_mapping field specifies how the Validation's input arguments map to the wrapped Tool's expected input arguments. This is necessary when:

  • The Validation's input_schema defines different field names than the wrapped Tool's input_schema.
  • The Validation accepts a subset of the wrapped Tool's arguments and wants to provide fixed values for the rest.
  • The Validation renames arguments for clarity in the validation context.

The argument_mapping is a dictionary where keys are the wrapped Tool's input parameter names and values are either:

  • A string referencing a Validation input parameter name (forwarded as-is).
  • A fixed literal value (string, number, boolean) that is always passed to the wrapped Tool regardless of the Validation's inputs.

# Example: Validation wraps local/run-tests but renames arguments
name: local/tests-pass
wraps: local/run-tests

argument_mapping: test_directory: source_dir # Forward Validation's "source_dir" → Tool's "test_directory" coverage_enabled: true # Always pass true for coverage_enabled verbose: false # Always pass false for verbose

transform: | def transform(tool_output): return {"passed": tool_output.get("returncode") == 0, "message": "Tests completed"}

validation: mode: required

When argument_mapping is omitted, the Validation's input arguments are passed through to the wrapped Tool unchanged. This is the common case when the Validation inherits the wrapped Tool's input_schema without modification (i.e., the Validation YAML does not define its own input_schema).

When argument_mapping is present, only the mapped arguments are forwarded to the wrapped Tool. Any Validation input arguments not referenced in the mapping are not passed to the wrapped Tool. Any wrapped Tool arguments not listed as keys in the mapping receive no value (and must either be optional in the Tool's schema or have defaults).

Read-Only Semantics for Wrapped Tools

Validations are always read-only, but the wrapped Tool may not be. The wrapping enforces read-only semantics:

  • The wrapped Tool is invoked within the Validation's read-only execution context. Resource bindings are resolved with access: read_only regardless of what the wrapped Tool's resource_slots specify.
  • If the wrapped Tool's implementation attempts to write (e.g., mutate files, insert database rows), the sandbox enforces the read-only constraint and the invocation fails as a validation error.
  • At registration time, if the wrapped Tool has writes: true, the system emits a warning (not an error) advising the user that the wrapped Tool is marked as writing and may fail at runtime under read-only enforcement. This allows wrapping tools that are conservatively marked writes: true but whose actual behavior for the given inputs is read-only.

This design means wrapping is always safe from the Validation perspective — the worst case is a runtime error, never an unintended write.

Multiple Validations Wrapping the Same Tool

The same Tool can be wrapped by multiple Validations with different transform functions and different modes. This is a common pattern:


# The base tool runs tests and produces a comprehensive report
# Tool: local/run-tests (already registered)

# Validation 1: All tests must pass (required) # validations/tests-pass.yaml name: local/tests-pass description: "All unit tests must pass" wraps: local/run-tests transform: | def transform(tool_output): return { "passed": tool_output.get("returncode") == 0, "message": f"{tool_output.get('tests_passed', 0)}/{tool_output.get('tests_run', 0)} tests passed", "data": tool_output } validation: mode: required


# Validation 2: Coverage must exceed threshold (informational for now)
# validations/coverage-check.yaml
name: local/coverage-check
description: "Coverage must exceed threshold (advisory)"
wraps: local/run-tests
transform: |
  def transform(tool_output):
      coverage = tool_output.get("coverage_percent", 0)
      threshold = 80
      return {
          "passed": coverage >= threshold,
          "message": f"Coverage: {coverage}% (threshold: {threshold}%)",
          "data": {"coverage_percent": coverage, "threshold": threshold}
      }
validation:
  mode: informational

Both validations wrap the same local/run-tests tool but extract different signals. When both are attached to the same project, each Validation independently invokes the wrapped Tool and applies its own transform function. A future optimization may deduplicate these invocations when the forwarded arguments are identical (see Validation Ordering and Parallelism).

When to Wrap vs. When to Write From Scratch
Scenario Approach
An existing Tool already does the work; you just need pass/fail Wrap it — use wraps + transform
Multiple Validations need different interpretations of the same Tool's output Wrap it multiple times with different transforms
The validation logic is simple and self-contained (e.g., run a shell command, check exit code) Write from scratch — use source: custom with inline code
The validation needs logic that the existing Tool doesn't provide (different inputs, different execution) Write from scratch — the Tool's implementation isn't relevant
The existing Tool has side effects that are incompatible with read-only enforcement Write from scratch — wrapping would fail at runtime

Validation Data Model

The validation-related fields stored in the plan data model:

Field Location Description
validation_summary plan.execution Array of validation results collected during Execute. Each entry includes the validation name, mode, passed, message, data, execution duration, and attempt number.
final_validation_results plan.apply Snapshot of the final validation state at the time execution completed (all required validations passing). Identical to the last state of validation_summary but stored separately for quick reference.
validation_attempts plan.execution Total number of validation invocations across all fix-then-revalidate loops. Useful for understanding how much effort was spent on validation remediation.
validation_fix_history plan.execution Per-validation log of fix attempts. Each entry records: the validation that failed, the fix the actor applied, and whether the subsequent re-validation passed. Enables auditing of the fix-then-revalidate process.

A validation result entry in validation_summary:


{
  "validation": "local/run-tests",
  "mode": "required",
  "passed": true,
  "message": "All 247 tests passed, 94% coverage",
  "data": {
    "tests_run": 247,
    "tests_passed": 247,
    "coverage_percent": 94.2
  },
  "duration_ms": 12400,
  "attempt": 2,
  "attachment_id": "01HXM5A1B2C3D4E5F6G7H8J9K0",
  "attachment_resource": "local/api-repo",
  "attachment_scope": "project",
  "attachment_scope_target": "local/api-service"
}

Validation Ordering and Parallelism

Since validations are read-only, they are inherently safe to run in parallel. The execution runtime may parallelize validation invocations subject to:

  • Resource constraints: Validations that require significant compute (e.g., full test suites) may be serialized to avoid resource exhaustion.
  • Timeout budgets: Each validation has its own timeout (from the tool configuration). The total validation phase has an implicit budget derived from the sum of individual timeouts, but the runtime may use parallelism to finish faster.
  • Dependency hints: A future extension may allow declaring ordering dependencies between validations (e.g., "run lint before tests" to fail fast on formatting issues). Currently, all validations run independently.

Wrapped Tool deduplication (deferred): When multiple Validations wrap the same Tool (via wraps), a future optimization could invoke the wrapped Tool once and fan out its output to each Validation's transform function. However, deduplication is only valid when multiple Validations wrap the same Tool and receive the exact same forwarded arguments (after argument_mapping resolution) and are evaluated in the same calling chain. This complexity — particularly around argument equivalence and cache invalidation during fix-then-revalidate loops — means deduplication is deferred for future design. The current implementation invokes the wrapped Tool separately for each Validation that wraps it.

The recommended execution strategy is:

  1. Run all validations in parallel (each Validation independently invokes its wrapped Tool if applicable).
  2. Collect results.
  3. If any required validation fails, fix and re-run only the failing validations (not all of them).
  4. Repeat until all pass or retry limit is reached.

Registering and Managing Validations

Validations are registered via agents validation add and attached via agents validation attach. These are the only validation-specific CLI commands. All other management operations use the standard agents tool commands:

Operation Command Notes
Register agents validation add --config <FILE> Validation-specific. Config file defines all properties including mode (required/informational).
Update agents validation add --config <FILE> --update Uses the --update flag to overwrite existing registration.
Attach agents validation attach [--project|--plan] <RESOURCE> <VALIDATION> [args...] Returns an attachment ULID. Resource is always required; project/plan scope is optional.
Detach agents validation detach [--yes] <ATTACHMENT_ID> Uses the attachment ULID, not the validation name.
List agents tool list --type validation Shared with tools. Use --type validation to filter.
Show agents tool show <NAME> Shows validation-specific fields when the entry is a Validation.
Remove agents tool remove <NAME> Shared with tools. Automatically detaches from all scopes.

See the agents validation and agents tool CLI Reference sections for full command details and examples.

Typical Validation Workflow

A typical workflow for setting up validations on a project:


# 1. Define validation YAML files
# (see Configuration > Validation Configuration Files for schema and examples)

# 2. Register validations agents validation add --config ./validations/run-tests.yaml agents validation add --config ./validations/lint-check.yaml agents validation add --config ./validations/type-check.yaml agents validation add --config ./validations/check-bundle-size.yaml

# 3. Attach to resource through a project (active only when this resource is accessed through this project) agents validation attach --project local/api-service local/api-repo local/run-tests agents validation attach --project local/api-service local/api-repo local/type-check agents validation attach --project local/api-service local/api-repo local/check-bundle-size

# 4. Attach directly to a resource (always active for any plan accessing this resource) agents validation attach local/api-repo local/lint-check

# 5. Verify setup agents tool list --type validation --namespace local agents tool show local/run-tests

# 6. Run a plan — validations execute automatically at end of Execute phase agents plan use local/implement-feature local/api-service

Design Rationale

Why Validation extends Tool rather than being a separate concept:

  • Reuses the entire tool infrastructure (registry, bindings, lifecycle, sources, schemas) without duplication.
  • Validations can be composed into skills, used in actor graphs, and invoked by LLM agents — all for free.
  • A single namespace and registry avoids the complexity of managing two parallel systems.
  • Tool-aware features (resource bindings, MCP integration, Agent Skills) apply to validations automatically.

Why read-only is enforced rather than advisory:

  • Validations that modify state defeat the purpose of the sandbox model.
  • Read-only guarantees enable safe parallelization and retry without side effects.
  • It makes validations categorically safe — no risk analysis needed for running a validation.

Why attachments use ULIDs rather than name+scope pairs:

  • A validation may be attached to the same scope multiple times with different arguments.
  • ULIDs provide an unambiguous handle for removal without complex composite keys.
  • Consistent with the ULID-based identity model used for plans, resources, and decisions.

Why validation runs during Execute, not Apply:

  • Apply is a controlled commit step. Validation failures during Apply would require rolling back committed changes.
  • The sandbox model ensures all verification occurs before the point of no return.
  • Post-apply verification (integration tests, smoke tests) is a separate concern best handled by external CI or follow-up plans.

Skills

!!! adr "Architecture Decision" The skill composition model, skill inclusion, and tool bundling are defined in ADR-012: Skill System.

What a Skill Is

!!! adr "Architecture Decision" The canonical definition of a skill — its four tool sources, flattening model, and role as the unit of capability assignment — is formalized in ADR-030: Skill Abstraction Definition.

A skill is a namespaced, reusable collection of tools that is registered in the system via its own YAML configuration file and managed through agents skill CLI commands. Skills are the unit of capability composition in CleverAgents — they define what an actor can do by assembling tools into coherent, reusable bundles.

A skill is not a single tool. It is a container that references one or more tools (by name from the Tool Registry) and/or defines anonymous inline tools, along with metadata describing the collection's purpose, capabilities, and safety characteristics. Tools are independently registered, callable operations (see the Tools section above). Skills organize them into reusable groups.

Key properties of a skill:

  • Named and namespaced: Skills follow the same <namespace>/<name> naming convention as actors, tools, actions, and plans (e.g., local/github-ops, cleverthis/file-management, local/deploy-tools).
  • Defined in their own YAML files: Skills are NOT defined inline in actor configurations. They have their own configuration files and are managed as independent, reusable entities.
  • A collection of tools: Each skill references named tools from the Tool Registry and/or defines anonymous inline tools. Skills can also expose tools from MCP servers, Agent Skills Standard folders, and built-in tool groups.
  • Hierarchically composable: A skill can include other skills by reference, inheriting all of their tools. This enables layered composition — a "full-stack" skill might include a "file-ops" skill, a "git-ops" skill, and a "github" skill. When including a sub-skill, individual tool metadata can optionally be overridden.
  • Referenced by actors: Actors reference skills by fully-qualified name. The actor's graph gains access to all tools within the referenced skills.

The Skill / Tool Distinction

!!! adr "Architecture Decision" The formal distinction between skills (containers) and tools (atomic operations), and the source-agnostic flattening model, are defined in ADR-030: Skill Abstraction Definition.

@startuml
skinparam packageStyle rectangle
skinparam defaultFontSize 12
skinparam componentFontSize 12

package "Skill: local/devops-toolkit" as Skill {
  package "builtin: file_operations" as FileOps {
    component [read_file()] as T1
    component [write_file()] as T2
    component [edit_file()] as T3
  }

  package "builtin: git_operations" as GitOps {
    component [git_status()] as T4
    component [git_diff()] as T5
  }

  package "mcp: github-server" as GH {
    component [create_issue()] as T6
    component [create_pr()] as T7
    component [list_repos()] as T8
  }

  package "custom" as Custom {
    component [run_migrations()] as T9
  }

  package "Included Skills" as Includes {
    component [local/pdf-processing\n(adds pdf tools)] as I1
    component [local/data-analysis\n(adds analysis tools)] as I2
  }
}
@enduml

Tools are independently registered, atomic units of execution (see the Tools section above for full details). Each tool has:

  • A namespaced name, description, and JSON Schema for inputs/outputs
  • Its own YAML configuration file, registered via agents tool add
  • A source type (mcp, agent_skill, builtin, custom)
  • Capability metadata (read_only, writes, checkpointable, etc.)
  • A lifecycle: discover(), activate(), execute(params, ctx), deactivate()

Skills are the organizational units. Each skill has:

  • A namespaced name
  • A YAML configuration file defining its tool composition
  • Zero or more named tool references (pointing to independently registered tools in the Tool Registry)
  • Zero or more anonymous inline tools (one-off tools defined directly in the skill YAML)
  • Zero or more included child skills (whose tools are merged in, with optional per-tool metadata overrides)
  • Tool sources: MCP servers, Agent Skills folders, built-in tool groups
  • Metadata (description, capability summary)

When an actor references a skill, it gains access to the flattened set of all tools — named tool references, anonymous tools, tools from MCP/Agent Skills/builtins, and those inherited from included child skills.

Skill Configuration (YAML)

Skills are defined in their own YAML configuration files, separate from tool and actor configurations. A skill YAML file declares which registered tools it includes (by name), which other skills it includes, and optionally defines anonymous inline tools:


# File: skills/devops-toolkit.yaml
cleveragents:
  version: "3.0"

skill: name: local/devops-toolkit description: "Full-stack development tools for file ops, git, GitHub, and deployment"

# ── Named Tool References ─────────────────────────────── # Reference independently registered tools by name. # These tools must already be registered via agents tool add. # Optional metadata overrides can be applied per tool. tools: - local/run-migrations # Simple reference, use as registered - name: local/deploy-staging # Reference with metadata override override: capability: human_approval_required: true # Require approval in this skill context

# ── Include other skills ───────────────────────────────── # All tools from included skills become part of this skill. # Included skills must already be registered in the system. # Individual tools from included skills can have metadata overridden. includes: - local/file-ops # built-in file + directory + search tools - local/git-ops # built-in git tools - name: local/github # MCP-based GitHub tools, with per-tool overrides tool_overrides: - tool: local/create-github-issue override: capability: write_scope: [github:issues:org-only]

# ── MCP Server Tools ───────────────────────────────────── # Connect to MCP servers and expose their tools. # Tools discovered from MCP servers are auto-registered in the # Tool Registry if not already present. mcp_servers: - name: linear command: "npx @anthropic/mcp-linear" env: LINEAR_API_KEY: "${LINEAR_API_KEY}" # Optional: override inferred capability metadata per tool overrides: - tool: create_issue writes: true write_scope: [linear:issues] - tool: list_issues read_only: true

# ── Agent Skills (SKILL.md folders) ────────────────────── # Each Agent Skill folder is loaded as a composite tool. # The agent discovers it via metadata, activates it by # loading SKILL.md instructions, and follows them. agent_skills: - path: ./skills/code-review-checklist sandbox_policy: none

# ── Built-in Tool Groups ───────────────────────────────── # Opt-in to built-in tool groups provided by CleverAgents. builtins: - group: shell_operations

# ── Anonymous Tools ────────────────────────────────────── # Inline tool definitions for one-off, skill-specific operations. # Same format as a named tool YAML body but without a name. # These are NOT registered in the Tool Registry and are NOT reusable. anonymous_tools: - description: "One-off cleanup for legacy migration artifacts" input_schema: type: object properties: directory: { type: string } capability: writes: true checkpointable: true checkpoint_scope: file code: | import os, glob directory = params["directory"] removed = [] for f in glob.glob(os.path.join(ctx.sandbox.root, directory, "*.legacy")): os.remove(f) removed.append(f) return {"removed": removed, "count": len(removed)}

Here is an example of a simpler skill that wraps only built-in tools, suitable for common reuse:


# File: skills/file-ops.yaml
cleveragents:
  version: "3.0"

skill: name: local/file-ops description: "File and directory operations"

builtins: - group: file_operations # read, write, edit, delete, move, copy - group: directory_operations # create, list, delete dirs

And an example of a skill that is purely MCP-based:


# File: skills/github.yaml
cleveragents:
  version: "3.0"

skill: name: local/github description: "GitHub operations via MCP"

mcp_servers: - name: github command: "npx @anthropic/mcp-github" env: GITHUB_TOKEN: "${GITHUB_TOKEN}" overrides: - tool: create_issue writes: true write_scope: [github:issues] checkpointable: false - tool: create_pull_request writes: true write_scope: [github:pulls] checkpointable: false - tool: list_repos read_only: true - tool: get_file_contents read_only: true

Skill Hierarchy and Composition

Skills can include other skills via the includes field. When a skill includes another, all tools from the child skill (and transitively, all tools from any skills it includes) become part of the parent skill's flattened tool set.


local/full-stack-dev
  ├── includes: local/file-ops
  │     └── builtins: file_operations, directory_operations
  ├── includes: local/git-ops
  │     └── builtins: git_operations
  ├── includes: local/github
  │     └── mcp_servers: github (create_issue, create_pr, list_repos, ...)
  ├── agent_skills: code-review-checklist
  └── tools: local/run-migrations, local/deploy-staging  (named tool refs)

Flattened tool set available to actors referencing local/full-stack-dev: read_file, write_file, edit_file, delete_file, move_file, copy_file, create_directory, list_directory, delete_directory, git_status, git_diff, git_log, git_blame, create_issue, create_pr, list_repos, get_file_contents, code-review-checklist (agent skill), local/run-migrations, local/deploy-staging (named tools)

Rules for skill composition:

  1. Circular includes are forbidden. The system validates the include graph at registration time and rejects cycles.
  2. Tool name conflicts: If two included skills provide tools with the same name, the conflict is resolved by qualification — the tool must be referenced as <skill_name>.<tool_name> (e.g., local/github.create_issue vs local/linear.create_issue). Direct tools (defined in the skill itself) take precedence over included tools.
  3. Included skills must be registered before the including skill can be added. The agents skill add command validates this.
  4. Depth is unlimited but the flattened tool set is computed at registration time and cached. Deep hierarchies do not incur runtime overhead.

Skill Registration and Management

Skills are managed through the agents skill CLI commands. Named tools referenced by skills must first be registered via agents tool add (see the Tools section):


# First, register any named tools the skill will reference
agents tool add --config ./tools/run-migrations.yaml
agents tool add --config ./tools/deploy-staging.yaml

# Then register the skill (which references those tools by name) agents skill add --config ./skills/devops-toolkit.yaml

# Update an existing skill (re-reads the config file, overwrites registration) agents skill add --config ./skills/devops-toolkit.yaml --update

# List all registered skills agents skill list

# Show details for a skill (tools, includes, metadata) agents skill show local/devops-toolkit

# List all tools provided by a skill (flattened, including from child skills) agents skill tools local/devops-toolkit

# Remove a skill agents skill remove local/devops-toolkit

Once registered, a skill is available to be referenced by any actor configuration. Skills persist in the database (local or server) and follow the same namespace rules as actors, tools, and actions.

Actor References to Skills and Tools

Actors reference skills by name to make collections of tools available for LLM tool-calling. Additionally, actor graphs can include tool nodes that directly reference named tools or define anonymous inline tools (see Nodes in the Graph in the Actor section).

The actor's configuration lists which skills it should have access to:


# File: actors/code-assistant.yaml
cleveragents:
  version: "3.0"
  default_actor: code_assistant

actors: code_assistant: type: llm config: actor: anthropic/claude-3-opus temperature: 0.3 system_prompt: | You are a code assistant with access to file, git, and GitHub tools. Current task: {{ context.task_description }}

# Reference skills by fully-qualified name. # All tools from these skills become available to this actor. skills: - local/file-ops - local/git-ops - local/github

# Or reference a single composite skill that includes all of the above full_stack_assistant: type: llm config: actor: anthropic/claude-3-opus system_prompt: | You are a full-stack development assistant.

<span style="color: cyan; font-weight: 600;">skills</span>:
  - local/full-stack-dev    # includes file-ops, git-ops, github, etc.

Server-qualified skill references: When connected to multiple servers, skills can be disambiguated with a server prefix, same as actors:


skills:
  - dev:freemo/custom-analysis     # from dev server, personal namespace
  - prod:cleverthis/deploy-tools   # from prod server, org namespace
  - local/file-ops                 # local skill

Skill Registry

To enable plan validation and discovery at scale, CleverAgents maintains two registries that work together:

  1. Tool Registry — a persistent catalog of all independently registered tools (described in the Tools section above). Managed via agents tool add/remove/list/show.

  2. Skill Registry — a persistent catalog of all registered skills and their flattened tool sets. The Skill Registry composes its tool sets by resolving named tool references from the Tool Registry, incorporating anonymous inline tools, and merging tools from included child skills.

@startuml
skinparam classAttributeIconSize 0
skinparam classFontSize 13
skinparam defaultFontSize 12

class SkillRegistry {
  - skillIndex : Map<String, SkillRecord>
  --
  + add(config_path) : SkillRecord
  + update(name, config_path) : SkillRecord
  + remove(name) : void
  + lookup(name) : SkillRecord
  + list(filters) : SkillRecord[]
  + tools(name) : ToolDescriptor[]
  + validate_plan(plan) : ValidationResult
  + refresh(name) : void
}

class SkillRecord {
  + name : String
  + description : String
  + config_path : String
  + includes : List<String>
  + tool_refs : List<String>
  + anonymous_tools : List<ToolDef>
  + flattened_tools : List<ToolDescriptor>
  + overrides : Map<String, Object>
  + capability_summary : CapabilitySummary
}

SkillRegistry "1" *-- "0..*" SkillRecord : indexes >

note right of SkillRegistry
  **Populated by:**
  - agents skill add CLI command
  - Dynamic refresh on MCP notifications

  **Depends on:**
  - Tool Registry (resolve named tool references)

  **Consumed by:**
  - Actor activation
  - Plan validation
  - Agent context injection
end note
@enduml

Both registries persist in the database (local SQLite or server). MCP server tools are refreshed dynamically when notifications/tools/list_changed events are received.

Session

!!! adr "Architecture Decision" Session persistence, session lifecycle, and session-plan relationships are defined in ADR-020: Session Model.

What a Session Is

A session is a user's interactive thread with CleverAgents across time.

!!! abstract "Session Responsibilities" - [x] Maintain ==conversational continuity== across interactions - [x] Store plan references for active and past plans - [x] Persist memory (if enabled) across CLI invocations - [x] Provide a UI anchor (CLI invocation, TUI workspace, web session)

Session and Memory Persistence

The notes include a known issue: conversation history can be lost between CLI invocations depending on connection string configuration, implying the system needs a stable memory service backend.

!!! warning "Persistence Requirements" - Sessions must have ==stable IDs== - Sessions must be ==resumable== across CLI invocations - Session storage backend must be ==configured explicitly== - If session persistence is disabled, the UX must be explicit about it — ==no silent history loss==

Server

!!! adr "Architecture Decision" The server architecture, multi-user support, and local/server mode duality are defined in ADR-023: Server Mode. The server application architecture is defined in ADR-048: Server Application Architecture. The A2A standard adoption is defined in ADR-047: A2A Standard Adoption.

What a Server Is

A server is an optional, separate application that enables:

  • multi-user access
  • shared org namespaces (<username>/ and <orgname>/)
  • persistent plan records (PostgreSQL)
  • remote plan execution (via LangGraph Platform)
  • governance and auditing
  • entity synchronization across devices and team members

The server shares the same Domain and Application layers as the client (per ADR-001), differing only in Infrastructure and Presentation layers. Its sole client-facing interface is an A2A JSON-RPC 2.0 endpoint — there is no REST API.

Single-user local mode is the default (so setup is easy), but the architecture anticipates server mode for shared skills, org-level actions, and collaborative workflows.

Client-Only vs Server Mode

Mode Description Protocol Plan Execution
Client-only No server connection. All data in local database. Agent runs as local subprocess. A2A over stdio Always local
Server mode Connected to a CleverAgents server. Namespaced items sync. A2A over HTTP Local or server (via LangGraph Platform RemoteGraph)

It is possible to run a client with no server at all. Server is optional.

Server Configuration and Connection

Connecting to a server requires two configuration keys:

Key Env Var Purpose
server.url CLEVERAGENTS_SERVER_URL URL of the CleverAgents A2A server endpoint
server.token CLEVERAGENTS_SERVER_TOKEN Authentication token (obtained via server registration or team invite)

When server.url is set, the client switches to server mode: A2A methods flow over HTTP instead of stdio, and server namespaces become available. The connection lifecycle follows the A2A standard: the client discovers the server's Agent Card (capability exchange), authenticates via the declared HTTP auth scheme, and begins sending message/send or message/stream requests.

Cloud Plan Execution

In server mode, actor graphs are deployed to LangGraph Platform and invoked via RemoteGraph. This means different actors for different plan phases (strategy, execution, estimation) each deploy as separate RemoteGraphs, enabling independent scaling without limiting plan capabilities.

When an agent executing on the server needs to access client-local resources (files, terminals), it uses _cleveragents/ extension methods (fs/read_text_file, fs/write_text_file, terminal/*) which the client handles locally via the A2A multi-turn interaction pattern (Task enters input-required state). This enables server-hosted plan execution even when some resources exist only on the client machine.

Entity Sync and Sharing

Entity synchronization between client and server uses A2A extension methods (_cleveragents/sync/*):

  • Auto-sync (server.sync.auto): Entities sync on connection and at server.sync.interval (default: 300s)
  • Pull: Server namespace entities are downloaded to local cache
  • Push: Local entity definitions can be explicitly pushed to a server namespace
  • Status: Compare local and server entity versions to detect drift
  • The local/ namespace is never synced — it exists only on the client

Multi-Device Experience

With server mode, a user can:

  • Start a plan on their laptop, close the lid, and monitor progress from another device
  • Share entity definitions (actors, skills, actions) across team members via server namespaces
  • Run long-running plans on server infrastructure without keeping a client connected
  • Access the same session history from CLI, TUI, or IDE plugin on different machines

IDE Integration via A2A

IDE plugins (e.g., VS Code, JetBrains) communicate with CleverAgents exclusively through A2A — the same protocol used by CLI and TUI. The IDE plugin can operate in either local mode (spawning an agent subprocess over stdio) or server mode (connecting to a remote CleverAgents server over HTTP). The _cleveragents/ extension methods for file and terminal operations (fs/*, terminal/*) allow the agent to interact with the IDE's workspace and integrated terminal.

Agent-to-Agent Protocol (A2A)

!!! adr "Architecture Decision" The Agent-to-Agent Protocol is defined in ADR-026: Agent-to-Agent Protocol (A2A). The adoption of the A2A standard is defined in ADR-047: A2A Standard Adoption.

CleverAgents adopts the Agent-to-Agent Protocol standard (a2a-protocol.org) as the sole communication protocol for all client-server interaction. A2A is built on JSON-RPC 2.0 and serves as the fundamental boundary between the Presentation and Application layers — every client operation flows through A2A regardless of deployment mode. No client ever bypasses A2A to touch storage, repositories, or internal domain services directly.

A2A solves a fundamental architectural problem: CleverAgents has multiple presentation surfaces (CLI, TUI, IDE plugin) and multiple deployment modes (local and server), but every client must observe identical behavior regardless of how or where it connects. The A2A standard provides a mature, ecosystem-aligned protocol surface that third parties can implement against.

!!! tip "For the complete architectural deep-dive — transport modes, method catalog, extension methods, wire format, authentication, and error handling — see the Server and Client Architecture section under Architecture."

A2A Standard Operations

The A2A standard defines operations for the core agent interaction lifecycle. These map directly to CleverAgents concepts:

A2A Operation Direction CleverAgents Mapping
Agent Card discovery Client → Server Capability negotiation — advertises supported _cleveragents/ extensions, automation profiles, tool registries
HTTP auth (OAuth2 / API key) Client → Server Token-based authentication (server.token) via auth schemes declared in Agent Card
message/send Client → Server SessionWorkflow.tell() — send user message to orchestrator actor; creates or updates a Task
message/stream Client → Server SessionWorkflow.tell() with streaming — returns TaskStatusUpdateEvent / TaskArtifactUpdateEvent via SSE

A2A uses a Task-centric model: each message/send or message/stream creates or updates a Task that tracks the lifecycle of the interaction. Tasks transition through states: submittedworkingcompleted (or failed, canceled, input-required).

A2A Streaming Events

The A2A standard defines Server-Sent Events (SSE) for real-time streaming from server to client during message/stream:

Event Type CleverAgents Mapping
TaskStatusUpdateEvent Task state transitions, streaming agent response tokens during plan execution
TaskArtifactUpdateEvent Plan artifacts, tool invocation results, generated outputs

Events within a single plan lifecycle preserve causal ordering: a phase transition event always arrives after the corresponding prior phase completion.

Multi-Turn Interactions (Server → Client)

A2A supports multi-turn interactions for operations that require client-side input. When the server-hosted agent needs client-local resources or human approval, the Task enters input-required state:

Interaction Pattern Purpose
Task input-required state Human-in-the-loop approval — maps to automation profile gates
_cleveragents/fs/read_text_file Agent reads a file on the client machine (local project resources)
_cleveragents/fs/write_text_file Agent writes a file on the client machine
_cleveragents/terminal/create Agent requests a terminal on the client machine (sandbox execution)
_cleveragents/terminal/output Terminal output streaming
_cleveragents/terminal/release / wait_for_exit / kill Terminal lifecycle management

These multi-turn interactions enable a server-hosted agent to access client-local resources without requiring the server to have direct access to the user's filesystem or terminal.

CleverAgents Extension Methods

Platform operations beyond the core agent conversation use A2A extension methods — declared in the Agent Card's extensions section per the A2A extensibility mechanism. All CleverAgents extensions use the _cleveragents/ namespace:

Extension Group Methods Service(s)
Plan lifecycle _cleveragents/plan/use, execute, apply, cancel, status, tree, explain, correct, diff, artifacts, prompt, rollback, list PlanService, PlanLifecycle, CorrectionFlow
Registries _cleveragents/registry/{entity}/list, show, add, update, remove (for each entity type: actor, skill, tool, validation, resource, resource_type, project, action, automation_profile, invariant, lsp) ActorService, ToolService, SkillService, ResourceService, ProjectService
Context _cleveragents/context/show, inspect, simulate, set ContextService
Sync _cleveragents/sync/pull, push, status SyncService
Namespace _cleveragents/namespace/list, show, members NamespaceService
Health _cleveragents/health/check, _cleveragents/diagnostics/run Health/diagnostic services

Every CLI command listed in the CLI Commands section maps to either a standard A2A operation or a _cleveragents/ extension method. When a user runs agents plan status <ID>, the CLI sends a _cleveragents/plan/status JSON-RPC request through the active transport and renders the response. The CLI is a thin rendering layer — it contains no business logic.

Transport Modes

A2A operates over two transports provided by the A2A Python SDK:

Mode Transport How It Works Authentication
Local A2A over stdio Client spawns agent as subprocess; JSON-RPC messages flow over stdin/stdout. Platform extension methods are resolved in-process via A2aLocalFacade. Bypassed (local user permissions)
Server A2A over HTTP Client connects to CleverAgents server via A2A SDK HTTP transport. All methods (standard + extensions) flow through the single A2A endpoint. HTTP auth schemes declared in Agent Card (OAuth2, API key) + Authorization: Bearer header

In local mode, the agent runs as a subprocess. Standard A2A operations (message/send, message/stream) drive the conversation. Extension methods (_cleveragents/*) are intercepted by A2aLocalFacade and routed to in-process Application-layer services. No serialization beyond JSON-RPC framing, no network, no authentication overhead.

In server mode, the client connects to the CleverAgents server. All communication — both agent conversations and platform operations — flows through the single A2A JSON-RPC 2.0 endpoint. The server delegates actor execution to LangGraph Platform via RemoteGraph.

A third configuration supports external A2A agents: standard A2A operations go to the external agent's server, while _cleveragents/ extension methods go to the CleverAgents server. This enables interoperability with any A2A-compliant agent.

sequenceDiagram
    participant U as User
    participant C as CLI / TUI / IDE
    participant A2A as A2A Client (SDK)
    participant S as A2A Server
    participant LG as LangGraph Platform

    rect rgb(220, 240, 255)
        Note over C,A2A: Local Mode (stdio)
        U->>C: agents plan status <ID>
        C->>A2A: _cleveragents/plan/status {JSON-RPC 2.0}
        A2A->>A2A: A2aLocalFacade → PlanService.get_status()
        A2A-->>C: JSON-RPC result
        C-->>U: Rendered output
    end

    rect rgb(255, 235, 220)
        Note over C,LG: Server Mode (HTTP)
        U->>C: agents session tell "Refactor auth"
        C->>A2A: message/send {JSON-RPC 2.0}
        A2A->>S: HTTP POST (JSON-RPC)
        S->>LG: RemoteGraph.invoke(actor_graph)
        LG-->>S: Actor result
        S-->>A2A: TaskStatusUpdateEvent / TaskArtifactUpdateEvent (SSE)
        A2A-->>C: Streaming response
        C-->>U: Rendered output
    end
Wire Format

All A2A communication uses JSON-RPC 2.0 framing:

Request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "message/send",
  "params": { "message": { "role": "user", "parts": [{ "kind": "text", "text": "Refactor the auth module" }] }, "taskId": "task_01HXR..." }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": { "status": "accepted" }
}

Streaming event (SSE via message/stream):

{
  "jsonrpc": "2.0",
  "method": "task/statusUpdate",
  "params": { "taskId": "task_01HXR...", "status": { "state": "working" }, "message": { "role": "agent", "parts": [{ "kind": "text", "text": "I'll start by..." }] } }
}

Extension method request:

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "_cleveragents/plan/status",
  "params": { "plan_id": "01HXRCF1..." }
}

Error response:

{
  "jsonrpc": "2.0",
  "id": 42,
  "error": { "code": -32001, "message": "Plan not found", "data": { "plan_id": "01HXRCF1..." } }
}
Authentication

Authentication uses HTTP auth schemes declared in the server's Agent Card:

  1. Client fetches the server's Agent Card (via /.well-known/agent.json or configured URL)
  2. Agent Card declares supported authentication schemes (OAuth2, API key, Bearer token)
  3. Client authenticates using the declared scheme — typically Authorization: Bearer <server.token> header
  4. All subsequent requests carry the authentication credentials

Local mode (stdio) bypasses authentication entirely — the agent subprocess runs with the user's local permissions.

Versioning
  • The JSON-RPC protocol version is always "2.0" (in the jsonrpc field)
  • A2A protocol version is declared in the Agent Card and sent via the A2A-Version HTTP header
  • CleverAgents extension version is declared in the Agent Card's extensions section under _cleveragents.version
  • Servers support the current extension version plus one prior minor version
  • Backward-compatible additions (new optional params, new extension methods) are permitted within a major version; breaking changes require a major version bump

Plan Execution Location

Where a plan executes depends on the project type:

Project Type Client Execution Server Execution
Local (has local-only resources) Yes No (server can't access local resources)
Remote (all resources remotely accessible) Yes Yes

When acting on local projects, the client must be running because only the client can access local resources.

Remote projects can execute on either:

  • The client (if user prefers local execution)
  • The server (for long-running plans, since client may be transient)

Server Execution Benefits

Server execution is useful when:

  • Plans take a long time to execute
  • Client may disconnect (laptop closes, network issues)
  • Multiple team members need to monitor plan progress
  • Centralized logging and auditing required

No Plan Queuing

Plans are not queued. When a plan is used on projects and executed, it runs immediately. There is no worker queue or delayed execution model.

Multi-user Risks and Prompt Injection

Prompt injection isn't critical in single-user mode but becomes important for multi-user server environments.

Server mode must include:

  • access boundaries
  • prompt sanitization / safe templating
  • resource access controls
  • auditing

Resources

!!! adr "Architecture Decision" The resource model, resource types, DAG structure, and resource lifecycle are defined in ADR-008: Resource System.

A resource is an independently registered entity representing anything that a plan can reason about or manipulate — git repositories, filesystems, databases, APIs, documents, and more. Resources are first-class citizens in CleverAgents, managed through the agents resource CLI commands and stored in the Resource Registry.

CleverAgents extends the MCP resource concept to support both read AND write operations (MCP resources are read-only). Unlike MCP's flat resource model, CleverAgents resources form a directed acyclic graph (DAG) with parent/child relationships, support physical vs virtual distinction for content identity tracking, and are governed by a resource type system that constrains their structure and behavior.

Resources are registered independently of projects. Projects link to resources from the Resource Registry — a resource can be linked to multiple projects, enabling shared resources across teams and workflows.

What a Resource Is

A resource has:

  • Name and namespace: User-added resources follow the same <namespace>/<name> naming convention as actors, tools, skills, and other entities (e.g., local/api-repo, cleverthis/staging-db). Auto-discovered child resources do not have names — they are identified by ULID only. Every resource (whether user-added or auto-discovered) always has a system-assigned ULID.
  • Resource type: Every resource has a type (e.g., git, fs-mount, git-branch, fs-file) that determines its properties, CLI arguments, allowed parent/child relationships, sandbox strategy, and handler implementation.
  • Physical or virtual nature: Resources are either physical (a specific, concrete manifestation) or virtual (an abstract identity linking equivalent physical resources). This is determined by the resource type.
  • Value/properties: Type-specific properties (a file path, a URL, a connection string, a commit hash, etc.).
  • Parent/child relationships: Resources form a DAG. A resource can have multiple parents and multiple children, subject to type constraints.
  • Capabilities: Whether the resource is readable, writable, sandboxable, and checkpointable.

Resource Types

A resource type is a schema-level definition that constrains a category of resources. Resource types define:

  • CLI arguments: What arguments agents resource add <type> accepts (for user-addable types), including which are required vs optional and validation rules.
  • Physical or virtual: Whether instances of this type are physical or virtual resources.
  • Allowed parent types: What resource types are valid parents.
  • Allowed child types: What resource types are valid children, and whether they are auto-discovered or manually linkable.
  • Auto-discovery behavior: What child resources are automatically created when an instance of this type is registered (e.g., registering a git-checkout resource auto-discovers a git child and an fs-directory child for the worktree root; the git child in turn auto-discovers remotes, branches, commits, and tree entries).
  • User addable: Whether users can create instances of this type directly via agents resource add <type>. Types with user_addable: false are only generated as auto-discovered children of other resources.
  • Sandbox strategy: The default sandboxing approach for instances of this type.
  • Handler: The resource handler implementation that provides read/write/sandbox/checkpoint operations.
  • Inheritance: Optionally, a parent type from which this type inherits all of the above via the inherits field.
Resource Type Inheritance

!!! adr "Architecture Decision" Resource type inheritance is defined in ADR-042: Resource Type Inheritance.

Resource types support single-inheritance specialization via an inherits field. A subtype inherits all properties, capabilities, child type constraints, sandbox strategy, and handler behavior from its parent type, and can selectively override or extend any inherited field.

Core polymorphism guarantee: Tools bound to a parent type automatically work with all subtypes. Auto-discovery child type matching and DAG queries that reference a parent type automatically include subtypes.

Field resolution: When the system resolves a field for a resource type, it walks the inheritance chain from the most specific type to the root. If the subtype declares the field, the subtype's value is used (override). If omitted, the parent's value is inherited.

Collection field merging: Fields that are collections (cli_args, child_types, parent_types) use additive merging by default — the subtype's entries are appended to the parent's, with same-name entries replaced. A subtype can use <field>_replace: true to replace a collection entirely.

Inheritance rules:

  1. Single inheritance only (no diamond).
  2. Maximum chain depth of 5 levels.
  3. No circular inheritance (validated at registration time).
  4. Built-in types may not inherit from custom (namespaced) types.
  5. Removing a parent type is prohibited while subtypes exist.

Example:


# A subtype that inherits from container-instance
name: devcontainer-instance
inherits: container-instance
description: "A container provisioned from a devcontainer.json configuration"

# Only fields that differ from or extend container-instance need to be declared. # All other fields (capabilities, sandbox_strategy, child_types, etc.) are inherited. cli_args: # Inherited args from container-instance remain available. # Additional args specific to devcontainer:

  • name: config-path type: path required: false description: "Path to .devcontainer/devcontainer.json or .devcontainer/ directory"

handler: class: DevcontainerHandler module: cleveragents.resource.handlers.devcontainer

Built-in Resource Types

Built-in types are organized into four layers: git (version control structure), git-checkout (a composition that bridges git metadata with a local directory), filesystem (physical files on disk), and container (containerized execution environments). Virtual types link equivalent physical resources across these layers through content/identity matching. There are 34 built-in types total (24 physical + 1 subtype + 9 virtual), of which 4 are user-addable as top-level resources (git-checkout, git, fs-mount, fs-directory), plus container types documented in ADR-039 and the devcontainer-instance subtype documented in ADR-043.

Each table below includes the full parent/child relationship constraints. Allowed Parents lists what types may be a parent of this type, with cardinality (how many parents of that type are allowed) and whether the relationship is required or optional. Allowed Children lists what types may be children, with cardinality and whether they are required or optional. 0..* means zero or more, 1 means exactly one (required), 0..1 means zero or one (optional).

Physical types — Git layer (version control structure — every instance is a concrete manifestation in a specific repository):

Type User Addable Sandbox Allowed Parents Allowed Children Description
git yes none git-checkout (0..1, optional) git-remote (0.., optional), git-branch (0.., optional), git-tag (0.., optional), git-commit (0.., optional), git-stash (0.., optional), git-submodule (0.., optional) A git repository — the object database, refs, and full history. Accessible via a local .git directory path or a remote URL. Every git resource is a specific, concrete repo instance (local or hosted on a remote server).
git-remote no none git (1, required) (none) A remote URL configured on a git repo (e.g., origin → https://github.com/org/repo). Auto-discovered child of git. Optionally linked as a child of a remote virtual resource when the same URL appears in multiple repos.
git-branch no (inherits) git (1, required) git-commit (0..*, optional) A named branch ref (e.g., main, feature/auth). Auto-discovered child of git. Optionally linked as a child of a branch virtual resource when the same branch name + HEAD exists in multiple repos.
git-tag no (inherits) git (1, required) (none) A tag ref — lightweight or annotated (e.g., v1.0.0). Auto-discovered child of git. Optionally linked as a child of a tag virtual resource when the same tag name + target exists in multiple repos.
git-commit no (inherits) git-branch (1..*, required), git (1, required) git-tree (1, required) A specific commit object. Auto-discovered child of git-branch (a commit can belong to multiple branches). Each commit contains exactly one root git-tree. Also a direct child of git for ancestry traversal. Optionally linked as a child of a commit virtual resource when the same commit hash exists in multiple repos (e.g., shared history between fork and upstream).
git-tree no (inherits) git-commit (1..*, required) git-tree-entry (0.., optional), git-tree (0.., optional) A tree object — a directory listing at a specific commit. Each tree contains entries (blobs and subtrees). Auto-discovered child of git-commit. Trees are recursive: a tree can contain subtrees as children. Optionally linked as a child of a tree virtual resource when the same tree hash exists across commits/repos.
git-tree-entry no (inherits) git-tree (1, required) (none) A blob entry in a tree — a specific file's content at a specific path and mode. Auto-discovered child of git-tree. Represents the leaf of git's content-addressable storage. Optionally linked as a child of a file virtual resource when byte-identical content with the same name and permissions exists in the filesystem layer.
git-stash no (inherits) git (1, required) (none) A stash entry (e.g., stash@{0}). Auto-discovered child of git. Represents work-in-progress saved via git stash.
git-submodule no (inherits) git (1, required) (none) A submodule reference — a pointer to another git repository at a specific commit and path. Auto-discovered child of git. Optionally linked as a child of a submodule virtual resource when the same submodule URL + path exists in multiple repos.

Physical types — Git checkout (composition: git metadata + local worktree directory):

Type User Addable Sandbox Allowed Parents Allowed Children Description
git-checkout yes git_worktree (none — always a top-level resource) git (1, required), fs-directory (1, required) A locally checked-out git repository. The composition type — auto-discovers a git child (the repo's object database, branches, tags, commits, trees, and remotes) and an fs-directory child (the worktree root directory, e.g., /home/user/projects/my-app). Most users register this type. The worktree root IS a directory, not a mount point, so git-checkout composes with fs-directory directly.

Physical types — Filesystem layer (files on disk, used by git checkouts and standalone directories alike):

Type User Addable Sandbox Allowed Parents Allowed Children Description
fs-mount yes copy_on_write (none — always a top-level resource) fs-directory (1, required) A physical mount point on the local system (e.g., /mnt/data, /home). Represents the mount itself — the filesystem type (ext4, btrfs, etc.) is a property. The root directory is a required auto-discovered fs-directory child. Used for registering entire mount points or storage volumes.
fs-directory yes copy_on_write git-checkout (0..1, optional), fs-mount (0..1, optional), fs-directory (0..1, optional) fs-directory (0.., optional), fs-file (0.., optional), fs-symlink (0.., optional), fs-hardlink (0.., optional) A directory on the filesystem. Can be the root child of fs-mount (mount point root), the worktree root child of git-checkout, a subdirectory child of another fs-directory, or a standalone user-registered directory. Optionally linked as a child of a directory virtual resource when equivalent directory content exists elsewhere. When user-addable, accepts --path flag.
fs-file no (inherits) fs-directory (1, required) (none) A regular file on the local filesystem. Auto-discovered child of fs-directory. Optionally linked as a child of a file virtual resource when byte-identical content with the same name and permissions exists elsewhere (in another fs-file or a git-tree-entry).
fs-symlink no (inherits) fs-directory (1, required) (none) A symbolic link on the local filesystem. Auto-discovered child of fs-directory. Optionally linked as a child of a symlink virtual resource when a symlink with the same name and target exists elsewhere.
fs-hardlink no (inherits) fs-directory (1, required) (none) A hard link on the local filesystem — a file with link count > 1. Auto-discovered child of fs-directory. The system tracks hard link relationships by inode to avoid treating the same underlying data as distinct resources.

Virtual types (abstract identity types that link equivalent physical resources — never user-addable, no sandbox):

Virtual types use simple names that mirror their physical counterparts. A virtual resource answers the question: "where else does this same thing exist?" Two physical resources share a virtual parent when they are equivalent by the virtual type's criteria (same content, same name, same permissions, same hash, etc.).

Type Allowed Children Description
file fs-file (0.., optional), git-tree-entry (0.., optional) Cross-layer file identity. Links physical fs-file and git-tree-entry resources that represent the same file — identical content bytes, filename, and permissions. Answers: "this file in the working tree and this blob in git's tree are the same file."
directory fs-directory (0.., optional), git-tree (0.., optional) Cross-layer directory identity. Links physical fs-directory and git-tree resources whose recursive contents are equivalent. Answers: "this directory on disk matches this tree object in git."
symlink fs-symlink (0.., optional), git-tree-entry (0.., optional) Cross-layer symlink identity. Links physical fs-symlink and git-tree-entry (mode 120000) resources with the same name and target.
commit git-commit (0..*, optional) Cross-repo commit identity. Links git-commit resources across different repos that share the same commit hash — typically a fork and its upstream. Answers: "this commit exists in both repos."
branch git-branch (0..*, optional) Cross-repo branch identity. Links git-branch resources across different repos with the same branch name and HEAD commit hash.
tag git-tag (0..*, optional) Cross-repo tag identity. Links git-tag resources across different repos with the same tag name and target object.
remote git-remote (0..*, optional) Cross-repo remote identity. Links git-remote resources across different repos that point to the same URL. Answers: "these repos share the same upstream."
submodule git-submodule (0..*, optional) Cross-repo submodule identity. Links git-submodule resources across different repos with the same submodule URL and path.
tree git-tree (0..*, optional) Cross-repo/cross-commit tree identity. Links git-tree resources across different commits or repos that have the same tree hash — identical directory structure and contents.

Key design notes:

  • Virtual types are never user-addable — they are created and maintained automatically by the system when equivalent physical resources are detected.
  • Virtual types have no sandbox strategy because they represent abstract identities, not concrete locations. Tools always operate on the physical children.
  • Physical types list virtual parents in their Allowed Parents column (via the "optionally linked" descriptions). For example, an fs-file can be linked as a child of a file virtual resource when its content matches a git-tree-entry. This is how the DAG connects physical and virtual layers.
  • git represents a specific git repository instance — whether hosted remotely (e.g., on GitHub's servers) or locally (a .git directory on disk). It is physical because it is a concrete manifestation that exists somewhere, not an abstract identity. A git resource can be created from a remote URL alone (accessing the object database via the git protocol) or from a local .git directory.
  • git-checkout is the type most users register for repos they've cloned. It composes a git child (repo metadata) and an fs-directory child (the worktree root directory). The worktree root is a directory on an existing filesystem — NOT a separate mount point — so git-checkout links directly to fs-directory, not fs-mount. This means the worktree's files use the same fs-directory and fs-file types regardless of how they were created.
  • git-commit has a git-tree child (the root tree object), which in turn has git-tree-entry and nested git-tree children. This properly models git's internal object structure: commits point to trees, trees contain entries (blobs) and subtrees.
  • fs-mount represents a physical mount point, not a directory. The filesystem type (ext4, btrfs, etc.) is a property on the mount. The root directory is an auto-discovered fs-directory child. Use fs-mount for registering mount points and storage volumes. Use fs-directory (user-addable) for registering arbitrary directories.
  • fs-directory is user-addable, allowing users to register standalone directories (e.g., a build output folder, a config directory) without wrapping them in an fs-mount. An fs-directory registered standalone or as a child of fs-mount/git-checkout uses the same type and auto-discovers the same fs-file, fs-symlink, fs-hardlink, and nested fs-directory children.
  • git-tree-entry represents a blob in git's tree — a specific file's content at a specific path and mode. The corresponding file on disk (if checked out) is an fs-file. The file virtual type bridges these two when their content, name, and permissions are identical.
  • 4 user-addable types: git-checkout (local repo), git (remote or local metadata-only), fs-mount (mount point), fs-directory (arbitrary directory).
Built-in Type Hierarchy

The parent-child relationships between built-in resource types form three layers — git structure, git checkout (composition), and filesystem — bridged by virtual identity types:

@startuml
skinparam defaultFontSize 11
skinparam objectFontSize 11
skinparam packageStyle rectangle

package "GIT-CHECKOUT (composition)" as GC #LightBlue {
  object "git-checkout" as gc {
    /home/user/projects/myapp
  }
}

package "GIT STRUCTURE" as GS #LightYellow {
  object "git" as git {
    repo object DB
  }
  object "git-remote" as remote {
    origin
  }
  object "git-tag" as tag {
    v1.0.0
  }
  object "git-submodule" as submod {
    lib/shared
  }
  object "git-branch" as branch {
    main
  }
  object "git-stash" as stash {
    stash@0
  }
  object "git-commit" as commit {
    a1b2c3d
  }
  object "git-tree" as tree {
    e8f1... root
  }
  object "git-tree-entry" as entry1 {
    README.md
  }
  object "git-tree" as subtree {
    src/ subtree
  }
  object "git-tree-entry" as entry2 {
    src/app.ts
  }
  object "git-tree-entry" as entry3 {
    src/main.ts
  }
}

package "FILESYSTEM (worktree)" as FS #LightGreen {
  object "fs-directory" as fsroot {
    worktree root
  }
  object "fs-directory" as srcdir {
    src/
  }
  object "fs-file" as readme {
    README.md
  }
  object "fs-file" as app {
    app.ts
  }
  object "fs-file" as main {
    main.ts
  }
}

package "STANDALONE FS-DIRECTORY" as SFS #Wheat {
  object "fs-directory" as sfs {
    /opt/deploy/myapp
  }
  object "fs-directory" as ssrcdir {
    src/
  }
  object "fs-file" as sreadme {
    README.md
  }
  object "fs-file" as sapp {
    app.ts
  }
  object "fs-file" as smain {
    main.ts
  }
  object "fs-symlink" as symlink {
    link.txt
  }
}

package "STANDALONE FS-MOUNT" as SM #LightCoral {
  object "fs-mount" as mount {
    /mnt/data
  }
  object "fs-directory" as mountroot {
    root: /
  }
}

package "VIRTUAL LAYER (abstract identities)" as VL #Lavender {
  object "file" as vfile {
    app.ts @ sha256:9f8e...
  }
  object "directory" as vdir {
    src/ @ merkle:3d4f...
  }
  object "commit" as vcommit {
    a1b2c3d
  }
  object "branch" as vbranch {
    main @ a1b2c3d
  }
  object "remote" as vremote {
    github.com/org/repo
  }
  object "tree" as vtree {
    e8f1...9d2a
  }
}

gc --> git
gc --> fsroot

mount --> mountroot

git --> remote
git --> tag
git --> submod
git --> branch
git --> stash

branch --> commit
commit --> tree
tree --> entry1
tree --> subtree
subtree --> entry2
subtree --> entry3

fsroot --> srcdir
fsroot --> readme
srcdir --> app
srcdir --> main

sfs --> ssrcdir
sfs --> sreadme
sfs --> symlink
ssrcdir --> sapp
ssrcdir --> smain

app ..> vfile : equivalent
sapp ..> vfile : equivalent
entry2 ..> vfile : equivalent
srcdir ..> vdir : equivalent
ssrcdir ..> vdir : equivalent
subtree ..> vdir : equivalent
commit ..> vcommit : equivalent
branch ..> vbranch : equivalent
remote ..> vremote : equivalent
tree ..> vtree : equivalent
@enduml

Reading the diagram:

  • Top left: A git-checkout resource decomposes into a git child (left — the repository's full structure) and an fs-directory child (center — the worktree root directory at /home/user/projects/myapp). The worktree root is a directory, not a mount point.
  • Git structure (left column): The git child contains git-remote (origin), git-tag (v1.0.0), git-submodule (lib/shared), git-stash (stash@{0}), and git-branch (main). The branch contains git-commit objects, each commit contains a root git-tree, and trees contain git-tree-entry (blobs) and nested git-tree (subtrees). This properly models git's internal object structure.
  • Filesystem (center/right): The worktree's fs-directory contains src/ (an fs-directory), README.md (an fs-file), and files including fs-symlink resources. A standalone fs-directory (/opt/deploy/myapp) with the same structure. A standalone fs-mount (/mnt/data) with a root fs-directory child.
  • Virtual layer (boxed area): Shows how virtual types link equivalent physical resources:
    • A file virtual resource links fs-file and git-tree-entry resources that have the same content, filename, and permissions — bridging the filesystem and git layers.
    • A directory virtual resource links fs-directory and git-tree resources with the same recursive content.
    • A commit virtual resource links git-commit resources across repos with the same commit hash.
    • A branch virtual resource links git-branch resources across repos with the same name and HEAD.
    • A remote virtual resource links git-remote resources across repos with the same URL.
    • A tree virtual resource links git-tree resources across repos/commits with the same tree hash.

Key separations:

  • git vs git-checkout: A git resource represents a specific repository instance (local or remote). It can exist without any local directory (created from a remote URL — the repo exists on the remote server). A git-checkout always has both a git child and an fs-directory child — it represents a locally cloned repo with files on disk.
  • git-tree-entry vs fs-file: A git-tree-entry is a blob entry in git's tree (path + content hash + mode). An fs-file is a physical file on disk. When a repo is checked out, both exist and typically have matching content — the file virtual type links them when content, name, and permissions match.
  • git-tree vs fs-directory: A git-tree is a tree object in git's object database (a directory listing at a commit). An fs-directory is a physical directory on disk. The directory virtual type links them when their recursive contents match.
  • git-commitgit-treegit-tree-entry: This chain properly models git internals. Commits point to a root tree, trees contain entries (blobs) and subtrees. The old design skipped the tree level; the current design preserves it.
  • fs-mount vs fs-directory: An fs-mount is a mount point (e.g., /mnt/data). The filesystem type (ext4, btrfs, etc.) is a property. The root directory is an fs-directory child. An fs-directory is a directory — it can be a child of fs-mount, git-checkout, or another fs-directory, or it can be registered standalone by the user.
  • git-checkout composes with fs-directory, not fs-mount: A git checkout's worktree is a directory on an existing filesystem, not a separate mount point. This is why git-checkout's required child is fs-directory (the worktree root), not fs-mount.
  • Virtual types bridge physical equivalents: file bridges fs-file + git-tree-entry (cross-layer). directory bridges fs-directory + git-tree (cross-layer). commit, branch, tag, remote, submodule, and tree link the same git structural element across different repos.
Concrete Example: A Made-Up Project

Consider a web application called "Acme Dashboard" with three registered resources:

  1. local/acme-app — a checked-out git repo at /home/alice/projects/acme-dashboard (type: git-checkout)
  2. local/acme-upstream — a git repo accessed via remote URL, not cloned (type: git)
  3. local/acme-deploy — a standalone directory at /opt/deploy/acme-dashboard containing a production build snapshot (type: fs-directory)

Registration:


# 1) Checked-out git repo (has local files on disk)
agents resource add git-checkout local/acme-app \
  --path /home/alice/projects/acme-dashboard --branch main

# 2) Git repo via remote URL (no local checkout — metadata only) agents resource add git local/acme-upstream </span> --url git@github.com:acmecorp/dashboard.git

# 3) Standalone directory (not a git repo — just files on disk) agents resource add fs-directory local/acme-deploy </span> --path /opt/deploy/acme-dashboard

What gets auto-discovered for each:

local/acme-app (type: git-checkout) discovers two children — a git and an fs-directory (the worktree root):

@startwbs
* local/acme-app\n(git-checkout / physical)
** local/acme-app:repo\n(git / physical)
*** acme-app:repo:origin\n(git-remote)\ngit@github.com:acmecorp/dashboard.git
*** acme-app:repo:v1.0.0\n(git-tag)
*** acme-app:repo:lib/shared\n(git-submodule @ c4d5e6f)
*** acme-app:repo:stash@0\n(git-stash)
*** acme-app:repo:main\n(git-branch)
**** main:a7f3e21\n(git-commit)
***** main:a7f3e21:tree\n(git-tree / root)
****** a7f3e21:README.md\n(git-tree-entry)
****** a7f3e21:package.json\n(git-tree-entry)
****** a7f3e21:src/\n(git-tree / subtree)
******* a7f3e21:src/app.ts\n(git-tree-entry)
******* a7f3e21:src/api.ts\n(git-tree-entry)
******* a7f3e21:src/utils.ts\n(git-tree-entry)
*** acme-app:repo:develop\n(git-branch)
**** develop:b2c4d8e\n(git-commit)
***** develop:b2c4d8e:tree\n(git-tree)
****** b2c4d8e:src/\n(git-tree)
******* b2c4d8e:src/app.ts\n(git-tree-entry)
******* b2c4d8e:src/api.ts\n(git-tree-entry / modified)
** local/acme-app:worktree\n(fs-directory / physical)\n/home/alice/projects/acme-dashboard/
*** worktree:src/\n(fs-directory)
**** worktree:src/app.ts\n(fs-file)
**** worktree:src/api.ts\n(fs-file)
**** worktree:src/utils.ts\n(fs-file)
*** worktree:package.json\n(fs-file)
*** worktree:README.md\n(fs-file)
*** worktree:docs -> ../docs\n(fs-symlink)
@endwbs

When the git-checkout contains a .devcontainer/devcontainer.json, an additional devcontainer-instance child is auto-discovered:

@startwbs
* local/acme-app\n(git-checkout / physical)
** local/acme-app:repo\n(git / physical)
*** (branches, commits, trees, ...)
** local/acme-app:worktree\n(fs-directory / physical)
*** worktree:.devcontainer/\n(fs-directory)
**** worktree:.devcontainer/devcontainer.json\n(fs-file)
*** worktree:src/\n(fs-directory)
**** worktree:src/app.ts\n(fs-file)
*** worktree:package.json\n(fs-file)
** local/acme-app:devcontainer\n(devcontainer-instance / discovered)
*** [container-mount — pending activation]
*** [container-exec-env — pending activation]
*** [container-port — pending activation]
@endwbs

The devcontainer-instance is in discovered state — its container-mount, container-exec-env, and container-port children are only created when the container is activated during plan execution. This is consistent with lazy sandboxing: no container is built until a tool actually needs to execute inside it.

The git-checkout cleanly separates two concerns: the git child contains version control structure (remotes, branches, tags, stashes, submodules, commits, trees, and tree entries — git's full object model), while the fs-directory child is the worktree root directory containing the actual files on disk. Note how git's internal structure is fully modeled: git-commitgit-tree (root tree object) → git-tree-entry (blobs) and nested git-tree (subtrees). The worktree root is a directory (fs-directory), not a mount point — git-checkout does not own an fs-mount resource because a git checkout's worktree is just a directory on an existing filesystem. When content matches (as it does for a clean checkout), virtual types link the fs-file and git-tree-entry resources.

local/acme-upstream (type: git, remote URL — NOT checked out) discovers:

@startwbs
* local/acme-upstream\n(git / physical)\ngit@github.com:acmecorp/dashboard.git
** acme-upstream:origin\n(git-remote)
** acme-upstream:v1.0.0\n(git-tag)
** acme-upstream:main\n(git-branch)
*** main:a7f3e21\n(git-commit)
**** main:a7f3e21:tree\n(git-tree)
***** a7f3e21:src/\n(git-tree)
****** a7f3e21:src/app.ts\n(git-tree-entry)
****** a7f3e21:src/api.ts\n(git-tree-entry)
** acme-upstream:develop\n(git-branch)
*** ...\n(remaining structure)
@endwbs

A standalone git resource has full access to branches, tags, commits, trees, and tree entries — everything in the git object database — but no fs-directory child and no fs-file resources. There are no files on the local disk (the repo exists on GitHub's servers). Tools that need local file access cannot bind to it.

This is the key difference from git-checkout: a git resource represents a specific repo instance. Plans can reason about history, diffs between branches, remote relationships — without a local clone. A git-checkout adds the local worktree directory on top.

local/acme-deploy (type: fs-directory, standalone) discovers:

@startwbs
* local/acme-deploy\n(fs-directory / physical)\n/opt/deploy/acme-dashboard/
** acme-deploy:src/\n(fs-directory)
*** acme-deploy:src/app.ts\n(fs-file)
*** acme-deploy:src/api.ts\n(fs-file)
*** acme-deploy:src/utils.ts\n(fs-file)
** acme-deploy:package.json\n(fs-file)
** acme-deploy:README.md\n(fs-file)
@endwbs

A standalone fs-directory — no git metadata, no branches, no commits, no tree entries. Just a directory containing files and subdirectories. Uses the same fs-directory and fs-file types as the git checkout's worktree root.

Virtual resource linking across all three:

After all three resources are registered, the system detects equivalent physical resources and creates virtual parents to link them:

@startuml
skinparam defaultFontSize 10
skinparam objectFontSize 10
skinparam packageStyle rectangle
left to right direction

package "PHYSICAL: local/acme-app" as P1 #LightBlue {
  object "fs-directory" as acmeWtSrc {
    worktree:src/
  }
  object "fs-file" as acmeWtApp {
    worktree:src/app.ts
  }
  object "fs-file" as acmeWtUtils {
    worktree:src/utils.ts
  }
  object "fs-file" as acmeWtApi {
    worktree:src/api.ts
  }
  object "git-tree" as acmeGitSrc {
    main:a7f3e21:src/
  }
  object "git-tree-entry" as acmeGitApp {
    main:src/app.ts
  }
  object "git-tree-entry" as acmeGitUtils {
    main:src/utils.ts
  }
  object "git-tree-entry" as acmeGitApi {
    main:src/api.ts
  }
  object "git-commit" as acmeCommit {
    main:a7f3e21
  }
  object "git-branch" as acmeBranch {
    main
  }
  object "git-tag" as acmeTag {
    v1.0.0
  }
  object "git-remote" as acmeRemote {
    origin
  }
  object "git-tree" as acmeTree {
    main:a7f3e21:tree
  }
  object "git-submodule" as acmeSubmod {
    lib/shared
  }
}

package "PHYSICAL: local/acme-deploy" as P2 #LightGreen {
  object "fs-directory" as deploySrc {
    src/
  }
  object "fs-file" as deployApp {
    src/app.ts
  }
  object "fs-file" as deployUtils {
    src/utils.ts
  }
  object "fs-file" as deployApi {
    src/api.ts
  }
}

package "PHYSICAL: local/acme-upstream" as P3 #LightYellow {
  object "git-tree" as upstreamSrc {
    main:a7f3e21:src/
  }
  object "git-tree-entry" as upstreamApp {
    main:src/app.ts
  }
  object "git-tree-entry" as upstreamUtils {
    main:src/utils.ts
  }
  object "git-tree-entry" as upstreamApi {
    main:src/api.ts
  }
  object "git-commit" as upstreamCommit {
    main:a7f3e21
  }
  object "git-branch" as upstreamBranch {
    main
  }
  object "git-tag" as upstreamTag {
    v1.0.0
  }
  object "git-remote" as upstreamRemote {
    origin
  }
  object "git-tree" as upstreamTree {
    main:a7f3e21:tree
  }
}

package "VIRTUAL LAYER\n(auto-created by equivalence)" as VL #Lavender {
  object "directory" as vDir {
    src/ (merkle:3d4f...)
  }
  object "file" as vAppTs {
    app.ts (sha256:9f8e...)
  }
  object "file" as vUtilsTs {
    utils.ts (sha256:a2b1...)
  }
  object "file" as vApiTs {
    api.ts (sha256:e1d3...)
  }
  object "commit" as vCommit {
    a7f3e21
  }
  object "branch" as vBranch {
    main @ a7f3e21
  }
  object "tag" as vTag {
    v1.0.0
  }
  object "remote" as vRemote {
    github.com:acmecorp/dashboard.git
  }
  object "tree" as vTree {
    e8f1...9d2a
  }
  object "submodule" as vSubmod {
    lib/shared
  }
}

acmeWtSrc ..> vDir
deploySrc ..> vDir
acmeGitSrc ..> vDir
upstreamSrc ..> vDir

acmeWtApp ..> vAppTs
deployApp ..> vAppTs
acmeGitApp ..> vAppTs
upstreamApp ..> vAppTs

acmeWtUtils ..> vUtilsTs
deployUtils ..> vUtilsTs
acmeGitUtils ..> vUtilsTs
upstreamUtils ..> vUtilsTs

acmeWtApi ..> vApiTs
deployApi ..> vApiTs
acmeGitApi ..> vApiTs
upstreamApi ..> vApiTs

note "NOT linked: develop:b2c4d8e:src/api.ts\n(different content on develop branch)" as N1

acmeCommit ..> vCommit
upstreamCommit ..> vCommit

acmeBranch ..> vBranch
upstreamBranch ..> vBranch

acmeTag ..> vTag
upstreamTag ..> vTag

acmeRemote ..> vRemote
upstreamRemote ..> vRemote

acmeTree ..> vTree
upstreamTree ..> vTree

acmeSubmod ..> vSubmod
@enduml

How the directory virtual type works:

The directory virtual resource for src/ exists because the src/ directory has identical recursive content across multiple physical locations. Its children include both fs-directory resources (physical directories on disk) and git-tree resources (tree objects in git's object database). The system detects directory equivalence by computing a Merkle hash over the sorted child content hashes. If someone adds a file to the deploy directory but not the git checkout's worktree, the Merkle hashes diverge, and local/acme-deploy:src/ is unlinked from the directory virtual parent.

How the file virtual type works:

The file virtual type links physical resources that represent the same file — identical content bytes, filename, and permissions. It bridges across layers: an fs-file on disk and a git-tree-entry in git's tree are linked when they have matching content. This is the primary cross-layer bridge. When content diverges (e.g., an uncommitted edit), the virtual link is broken.

How commit works across repos:

When two git resources share history (e.g., a fork and its upstream), the same commit hash will appear in both repos' branches. The commit virtual type links these — the commit object a7f3e21 in the local repo and a7f3e21 in the upstream repo are the same commit, and the virtual parent captures this identity.

Divergence scenario:

If a developer edits src/api.ts in the deploy directory (/opt/deploy/acme-dashboard/src/api.ts), the system detects the content hash change and:

  1. Unlinks local/acme-deploy:src/api.ts from file: api.ts (sha256:e1d3...8a9b) (content no longer matches).
  2. Unlinks local/acme-deploy:src/ from directory: src/ (merkle:3d4f...a2b1) (directory contents no longer identical — the Merkle hash has changed).
  3. The git checkout's worktree files and git-tree-entry resources remain linked (their content hasn't changed).
  4. If the edit makes the deploy file match some other known content hash, a new virtual link may be created.

Additional resource types (databases, APIs, cloud infrastructure, etc.) can be added as custom resource types via agents resource type add.

Cloud Infrastructure Resource Types

Cloud infrastructure types follow a hierarchical model with two layers:

  1. Generic cloud base types (cloud-*) — provider-agnostic abstractions for common cloud concepts (compute, network, storage, IAM, observability, messaging, containers). These are abstract (not user-addable) and serve as inheritance roots.
  2. Provider-specific types (e.g., aws-*) — concrete types that inherit from the generic base layer and model a specific provider's resource hierarchy.

Generic Cloud Base Types (19 types — abstract, not user-addable):

Type Category Description
cloud-account Structure Cloud provider account or subscription
cloud-region Structure Geographic region within an account
cloud-network Network Virtual network (VPC, VNet, etc.)
cloud-subnet Network Subnet within a virtual network
cloud-security-group Network Network security rules / firewall group
cloud-load-balancer Network Network load balancer
cloud-compute-instance Compute Virtual machine or compute instance
cloud-object-store Storage Object / blob storage bucket
cloud-block-storage Storage Block storage volume
cloud-identity-principal IAM IAM user or service principal
cloud-role IAM IAM role
cloud-policy IAM IAM or access policy document
cloud-log-group Observability Log aggregation group
cloud-alarm Observability Monitoring alarm or alert
cloud-queue Messaging Message queue
cloud-topic Messaging Pub/sub notification topic
cloud-container-repo Containers Container image registry / repository
cloud-container-cluster Containers Container orchestration cluster
cloud-container-service Containers Container workload / service

AWS Provider Types (39 types — inheriting from generic base where applicable):

Type Inherits User Addable Parent Types Category
aws-account cloud-account yes (root) Account
aws-region cloud-region no aws-account Structure
aws-vpc cloud-network no aws-region Network
aws-subnet cloud-subnet no aws-vpc Network
aws-igw no aws-vpc Network
aws-nat-gw no aws-subnet, aws-vpc Network
aws-route-table no aws-vpc Network
aws-nacl no aws-vpc Network
aws-security-group cloud-security-group no aws-vpc Network
aws-alb cloud-load-balancer no aws-vpc Network
aws-nlb cloud-load-balancer no aws-vpc Network
aws-target-group no aws-vpc Network
aws-listener no aws-alb, aws-nlb Network
aws-ec2-instance cloud-compute-instance no aws-subnet, aws-region Compute
aws-ami no aws-region Compute
aws-launch-template no aws-region Compute
aws-asg no aws-region Compute
aws-s3-bucket cloud-object-store no aws-region Storage
aws-ebs-volume cloud-block-storage no aws-region Storage
aws-efs-filesystem no aws-region Storage
aws-iam-user cloud-identity-principal no aws-account IAM
aws-iam-role cloud-role no aws-account IAM
aws-iam-policy cloud-policy no aws-account IAM
aws-iam-instance-profile no aws-account IAM
aws-cloudwatch-log-group cloud-log-group no aws-region Observability
aws-cloudwatch-alarm cloud-alarm no aws-region Observability
aws-cloudwatch-metric no aws-region Observability
aws-eventbridge-bus no aws-region Observability
aws-eventbridge-rule no aws-eventbridge-bus Observability
aws-eventbridge-target no aws-eventbridge-rule Observability
aws-sqs-queue cloud-queue no aws-region Messaging
aws-sns-topic cloud-topic no aws-region Messaging
aws-sns-subscription no aws-sns-topic Messaging
aws-ecr-repo cloud-container-repo no aws-region Containers
aws-ecs-cluster cloud-container-cluster no aws-region Containers
aws-ecs-service cloud-container-service no aws-ecs-cluster Containers
aws-ecs-task-def no aws-region Containers
aws-eks-cluster cloud-container-cluster no aws-region Containers
aws-eks-nodegroup no aws-eks-cluster Containers

GCP and Azure are registered as flat provider-level types inheriting from cloud-account. Full hierarchies for these providers are deferred to future PRs.

Only aws-account is user-addable — it is the top-level entry point carrying credential CLI args (--access-key-id, --secret-access-key, --session-token, --region, --profile). All other AWS types are children discovered or created within the account/region/VPC containment hierarchy.

Cloud resource execution is stubbed — the handler validates configuration and resolves credentials but raises NotImplementedError for actual sandbox provisioning. Cloud SDK integration is planned for a future milestone.

Database Resource Types

Built-in database types use the transaction_rollback sandbox strategy:

Type User Addable Sandbox Description
postgres yes transaction_rollback PostgreSQL database connection
mysql yes transaction_rollback MySQL database connection
sqlite yes transaction_rollback SQLite database file
duckdb yes transaction_rollback DuckDB database (file-based or in-memory)

Networked databases (postgres, mysql) accept --connection-string, --host, --port, --dbname, --user, --password CLI args. File-based databases (sqlite, duckdb) accept --path. Database hierarchy restructuring (inheritance from a generic database base type) is deferred to a separate effort.

Custom Resource Types

Custom resource types are defined in YAML configuration files and registered via agents resource type add. Once registered, a custom type automatically becomes available as a new subcommand under agents resource add.


# File: resource-types/database.yaml
cleveragents:
  version: "3.0"

resource_type: name: local/database description: "A SQL database (PostgreSQL, MySQL, SQLite, etc.)" physical_or_virtual: physical user_addable: true

# CLI arguments for agents resource add local/database cli_arguments: - name: connection-string type: string required: true description: "Database connection string (e.g., postgresql://host/dbname)" validation: pattern: "^(postgresql|mysql|sqlite)://" - name: schema type: string required: false description: "Default schema to use" - name: read-only type: boolean required: false default: false description: "Whether the database should be treated as read-only"

# Sandbox and handler sandbox_strategy: transaction_rollback handler: DatabaseHandler checkpointable: true

# Allowed parent types (empty means can be top-level) allowed_parent_types: []

# Child types child_types: - type: local/db-schema auto_discover: true manual_link: false description: "Discovered database schemas" - type: local/db-table auto_discover: true manual_link: false description: "Discovered tables within schemas"

# Capabilities capabilities: readable: true writable: true sandboxable: true checkpointable: true

When this type is registered:


agents resource type add --config ./resource-types/database.yaml
# Now available: agents resource add local/database <NAME> --connection-string CONN [--schema SCHEMA] [--read-only]

The user_addable field determines whether the type appears as a subcommand. Types with user_addable: false are only auto-generated as children — for example, git-remote, git-branch, git-commit, and git-tree-entry are never created directly by users but are discovered when a git (or git-checkout) resource is registered.

The Resource DAG

Resources form a directed acyclic graph (DAG), not a simple tree. A resource can have multiple parents and multiple children, subject to type constraints. The diagram below shows how a git-checkout, a standalone git repo (remote), a standalone fs-directory, and virtual resources interconnect:

@startuml
skinparam defaultFontSize 10
skinparam objectFontSize 10
skinparam packageStyle rectangle

package "GIT-CHECKOUT: local/app" as GCO #LightBlue {
  object "git-checkout" as gco {
    local/app
  }
  object "git" as gcoGit {
    local/app:repo
  }
  object "git-remote" as gcoRemote {
    origin
  }
  object "git-tag" as gcoTag {
    v1.0.0
  }
  object "git-branch" as gcoBranch {
    main
  }
  object "git-commit" as gcoCommit {
    a1b2c3d
  }
  object "git-tree" as gcoTree {
    e8f1... root
  }
  object "git-tree-entry" as gcoEntry1 {
    README.md
  }
  object "git-tree" as gcoSubtree {
    src/
  }
  object "git-tree-entry" as gcoEntry2 {
    src/app.ts
  }
  object "git-tree-entry" as gcoEntry3 {
    src/main.ts
  }
  object "fs-directory" as gcoFs {
    local/app:worktree
  }
  object "fs-directory" as gcoSrcDir {
    src/
  }
  object "fs-file" as gcoReadme {
    README.md
  }
  object "fs-file" as gcoApp {
    app.ts
  }
  object "fs-file" as gcoMain {
    main.ts
  }
}

package "GIT (remote): local/upstream" as GR #LightYellow {
  object "git" as gr {
    local/upstream
  }
  object "git-remote" as grRemote {
    origin
  }
  object "git-tag" as grTag {
    v1.0.0
  }
  object "git-branch" as grBranch {
    main
  }
  object "git-commit" as grCommit {
    a1b2c3d
  }
  object "git-tree" as grTree {
    e8f1... root
  }
  object "git-tree-entry" as grEntry1 {
    README.md
  }
  object "git-tree" as grSubtree {
    src/
  }
  object "git-tree-entry" as grEntry2 {
    src/app.ts
  }
  object "git-tree-entry" as grEntry3 {
    src/main.ts
  }
}

package "STANDALONE FS-DIRECTORY: local/deploy" as SFD #LightGreen {
  object "fs-directory" as sfd {
    /opt/deploy/myapp
  }
  object "fs-directory" as sfdSrcDir {
    src/
  }
  object "fs-file" as sfdReadme {
    README.md
  }
  object "fs-file" as sfdApp {
    app.ts
  }
  object "fs-file" as sfdMain {
    main.ts
  }
}

package "VIRTUAL LAYER" as VL #Lavender {
  object "file" as vFile {
    app.ts@v1
  }
  object "directory" as vDir {
    src/@v1
  }
  object "commit" as vCommit {
    a1b2c3d
  }
  object "branch" as vBranch {
    main@a1b...
  }
  object "tag" as vTag {
    v1.0.0
  }
  object "remote" as vRemote {
    github.com/org/repo
  }
  object "tree" as vTree {
    e8f1...9d2a
  }
}

' Physical hierarchy
gco --> gcoGit
gco --> gcoFs
gcoGit --> gcoRemote
gcoGit --> gcoTag
gcoGit --> gcoBranch
gcoBranch --> gcoCommit
gcoCommit --> gcoTree
gcoTree --> gcoEntry1
gcoTree --> gcoSubtree
gcoSubtree --> gcoEntry2
gcoSubtree --> gcoEntry3
gcoFs --> gcoSrcDir
gcoFs --> gcoReadme
gcoSrcDir --> gcoApp
gcoSrcDir --> gcoMain

gr --> grRemote
gr --> grTag
gr --> grBranch
grBranch --> grCommit
grCommit --> grTree
grTree --> grEntry1
grTree --> grSubtree
grSubtree --> grEntry2
grSubtree --> grEntry3

sfd --> sfdSrcDir
sfd --> sfdReadme
sfdSrcDir --> sfdApp
sfdSrcDir --> sfdMain

' Virtual equivalence links
gcoApp ..> vFile
sfdApp ..> vFile
gcoEntry2 ..> vFile
grEntry2 ..> vFile
gcoSrcDir ..> vDir
sfdSrcDir ..> vDir
gcoSubtree ..> vDir
grSubtree ..> vDir
gcoCommit ..> vCommit
grCommit ..> vCommit
gcoBranch ..> vBranch
grBranch ..> vBranch
gcoTag ..> vTag
grTag ..> vTag
gcoRemote ..> vRemote
grRemote ..> vRemote
gcoTree ..> vTree
grTree ..> vTree
@enduml

Key properties of the DAG:

  1. Multiple parents: A git-tree-entry has a git-tree parent (git structure) and potentially a file virtual parent (content identity). An fs-file has an fs-directory parent (filesystem hierarchy) and potentially a file virtual parent. An fs-directory can be a child of git-checkout (as its worktree root), a child of fs-mount (as the mount root), a child of another fs-directory (as a subdirectory), a child of a directory virtual (identity), or a standalone user-registered resource.
  2. Multiple children: A git-checkout has a git child and an fs-directory child. A git resource has git-remote, git-branch, git-tag, git-stash, git-submodule, and git-commit children. A git-commit has a git-tree child. A git-tree has git-tree-entry and nested git-tree children.
  3. Cycles are forbidden: The graph is always a DAG. The system validates this when links are created.
  4. Type constraints: Not any resource can be a child of any other — the parent's resource type defines which child types are allowed (see the Allowed Parents / Allowed Children columns in the built-in types tables).
  5. Cross-layer bridge: Virtual resources link equivalent physical resources from different layers (git structure, filesystem, different repos). The file virtual type bridges fs-file + git-tree-entry. The directory virtual type bridges fs-directory + git-tree. The commit, branch, tag, remote, submodule, and tree virtual types link the same git structural element across different repositories.
Purpose of the Resource DAG

!!! adr "Architecture Decision" The operational semantics of the resource DAG — what the DAG is for at runtime — are defined in ADR-036: Resource DAG Operational Semantics. Tool reachability and access projection are defined in ADR-037. Cross-mechanism coordination is defined in ADR-038.

The DAG is not just a structural record of resource relationships — it is the topology of the system's operational world. Every runtime decision about how to reach a resource, how to safely change it, and what else is affected flows through the DAG. The DAG answers five fundamental questions:

Question DAG Feature Runtime Operation
What is this? Virtual resource identity (hub node linking equivalents) Equivalence class queries, divergence detection
Where does it live? Physical manifestations and containment hierarchy Auto-discovery, resource registration
How do I reach it? Tool binding → containment edges → access projection Forward/inverse reachability, read/write routing
How do I safely change it? Sandbox boundaries and cross-mechanism coordination Sandbox boundary algebra, coherence-aware commit
What else is affected? Descendant invalidation, ancestor propagation, sibling sync Change propagation, access control propagation

These decompose into ten specific operational purposes:

  1. Tool reachability: Knowing which tools can reach a resource transitively through containment. A tool bound to a git-checkout can reach every fs-file descendant. The inverse query — given a file, what tools can reach it — requires walking up containment edges.

  2. Equivalence and alternative paths: Virtual resources link physical resources that represent the same logical identity. When one path is unavailable, the system finds alternatives through equivalence.

  3. Read/write routing: Different physical manifestations of the same virtual resource offer different access richness. An lsp-document provides semantic reads (types, diagnostics); an fs-file via git-checkout provides sandbox-tracked writes. The DAG enables routing reads through the richest path and writes through the canonical sandbox-tracked path.

  4. Sandbox boundary algebra: Not every resource is independently sandboxable. A file's sandbox boundary is its containing git-checkout or fs-directory. All resources sharing a sandbox boundary share one sandbox instance. The function sandbox_boundary(r) walks up containment edges to find the nearest sandboxable ancestor.

  5. Cross-mechanism sandbox coordination: When the same virtual resource has physical manifestations in different sandbox domains (e.g., git-checkout and container mount), writes through one path must be coordinated with the other at commit time. The coherence property (transparent, cached, independent) on physical-to-virtual edges determines what coordination is needed.

  6. Dependency ordering: The DAG provides topological orderings for lifecycle operations — sandbox creation (top-down), commit (bottom-up), rollback (top-down), cleanup (bottom-up), auto-discovery (top-down).

  7. Change propagation: When a file changes, the DAG determines what else is affected: parent directories need status updates, virtual parents need identity re-evaluation, equivalent physical siblings need cache invalidation.

  8. Access control propagation: A read_only constraint on a git-checkout propagates to all descendant files. The effective writability of a resource is the conjunction of its own writability and all its containment ancestors' writability.

  9. Auto-discovery scope: The DAG defines the cascade structure for resource auto-discovery — directories before files, git repos before branches, containers before mounts.

  10. Tool capability inference: A tool's effective write scope is bounded by the sandbox domain of its bound resource. A tool bound to an fs-file cannot create sibling directories; a tool bound to a git-checkout can reach the entire worktree.

Physical vs Virtual Resources

Resources are either physical or virtual, a distinction determined by their resource type.

Physical resources are specific, concrete manifestations. Each physical resource is a particular instance that exists somewhere — a file at a particular path on a particular machine, a git repository at a particular URL on a particular server, a commit in a particular repo. Physical resources can be directly read and written by tools. Most resources are physical.

Virtual resources represent an abstract identity that links equivalent physical resources. They answer the question: "where else does this same thing exist?" A file virtual resource links all the physical fs-file and git-tree-entry resources that have the same content, filename, and permissions. A commit virtual resource links git-commit resources across repos that share the same commit hash. Virtual resources have no location of their own and cannot be directly read or written.

Rules for the physical/virtual boundary:

  1. The children of a virtual resource can be physical resources, other virtual resources, or a combination of both.
  2. A physical resource's parents can be either physical or virtual resources.
  3. Not all physical resources need a virtual parent. Virtual resource linking is optional and only applies when equivalence tracking is meaningful.

Equivalence linking:

Two physical resources share a virtual parent when they are equivalent by the virtual type's criteria. Each virtual type has its own equivalence semantics:

  • file: Physical files (fs-file or git-tree-entry) share a file parent when they have the same content bytes (SHA-256), the same filename, and the same permissions. This is the primary cross-layer bridge — it links filesystem files with git blob entries. Example: local/app:worktree:src/main.py (an fs-file) and local/app:repo:main:a1b...:src/main.py (a git-tree-entry) both have the same content in a clean checkout, so they share a file virtual parent.

  • directory: Physical directories (fs-directory) and git trees (git-tree) share a directory parent when their full recursive contents are equivalent. This bridges the filesystem and git layers — the src/ directory on disk and the src/ subtree in git's tree object can be linked when their contents match.

  • symlink: Physical symlinks (fs-symlink) and git tree entries with symlink mode (git-tree-entry mode 120000) share a symlink parent when they have the same name and target.

  • commit: Physical commits (git-commit) in different repos share a commit parent when they have the same commit hash. This is common when repos share history (e.g., a fork and its upstream).

  • branch: Physical branches (git-branch) in different repos share a branch parent when they have the same name and the same HEAD commit hash. A push or local commit on one repo causes divergence.

  • tag: Physical tags (git-tag) in different repos share a tag parent when they have the same tag name and target object.

  • remote: Physical remotes (git-remote) in different repos share a remote parent when they point to the same URL. Answers: "these repos share the same upstream."

  • submodule: Physical submodules (git-submodule) in different repos share a submodule parent when they have the same submodule URL and path.

  • tree: Physical tree objects (git-tree) in different commits or repos share a tree parent when they have the same tree hash — identical directory structure and contents.

Divergence detection:

When a physical resource's content changes (e.g., a file is edited, a directory gains a new file, a branch advances), the system detects that it may no longer match its virtual parent's identity. At that point:

  • If the edit causes the physical resource to diverge from all other physical siblings under the same virtual parent, the physical resource is unlinked from that virtual parent.
  • If the edit makes it match a different virtual resource's identity, it may be re-linked.
  • Content equivalence is tracked via content hashing: SHA-256 for files, Merkle hashes for directories, tree hashes for git trees, commit hashes for commits, HEAD+name for branches, name+target for tags, URL for remotes.
  • Cascading divergence: When a file link breaks, the parent directory virtual resource is also re-evaluated (since its identity depends on all child identities).

This enables powerful queries like:

  • "What physical locations exist for this file content?" → Find all physical resources sharing the file virtual parent.
  • "What tools can edit this file?" → Find all tools with resource bindings compatible with the physical resource types.
  • "Has this file been modified in any location?" → Check if all physical siblings still share the same file virtual parent.
  • "Are these two repos in sync?" → Check if their branches share a branch virtual parent.
  • "Which directories are identical across deployments?" → Find directory virtual resources with multiple physical children.
  • "What submodules are shared across projects?" → Find submodule virtual resources with multiple physical children.
Lazy Virtual Node Materialization

!!! adr "Architecture Decision" Lazy virtual node materialization is defined in ADR-038: Cross-Mechanism Sandbox Coordination.

Virtual resource nodes are not created eagerly for every physical resource. They are materialized lazily when a second physical manifestation sharing the same identity is discovered.

Lifecycle:

  1. Single manifestation: A physical resource exists with no virtual parent. It has a content hash but no equivalence link. This is the common case — most files exist in only one location.

  2. Second manifestation discovered: When a new physical resource is registered (or auto-discovered) and its identity matches an existing physical resource's identity (per the virtual type's equivalence rule), a virtual node is created and both physical resources are linked as children.

  3. Additional manifestations: Subsequent physical resources matching the same identity are linked to the existing virtual node.

  4. Manifestation removed: When a physical resource is deregistered or diverges, its edge to the virtual parent is removed. If only one physical child remains, the virtual node is removed (collapsed back to single manifestation). If zero remain, the virtual node is removed entirely.

Each virtual type defines an identity function mapping physical resources to canonical identity keys:

Virtual Type Identity Key
file SHA-256(content) + filename + permissions
directory Merkle hash of recursive child identities
commit Commit hash
branch Branch name + HEAD commit hash
tag Tag name + target object
remote Normalized URL
submodule Submodule URL + path
tree Tree hash
symlink Symlink name + target path

Materialization is triggered by: resource registration, auto-discovery, content change detection, and on-demand refresh.

Coherence Property

!!! adr "Architecture Decision" The coherence property and cross-mechanism coordination protocol are defined in ADR-038: Cross-Mechanism Sandbox Coordination.

Every edge from a physical resource to its virtual parent carries a coherence property describing how changes to this physical manifestation relate to other manifestations of the same virtual identity:

Coherence Meaning Sync Action at Commit
transparent Changes are immediately visible in sibling manifestations (shared storage). No action — verify by content hash.
cached The sibling caches content; changes require an explicit refresh signal. Send invalidation signal (e.g., LSP workspace/didChangeWatchedFiles).
independent Siblings are fully independent copies; changes are invisible until explicit sync. Copy content from canonical target, or unlink the sibling (accept divergence).

Common coherence assignments:

Scenario Coherence
fs-file via git-checkout ↔ fs-file via container bind mount transparent (same inode through bind mount)
fs-file via git-checkout ↔ fs-file via devcontainer workspace bind mount transparent (default devcontainer behavior: bind mount)
fs-file via git-checkout ↔ fs-file via devcontainer workspace volume mount independent (configurable via workspaceMount in devcontainer.json)
fs-filelsp-document cached (LSP buffers file content)
fs-file via git-checkout ↔ fs-file via container volume mount independent (separate copy)
git-commit in repo A ↔ git-commit in repo B independent (same hash but separate stores)
Cross-Mechanism Write Coordination

When a plan modifies a physical resource that has siblings under the same virtual parent (i.e., equivalent physical resources in different sandbox domains), the system coordinates at sandbox commit time using a write-then-sync protocol:

  1. During execution: Tools write through their bound physical resource into its sandbox domain. No cross-domain propagation occurs. Other manifestations see pre-sandbox content.

  2. At commit time: The system identifies dirty virtual resources (virtual resources with at least one modified physical child), checks for conflicts (multiple dirty children of the same virtual), and propagates changes based on coherence.

  3. Conflict resolution: If multiple physical manifestations of the same virtual resource were modified during the same plan:

    • canonical-wins (default): The canonical write target's changes are accepted; others are discarded with a warning.
    • merge: Three-way merge for text content.
    • fail: Reject the commit; surface the conflict for human resolution.
    • last-writer-wins: Accept the most recent modification.

The canonical write target for a virtual resource is the physical manifestation in the strongest sandbox domain (git_worktree > snapshot > copy_on_write > transaction_rollback), with user override available. Writes should be routed to the canonical target whenever possible to avoid conflicts.

Resource Registration (CLI)

Resources are created via agents resource add <type> with type-specific arguments. Auto-discovered children are created automatically:


# Register a checked-out git repository (most common)
agents resource add git-checkout local/api-repo --path /home/user/projects/api-service --branch main
# Auto-discovers: git child (repo metadata, remotes, branches, commits, tree entries)
#                 + fs-directory child (worktree root directory, subdirectories, files)

# Register a git repo via remote URL (no local checkout — exists on remote server) agents resource add git local/upstream --url git@github.com:org/upstream.git # Auto-discovers: remotes, branches, tags, commits, trees, tree entries, stashes, submodules # (no fs-directory — not checked out locally)

# Register a standalone directory (not a git repo — just files on disk) agents resource add fs-directory local/docs --path /opt/docs/api-reference # Auto-discovers: subdirectories, files, symlinks, hardlinks

# Register a standalone filesystem mount (entire volume) agents resource add fs-mount local/data-volume --mount-path /mnt/data # Auto-discovers: root fs-directory, subdirectories, files, symlinks, hardlinks

# Link resources to projects (resources must be registered first) agents project link-resource local/api-service local/api-repo agents project link-resource local/api-service local/docs --read-only

Auto-Discovery

When a resource is registered, its resource type's handler auto-discovers child resources. This process:

  1. Scans the resource to identify children (e.g., a git-checkout handler creates a git child and an fs-directory child for the worktree root; a git handler lists remotes, branches, tags, commits, stashes, and submodules; a git-commit handler creates a root git-tree child; a git-tree handler lists tree entries and subtrees; an fs-mount handler creates a root fs-directory and discovers its contents; an fs-directory handler discovers subdirectories, files, symlinks, and hardlinks; a git-checkout or fs-directory handler additionally detects .devcontainer/devcontainer.json and creates a devcontainer-instance child in discovered state — see ADR-043).
  2. Creates child resource records in the Resource Registry identified by auto-generated ULIDs. Auto-discovered children do not receive names — they are identified by ULID only, even if their resource type is user-addable.
  3. Reuses existing resources: If a discovered child matches an already-registered resource (same type + same value/location), the existing resource is linked as a child rather than creating a duplicate. For example, if local/docs (an fs-directory resource at /home/user/projects/api-service) is already registered and the git-checkout handler discovers its worktree root at the same path, it links to the existing local/docs resource instead of creating a new one.
  4. Links virtual resources: When auto-discovery detects that a physical resource's content matches an existing virtual resource (via content hashing), it links the physical resource as a child of the virtual resource.

Auto-discovered children are marked as auto: true in the DAG, distinguishing them from manually linked children. Auto-discovered links cannot be manually unlinked (they are managed by the handler), while manually created links can be freely managed.

Auto-discovery runs:

  • At registration time (agents resource add)
  • On refresh (when the system detects changes, e.g., new commits, new files)
  • On demand (when a tool accesses the resource and the handler detects staleness)
Devcontainer Auto-Discovery

!!! adr "Architecture Decision" Devcontainer auto-discovery is defined in ADR-043: Devcontainer Integration and Container-Project Association.

When git-checkout or fs-directory auto-discovery scans the filesystem tree, if a .devcontainer/devcontainer.json file is found, a devcontainer-instance child resource is created with provisioning_state: discovered. The devcontainer.json is parsed to populate configuration properties (image, features, workspace mount, ports), but no container is built or started — consistent with the system's lazy sandboxing philosophy.

Key behaviors:

  • Lazy activation: The container is only built when a plan first needs to execute a tool inside it. Child resources (container-mount, container-exec-env, container-port) are created on activation, not at discovery time.
  • Multiple devcontainers: Nested .devcontainer/ directories (e.g., per-service devcontainers in a monorepo) each produce separate devcontainer-instance resources. The devcontainer spec supports multiple named configurations via .devcontainer/<name>/devcontainer.json.
  • Workspace mount: The devcontainer's workspace mount defaults to the parent git-checkout path (or fs-directory path). The container-mount relationship is recorded at discovery time but not materialized until activation.
  • Auto-detected execution environment: The discovered devcontainer automatically becomes the default execution environment for tools operating on the parent resource, subject to execution environment precedence rules (see Execution Environment Routing).

Resource Capabilities

Each resource declares its capabilities, derived from its resource type:

Capability Description
readable Whether the resource can be read
writable Whether the resource can be modified
sandboxable Whether the resource supports sandbox isolation
checkpointable Whether the resource supports checkpoint/rollback

These capabilities are used by the tool execution flow to validate that a tool's resource binding is compatible — a tool declaring access: read_write on a resource slot cannot be bound to a read-only resource.

Resource Registry

CleverAgents maintains a Resource Registry — a persistent catalog of all registered resources and their DAG relationships:

@startuml
skinparam classAttributeIconSize 0
skinparam classFontSize 13
skinparam defaultFontSize 12

class ResourceRegistry {
  - resourceIndex : Map<String, ResourceRecord>
  - typeIndex : Map<String, ResourceTypeRecord>
  --
  + add(type, name, properties) : ResourceRecord
  + update(name, properties) : ResourceRecord
  + remove(name) : void
  + lookup(name) : ResourceRecord
  + list(filters) : ResourceRecord[]
  + tree(name, depth) : DAG_subtree
  + link_child(parent, child) : void
  + unlink_child(parent, child) : void
  + refresh(name) : void
  + find_by_content(hash) : ResourceRecord[]
  + find_virtual_parent(resource) : ResourceRecord
}

class ResourceRecord {
  + name : String
  + type : String
  + physical_or_virtual : PhysVirt
  + properties : Map<String, Object>
  + capabilities : Capabilities
  + parents : List<ResourceRecord>
  + children : List<ResourceRecord>
  + content_hash : String
  + linked_projects : List<String>
  + created_at : DateTime
  + updated_at : DateTime
}

class ResourceTypeRecord {
  + name : String
  + description : String
  + source : String
  + physical_or_virtual : PhysVirt
  + user_addable : Boolean
  + cli_arguments : List<CLIArgument>
  + allowed_parent_types : List<String>
  + child_types : Map<String, ChildTypeConfig>
  + sandbox_strategy : String
  + handler : String
  + capabilities : Capabilities
  + config_path : String
}

enum PhysVirt {
  physical
  virtual
}

ResourceRegistry "1" *-- "0..*" ResourceRecord : indexes resources >
ResourceRegistry "1" *-- "0..*" ResourceTypeRecord : indexes types >
ResourceRecord --> PhysVirt
ResourceTypeRecord --> PhysVirt

note right of ResourceRegistry
  **Populated by:**
  - agents resource add CLI
  - Auto-discovery during registration
  - Content-identity linking

  **Consumed by:**
  - Tool binding resolution
  - Sandbox creation
  - Change tracking
  - Project linking
  - Plan validation
end note
@enduml

The Resource Registry persists in the database (local SQLite or server). It works alongside the Tool Registry and Skill Registry.

Resource Sandbox Strategy

Each resource defines its own sandbox strategy, determined by its resource type. This is critical because:

  1. The same resource may be accessed through different tools and skills
  2. Different resource types require different sandboxing approaches
  3. Some resources cannot be sandboxed at all
Resource Type Sandbox Strategy Rollback Mechanism
git-checkout git_worktree Git reset/checkout
git none N/A (represents a repo instance — not directly sandboxable)
fs-mount copy_on_write, filesystem_copy, or overlay Restore from snapshot or delete copy
fs-directory copy_on_write or filesystem_copy Restore from snapshot or delete copy
Custom database types transaction_rollback Transaction rollback
Custom API types none (often not sandboxable) N/A

Filesystem sandbox strategies differ in their prerequisites and tradeoffs:

  • copy_on_write: Leverages the filesystem's native copy-on-write capability (e.g., BTRFS, ZFS). The filesystem preserves original data blocks when edits occur, creating lightweight snapshots without duplicating data upfront. Only available on CoW-capable filesystems.
  • filesystem_copy: Performs an explicit full copy of the resource directory (e.g., via cp). Works on all writable filesystems regardless of CoW support, at the cost of duplicating data upfront.
  • overlay: Uses an overlay filesystem (e.g., OverlayFS) to layer changes on top of the original directory. Writes go to the upper layer while the lower layer remains untouched. Requires OS-level overlay mount support.

The sandbox strategy is inherited by child resources from their parent unless the child type defines its own. For example, git-branch, git-commit, git-tree, and git-tree-entry all inherit from their git ancestor. fs-file, fs-symlink, and fs-hardlink inherit from their fs-directory parent.

Resource Type Sandbox Strategy Rollback Mechanism
container-instance snapshot Container commit/checkpoint
devcontainer-instance (inherits container-instance) snapshot (inherited) Container commit/checkpoint (inherited)
container-volume snapshot Volume snapshot
container-mount (inherits from container-instance) (inherits)
container-exec-env (inherits from container-instance) (inherits)
lsp-server none N/A (server process, not directly sandboxable)
lsp-workspace none (delegates to backing resource) N/A
lsp-document none (delegates to backing file) N/A
Sandbox Boundary Algebra

!!! adr "Architecture Decision" The sandbox boundary algebra is defined in ADR-036: Resource DAG Operational Semantics.

Not every resource in the DAG is independently sandboxable — a file cannot be sandboxed in isolation. Instead, the DAG has sandbox boundaries: specific nodes at which sandboxing is physically implementable. All resources within a sandbox boundary's domain share one sandbox instance.

Definitions:

  • Sandbox boundary: A resource b where b.capabilities.sandboxable == true and b.sandbox_strategy != none. Examples: git-checkout (git_worktree), fs-directory (copy_on_write), container-instance (snapshot).

  • sandbox_boundary(r): The nearest ancestor of r (inclusive) along contains edges that is a sandbox boundary. If r itself is a boundary, returns r. If no boundary exists in r's ancestor chain, returns None (unsandboxable).

  • Sandbox domain: The set of all resources {r : sandbox_boundary(r) == b} — resources whose nearest sandbox boundary is b. All resources in a domain share the same sandbox instance during plan execution.

Properties:

  1. Sandbox domains partition all sandboxable resources into disjoint groups.
  2. The SandboxManager keys sandboxes by (plan_id, sandbox_boundary_id), not by individual resource ID.
  3. Multiple files in the same git checkout share one git_worktree sandbox.
  4. When a virtual resource has physical manifestations in different sandbox domains (e.g., a file in both a git-checkout domain and a container-instance domain), cross-mechanism coordination is needed at commit time (see Cross-Mechanism Write Coordination above).

Dependency ordering for lifecycle operations:

Operation Traversal Direction Rationale
Sandbox creation Top-down (parent before child) Parent must be sandboxed before children can be accessed within it
Sandbox commit Bottom-up (child before parent) Child changes finalized before parent incorporates them
Sandbox rollback Top-down (parent before child) Rolling back parent implicitly rolls back children
Sandbox cleanup Bottom-up (child before parent) Clean up children before removing parent

Lazy Sandboxing

Resources are sandboxed lazily when accessed, not upfront. This is different from indexing — resources are indexed immediately when registered, but sandboxes are only created when execution needs to modify a resource:

  1. A project may link many resources (e.g., git repo + databases + cloud accounts)
  2. A plan may only need to modify one resource
  3. Only the accessed resources are sandboxed
  4. Each plan/child plan has its own sandbox containing only edited resources

This is efficient for large projects where most resources remain untouched.

Resource Access Tracking

The system tracks which tools access which resources through the tool-resource binding system. This tracking happens at multiple levels:

  1. Declaration-time: Tool YAML declares resource slots with type and access mode (see Resource Bindings in the Tools section).
  2. Activation-time: When a tool is activated for a plan, its resource slots are bound to specific resources. The system records these bindings.
  3. Execution-time: Every tool invocation logs which bound resources were actually accessed, what operations were performed (read/write/delete), and what paths or objects were touched.

This enables:

  • Accurate sandbox scoping: Only sandbox resources that will actually be modified.
  • Rollback feasibility analysis: Know exactly which resources were modified and whether they support rollback.
  • Security auditing: Complete record of which tools accessed which resources and how.
  • Cross-resource impact analysis: Determine if changes to a resource affect tools bound to sibling or child resources.
  • Virtual resource consistency: Detect when a physical resource diverges from its virtual parent.

Unified Resource Abstraction Layer

CleverAgents provides a unified abstraction that allows tools to work with any resource type through a consistent interface. This enables:

  1. Resource-agnostic tools: A tool like read_content(path) works whether the path refers to a file, database record, or API endpoint.
  2. Consistent sandbox semantics: All resources support the same sandbox lifecycle (create, read, write, checkpoint, rollback).
  3. Pluggable resource handlers: New resource types can be added by registering custom resource types without modifying existing tools.
  4. Unified change tracking: All resource modifications flow into the same ChangeSet model.
Resource Handler Interface

Every resource type provides a handler that implements this interface:


class ResourceHandler(Protocol):
    """Handler for a specific resource type."""
<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">read</span>(self, path: <span style="color: cyan;">str</span>, sandbox: Sandbox) -&gt; Content:
    <span style="color: #66cc66;">&quot;&quot;&quot;Read content from the sandboxed resource.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">write</span>(self, path: <span style="color: cyan;">str</span>, content: Content, sandbox: Sandbox) -&gt; Change:
    <span style="color: #66cc66;">&quot;&quot;&quot;Write content and return the Change record.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">delete</span>(self, path: <span style="color: cyan;">str</span>, sandbox: Sandbox) -&gt; Change:
    <span style="color: #66cc66;">&quot;&quot;&quot;Delete resource and return the Change record.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;"><span style="color: cyan;">list</span></span>(self, pattern: <span style="color: cyan;">str</span>, sandbox: Sandbox) -&gt; <span style="color: cyan;">list</span>[<span style="color: cyan;">str</span>]:
    <span style="color: #66cc66;">&quot;&quot;&quot;List paths matching pattern.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">diff</span>(self, path: <span style="color: cyan;">str</span>, sandbox: Sandbox) -&gt; <span style="color: cyan;">str</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Generate diff between sandbox and original state.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">supports_operation</span>(self, operation: OperationType) -&gt; <span style="color: cyan;">bool</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Check if this resource supports the given operation.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">discover_children</span>(self, resource: ResourceRecord) -&gt; <span style="color: cyan;">list</span>[ResourceRecord]:
    <span style="color: #66cc66;">&quot;&quot;&quot;Auto-discover child resources (called at registration and refresh).&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">content_hash</span>(self, path: <span style="color: cyan;">str</span>, sandbox: Sandbox) -&gt; <span style="color: cyan;">str</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Compute content hash for identity tracking.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">create_sandbox</span>(self, resource: ResourceRecord) -&gt; Sandbox:
    <span style="color: #66cc66;">&quot;&quot;&quot;Create a sandbox for this resource using its type&#x27;s strategy.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">create_checkpoint</span>(self, sandbox: Sandbox) -&gt; Checkpoint:
    <span style="color: #66cc66;">&quot;&quot;&quot;Create a checkpoint within the sandbox.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">rollback_to</span>(self, sandbox: Sandbox, checkpoint: Checkpoint) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Roll back sandbox state to a checkpoint.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">project_access</span>(
    self,
    binding_resource: ResourceRecord,
    target_resource: ResourceRecord,
    containment_path: <span style="color: cyan;">list</span>[ResourceRecord],
    sandbox: Sandbox | <span style="color: magenta; font-weight: 600;">None</span>,
) -&gt; AccessProjection | <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Compute how to reach target_resource from binding_resource.

    Returns an AccessProjection with access_path, protocol,
    crosses_sandbox flag, and read_richness score. Returns None
    if this handler cannot project access to the target type.
    Used by the tool reachability and read/write routing system.
    &quot;&quot;&quot;</span>
    ...

Built-in Resource Handlers
Resource Type Handler Read Write Delete Sandbox Strategy
git-checkout GitCheckoutHandler git_worktree
git GitHandler none
git-commit, git-tree, git-tree-entry GitObjectHandler (inherits)
git-branch, git-tag, git-stash GitRefHandler (inherits)
git-remote, git-submodule GitConfigHandler (inherits)
fs-mount FilesystemHandler copy_on_write
fs-directory FilesystemHandler copy_on_write
fs-file, fs-symlink, fs-hardlink FilesystemHandler (inherits)
container-runtime ContainerRuntimeHandler none
container-image ContainerImageHandler none
container-instance ContainerInstanceHandler snapshot
devcontainer-instance (inherits container-instance) DevcontainerHandler snapshot (inherited)
container-mount, container-exec-env, container-port ContainerChildHandler varies (inherits)
container-volume ContainerVolumeHandler snapshot
container-network ContainerNetworkHandler none
executable ExecutableHandler none
lsp-server LSPServerHandler none
lsp-workspace LSPWorkspaceHandler none
lsp-document LSPDocumentHandler none

Additional handlers are provided by custom resource types when they are registered.

!!! adr "Architecture Decision" Container resource types are defined in ADR-039: Container and Execution Environment Resource Types. The devcontainer-instance subtype, container-project association patterns, and execution environment routing are defined in ADR-043: Devcontainer Integration and Container-Project Association. Resource type inheritance (the inherits mechanism) is defined in ADR-042: Resource Type Inheritance. LSP resource types are defined in ADR-040: LSP Resource Types.

Resource Path Resolution

Paths in tool invocations are resolved through a resource routing system that uses tool-resource bindings to determine the target resource:

sequenceDiagram
    participant Tool as Tool Invocation
    participant Router as Resource Router
    participant Handler as GitCheckoutHandler

    Tool->>Router: edit_file(path='src/main.py', ...)
    Router->>Router: Check tool's resource bindings
    Note right of Router: slot 'repo' bound to<br/>local/api-repo (git-checkout)
    Router->>Router: Resolve path within bound resource
    Note right of Router: local/api-repo:worktree:src/main.py
    Router->>Handler: Route to resource handler
    Handler->>Handler: Resolve path to sandbox worktree
    Handler->>Handler: Operate on sandboxed state
    Handler-->>Tool: Return Change record

When a tool has multiple resource slots bound, the path scheme or slot name disambiguates:


# Explicit slot reference
path://repo/src/main.py      → routes to the "repo" slot's bound resource
path://docs/api/readme.md    → routes to the "docs" slot's bound resource

Default: unqualified paths route to the tool's primary resource slot

src/main.py → routes to the first (or only) resource slot

Context

!!! adr "Architecture Decision" The Advanced Context Management System, UKO ontology, CRP protocol, and context strategies are defined in ADR-014: Context Management (ACMS).

The Advanced Context Management System (ACMS) is CleverAgents' fully pluggable, strategy-driven framework for assembling context for actors. It replaces and subsumes the basic context tier system with a comprehensive architecture built around three core innovations:

  1. A Universal Knowledge Ontology (UKO) -- an inheritance-based RDF ontology hierarchy that represents any resource (source code, documents, databases, infrastructure) at multiple levels of abstraction, with full provenance back to the originating resource and temporal versioning across revisions.

  2. A Context Request Protocol (CRP) -- a structured vocabulary through which actors (via skills) declare what information they need, at what level of detail, and with what scope, while they are reasoning. This replaces static "context view" approaches with a dynamic, demand-driven model.

  3. A Context Assembly Pipeline -- a pluggable, 10-component pipeline that replaces the former monolithic Strategy Coordinator and Fusion Engine. Multiple independent context strategies can be registered, each working with different data backends and different abstraction levels of the UKO. The pipeline orchestrates strategy selection, budget allocation, parallel execution, fragment deduplication, depth resolution, scoring, budget packing, ordering, preamble generation, and skeleton compression -- each step backed by a replaceable Protocol implementation configurable at global, project, or plan scope.

The system is designed around the hierarchical nature of plans: parent plans see wide, shallow context (breadth); child plans see narrow, deep context (depth). Context inheritance flows parent-to-child with progressive focusing, and every context assembly respects a dynamically-changing token budget. This architecture is domain-agnostic: the same breadth/depth/DetailDepth mechanics work identically whether the underlying resources are source code repositories, technical documents, database schemas, or infrastructure configurations.

Critical Design Decision: All indexing happens immediately when resources are added to projects or when code changes. There is no "on-demand" indexing during agent execution. This ensures that agents always have instant access to search capabilities without any indexing delays. The computational cost is paid once upfront, not repeatedly during agent operations.

The full architectural design of the ACMS -- including the UKO ontology hierarchy, backend abstraction layer, Context Assembly Pipeline (the 10-component pluggable pipeline that replaced the former Strategy Coordinator and Fusion Engine), index synchronization, performance characteristics, and plugin architecture -- is specified in the Architecture > ACMS Architecture section.

Core Data Types

The ACMS defines several data types that are used throughout the system by actors, skills, strategies, and the plan lifecycle.

DetailDepth and DetailLevelMap

At the most general level (UKO Layer 0), detail depth is simply a non-negative integer — 0 being the most minimal representation and each increment revealing more. There is no fixed upper bound; the maximum meaningful depth depends on the domain and the complexity of the information unit. This is the DetailDepth type.

Each UKO domain extension then registers a DetailLevelMap — a table of named labels mapped to specific integer depths. This allows domain users to work with meaningful names (like MODULE_LISTING or TABLE_OF_CONTENTS) rather than raw integers, while the underlying system always operates on the universal integer scale. Maps are inherited: a language-specific map includes all levels from its parent paradigm map, which includes all levels from the general code map.


DetailDepth = int  # Non-negative integer, 0 = most minimal, no upper bound

@dataclass class DetailLevelMap: """Maps named detail levels to integer depths for a UKO domain. Inherited: a child map includes all entries from its parent map, and may insert additional levels at any integer position.""" domain: str # UKO namespace (e.g., "uko-code:", "uko-py:") parent: DetailLevelMap | None # Parent map to inherit from levels: dict[str, int] # Named level -> integer depth max_depth: int # Maximum meaningful depth for this domain

<span style="color: cyan; font-weight: 600;">def</span> <span style="color: #66cc66;">resolve</span>(<span style="color: #5599ff;">self</span>, depth: <span style="color: #66cc66;">int | str</span>) -> <span style="color: #66cc66;">int</span>:
    <span style="color: #888;">&quot;&quot;&quot;Resolve a named level or integer to an integer depth.&quot;&quot;&quot;</span>
    <span style="color: cyan; font-weight: 600;">if</span> <span style="color: #66cc66;">isinstance</span>(depth, <span style="color: #66cc66;">int</span>):
        <span style="color: cyan; font-weight: 600;">return</span> <span style="color: #66cc66;">min</span>(depth, <span style="color: #5599ff;">self</span>.max_depth)
    <span style="color: #888;"># Look up named level in this map, then parent maps</span>
    <span style="color: cyan; font-weight: 600;">if</span> depth <span style="color: cyan; font-weight: 600;">in</span> <span style="color: #5599ff;">self</span>.levels:
        <span style="color: cyan; font-weight: 600;">return</span> <span style="color: #5599ff;">self</span>.levels[depth]
    <span style="color: cyan; font-weight: 600;">if</span> <span style="color: #5599ff;">self</span>.parent:
        <span style="color: cyan; font-weight: 600;">return</span> <span style="color: #5599ff;">self</span>.parent.resolve(depth)
    <span style="color: cyan; font-weight: 600;">raise</span> <span style="color: #ff6666;">ValueError</span>(<span style="color: #66cc66;">f&quot;Unknown detail level: {depth}&quot;</span>)

Universal (Layer 0) Semantics:

At the universal level, each integer depth has a domain-agnostic meaning. The principle is that depth 0 answers "what exists?", and each subsequent depth answers progressively more detailed questions:

Depth Universal Question Answered What Gets Included
0 What exists? Just the name/identifier of the information unit.
1 How is it organized? Names of immediate children (structural skeleton).
2 What are the key relationships? Children + dependency/reference edges to other units.
3 What is each thing's purpose? + Short descriptions/summaries for each child.
4 What is the structural shape? + Type information, size/count metadata, categories.
5+ How does it work? What does it say? Progressively more content, up to complete verbatim content at max depth.
max Everything. Complete content — nothing omitted.

The exact number of meaningful depths varies by domain (source code may have 12+ levels, a flat config file may only have 4). The system never forces content into a fixed number of buckets.

Source Code DetailLevelMap (uko-code:, extended by uko-oo:, uko-py:, etc.):

The general software domain defines a base set of named levels. Paradigm-specific and language-specific extensions insert additional levels where their semantics provide meaningful distinctions.

Depth uko-code: Name Content Shown
0 MODULE_LISTING Module/package names only, no internal structure.
1 MODULE_GRAPH Module names + inter-module dependency edges (imports graph).
2 MEMBER_LISTING + Names of top-level members within each module (classes, functions, constants) — no signatures.
3 MEMBER_SUMMARY + One-line docstring or LLM-generated summary for each member.
4 SIGNATURES + Full type signatures (parameter names, types, return types) for all callables and type definitions.
5 SIGNATURES_WITH_DOCS + Complete docstrings/comments attached to each member.
6 STRUCTURAL_OUTLINE + Control flow structure within callable bodies (branches, loops, try/except) shown as outline, no expressions.
7 KEY_LOGIC + Key expressions: return statements, assertions, assignments to important variables.
8 NEAR_COMPLETE + All statements except comments, logging, and boilerplate (imports, __all__, etc.).
9 FULL_SOURCE Complete source code — nothing omitted.

Object-Oriented effective map (uko-oo:) — inherits from uko-code:, inserts CLASS_HIERARCHY and VISIBILITY_ANNOTATED:

When the uko-oo: extension is active, the effective DetailLevelMap is re-numbered with consecutive integers. The two inserted levels shift all subsequent depths upward:

Depth Name Origin Content Shown
0 MODULE_LISTING uko-code: Module/package names only, no internal structure.
1 MODULE_GRAPH uko-code: Module names + inter-module dependency edges (imports graph).
2 MEMBER_LISTING uko-code: + Names of top-level members within each module (classes, functions, constants) — no signatures.
3 CLASS_HIERARCHY uko-oo: Inheritance chains and interface implementation relationships between classes.
4 MEMBER_SUMMARY uko-code: + One-line docstring or LLM-generated summary for each member.
5 SIGNATURES uko-code: + Full type signatures (parameter names, types, return types) for all callables and type definitions.
6 SIGNATURES_WITH_DOCS uko-code: + Complete docstrings/comments attached to each member.
7 VISIBILITY_ANNOTATED uko-oo: Public/protected/private modifiers on all members + abstract/final markers.
8 STRUCTURAL_OUTLINE uko-code: + Control flow structure within callable bodies (branches, loops, try/except) shown as outline, no expressions.
9 KEY_LOGIC uko-code: + Key expressions: return statements, assertions, assignments to important variables.
10 NEAR_COMPLETE uko-code: + All statements except comments, logging, and boilerplate (imports, __all__, etc.).
11 FULL_SOURCE uko-code: Complete source code — nothing omitted.

Python-specific effective map (uko-py:) — inherits from uko-oo:, inserts DECORATED_SIGNATURES, TYPE_STUBS, and appends WITH_TESTS:

When the uko-py: extension is active, the effective map builds on uko-oo: and re-numbers again:

Depth Name Origin Content Shown
0 MODULE_LISTING uko-code: Module/package names only, no internal structure.
1 MODULE_GRAPH uko-code: Module names + inter-module dependency edges (imports graph).
2 MEMBER_LISTING uko-code: + Names of top-level members within each module (classes, functions, constants) — no signatures.
3 CLASS_HIERARCHY uko-oo: Inheritance chains and interface implementation relationships between classes.
4 MEMBER_SUMMARY uko-code: + One-line docstring or LLM-generated summary for each member.
5 SIGNATURES uko-code: + Full type signatures (parameter names, types, return types) for all callables and type definitions.
6 SIGNATURES_WITH_DOCS uko-code: + Complete docstrings/comments attached to each member.
7 DECORATED_SIGNATURES uko-py: @property, @staticmethod, @classmethod, custom decorator chains shown on each member.
8 VISIBILITY_ANNOTATED uko-oo: Public/protected/private modifiers on all members + abstract/final markers.
9 STRUCTURAL_OUTLINE uko-code: + Control flow structure within callable bodies (branches, loops, try/except) shown as outline, no expressions.
10 KEY_LOGIC uko-code: + Key expressions: return statements, assertions, assignments to important variables.
11 TYPE_STUBS uko-py: Type annotations from .pyi stub files merged with source, typing module usage.
12 NEAR_COMPLETE uko-code: + All statements except comments, logging, and boilerplate (imports, __all__, etc.).
13 FULL_SOURCE uko-code: Complete source code — nothing omitted.
14 WITH_TESTS uko-py: Full source + associated test cases for each callable (from uko-code:testsCallable edges).

Document DetailLevelMap (uko-doc:):

Depth uko-doc: Name Content Shown
0 TITLE_ONLY Document title / section heading only.
1 TABLE_OF_CONTENTS_L1 Title + top-level (depth-1) section/chapter headings.
2 TABLE_OF_CONTENTS_L2 + Second-level subsection headings.
3 FULL_TOC Complete table of contents to all heading depths.
4 TOC_WITH_SUMMARIES Full TOC + one-sentence abstract per section (LLM-generated or extracted from first paragraph).
5 SECTION_OVERVIEWS + Topic keywords per section + cross-reference targets (which other sections this section discusses).
6 TOPIC_SENTENCES + First sentence of every paragraph within each section.
7 PARAGRAPH_SUMMARIES + LLM-generated summary for each paragraph (2-3 sentences compressed to 1).
8 STRUCTURAL_DETAIL + All figure/table captions + code block headers + list item first lines + blockquote sources.
9 NEAR_COMPLETE + Full paragraph text, but with inline formatting stripped and code blocks truncated to first/last 3 lines.
10 FULL_CONTENT Complete document content — all text, formatting, code blocks, figures, footnotes.

Database DetailLevelMap (uko-data:):

Depth uko-data: Name Content Shown
0 SCHEMA_LISTING Schema/database names only.
1 TABLE_LISTING + Table and view names within each schema.
2 COLUMN_LISTING + Column names within each table (no types).
3 TYPED_COLUMNS + Column data types + nullability.
4 CONSTRAINTS + Primary keys, unique constraints, check constraints, defaults.
5 RELATIONSHIPS + Foreign key relationships (full referential graph between tables).
6 INDEXES_AND_STATS + Index definitions + estimated row counts + basic statistics (cardinality, avg row size).
7 DDL Full CREATE TABLE / CREATE VIEW DDL (reconstructed from metadata).
8 DDL_WITH_TRIGGERS + Trigger definitions + stored procedure signatures that reference each table.
9 FULL_PROCEDURES + Complete stored procedure and function bodies.
10 WITH_SAMPLE_DATA + Sample rows (configurable N, default 5) for each table.
11 FULL_CATALOG Complete catalog: DDL + all procedure bodies + triggers + sample data + value distribution histograms + query plan statistics.

Infrastructure DetailLevelMap (uko-infra:):

Depth uko-infra: Name Content Shown
0 SERVICE_LISTING Service/component names only.
1 SERVICE_GRAPH + Service dependency edges (which services connect to which).
2 ENDPOINT_LISTING + Endpoint paths/ports for each service.
3 ENDPOINT_DETAIL + HTTP methods, parameters, authentication requirements per endpoint.
4 CONFIG_KEYS + Configuration key names and their sections.
5 CONFIG_VALUES + Configuration values (with secrets masked).
6 RESOURCE_LIMITS + Resource allocations (CPU, memory, replicas, storage).
7 FULL_CONFIG Complete configuration files for each service.
8 WITH_DEPLOYMENT + Deployment descriptors (Dockerfiles, Kubernetes manifests, Compose files).

How depth resolution works in practice: When a context request specifies depth="SIGNATURES" and the target node is a uko-py:Module, the system looks up SIGNATURES in the uko-py: DetailLevelMap first. If found, it uses that integer. If not, it walks up to uko-oo:, then uko-code:, then uko: until it finds a match. If the request specifies depth=4 (a raw integer), it is used directly — the system renders the target node at integer depth 4 regardless of what named level that corresponds to.

ContextRequest

A structured request for context, issued by an actor or skill via the CRP.


@dataclass
class ContextRequest:
    """A structured request for context, issued by an actor or skill."""
<span style="color: #888;"># === What to find ===</span>
query: <span style="color: cyan;">str</span> | <span style="color: cyan;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>          <span style="color: #888;"># Natural language query</span>
entities: <span style="color: cyan;">list</span>[<span style="color: cyan;">str</span>] = field(default_factory=<span style="color: cyan;">list</span>)  <span style="color: #888;"># Named entities to focus on</span>
uko_types: <span style="color: cyan;">list</span>[<span style="color: cyan;">str</span>] = field(default_factory=<span style="color: cyan;">list</span>)  <span style="color: #888;"># UKO types to filter</span>

<span style="color: #888;"># === Scope control ===</span>
focus: <span style="color: cyan;">list</span>[<span style="color: cyan;">str</span>] = field(default_factory=<span style="color: cyan;">list</span>)
<span style="color: #888;"># URIs or identifiers of specific items to focus on</span>

breadth: <span style="color: cyan;">int</span> = <span style="color: yellow;">2</span>
<span style="color: #888;"># How many hops outward in the dependency/reference graph.</span>

depth: <span style="color: cyan;">int</span> | <span style="color: cyan;">str</span> = <span style="color: yellow;">3</span>
<span style="color: #888;"># How much detail to include for each item found.</span>
<span style="color: #888;"># May be a raw integer (0-N) or a named level string (e.g., "SIGNATURES")</span>
<span style="color: #888;"># resolved via the active DetailLevelMap for the target node's UKO type.</span>

<span style="color: #888;"># === Focus depth gradient ===</span>
depth_gradient: <span style="color: cyan;">bool</span> = <span style="color: magenta; font-weight: 600;">True</span>
<span style="color: #888;"># When True, items closer to the focus get more detail.</span>

<span style="color: #888;"># === Temporal scope ===</span>
temporal: <span style="color: cyan; font-weight: 600;">TemporalScope</span> = <span style="color: cyan; font-weight: 600;">TemporalScope</span>.CURRENT

<span style="color: #888;"># === Budget ===</span>
max_tokens: <span style="color: cyan;">int</span> | <span style="color: cyan;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>

<span style="color: #888;"># === Strategy hints ===</span>
preferred_strategies: <span style="color: cyan;">list</span>[<span style="color: cyan;">str</span>] = field(default_factory=<span style="color: cyan;">list</span>)
required_backends: <span style="color: cyan;">list</span>[<span style="color: cyan;">str</span>] = field(default_factory=<span style="color: cyan;">list</span>)

<span style="color: #888;"># === Priority and purpose ===</span>
priority: <span style="color: cyan;">float</span> = <span style="color: yellow;">0.5</span>           <span style="color: #888;"># 0.0 = background, 1.0 = critical</span>
purpose: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">""</span>               <span style="color: #888;"># Why is this context needed?</span>

ContextFragment

The atomic unit of context returned by strategies and consumed by the Context Assembly Pipeline's fusion phase (FragmentDeduplicator, DetailDepthResolver, FragmentScorer, BudgetPacker, FragmentOrderer).


@dataclass
class ContextFragment:
    """A single piece of context assembled by a strategy."""
    uko_node: str                     # UKO URI of the source node
    content: str                      # Rendered text content
    detail_depth: int                 # Resolved integer depth of this fragment
    token_count: int                  # Token count of content
    relevance_score: float            # 0.0-1.0 relevance to the request
    provenance: FragmentProvenance     # Trace back to resource + location
    metadata: dict = field(default_factory=dict)
AssembledContext

The output of a context assembly cycle -- the final, budget-respecting payload delivered to an actor.


@dataclass
class AssembledContext:
    """The fused, budget-respecting context payload."""
    fragments: list[ContextFragment]   # Ordered context fragments
    total_tokens: int                  # Total token count
    budget_used: float                 # Fraction of budget consumed (0.0-1.0)
    strategies_used: list[str]         # Which strategies contributed
    context_hash: str                  # Cryptographic hash for snapshot
    preamble: str | None               # Optional structure summary
    provenance_map: dict               # Fragment -> resource/location mapping

Context Request Protocol (CRP)

Instead of static context views, actors (through their skills) actively request context as they reason. The CRP defines a structured vocabulary for these requests.

This is fundamentally different from existing approaches where context is assembled once before the actor runs. With CRP:

  1. The actor starts with an initial context (assembled from the plan's context view, parent context inheritance, and strategy defaults).
  2. As the actor reasons, skills can issue refinement requests to pull in additional context dynamically.
  3. Each request triggers a new context assembly cycle with the remaining token budget.
The builtin/context Skill

Actors issue context requests through a context skill that is automatically injected into every actor's skill set:

# Built-in skill, always available
skill:
  name: builtin/context
  description: "Request additional context during reasoning"

anonymous_tools: - name: request_context description: "Request specific context to be added to the conversation" input_schema: type: object properties: query: { type: string, description: "What information do you need?" } focus: { type: array, items: { type: string }, description: "Specific files, classes, or functions to focus on" } breadth: { type: integer, default: 2, description: "How many dependency hops outward (0-5)" } depth: { oneOf: [{ type: integer, minimum: 0 }, { type: string }], default: 3, description: "Detail depth — integer (0-N) or named level from the active DetailLevelMap (e.g., 'SIGNATURES', 'FULL_SOURCE')" } purpose: { type: string, description: "Why do you need this context?" } required: [purpose]

- <span style="color: cyan; font-weight: 600;">name</span>: <span style="color: #66cc66;">query_history</span>
  <span style="color: cyan; font-weight: 600;">description</span>: <span style="color: #66cc66;">"Query historical context about past decisions and changes"</span>
  <span style="color: cyan; font-weight: 600;">input_schema</span>:
    <span style="color: cyan; font-weight: 600;">type</span>: <span style="color: #66cc66;">object</span>
    <span style="color: cyan; font-weight: 600;">properties</span>:
      <span style="color: cyan; font-weight: 600;">query</span>: { <span style="color: cyan; font-weight: 600;">type</span>: <span style="color: #66cc66;">string</span> }
      <span style="color: cyan; font-weight: 600;">scope</span>: { <span style="color: cyan; font-weight: 600;">type</span>: <span style="color: #66cc66;">string</span>, <span style="color: cyan; font-weight: 600;">enum</span>: [<span style="color: #66cc66;">"current_plan"</span>, <span style="color: #66cc66;">"plan_tree"</span>, <span style="color: #66cc66;">"all_plans"</span>],
               <span style="color: cyan; font-weight: 600;">default</span>: <span style="color: #66cc66;">"plan_tree"</span> }
    <span style="color: cyan; font-weight: 600;">required</span>: [<span style="color: #66cc66;">query</span>]

- <span style="color: cyan; font-weight: 600;">name</span>: <span style="color: #66cc66;">get_context_budget</span>
  <span style="color: cyan; font-weight: 600;">description</span>: <span style="color: #66cc66;">"Check remaining context token budget"</span>
  <span style="color: cyan; font-weight: 600;">input_schema</span>:
    <span style="color: cyan; font-weight: 600;">type</span>: <span style="color: #66cc66;">object</span>
    <span style="color: cyan; font-weight: 600;">properties</span>: {}

Context Strategy Protocol

Every context strategy implements this protocol. Strategies are pluggable -- built-in strategies ship with CleverAgents, and custom strategies can be registered via configuration.

@runtime_checkable
class ContextStrategy(Protocol):
    """A pluggable context assembly strategy."""
<span style="color: cyan;">@property</span>
<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">name</span>(<span style="color: cyan;">self</span>) -> <span style="color: cyan;">str</span>: ...

<span style="color: cyan;">@property</span>
<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">capabilities</span>(<span style="color: cyan;">self</span>) -> <span style="color: cyan;">StrategyCapabilities</span>: ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">can_handle</span>(<span style="color: cyan;">self</span>, request: <span style="color: cyan;">ContextRequest</span>,
               backends: <span style="color: cyan;">BackendSet</span>) -> <span style="color: cyan;">float</span>:
    <span style="color: #66cc66;">"""Returns 0.0-1.0 confidence that this strategy can usefully
    contribute to this request."""</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">assemble</span>(<span style="color: cyan;">self</span>, request: <span style="color: cyan;">ContextRequest</span>,
             backends: <span style="color: cyan;">BackendSet</span>,
             budget: <span style="color: cyan;">int</span>,
             plan_context: <span style="color: cyan;">PlanContext</span>) -> <span style="color: cyan;">list</span>[<span style="color: cyan;">ContextFragment</span>]:
    <span style="color: #66cc66;">"""Execute the strategy. Must respect the budget."""</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">explain</span>(<span style="color: cyan;">self</span>) -> <span style="color: cyan;">str</span>: ...

@dataclass
class StrategyCapabilities:
    """Declares what a strategy is capable of."""
    uses_text: bool = False
    uses_vector: bool = False
    uses_graph: bool = False
    uses_temporal: bool = False
    uko_levels: list[str] = field(default_factory=list)
    resource_types: list[str] = field(default_factory=list)
    supports_depth_breadth: bool = False
    supports_plan_hierarchy: bool = False
    supports_temporal: bool = False
    quality_score: float = 0.5
Built-in Strategies
Strategy Quality Backends Required Description
simple-keyword 0.3 Text only Basic keyword/regex text search. Universal fallback.
semantic-embedding 0.6 Vector Vector similarity search for semantically related content.
breadth-depth-navigator 0.85 Graph Graph-aware UKO traversal with depth/breadth projection. Primary strategy for code-aware context.
arce 0.95 All Multi-modal pipeline combining text, vector, and graph with intent analysis. Highest quality.
temporal-archaeology 0.5 Graph + Cold tier Historical pattern discovery from past decisions and archived context.
plan-decision-context 0.7 Warm/Cold tiers Retrieves context from parent/ancestor plan decisions.
Strategy Registration

Strategies are registered through configuration. The context assembly pipeline itself is also fully pluggable — see Architecture > ACMS > Context Assembly Pipeline for the ten pluggable pipeline components.

[context.strategies]
enabled = ["simple-keyword", "semantic-embedding", "arce", "breadth-depth-navigator"]

[context.strategies.custom] "my-domain-strategy" = "my_package.strategies.DomainStrategy"

# Pipeline component overrides (see Architecture > ACMS > Context Assembly Pipeline) [context.pipeline] strategy-selector = "builtin:ConfidenceWeightedSelector" # default budget-allocator = "builtin:ProportionalBudgetAllocator" # default fragment-scorer = "my_extensions.scorers:DomainAwareScorer" # custom override

Hierarchical Plan Context

In a plan hierarchy, each level sees a different slice of the project's resources. Context inheritance flows parent-to-child with progressive focusing. This applies universally across all UKO domains — the same breadth/depth mechanics work for codebases, documents, databases, and infrastructure.

Source Code Example:

Level Breadth Depth Example Content
Root Entire project 0 (MODULE_LISTING) Module names, dependency graph edges
Subplan A src/auth/ module + direct deps 4 (SIGNATURES) Function signatures with types, class inheritance chains
Sub-subplan A1 AuthManager class + 2-hop deps 9 (FULL_SOURCE) Complete function bodies, all parameters, all call sites

Document Example (e.g., a technical specification):

Level Breadth Depth Example Content
Root Entire document 1 (TABLE_OF_CONTENTS_L1) Document title + top-level chapter headings
Subplan A "Architecture" chapter + sections that discussesTopic it 5 (SECTION_OVERVIEWS) Section headings + topic keywords + cross-reference targets + one-sentence abstracts
Sub-subplan A1 "ACMS Architecture" section + 2-hop topic references 10 (FULL_CONTENT) Complete section text with all paragraphs, figures, code blocks

Database Example (e.g., schema migration planning):

Level Breadth Depth Example Content
Root Entire database 1 (TABLE_LISTING) Schema names + table and view names
Subplan A auth schema + tables with foreign keys into it 5 (RELATIONSHIPS) Table names + typed columns + constraints + foreign key graph
Sub-subplan A1 users table + dependent views and stored procedures 9 (FULL_PROCEDURES) Complete DDL + triggers + stored procedure bodies + sample data

The skeleton is a compact, low-depth representation (typically depth 0-1) of the parent's context window that is passed to every child. This ensures children always have the "big picture" available, even though they focus on a narrow area. The skeleton typically consumes 5-15% of the child's token budget (controlled by skeleton_ratio). For a source code resource, the skeleton is a MODULE_LISTING (depth 0) — just module names. For a document resource, the skeleton is a TABLE_OF_CONTENTS_L1 (depth 1) — the table of contents. For a database resource, it is a TABLE_LISTING (depth 1) — schema and table names.

Depth/Breadth Projection

The depth/breadth projection system allows fine-grained control over how much of the knowledge graph is materialized into context. It operates on the UKO graph structure.

  • Breadth (integer, 0-N): How many hops in the uko:references / uko:dependsOn / uko:contains^-1 graph to traverse from the focus items.
  • Depth (integer or named level): How much content to include for each item found. Specified as a raw integer or a named level resolved via the active DetailLevelMap.
  • Depth gradient: When enabled, detail decreases with distance from focus.

Source Code Projection Example:


Request: focus=["class://AuthManager"], breadth=2, depth=9 (FULL_SOURCE), gradient=True

Result (token costs approximate):

Distance 0 — depth 9 (FULL_SOURCE): class AuthManager: # 800 tokens """Manages user authentication...""" def authenticate(self, username, password): ...full body... def validate_token(self, token): ...full body...

Distance 1 — depth 4 (SIGNATURES): class BaseManager: # 120 tokens def connect(self) -> Connection: ... def disconnect(self) -> None: ... class CryptoUtils: # 80 tokens def hash_password(pwd: str) -> str: ... def verify_hash(pwd: str, hash: str) -> bool: ... class UserDB: # 100 tokens def find_user(username: str) -> User | None: ... def create_user(user: User) -> None: ...

Distance 2 — depth 0 (MODULE_LISTING): class Connection # 20 tokens class User # 15 tokens class DatabasePool # 15 tokens

Total: ~1,150 tokens (vs ~15,000 if everything were depth 9)

Document Projection Example:


Request: focus=["uko-doc:section/security-architecture"], breadth=2, depth=10 (FULL_CONTENT), gradient=True

Result (token costs approximate):

Distance 0 — depth 10 (FULL_CONTENT): ## 5. Security Architecture # 2,400 tokens Complete section text including all paragraphs, code examples, diagrams, and subsections: 5.1 Authentication Flow 5.2 Authorization Model 5.3 Token Management

Distance 1 — depth 6 (TOPIC_SENTENCES — sections that discuss security topics): ## 3. API Design # 180 tokens Section headings + first sentence of each paragraph: "The API uses JWT tokens for authentication..." "Rate limiting is enforced per-client..." ## 8. Deployment # 120 tokens "TLS termination occurs at the load balancer..." "Secrets are injected via Vault..."

Distance 2 — depth 0 (TITLE_ONLY — sections referenced by distance-1 sections): ## 2. System Overview # 30 tokens ## 9. Monitoring # 25 tokens ## Appendix A: Threat Model # 20 tokens

Total: ~2,775 tokens (vs ~18,000 if the entire document were depth 10)

Database Projection Example:


Request: focus=["uko-data:table/auth.users"], breadth=2, depth=11 (FULL_CATALOG), gradient=True

Result (token costs approximate):

Distance 0 — depth 11 (FULL_CATALOG): CREATE TABLE auth.users ( # 650 tokens id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(60) NOT NULL, ...complete DDL + triggers + sample rows + statistics... );

Distance 1 — depth 7 (DDL — tables with foreign keys to/from users): CREATE TABLE auth.sessions ( # 180 tokens id UUID PRIMARY KEY, user_id UUID REFERENCES auth.users(id), ...DDL with constraints... ); CREATE TABLE auth.roles ( # 120 tokens ...DDL with constraints... ); CREATE VIEW auth.active_users AS ... # 90 tokens

Distance 2 — depth 1 (TABLE_LISTING — tables referenced by distance-1 tables): auth.permissions # 20 tokens auth.audit_log # 15 tokens public.organizations # 15 tokens

Total: ~1,090 tokens (vs ~8,500 if the entire schema were depth 11)

Integration with Plan Lifecycle


Plan Lifecycle                        ACMS Actions
─────────────────                     ────────────────────────────
Plan created (agents plan use)        ResourceScope resolved.
                                      ScopedBackendViews created.

Strategize phase begins InitialContextAssembler runs: - Inherits parent context (if subplan) - Runs Context Assembly Pipeline - Produces AssembledContext - Injects into actor's system prompt

Strategy actor reasons Actor may issue ContextRequests via builtin/context skill. Each request triggers re-assembly with remaining budget.

Strategy actor produces decisions Each Decision's context_snapshot captures AssembledContext hash + provenance map.

Execute phase begins New InitialContextAssembler run with execute-phase view. Decisions from Strategize are in warm tier.

Execution actor works Dynamic ContextRequests as needed.

Subplan spawned PlanContextInheritance computes child context from parent. SkeletonCompressor propagates parent context as skeleton. New ResourceScope (possibly narrower).

Apply phase Minimal context assembly (validation results, diff summary).

Plan completes Hot context archived to warm. Warm context ages to cold based on retention policy.

Output Rendering Framework

!!! adr "Architecture Decision" The output rendering framework, format system, and reactive output sessions are defined in ADR-021: CLI and Output Rendering.

Overview

CleverAgents uses a unified Output Rendering Framework to decouple command output data from its visual presentation. Every CLI command produces structured output through a common abstraction layer, and the active format determines how that output is rendered to the terminal (or piped to external consumers). The format is set via the global --format flag, the format config key, or defaults to rich.

The framework is reactive-first: commands do not build a static data structure and hand it to a renderer. Instead, commands open an output session, create element handles for each piece of output (a panel, a table, a progress indicator), and write data to those handles — potentially from multiple concurrent producers. The session coordinates with a materialization strategy selected by the active format, which decides when and how each element's content reaches the terminal. A rich session renders updates in-place as they arrive; a plain session buffers each element and flushes sequentially; a json session accumulates everything and serializes once at the end. Producer code is format-agnostic — it writes to handles without knowing which format is active.

This architecture is designed for modularity, extensibility, and future-proofing — the same session-based output can be consumed by the CLI, a future TUI, a web frontend, or programmatic integrations. The design uses a pipeline of composable stages: session lifecycle management, typed element handles, event-driven materialization, and format-specific element rendering.

Architecture

Rendering Pipeline

All CLI output flows through a five-stage reactive pipeline:

flowchart LR
    A["Command Logic"] --> B["OutputSession\n(lifecycle)"]
    B --> C["ElementHandles\n(typed producers)"]
    C --> D["MaterializationStrategy\n(format-driven policy)"]
    D --> E["Terminal / Pipe\n(stdout/stderr)"]
  1. Command Logic opens an OutputSession and creates typed element handlesPanelHandle, TableHandle, ProgressHandle, etc. — for each piece of output the command will produce. Handles are created in declaration order, which determines the canonical order in which elements appear in sequential formats.

  2. OutputSession is the central coordinator. It owns the set of active handles, tracks their lifecycle (open → writing → closed), emits ElementEvent objects to the active materialization strategy, and provides a snapshot() method that returns a static StructuredOutput representing the accumulated state at any point in time.

  3. ElementHandles are the producer-facing API. Each handle is typed for a specific element kind (panel, table, tree, etc.) and exposes write methods appropriate to that kind (add_row(), set_entry(), set_step_status(), etc.). Handles are thread-safe — multiple concurrent coroutines or threads can write to different handles simultaneously. Handles are format-agnostic — the producer never knows or cares which format is active.

  4. MaterializationStrategy is a polymorphic observer selected by the active format. It receives ElementEvent notifications from the session and decides when and how to render content. Each strategy delegates the actual visual rendering of an element's accumulated state to a paired ElementRenderer.

  5. Terminal/Pipe receives the final byte stream. The framework auto-detects whether stdout is a TTY and degrades gracefully (e.g., rich falls back to table when piped to a non-TTY unless --format rich was explicitly set).

OutputSession

The OutputSession is the core abstraction that replaces direct construction of static output objects. Commands receive a session (typically injected by the CLI framework) and interact with it throughout their execution:


class OutputSession:
    """A live output document that coordinates element production and materialization.
    
    The session manages the lifecycle of all output elements for a single command
    invocation. It is the bridge between format-agnostic producer code and the
    format-specific materialization strategy.
    
    Thread Safety:
        The session is thread-safe. Multiple producers may create and write to
        handles concurrently. The session serializes event delivery to the
        materialization strategy using an internal event queue.
    
    Lifecycle:
        session = OutputSession.open(command, strategy)
        handle_a = session.panel("Title")       # create handles
        handle_b = session.table("Results", ...) 
        handle_a.set_entry(...)                  # write to handles (concurrent OK)
        handle_b.add_row(...)
        handle_a.close()                         # close handles when done
        handle_b.close()
        session.close()                          # finalize the session
    """
<span style="opacity: 0.7;"># --- Session lifecycle ---</span>

command: <span style="color: cyan;">str</span>                                  <span style="opacity: 0.7;"># The command that owns this session</span>
session_id: <span style="color: cyan;">str</span>                               <span style="opacity: 0.7;"># Unique session identifier (ULID)</span>
created_at: <span style="color: cyan;">datetime</span>                          <span style="opacity: 0.7;"># Session creation timestamp</span>

_strategy: MaterializationStrategy            <span style="opacity: 0.7;"># The active materialization strategy</span>
_handles: <span style="color: cyan;">OrderedDict</span>[<span style="color: cyan;">str</span>, ElementHandle]     <span style="opacity: 0.7;"># handle_id → handle, in declaration order</span>
_event_queue: asyncio.Queue[ElementEvent]     <span style="opacity: 0.7;"># Internal event queue for serialization</span>
_state: SessionState                          <span style="opacity: 0.7;"># &quot;open&quot; | &quot;closing&quot; | &quot;closed&quot;</span>
_lock: threading.Lock                         <span style="opacity: 0.7;"># Protects handle creation/removal</span>

<span style="color: yellow;">@classmethod</span>
<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">open</span>(cls, command: <span style="color: cyan;">str</span>, strategy: MaterializationStrategy,
         metadata: <span style="color: cyan;">dict</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: #66cc66;">&quot;OutputSession&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Open a new output session.</span>

Called by the CLI framework before command execution. The strategy is selected based on the resolved format (see Format Resolution). Args: command: The command string (e.g., "project show"). strategy: The materialization strategy for the active format. metadata: Optional command metadata (user, timestamp, etc.). Returns: A new OutputSession ready for element creation. """ ...

<span style="opacity: 0.7;"># --- Element handle factories ---</span>
<span style="opacity: 0.7;"># Each factory creates a typed handle, registers it with the session in</span>
<span style="opacity: 0.7;"># declaration order, and emits an ElementCreated event to the strategy.</span>

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">panel</span>(self, title: <span style="color: cyan;">str</span>, *,
          border_style: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;rounded&quot;</span>,
          priority: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;normal&quot;</span>,
          collapse_hint: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;auto&quot;</span>,
          metadata: <span style="color: cyan;">dict</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: #66cc66;">&quot;PanelHandle&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Create a panel element handle for key-value pair output.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">table</span>(self, title: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span>, *,
          columns: <span style="color: cyan;">list</span>[<span style="color: #66cc66;">&quot;ColumnDef&quot;</span>],
          summary: <span style="color: cyan;">dict</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
          max_rows_hint: <span style="color: cyan;">int</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
          sort_key: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
          priority: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;normal&quot;</span>,
          collapse_hint: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;auto&quot;</span>,
          metadata: <span style="color: cyan;">dict</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: #66cc66;">&quot;TableHandle&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Create a table element handle for tabular data.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">tree</span>(self, root_label: <span style="color: cyan;">str</span>, *,
         root_style: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
         max_depth_hint: <span style="color: cyan;">int</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
         show_guides: <span style="color: cyan;">bool</span> = <span style="color: magenta; font-weight: 600;">True</span>,
         priority: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;normal&quot;</span>,
         collapse_hint: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;auto&quot;</span>,
         metadata: <span style="color: cyan;">dict</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: #66cc66;">&quot;TreeHandle&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Create a tree element handle for hierarchical data.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">progress</span>(self, label: <span style="color: cyan;">str</span>, *,
             total: <span style="color: cyan;">int</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
             indeterminate: <span style="color: cyan;">bool</span> = <span style="color: magenta; font-weight: 600;">False</span>,
             steps: <span style="color: cyan;">list</span>[<span style="color: cyan;">str</span>] | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
             priority: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;normal&quot;</span>,
             metadata: <span style="color: cyan;">dict</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: #66cc66;">&quot;ProgressHandle&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Create a progress indicator handle.</span>

Args: label: Display label for the progress indicator. total: Total units of work (None for indeterminate). indeterminate: If True, show a spinner instead of a progress bar. steps: Named steps to track (creates ProgressStep objects with initial status "pending"). """ ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">status</span>(self, message: <span style="color: cyan;">str</span>, *,
           level: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;info&quot;</span>,
           detail: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
           priority: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;normal&quot;</span>,
           metadata: <span style="color: cyan;">dict</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: #66cc66;">&quot;StatusHandle&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Create a status message handle.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">text</span>(self, content: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;&quot;</span>, *,
         wrap: <span style="color: cyan;">bool</span> = <span style="color: magenta; font-weight: 600;">True</span>,
         indent: <span style="color: cyan;">int</span> = 0,
         priority: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;normal&quot;</span>,
         metadata: <span style="color: cyan;">dict</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: #66cc66;">&quot;TextHandle&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Create a text block handle.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">code</span>(self, content: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;&quot;</span>, *,
         language: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
         line_numbers: <span style="color: cyan;">bool</span> = <span style="color: magenta; font-weight: 600;">False</span>,
         highlight_lines: <span style="color: cyan;">list</span>[<span style="color: cyan;">int</span>] | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
         priority: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;normal&quot;</span>,
         metadata: <span style="color: cyan;">dict</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: #66cc66;">&quot;CodeHandle&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Create a code block handle.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">diff</span>(self, *,
         file_a: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
         file_b: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
         priority: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;normal&quot;</span>,
         metadata: <span style="color: cyan;">dict</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: #66cc66;">&quot;DiffHandle&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Create a diff block handle.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">separator</span>(self, style: <span style="color: cyan;">str</span> = <span style="color: #66cc66;">&quot;line&quot;</span>) -&gt; <span style="color: #66cc66;">&quot;SeparatorHandle&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Create a visual separator. Separators are auto-closed on creation.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">action_hint</span>(self, commands: <span style="color: cyan;">list</span>[<span style="color: cyan;">str</span>],
                description: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: #66cc66;">&quot;ActionHintHandle&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Create an action hint. Action hints are auto-closed on creation.&quot;&quot;&quot;</span>
    ...

<span style="opacity: 0.7;"># --- Session operations ---</span>

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">snapshot</span>(self) -&gt; <span style="color: #66cc66;">&quot;StructuredOutput&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Return a static snapshot of all elements accumulated so far.</span>

The snapshot captures the current state of every handle (open or closed) as a StructuredOutput object. This is used by accumulate-mode strategies (json/yaml) at session end, and is available at any time for logging, debugging, or programmatic inspection. """ ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">close</span>(self, *, exit_code: <span style="color: cyan;">int</span> = 0) -&gt; <span style="color: #66cc66;">&quot;StructuredOutput&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Close the session and finalize all output.</span>

Any handles still open are force-closed (with a warning logged). Emits a SessionEnd event to the strategy. Returns the final StructuredOutput snapshot. Args: exit_code: The command's exit code (included in the snapshot). Returns: The final StructuredOutput snapshot. """ ...

<span style="opacity: 0.7;"># --- Context manager support ---</span>

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">__enter__</span>(self) -&gt; <span style="color: #66cc66;">&quot;OutputSession&quot;</span>:
    <span style="color: magenta; font-weight: 600;">return</span> self

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">__exit__</span>(self, exc_type, exc_val, exc_tb) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Auto-close session on context exit.</span>

If exiting due to an exception, sets exit_code to 1 and emits an error status element before closing. """ ...

Element Handles

Element handles are the producer-facing API. Each handle type wraps a specific element kind and provides methods appropriate to that kind. All handles share a common base:


class ElementHandle(Generic[E]):
    """Base class for all element handles.
    
    An element handle is a write-only view of an output element. Producers use
    handles to incrementally build element content without knowledge of the
    active format or materialization strategy.
    
    Type Parameter:
        E: The OutputElement subclass this handle wraps (e.g., Panel, Table).
    
    Thread Safety:
        Individual handle methods are thread-safe. Multiple threads may call
        methods on *different* handles concurrently. Concurrent writes to the
        *same* handle are serialized via an internal lock.
    
    Lifecycle:
        handle = session.table(...)    # Created by session factory
        handle.add_row(...)            # Write operations (zero or more)
        handle.close()                 # Finalize (required unless auto-closed)
    
    Closed Handle Behavior:
        Calling any write method on a closed handle raises ElementClosedError.
    """
handle_id: <span style="color: cyan;">str</span>                       <span style="opacity: 0.7;"># Unique handle identifier (ULID)</span>
element_type: <span style="color: cyan;">str</span>                    <span style="opacity: 0.7;"># Semantic type (&quot;panel&quot;, &quot;table&quot;, etc.)</span>
declaration_index: <span style="color: cyan;">int</span>               <span style="opacity: 0.7;"># Position in session&#x27;s declaration order</span>

_session: OutputSession              <span style="opacity: 0.7;"># Owning session (for event emission)</span>
_element: E                          <span style="opacity: 0.7;"># The accumulated element state</span>
_state: HandleState                  <span style="opacity: 0.7;"># &quot;open&quot; | &quot;closed&quot;</span>
_lock: threading.Lock                <span style="opacity: 0.7;"># Serializes writes to this handle</span>

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">close</span>(self) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Close this handle, signaling that no more data will be written.</span>

Emits an ElementClosed event to the materialization strategy. For buffered strategies, this triggers rendering of the element. """ ...

<span style="color: yellow;">@property</span>
<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">is_open</span>(self) -&gt; <span style="color: cyan;">bool</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Whether this handle is still accepting writes.&quot;&quot;&quot;</span>
    ...

<span style="color: yellow;">@property</span>
<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">element</span>(self) -&gt; E:
    <span style="color: #66cc66;">&quot;&quot;&quot;The accumulated element state (read-only snapshot).&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">__enter__</span>(self) -&gt; <span style="color: cyan;">Self</span>:
    <span style="color: magenta; font-weight: 600;">return</span> self

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">__exit__</span>(self, exc_type, exc_val, exc_tb) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Auto-close handle on context exit.&quot;&quot;&quot;</span>
    <span style="color: magenta; font-weight: 600;">if</span> self.is_open:
        self.close()

class PanelHandle(ElementHandle[Panel]): """Handle for building a Panel element incrementally. Panels are titled groups of key-value pairs. Entries can be added, updated, or removed after creation. """

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_entry</span>(self, key: <span style="color: cyan;">str</span>, value: <span style="color: cyan;">str</span>, *,
              style_hint: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
              icon: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Set or update a key-value entry in the panel.</span>

If an entry with the given key already exists, it is updated. Otherwise, a new entry is appended. Emits an ElementUpdated event. """ ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_entries</span>(self, entries: <span style="color: cyan;">dict</span>[<span style="color: cyan;">str</span>, <span style="color: cyan;">str</span>], *,
                style_hints: <span style="color: cyan;">dict</span>[<span style="color: cyan;">str</span>, <span style="color: cyan;">str</span>] | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Set multiple entries at once (batch update). Emits a single event.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">remove_entry</span>(self, key: <span style="color: cyan;">str</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Remove an entry by key. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

class TableHandle(ElementHandle[Table]): """Handle for building a Table element incrementally. Tables are the primary element for streamed data. Rows can be added one at a time or in batches as data becomes available from queries, API calls, or concurrent operations. """

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">add_row</span>(self, row: <span style="color: cyan;">dict</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Append a single row to the table.</span>

The row dict maps column names to cell values. Missing columns are filled with None. Extra columns not in the schema are ignored. Emits an ElementUpdated event (type=row_added). """ ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">add_rows</span>(self, rows: <span style="color: cyan;">list</span>[<span style="color: cyan;">dict</span>]) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Append multiple rows in a batch. Emits a single ElementUpdated event.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_summary</span>(self, summary: <span style="color: cyan;">dict</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Set or update the summary/aggregation row. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_sort_key</span>(self, column: <span style="color: cyan;">str</span>, *, descending: <span style="color: cyan;">bool</span> = <span style="color: magenta; font-weight: 600;">False</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Change the sort key. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

class TreeHandle(ElementHandle[Tree]): """Handle for building a Tree element incrementally. Trees are built by adding child nodes to existing nodes. The root node is created with the handle. Subtrees can be constructed incrementally as hierarchical data is discovered. """

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">add_child</span>(self, parent_path: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span>, label: <span style="color: cyan;">str</span>, *,
              style_hint: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>,
              collapsed: <span style="color: cyan;">bool</span> = <span style="color: magenta; font-weight: 600;">False</span>,
              metadata: <span style="color: cyan;">dict</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: cyan;">str</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Add a child node to the tree.</span>

Args: parent_path: Slash-separated path to the parent node (None = root). label: Display label for the new node. style_hint: Optional color/style hint. collapsed: Whether this node starts collapsed in interactive renderers. metadata: Arbitrary data attached to the node. Returns: The full path to the newly created node (for use as parent_path in subsequent add_child calls). Emits an ElementUpdated event. """ ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_node_style</span>(self, path: <span style="color: cyan;">str</span>, style_hint: <span style="color: cyan;">str</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Update the style of an existing node. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

class ProgressHandle(ElementHandle[ProgressIndicator]): """Handle for updating a ProgressIndicator element. Progress handles are unique in that they are expected to receive many rapid updates. The materialization strategy may throttle update events to avoid overwhelming the terminal (e.g., limiting redraws to 10/sec). """

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_progress</span>(self, current: <span style="color: cyan;">int</span>, total: <span style="color: cyan;">int</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Update the progress counter.</span>

Args: current: Current progress value. total: Total value (can change, e.g., when total is discovered late). Emits an ElementUpdated event (may be throttled by the strategy). """ ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_step_status</span>(self, step_label: <span style="color: cyan;">str</span>, status: <span style="color: cyan;">str</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Update the status of a named step.</span>

Args: step_label: The label of the step to update. status: New status — "pending" | "active" | "done" | "error" | "skipped". Emits an ElementUpdated event. """ ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_label</span>(self, label: <span style="color: cyan;">str</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Update the progress label text. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">increment</span>(self, delta: <span style="color: cyan;">int</span> = 1) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Increment progress by delta. Convenience wrapper around set_progress.&quot;&quot;&quot;</span>
    ...

class StatusHandle(ElementHandle[StatusMessage]): """Handle for a status message. Status handles are typically created and immediately closed (fire-and-forget messages). However, they can be kept open for messages that may be revised (e.g., a "Working..." status that becomes "Done" or "Failed"). """

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_message</span>(self, message: <span style="color: cyan;">str</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Update the status message text. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_level</span>(self, level: <span style="color: cyan;">str</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Change the status level. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_detail</span>(self, detail: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Set or clear the detail text. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

class TextHandle(ElementHandle[TextBlock]): """Handle for a text block. Supports appending text incrementally."""

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">append</span>(self, text: <span style="color: cyan;">str</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Append text to the block. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_content</span>(self, content: <span style="color: cyan;">str</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Replace the entire content. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

class CodeHandle(ElementHandle[CodeBlock]): """Handle for a code block."""

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_content</span>(self, content: <span style="color: cyan;">str</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Set the code content. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_language</span>(self, language: <span style="color: cyan;">str</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Set the language for syntax highlighting. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_highlight_lines</span>(self, lines: <span style="color: cyan;">list</span>[<span style="color: cyan;">int</span>]) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Set lines to highlight. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

class DiffHandle(ElementHandle[DiffBlock]): """Handle for a diff block. Hunks can be added incrementally."""

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">add_hunk</span>(self, header: <span style="color: cyan;">str</span>, lines: <span style="color: cyan;">list</span>[<span style="color: #66cc66;">&quot;DiffLine&quot;</span>]) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Add a diff hunk. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">set_stats</span>(self, insertions: <span style="color: cyan;">int</span>, deletions: <span style="color: cyan;">int</span>, **extra: <span style="color: cyan;">int</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Set diff statistics. Emits an ElementUpdated event.&quot;&quot;&quot;</span>
    ...

Element Events

Element handles communicate with the materialization strategy through a typed event system. Events are the sole interface between production (handles) and consumption (strategy) — this indirection is what enables format-agnostic producer code:


class ElementEvent:
    """Base class for all events emitted by element handles."""
    event_type: str                      # "created" | "updated" | "closed"
    handle_id: str                       # The handle that emitted this event
    element_type: str                    # The element kind ("panel", "table", etc.)
    timestamp: datetime                  # When the event occurred
    session_id: str                      # The owning session

class ElementCreated(ElementEvent): """Emitted when a new element handle is created via a session factory.""" event_type = "created" declaration_index: int # Position in session declaration order initial_state: OutputElement # The element's initial state

class ElementUpdated(ElementEvent): """Emitted when data is written to an element handle.""" event_type = "updated" update_type: str # Kind-specific: "entry_set", "row_added", # "progress_changed", "step_status_changed", etc. delta: dict # The change payload (what was added/modified) element_snapshot: OutputElement # The full element state after this update

class ElementClosed(ElementEvent): """Emitted when an element handle is closed (no more data will arrive).""" event_type = "closed" final_state: OutputElement # The element's final accumulated state

class SessionEnd(ElementEvent): """Emitted when the session itself is closed.""" event_type = "session_end" exit_code: int snapshot: "StructuredOutput" # The complete accumulated output

Element Data Model (Snapshot Types)

Each element handle accumulates state into a typed snapshot object. These are the data classes that represent a fully-built element — they are what the ElementRenderer receives when it is time to paint. They are also the building blocks of the StructuredOutput returned by session.snapshot():


class OutputElement:
    """Base class for all output element snapshot types."""
    element_type: str                    # Semantic type identifier
    metadata: dict                       # Arbitrary metadata (timestamps, IDs, etc.)
    priority: str = "normal"             # "critical" | "normal" | "supplementary"
    collapse_hint: str = "auto"          # "always" | "auto" | "never" — guidance for renderers
                                         #   on whether this element can be collapsed/hidden

class Panel(OutputElement): """A titled group of key-value pairs.""" element_type = "panel" title: str entries: list[PanelEntry] # Each entry: key, value, style_hint (color, icon, etc.) border_style: str = "rounded" # "rounded" | "square" | "heavy" | "none"

class PanelEntry: """A single key-value pair within a Panel.""" key: str value: str style_hint: str | None = None # Color/style for the value (e.g., "success", "warning") icon: str | None = None # Optional icon/prefix character

class Table(OutputElement): """A tabular data set with typed columns.""" element_type = "table" title: str | None columns: list[ColumnDef] # name, type, alignment, width_hint, sortable rows: list[dict] # Column name → cell value summary: dict | None # Optional aggregation row (totals, counts) max_rows_hint: int | None # Suggest truncation for large datasets sort_key: str | None # Default sort column

class ColumnDef: """Schema for a single table column.""" name: str # Column display name type: str = "string" # "string" | "number" | "boolean" | "datetime" | "id" alignment: str = "left" # "left" | "right" | "center" width_hint: int | None = None # Suggested character width (None = auto) sortable: bool = False # Whether this column can be sorted style_hint: str | None = None # Default style for cells in this column

class Tree(OutputElement): """A hierarchical tree structure.""" element_type = "tree" root: TreeNode # Recursive node structure max_depth_hint: int | None # Suggest depth truncation show_guides: bool = True # Whether to show tree guide lines

class TreeNode: """A node in a tree structure.""" label: str style_hint: str | None # Color/style for this node children: list["TreeNode"] collapsed: bool = False # Hint: start collapsed in interactive renderers metadata: dict # Arbitrary data attached to the node

class StatusMessage(OutputElement): """A status line (success, warning, error, info).""" element_type = "status" level: str # "ok" | "warn" | "error" | "info" message: str detail: str | None # Optional detail text

class ProgressIndicator(OutputElement): """A progress bar or spinner for long-running operations.""" element_type = "progress" label: str current: int | None total: int | None indeterminate: bool = False # Spinner mode vs. progress bar mode steps: list[ProgressStep] | None # Named steps with status (pending/active/done)

class ProgressStep: """A named step within a progress indicator.""" label: str status: str # "pending" | "active" | "done" | "error" | "skipped"

class CodeBlock(OutputElement): """A block of source code with optional syntax highlighting.""" element_type = "code" content: str language: str | None # For syntax highlighting line_numbers: bool = False highlight_lines: list[int] | None # Lines to emphasize

class DiffBlock(OutputElement): """A unified diff display.""" element_type = "diff" hunks: list[DiffHunk] file_a: str | None file_b: str | None stats: dict | None # insertions, deletions, etc.

class DiffHunk: """A single hunk within a diff.""" header: str # @@ line range @@ lines: list[DiffLine]

class DiffLine: """A single line in a diff hunk.""" type: str # "context" | "add" | "remove" content: str line_number_old: int | None line_number_new: int | None

class TextBlock(OutputElement): """A free-form text block (descriptions, rationale, etc.).""" element_type = "text" content: str wrap: bool = True indent: int = 0

class Separator(OutputElement): """A visual separator between logical groups.""" element_type = "separator" style: str = "line" # "line" | "blank" | "double"

class ActionHint(OutputElement): """A suggested next-step action for the user.""" element_type = "action_hint" commands: list[str] # Suggested CLI commands description: str | None

class StructuredOutput: """Static snapshot of a complete command output. This is the accumulated state of all elements at a point in time. It is produced by OutputSession.snapshot() and OutputSession.close(). Uses: - Final serialization for json/yaml formats - Logging and audit trails - Programmatic inspection and testing - TUI widget data binding (initial state) """ command: str # The command that produced this output session_id: str # The session that produced this output elements: list[OutputElement] # Ordered list of element snapshots exit_code: int = 0 timing: dict | None # start_time, end_time, duration metadata: dict # command-specific metadata

Materialization Strategies

The materialization strategy is the format-side counterpart to the output session. It receives element events and decides when and how to render content. Each strategy is paired with an ElementRenderer that handles the actual visual formatting of individual elements.

The strategy pattern creates a clean separation between timing/ordering policy (when to render) and visual formatting (how to render). This means the same PlainElementRenderer can be used whether elements arrive all-at-once or are streamed concurrently — the strategy handles the coordination.


class MaterializationStrategy(Protocol):
    """Interface for format-driven output materialization.
    
    A materialization strategy receives element lifecycle events from the
    OutputSession and decides when to render element content to the output
    stream. Strategies do not render elements themselves — they delegate
    to a paired ElementRenderer at the appropriate time.
    
    The strategy is the mechanism by which format-agnostic producer code
    produces correct output regardless of format. The producer writes to
    handles; the strategy decides what reaches the terminal and when.
    """
strategy_name: <span style="color: cyan;">str</span>                   <span style="opacity: 0.7;"># &quot;live&quot; | &quot;sequential_buffer&quot; | &quot;accumulate&quot;</span>

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">bind</span>(self, renderer: <span style="color: #66cc66;">&quot;ElementRenderer&quot;</span>,
         terminal_caps: <span style="color: #66cc66;">&quot;TerminalCapabilities&quot;</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Bind this strategy to a renderer and terminal capabilities.</span>

Called once during format resolution, before the session opens. The strategy retains a reference to the renderer for use during event handling. The output stream is provided separately via on_session_begin, since the session owns the stream. """ ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">on_session_begin</span>(self, session: OutputSession, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Called when the session opens. The strategy receives the output</span>

stream and may write preamble (e.g., opening JSON bracket).""" ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">on_element_created</span>(self, event: ElementCreated) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Called when a new element handle is created.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">on_element_updated</span>(self, event: ElementUpdated) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Called when data is written to an element handle.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">on_element_closed</span>(self, event: ElementClosed) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Called when an element handle is closed.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">on_session_end</span>(self, event: SessionEnd) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Called when the session closes. The strategy may write epilogue.&quot;&quot;&quot;</span>
    ...

class LiveMaterializer(MaterializationStrategy): """Materialization strategy for the rich format. Renders element updates in real-time using terminal cursor movement. Multiple elements can be visually active and updating simultaneously. The terminal display is a live document that is rewritten in place. Behavior: - on_element_created: Allocates screen region for the element, renders initial (possibly empty) visual state. - on_element_updated: Re-renders the element in place using cursor movement. For progress indicators, updates may be throttled to a maximum refresh rate (default: 15 fps) to avoid terminal flooding. - on_element_closed: Renders the final state and freezes the screen region (no further updates). May apply a visual transition (e.g., spinner resolves to a checkmark). - on_session_end: Finalizes the display, moves cursor to end, and restores normal terminal scrolling. Screen Layout: Elements are arranged vertically in declaration order. Each element occupies a contiguous block of terminal lines. The materializer tracks the line offset and height of each element's region. When an element's height changes (e.g., a table gains rows), subsequent elements are shifted down. Concurrent Updates: Updates from multiple handles are coalesced into a single frame refresh at the target frame rate. The materializer maintains a dirty-element set and redraws all dirty elements in a single pass per frame. """ strategy_name = "live"

_frame_rate: <span style="color: cyan;">float</span> = 15.0            <span style="opacity: 0.7;"># Maximum redraws per second</span>
_element_regions: <span style="color: cyan;">OrderedDict</span>[<span style="color: cyan;">str</span>, ScreenRegion]  <span style="opacity: 0.7;"># handle_id → screen region</span>
_dirty_set: <span style="color: cyan;">set</span>[<span style="color: cyan;">str</span>]                 <span style="opacity: 0.7;"># handle_ids that need redraw</span>
_frame_timer: asyncio.TimerHandle    <span style="opacity: 0.7;"># Coalescing timer for frame redraws</span>

class SequentialBufferMaterializer(MaterializationStrategy): """Materialization strategy for plain, color, and table formats. Buffers element content and renders elements sequentially in declaration order. An element's content is rendered to the output stream only when its handle is closed. If handles are closed out of declaration order, the out-of-order element's rendered content is held in a buffer until all preceding elements have been rendered. This strategy ensures that static, scrolling output formats produce coherent sequential output even when producers write to handles concurrently and close them in arbitrary order. Behavior: - on_element_created: Records the element's declaration index. No output. - on_element_updated: Buffers the update internally. No output. - on_element_closed: If this element is the next in declaration order, renders it immediately (and any buffered subsequent elements that are also closed). Otherwise, buffers the rendered content. - on_session_end: Force-renders any remaining buffered elements (handles that were never closed, in declaration order). Example (two tables populated concurrently): 1. Handle A (index 0) created — table "Resources" 2. Handle B (index 1) created — table "Validations" 3. Handle B receives rows, Handle A receives rows (interleaved) 4. Handle B closes (index 1) — rendered content buffered (waiting for A) 5. Handle A closes (index 0) — A is rendered to stream, then buffered B is rendered to stream Result: Output shows table A followed by table B, regardless of the order in which data arrived or handles closed. """ strategy_name = "sequential_buffer"

_next_render_index: <span style="color: cyan;">int</span> = 0          <span style="opacity: 0.7;"># The declaration index to render next</span>
_rendered_buffers: <span style="color: cyan;">dict</span>[<span style="color: cyan;">int</span>, <span style="color: cyan;">str</span>]     <span style="opacity: 0.7;"># index → pre-rendered content (waiting)</span>
_closed_set: <span style="color: cyan;">set</span>[<span style="color: cyan;">int</span>]                <span style="opacity: 0.7;"># Declaration indices of closed elements</span>

class AccumulateMaterializer(MaterializationStrategy): """Materialization strategy for json and yaml formats. Accumulates all element data silently until the session ends, then serializes the complete StructuredOutput as a single JSON or YAML document. Behavior: - on_element_created: No output. - on_element_updated: No output. - on_element_closed: No output. - on_session_end: Calls session.snapshot() to get the final StructuredOutput, then delegates to the ElementRenderer's serialize() method for complete document serialization. This strategy is the simplest — it ignores all intermediate events and only acts on session_end. It exists as a distinct strategy (rather than a special case) to maintain the uniform strategy interface. """ strategy_name = "accumulate"

ElementRenderer Protocol

While the MaterializationStrategy controls when elements are rendered, the ElementRenderer controls how each element type is visually formatted. Each format has a paired ElementRenderer implementation:


class ElementRenderer(Protocol):
    """Interface for format-specific element rendering.
    
    An ElementRenderer knows how to paint each element type for a specific
    output format. It is called by the MaterializationStrategy when it is
    time to render an element.
    
    Implementations:
    - PlainElementRenderer: ASCII text, no escapes
    - ColorElementRenderer: ANSI-colored text, same layout as plain
    - TableElementRenderer: Unicode box-drawing with color
    - RichElementRenderer: Advanced terminal features (cursor, animation)
    - JsonElementRenderer: JSON serialization
    - YamlElementRenderer: YAML serialization
    """
format_name: <span style="color: cyan;">str</span>                     <span style="opacity: 0.7;"># &quot;plain&quot;, &quot;color&quot;, &quot;table&quot;, &quot;rich&quot;, &quot;json&quot;, &quot;yaml&quot;</span>

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">render_panel</span>(self, panel: Panel, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Render a panel element to the stream.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">render_table</span>(self, table: Table, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Render a table element to the stream.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">render_tree</span>(self, tree: Tree, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Render a tree element to the stream.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">render_status</span>(self, status: StatusMessage, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Render a status message to the stream.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">render_progress</span>(self, progress: ProgressIndicator, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Render a progress indicator to the stream.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">render_code</span>(self, code: CodeBlock, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Render a code block to the stream.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">render_diff</span>(self, diff: DiffBlock, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Render a diff block to the stream.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">render_text</span>(self, text: TextBlock, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Render a text block to the stream.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">render_separator</span>(self, separator: Separator, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Render a visual separator to the stream.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">render_action_hint</span>(self, hint: ActionHint, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Render an action hint to the stream.&quot;&quot;&quot;</span>
    ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">render_element</span>(self, element: OutputElement, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Dispatch to the appropriate render method based on element type.</span>

This is the primary entry point used by materialization strategies. It uses a dispatch table to route to the correct typed method. """ dispatch = { "panel": self.render_panel, "table": self.render_table, "tree": self.render_tree, "status": self.render_status, "progress": self.render_progress, "code": self.render_code, "diff": self.render_diff, "text": self.render_text, "separator": self.render_separator, "action_hint": self.render_action_hint, } handler = dispatch.get(element.element_type) if handler: handler(element, stream)

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">serialize</span>(self, output: StructuredOutput, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Serialize a complete StructuredOutput to the stream.</span>

Used by AccumulateMaterializer for json/yaml formats. For visual formats (plain/color/table/rich), this method iterates over output.elements and calls render_element for each. """ ...

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">can_render</span>(self, terminal_caps: <span style="color: #66cc66;">&quot;TerminalCapabilities&quot;</span>) -&gt; <span style="color: cyan;">bool</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Whether this renderer can operate in the given terminal environment.&quot;&quot;&quot;</span>
    ...

Format Resolution

The active format is resolved using a precedence chain:

  1. CLI flag: --format <value> on the command line (highest priority).
  2. Environment variable: CLEVERAGENTS_FORMAT=<value>.
  3. Config file: The core.format key in the global config (agents config set core.format <value>).
  4. TTY detection: If stdout is not a TTY and no explicit format was set, fall back to plain (not rich), since non-TTY consumers cannot interpret ANSI codes or cursor movement.
  5. Default: rich.

Once the format is resolved, the CLI framework selects the corresponding (MaterializationStrategy, ElementRenderer) pair from the RendererRegistry, opens an OutputSession bound to that strategy, and passes the session to the command implementation.

Format Specifications

plain — Plain Text

Philosophy: Maximum portability. Output is pure ASCII text with no escape codes, no box-drawing characters, and no color. Suitable for piping to files, logs, grep, awk, or any non-terminal consumer.

Rendering rules:

  • Panels: Rendered as indented key-value pairs with a header line.
  • Tables: Rendered as aligned columns separated by whitespace (no box drawing). Column headers are separated from data by a dashed line.
  • Trees: Rendered with ASCII indentation using +-- and | characters.
  • Status messages: Prefixed with [OK], [WARN], [ERROR], [INFO].
  • Progress: Rendered as static status lines (no animation). Steps shown as [x] (done), [ ] (pending), [>] (active).
  • Diffs: Standard unified diff format.
  • Code blocks: Raw text with optional line numbers.
  • No ANSI escape codes of any kind.
  • No Unicode characters beyond basic ASCII (no box drawing, no checkmarks, no arrows).

Example (agents --format plain project show local/api-service):


$ agents --format plain project show local/api-service

Project Details Name: local/api-service Description: Backend API Resources: 2 Remote: no Created: 2026-02-08 12:46

Linked Resources Resource Type Sandbox Read-Only


local/api-repo git-checkout git_worktree no local/staging-db local/database transaction_rollback yes

Validations (3) local/run-tests pytest --cov=src --cov-fail-under=80 required local/lint-check ruff check . required local/check-bundle-size node scripts/check-bundle-size.js informational

Context Include: repo Exclude: /node_modules/ Max File Size: 1 MB

Indexing Status Text Index: ready Vector Index: ready Graph Store: disabled Indexed Files: 347 Last Indexed: 12:48

Active Plans Plan ID Action Phase


01HXM7A9 local/code-coverage execute

[OK] Project loaded

Example (agents --format plain plan list):


$ agents --format plain plan list --phase execute

Plans ID Phase State Action Project Elapsed


01HXM7A9 execute processing local/code-coverage local/api-service 00:01:12

Filters Phase: execute State: (any) Project: (any) Action: (any)

Summary Total: 1 Processing: 1 Completed: 0 Errored: 0

[OK] 1 plan listed

color — Colored Plain Text

Philosophy: Same structural layout as plain, but with ANSI color codes applied to improve readability. No box-drawing characters, no cursor movement, no animation.

Rendering rules:

  • Identical layout to plain, but with color applied:
    • Headers/titles: Bold cyan.
    • Keys: Bold blue.
    • Values: Default color, with semantic coloring:
      • Success/positive: Green.
      • Warnings/attention: Yellow.
      • Errors/failures: Red.
      • Identifiers/names: Cyan.
      • Counts/numbers: Default (white).
    • Table headers: Bold cyan with underlines rendered as dim dashes.
    • Status prefixes: [OK] in green, [WARN] in yellow, [ERROR] in red, [INFO] in blue.
    • Diff lines: + lines green, - lines red, @@ headers cyan.
  • No box-drawing characters — uses the same whitespace/dash layout as plain.
  • No cursor movement or animation — pure scrolling output.
  • Respects NO_COLOR environment variable: If NO_COLOR is set, color format falls back to plain.

Example (agents --format color project show local/api-service):


$ agents --format color project show local/api-service

Project Details Name: local/api-service Description: Backend API Resources: 2 Remote: no Created: 2026-02-08 12:46

Linked Resources Resource Type Sandbox Read-Only ---------------- -------------- -------------------- --------- local/api-repo git-checkout git_worktree no local/staging-db local/database transaction_rollback yes

Validations (3) local/run-tests pytest --cov=src --cov-fail-under=80 required local/lint-check ruff check . required local/check-bundle-size node scripts/check-bundle-size.js info

Context Include: repo Exclude: /node_modules/ Max File Size: 1 MB

Indexing Status Text Index: ready Vector Index: ready Graph Store: disabled Indexed Files: 347 Last Indexed: 12:48

Active Plans Plan ID Action Phase -------- ------------------- ------- 01HXM7A9 local/code-coverage execute

[OK] Project loaded

table — ASCII Box-Drawing Tables

Philosophy: Structured, visually distinct panels and tables using Unicode box-drawing characters (╭──────────────────╮╰╯│─). Uses color. This is the style shown in the existing CLI examples throughout this document.

Rendering rules:

  • Panels: Rendered as bordered boxes with title in the top border. Uses ╭─╮│╰──────────────────╯ characters for rounded corners.
  • Tables: Rendered inside bordered boxes with column-aligned headers and separator lines using and .
  • Trees: Rendered inside a bordered box using ├──, └──, tree guide characters.
  • Status messages: Use Unicode indicators: (green) for OK, (yellow) for WARN, (red) for ERROR, (blue) for INFO.
  • Progress: Rendered as a step list inside a panel with , , markers.
  • Color scheme: Same semantic coloring as color format, applied within box structures.
  • No animation or cursor movement — the boxes are static, scrolling output.

Distinction from rich: The table format uses the same box-drawing panels and color as rich for static content, but it does not use any dynamic or interactive terminal features. There are no animated spinners, no live-updating progress bars, no cursor movement, and no in-place redraws. All output is static and scrolls sequentially. Where rich would show a spinning and a live progress bar, table renders a static snapshot using fixed markers (, , ). This makes table suitable for terminals without advanced capabilities, and for output that will be reviewed after the fact (e.g., scrollback buffers).

Example (agents --format table project show local/api-service):


$ agents --format table project show local/api-service

╭─ Project Details ──────────────╮ │ Name: local/api-service │ │ Description: Backend API │ │ Resources: 2 │ │ Remote: no │ │ Created: 2026-02-08 12:46 │ ╰────────────────────────────────╯

╭─ Linked Resources ──────────────────────────────────────────────────────╮ │ Resource Type Sandbox Read-Only │ │ ──────────────── ────────────── ──────────────────── ───────── │ │ local/api-repo git-checkout git_worktree no │ │ local/staging-db local/database transaction_rollback yes │ ╰─────────────────────────────────────────────────────────────────────────╯

╭─ Validations (3) ───────────────────────────────────────────────────────────────╮ │ local/run-tests pytest --cov=src --cov-fail-under=80 required │ │ local/lint-check ruff check . required │ │ local/check-bundle-size node scripts/check-bundle-size.js informational │ ╰─────────────────────────────────────────────────────────────────────────────────╯

╭─ Context ───────────────────╮ │ Include: repo │ │ Exclude: /node_modules/ │ │ Max File Size: 1 MB │ ╰─────────────────────────────╯

╭─ Indexing Status ──────────╮ │ Text Index: ready │ │ Vector Index: ready │ │ Graph Store: disabled │ │ Indexed Files: 347 │ │ Last Indexed: 12:48 │ ╰────────────────────────────╯

╭─ Active Plans ──────────────────────────╮ │ Plan ID Action Phase │ │ ──────── ─────────────────── ─────── │ │ 01HXM7A9 local/code-coverage execute │ ╰─────────────────────────────────────────╯

✓ OK Project loaded

Example (agents --format table plan execute 01HXM8C2ZK):

Unlike rich mode which would show animated spinners and a live progress bar, the table format renders a static snapshot of execution state:


$ agents --format table plan execute 01HXM8C2ZK

╭─ Execution ──────────────────────╮ │ Plan: 01HXM8C2ZK4Q7C2B3F2R4VYV6J │ │ Phase: execute │ │ Sandbox: git_worktree │ │ Worker: local/executor │ │ Started: 12:58:10 │ │ Attempt: 1 │ ╰──────────────────────────────────╯

╭─ Strategy Summary ─────────────────────╮ │ Decisions: 8 │ │ Invariants: 2 │ │ Planned Child Plans: 2+ │ │ Estimated Files: ~12 │ │ Risk: low │ ╰────────────────────────────────────────╯

╭─ Progress ─────────╮ │ Collect context │ │ Run tools │ │ Build changeset │ │ Validate │ ╰────────────────────╯

✓ OK Execution started

rich — Modern Rich CLI Elements

Philosophy: The premium interactive terminal experience. Uses advanced terminal capabilities: cursor movement, inline updates, animated spinners, live-updating progress bars, collapsible sections, syntax highlighting, and dynamic layout. This is the default format.

Rendering rules:

  • Panels: Rich bordered panels with rounded corners, title bars, and optional collapse/expand behavior. Panels may animate into view.
  • Tables: Full-featured tables with automatic column sizing, truncation with ellipsis, sortable column indicators, alternating row shading, and horizontal scrolling for wide tables.
  • Trees: Interactive collapsible trees. Nodes expand/collapse with visual animation. Color-coded by node type. Depth guides use dotted lines.
  • Status messages: Use animated checkmarks/spinners that resolve to final state. Success messages may briefly flash or highlight.
  • Progress: Live-updating progress bars with:
    • Animated spinners (Braille, dots, or bars depending on terminal capability).
    • Elapsed time and ETA.
    • Per-step status with animated transitions (pending → active → done).
    • Multi-line progress for parallel operations.
  • Diffs: Syntax-highlighted side-by-side or unified diffs with line numbers, change highlighting at the character level (not just line level), and navigable hunks.
  • Code blocks: Full syntax highlighting using terminal colors (256-color or truecolor when available). Line numbers in dim color. Highlighted lines with background color.
  • Dynamic layout: Adapts to terminal width. Narrow terminals get a stacked layout; wide terminals get side-by-side panels.
  • Live updates: Long-running commands (plan execute, plan status) use live-updating displays that redraw in place rather than scrolling.
  • Graceful degradation: If the terminal does not support required capabilities (e.g., no truecolor, no cursor movement), the renderer automatically falls back to table rendering for those elements.

Example (agents --format rich plan execute 01HXM8C2ZK):

The rich format produces output that cannot be fully represented in static documentation — animated spinners cycle in place, progress bars fill smoothly, and elements update without scrolling. The rendering below is a static snapshot of what the terminal would display at a given moment:


$ agents --format rich plan execute 01HXM8C2ZK

╭─ Execution ──────────────────────╮ │ Plan: 01HXM8C2ZK4Q7C2B3F2R4VYV6J │ │ Phase: execute │ │ Sandbox: git_worktree │ │ Worker: local/executor │ │ Started: 12:58:10 │ ╰──────────────────────────────────╯

Collecting context... (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ animates in place) ├── repo: local/api-repo └── db: local/staging-db

╭─ Strategy Summary ──────────────────────────────────────────────────────╮ │ 8 decisions │ 2 invariants │ 2+ child plans │ ~12 files │ risk: low │ ╰─────────────────────────────────────────────────────────────────────────╯

Progress ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 42% elapsed 0:01:12 ETA 0:01:40 Collect context .................. 0.8s Run tools (8 calls) ............. 12.4s Build changeset ................. (running) (animates in place) Validate ........................ (pending)

╭─ Live Tool Calls ───────────────────────────────────────╮ │ #6 read_file src/auth/__init__.py 0.1s │ │ #7 write_file tests/test_auth.py 0.2s │ │ #8 edit_file src/auth/session.py ... │ ╰─────────────────────────────────────────────────────────╯

In rich mode:

  • The Braille spinner characters (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏) animate in real-time, cycling in place without scrolling.
  • The progress bar (━━━) fills smoothly as work completes.
  • Completed steps appear with a green via in-place line update (the line rewrites, it does not scroll).
  • The "Live Tool Calls" panel scrolls its content internally, showing only the most recent N calls.
  • The terminal is not flooded with scrolling text — elements update in place using cursor movement.

Example (agents --format rich version):


$ agents --format rich version

╭─────────────────────────────────────╮ │ CleverAgents CLI v1.0.0 │ │ channel: stable │ ╰─────────────────────────────────────╯

╭─ Build ─────────────────────────────╮ │ Build Date: 2026-02-08 │ │ Commit: a17c3f9 │ │ Schema: v3 │ │ Platform: linux-x86_64 │ │ Python: 3.13.1 │ ╰─────────────────────────────────────╯

╭─ Dependencies ─────────────────────────────────────────────────────────╮ │ LangGraph 0.2.60 │ LangChain 0.3.18 │ MCP SDK 1.4.0 │ Pydantic 2.10.4 │ ╰────────────────────────────────────────────────────────────────────────╯

✓ OK Version reported

In rich mode, the version card may use background colors, bold gradients, or subtle box shadows (depending on terminal truecolor support). Elements may animate into view with a brief slide or fade transition.

json — JSON Data Structure

Philosophy: Machine-readable output for programmatic consumption. Every command produces a well-defined JSON object. No ANSI color codes in the structural data. Color codes may appear only within text values that represent verbatim content (such as code blocks or diff output where the original text contained ANSI sequences), but all structural keys, labels, and metadata are plain strings.

Rendering rules:

  • Top-level structure: Always a JSON object with a standard envelope:
    
    {
      "command": "project show",
      "status": "ok",
      "exit_code": 0,
      "data": { ... },
      "timing": { "duration_ms": 42 },
      "metadata": { ... }
    }
    
  • Panels: Rendered as nested objects within data.
  • Tables: Rendered as arrays of objects within data.
  • Trees: Rendered as nested objects with children arrays.
  • Status messages: Included in a messages array within the envelope.
  • Progress: Not rendered (JSON output is non-interactive; progress is omitted).
  • Diffs: Rendered as structured objects with hunks arrays.
  • No ANSI codes in any structural element. Raw ANSI codes are preserved only in string values that represent verbatim terminal output.
  • Pretty-printed by default (indented). Compact mode available via <span style="color: cyan;">--json-compact</span> (future option).
  • Consistent schema per command: Each command's JSON schema is stable and documented, enabling reliable programmatic parsing.

Example (agents <span style="color: cyan;">--format</span> json project show local/api-service):


{
  "command": "project show",
  "status": "ok",
  "exit_code": 0,
  "data": {
    "project": {
      "name": "local/api-service",
      "description": "Backend API",
      "type": "local",
      "remote": false,
      "created_at": "2026-02-08T12:46:00Z"
    },
    "linked_resources": [
      {
        "name": "local/api-repo",
        "type": "git-checkout",
        "sandbox_strategy": "git_worktree",
        "read_only": false
      },
      {
        "name": "local/staging-db",
        "type": "local/database",
        "sandbox_strategy": "transaction_rollback",
        "read_only": true
      }
    ],
    "validations": [
      {
        "name": "local/run-tests",
        "command": "pytest --cov=src --cov-fail-under=80",
        "mode": "required",
        "timeout": 600,
        "resource": "repo"
      },
      {
        "name": "local/lint-check",
        "command": "ruff check .",
        "mode": "required",
        "timeout": 300,
        "resource": null
      },
      {
        "name": "local/check-bundle-size",
        "command": "node scripts/check-bundle-size.js",
        "mode": "informational",
        "timeout": 300,
        "resource": null
      }
    ],
    "context": {
      "include_resources": ["repo"],
      "exclude_paths": ["**/node_modules/**"],
      "max_file_size_bytes": 1048576
    },
    "indexing": {
      "text_index": "ready",
      "vector_index": "ready",
      "graph_store": "disabled",
      "indexed_files": 347,
      "last_indexed_at": "2026-02-08T12:48:00Z"
    },
    "active_plans": [
      {
        "plan_id": "01HXM7A9",
        "action": "local/code-coverage",
        "phase": "execute"
      }
    ]
  },
  "timing": {
    "duration_ms": 42
  },
  "messages": [
    { "level": "ok", "text": "Project loaded" }
  ]
}

Example (agents --format json plan list --phase execute):


{
  "command": "plan list",
  "status": "ok",
  "exit_code": 0,
  "data": {
    "plans": [
      {
        "id": "01HXM7A9",
        "phase": "execute",
        "state": "processing",
        "action": "local/code-coverage",
        "project": "local/api-service",
        "elapsed": "00:01:12"
      }
    ],
    "filters": {
      "phase": "execute",
      "state": null,
      "project": null,
      "action": null
    },
    "summary": {
      "total": 1,
      "processing": 1,
      "completed": 0,
      "errored": 0
    }
  },
  "timing": {
    "duration_ms": 18
  },
  "messages": [
    { "level": "ok", "text": "1 plan listed" }
  ]
}
yaml — YAML Data Structure

Philosophy: Same data as json but in YAML format. Preferred by users who find YAML more readable for configuration and scripting workflows. Follows the same structural conventions as json.

Rendering rules:

  • Same data envelope as JSON (command, status, exit_code, data, timing, messages).
  • YAML 1.2 compliant output.
  • Multi-line strings use YAML block scalars (| for literal, > for folded) when appropriate.
  • No ANSI codes in structural elements (same rule as JSON).
  • Sorted keys for deterministic output.

Example (agents --format yaml project show local/api-service):


command: project show
status: ok
exit_code: 0
data:
  project:
    name: local/api-service
    description: Backend API
    type: local
    remote: false
    created_at: "2026-02-08T12:46:00Z"
  linked_resources:
    - name: local/api-repo
      type: git-checkout
      sandbox_strategy: git_worktree
      read_only: false
    - name: local/staging-db
      type: local/database
      sandbox_strategy: transaction_rollback
      read_only: true
  validations:
    - name: local/run-tests
      command: "pytest --cov=src --cov-fail-under=80"
      mode: required
      timeout: 600
      resource: repo
    - name: local/lint-check
      command: "ruff check ."
      mode: required
      timeout: 300
      resource: null
    - name: local/check-bundle-size
      command: "node scripts/check-bundle-size.js"
      mode: informational
      timeout: 300
      resource: null
  context:
    include_resources:
      - repo
    exclude_paths:
      - "**/node_modules/**"
    max_file_size_bytes: 1048576
  indexing:
    text_index: ready
    vector_index: ready
    graph_store: disabled
    indexed_files: 347
    last_indexed_at: "2026-02-08T12:48:00Z"
  active_plans:
    - plan_id: 01HXM7A9
      action: local/code-coverage
      phase: execute
timing:
  duration_ms: 42
messages:
  - level: ok
    text: Project loaded

Format Comparison Matrix

Capability plain color table rich json yaml
Color codes No Yes Yes Yes No No
Box drawing No No Yes Yes No No
Animation/spinners No No No Yes No No
Live updates No No No Yes No No
Cursor movement No No No Yes No No
Syntax highlighting No No No Yes No No
Collapsible sections No No No Yes No No
Machine-parseable Partially Partially No No Yes Yes
Pipe-safe Yes No* No No Yes Yes
Unicode required No No Yes Yes No No
TTY required No No No Yes** No No

* color output can be piped if the consumer understands ANSI codes (e.g., less -R). ** rich gracefully degrades to table when stdout is not a TTY.

Renderer Registration and Extension

The framework uses a registry pattern for format renderers, enabling third-party or plugin renderers. Each format is registered as a (MaterializationStrategy, ElementRenderer) pair — the strategy controls timing/ordering, and the renderer controls visual formatting:


@dataclass
class FormatRegistration:
    """A registered format: its strategy factory, renderer factory, and fallback."""
    strategy_factory: Callable[[TerminalCapabilities], MaterializationStrategy]
    renderer_factory: Callable[[TerminalCapabilities], ElementRenderer]
    fallback: str | None                 # Format name to fall back to, or None

class RendererRegistry: """Central registry for format (strategy, renderer) pairs. Formats are registered by name and resolved at runtime based on the active format and terminal capabilities. The registry supports dynamic registration, enabling plugins to add custom formats (e.g., 'html', 'csv', 'markdown'). Built-in registrations: "rich" → (LiveMaterializer, RichElementRenderer), fallback="table" "table" → (SequentialBufferMaterializer, TableElementRenderer), fallback="color" "color" → (SequentialBufferMaterializer, ColorElementRenderer), fallback="plain" "plain" → (SequentialBufferMaterializer, PlainElementRenderer), fallback=None "json" → (AccumulateMaterializer, JsonElementRenderer), fallback=None "yaml" → (AccumulateMaterializer, YamlElementRenderer), fallback=None """

_formats: <span style="color: cyan;">dict</span>[<span style="color: cyan;">str</span>, FormatRegistration] = {}

<span style="color: yellow;">@classmethod</span>
<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">register</span>(cls, format_name: <span style="color: cyan;">str</span>,
             strategy_factory: <span style="color: cyan;">Callable</span>[[TerminalCapabilities], MaterializationStrategy],
             renderer_factory: <span style="color: cyan;">Callable</span>[[TerminalCapabilities], ElementRenderer],
             fallback: <span style="color: cyan;">str</span> | <span style="color: magenta; font-weight: 600;">None</span> = <span style="color: magenta; font-weight: 600;">None</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Register a format.</span>

Args: format_name: The format identifier (e.g., 'rich', 'json'). strategy_factory: Callable that creates a MaterializationStrategy, given terminal capabilities. renderer_factory: Callable that creates an ElementRenderer, given terminal capabilities. fallback: Optional fallback format name if this format cannot operate in the current terminal environment. """ cls._formats[format_name] = FormatRegistration( strategy_factory=strategy_factory, renderer_factory=renderer_factory, fallback=fallback, )

<span style="color: yellow;">@classmethod</span>
<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">resolve</span>(cls, format_name: <span style="color: cyan;">str</span>,
            terminal_caps: TerminalCapabilities
            ) -&gt; <span style="color: cyan;">tuple</span>[MaterializationStrategy, ElementRenderer]:
    <span style="color: #66cc66;">&quot;&quot;&quot;Resolve the best (strategy, renderer) pair for the given format.</span>

Walks the fallback chain if the requested format's renderer cannot operate in the current terminal environment. Returns the first pair where the renderer reports can_render(terminal_caps) == True. Raises: ValueError: If no usable format is found (should never happen since 'plain' has no fallback and always works). """ current = format_name visited: set[str] = set()

    <span style="color: magenta; font-weight: 600;">while</span> current <span style="color: magenta; font-weight: 600;">and</span> current <span style="color: magenta; font-weight: 600;">not</span> <span style="color: magenta; font-weight: 600;">in</span> visited:
        visited.add(current)
        registration = cls._formats.get(current)
        <span style="color: magenta; font-weight: 600;">if</span> registration <span style="color: magenta; font-weight: 600;">is</span> <span style="color: magenta; font-weight: 600;">None</span>:
            <span style="color: magenta; font-weight: 600;">break</span>

        renderer = registration.renderer_factory(terminal_caps)
        <span style="color: magenta; font-weight: 600;">if</span> renderer.can_render(terminal_caps):
            strategy = registration.strategy_factory(terminal_caps)
            strategy.bind(renderer, terminal_caps=terminal_caps)
            <span style="color: magenta; font-weight: 600;">return</span> strategy, renderer

        current = registration.fallback

    <span style="opacity: 0.7;"># Ultimate fallback is always plain</span>
    plain = cls._formats[<span style="color: #66cc66;">&quot;plain&quot;</span>]
    renderer = plain.renderer_factory(terminal_caps)
    strategy = plain.strategy_factory(terminal_caps)
    strategy.bind(renderer, terminal_caps=terminal_caps)
    <span style="color: magenta; font-weight: 600;">return</span> strategy, renderer

<span style="color: yellow;">@classmethod</span>
<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">available_formats</span>(cls) -&gt; <span style="color: cyan;">list</span>[<span style="color: cyan;">str</span>]:
    <span style="color: #66cc66;">&quot;&quot;&quot;Return all registered format names.&quot;&quot;&quot;</span>
    <span style="color: magenta; font-weight: 600;">return</span> sorted(cls._formats.keys())

<span style="color: yellow;">@classmethod</span>
<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">is_registered</span>(cls, format_name: <span style="color: cyan;">str</span>) -&gt; <span style="color: cyan;">bool</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Check if a format is registered.&quot;&quot;&quot;</span>
    <span style="color: magenta; font-weight: 600;">return</span> format_name <span style="color: magenta; font-weight: 600;">in</span> cls._formats

Terminal Capability Detection

The framework detects terminal capabilities to guide format resolution, strategy selection, and renderer fallback:


@dataclass
class TerminalCapabilities:
    """Detected capabilities of the output terminal.
    
    This dataclass is populated once at CLI startup and passed to the
    RendererRegistry for format resolution. It is also available to
    individual strategies and renderers for fine-grained adaptation
    (e.g., adjusting column widths to terminal width, choosing between
    256-color and truecolor palettes).
    """
    is_tty: bool                         # Is stdout a TTY?
    width: int                           # Terminal width in columns
    height: int                          # Terminal height in rows
    supports_ansi: bool                  # Supports basic ANSI escape codes?
    supports_256_color: bool             # Supports 256-color palette?
    supports_truecolor: bool             # Supports 24-bit truecolor?
    supports_unicode: bool               # Supports Unicode (box-drawing, etc.)?
    supports_cursor_movement: bool       # Supports cursor repositioning?
    supports_alternate_screen: bool      # Supports alternate screen buffer?
    no_color: bool                       # Is NO_COLOR environment variable set?
    term_program: str | None             # TERM_PROGRAM value (e.g., "iTerm2", "vscode")
<span style="color: yellow;">@classmethod</span>
<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">detect</span>(cls) -&gt; <span style="color: #66cc66;">&quot;TerminalCapabilities&quot;</span>:
    <span style="color: #66cc66;">&quot;&quot;&quot;Auto-detect terminal capabilities from the environment.</span>

Detection logic: - is_tty: os.isatty(sys.stdout.fileno()) - width/height: os.get_terminal_size() with fallback to (80, 24) - supports_ansi: True if is_tty and not Windows legacy console - supports_256_color: True if TERM contains "256color" or COLORTERM is set - supports_truecolor: True if COLORTERM is "truecolor" or "24bit" - supports_unicode: True if locale encoding is UTF-8 - supports_cursor_movement: True if is_tty and TERM is not "dumb" - supports_alternate_screen: True if supports_cursor_movement - no_color: True if NO_COLOR environment variable is set (any value) - term_program: Value of TERM_PROGRAM environment variable """ ...

Plugin Format Registration

Third-party plugins can register custom formats using the registry:


# Example: Registering a custom 'csv' format plugin
from cleveragents.output import RendererRegistry, SequentialBufferMaterializer

class CsvElementRenderer(ElementRenderer): """Renders tables as CSV, other elements as plain text.""" format_name = "csv"

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">render_table</span>(self, table: Table, stream: <span style="color: cyan;">IO</span>) -&gt; <span style="color: magenta; font-weight: 600;">None</span>:
    writer = csv.writer(stream)
    writer.writerow([col.name <span style="color: magenta; font-weight: 600;">for</span> col <span style="color: magenta; font-weight: 600;">in</span> table.columns])
    <span style="color: magenta; font-weight: 600;">for</span> row <span style="color: magenta; font-weight: 600;">in</span> table.rows:
        writer.writerow([row.get(col.name, <span style="color: #66cc66;">&quot;&quot;</span>) <span style="color: magenta; font-weight: 600;">for</span> col <span style="color: magenta; font-weight: 600;">in</span> table.columns])

<span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">can_render</span>(self, terminal_caps: TerminalCapabilities) -&gt; <span style="color: cyan;">bool</span>:
    <span style="color: magenta; font-weight: 600;">return</span> <span style="color: magenta; font-weight: 600;">True</span>  <span style="opacity: 0.7;"># CSV works everywhere</span>

# Register at plugin load time RendererRegistry.register( format_name="csv", strategy_factory=lambda caps: SequentialBufferMaterializer(), renderer_factory=lambda caps: CsvElementRenderer(), fallback="plain", )

Edge Cases and Special Behaviors

Error Output

Errors are rendered through the same framework. When a command raises an exception or signals an error, the session's context manager (__exit__) catches the exception, creates a StatusMessage with level="error" and an optional TextBlock with details, and closes the session with exit_code=1. In json/yaml formats, errors produce:


{
  "command": "project show",
  "status": "error",
  "exit_code": 1,
  "error": {
    "code": "NOT_FOUND",
    "message": "Project 'local/nonexistent' not found",
    "detail": "No project with name 'local/nonexistent' exists. Run 'agents project list' to see available projects.",
    "suggestions": [
      "agents project list",
      "agents project create local/nonexistent"
    ]
  },
  "timing": { "duration_ms": 5 }
}
Empty Results

When a list command returns no results, all formats handle it gracefully:

  • plain/color: Prints a message like No projects found.
  • table: Renders an empty table with headers and a (empty) message.
  • rich: Renders a dimmed panel with an empty-state message and suggested actions.
  • json/yaml: Returns an empty array in the appropriate data field.
Large Data Sets

For commands that may return large result sets (e.g., resource list with thousands of resources):

  • plain/color/table (SequentialBufferMaterializer): The table handle accumulates rows as the producer adds them. Since the buffer is in-memory, very large result sets benefit from the max_rows_hint — the renderer truncates display at N rows with a (... and N more rows) indicator. The full data is still available in the snapshot for json/yaml.
  • rich (LiveMaterializer): Uses a virtual-scrolling table that renders only visible rows. Shows a count indicator (e.g., "Showing 1-50 of 2,847"). New rows animate into view as they are added.
  • json/yaml (AccumulateMaterializer): Accumulates and emits a complete array at session end. Streaming JSON lines (one JSON object per row) for very large sets is a future consideration.
Nested/Recursive Structures

Tree-like data (resource trees, decision trees, plan hierarchies) may be arbitrarily deep. Renderers respect max_depth_hint:

  • plain/color: Truncate at depth N with a ... (N more levels) indicator.
  • table: Same truncation, rendered inside a box.
  • rich: Collapsible tree nodes — deep levels start collapsed. User can expand interactively if the terminal supports it.
  • json/yaml: Full depth — no truncation (programmatic consumers need complete data).
Mixed Content

Some commands produce mixed output (e.g., plan status has panels, tables, progress bars, and status messages). Each element is created via its own handle on the session, and the materialization strategy renders them in declaration order. The ElementRenderer is responsible for visual spacing and grouping between heterogeneous elements (e.g., inserting blank lines between a panel and a table in plain format, or adding visual margins in rich format).

Producer Error Mid-Stream

When a producer encounters an error while writing to a handle (e.g., an API call fails while populating a table), the framework handles it as follows:

  1. The handle is closed with partial data — the producer catches its exception, optionally calls handle.close() (or lets the context manager close it), and then creates a StatusMessage handle with level="error" to report the failure.

  2. The materialization strategy renders whatever was accumulated — a table with 3 of an expected 10 rows is rendered with those 3 rows, followed by the error message. This is better than rendering nothing.

  3. The session's exit code is set to 1 — indicating partial failure.

  4. For json/yaml formats, the accumulated snapshot includes both the partial data and the error message in the messages array, giving programmatic consumers full visibility.

Example of producer error handling:


async def list_resources(session: OutputSession, client: ApiClient) -> None:
    table = session.table("Resources", columns=[
        ColumnDef(name="Name"), ColumnDef(name="Type"), ColumnDef(name="Status"),
    ])
<span style="color: magenta; font-weight: 600;">try</span>:
    <span style="color: magenta; font-weight: 600;">async</span> <span style="color: magenta; font-weight: 600;">for</span> resource <span style="color: magenta; font-weight: 600;">in</span> client.list_resources():
        table.add_row({
            <span style="color: #66cc66;">&quot;Name&quot;</span>: resource.name,
            <span style="color: #66cc66;">&quot;Type&quot;</span>: resource.type,
            <span style="color: #66cc66;">&quot;Status&quot;</span>: resource.status,
        })
<span style="color: magenta; font-weight: 600;">except</span> ApiError <span style="color: magenta; font-weight: 600;">as</span> e:
    table.close()  <span style="opacity: 0.7;"># Close with partial data</span>
    session.status(<span style="color: #66cc66;">f&quot;Error fetching resources: {e}&quot;</span>, level=<span style="color: #66cc66;">&quot;error&quot;</span>)
    <span style="color: magenta; font-weight: 600;">return</span>

table.close()
session.status(<span style="color: #66cc66;">f&quot;{table.element.row_count} resources listed&quot;</span>, level=<span style="color: #66cc66;">&quot;ok&quot;</span>)

Abandoned Handles

If a handle is never explicitly closed and the session ends (either normally via session.close() or via the context manager's __exit__), the session force-closes all remaining open handles:

  1. A warning is logged (not rendered to the user): "Handle {handle_id} ({element_type}) was not explicitly closed; force-closing at session end."
  2. The handle is closed with its current accumulated state.
  3. The materialization strategy processes the ElementClosed event normally.

This ensures that no data is silently lost, even if producer code forgets to close a handle due to an unhandled code path.

Back-Pressure and Throttling

The LiveMaterializer (used by rich format) limits terminal redraws to its configured frame rate (default 15 fps). When handles emit updates faster than the frame rate:

  1. Updates are coalesced — the materializer tracks a dirty set of handle IDs that have been updated since the last frame.
  2. On each frame tick, all dirty elements are redrawn in a single pass, and the dirty set is cleared.
  3. The event queue between session and strategy uses bounded capacity. If the queue fills (producer is vastly faster than rendering), the session's _emit_event method drops ElementUpdated events for handles that are already in the dirty set (since the next frame will redraw them anyway). ElementCreated and ElementClosed events are never dropped.

For SequentialBufferMaterializer and AccumulateMaterializer, there is no back-pressure concern — updates are buffered in memory and never rendered incrementally.

Cancellation Semantics

When a command is cancelled (e.g., user presses Ctrl+C):

  1. The session receives a cancellation signal and enters the "closing" state.
  2. All open handles are force-closed with their current state.
  3. A StatusMessage with level="warn" and message "Operation cancelled" is emitted.
  4. The session closes with exit_code=130 (standard SIGINT exit code).
  5. For rich format: The LiveMaterializer freezes the display, resolves any active spinners to a cancellation indicator (e.g., yellow ), and moves the cursor to the end of the output.
  6. For buffered formats: Any elements that have been rendered stay on screen. Pending buffered elements are flushed in order, followed by the cancellation message.
  7. For json/yaml: The accumulated snapshot is serialized with "status": "cancelled" and the appropriate exit code.
Interleaved Status Messages During Concurrent Production

When multiple producers are running concurrently (e.g., populating two tables), status messages may be created at any time by any producer. The materialization strategy handles these based on format:

  • rich (LiveMaterializer): Status messages are rendered immediately in a dedicated status region at the bottom of the display (below all element regions). Multiple concurrent status messages stack vertically.
  • plain/color/table (SequentialBufferMaterializer): Status messages created during concurrent production are treated as elements in declaration order, just like tables and panels. A status message created between two table creations will be rendered between those tables. Status messages created after all tables will render after all tables are flushed.
  • json/yaml (AccumulateMaterializer): All status messages are collected in the messages array of the final snapshot, ordered by timestamp.

Integration with Future TUI

The reactive OutputSession architecture is intentionally designed to serve as the data layer for a future TUI (text user interface). The session's event-driven model maps directly to TUI widget patterns:

  1. Element handles become observable data sources. A TUI MaterializationStrategy (e.g., TuiMaterializer) would subscribe to element events and route them to TUI widgets. The producer code (command logic) is completely unaware of whether it is driving a CLI, TUI, or web frontend — it writes to handles identically in all cases.

  2. Element types map to TUI widgets:

    • PanelHandle → info pane or detail card widget
    • TableHandle → sortable, filterable data grid widget (rows arrive incrementally via add_row events)
    • TreeHandle → collapsible tree view widget (nodes arrive incrementally via add_child events)
    • ProgressHandle → animated progress bar or step-list widget
    • StatusHandle → toast notification or status bar message
    • CodeHandle → syntax-highlighted code viewer widget
    • DiffHandle → side-by-side diff viewer widget
  3. Interactive features are additive. The TUI can offer features that the CLI cannot — sorting table columns, filtering rows, collapsing/expanding tree nodes, searching within code blocks — without any changes to producer code. These features are implemented in the TUI's ElementRenderer and widget layer.

  4. Concurrent updates are native. Because the session already supports multiple concurrent producers writing to different handles, the TUI naturally displays multiple simultaneously-updating widgets (e.g., two tables being populated in parallel by concurrent operations). The TuiMaterializer routes events to widgets, and each widget redraws independently using the TUI framework's event loop.

  5. The StructuredOutput snapshot provides the initial state when navigating to a completed session in the TUI (e.g., reviewing a past command's output), while live sessions use the event stream.

The separation between production (element handles), timing (materialization strategy), and presentation (element renderer) ensures that the same command logic supports CLI, TUI, and web frontends without modification — only the (MaterializationStrategy, ElementRenderer) pair changes.

Programmatic Usage Examples

This section demonstrates how command implementations use the Output Rendering Framework through the OutputSession API, and how the same producer code produces correct output across all formats.

Example 1: Simple Static Command Output

The simplest usage — a command that creates elements, populates them synchronously, and closes them. No concurrency, no streaming.

Producer code (agents project show):


async def cmd_project_show(session: OutputSession, project_name: str) -> None:
    """Implementation of 'agents project show <project>'."""
    project = await api.get_project(project_name)
    resources = await api.list_project_resources(project.name)
    validations = await api.list_project_validations(project.name)
<span style="opacity: 0.7;"># --- Build output elements ---</span>

<span style="opacity: 0.7;"># Panel: Project details</span>
<span style="color: magenta; font-weight: 600;">with</span> session.panel(<span style="color: #66cc66;">&quot;Project Details&quot;</span>) <span style="color: magenta; font-weight: 600;">as</span> panel:
    panel.set_entries({
        <span style="color: #66cc66;">&quot;Name&quot;</span>: project.name,
        <span style="color: #66cc66;">&quot;Description&quot;</span>: project.description,
        <span style="color: #66cc66;">&quot;Resources&quot;</span>: <span style="color: cyan;">str</span>(len(resources)),
        <span style="color: #66cc66;">&quot;Remote&quot;</span>: <span style="color: #66cc66;">&quot;yes&quot;</span> <span style="color: magenta; font-weight: 600;">if</span> project.remote <span style="color: magenta; font-weight: 600;">else</span> <span style="color: #66cc66;">&quot;no&quot;</span>,
        <span style="color: #66cc66;">&quot;Created&quot;</span>: project.created_at.strftime(<span style="color: #66cc66;">&quot;%Y-%m-%d %H:%M&quot;</span>),
    }, style_hints={
        <span style="color: #66cc66;">&quot;Name&quot;</span>: <span style="color: #66cc66;">&quot;identifier&quot;</span>,
        <span style="color: #66cc66;">&quot;Resources&quot;</span>: <span style="color: #66cc66;">&quot;number&quot;</span>,
        <span style="color: #66cc66;">&quot;Remote&quot;</span>: <span style="color: #66cc66;">&quot;success&quot;</span> <span style="color: magenta; font-weight: 600;">if</span> <span style="color: magenta; font-weight: 600;">not</span> project.remote <span style="color: magenta; font-weight: 600;">else</span> <span style="color: #66cc66;">&quot;info&quot;</span>,
        <span style="color: #66cc66;">&quot;Created&quot;</span>: <span style="color: #66cc66;">&quot;success&quot;</span>,
    })

<span style="opacity: 0.7;"># Table: Linked resources</span>
<span style="color: magenta; font-weight: 600;">with</span> session.table(<span style="color: #66cc66;">&quot;Linked Resources&quot;</span>, columns=[
    ColumnDef(name=<span style="color: #66cc66;">&quot;Resource&quot;</span>, type=<span style="color: #66cc66;">&quot;string&quot;</span>, style_hint=<span style="color: #66cc66;">&quot;identifier&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Type&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Sandbox&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Read-Only&quot;</span>),
]) <span style="color: magenta; font-weight: 600;">as</span> table:
    <span style="color: magenta; font-weight: 600;">for</span> r <span style="color: magenta; font-weight: 600;">in</span> resources:
        table.add_row({
            <span style="color: #66cc66;">&quot;Resource&quot;</span>: r.name,
            <span style="color: #66cc66;">&quot;Type&quot;</span>: r.type,
            <span style="color: #66cc66;">&quot;Sandbox&quot;</span>: r.sandbox_strategy,
            <span style="color: #66cc66;">&quot;Read-Only&quot;</span>: <span style="color: #66cc66;">&quot;yes&quot;</span> <span style="color: magenta; font-weight: 600;">if</span> r.read_only <span style="color: magenta; font-weight: 600;">else</span> <span style="color: #66cc66;">&quot;no&quot;</span>,
        })

<span style="opacity: 0.7;"># Table: Validations</span>
<span style="color: magenta; font-weight: 600;">with</span> session.table(<span style="color: #66cc66;">f&quot;Validations ({len(validations)})&quot;</span>, columns=[
    ColumnDef(name=<span style="color: #66cc66;">&quot;ID&quot;</span>, type=<span style="color: #66cc66;">&quot;id&quot;</span>, style_hint=<span style="color: #66cc66;">&quot;identifier&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Command&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Mode&quot;</span>),
]) <span style="color: magenta; font-weight: 600;">as</span> table:
    <span style="color: magenta; font-weight: 600;">for</span> v <span style="color: magenta; font-weight: 600;">in</span> validations:
        table.add_row({
            <span style="color: #66cc66;">&quot;ID&quot;</span>: v.id,
            <span style="color: #66cc66;">&quot;Command&quot;</span>: v.command,
            <span style="color: #66cc66;">&quot;Mode&quot;</span>: v.mode,
        })

<span style="opacity: 0.7;"># Status: Final message</span>
session.status(<span style="color: #66cc66;">&quot;Project loaded&quot;</span>, level=<span style="color: #66cc66;">&quot;ok&quot;</span>)

What this produces in plain format:


Project Details
  Name: local/api-service
  Description: Backend API
  Resources: 2
  Remote: no
  Created: 2026-02-08 12:46

Linked Resources Resource Type Sandbox Read-Only


local/api-repo git-checkout git_worktree no local/staging-db local/database transaction_rollback yes

Validations (3) Name Command Mode


local/run-tests pytest --cov=src --cov-fail-under=80 required local/lint-check ruff check . required local/check-bundle-size node scripts/check-bundle-size.js informational

[OK] Project loaded

What this produces in rich format:


╭─ Project Details ──────────────╮
│ Name: local/api-service        │
│ Description: Backend API       │
│ Resources: 2                   │
│ Remote: no                     │
│ Created: 2026-02-08 12:46      │
╰────────────────────────────────╯

╭─ Linked Resources ──────────────────────────────────────────────────────╮ │ Resource Type Sandbox Read-Only │ │ ──────────────── ────────────── ──────────────────── ───────── │ │ local/api-repo git-checkout git_worktree no │ │ local/staging-db local/database transaction_rollback yes │ ╰─────────────────────────────────────────────────────────────────────────╯

╭─ Validations (3) ──────────────────────────────────────────────────────────────╮ │ Name Command Mode │ │ ─────────────────────── ───────────────────────────────────── ─────────── │ │ local/run-tests pytest --cov=src --cov-fail-under=80 required │ │ local/lint-check ruff check . required │ │ local/check-bundle-size node scripts/check-bundle-size.js informational │ ╰────────────────────────────────────────────────────────────────────────────────╯

✓ OK Project loaded

What this produces in json format:


{
  "command": "project show",
  "status": "ok",
  "exit_code": 0,
  "data": {
    "project_details": {
      "Name": "local/api-service",
      "Description": "Backend API",
      "Resources": "2",
      "Remote": "no",
      "Created": "2026-02-08 12:46"
    },
    "linked_resources": [
      {
        "Resource": "local/api-repo",
        "Type": "git-checkout",
        "Sandbox": "git_worktree",
        "Read-Only": "no"
      },
      {
        "Resource": "local/staging-db",
        "Type": "local/database",
        "Sandbox": "transaction_rollback",
        "Read-Only": "yes"
      }
    ],
    "validations": [
      {
        "Name": "local/run-tests",
        "Command": "pytest --cov=src --cov-fail-under=80",
        "Mode": "required"
      },
      {
        "Name": "local/lint-check",
        "Command": "ruff check .",
        "Mode": "required"
      },
      {
        "Name": "local/check-bundle-size",
        "Command": "node scripts/check-bundle-size.js",
        "Mode": "informational"
      }
    ]
  },
  "timing": { "duration_ms": 42 },
  "messages": [
    { "level": "ok", "text": "Project loaded" }
  ]
}

In all three formats, the producer code is identical. The OutputSession and its materialization strategy handle the differences transparently.

Example 2: Streaming Rows into a Table

A command that streams rows into a table as results arrive from a paginated API. The table handle stays open while the producer fetches pages.

Producer code (agents resource list):


async def cmd_resource_list(session: OutputSession, project: str | None) -> None:
    """Implementation of 'agents resource list'."""
<span style="opacity: 0.7;"># Create the table handle — it will accumulate rows as we stream them</span>
table = session.table(<span style="color: #66cc66;">&quot;Resources&quot;</span>, columns=[
    ColumnDef(name=<span style="color: #66cc66;">&quot;Name&quot;</span>, type=<span style="color: #66cc66;">&quot;string&quot;</span>, style_hint=<span style="color: #66cc66;">&quot;identifier&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Type&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Project&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Sandbox&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Status&quot;</span>),
], sort_key=<span style="color: #66cc66;">&quot;Name&quot;</span>)

<span style="opacity: 0.7;"># Create a progress indicator for the fetch operation</span>
progress = session.progress(<span style="color: #66cc66;">&quot;Fetching resources...&quot;</span>, indeterminate=<span style="color: magenta; font-weight: 600;">True</span>)

<span style="opacity: 0.7;"># Stream pages from the API</span>
count = 0
<span style="color: magenta; font-weight: 600;">async</span> <span style="color: magenta; font-weight: 600;">for</span> page <span style="color: magenta; font-weight: 600;">in</span> api.list_resources_paginated(project=project):
    <span style="color: magenta; font-weight: 600;">for</span> resource <span style="color: magenta; font-weight: 600;">in</span> page.items:
        table.add_row({
            <span style="color: #66cc66;">&quot;Name&quot;</span>: resource.name,
            <span style="color: #66cc66;">&quot;Type&quot;</span>: resource.type,
            <span style="color: #66cc66;">&quot;Project&quot;</span>: resource.project,
            <span style="color: #66cc66;">&quot;Sandbox&quot;</span>: resource.sandbox_strategy,
            <span style="color: #66cc66;">&quot;Status&quot;</span>: resource.status,
        })
        count += 1

    <span style="opacity: 0.7;"># Update progress label with count so far</span>
    progress.set_label(<span style="color: #66cc66;">f&quot;Fetching resources... ({count} found)&quot;</span>)

<span style="opacity: 0.7;"># Close the progress indicator (it has served its purpose)</span>
progress.close()

<span style="opacity: 0.7;"># Set summary and close the table</span>
table.set_summary({<span style="color: #66cc66;">&quot;total&quot;</span>: count})
table.close()

<span style="opacity: 0.7;"># Final status</span>
session.status(<span style="color: #66cc66;">f&quot;{count} resources listed&quot;</span>, level=<span style="color: #66cc66;">&quot;ok&quot;</span>)

What this looks like in plain format (SequentialBufferMaterializer):

The progress indicator is rendered as a static line. The table is buffered until table.close() is called, then rendered in full. The user sees nothing until the fetch is complete — then the entire result appears at once:


Fetching resources... (47 found) [done]

Resources Name Type Project Sandbox Status


local/api-repo git-checkout local/api-service git_worktree active local/staging-db local/database local/api-service transaction_rollback active local/docs-repo git-checkout local/docs-site git_worktree active ... (44 more rows)

Total: 47

[OK] 47 resources listed

What this looks like in rich format (LiveMaterializer):

The progress spinner animates in real-time. The table updates in-place as rows arrive — each new row appears at the bottom of the table, the row count updates, and the terminal display is rewritten without scrolling. This is a static snapshot of the live display mid-fetch:


 Fetching resources... (23 found)

╭─ Resources ────────────────────────────────────────────────────────────────────────────╮ │ Name Type Project Sandbox Status │ │ ──────────────────── ────────────── ───────────────── ──────────────────── ────── │ │ local/api-repo git-checkout local/api-service git_worktree active │ │ local/staging-db local/database local/api-service transaction_rollback active │ │ local/docs-repo git-checkout local/docs-site git_worktree active │ │ ... │ │ local/test-fixtures git-checkout local/api-service git_worktree active │ │ │ │ Showing 1-23 of 23 (fetching...) │ ╰────────────────────────────────────────────────────────────────────────────────────────╯

In rich mode, the spinner animates, the table grows as rows arrive, and the row count updates — all in-place without scrolling. When the fetch completes, the spinner resolves to , and the table shows its final state.

Example 3: Concurrent Parallel Operations (Two Tables Simultaneously)

This is the key motivating example for the reactive architecture. Two tables are populated simultaneously by parallel workers, and the producer code is completely format-agnostic.

Producer code (agents plan status — showing resources and active tool calls concurrently):


async def cmd_plan_status(session: OutputSession, plan_id: str) -> None:
    """Implementation of 'agents plan status <plan_id>'.
    
    This command fetches plan metadata, then concurrently streams two data
    sources: resource statuses and active tool call logs. Both data sources
    are long-running — they produce results over several seconds as the
    backend resolves each item.
    """
    plan = await api.get_plan(plan_id)
<span style="opacity: 0.7;"># Panel: Plan metadata (created and closed synchronously)</span>
<span style="color: magenta; font-weight: 600;">with</span> session.panel(<span style="color: #66cc66;">&quot;Plan&quot;</span>) <span style="color: magenta; font-weight: 600;">as</span> panel:
    panel.set_entries({
        <span style="color: #66cc66;">&quot;Plan ID&quot;</span>: plan.id,
        <span style="color: #66cc66;">&quot;Phase&quot;</span>: plan.phase,
        <span style="color: #66cc66;">&quot;State&quot;</span>: plan.state,
        <span style="color: #66cc66;">&quot;Action&quot;</span>: plan.action,
        <span style="color: #66cc66;">&quot;Project&quot;</span>: plan.project,
        <span style="color: #66cc66;">&quot;Started&quot;</span>: plan.started_at.strftime(<span style="color: #66cc66;">&quot;%H:%M:%S&quot;</span>),
    }, style_hints={
        <span style="color: #66cc66;">&quot;Plan ID&quot;</span>: <span style="color: #66cc66;">&quot;identifier&quot;</span>,
        <span style="color: #66cc66;">&quot;Phase&quot;</span>: <span style="color: #66cc66;">&quot;info&quot;</span>,
        <span style="color: #66cc66;">&quot;State&quot;</span>: <span style="color: #66cc66;">&quot;warning&quot;</span> <span style="color: magenta; font-weight: 600;">if</span> plan.state == <span style="color: #66cc66;">&quot;processing&quot;</span> <span style="color: magenta; font-weight: 600;">else</span> <span style="color: #66cc66;">&quot;success&quot;</span>,
    })

<span style="opacity: 0.7;"># Create both table handles BEFORE starting concurrent producers.</span>
<span style="opacity: 0.7;"># Declaration order determines rendering order in sequential formats.</span>
resource_table = session.table(<span style="color: #66cc66;">&quot;Resource Status&quot;</span>, columns=[
    ColumnDef(name=<span style="color: #66cc66;">&quot;Resource&quot;</span>, style_hint=<span style="color: #66cc66;">&quot;identifier&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Type&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Status&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Latency&quot;</span>, type=<span style="color: #66cc66;">&quot;string&quot;</span>, alignment=<span style="color: #66cc66;">&quot;right&quot;</span>),
])

tool_table = session.table(<span style="color: #66cc66;">&quot;Tool Call Log&quot;</span>, columns=[
    ColumnDef(name=<span style="color: #66cc66;">&quot;#&quot;</span>, type=<span style="color: #66cc66;">&quot;number&quot;</span>, alignment=<span style="color: #66cc66;">&quot;right&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Tool&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Target&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Result&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Duration&quot;</span>, type=<span style="color: #66cc66;">&quot;string&quot;</span>, alignment=<span style="color: #66cc66;">&quot;right&quot;</span>),
])

<span style="opacity: 0.7;"># --- Run two producers concurrently ---</span>
<span style="opacity: 0.7;"># Each producer writes to its own handle. Neither producer knows</span>
<span style="opacity: 0.7;"># which format is active. The materialization strategy handles</span>
<span style="opacity: 0.7;"># the coordination.</span>

<span style="color: magenta; font-weight: 600;">async</span> <span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">stream_resources</span>():
    <span style="color: #66cc66;">&quot;&quot;&quot;Producer A: streams resource status checks.&quot;&quot;&quot;</span>
    <span style="color: magenta; font-weight: 600;">async</span> <span style="color: magenta; font-weight: 600;">for</span> status <span style="color: magenta; font-weight: 600;">in</span> api.stream_resource_statuses(plan.id):
        resource_table.add_row({
            <span style="color: #66cc66;">&quot;Resource&quot;</span>: status.resource_name,
            <span style="color: #66cc66;">&quot;Type&quot;</span>: status.resource_type,
            <span style="color: #66cc66;">&quot;Status&quot;</span>: status.status,
            <span style="color: #66cc66;">&quot;Latency&quot;</span>: <span style="color: #66cc66;">f&quot;{status.latency_ms}ms&quot;</span>,
        })
    resource_table.close()

<span style="color: magenta; font-weight: 600;">async</span> <span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">stream_tool_calls</span>():
    <span style="color: #66cc66;">&quot;&quot;&quot;Producer B: streams tool call results.&quot;&quot;&quot;</span>
    <span style="color: magenta; font-weight: 600;">async</span> <span style="color: magenta; font-weight: 600;">for</span> call <span style="color: magenta; font-weight: 600;">in</span> api.stream_tool_calls(plan.id):
        tool_table.add_row({
            <span style="color: #66cc66;">&quot;#&quot;</span>: call.sequence_number,
            <span style="color: #66cc66;">&quot;Tool&quot;</span>: call.tool_name,
            <span style="color: #66cc66;">&quot;Target&quot;</span>: call.target,
            <span style="color: #66cc66;">&quot;Result&quot;</span>: call.result_summary,
            <span style="color: #66cc66;">&quot;Duration&quot;</span>: <span style="color: #66cc66;">f&quot;{call.duration_ms}ms&quot;</span>,
        })
    tool_table.close()

<span style="opacity: 0.7;"># Launch both producers concurrently</span>
<span style="color: magenta; font-weight: 600;">await</span> asyncio.gather(stream_resources(), stream_tool_calls())

<span style="opacity: 0.7;"># Final status</span>
session.status(<span style="color: #66cc66;">f&quot;Plan {plan_id} status retrieved&quot;</span>, level=<span style="color: #66cc66;">&quot;ok&quot;</span>)

What this produces in plain format (SequentialBufferMaterializer):

Both tables are populated concurrently, but the materializer buffers each one and renders them in declaration order when their handles close. The user sees nothing until the first-declared table (Resource Status) closes, then it prints. Then when the second table (Tool Call Log) closes, it prints. Data may have arrived interleaved across both tables, but the output is perfectly sequential:


Plan
  Plan ID: 01HXM7A9
  Phase: execute
  State: processing
  Action: local/code-coverage
  Project: local/api-service
  Started: 12:58:10

Resource Status Resource Type Status Latency


local/api-repo git-checkout ready 42ms local/staging-db local/database ready 128ms

Tool Call Log

Tool Target Result Duration


1 read_file src/auth/__init__.py 200 lines 0.1s 2 read_file src/auth/session.py 340 lines 0.1s 3 write_file tests/test_auth.py created 0.2s 4 edit_file src/auth/session.py 12 lines +/- 0.3s 5 run_tests pytest tests/test_auth 3 passed 2.1s

[OK] Plan 01HXM7A9 status retrieved

What this produces in rich format (LiveMaterializer):

Both tables are visible simultaneously and update in-place as data arrives. This snapshot shows the display mid-stream — the resource table has two rows and the tool call table has three so far:


╭─ Plan ──────────────────────────────╮
│ Plan ID: 01HXM7A9                   │
│ Phase: execute                      │
│ State: processing                   │
│ Action: local/code-coverage         │
│ Project: local/api-service          │
│ Started: 12:58:10                   │
╰─────────────────────────────────────╯

╭─ Resource Status ───────────────────────────────────────────╮ │ Resource Type Status Latency │ │ ──────────────── ────────────── ─────── ─────── │ │ local/api-repo git-checkout ready 42ms │ │ local/staging-db local/database ready 128ms │ │ │ │ 2 resources (streaming...) │ ╰───────────────────────────────────────────────────────────────╯

╭─ Tool Call Log ────────────────────────────────────────────────────╮ │ # Tool Target Result Duration │ │ ── ────────── ────────────────────── ───────────── ──────── │ │ 1 read_file src/auth/__init__.py 200 lines 0.1s │ │ 2 read_file src/auth/session.py 340 lines 0.1s │ │ 3 write_file tests/test_auth.py created 0.2s │ │ │ │ 3 calls (streaming...) │ ╰──────────────────────────────────────────────────────────────────────╯

In rich mode, both tables have animated spinners in their titles indicating active streaming. As new rows arrive from either producer, the corresponding table's display is updated in-place. When a producer finishes and closes its handle, the spinner resolves to a and the "(streaming...)" indicator is removed. The other table continues updating independently.

What this produces in json format (AccumulateMaterializer):

Nothing is printed until the session closes. Then the complete accumulated state is serialized:


{
  "command": "plan status",
  "status": "ok",
  "exit_code": 0,
  "data": {
    "plan": {
      "Plan ID": "01HXM7A9",
      "Phase": "execute",
      "State": "processing",
      "Action": "local/code-coverage",
      "Project": "local/api-service",
      "Started": "12:58:10"
    },
    "resource_status": [
      { "Resource": "local/api-repo", "Type": "git-checkout", "Status": "ready", "Latency": "42ms" },
      { "Resource": "local/staging-db", "Type": "local/database", "Status": "ready", "Latency": "128ms" }
    ],
    "tool_call_log": [
      { "#": 1, "Tool": "read_file", "Target": "src/auth/__init__.py", "Result": "200 lines", "Duration": "0.1s" },
      { "#": 2, "Tool": "read_file", "Target": "src/auth/session.py", "Result": "340 lines", "Duration": "0.1s" },
      { "#": 3, "Tool": "write_file", "Target": "tests/test_auth.py", "Result": "created", "Duration": "0.2s" },
      { "#": 4, "Tool": "edit_file", "Target": "src/auth/session.py", "Result": "12 lines +/-", "Duration": "0.3s" },
      { "#": 5, "Tool": "run_tests", "Target": "pytest tests/test_auth", "Result": "3 passed", "Duration": "2.1s" }
    ]
  },
  "timing": { "duration_ms": 3200 },
  "messages": [
    { "level": "ok", "text": "Plan 01HXM7A9 status retrieved" }
  ]
}

The critical point: the producer code in all three formats is exactly the same. The asyncio.gather call runs both producers concurrently regardless of format. The materialization strategy — LiveMaterializer, SequentialBufferMaterializer, or AccumulateMaterializer — transparently decides how that concurrent data reaches the user.

Example 4: Progress with Concurrent Sub-Operations

A command that executes a multi-step process with a progress indicator, where some steps involve parallel sub-operations.

Producer code (agents plan execute):


async def cmd_plan_execute(session: OutputSession, plan_id: str) -> None:
    """Implementation of 'agents plan execute <plan_id>'."""
    plan = await api.get_plan(plan_id)
<span style="opacity: 0.7;"># Panel: Execution metadata</span>
<span style="color: magenta; font-weight: 600;">with</span> session.panel(<span style="color: #66cc66;">&quot;Execution&quot;</span>) <span style="color: magenta; font-weight: 600;">as</span> panel:
    panel.set_entries({
        <span style="color: #66cc66;">&quot;Plan&quot;</span>: plan.id,
        <span style="color: #66cc66;">&quot;Phase&quot;</span>: <span style="color: #66cc66;">&quot;execute&quot;</span>,
        <span style="color: #66cc66;">&quot;Sandbox&quot;</span>: plan.sandbox_strategy,
        <span style="color: #66cc66;">&quot;Worker&quot;</span>: plan.worker,
        <span style="color: #66cc66;">&quot;Started&quot;</span>: <span style="color: cyan;">datetime</span>.now().strftime(<span style="color: #66cc66;">&quot;%H:%M:%S&quot;</span>),
    })

<span style="opacity: 0.7;"># Progress indicator with named steps</span>
progress = session.progress(<span style="color: #66cc66;">&quot;Executing plan&quot;</span>, total=4, steps=[
    <span style="color: #66cc66;">&quot;Collect context&quot;</span>,
    <span style="color: #66cc66;">&quot;Run tools&quot;</span>,
    <span style="color: #66cc66;">&quot;Build changeset&quot;</span>,
    <span style="color: #66cc66;">&quot;Validate&quot;</span>,
])

<span style="opacity: 0.7;"># Step 1: Collect context</span>
progress.set_step_status(<span style="color: #66cc66;">&quot;Collect context&quot;</span>, <span style="color: #66cc66;">&quot;active&quot;</span>)
context = <span style="color: magenta; font-weight: 600;">await</span> api.collect_context(plan.id)
progress.set_step_status(<span style="color: #66cc66;">&quot;Collect context&quot;</span>, <span style="color: #66cc66;">&quot;done&quot;</span>)
progress.set_progress(1, 4)

<span style="opacity: 0.7;"># Step 2: Run tools (parallel sub-operations)</span>
progress.set_step_status(<span style="color: #66cc66;">&quot;Run tools&quot;</span>, <span style="color: #66cc66;">&quot;active&quot;</span>)
tool_results = <span style="color: magenta; font-weight: 600;">await</span> api.run_tools(plan.id, context)
progress.set_step_status(<span style="color: #66cc66;">&quot;Run tools&quot;</span>, <span style="color: #66cc66;">&quot;done&quot;</span>)
progress.set_progress(2, 4)

<span style="opacity: 0.7;"># Step 3: Build changeset</span>
progress.set_step_status(<span style="color: #66cc66;">&quot;Build changeset&quot;</span>, <span style="color: #66cc66;">&quot;active&quot;</span>)
changeset = <span style="color: magenta; font-weight: 600;">await</span> api.build_changeset(plan.id, tool_results)
progress.set_step_status(<span style="color: #66cc66;">&quot;Build changeset&quot;</span>, <span style="color: #66cc66;">&quot;done&quot;</span>)
progress.set_progress(3, 4)

<span style="opacity: 0.7;"># Step 4: Validate</span>
progress.set_step_status(<span style="color: #66cc66;">&quot;Validate&quot;</span>, <span style="color: #66cc66;">&quot;active&quot;</span>)
validation = <span style="color: magenta; font-weight: 600;">await</span> api.validate_changeset(plan.id, changeset)
progress.set_step_status(<span style="color: #66cc66;">&quot;Validate&quot;</span>, <span style="color: #66cc66;">&quot;done&quot;</span>)
progress.set_progress(4, 4)

progress.close()

<span style="opacity: 0.7;"># Summary panel</span>
<span style="color: magenta; font-weight: 600;">with</span> session.panel(<span style="color: #66cc66;">&quot;Strategy Summary&quot;</span>) <span style="color: magenta; font-weight: 600;">as</span> panel:
    panel.set_entries({
        <span style="color: #66cc66;">&quot;Decisions&quot;</span>: <span style="color: cyan;">str</span>(changeset.decision_count),
        <span style="color: #66cc66;">&quot;Invariants&quot;</span>: <span style="color: cyan;">str</span>(changeset.invariant_count),
        <span style="color: #66cc66;">&quot;Planned Child Plans&quot;</span>: <span style="color: #66cc66;">f&quot;{changeset.child_plan_count}+&quot;</span>,
        <span style="color: #66cc66;">&quot;Estimated Files&quot;</span>: <span style="color: #66cc66;">f&quot;~{changeset.file_count}&quot;</span>,
        <span style="color: #66cc66;">&quot;Risk&quot;</span>: changeset.risk_level,
    })

<span style="opacity: 0.7;"># Final status</span>
<span style="color: magenta; font-weight: 600;">if</span> validation.passed:
    session.status(<span style="color: #66cc66;">&quot;Execution complete — all validations passed&quot;</span>, level=<span style="color: #66cc66;">&quot;ok&quot;</span>)
<span style="color: magenta; font-weight: 600;">else</span>:
    session.status(
        <span style="color: #66cc66;">f&quot;Execution complete — {validation.failure_count} validation(s) failed&quot;</span>,
        level=<span style="color: #66cc66;">&quot;warn&quot;</span>,
        detail=validation.summary,
    )

What this looks like in plain format:

The progress indicator renders as a static step list. Since SequentialBufferMaterializer buffers each element until its handle closes, the progress indicator is not visible during execution — it appears as a completed snapshot after the fact:


Execution
  Plan: 01HXM8C2ZK4Q7C2B3F2R4VYV6J
  Phase: execute
  Sandbox: git_worktree
  Worker: local/executor
  Started: 12:58:10

Executing plan [4/4] [x] Collect context [x] Run tools [x] Build changeset [x] Validate

Strategy Summary Decisions: 8 Invariants: 2 Planned Child Plans: 2+ Estimated Files: ~12 Risk: low

[OK] Execution complete — all validations passed

What this looks like in rich format:

The progress indicator is live — the spinner animates, the progress bar fills, and steps transition from pending to active to done in real-time. This snapshot shows the display mid-execution (step 3 active):


╭─ Execution ──────────────────────╮
│ Plan: 01HXM8C2ZK4Q7C2B3F2R4VYV6J │
│ Phase: execute                   │
│ Sandbox: git_worktree            │
│ Worker: local/executor           │
│ Started: 12:58:10                │
╰──────────────────────────────────╯

Executing plan ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 50% elapsed 0:00:13 Collect context .................. 0.8s Run tools ...................... 12.4s Build changeset ................. (running) Validate ........................ (pending)

When execution completes, the progress indicator resolves to its final state (all steps ), the Strategy Summary panel appears below it, and the final status message is displayed.

Example 5: Error Mid-Stream with Partial Output

A command where one of multiple concurrent producers fails, demonstrating graceful partial output.

Producer code (hypothetical agents resource verify):


async def cmd_resource_verify(session: OutputSession, project: str) -> None:
    """Verify all resources in a project. Some verifications may fail."""
    resources = await api.list_project_resources(project)
<span style="opacity: 0.7;"># Create a table that will be populated concurrently</span>
results_table = session.table(<span style="color: #66cc66;">&quot;Verification Results&quot;</span>, columns=[
    ColumnDef(name=<span style="color: #66cc66;">&quot;Resource&quot;</span>, style_hint=<span style="color: #66cc66;">&quot;identifier&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Type&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Check&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Status&quot;</span>),
    ColumnDef(name=<span style="color: #66cc66;">&quot;Detail&quot;</span>),
])

<span style="opacity: 0.7;"># Progress indicator</span>
progress = session.progress(
    <span style="color: #66cc66;">&quot;Verifying resources&quot;</span>,
    total=len(resources),
    steps=[r.name <span style="color: magenta; font-weight: 600;">for</span> r <span style="color: magenta; font-weight: 600;">in</span> resources],
)

<span style="opacity: 0.7;"># Verify each resource concurrently</span>
<span style="color: magenta; font-weight: 600;">async</span> <span style="color: magenta; font-weight: 600;">def</span> <span style="color: cyan; font-weight: 600;">verify_one</span>(resource):
    progress.set_step_status(resource.name, <span style="color: #66cc66;">&quot;active&quot;</span>)
    <span style="color: magenta; font-weight: 600;">try</span>:
        result = <span style="color: magenta; font-weight: 600;">await</span> api.verify_resource(resource.id)
        results_table.add_row({
            <span style="color: #66cc66;">&quot;Resource&quot;</span>: resource.name,
            <span style="color: #66cc66;">&quot;Type&quot;</span>: resource.type,
            <span style="color: #66cc66;">&quot;Check&quot;</span>: result.check_name,
            <span style="color: #66cc66;">&quot;Status&quot;</span>: <span style="color: #66cc66;">&quot;pass&quot;</span> <span style="color: magenta; font-weight: 600;">if</span> result.passed <span style="color: magenta; font-weight: 600;">else</span> <span style="color: #66cc66;">&quot;fail&quot;</span>,
            <span style="color: #66cc66;">&quot;Detail&quot;</span>: result.detail,
        })
        progress.set_step_status(
            resource.name,
            <span style="color: #66cc66;">&quot;done&quot;</span> <span style="color: magenta; font-weight: 600;">if</span> result.passed <span style="color: magenta; font-weight: 600;">else</span> <span style="color: #66cc66;">&quot;error&quot;</span>,
        )
    <span style="color: magenta; font-weight: 600;">except</span> ApiError <span style="color: magenta; font-weight: 600;">as</span> e:
        results_table.add_row({
            <span style="color: #66cc66;">&quot;Resource&quot;</span>: resource.name,
            <span style="color: #66cc66;">&quot;Type&quot;</span>: resource.type,
            <span style="color: #66cc66;">&quot;Check&quot;</span>: <span style="color: #66cc66;">&quot;connection&quot;</span>,
            <span style="color: #66cc66;">&quot;Status&quot;</span>: <span style="color: #66cc66;">&quot;error&quot;</span>,
            <span style="color: #66cc66;">&quot;Detail&quot;</span>: <span style="color: cyan;">str</span>(e),
        })
        progress.set_step_status(resource.name, <span style="color: #66cc66;">&quot;error&quot;</span>)
    progress.increment()

<span style="opacity: 0.7;"># Launch all verifications concurrently</span>
<span style="color: magenta; font-weight: 600;">await</span> asyncio.gather(
    *[verify_one(r) <span style="color: magenta; font-weight: 600;">for</span> r <span style="color: magenta; font-weight: 600;">in</span> resources],
    return_exceptions=<span style="color: magenta; font-weight: 600;">True</span>,  <span style="opacity: 0.7;"># Don&#x27;t fail fast — collect all results</span>
)

progress.close()
results_table.close()

<span style="opacity: 0.7;"># Summarize</span>
snapshot = results_table.element
pass_count = sum(1 <span style="color: magenta; font-weight: 600;">for</span> r <span style="color: magenta; font-weight: 600;">in</span> snapshot.rows <span style="color: magenta; font-weight: 600;">if</span> r[<span style="color: #66cc66;">&quot;Status&quot;</span>] == <span style="color: #66cc66;">&quot;pass&quot;</span>)
fail_count = sum(1 <span style="color: magenta; font-weight: 600;">for</span> r <span style="color: magenta; font-weight: 600;">in</span> snapshot.rows <span style="color: magenta; font-weight: 600;">if</span> r[<span style="color: #66cc66;">&quot;Status&quot;</span>] <span style="color: magenta; font-weight: 600;">in</span> (<span style="color: #66cc66;">&quot;fail&quot;</span>, <span style="color: #66cc66;">&quot;error&quot;</span>))

<span style="color: magenta; font-weight: 600;">if</span> fail_count == 0:
    session.status(<span style="color: #66cc66;">f&quot;All {pass_count} resources verified&quot;</span>, level=<span style="color: #66cc66;">&quot;ok&quot;</span>)
<span style="color: magenta; font-weight: 600;">else</span>:
    session.status(
        <span style="color: #66cc66;">f&quot;{fail_count} of {pass_count + fail_count} resources failed verification&quot;</span>,
        level=<span style="color: #66cc66;">&quot;error&quot;</span>,
    )

What this produces in plain format (after all concurrent verifications complete):


Verifying resources  [3/3]
  [x] local/api-repo
  [!] local/staging-db
  [x] local/docs-repo

Verification Results Resource Type Check Status Detail


local/api-repo git-checkout integrity pass All refs valid local/staging-db local/database connection error Connection refused (port 5432) local/docs-repo git-checkout integrity pass All refs valid

[ERROR] 1 of 3 resources failed verification

What this produces in color format:


Verifying resources  [3/3]
  [x] local/api-repo
  [!] local/staging-db
  [x] local/docs-repo

Verification Results Resource Type Check Status Detail ---------------- -------------- ---------- ------ ---------------------------------- local/api-repo git-checkout integrity pass All refs valid local/staging-db local/database connection error Connection refused (port 5432) local/docs-repo git-checkout integrity pass All refs valid

[ERROR] 1 of 3 resources failed verification

What this produces in yaml format:


command: resource verify
status: error
exit_code: 1
data:
  verification_results:
    - Resource: local/api-repo
      Type: git-checkout
      Check: integrity
      Status: pass
      Detail: All refs valid
    - Resource: local/staging-db
      Type: local/database
      Check: connection
      Status: error
      Detail: "Connection refused (port 5432)"
    - Resource: local/docs-repo
      Type: git-checkout
      Check: integrity
      Status: pass
      Detail: All refs valid
timing:
  duration_ms: 2840
messages:
  - level: error
    text: "1 of 3 resources failed verification"

In all formats, the concurrent verification produces a complete result set with partial failures clearly visible. The producer code uses return_exceptions=True on asyncio.gather to ensure all verifications complete even if some fail, and the error handling within each verify_one coroutine ensures that failures are recorded as table rows rather than causing the entire command to abort.