feat(execution-limits): add structured ExecutionError kind/reason fields; enforce all 5 execution limits in PureLangGraph #15
Closed
opened 2026-06-03 06:00:01 +00:00 by hurui200320
·
3 comments
No Branch/Tag Specified
Labels
Clear labels
auto/blocked-by-deps
auto/ci-timeout
auto/claimed-implementer
auto/claimed-merge
auto/claimed-reviewer
auto/driver-down
auto/invariant-violation
auto/last-attempt-tier-0
auto/last-attempt-tier-1
auto/last-attempt-tier-2
auto/last-attempt-tier-min
Automation Tracking
auto/needs-conflict-resolution
auto/needs-implementer
auto/postmortem
auto/ready-to-merge
auto/restart-throttled
auto/revert
auto/sentinel
auto/stale-inactivity
auto/unstable
Blocked
Needs Feedback
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
PR blocked by an open issue dependency. Operator must close the dep (or remove the dependency link) before the merge driver can act. Auto-cleared by merge_drive when no open deps remain.
Most recent merge cycle hit CI timeout. Driver excludes this PR while last merge_cycle row is < 30 min old; label persists thereafter as visible history.
Currently being processed by an implementer worker.
Currently being processed by the merge driver.
Currently being processed by a reviewer worker.
Merge driver heartbeat stale; pipeline halted. Closed automatically on next clean tick.
Detected master commit violating the strict merge invariant. Tracked as an issue (not a PR label); kept here for label completeness.
In-cycle escalation: most recent attempt ran at the Tier 0 slot (`tier-0`). Slot's model defined in .opencode/models/tiers.yaml.
In-cycle escalation: most recent attempt ran at the Tier 1 slot (`tier-1`). Slot's model defined in .opencode/models/tiers.yaml.
In-cycle escalation: most recent attempt ran at the Tier 2 slot (`tier-2`). Slot's model defined in .opencode/models/tiers.yaml. Gated behind IMPLEMENTER_ESCALATION_TIER2_ENABLED.
In-cycle escalation: most recent attempt ran at the Tier -1 slot (`tier-min`). Slot's model defined in .opencode/models/tiers.yaml. Suffix is ``-min`` (not ``--1``) so the Forgejo UI reads naturally.
Tracking issues used by the AI Automation system for agents to communicate and report.
Rebase conflict needs LLM conflict-resolver.
Failing CI needs implementer attention.
Documenting a driver incident or rollback.
Reviewer has APPROVED this PR and no later REQUEST_CHANGES is outstanding. The merge driver requires this label to even consider a PR for merging. Set by the reviewer worker on APPROVE; cleared on REQUEST_CHANGES.
Train repeatedly lost master-tempo races. Driver excludes via merge_cycle until cooldown elapses; label persists as visible history.
Revert PR backing out an invariant violation. Fast-tracked through the merge driver.
Sentinel PR duplicated from upstream into a personal fork by tools/duplicate_prs_to_fork.py for pipeline testing. Lives only in the fork; the canonical pipeline never sees it.
No implementer activity for N days. Flagged for human review. Auto-cleared on next push to head branch.
Repeatedly fails on current master (>= 3 ci-fail-on-rebased-sha releases in 12 h). Excluded from driver until human triage.
A ticket in a blocked state and unable to complete until some other task is completed first.
Bounty
$100
A bounty of $100 for any open-source contributor who provides a MR that solves this issue
Bounty
$1000
A bounty of $1000 for any open-source contributor who provides a MR that solves this issue
Bounty
$10000
A bounty of $10000 for any open-source contributor who provides a MR that solves this issue
Bounty
$20
A bounty of $20 for any open-source contributor who provides a MR that solves this issue
Bounty
$2000
A bounty of $2000 for any open-source contributor who provides a MR that solves this issue
Bounty
$250
A bounty of $250 for any open-source contributor who provides a MR that solves this issue
Bounty
$50
A bounty of $50 for any open-source contributor who provides a MR that solves this issue
Bounty
$500
A bounty of $500 for any open-source contributor who provides a MR that solves this issue
Bounty
$5000
A bounty of $5000 for any open-source contributor who provides a MR that solves this issue
Bounty
$750
A bounty of $750 for any open-source contributor who provides a MR that solves this issue
MoSCoW
Could have
Could have feature in order to satisfy the epic/legendary.
MoSCoW
Must have
Must have feature in order to satisfy the epic/legendary.
MoSCoW
Should have
Should have feature in order to satisfy the epic/legendary.
There are questions in the ticket that can not be completed until the project owner provides clarity.
Points
1
1 man-hours worth of work for an expert with no learning curve.
Points
13
13 man-hours worth of work for an expert with no learning curve.
Points
2
2 man-hours worth of work for an expert with no learning curve.
Points
21
21 man-hours worth of work for an expert with no learning curve.
Points
3
3 man-hours worth of work for an expert with no learning curve.
Points
34
34 man-hours worth of work for an expert with no learning curve.
Points
5
5 man-hours worth of work for an expert with no learning curve.
Points
55
55 man-hours worth of work for an expert with no learning curve.
Points
8
8 man-hours worth of work for an expert with no learning curve.
Points
88
88 man-hours worth of work for an expert with no learning curve.
Priority
Backlog
This ticket has backlogged priority and is not to be worked on yet
Priority
CI Blocker
Critical priority issue that blocks CI/CD pipeline and prevents PR merges
Priority
Critical
The priority is critical
Priority
High
The priority is high
Priority
Low
The priority is low
Priority
Medium
The priority is medium
When an epic or legendary is in review it must be signed off by owner, tech lead, and scrum master before being marked as completed.
When an epic or legendary is in review it must be signed off by owner, tech lead, and scrum master before being marked as completed.
When an epic or legendary is in review it must be signed off by owner, tech lead, and scrum master before being marked as completed.
A ticket for learning a tool or technology that is needed to be able to do future planning and design.
State
Completed
The ticket has been fully implemented, completed, and merged with the source code. This label should only be applied once a ticket is closed.
State
Duplicate
A ticket that represents the same content as an existing ticket.
State
In Progress
A ticket that is actively being developed.
State
In Review
A ticket that has had some code completed to implement but is waiting to pass peer review and is not yet merged in.
State
Paused
This ticket's work started but wasn't finished. It's on hold (likely in a feature branch) and will be resumed later, either due to a blocker or a delay.
State
Unverified
All new tickets start in this state. A developer may set it to show the ticket is unverified. This means we haven't agreed to work on it. It will either move to a verified state or be closed as wontdo.
State
Verified
The issue has been verified by a developer as legitimate. It will be worked on and verified tickets are now considered part of the backlog.
State
Wont Do
This ticket has been decided it wont be done. This may mean the bug has been determined to not be real (cant verify) or the feature is one we have decided we dont want to adopt.
Type
Automation
Any edits or discussion about the AI automated coding system.
Type
Bug
Something that doesnt work as intended.
Type
Discussion
Anytime a ticket represents a discussion about a subject and doesnt fall into one of the other categories.
Type
Documentation
An error or improvement needed in the documentation.
Type
Epic
Any first tier epic. That is, an epic which contains only issues as children and will not have sub-epics.
Type
Feature
Some new functionality not present.
Type
Legendary
A type of Epic which will contain other Epics.
Type
Refactor
A code change that restructures existing code without changing its external behavior.
Type
Support
Someone needs help using the project.
Type
Task
A generic task that doesnt fit into the other type categories.
Type
Testing
Work exclusively focusing on fixing or expanding testing.
Milestone
No items
No Milestone
Projects
Clear projects
No project
Assignees
aditya (Aditya Chhabra)
aleenaumair (Aleena Umair)
brent.edwards (Brent Edwards)
CoreRasurae (Luis Mendes)
drew (Drew Morris)
eugen.thaci (Eugen Thaci)
freemo (Jeffrey Phillips Freeman)
HAL9000 (HAL 9000)
HAL9001 (HAL9001)
hamza.khyari (Hamza Khyari)
hurui200320 (Rui Hu)
justin.morris
khird (Kyle Hird)
org.cleveragents
Clear assignees
No Assignees
hurui200320
Notifications
Due Date
No due date set.
Blocks
Depends on
#17 feat(public-api): expose all router-facing APIs at cleveractors package level; update README
cleveragents/cleveractors-core
You do not have permission to read 1 dependency
#14 feat(ActorResult): implement ActorResult and NodeUsage types; capture per-node token counts from LangChain responses
cleveragents/cleveractors-core
Reference: cleveragents/cleveractors-core#15
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Background
PureLangGraphhas a hardcoded depth heuristic (max(2000, len(self.nodes) * 50)) and no concept of model-call budgets, tool-call budgets, request timeout, or cost limits.ExecutionErroris a bare subclass with no structured fields, making it impossible for the router to programmatically determine why an execution failed.The CleverThis platform needs to enforce per-plan resource quotas and return the correct HTTP status code (429 for budget exhaustion, 500 for platform configuration errors like a missing pricing entry).
Spec references: ADR-2029 (Execution Limits, Error Mapping)
Context (post-bot):
create_executor()(Wave 4 / #13) is now implemented.Executoralready storeslimitsandpricingpassed by the caller.PureLangGraph.execute()already returns per-node token-usage tuples (Wave 5 / #14 is now implemented). What remains is entirely within this ticket: addingkind/reasontoExecutionError, passinglimits/pricingfrom the dispatch layer intoPureLangGraph, and enforcing all five limits there.What Is Currently Missing
ExecutionErrorhas nokindorreasonfields.PureLangGraphenforces only a hardcoded depth heuristic (max(2000, len(self.nodes) * 50)); no other limits. Critically, a depth breach silently returns the current message rather than raisingExecutionErrorat all (see_execute_from_node, line ~447).Executoralready storeslimitsandpricing, but the dispatch functions inruntime_dispatch._execute_graph()never pass them intoPureLangGraph.ExecutionErroris also missing fromcleveractors.__all__.Acceptance Criteria
ExecutionErrorupdate (cleveractors/core/exceptions.py):All existing
raise ExecutionError(msg)call sites continue to work (both fields default to"").PureLangGraphlimit enforcement (usinglimitsandpricingalready stored onExecutor):max_depth: Replace heuristic withlimits["max_depth"]. Breach →ExecutionError(..., kind="depth").max_model_calls: Counter per LLM node invocation. Breach →ExecutionError(..., kind="model_calls").max_tool_calls: Counter per tool node invocation. Breach →ExecutionError(..., kind="tool_calls").timeout_ms:asyncio.wait_for(coro, timeout=limits["timeout_ms"]/1000).asyncio.TimeoutError→ExecutionError(..., kind="timeout").max_cost_usd: After each LLM node, compute cost frompricing[provider][model]× token counts. Cumulative breach →ExecutionError(..., kind="cost", reason="budget_exhausted"). Missing pricing entry →ExecutionError(..., kind="cost", reason="missing_pricing_entry")— never proceed with assumed zero cost.Subtasks
kindandreasonfields toExecutionErrorwith default""valueslimitsandpricingfromruntime_dispatch._execute_graph()intoPureLangGraph(Executoralready stores both; the missing wire is from the dispatch call site toPureLangGraph.__init__)limits["max_depth"]max_model_callscounter and enforcementmax_tool_callscounter and enforcementasyncio.wait_forfortimeout_msNodeUsagemax_cost_usdwithmissing_pricing_entrydetectionExecutionErrorfromcleveractors/__init__.pyand__all__missing_pricing_entryscenario (never zero-cost fallback)Definition of Done
ExecutionErrorwith the correctkind(andreasonwhere applicable).from cleveractors import ExecutionErrorexposes the class withkindandreasonattributes.Implementation Plan — feat/execution-limits
Background from ADR-2029
ADR-2029 ("Actor Execution Limits and Budget Enforcement via cleveractors-core") mandates that:
max_depth,max_model_calls,max_tool_calls,timeout_ms,max_cost_usd) and apricingtable tocreate_executor()at request time.PureLangGraphenforces these limits internally during graph traversal.{"openai": {"gpt-4.1-mini": {"prompt": 0.15, "completion": 0.60}}}).ExecutionError(kind='cost', reason='missing_pricing_entry')— never proceed with assumed zero cost.2**31 - 1(very large; the router's values are the binding constraint per ADR-2029).Branch
feat/execution-limits(frommasterat commit2664ebf)Commit Message
feat(execution-limits): add structured ExecutionError kind/reason fields; enforce all 5 execution limits in PureLangGraphFiles Changed
cleveractors/core/exceptions.py— Addkind: str = ""andreason: str = ""fields toExecutionError.__init__. All existingraise ExecutionError(msg)call sites are backward-compatible (both fields default to"").cleveractors/langgraph/pure_graph.py—PureLangGraphchanges:__init__gainslimits: dict[str, Any]andpricing: dict[str, Any]parameters (both default to{}).execute()wraps_execute_from_node()inasyncio.wait_for()whentimeout_msis in limits.execute()resets_model_call_count,_tool_call_count,_accumulated_coston each invocation._execute_from_node(): depth check now useslimits.get("max_depth", 2**31 - 1)and raisesExecutionError(kind="depth")instead of silently returning._execute_from_node(): AGENT nodes checkmax_model_callsbefore execution._execute_from_node(): TOOL nodes checkmax_tool_callsbefore execution._execute_from_node(): After each LLM node result is collected, compute cost (rates per million tokens), checkmax_cost_usd, and enforce missing pricing entry guard.cleveractors/runtime_dispatch.py—_execute_graph()passeslimits=executor.limits, pricing=executor.pricingtoPureLangGraph().cleveractors/__init__.py— AddExecutionErrorto imports and__all__.features/execution_limits.feature+features/steps/execution_limits_steps.py— BDD scenarios covering all 5 limit types + missing_pricing_entry.CHANGELOG.md— Document changes under[Unreleased].Design Decisions
cost = (prompt_tokens / 1_000_000 * prompt_rate) + (completion_tokens / 1_000_000 * completion_rate).pricing={}(router did not supply a pricing table), skip all cost calculations. Ifpricingis non-empty but a provider/model entry is missing, raisemissing_pricing_entry.limits.get(key, fallback)throughout — no limit is enforced when the key is absent from the dict. Default fallbacks:max_depth=2**31-1, all counters/cost unchecked.max_model_calls(AGENT nodes) andmax_tool_calls(TOOL nodes), so the limit is enforced before the N+1 invocation._execute_from_node()coroutine inasyncio.wait_for(). The setup/teardown code inexecute()is outside the timeout scope (as intended by ADR-2029).Implementation Notes — Commit
55c82baBranch
feat/execution-limits→ PR #44Key Design Decisions
1. Depth limit backward compatibility
When
limits["max_depth"]IS provided: strictly enforce withExecutionError(kind="depth")(new behavior per ADR-2029).When
limits["max_depth"]is NOT provided (e.g.,limits={}): use the legacy heuristicmax(2000, len(nodes)*50)with a silent cap (warning log only). This preserves backward compatibility for existing callers ofPureLangGraphthat don't pass limits — including several unit tests that createPureLangGraph(config)directly and call_execute_from_nodewith high depth values.Motivation: Changing the default to
2**31-1(the ADR's "very large internal ceiling") causedRecursionErrorin existing cycle-detection tests that useauto_finish_active=Trueto bypass loop detection — those tests rely on the old heuristic as a safety net. The two-path design keeps the tests green without weakening the new enforcement for actual router callers.2. Cost calculation unit
Rates are USD per million tokens, matching ADR-2029 examples (
gpt-4.1-mini: prompt=$0.15/1M, completion=$0.60/1M). Formula:cost = (prompt_tokens / 1_000_000 * prompt_rate) + (completion_tokens / 1_000_000 * completion_rate).3. Empty pricing dict
If
pricing={}is passed (orcreate_executor()is called without pricing), all cost calculations are skipped entirely. This handles legacy callers that providemax_cost_usdin limits but no pricing table. Themissing_pricing_entryerror only fires whenpricingis non-empty and a provider/model key is missing.4. ExecutionError re-raise
Added
except ExecutionError: raiseBEFORE the broadexcept Exception as ein_execute_from_node(). Without this, cost enforcement errors raised inside the post-execution dict-processing block would be silently swallowed and the node would "succeed" with an empty output.File Locations
cleveractors.core.exceptions.ExecutionError—kind/reasonfieldscleveractors.langgraph.pure_graph.PureLangGraph.__init__— newlimits/pricingparams, counter initcleveractors.langgraph.pure_graph.PureLangGraph.execute— counter reset, timeout wrappingcleveractors.langgraph.pure_graph.PureLangGraph._execute_from_node— depth, model_calls, tool_calls, cost enforcementcleveractors.runtime_dispatch._execute_graph—limits/pricingwiring toPureLangGraphcleveractors.__init__—ExecutionErrorexportQuality Gates
Self-QA Implementation Notes (Cycles 1–3)
Self-QA loop completed in 3 cycles with final verdict: Approved.
Cycle 1
Review findings (2C / 4M / 7m / 6n):
float(_max_cost)inside the broadexcept Exceptionblock silently swallowed malformedmax_cost_usdvalues, disabling cost enforcement with no error raised. Empirically confirmed: withlimits={"max_cost_usd": "not-a-number"}and 2M prompt tokens, the graph returned the input string silently.endterminal node was subject to the depth check (depth check ran before theendshort-circuit). Any graph withmax_depth ≤ N(number of real nodes) could never complete. The ADR-2029 MVP value ofMAX_DEPTH=5could not complete a 5-node graph. BDD tests masked this by usingmax_depth=10for the success scenario.asyncio.gather()did not cancel sibling branches when one raisedExecutionError— parallel branches continued spending budget after a limit breach.max(2000, len(self.nodes) * 50)instead of2**31-1per ADR-2029 spec. Docstring claimed2**31-1but code used the heuristic._limitsand_pricingstored by reference — caller mutation could silently change enforcement behavior mid-flight.max_model_calls,max_tool_calls,max_depthproduced unstructuredValueErrorthat the router could not map to a specific limit kind._safe_token_intcalls, tautological propagation test, missing multi-node cost accumulation test, missing completion tokens test, happy-path tests only asserting absence of error,timeout_ms=0causing immediate timeout with no validation.Fixes applied:
float(max_cost_usd)in a narrowtry/except (TypeError, ValueError)that raisesExecutionError(kind="cost")immediately, outside the broad handler's reach.if node_name in ("end", "END"): return messageshort-circuit to the very top of_execute_from_node(), before the depth check.asyncio.gather(*tasks)with a try/except that cancels all non-done tasks when one raises, then awaits them withreturn_exceptions=True. Also replacedasyncio.ensure_futurewithasyncio.create_task.max(2000, len(self.nodes) * 50)with2**31 - 1as the default. Updated cycle-detection tests to pass explicitmax_depth=50and handleExecutionError(kind="depth")as expected termination.self._limits = dict(limits)andself._pricing = copy.deepcopy(pricing).try/except (TypeError, ValueError)with bool guards formax_depth,max_model_calls, andmax_tool_calls, each raising structuredExecutionErrorwith the correctkind._safe_token_intcalls, strengthened propagation test, added multi-node cost accumulation scenario, added completion tokens scenario, strengthened happy-path assertions, addedtimeout_ms > 0validation.Quality gates after Cycle 1: ✅ lint, typecheck, 2331/2331 scenarios, 97.1% coverage.
Cycle 2
Review findings (0C / 1M / 8m / 5n):
reasonfield for malformedmax_cost_usd— code raisedExecutionError(kind="cost", reason="missing_pricing_entry")but per ADR-2029,missing_pricing_entrymeans the pricing table lacks an entry for the provider/model, not that the limit value itself is malformed. This would produce misleading router logs and diagnostics.ExecutionErrorwas raised, not that sibling tasks were cancelled._node_token_usagetest did not verify the warning was logged — only checked no error was raised.timeout_msandmax_cost_usdmissing bool guards (inconsistency with other limits).pure_graph_coverage_steps.pylackedvisit_count[0] >= 2assertion.max_depthinput validation (non-numeric and bool).Fixes applied:
reason="missing_pricing_entry"toreason=""for malformedmax_cost_usd. Added comment explaining the semantic distinction per ADR-2029.branch_b_cancelled = [False]flag set insideexcept asyncio.CancelledErrorin_slow_execute. Fixed graph topology — original test usedstart → branch_a/branch_bbutstartnode only routes tonext_nodes[0], so parallel execution was never triggered. Fixed by adding asplitternode that fans out to both branches. AddedThenstep assertingbranch_b_cancelled[0] is True.Thenstep that patchesgraph.loggerand re-runs the graph to assertlogger.warningwas called with a message about non-dict token usage.timeout_msandmax_cost_usdbeforefloat()conversion. Added BDD scenarios for both.visit_count[0] >= 2assertions topure_graph_coverage_steps.pyauto-finish and ping-pong bypass tests.except BaseExceptioncomment, removed in-functionimport sys, convertedprint(..., file=sys.stderr)toself.logger.warning(...), moved in-function imports to top-level in step file.Quality gates after Cycle 2: ✅ lint, typecheck, 2335/2335 scenarios, 97.2% coverage.
Cycle 3
Review findings: No critical or major issues. Minor code quality suggestions (DRY violation in limit validation pattern, magic numbers without named constants,
create_pure_langgraph()factory silently omitting limits/pricing, weak warning assertion, deadmock_loggervariable, mixedOptional[X]vsX | Nonestyle). All nits.Verdict: Approve — The implementation is functionally correct and complete. All 5 execution limits are properly enforced per ADR-2029, the
ExecutionErrorstructured fields are correctly implemented, the wiring from_execute_graph()toPureLangGraphis correct, and the 38 BDD scenarios provide solid coverage.Remaining Issues
The Cycle 3 minor/nit findings (DRY violation in limit validation, magic number constants,
create_pure_langgraph()factory documentation, warning assertion tightening) are code quality improvements with no correctness impact. They can be addressed in a follow-up PR or deferred to a future refactor cycle.Final quality gate results:
nox -e lintnox -e typechecknox -e unit_testsnox -e integration_testsnox -e coverage_report