Conversation
The async runner currently wraps every node downstream of any async node in asyncio.create_task, even when its inputs resolve synchronously. Measured at ~6.3us/node of pure task machinery on trivial nodes (matches the ~7.3us/node voice plumbing floor). This adds an opt-in runner (env flag CG_ASYNC_INLINE=1, off by default -> zero behaviour change) that runs a node INLINE when its dependencies are already concrete values, and only spends a task on nodes that truly depend on a still-pending async result. Short-circuiting async nodes complete via eager tasks, so their downstream collapses to inline execution. Semantics preserved: make_first/make_or priority fallback (deferred path mirrors node_to_input_async), side-effect/_profile hooks, the sync isawaitable guard, concurrency of independent async ops, and the unhandled-exception cyclic-ref cleanup. Full suite passes 69/69 with the flag on and off. Measured 4.4x per-node on the short-circuit scenario (7724 -> 1741 ns/node), projecting the ~440ms voice sync floor to ~99ms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
eager_start (asyncio.Task) is 3.12+, but nlu-runtime runs 3.11. Add a version-portable _eager_task: native eager-start on 3.12+, and on 3.11 a faithful copy of asyncio Task.__step (step the coroutine; on a yielded future register add_done_callback to resume; reschedule bare None yields via call_soon) so a short-circuiting async node still completes synchronously and its downstream collapses to inline execution. Full suite passes under both interpreters: 69/69 on 3.14 (native eager) and 68/68 on 3.11 (manual driver; trace test excluded — graphviz dep missing in the 3.11 env, unrelated). Concurrency (test_async_run_as_soon_as_possible) and make_first/make_or fallback verified on both. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the Python 3.11 eager-task fallback (we only support 3.12+, where eager asyncio tasks are native): _eager_task is now a one-liner, and the inline runner raises a clear error if selected on <3.12 instead of failing cryptically. Un-nest _run_graph_async_inline: the five inner closures are now named module-level helpers (_inline_present / _inline_resolve_async / _inline_fast_resolve / _inline_compute_deferred / _inline_run_async) taking explicit args, and dep-classification is consolidated in _inline_dep_status (value/pending/pruned/error) instead of being duplicated. The driver loop is now a short, readable sequence; no behaviour change. Verified identical: 69/69 on 3.14 and 68/68 on 3.12 (trace excluded — graphviz not in that venv), both flag on and off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The async runner currently wraps every node downstream of any async node in
asyncio.create_task, even when that node's inputs resolve synchronously. Measured at ~6.3µs/node of pure task machinery on trivial nodes — which matches the ~7.3µs/node "plumbing floor" we see on large voice agents (the floor is asyncio scheduling, not node bodies).This adds an opt-in runner behind the env flag
CG_ASYNC_INLINE=1(off by default → zero behavior change for every existing caller) that:Only the async path is affected; the sync path is untouched.
Semantics preserved
make_first/make_orpriority option-fallback on handled raises (the deferred path mirrorsnode_to_input_async)_profilehooks, and the syncisawaitableguardValidation
bon_secours_voiceagent (63,912 nodes), Python 3.12, baseline vs flag-on:ASKED_DOB_QUESTION/FLOW_STARTED/SKILL_STARTED), no crashRequires Python 3.12+
Eager asyncio tasks are native only on 3.12+. The runner raises a clear error if the flag is set on an older interpreter; the default (flag off) path is unaffected on any version.
🤖 Generated with Claude Code