Skip to content

feat(engine)!: run containers as resumable engine frames - #197

Merged
laipz8200 merged 39 commits into
mainfrom
laipz8200/frame-aware-execution
Jul 28, 2026
Merged

laipz8200 merged 39 commits into
mainfrom
laipz8200/frame-aware-execution

Conversation

@laipz8200

@laipz8200 laipz8200 commented Jun 28, 2026 •

Copy link
Copy Markdown
Member

Important

  1. Make sure you have read our contribution guidelines
  2. Search existing issues and pull requests to confirm this change is not a duplicate
  3. Open or identify the issue this pull request resolves or advances
  4. Use a Conventional Commits title for this pull request, and mark breaking changes with !
  5. Remember that the pull request title will become the squash merge commit message
  6. If CLA Assistant prompts you, sign CLA.md in the pull request conversation

Related Issue

Closes #198

Summary

  • Runs Loop and Iteration through the normal BaseNode.run() lifecycle while GraphEngine interprets their child graphs as execution frames.
  • Replaces the nested child-engine execution used on main with semantic container requests, typed results, and explicit start/resume tasks.
  • Places all resumable execution data in GraphRuntimeState, including graph execution, ready and deferred work, container invocations, and child frame snapshots.
  • Introduces a shared engine-facing ContainerHandler protocol while keeping Loop and Iteration scheduling and aggregation independent.

Architecture

Node lifecycle

Loop and Iteration emit their normal start events and then yield LoopFrameRequest or IterationFrameRequest. Nodes describe container semantics only; they do not receive frame ids or manage child frames.

When a handler produces another frame request or a terminal ContainerExecutionResult, a worker resumes the original node through its explicit container resume path. The node emits its own progress, success, or failure event and then completes through the standard BaseNode event conversion.

Scheduling

The ready queue stores a discriminated ReadyTask union:

  • StartTask(frame_id, node_id) starts a node invocation inside a frame.
  • ResumeTask(invocation_id, result) resumes a suspended container invocation.

Workers execute both task variants without branching on concrete node types. A container invocation does not occupy a worker while its child frames are running.

Container handlers

GraphEngine registers Loop and Iteration handlers through ContainerHandler. EventHandler uses the frame kind to select the registered handler through one common path. Each handler independently owns child frame creation, scheduling, output and usage aggregation, break or error state, and terminal result construction.

Container handlers produce typed results but do not emit the parent container node terminal events and do not control GraphEngine lifecycle.

Runtime state and pause boundary

GraphRuntimeState owns all state required to resume execution:

  • graph execution and frame-scoped node executions
  • ready and deferred tasks
  • container invocation state
  • container child frame state and frame-local runtime data
  • variable pools, outputs, usage, node states, and edge states needed to rebuild frames

The graph definition is intentionally excluded. Restoring execution requires constructing an engine with the same graph definition and a runtime state loaded through GraphRuntimeState.from_snapshot().

When a pause is requested, the dispatcher stops new scheduling, moves queued tasks to deferred storage, and waits for in-flight nodes to finish before child frames are snapshotted. Execution may resume with a newly constructed engine or by calling run() again on the same paused engine instance. Each engine run resets its transient event buffer and completion signal so events from the paused run are not replayed.

Event flow

Child node events use the normal dispatcher and event handler pipeline. They carry in_loop_id or in_iteration_id and frame index metadata. Container start pseudo-nodes remain internal, while the parent Loop or Iteration node emits the public container lifecycle events.

Output storage, parent edge traversal, and parent frame completion happen only after the resumed container node emits its normal node completion event.

Scope

This change covers the existing Loop and Iteration nodes and establishes the protocol needed by future container types. It does not add a generic Subgraph node or WorkflowToolContainer, serialize graph definitions, provide a persistence service, or retain a compatibility path for the old child-engine execution model.

Breaking Changes

  • LoopNode and IterationNode no longer create child GraphEngine instances or execute child graphs inside _run().
  • The old child-engine container path and its serialized snapshot format are removed without a compatibility layer.
  • ReadyQueueProtocol is renamed to ReadyQueue.
  • Ready queue items change to the ReadyTask union of StartTask and ResumeTask; implementations must support drain(), dumps(), and loads().
  • GraphRuntimeState snapshot data now includes graph execution, deferred scheduling, container runs, and container child frames. Earlier snapshots cannot be loaded into this model.
  • Node factories used to materialize child frames must implement with_runtime_state(graph_runtime_state).
  • NodeExecution.execution_id is required when the domain object is created. NodeExecution.mark_started() no longer accepts an execution id, and Node.ensure_execution_id() is removed.
  • ExecutionCoordinator, the ready queue factory, LLMUsageTrackingMixin, and legacy Iteration exception types from the old execution path are removed.

Downstream Adaptation

  • Execute complete graphs through GraphEngine.run() or graphon.dsl.loads(...).run() instead of invoking Loop or Iteration execution methods directly.
  • Consume emitted graph events as the execution record. Child events continue to expose their Loop or Iteration context through event fields and metadata.
  • Update custom ready queues to implement ReadyQueue, persist both StartTask and ResumeTask, and preserve deferred work across serialization.
  • Treat runtime snapshots as opaque GraphRuntimeState data. Restore them with GraphRuntimeState.from_snapshot() and supply the same graph definition when rebuilding the engine.
  • Add with_runtime_state() to custom node factories that can create nodes inside container frames.
  • Provide execution_id when constructing NodeExecution, replace mark_started(execution_id) with mark_started(), and bind the frame-scoped id to a node before invoking it.
  • Replace imports of removed engine internals with GraphEngine, ReadyQueue, runtime snapshot, graph event, and container handler interfaces as appropriate.

Checklist

  • This pull request links the issue it resolves or advances
  • This pull request title follows Conventional Commits, and any breaking change is marked with !
  • If CLA Assistant prompted me, I signed CLA.md in the pull request conversation

@laipz8200 laipz8200 changed the title refactor(engine)!: execute loop and iteration in engine frames feat(engine)!: execute loop and iteration in engine frames Jun 28, 2026
@laipz8200
laipz8200 force-pushed the laipz8200/frame-aware-execution branch from fac929a to 597a639 Compare July 15, 2026 01:53
Comment thread src/graphon/graph_engine/container_handlers.py Dismissed
Comment thread src/graphon/graph_engine/container_handlers.py Dismissed
Comment thread src/graphon/graph_engine/container_handlers.py Dismissed
Comment thread src/graphon/graph_engine/container_handlers.py Dismissed
Comment thread src/graphon/graph_engine/container_handlers.py Dismissed
Comment thread src/graphon/graph_engine/container_handlers.py Dismissed
Comment thread src/graphon/graph_engine/frames.py Dismissed
Comment thread tests/dsl/test_node_factory.py Fixed
Comment thread tests/dsl/test_node_factory.py Fixed
@laipz8200 laipz8200 changed the title feat(engine)!: execute loop and iteration in engine frames refactor(engine)!: run containers as resumable engine frames Jul 15, 2026
@laipz8200
laipz8200 marked this pull request as ready for review July 15, 2026 05:07
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. enhancement New feature or request labels Jul 15, 2026
@laipz8200

Copy link
Copy Markdown
Member Author

The next step in the plan is to directly remove control flow nodes, such as loops and iterations. We will then transform the DSL files into their corresponding container handler implementations within the Node Factory.

@laipz8200 laipz8200 changed the title refactor(engine)!: run containers as resumable engine frames feat(engine)!: run containers as resumable engine frames Jul 20, 2026
laipz8200 added 20 commits July 20, 2026 11:26
Move loop and iteration execution into the main graph engine frame model.

Add frame-aware task routing and event handling so container bodies run without creating nested engines.

Extend DSL loading for container node types and add workflow event scenarios for complete graphs.
Rename ReadyQueueProtocol to ReadyQueue and update direct imports.
Add field comments for loop and iteration run contexts.
Keep nested iteration outputs when flatten_output is disabled.
Route LoopNode and IterationNode through the normal BaseNode.run() generator path. Add explicit start/resume ready tasks, WorkerPool-owned suspended invocation storage, and semantic container await/result models so child-frame execution can complete without occupying a worker thread.
Avoid firing GraphEngineLayer.on_node_run_end when a cooperative container node yields an await request. The hook now runs only when the invocation actually completes after resume.
Protect ContainerExecution bookkeeping with a reentrant lock because worker threads start container awaits while the dispatcher completes child frames. Add a regression test that blocks complete_frame and verifies concurrent start_container_await waits.
@laipz8200
laipz8200 force-pushed the laipz8200/frame-aware-execution branch from 6d85e98 to aceb267 Compare July 20, 2026 03:27
Comment thread src/graphon/graph_engine/graph_engine.py
Comment thread src/graphon/graph_engine/worker.py Outdated
Comment thread tests/graph_engine/test_dispatch_patterns.py Outdated
Comment thread src/graphon/graph_engine/loop_container_handler.py
Comment thread src/graphon/runtime/graph_runtime_state.py Outdated
Dispatch container await requests through the event loop so workers remain available to consume child tasks. Start workers before replaying bounded ready queues and consolidate the associated lifecycle and full-graph tests.
Keep child-frame events and execution metrics for diagnostics, but merge response outputs into the parent runtime only when the Loop or Iteration frame succeeds.
Emit frame-aware runtime, graph execution, and ready queue snapshots as version 2. Restore version 1 snapshots by converting legacy node IDs into root-frame start tasks.
@laipz8200
laipz8200 enabled auto-merge (squash) July 28, 2026 04:57
Remove redundant built-in handler locks under the single-dispatcher execution model and share Iteration success and failure result construction.
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jul 28, 2026
@laipz8200
laipz8200 merged commit c3870df into main Jul 28, 2026
8 checks passed
@laipz8200
laipz8200 deleted the laipz8200/frame-aware-execution branch July 28, 2026 06:24
@laipz8200 laipz8200 mentioned this pull request Jul 29, 2026
2 of 19 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Execute Loop and Iteration as resumable engine-managed containers

2 participants