# Execution Architecture Refactor

Status: Proposed
Date: 2026-03-14
Scope: Repository-level architecture initiative

## Summary

`anima` has reached the point where repeated patch-level fixes are no longer the
right maintenance strategy. The current system works, but important execution
facts are still inferred in multiple layers: schema validation, CLI assembly,
runtime orchestration, state persistence, transport/session handling, and MCP
control surfaces. This has produced a cluster of user-visible issues that are
symptoms of the same architectural problem rather than isolated bugs.

This document proposes a coordinated refactor built around:

1. a clear separation between intent, resolved execution contract, and live
   session state,
2. an application-service layer that owns runtime orchestration,
3. adapter boundaries for CLI, MCP, filesystem persistence, and model/runtime
   integration,
4. a phased migration that preserves external behavior while removing duplicate
   inference paths.

## Problems This Initiative Must Solve

### Immediate user-visible issues already observed

1. Misleading tool-access warning:
   `WARNING #2: tools is empty; persona will run without tool access` is derived
   from `PersonaSpec.tools`, even when CLI `--tools` later provides effective
   tool access.
2. Oversized tool output failures:
   large tool results such as `directory_tree` can exceed provider single-message
   limits during current-turn model execution.
3. Lifecycle/completion confusion:
   `spawn --lifecycle daemon` and completion behavior do not align with user
   expectations because lifecycle and completion policy are resolved separately.
4. Talk/chat disconnect confusion:
   `talk` treats missing transport/socket as the dominant failure mode even when
   the real condition is that the agent reached a normal terminal state.
5. Missing effective runtime context for the model:
   the model may ask for the project path or working directory even when the
   agent already has filesystem access to the current repository.
6. Subagent persona inheritance failure:
   agents started from inline or synthesized persona context may be fully
   runnable, but still fail `ctl_spawn_subagent` because no reusable
   `persona_ref` is available for child-agent creation.

### Broader architectural issues

1. No stable application-service layer exists between adapters and core rules.
2. Runtime state, lifecycle policy, process handling, and persistence are mixed
   into overloaded shell modules.
3. Launch/config/tool/lifecycle resolution is duplicated across entrypoints.
4. MCP control semantics are implemented in more than one surface.
5. `core/schemas/models.py` acts as a monolithic schema kernel shared by almost
   every subsystem.
6. Some core modules depend on runtime-specific representations instead of
   anima-owned domain types.
7. Session architecture is fragmented across command, transport, streaming, and
   display modules with weak domain boundaries.

## Architecture Diagnosis

The root problem is boundary collapse.

Today, multiple layers all try to answer questions like:

- What tools are really available?
- What project root or filesystem scope is in effect?
- Should this agent stay alive for follow-up interaction?
- How much tool output is safe to send to the model?
- Is a missing socket a transport failure or a normal terminal state?

Because these questions are answered separately in different places, each layer
can be locally reasonable while the system is globally inconsistent.

This repo no longer primarily suffers from missing conditionals. It suffers from
missing ownership.

## Architectural Decision

### Decision

Adopt a three-layer execution model and a four-tier repository structure:

- `ExecutionIntent`
- `ResolvedExecutionContract`
- `ExecutionSessionState`

and

- `core <- app <- adapters`

### Rationale

This separates:

- what the user/config requested,
- what the system resolved as effective runtime truth,
- what is currently happening in a live attached session.

It also separates:

- domain rules,
- orchestration/use-case logic,
- I/O and protocol adapters.

## Data Model Boundaries

### ExecutionIntent

Purpose: represent raw requested intent before precedence resolution.

Inputs include:

- `PersonaSpec`
- `JobSpec`
- CLI arguments
- config defaults and overrides

Properties:

- May be incomplete or conflicting.
- Must not be treated as runtime-effective truth.

### ResolvedExecutionContract

Purpose: immutable, versioned snapshot of effective runtime truth after all
resolution rules and runtime probes are applied.

Suggested sections:

- `identity`
  - `instance_id`
  - `contract_version`
  - resolver version
  - source hashes / provenance
- `runtime`
  - effective model
  - timeout/max-turn budget
  - work dir / project root
  - filesystem scope
  - sandbox posture
- `tool_access`
  - declared persona tools
  - requested CLI/MCP tools
  - effective tool names/presets
  - effective side-effect posture
- `persona_provenance`
  - persona source (`file`, `inline`, `synthesized`, `snapshot`)
  - reusable persona reference when available
  - materialization status for child-agent inheritance
  - subagent-spawnability policy and reason
- `lifecycle_policy`
  - effective lifecycle
  - effective completion policy
  - attachability expectations
- `output_policy`
  - provider profile
  - per-call tool-output cap
  - per-turn tool-output budget
  - truncation/pagination strategy
- `model_context`
  - runtime facts safe to inject into prompts/instructions

Properties:

- Immutable after resolution.
- Consumers should read narrow views rather than the whole object when possible.
- This is the single source of truth for user-facing runtime facts.

### ExecutionSessionState

Purpose: represent live attach/session/transport state.

Examples:

- stream active
- socket connected
- last seen terminal snapshot
- attach mode
- pending approval prompt state

Properties:

- Ephemeral, not part of the persisted execution contract.
- May be reconstructed or discarded without changing execution truth.

### AgentStateData

Purpose: persist mutable runtime state only.

Should own:

- lifecycle state (`pending/running/completed/failed/...`)
- pid/socket references
- token usage
- result/error references
- mutable timestamps and turn counters
- **persona identity fields** (`persona_name`, `persona_ref`, `spec_id`, `spec_digest`)
  - **Note:** These fields are currently immutable after agent start. When `ctl_switch_persona`
    is implemented, they will become mutable and reflect the current effective persona.

Should not own:

- effective tool semantics
- lifecycle/completion resolution logic
- model-context truth

**Note on Persona Mutability:**

> **Status:** `ctl_switch_persona` is planned but not yet implemented. Persona is currently
> fixed at agent start.

When implemented, the `ResolvedExecutionContract` would capture the initial persona,
but `AgentStateData` would track the current persona and may be updated mid-session.
The planned behavior:

1. The switch request would be queued for the next turn boundary
2. At the turn boundary, the agent would be rebuilt via `AgentFactory.create()`
3. `message_history` would be preserved; the agent object would be replaced
4. `AgentStateData` persona fields would be updated to reflect the new persona
5. Ongoing tool calls in the current turn would complete normally

This design would allow agents to adapt their capabilities mid-session while
maintaining conversation continuity.

## Repository Target Architecture

### Core

`src/anima/core/contracts/`
- errors
- ids
- small shared value types

`src/anima/core/execution/`
- execution intent types
- resolved execution contract
- lifecycle rules
- completion-policy rules
- budget policy

`src/anima/core/personas/`
- persona normalization
- persona declaration rules

`src/anima/core/tools/`
- tool classification
- effective tool resolution
- preset resolution logic
- side-effect posture rules

`src/anima/core/conversation/`
- anima-owned conversation/session event model
- pruning/compaction against internal types

### App

`src/anima/app/launch_service.py`
- run/spawn/chat launch orchestration
- contract resolution boundary

`src/anima/app/control_service.py`
- status/interact/approve/reject/resume/kill flows

`src/anima/app/session_service.py`
- attach/session semantics
- terminal-state projection
- stop-turn and follow-up behavior

`src/anima/app/context_service.py`
- model-visible runtime context generation

`src/anima/app/result_service.py`
- terminal result shaping and presentation models

### Adapters

`src/anima/adapters/cli/`
- click command entrypoints
- talk/chat terminal presenters

`src/anima/adapters/mcp/`
- per-agent MCP server
- serve MCP server
- shared response envelopes/presenters

`src/anima/adapters/fs/`
- state store
- result store
- log store
- approval store

`src/anima/adapters/runtime/`
- PydanticAI integration
- model provider profiles
- MCP toolset wrapping

### Dependency Direction

- `core` depends on no adapters or framework-specific runtime surfaces.
- `app` depends on `core` only.
- `adapters` depend on `app` and `core` views.
- CLI/MCP must not directly orchestrate runtime or persistence when a service
  exists.

## Major Issues Ranked by Severity

1. No application-service layer; adapter modules directly coordinate business
   behavior.
2. `state.py` is overloaded with persistence, lifecycle, process, and channel
   responsibilities.
3. Effective runtime resolution is duplicated across multiple shell entrypoints.
4. MCP control semantics are implemented in multiple places.
5. `core/schemas/models.py` is too broad and central.
6. Core/Shell boundaries are softened by runtime-specific types leaking into
   core modules.
7. Session/talk/chat architecture is fragmented by mechanism rather than domain.

## Execution-Contract-Specific Design Rules

### Tool access semantics

The system must distinguish:

- declared persona tools
- requested CLI/MCP tools
- effective runtime tools

Warnings, UI, and runtime decisions must use effective tools, not declared tools
alone.

### Persona provenance and subagent inheritance

The system must distinguish:

- original persona source,
- reusable persona reference,
- whether the current persona has been materialized into a spawnable snapshot,
- whether child agents may inherit it automatically.

`ctl_spawn_subagent` must not rely on a bare `persona_ref` field happening to be
present. It must consume resolved persona provenance from the contract.

For inline or synthesized personas, the contract must choose one of two explicit
behaviors:

1. auto-materialize a reusable persona snapshot for child-agent inheritance, or
2. reject subagent spawning with a structured, user-facing reason that explains
   the persona is not currently spawnable.

### Lifecycle semantics

Lifecycle and completion policy remain separate fields, but their combination
must be resolved once.

Recommended default rule:

- interactive/attachable daemon flows default to explicit completion
- ephemeral task flows may default to auto completion

The system must never silently rewrite an explicit lifecycle choice into another
lifecycle.

### Output governance

Output governance belongs to the resolved contract and must be applied before any
provider-bound serialization.

Required layers:

1. tool-source limiting/pagination/summary where available,
2. current-turn message-budget enforcement,
3. failsafe truncation before model delivery.

### Terminal attach semantics

`talk` and `chat` must prefer agent terminal state over transport failure when
both are observable.

If an agent is already completed/failed/killed, adapters should present that
terminal result cleanly rather than surfacing a missing-socket error first.

### Model-visible runtime context

When filesystem/shell tools are effectively available, the model should receive
stable runtime context such as:

- project root/work dir
- accessible filesystem scope
- effective tools summary
- how to progressively inspect large outputs

## Invariants

1. Same effective inputs must produce the same `ResolvedExecutionContract`.
2. User-visible tool-access warnings must derive from effective tool access.
3. Subagent spawning decisions must derive from resolved persona provenance, not
   raw `persona_ref` field presence.
4. No consumer may re-derive effective lifecycle/tool/output facts once contract
   adoption is complete.
5. Provider-bound tool results must pass through contract-governed output policy.
6. Model-visible runtime context must match actual runtime scope.
7. Terminal agent state must outrank transport failure in attach UX.
8. Contract snapshot and mutable state must not drift in ownership.

## Tests Required

### Resolution tests

- precedence matrix across persona/job/CLI/config/runtime probes
- determinism tests for resolved contracts
- backward compatibility for legacy agents without contract snapshots

### Consumer conformance tests

- warnings, UI panels, runtime policy, and attach flows produce consistent
  decisions from the same contract fixture

### Session/runtime tests

- daemon + explicit completion remains attachable across multiple no-tool turns
- terminal state is reported cleanly even when transport/socket disappears
- project-root/filesystem-scope hints are injected when effective filesystem
  access exists
- inline/synthesized parent agents either spawn child agents successfully via
  materialized persona provenance or fail with a structured spawnability error

### Output-policy tests

- current-turn oversized tool output cannot exceed provider-safe caps
- per-call and per-turn budget enforcement work together
- paginated/summary responses are favored over raw truncation when supported

## Refactor Program

### Wave 1: Execution Kernel and Contract Snapshot

Goals:

- define `ExecutionIntent`, `ResolvedExecutionContract`, `ExecutionSessionState`
- build a resolver with provenance metadata
- persist contract snapshots without changing all consumers yet

Include:

- tool-access truth
- lifecycle/completion truth
- output-policy truth
- model-context truth

Do not include:

- storage backend replacement
- PydanticAI replacement
- external wire-contract changes

### Wave 2: Launch and Control Unification

Goals:

- introduce `launch_service` and `control_service`
- route CLI and MCP control flows through shared services
- reduce duplication in `run_spawn`, runner bootstrapping, and MCP control paths

### Wave 3: Session Architecture

Goals:

- introduce `session_service`
- unify `talk` and `chat` attach/session semantics
- prioritize terminal-state reporting over transport failures

### Wave 4: Schema and Core Cleanup

Goals:

- split `core/schemas/models.py`
- narrow barrel imports
- move conversation/prune/compaction onto anima-owned message/session types

## What Should Be Refactored Together

These belong in one coordinated initiative:

- resolved execution contract
- launch/config/runtime resolution unification
- state/control service extraction
- MCP control-path unification
- output-governance policy integration

## What Should Stay Out of the Same Wave

To limit risk, do not combine this initiative with:

- storage-backend replacement (filesystem to DB/event store)
- terminal UI redesign
- PydanticAI runtime replacement
- broad external MCP wire-contract changes
- persona format redesign

## Risks and Trade-offs

### Benefits

- future fixes happen once in services/resolver layers instead of once per
  command/transport path,
- runtime semantics become inspectable and testable,
- user-visible behavior becomes explainable from one source of truth.

### Costs

- temporary dual-read/dual-write complexity during migration,
- additional adapter code until old entrypoints are retired,
- more up-front architecture work before visible simplification.

### Top Risks

1. source-of-truth relapse: contract, job, and mutable state drift because
   ownership is not enforced,
2. god-object relapse: resolved contract absorbs volatile session/transport
   details,
3. accidental behavioral breakage during launch/talk/chat unification.

## Open Questions

1. Which external wire contracts are frozen and must remain backward compatible:
   CLI JSON output, per-agent MCP, serve MCP, or all of them?
2. Should `ResolvedExecutionContract` be persisted for every run, or only for
   attachable/daemon flows?
3. Should provider capability/profile be part of the resolved snapshot, or read
   dynamically at send time with a static fallback in the snapshot?
4. Should `daemon -> explicit completion` ship directly or behind a temporary
   compatibility flag?
5. Is `chat` a convenience wrapper long-term, or should it become a primary
   session mode over the same app/session services as `talk`?

## Implementation Handoff

Design scope:

- repository-level execution architecture refactor
- unified runtime resolution model
- service-layer introduction and module-boundary cleanup

Suggested implementation order:

1. define contract and resolver types,
2. persist contract snapshots and add divergence telemetry,
3. add persona provenance and subagent-spawnability resolution,
4. move warning/UI/model-context consumers to the contract,
5. unify launch/control services,
6. migrate session attach semantics,
7. clean up schemas and conversation boundaries.

Watch for:

- hidden behavior encoded in CLI/MCP adapters,
- legacy fields in persisted state and compatibility helpers,
- tests that currently lock in duplicate behavior rather than desired semantics.
