Agent Canvas/Canvas & Execution/ — Canvas & Execution

Core execution engine for DAG-based workflows

Graph base class and Canvas subclass; JSON DSL parsing; backward compatibility via dsl_migration.py; topological sorting O(V+E); lazy variable resolution via attribute paths; output caching; immutable history tracking for audit trails.

N
Classes
N
Files
~N k
Lines
N
Key Metric

📚 What is this & why does it exist?

The Canvas class loads a DSL JSON definition, instantiates components, schedules them topologically, and executes them with variable resolution and history tracking. Without Canvas, workflows would be raw imperative code—impossible to serialize, introspect, version, or audit. — Replace with 2-4 plain-English sentences explaining what this sub-module does and why it exists. No jargon. Target audience: someone who hasn't read the code.

💡 Real-world analogy

Like a Python interpreter: canvas.py = bytecode loader + VM runtime; DSL JSON = bytecode; components = function objects; variables = stack frames; execution history = execution trace for debugging. — Replace with a vivid, concrete metaphor mapping this sub-module's core concept to everyday life (restaurant, airport, library, highway, etc.). 3-5 sentences.

🤔 Why Is It Designed This Way?

Canvas uses lazy variable resolution (resolve at runtime, not parse-time) for flexibility with non-deterministic LLM outputs. Single-threaded execution avoids races. Iteration nodes re-run sub-DAGs with updated scopes, preserving semantics. Immutable history enables perfect audit trails for debugging and compliance. — 150+ words explaining the design choices: what pattern does this follow and why? What constraints shaped the implementation? What trade-offs were made and what was sacrificed?

Design ChoiceWhat Was ChosenWhat Was RejectedWhy
Variable resolution timingLazy (at runtime)Eager (at load-time)Lazy enables maximum flexibility for non-deterministic LLM outputs; eager would require re-compilation on every variable change
Execution modelSingle-threaded greedy traversalMulti-threaded or async parallel executionSingle-threaded avoids race conditions and data corruption; simplicity is worth the latency cost for I/O-bound workflows
Iteration scope isolationDynamic variable rebinding per iterationExplicit scope stack with formal isolationDynamic rebinding is simpler but fragile; formal scope stack would add complexity. Current impl works but needs testing.
History persistenceImmutable list appended during executionOn-demand query of execution stateImmutable history is perfect for audit trails and debugging; on-demand would lose information and add query latency

Contents

Architecture

graph TB
    JSON["JSON DSL
Input"] --> MIGRATE["dsl_migration
Schema Normalize"] MIGRATE --> GRAPH["Graph
Base Class"] GRAPH --> CANVAS["Canvas
DSL Loader"] CANVAS --> LOAD["load()
Parse & Instantiate"] LOAD --> VALIDATE["Validate
DAG Integrity"] VALIDATE --> SCHEDULE["Topological
Sort"] SCHEDULE --> EXEC["execute()
Greedy Run"] EXEC --> RESOLVE["Variable
Resolution"] RESOLVE --> CACHE["Output
Cache"] CACHE --> HISTORY["History
Tracking"]

Data Flow

flowchart LR
    DSL["DSL JSON
Input"] -->|load| Components["Components
Instantiated"] Components -->|execute| Sort["Topological
Sort"] Sort -->|greedy| Exec["Execute
Ready Nodes"] Exec -->|resolve| Vars["Variables
Resolved"] Vars -->|cache| Outputs["Outputs
Cached"] Outputs -->|history| History["History
Recorded"] style DSL fill:#e8f5e9 style Outputs fill:#fff3e0 style History fill:#e8f5e9

Key Classes

Class / FunctionFilePurpose
Graphcanvas.py (lines 43–284)Base class defining nodes, edges, and abstract execute() method; foundation for Canvas subclass
Canvascanvas.py (lines 285–350+)Extends Graph; implements DSL loading, topological scheduling, variable resolution, history tracking
load(dsl_json)canvas.pyParses JSON, normalizes via dsl_migration, instantiates components, validates DAG
execute(inputs)canvas.pyTopologically sorts nodes, greedily executes ready components, resolves variables, records history
get_variable_value(path)canvas.pyLazy variable resolution; traverses attribute path (e.g., llm_1@output.choices[0].text); returns value or None

Data Structures

TypeNameDescription
dictDSLJSON-serializable DAG: {nodes: [{id, type, config}], edges: [{from, to}]}
dict_componentsMap of component ID to Agent Canvas Component System — Component System" class="xref">ComponentBase instance; populated during load()
dict_outputsMap of component ID to output dict; populated as components execute
listhistoryImmutable sequence of execution steps: [{step_id, node_id, inputs, outputs, error, timestamp}]
dict_variablesGlobal variable namespace; updated during execution; used for variable resolution

Interactions

DirectionPartnerInterfaceDescription
→Component Systemregistry.get_component(type)Canvas looks up components by type name during load(); instantiates via factory
→Tool Systemregistry.get_tool(name)Agent component calls tools by name during execution; tool registry is separate from component registry
→dsl_migrationmigrate(old_dsl)Canvas calls migration function to normalize legacy schemas to current format before parsing
←CallerCanvas.execute(inputs) → (outputs, history)Caller passes inputs, Canvas returns final outputs and audit trail for inspection/logging

Configuration

Knob / ParameterDefaultDescription
max_iterations100Maximum times an Iteration node re-executes; prevents infinite loops
variable_max_depth10Maximum attribute path depth (e.g., a.b.c.d...); prevents stack overflow in resolution
history_enabledtrueIf false, skips history recording (saves memory but loses audit trail)
dsl_version"2.0"Current DSL schema version; dsl_migration uses this to detect legacy formats

Directory Map

Source tree for Canvas & Execution
submod_name/
├── file_a.ext                 ← Role: brief annotation
├── file_b.ext                 ← Role: brief annotation
├── file_c.ext                 ← Role: brief annotation
└── index.ext                  ← Role: entry point

📑 Annotated Code Walkthrough

The most important code in this sub-module, annotated with WHY each piece exists:

load() — Parse DSL and instantiate components

// Replace with actual code from the source
void key_function() {
    // ← WHY: Explain the purpose of this line
    // ← WHY: What constraint or requirement drives this?
}

Line-by-line:

  • dsl_migration.migrate(): Handles legacy DSL versions (e.g., v1.0 → v2.0); users don't need to manually upgrade old workflows.
  • component_registry.get(): Looks up factory function by component type name; if not found, raises error early at load-time, not execution-time.
  • self._validate_dag(): Checks for cycles using DFS; ensures all tool references exist in tool registry; validates parameter types against component schema.
  • nodes[id].config → factory(): Component factory receives config dict; factory is responsible for validating and storing parameters; separation of concerns.
  • self._edges storage: Edges are indexed by destination node; enables fast dependency lookup during execution (O(E) to build, O(1) per lookup).

⚠️ Edge Cases & Boundaries

Edge CaseWhat HappensWhere in CodeRisk Level
Empty graph (no nodes)execute() returns empty outputs, empty historyCanvas.execute()🟢 Low — not an error, just a no-op
Cyclic dependenciestopological_sort() fails; canvas.load() raises ValidationErrorCanvas.load()🔴 High — prevents any execution
Variable not found (e.g., undefined_node@output)get_variable_value() returns None; component behavior depends on parameter nullable configCanvas.execute()🔴 High — may cause downstream component failure
Tool not registeredAgent.run() calls registry.get_tool(name); KeyError raised; caught and handled via retry/fallbackComponent.run()🔴 High — workflow branch fails unless exception_goto or exception_default_value configured
Iteration with 0 itemsIteration.run() loop doesn't execute sub-DAG; returns empty results listIteration.run()🟢 Low — valid behavior; batch size can be 0
LLM timeout during chatException caught in LLM.run(); retry logic kicks in (exponential backoff); if all retries fail, exception_default_value usedLLM.run()🔴 High — affects workflow success; requires fallback strategy
Concurrent accessHow it's handledfunction_name:line🔴 High

Deep-Dive Highlights

Core mechanisms from this sub-module have been extracted into dedicated focus pages for in-depth exploration. Each page covers one algorithm, protocol, or subsystem with diagrams, code walks, and configuration details.

Deep-Dive Pages

DAG Execution Model

Topological scheduling using Kahn's algorithm (O(V+E)), lazy variable resolution, scoped iteration execution, and complete execution trace recording.

Code Examples

Example: Typical Usage

// Replace with actual code example
void example() {
    // ...
}

Execution Algorithm Details

The Canvas.execute() method follows a specific algorithm to schedule and run components. Understanding this is critical for debugging complex workflows.

Topological Scheduling Algorithm

The topological sort ensures that components with dependencies execute only after their dependencies complete. This is implemented using Kahn's algorithm (iterative BFS-based):

  1. Compute in-degrees: For each node, count how many edges point to it (incoming dependencies).
  2. Initialize queue: Add all nodes with in-degree 0 to a queue. These have no dependencies and can run immediately.
  3. Process queue: While queue is not empty:
    • Dequeue node N
    • Add N to sorted order
    • For each outgoing edge from N: decrement destination node's in-degree. If in-degree becomes 0, enqueue destination.
  4. Detect cycles: If sorted order length < graph size, a cycle exists. Raise ValidationError.
  5. Time complexity: O(V + E) where V = components, E = edges. Linear in graph size; very fast even for large workflows.

Greedy Execution Model

After topological sort, Canvas uses a greedy single-threaded execution model:

Variable Resolution Trace Example

When a component parameter contains a variable reference (e.g., "search_query": "{{llm_1@output.text}}"), Canvas resolves it at runtime:

1. Parameter value: "llm_1@output.text"
2. Split on @: component_id="llm_1", path="output.text"
3. Look up llm_1 in _outputs: {output: {text: "Hello", choices: [...]}}
4. Traverse path "output.text": _outputs["llm_1"]["output"]["text"]
5. Result: "Hello" → pass to component parameter
6. Component receives concrete value, not template

This lazy evaluation allows workflows to handle non-deterministic LLM outputs without requiring re-compilation or DAG reconstruction.

Performance Characteristics

The Canvas execution engine is optimized for real-world workflows with variable-length DAGs. Key performance considerations:

AspectCharacteristicsNotes
Topological SortO(V + E) where V = components, E = edgesSingle pass; reused across multiple runs if DAG unchanged
Variable ResolutionO(D × S) where D = path depth, S = scope stack depthLazy evaluation on first access; results cached in component outputs
Node ExecutionSequential; greedy when dependencies readyNo multi-threading; bottleneck is slowest component in critical path
History StorageO(N) memory where N = total executed nodesStored in _history list; enable only if needed for debugging/audit
Iteration ScalingSub-DAG cloning: O(T × |DAG|) where T = list item countEach iteration wraps sub-DAG in local scope; components re-instantiated
Error RecoveryPropagation is O(V) worst case; retry backoff exponentialRetry overhead: ~100ms base, then 2x per attempt. Max 5 retries standard.

Hot Paths & Optimization