actor/compiler.py:_map_node() already threaded provider and model from the
actor-level config into each AGENT node's merged_meta via setdefault, but
system_prompt was never added to the same pattern.
Three targeted additions across two files:
1. _map_node() in actor/compiler.py gains an actor_system_prompt parameter
and applies merged_meta.setdefault("system_prompt", actor_system_prompt)
inside the AGENT branch, directly after the existing provider/model calls.
setdefault semantics ensure per-node config.system_prompt overrides take
precedence, exactly matching the existing provider/model behaviour.
2. compile_actor() now passes actor_system_prompt=config.system_prompt to
_map_node() so the actor-level field reaches every AGENT node.
3. Node._execute_agent() in langgraph/nodes.py injects the node's
metadata["system_prompt"] into the context dict (before state.metadata
is merged) so host agents can read context.get("system_prompt") without
needing a direct reference to the raw ActorConfigSchema.
Seven BDD scenarios added in features/compiler_system_prompt.feature:
- Actor-level system_prompt is threaded into each AGENT node metadata.
- Per-node system_prompt takes precedence via setdefault semantics.
- process_message() receives system_prompt in the context dict.
- state.metadata["system_prompt"] overrides compiled default at runtime.
- Non-AGENT nodes (TOOL) do not receive system_prompt in metadata.
- Actor without system_prompt compiles metadata as None.
- Absent system_prompt is excluded from runtime context dict.
Bug fix: changed truthiness check from `if node_system_prompt:` to
`if node_system_prompt is not None:` in _execute_agent() to faithfully
pass empty-string system prompts into the context dict. Added explicit
`str | None` type annotation for node_system_prompt.
docs/reference/actor_compiler.md updated to document system_prompt
threading alongside provider and model in the Node Binding table.
Quality gates: lint, typecheck, unit_tests (14/14), coverage_report,
security_scan, dead_code, complexity all pass.
ISSUES CLOSED: #6
5.1 KiB
Actor Compiler
The actor compiler translates hierarchical YAML-defined GRAPH actors into
LangGraph StateGraph node/edge structures that can be executed by the
host application.
Module: cleveractors.actor.compiler
Compilation Pipeline
-
Input validation — The compiler accepts an
ActorConfigSchemawithtype=GRAPHand a populatedroutefield. Non-GRAPH types are rejected withActorCompilationError. -
Reference validation — All node IDs referenced in edges, entry, and exit points are checked against the declared node set.
-
Intra-graph cycle detection — The route's
detect_cycles()method verifies the node graph is acyclic. -
Cross-actor subgraph cycle detection — When an optional
actor_resolveris provided, the compiler followsSUBGRAPHnode references recursively and detects cycles across actor boundaries (e.g. actor A → actor B → actor A). -
Node mapping — Each
NodeDefinitionis mapped to a LangGraphNodeConfigwith the appropriateNodeType(AGENT, TOOL, CONDITIONAL, SUBGRAPH). -
Edge mapping — Each
EdgeDefinitionis mapped to a LangGraphEdge. Conditional expressions are preserved inedge.condition. -
LSP binding extraction — Per-node
lsp_bindingsconfig entries are extracted intoLspBindingobjects and stored in compilation metadata. -
Metadata assembly — The compiler returns a
CompiledActorcontaining the node map, edge list, entry point, and aCompilationMetadataobject for diagnostics.
Node Binding
| Actor Node Type | LangGraph NodeType | Notes |
|---|---|---|
agent |
AGENT |
LLM invocation node; resolved at runtime via ProviderRegistryPort. Graph-level provider, model, and system_prompt act as per-node defaults (see below). |
tool |
TOOL |
Tool execution node; reference verified at load time via ToolRegistryPort |
conditional |
CONDITIONAL |
Routing node |
subgraph |
SUBGRAPH |
Nested actor reference |
AGENT node defaults
Graph-level provider, model, and system_prompt are propagated as
fallback defaults into each AGENT node's NodeConfig.metadata via
setdefault. Per-node config.provider / config.model /
config.system_prompt override them. The system_prompt key is only
set in metadata when the actor defines a top-level system_prompt
value (including empty string); actors without the field do not carry
the key.
At runtime, Node._execute_agent() injects
metadata["system_prompt"] (when present) into the context dict
passed to agent.process_message(), so host agents can read it
without a direct reference to the raw ActorConfigSchema. Per-
execution state.metadata["system_prompt"] overrides the compiled
default.
LSP bindings are declared per-node in the config.lsp_bindings list:
nodes:
- id: coder
type: agent
name: Code Writer
description: Writes Python code
config:
agent: coder_agent
lsp_bindings:
- lsp_server_name: local/pyright
languages: [python]
auto_detect: true
Error Modes
| Error | Class | When |
|---|---|---|
| Non-GRAPH type | ActorCompilationError |
config.type != GRAPH |
| Missing route | ActorCompilationError |
config.route is None |
| Missing node | MissingNodeError |
Edge references unknown node |
| Invalid entry/exit | InvalidEntryExitError |
Entry/exit node not in graph |
| Intra-graph cycle | SubgraphCycleError |
Nodes form a cycle |
| Cross-actor cycle | SubgraphCycleError |
Subgraph refs form a cycle |
API Reference
compile_actor(config, *, actor_resolver=None) -> CompiledActor
Compile an ActorConfigSchema into a LangGraph-ready bundle.
from cleveractors.actor.compiler import compile_actor
from cleveractors.actor.schema import ActorConfigSchema
cfg = ActorConfigSchema.model_validate(yaml.safe_load(yaml_text))
compiled = compile_actor(cfg)
Parameters:
config— The actor configuration (must beActorType.GRAPH).actor_resolver— Optional callable(name: str) -> ActorConfigSchema | Nonefor resolving subgraph references during cross-actor cycle detection.
Returns: CompiledActor with nodes, edges, and metadata.
CompiledActor
| Field | Type | Description |
|---|---|---|
name |
str |
Actor name |
nodes |
dict[str, NodeConfig] |
LangGraph node configs |
edges |
list[Edge] |
LangGraph edges |
entry_point |
str |
Entry node ID |
metadata |
CompilationMetadata |
Diagnostic metadata |
CompilationMetadata
| Field | Type | Description |
|---|---|---|
node_ids |
list[str] |
All node IDs (sorted) |
tool_nodes |
list[str] |
Tool-type node IDs |
lsp_bindings |
list[LspBinding] |
Per-node LSP bindings |
subgraph_refs |
dict[str, str] |
Subgraph node → actor name |
entry_node |
str |
Entry point node ID |
exit_nodes |
list[str] |
Exit point node IDs |
See Also
- Actor Abstraction Definition — what an actor is
- Actor YAML Schema — full field reference
- Actor Configuration Guide — practical authoring guide