Artifex/docs/graph_runtime_audit.md
2026-08-15 16:58:54 +07:00

19 KiB

Graph Runtime Audit

Date: 2026-08-15

Baseline inspected: 6a73372665715950aee8416a9d2b3ec79955f816

Scope: audit current Artifex implementation against the intended graph-runtime architecture. This document does not propose or include a refactor.

Executive Summary

GraphRuntime is currently an interface boundary, not the production execution engine.

LangGraphRuntime is a deterministic placeholder adapter. It is not executing project, milestone, feature, task, Coder, Reviewer, Judge, retry, or commit workflows.

Real production task execution is driven by runtime_loop.autonomous_task_loop.AutonomousTaskLoop, graph.scheduler.TaskScheduler, Django ORM state, direct Python service calls, and persisted Event Bus events.

The current implementation has two separate concepts that are easy to conflate:

  • Project DAG: persisted project work decomposition and task dependencies.
  • Execution Graph: the operational workflow for executing one task attempt through Coder, tools, tests, Reviewer, Judge, retry, and commit.

The Project DAG exists today in Django models and scheduler logic. The Execution Graph exists only as imperative Python control flow inside AutonomousTaskLoop; it is not represented as a graph runtime yet.

Recommendation: keep Django/Postgres as canonical state and migrate execution to graph semantics incrementally. Start by extracting an explicit TaskExecutionGraph description around the existing task loop without changing behavior, then add graph telemetry/checkpoint metadata. LangGraph should remain behind GraphRuntime and should provide value only when conditional routing, resumable execution, human gates, subgraphs, and agent-team workflows become difficult to manage in plain Python.

Direct Answers

1. Is GraphRuntime used in production task execution?

No.

graph/runtime.py defines an abstract GraphRuntime with start, pause, resume, cancel, and signal, but no production task execution path depends on it.

Observed production path:

  • runtime_loop/autonomous_task_loop.py imports TaskScheduler, Coder, Reviewer, Judge, WorktreeTools, and DeterministicTestRunner.
  • It does not import or call GraphRuntime or LangGraphRuntime.

Current role of GraphRuntime: architectural boundary and future abstraction.

2. Is LangGraphRuntime actually executing task workflows?

No.

graph/langgraph_runtime.py returns a deterministic string from start and no-ops pause, resume, cancel, and signal.

It does not import LangGraph, build a state graph, execute graph nodes, route edges, persist graph checkpoints, or call Coder/Reviewer/Judge.

Its only observed test coverage is tests/test_interfaces.py::test_langgraph_runtime_is_hidden_behind_runtime_interface, which checks that start() returns a run id prefix.

Real Execution Path

Current task execution path for a single task:

  1. Scheduler

AutonomousTaskLoop.run_once() calls TaskScheduler.claim_next_ready_task().

TaskScheduler queries Task(status=READY) ordered by priority and creation time, skips tasks with incomplete dependencies, marks the selected task RUNNING, persists it, and publishes TASK_STARTED.

  1. Investigation/context building

AutonomousTaskLoop._execute_task() creates or reuses a worktree and builds context with WorkerContextBuilder.build_for_task().

This context builder reads selected Python files and tests directly from the worktree, excludes migrations and secrets, and attaches the current diff.

Coder also has an inspect-before-edit phase inside Coder.execute() for sensitive tasks. It asks the model for read-only inspection operations, then executes list_directory, read_file, search_code, find_symbol, git_status, or git_diff through WorktreeTools.

  1. Coder

AutonomousTaskLoop calls Coder.execute(context, tools, project, agent_version).

Coder.execute() may perform inspection, then creates a CoderToolLoop and calls CoderToolLoop.run().

  1. Mutation

CoderToolLoop asks the model for mutation operations and applies them through WorktreeTools.

Supported mutation operations include:

  • write_file
  • apply_patch
  • delete_file
  • move_file
  • create_directory
  • run_command

Patch mismatch recovery is internal to CoderToolLoop: it records structured failure evidence, re-reads the live target file excerpt, and asks the model once more for a corrected patch. It does not default to full-file replacement.

  1. Tests

AutonomousTaskLoop calls DeterministicTestRunner.run().

DeterministicTestRunner shells out via WorktreeTools.run(), persists an Artifact with stdout/stderr/returncode, and creates a TestRun with PASS or FAIL.

On failure, _attach_test_failure_evidence() stores stdout/stderr excerpts into the attempt's coder_result.metadata.test_failure_evidence, so later retry context can include the actual traceback.

  1. Reviewer

AutonomousTaskLoop stages intent-to-add with git add -N, gets git diff, then calls Reviewer.review(task, reviewer_version, diff, test_status).

Reviewer is currently a deterministic Python policy. It creates a Review row with PASS, REWORK_REQUIRED, or REJECTED based on test status, empty diff, and health-route-specific heuristics.

  1. Judge

If Reviewer passes, AutonomousTaskLoop calls Judge.judge(project, task, judge_version, diff, test_status).

Judge is currently deterministic Python. It creates a Verification row with task-level acceptance evidence and PASS or FAIL.

  1. Retry

If Coder, Reviewer, or Judge fails, _retry_or_fail() updates TaskAttempt.status, increments Task.retry_count, publishes TASK_FAILED, and either loops for another attempt or marks the task FAILED and publishes TASK_RETRY_EXHAUSTED plus final TASK_FAILED.

The effective retry budget is capped at two retries, giving three total attempts.

  1. Commit

If tests, Reviewer, and Judge all pass, WorktreeTools.commit_all() stages and commits all changes. CommitRecord is persisted with coder, reviewer, judge, test run, review, verification, SHA, branch, and message.

The task is marked COMPLETE, COMMIT_CREATED and TASK_COMPLETED are published with telemetry, the worktree is validated clean, and the task worktree is cleaned up.

Transition Classification

Explicit graph nodes/edges

Currently only the Project DAG has explicit persisted edges:

  • TaskDependency(task, depends_on) models task-to-task dependencies.
  • Milestone.dependencies models milestone-level dependencies but is not currently central in execution scheduling.
  • ProjectPlanBuilder validates task dependency acyclicity before materializing tasks.
  • TaskScheduler.claim_next_ready_task() treats incomplete TaskDependency rows as blocking edges.

No explicit graph nodes/edges currently model the execution workflow inside a task attempt.

Direct Python service calls

Most real execution transitions are direct service calls:

  • AutonomousTaskLoop.run_once() -> TaskScheduler.claim_next_ready_task()
  • _execute_task() -> WorkerContextBuilder.build_for_task()
  • _execute_task() -> Coder.execute()
  • Coder.execute() -> CoderToolLoop.run()
  • CoderToolLoop -> ModelRouter.complete()
  • CoderToolLoop -> WorktreeTools mutation/read/test helpers
  • _execute_task() -> DeterministicTestRunner.run()
  • _execute_task() -> Reviewer.review()
  • _execute_task() -> Judge.judge()
  • _execute_task() -> WorktreeTools.commit_all()
  • _retry_or_fail() direct retry/fail transition

Django state transitions

Django/Postgres is the canonical durable state machine today:

  • Task.status: READY, RUNNING, FAILED, COMPLETE
  • Task.retry_count
  • TaskAttempt.status: RUNNING, REWORK_REQUIRED, FAILED, COMPLETE
  • Worktree.status
  • TestRun.status
  • Review.status
  • Verification.result
  • CommitRecord
  • ModelRequest.status
  • ProgenySignal.status

Event Bus driven transitions

The Event Bus persists facts but does not currently drive the control flow.

Events published today include:

  • PLAN_APPROVED
  • MILESTONE_CREATED
  • TASK_CREATED
  • TASK_READY
  • TASK_STARTED
  • TEST_FAILED
  • REVIEW_FAILED
  • TASK_FAILED
  • TASK_RETRY_EXHAUSTED as a string event
  • COMMIT_CREATED
  • TASK_COMPLETED
  • AGENT_CREATED
  • AGENT_PROMOTED
  • PROGENY_SIGNAL_CREATED as a string event from ProgenyService

These are audit/event facts, not asynchronous orchestration triggers in the current implementation.

Project DAG vs Execution Graph

Project DAG

The Project DAG answers: what work exists, and what depends on what?

Current implementation:

  • ProjectPlanContract represents milestones, features, tasks, and task dependency references.
  • validate_project_plan() validates task dependency references and cycle freedom.
  • ProjectPlanBuilder.apply() persists ProjectPlan, Milestone, Feature, Task, and TaskDependency rows.
  • TaskScheduler consumes TaskDependency rows to claim only unblocked READY tasks.

This is real and production-relevant today.

Execution Graph

The Execution Graph answers: how is one task/feature/milestone autonomously executed?

Current implementation:

  • This graph is implicit in AutonomousTaskLoop._execute_task().
  • Nodes are not persisted as graph nodes.
  • Edges are Python if/continue/return branches.
  • Looping is a Python while task.retry_count <= effective_max_retries.
  • Checkpointing is coarse-grained through Django rows such as TaskAttempt, TestRun, Review, Verification, and CommitRecord.

The execution graph exists conceptually but not as a first-class runtime artifact.

CoderToolLoop Placement

Recommended placement: graph node containing an internal tool loop.

Do not make CoderToolLoop the top-level execution graph yet.

Rationale:

  • CoderToolLoop is a bounded inner loop for a single agent/tool interaction.
  • Its state is local and tactical: inspection results, mutation results, patch mismatch recovery, operation telemetry.
  • It should be resumable eventually, but it is not the same graph as the whole task lifecycle.
  • Making every tool call a top-level graph node now would increase orchestration complexity before there is enough operational pressure.

Near-term target:

  • TaskExecutionGraph has a coder node.
  • The coder node invokes Coder.execute().
  • Coder.execute() owns CoderToolLoop internally.
  • CoderToolLoop emits structured telemetry and failure evidence that the outer graph records.

Later target:

  • If long-running coding sessions, human approvals, streamed model/tool execution, or partial resume become necessary, promote internal tool-loop steps to a nested subgraph.

Runtime Capability Assessment

Conditional edges

Required.

Current conditional branches include:

  • no ready task -> stop
  • dependencies incomplete -> skip task
  • coder failed -> retry/fail
  • tests failed -> still Review, then retry/fail through Review
  • reviewer failed -> retry/fail
  • judge failed -> retry/fail
  • all checks passed -> commit

These map naturally to graph conditional edges.

Loops

Required.

Current loops include:

  • task retry loop capped at three total attempts
  • CoderToolLoop patch recovery loop
  • future investigation and experiment loops

The retry cap must remain durable and enforced by Django state, not hidden only inside graph runtime memory.

Parallel branches

Useful, not urgent for current V1/V2 guarantees.

Good candidates later:

  • independent task execution across the Project DAG
  • parallel tests/static checks/security checks
  • multi-agent review lanes
  • Progeny benchmark replay across candidates

Parallelism should be introduced only after worktree isolation and resource contention controls are explicit.

Subgraphs

Required long-term.

Natural subgraphs:

  • Task execution
  • Coder tool loop
  • Reviewer/Judge verification
  • Progeny investigation
  • Progeny experiment/champion-challenger evaluation
  • Project Brain planning/replanning

Checkpoint/resume

Required, but Django/Postgres should remain canonical.

Current checkpointing already exists at coarse boundaries:

  • task claimed
  • attempt created
  • coder result saved
  • test run saved
  • review saved
  • verification saved
  • commit saved

Graph checkpointing should augment these records, not replace them.

Human approval nodes

Required for safe V2+ autonomy.

Likely approval gates:

  • project plan approval
  • task split/replan approval
  • risky migration approval
  • provider credential/resource approval
  • promotion of Progeny candidates
  • destructive infrastructure changes

Human approval should persist as Django state and be represented as paused graph nodes.

Dynamic temporary agent teams

Required later, not required for the current loop.

Smart Investigation and Progeny experiments will likely need temporary teams such as:

  • incident analyst
  • domain expert
  • prompt/policy editor
  • benchmark designer
  • reviewer challenger

The runtime should support this as graph metadata and persisted AgentRun/future team records.

Graph versioning

Required before graph-based production execution.

Every accepted commit and Progeny investigation should know which execution graph version produced it. Without this, regressions in orchestration will be hard to diagnose.

Graph telemetry

Required.

Existing telemetry should become graph-node telemetry:

  • mutation operations
  • patch success/mismatch rates
  • write-file fallback rate
  • model requests per task
  • test duration/status
  • review findings
  • judge evidence
  • retry count
  • provider errors
  • signal counts

This telemetry should feed Progeny.

Progeny champion/challenger workflow graphs

Required for the future Replay Arena / experiment execution layer, but should not be the next refactor.

The Progeny workflow graph should model:

  • investigation selection
  • candidate generation
  • benchmark construction
  • champion/challenger replay
  • judge/evaluator comparison
  • approval/promotion/rejection

It should reuse the same runtime boundary but have separate graph versions from normal task execution.

Where LangGraph Adds Material Value

LangGraph is likely valuable when Artifex needs durable, inspectable, conditional, resumable multi-agent workflows with nested loops.

Material-value areas:

  • Explicit task execution graph with conditional retry/fail/commit branches.
  • Smart Investigation graph: signals -> clusters -> hypotheses -> routing -> candidates.
  • Human approval gates with pause/resume semantics.
  • Progeny champion/challenger workflows.
  • Dynamic temporary agent-team orchestration.
  • Future parallel branches for tests/reviews/replays.
  • Graph-level observability and visual traceability.

Low-value or risky areas for LangGraph right now:

  • Replacing TaskScheduler as the canonical Project DAG scheduler.
  • Replacing Django/Postgres state transitions.
  • Turning every small Python helper call into a graph node.
  • Moving worktree mutation safety into graph runtime abstractions.
  • Hiding retry counters/checkpoints inside LangGraph-only state.

Where Django/Postgres Should Remain Canonical

Django/Postgres should remain the source of truth for:

  • Projects, plans, milestones, features, tasks, and task dependencies.
  • Task statuses and retry counts.
  • Worktrees and commit records.
  • Attempts, coder results, reviews, judge verifications, test runs, artifacts.
  • Events.
  • Agent versions and promotion state.
  • Progeny signals, investigations, and improvement candidates.
  • Human approvals and audit history.

LangGraph, if introduced, should read/write through these canonical records rather than owning hidden state.

Migration Path

Phase 0: Preserve current behavior

Do not route production task execution through LangGraphRuntime yet.

Current guarantees to preserve:

  • M2 autonomous execution
  • M3 planning
  • M4 Archaeologist
  • M5 Agent Registry / Progeny
  • M6 self-bootstrap
  • CoderToolLoop
  • worktree isolation
  • inspect-before-edit
  • safe mutation tools
  • Reviewer/Judge separation
  • deterministic tests
  • three-attempt retry cap
  • Sol/Qwen provider boundaries
  • secret isolation

Phase 1: Document the implicit execution graph

Add a declarative graph spec that mirrors current AutonomousTaskLoop behavior without executing it yet.

Suggested nodes:

  • claim_task
  • prepare_worktree
  • build_context
  • coder
  • run_tests
  • review
  • judge
  • commit
  • retry_or_fail
  • cleanup

Suggested conditional edges:

  • coder.failed -> retry_or_fail
  • tests.failed -> review
  • review.failed -> retry_or_fail
  • judge.failed -> retry_or_fail
  • judge.passed -> commit
  • retry.available -> build_context
  • retry.exhausted -> fail

Phase 2: Add graph run telemetry around the existing loop

Add persisted graph-run/node-run records or event payloads while keeping direct Python execution.

This gives observability before changing orchestration.

Phase 3: Introduce TaskExecutionRuntime behind GraphRuntime

Implement a runtime adapter that can execute the graph spec using the existing service methods.

At this stage, AutonomousTaskLoop can delegate to the runtime but Django state remains canonical.

Phase 4: Evaluate LangGraph as one adapter

Only after the explicit graph spec and telemetry exist, wire LangGraphRuntime as an implementation option.

Acceptance criteria should require deterministic parity with the current loop.

Phase 5: Add subgraphs and human approval nodes

Promote Progeny investigation and champion/challenger replay workflows into graph subgraphs.

Add explicit approval nodes for candidate promotion and risky changes.

Current Architecture Risks

  1. GraphRuntime naming implies production graph execution, but it is currently a placeholder.

Risk: future contributors may assume LangGraph is already in the hot path.

  1. Execution graph is implicit in Python.

Risk: retry and failure behavior becomes harder to evolve as Progeny wiring, investigation, and human gates are added.

  1. Event Bus is persisted but not orchestration-driving.

Risk: event names may be interpreted as workflow triggers when they are currently audit facts.

  1. CoderToolLoop has internal iteration but only coarse persistence.

Risk: power loss during model/tool-loop execution resumes only at task-attempt granularity, not tool-step granularity.

  1. Reviewer/Judge are deterministic services, not model-driven graph nodes.

Risk: current separation is good, but future multi-agent review designs need explicit graph boundaries.

Recommendation

Do not refactor execution into LangGraph immediately.

The safest next step is to keep the existing Python/Django loop as the production engine and create a first-class, documented TaskExecutionGraph specification plus telemetry records around it. This preserves all working guarantees while making the intended graph architecture concrete.

CoderToolLoop should remain an internal loop inside the Coder graph node for now. Promote it to a subgraph only when resumability or multi-step observability at each tool call becomes necessary.

LangGraph should be introduced as an adapter behind GraphRuntime only after deterministic parity tests prove it reproduces the current loop exactly.