15 KiB
adr_number, title, status_history, tier, authors, superseded_by, related_adrs, acceptance
| adr_number | title | status_history | tier | authors | superseded_by | related_adrs | acceptance | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 31 | Actor Abstraction Definition |
|
4 |
|
null |
|
|
Context
CleverAgents needs a single abstraction that covers everything from a simple LLM wrapper answering questions to a complex multi-stage pipeline orchestrating dozens of sub-agents and tool calls. The system must support hierarchical composition (graphs containing graphs), role specialization (strategy, execution, estimation, invariant), and uniform treatment of all conversational participants. Without a formal definition of what an "actor" is, the boundary between actors, agents, graphs, and workflows becomes ambiguous — leading to inconsistent configuration, unclear composition rules, and conceptual fragmentation in both documentation and implementation. This ADR formalizes the actor abstraction as an architectural commitment.
Decision Drivers
- A single abstraction must cover everything from simple LLM wrappers to complex multi-stage pipelines without requiring separate concepts for "agent," "workflow," and "orchestrator"
- Actors must support hierarchical composition (graphs containing graphs) with no depth limit for orchestrator-of-orchestrators patterns
- The boundary between actors (conversational units) and agents (LLM-backed reasoning entities) must be formally defined to preserve precision
- All conversational participants must maintain a uniform text-in/text-out contract regardless of internal complexity
- Capability acquisition must be declarative and go exclusively through skills (for tools) and LSP bindings (for language intelligence)
Decision
CleverAgents defines an actor as a YAML-configured conversational unit that generalizes "agent" into "anything conversational." An actor can represent a single LLM node, a composed graph of actors and tool nodes, or any text-in/text-out system. Every custom actor is implemented as a graph (a LangGraph StateGraph), and graphs can compose and contain other actors — enabling hierarchical orchestration without a separate composition model.
Design
Canonical Definition
An actor is:
-
Anything conversational — the abstraction that generalizes "agent" into the broadest useful category. If it accepts text input and produces text output, it can be an actor.
-
A YAML-configured graph — every custom actor is technically a LangGraph StateGraph defined via YAML configuration. Even a simple actor wrapping a single LLM is a graph with one node.
-
Composable and hierarchical — an actor's graph can include nodes that reference other actors by name, and those actors are themselves graphs that may reference further actors. This enables "orchestrator of orchestrators" patterns with no depth limit.
-
The unit of conversation in CleverAgents — sessions are tied to actors, context is scoped to actors, and all plan phase transitions are driven by actors.
What an Actor Can Represent
| Scale | Example | Graph Shape |
|---|---|---|
| Single LLM node | openai/gpt-4 answering questions |
One-node graph: LLM agent with skills |
| Simple agent | local/code-reviewer with tools |
One-node graph: LLM + tool-calling loop |
| Linear pipeline | Analyze -> Implement -> Test | Multi-node graph: sequential actor and tool nodes |
| Branching workflow | Triage -> (Security / Performance / Style) -> Merge | Multi-node graph: conditional routing to sub-actors |
| Orchestrator of orchestrators | Strategy actor delegates to execution actors, each a graph | Nested graphs: top-level actor's nodes are themselves actors with their own graphs |
| External system wrapper | Third-party API with conversational interface | One-node graph: adapter translating text-in/text-out to API calls |
The Actor-as-Graph Principle
Every custom actor IS a graph. This is not a metaphor — it is the implementation model:
- A "simple" actor wrapping a single LLM is a graph with one agent node, zero or more tool nodes, and edges connecting them.
- A "composite" actor orchestrating multiple sub-agents is a graph whose nodes include references to other actors (which are themselves graphs).
- There is no separate "workflow" or "pipeline" abstraction — actors and graphs are the same thing.
This unification eliminates the need for separate concepts for "agent," "workflow," "pipeline," and "orchestrator." All are actors; they differ only in graph complexity.
Hierarchical Composition
Actors compose through named references in graph node definitions:
Actor: local/review-pipeline (graph)
nodes:
- orchestrator (agent: local/review-controller)
- security (agent: local/security-reviewer) <- itself a graph
- performance (agent: local/perf-reviewer) <- itself a graph
- style (agent: local/style-reviewer) <- itself a graph
- merge_results (tool: local/merge-reviews)
Each sub-actor (security, performance, style) is independently
defined, registered, and testable. The parent actor references
them by name — it does not inline their definitions.
Key composition rules:
- Named references only: Actor A references Actor B by its fully-qualified namespaced name (
local/security-reviewer), never by inlining B's definition. - Load order matters: Referenced actors must be loaded/defined before actors that depend on them.
- No depth limit: Actor A -> Actor B -> Actor C -> ... is valid and supported.
- Circular references are prohibited: Detected at registration time.
Actor vs Agent
The actor/agent distinction is intentional and precise:
| Actor | Agent | |
|---|---|---|
| Definition | Anything conversational — the general abstraction | A specialized actor with LLM reasoning, tool-calling, and memory |
| Scope | May be an agent, a composite workflow, a multi-step graph, or an external system wrapper | Always an LLM-backed conversational entity |
| Graph shape | Any graph topology | Typically a single-node graph with tool-calling loop |
| Key capability | Text-in / text-out | Text-in / text-out + tool-calling + planning + role identity |
| Relationship | Superset | Subset: every agent IS an actor, not every actor is an agent |
An agent is the most common type of actor, but the actor abstraction exists precisely to accommodate non-agent participants in graphs (tool nodes, external system adapters, sub-graph orchestrators).
Actor Roles in the Plan Lifecycle
Actors fill specialized roles within the plan lifecycle:
| Role | Purpose | Required |
|---|---|---|
| Strategy actor | Produces the initial decision tree during the Strategize phase | Yes |
| Execution actor | Carries out decisions during the Execute phase | Yes |
| Estimation actor | Provides effort/cost estimates before execution | No |
| Invariant actor | Reconciles invariant violations during execution | No |
| Orchestrator actor | Top-level actor that a session is bound to | Implicit |
Each role is filled by an actor reference (namespaced name) in the action configuration. The same actor definition can fill multiple roles across different actions.
Nodes in an Actor's Graph
An actor's graph contains two types of nodes:
-
Actor nodes (
type: agent) — references to other actors by name. These provide intelligence, reasoning, and conversational capability. -
Tool nodes (
type: tool) — deterministic, non-LLM steps that directly invoke a tool. Tool nodes can reference named registered tools or define anonymous inline tools.
Actors provide intelligence; tools provide capability. Both participate as first-class nodes in the same graph, connected by edges with optional conditional routing.
Capability Acquisition
Actors acquire capabilities through two complementary mechanisms:
Tool Capabilities (via Skills)
- An actor's YAML configuration lists skill references in its
skills:section. - At activation, the runtime resolves each skill to its flattened tool set.
- The merged tool surface is made available for both LLM tool-calling within agent nodes and as named tool nodes in the actor's graph.
There is no mechanism for an actor to gain tool capabilities outside the skill system.
Language Intelligence (via LSP)
- An actor's YAML configuration declares LSP server bindings in its
lsp:section — explicit server names, language-based resolution, or auto-discovery from project resources. - At activation, the runtime resolves each binding to registered LSP servers from the LSP Registry and starts the required language server processes.
- Language intelligence (diagnostics, type information, completions, symbol references, definition lookups) is exposed to the actor both as tools (via
LSPToolAdapter, analogous toMCPToolAdapter) and as ACMS context enrichment (auto-injected diagnostics and type annotations in code context windows).
This dual acquisition model — tools through skills, language intelligence through LSP — gives actors the same software development capabilities that IDEs give human developers, without coupling the actor system to any IDE or presentation layer. See ADR-027 for full details.
Constraints
- Every custom actor must be defined in a YAML configuration file with a namespaced name.
- Every custom actor is implemented as a LangGraph StateGraph — there is no "non-graph" actor type.
- Actors must only reference other actors and tools by fully-qualified namespaced name.
- Circular actor references are prohibited and must be detected at registration time.
- Actor capability acquisition must go through the skill system — direct tool references outside skills are not permitted for capability assignment.
- All actors must maintain the text-in/text-out conversational contract, regardless of internal complexity.
Consequences
Positive
- A single abstraction covers all scales from simple LLM wrappers to complex multi-stage pipelines, eliminating conceptual fragmentation.
- The actor-as-graph principle means there is one composition model, one configuration format, and one runtime execution model for all conversational participants.
- Hierarchical composition enables reuse — sub-actors are independently defined, tested, and composed into larger workflows.
- The actor/agent distinction preserves precision (agents have specific capabilities) while the actor generalization ensures nothing conversational is excluded.
- New types of conversational participants (e.g., external system adapters) fit naturally into the model without new abstractions.
Negative
- Even trivially simple LLM wrappers are technically graphs, adding conceptual overhead for users who just want "an LLM that answers questions."
- Named reference composition requires all sub-actors to be independently registered, increasing configuration surface for deep hierarchies.
- The text-in/text-out constraint excludes non-conversational processing (e.g., pure data transformation) from the actor model, requiring tool nodes instead.
Risks
- Users may conflate "actor" and "agent" despite the formal distinction, leading to configuration errors (e.g., treating a composite workflow as if it were a single LLM agent).
- Deeply nested actor hierarchies could become difficult to debug when errors propagate through multiple graph layers.
- The "everything is a graph" principle could discourage simpler mental models that are sufficient for basic use cases.
Alternatives Considered
Separate abstractions for agents, workflows, and orchestrators — Define distinct types (Agent, Workflow, Pipeline, Orchestrator) with different configuration formats and composition rules. Increases conceptual load, fragments the configuration model, and requires mapping between types when composing. The actor-as-graph unification is strictly more powerful and simpler. Rejected.
Agent-only model (no actor generalization) — All conversational participants are "agents." This conflates LLM-backed reasoning with graph orchestration and makes it unclear how to model non-LLM nodes or external system wrappers. The actor/agent distinction preserves precision. Rejected.
Code-defined actors (no YAML) — Define actors in Python instead of YAML. Provides maximum flexibility but sacrifices declarative configuration, CLI management (agents actor add/show/list), and the ability for non-developers to compose workflows. YAML configuration with code escape hatches (inline Python in tool nodes) is the correct balance. Rejected as the primary model.
Flat actor model (no hierarchical composition) — Actors cannot reference other actors; all graph nodes must be tool nodes or direct LLM nodes. This eliminates the "orchestrator of orchestrators" pattern, prevents sub-graph reuse, and forces users to flatten complex workflows into single monolithic graphs. Rejected.
Compliance
- Actor-as-graph tests: Verify that all custom actor configurations (including single-LLM actors) produce valid LangGraph StateGraph instances.
- Composition tests: Verify that actor graphs correctly resolve named actor references, detect circular references, and enforce load order.
- Role binding tests: Validate that actions correctly bind strategy, execution, estimation, and invariant roles to registered actors.
- Capability binding tests: Confirm that actor activation resolves skill references and produces the correct merged tool surface.
- Conversational contract tests: Verify that all actors (simple and composite) accept text input and produce text output regardless of internal graph complexity.