Skip to main content

LLD: Autonomous Loop — LangGraph adopt-and-wrap

FieldValue
StatusDraft — architect/agent-level (NOT founder-gated; HLD direction was the founder gate, approved via PR #1416 / ADR-0019)
Version1.0
Date2026-06-18
AuthorPrincipal Technical Architect Agent
Issue#1422 (T4)
Parent HLDHLD-autonomous-loop-langgraph.md (approved + merged)
De-risk spikespikes/T4-autonomous-loop-spike-findings.md (#1435) — verdict GO-WITH-CHANGES
Ratified byADR-0019 §1–§5
Tracking#1418 · Milestone #20 — Runtime Revamp (Adopt & Wrap)
Depends on (merged)T1/T3 #1427 (real governed tool execution — MCP client wired, deterministic governance wrap), T2 #1426 (compaction revived — assemble-time trigger + worker wiring)
Dependency note (deferred)Cross-agent plan delegation depends on ADR-0015 / A2A (#1226) — out of v1 scope; v1 is single-agent plans.

1. Scope

This LLD specifies the deep autonomous loop the HLD ruled on: an orchestrator-side (Go) goal → plan → step → observe → reflect → re-plan control loop that drives the stateless LangGraph worker one cycle per step, rehydrating it from a checkpoint each step. It bakes in the five mandatory constraints the de-risk spike returned (§3), specifies the persisted plan state model (§4), the control flow + all termination conditions (§5), how the loop drives the worker via ExecuteStep (§6), the in-step interrupt() approval path that is net-new (§7), idempotency + checkpoint-resume correctness (§8), integration with the merged governed-tool path (#1427) and live compaction (#1426) (§9), tenet preservation (§10), metrics/observability (§11), and the implementable task breakdown (§13).

In scope: the macro plan loop (Go), the additive EXECUTION_MODE_AUTONOMOUS path, plan-bearing checkpoint schema, step_goal/StepProgress contract additions, the in-step interrupt→approval→resume path, idempotency-key enforcement at the MCP proxy egress, autonomous-mode compaction default.

Out of scope (explicit): deprecating the relay path (additive coexistence — ADR-0012, founder call); multi-agent plan delegation (A2A / #1226); the worker's micro-cycle topology beyond the additions named here (#1427 owns it); RAG-as-attachable-entity backend (#1377, orthogonal).

Tenet anchors (non-negotiable, carried from HLD §4.2 / §7 / §10): stateless worker, immutable session, governed-every-hop, budget gate preserved, provider server-side tools forbidden. Any code path that violates these is rejected in review regardless of test status.


2. Component map (as-to-build)

#ComponentLocation (new unless noted)Language
C1PlanLoop — macro orchestrator loop (plan → step → observe → reflect → terminate)internal/runtime/autoloop/loop.goGo
C2Plan state model + codec (encode/decode into checkpoint blob, backward-compat)internal/runtime/autoloop/plan.goGo
C3Terminator — three-axis termination evaluator (goal / stall / budget)internal/runtime/autoloop/terminate.goGo
C4ProgressEval — stall detection (no-progress window + replan cap)internal/runtime/autoloop/progress.goGo
C5StepDriver — wraps the ExecuteStep RPC, maps step_goal in / StepProgress + interrupt outinternal/runtime/autoloop/driver.goGo
C6Checkpoint schema bump (plan-bearing blob, OQ-5 header)internal/runtime/checkpointstore/store.go (extend)Go
C7Proto additions — step_goal, StepProgress, interrupt_required event, EXECUTION_MODE_AUTONOMOUSproto/upsquad/runtime/v1/runtime.proto (extend)proto
C8In-step interrupt mapping → approval.Service + resumeinternal/runtime/server/lifecycle_pause.go (extend), C5Go
C9Worker: step_goal consumption + StepProgress emission + interrupt() raise on requires_approvalservices/agent-worker/.../graph/{nodes,edges,state}.py, executor/langgraph_executor.py (extend)Python
C10MCP proxy: idempotency-key enforcement (exactly-once on side-effecting calls)internal/mcp/middleware/ (extend)Go
C11Autonomous-mode knob defaults (compaction_threshold_events, stall window, replan cap)internal/context/.../knobs + internal/runtime/autoloop/config.goGo

Placement is normative (HLD §4): C1–C5 (plan, cursor, progress, termination, all stop axes) live only in Go. The Python worker (C9) reports signals and raises interrupts; it never decides termination, never holds plan state across the iteration boundary. A reviewer MUST reject any termination/stop logic that leaks into graph/edges.py beyond the existing budget-only should_continue (which stays as the per-cycle budget guard).


3. The five spike constraints — baked in (normative)

These are the load-bearing outputs of the de-risk spike (#1435). Each is restated here as a build constraint with its exact locus.

3.1 Hard replan cap (~5) — load-bearing

  • Why load-bearing: the spike proved a thrash model emitting novel text per step defeats the response_delta_hash novelty signal; the replan cap is the only backstop that catches novel-output thrash (the thrash worker terminated at stalled:replan_cap, not no_progress).
  • Where it lives: Plan.replans counter (C2), evaluated by ProgressEval (C4) and surfaced to Terminator (C3). The cap is a config value ReplanCap (C11), execution_mode-tunable, seed default = 4 (spike seed; "~5" budget envelope). Incremented exactly once per accepted re-plan event in PlanLoop.reflect() (C1).
  • Terminal state when hit: status = stalled, stall_reason = replan_cap. The loop emits a best-effort partial answer + structured reason and stops (no further ExecuteStep). This is distinct from no_progress and from budget_exhausted so operators can tell novel-thrash apart from genuine stall.
  • Both stall sub-axes are mandatory (spike R1): no_progress (flat window) AND replans > ReplanCap. Shipping only one is a review-blocking gap.

3.2 Non-zero compaction_threshold_events default for autonomous mode

  • Why: the platform default is manual-only / 0 (pipeline.go:704–7080 means "no threshold producer"; only the higher-latency sync-blown fallback would fire). The spike proved the async producer firing one step before budget is blown yields 0 sync fallbacks, 0 truncations; relying on the sync path under load is a latency regression.
  • Where set: internal/context/.../knobs — the EffectiveForTenant resolution gains an execution_mode-aware default so that execution_mode = autonomous resolves CompactionThresholdEvents to a non-zero seed (default = 6 events) when the tenant has not overridden it. C11 wires this; the pipeline (maybeCompact, pipeline.go:688) is unchanged — it already honours the resolved knob.
  • Also (spike R3 / HLD R4): apply compaction discipline to the plan blob — cap Plan.progress[] to the stall window, store completed-todo summaries not full transcripts (§4.3). Alert if sync-fallback rate is non-trivial (§11).

3.3 Persist tool_results into the interrupt checkpoint

  • Why: spike R5 — the NAIVE design (checkpoint only the original input, full re-run on resume) double-executed an already-run sibling tool call (). Persisting executed tool_results into the interrupt checkpoint + skip-done on resume yields exactly-once for the clean-resume case.
  • Where: the interrupt checkpoint blob (C6 + C9) carries the already-executed tool_results for the in-flight cycle. On resume, the worker skips any tool_call_id already present in tool_results and re-enters the graph only at the interrupted (gated) call. The in-step LangGraph checkpointer is scoped to the single step only; the macro plan stays in Go + agent_checkpoints (§8.2).
  • OQ-3 guard (spike constraint 4): the interrupt checkpoint MUST use a distinct LoopIteration / sub-key so it does not collide with the step's normal checkpoint and trip a false HashMatch=false P1 (the checkpointstore unique constraint is on (session_id, loop_iteration); see store.go:29). §8.3 pins the sub-key derivation.

3.4 Idempotency keys on every side-effecting tool call

  • Why: spike R5 second finding — under at-least-once resume delivery (pod churn / duplicate resume), a non-gated side-effecting tool without an idempotency key fired twice. Persist-results protects the clean resume; idempotency keys protect against duplicate resume. Both required — different failure modes.
  • Key derivation: idempotency_key = (session_id, loop_iteration, tool_call_id) — exactly the tuple the spike validated. Deterministic, collision-free across resumes of the same logical call, distinct across logical calls.
  • Where enforced: the MCP proxy egress (C10) is the un-bypassable backstop — it is the network choke point / PEP (ADR-0019 §3b). The worker tool path (C9) propagates the key on the call; the proxy de-duplicates: first call with a given key executes and the result is recorded; a duplicate within the dedup window returns the recorded result without re-invoking the upstream tool. Enforcement at the proxy (not only the worker) is mandatory because the worker pod may be the thing that churned.
  • Coverage: every side-effecting MCP call in a step, not only the gated one (the spike's exact failure was a non-gated sibling). Read-only tools may skip dedup but MUST still carry the key (cheap, and the proxy classifies by tool metadata, not by trusting the caller).

3.5 In-step interrupt() approval path is net-new

  • Finding (spike R5 first finding): today requires_approval hard-fails-closed_governance_block_tool (nodes.py:816) returns an error ToolResult; there is no interrupt() anywhere in the worker. HLD §7.4-item-2 is therefore a net-new build, not a re-wire. §7 designs it.

4. State model — Plan (orchestrator-side, persisted in the checkpoint)

4.1 Plan (C2)

The Plan is owned by the Go orchestrator and serialized into the checkpoint blob the orchestrator already writes — no new persistence store (HLD §5.1, §6). It is additive to the worker's AgentState.

// internal/runtime/autoloop/plan.go
type Plan struct {
Goal string // the user/parent objective for this run
Todos []Todo // ordered sub-goals
Cursor int // index of the active todo
Progress []StepProgress// bounded ring; capped to StallWindow (4.3)
Replans int // re-plan count — the load-bearing stall sub-axis (3.1)
Budget Budget // loops/tokens/wall
Status PlanStatus // active | complete | stalled | budget_exhausted
StallReason string // "", "no_progress", "replan_cap" — set on stalled
}

type Todo struct {
ID string
Text string
Status TodoStatus // pending | active | done | abandoned
Attempts int
Summary string // completed-todo summary (compaction discipline, 3.2/4.3)
}

type Budget struct {
LoopsUsed, LoopsMax int // LoopsMax == existing max_loops (budget axis, unchanged semantics)
TokensUsed, TokensMax int64
WallMs, WallMaxMs int64
}

4.2 Persistence + backward compatibility (C6)

  • The Plan rides the checkpoint blob, behind the existing OQ-5 schema-version header (PrependSchemaVersion / StripSchemaVersion, store.go). The checkpoint SchemaVersion constant bumps from 1 → 2.
  • Backward-compat decode: a v1 checkpoint (no plan) decodes to a single-todo plan (Todos = [{ Text: goal, Status: active }], Cursor: 0) — i.e. exactly today's relay behaviour. A v2 decoder reading a v1 blob never errors; a v1 decoder reading a v2 blob is rejected with a clear "schema too new" error (forward-incompat is expected and gated by deploy ordering).
  • Provider keys remain stripped at checkpoint time (existing contract, #993). The plan blob carries no secrets and no raw transcripts (only todo summaries).

4.3 Checkpoint bloat discipline (spike 3.2 / HLD R4)

  • Plan.Progress is a bounded ring capped to StallWindow (default 3) — only the trailing window needed for stall detection is retained.
  • Completed todos store a summary (produced by leaning on the same compaction path), not the full per-todo transcript. Earlier-todo results survive in the Context Engine's compaction summaries (#1426), not in the plan blob.

5. Control flow + termination (C1, C3, C4)

5.1 The macro loop (PlanLoop.Run, C1)

load checkpoint → decode Plan (or synthesize single-todo from v1)
loop:
if Plan empty / first step → PLAN: ExecuteStep(step_goal="decompose goal into todo plan") → parse → store Todos
select active Todo (Cursor)
PURSUE: ExecuteStep(step_goal = active Todo.text) # worker runs ONE assemble→…→ingest cycle
OBSERVE: collect StepProgress + token/cost/wall deltas + (maybe) interrupt
if interrupt_required → §7 in-step approval path (suspend → approval.Service → resume), then continue OBSERVE
REFLECT: update Todo status; append StepProgress (ring); maybe re-plan (Replans++ , cap-checked)
persist checkpoint (new LoopIteration)
TERMINATE-CHECK (§5.2): if any axis fires → set Status, emit disposition, STOP
else → loop

Each iteration writes a checkpoint before the next ExecuteStep, so a pod death between any two sub-goals resumes cleanly from the last persisted plan (HLD §6).

5.2 Termination — three axes (C3), all evaluated only orchestrator-side

AxisConditionDisposition (Status / reason)Today?
Goal-completeAll todos done (or model emits completion sentinel and orchestrator confirms no pending/active todos)complete; emit final responseNo
No-progress / stallProgressEval flat over StallWindow OR Replans > ReplanCapstalled + stall_reason ∈ {no_progress, replan_cap}; emit best-effort partial + structured reasonNo
BudgetLoopsUsed >= LoopsMax (existing max_loops) OR tokens/wall exceededbudget_exhausted (maps to today's loop_limit_handler semantics)Yes (only this)
  • Empty-plan guard (spike 3.1): goal_complete MUST NOT fire on an empty/zero-todo plan (otherwise a failed decompose looks "complete"). Terminator requires len(Todos) > 0 && no pending/active for goal-complete.
  • Order of evaluation: budget first (cheapest, hard ceiling), then stall, then goal-complete. The worker reports signals; it never decides any of these (HLD R7 — no split-brain).

5.3 Progress signal + stall detection (C4)

Each step the worker returns a StepProgress (C7/C9):

StepProgress { made_tool_calls, tool_calls_errored, produced_new_artifacts,
todo_marked_complete_by_model, response_delta_hash }

ProgressEval derives a progress score and flags no-progress when, over StallWindow steps (default 3): no new artifacts AND no completed todos AND a repeating response_delta_hash. Per spike R1: this novelty signal alone is insufficient against novel-output thrash — the ReplanCap (§3.1) is the companion backstop, and both sub-axes feed the stall axis. Window + cap are execution_mode-tunable (C11; seeds: window 3, cap 4). Stall surfaces as stalled (recoverable), never error (HLD R2).


6. Driving the stateless worker via ExecuteStep (C5, C7, C9)

6.1 Proto additions (C7) — additive, no breaking change

// ExecutionMode — additive value
EXECUTION_MODE_AUTONOMOUS = 3; // orchestrator-driven deep plan loop

// ExecuteStepRequest — additive optional field
string step_goal = 8; // active todo / "decompose goal" for the planning step;
// empty ⇒ legacy user_message behaviour (relay path preserved)

// New worker→orchestrator event in ExecuteStepEvent oneof
message StepProgress {
bool made_tool_calls = 1;
uint32 tool_calls_errored = 2;
bool produced_new_artifacts = 3;
bool todo_marked_complete_by_model = 4;
bytes response_delta_hash = 5; // for the novelty signal
}
message InterruptRequired { // §7 — in-step approval suspend
string tool_call_id = 1;
string tool_name = 2;
string idempotency_key = 3; // (session_id, loop_iteration, tool_call_id)
bytes pending_tool_intent = 4; // structured intent for the approval payload
}

Both new events are added to the existing ExecuteStepEvent oneof; existing relay clients ignore unknown oneof arms. step_goal is field 8 (next free) on ExecuteStepRequest.

6.2 Worker changes (C9)

  • execute_step (langgraph_executor.py:167) consumes step_goal; when set, the planning/pursuit sub-goal seeds the cycle's intent instead of the raw user_message.
  • The graph emits a StepProgress event at ingest_result (nodes.py:639) with the five fields; response_delta_hash is the existing structured-answer sniff hashed.
  • should_continue (edges.py) is unchanged in spirit — it remains the per-cycle budget guard for the single micro-loop; it does not gain plan/stall/goal logic.

7. In-step interrupt() approval path (net-new — C8, C9)

Two pause granularities (HLD §7.4); the spike confirmed only the second is net-new.

7.1 Between-step pause (macro — already exists)

When a policy requires approval before the next todo, PlanLoop simply does not issue the next ExecuteStep; it persists the checkpoint and parks the session via lifecycle_pause.go / PauseManager / approval.Service. Resume = load checkpoint, continue the loop. No LangGraph interrupt needed. This is the dominant, clean path.

7.2 In-step pause (micro — NET-NEW)

A single tool call inside the worker's ToolNode trips a requires_approval policy mid-cycle. Today this hard-fails-closed (_governance_block_tool, nodes.py:816). New behaviour:

  1. Worker raises interrupt() carrying InterruptRequired{tool_call_id, tool_name, idempotency_key, pending_tool_intent} instead of returning the fail-closed error result. The already-executed sibling tool_results in this cycle are written into the in-step checkpoint (§3.3) so resume skips them.
  2. Executor surfaces the interrupt as an InterruptRequired event on the ExecuteStep stream; the cycle suspends (LangGraph in-step checkpointer, scoped to this step only).
  3. Orchestrator (PlanLoop + C8) routes pending_tool_intent through approval.Service (the existing approval/pause lifecycle).
  4. On approval, resume re-enters the graph only at the interrupted tool call (skip-done on tool_results), executes the single gated call, the cycle completes, and a normal StepProgress is returned. The macro plan state is untouched and still in Go.
  5. On reject/timeout, the gated call returns the fail-closed result (today's behaviour) and the cycle completes — the plan continues / re-plans normally.

7.3 Mapping table

ConcernMechanism
Suspend pointLangGraph interrupt() inside execute_tools parallel-tool loop, per-call (not per batch — ADR-0019 §3a / HLD §7.4 gotcha)
Approval transportapproval.Service + PauseManager (reused, unchanged)
In-step state survivalLangGraph checkpointer scoped to the single step; distinct LoopIteration sub-key (§8.3) so it never collides with the step checkpoint (OQ-3 guard)
Resume correctnessskip tool_call_id already in persisted tool_results; re-run only the gated call (§3.3)
Double-exec backstopidempotency key at MCP proxy (§3.4) covers the duplicate-resume failure mode

Pinned (HLD §7.4 / ADR-0019 §3 gotchas): gate per-call inside the parallel-tool loop, not once per batch; you cannot gate post-stream without buffering; an in-step interrupt resume re-runs only the interrupted tool, not the whole cycle.


8. Idempotency, checkpoint resume & no-double-exec (C6, C9, C10)

8.1 Two failure modes, two mechanisms (spike R5)

Failure modeMechanismLocus
Clean resume after in-step interrupt re-runs already-done siblingspersist executed tool_results + skip-doneinterrupt checkpoint (C6/C9)
Duplicate resume delivery (pod churn / at-least-once) re-fires a side-effecting toolidempotency key dedupMCP proxy egress (C10)

Both are required. Neither alone is sufficient.

8.2 In-step checkpointer scoping

The LangGraph in-step checkpointer used by interrupt()/resume is scoped to the single orchestrator step. It MUST NOT persist macro plan state — that lives only in Go + agent_checkpoints. The in-step checkpointer's blob is discarded once the step completes (success or terminal failure).

8.3 LoopIteration sub-key derivation (OQ-3 guard, spike constraint 4)

  • The step's normal checkpoint uses the monotonic LoopIteration (store.go:115).
  • The interrupt checkpoint for the same step uses a distinct, deterministic sub-key so the (session_id, loop_iteration) unique constraint (store.go:29) is not violated and HashMatch (store.go:139) is not falsely tripped. Derivation: reserve a high-bit-tagged iteration namespace for interrupt checkpoints, e.g. interrupt_iteration = base_iteration | INTERRUPT_FLAG, so it is monotonic-distinct within the session and never aliases a future normal iteration. Pinned exact bit layout to be fixed in the C6 implementation PR and asserted by a checkpointstore test.

8.4 Idempotency key — derivation & enforcement (C10)

  • key = sha256(session_id || ":" || loop_iteration || ":" || tool_call_id) — deterministic from the tuple the spike validated.
  • Enforced at the MCP proxy: a dedup table keyed by key records (result, status, expiry). First call executes upstream + records; a duplicate within the window returns the recorded result. TTL bounds the table (e.g. session lifetime + grace). This is the un-bypassable backstop — the worker also carries the key, but the proxy is authoritative because the worker is the component that may have churned.

9. Integration with merged substrate

9.1 Governed tool path (#1427 — merged)

The deep loop adds no new tool path. Tools execute only through the two governed paths (ADR-0019 §3): the worker's ToolNode wrapped by the deterministic governance wrapper (per-call engine.Check() inside the parallel-tool loop — already merged in #1427) and the MCP proxy egress as the PEP. The autonomous loop's only addition on this path is the idempotency key (§3.4/§8.4) propagated through it. Provider server-side tools stay disabled (ADR-0019 §3 forbidden path) — enforced in review + the CI assertion (§13 task T4-9).

9.2 Live compaction (#1426 — merged)

maybeCompact (pipeline.go:688) is unchanged and already honours the resolved CompactionThresholdEvents knob. The autonomous path's contribution is the non-zero default for execution_mode=autonomous (§3.2, C11) so the async producer fires one step before budget is blown (spike R3: 0 sync fallbacks, 0 truncations). Without this, long plans rely on the higher-latency sync-blown fallback. Compaction is a hard dependency of the deep loop past the context window (HLD §5.5) — satisfied by #1426.

9.3 BYOK + assembly + RAG

Unchanged: planning and pursuit ExecuteStep calls both route the model call through the per-tenant BYOK adapter (LLMAdapterFactory); assemble_context calls the Context Engine for layered assembly + hybrid RAG. RAG-as-attachable-entity (#1377) is orthogonal.


10. Tenet preservation matrix

TenetHow this LLD preserves itEnforcing artifact
Stateless workerPlan lives in Go + checkpoint; worker re-hydrates per step, runs one cycle, may be killed between any two sub-goals. In-step checkpointer is single-step-scoped + discarded.§4.2, §5.1, §8.2
Immutable sessionSession snapshot unchanged across the run; only the checkpoint (append-only versions) carries plan progress. Plan blob carries no secrets, no raw transcripts.§4.2, §4.3
Governed-every-hopAll tool calls ride the merged #1427 governed path (per-call engine.Check() + MCP PEP). Plan/re-plan/terminate decisions sit in Go next to the PDP. Idempotency dedup does not bypass governance — the first call is still fully governed; a deduped duplicate returns the already-governed result.§9.1, §3.4
Budget gatePreserved as one of three axes; LoopsMax == max_loops semantics intact; tokens + wall added.§5.2
Approval pause/resumeMacro (between-step) reuses existing lifecycle; micro (in-step) maps interrupt()approval.Service, net-new but over the same service.§7
AuditEach step, plan mutation, termination decision, and interrupt emits to the existing audit chain + OTel spans (orchestrator-side).§11
No provider server-side toolsForbidden path stays off; CI assertion gates it.§9.1, T4-9

11. Observability (C1, C11)

OTel spans + metrics emitted orchestrator-side (so the agent team can inspect a stalled plan where they inspect everything else — HLD §4.2.4):

  • Metrics: autoloop_plans_started_total, autoloop_todos_completed_total, autoloop_replans_total, autoloop_terminations_total{axis=goal|stall|budget}, autoloop_stall_total{reason=no_progress|replan_cap}, autoloop_step_overhead_seconds (checkpoint write + rehydrate), autoloop_interrupt_approvals_total{outcome=approved|rejected|timeout}, mcp_idempotency_dedup_hits_total, compaction_sync_fallback_total (alert if non-trivial — spike 3.2).
  • Spans: one span per orchestrator step (autoloop.step), child spans for plan/pursue/observe/reflect; plan lifecycle events (plan.created, todo.done, replan, terminate.<axis>) as span events on the session trace.
  • Audit: plan creation, each todo state transition, each re-plan, each interrupt+approval outcome, and the terminal disposition append to the existing audit chain.

12. Risk → mitigation traceability (HLD §10 + spike)

HLD riskStatus after spikeLLD mitigation
R1 re-plan thrashRETIREDReplanCap load-bearing, both stall sub-axes (§3.1, §5.3)
R2 stall false-positiveMitigatedsurface as stalled not error; tunable window (§5.3)
R3 compaction timingRETIREDnon-zero autonomous default + sync fallback net (§3.2, §9.2)
R4 checkpoint bloatMitigatedprogress ring + todo summaries (§4.3)
R5 interrupt double-execRETIREDpersist tool_results + skip-done and idempotency keys (§3.3, §3.4, §8)
R6 server-side toolsOpen → gatedCI assertion (T4-9), review discipline (§9.1)
R7 termination split-brainClosed by designall axes Go-only; worker reports signals (§2, §5.2)
R8 two runtime pathsAccepted (ADR-0019)additive EXECUTION_MODE_AUTONOMOUS; relay default unchanged

13. Task breakdown (implementable sub-issues)

Each becomes a GitHub issue under Milestone #20, labelled per track, with the dependency edges below. Ordering respects: proto/schema first, worker + orchestrator in parallel, then interrupt path + idempotency, then QA.

IDTitleTrackPtsBlocked by
T4-1BACKEND: Plan state model + checkpoint schema v2 (plan-bearing blob, backward-compat decode)backend5
T4-2BACKEND: Proto additions — EXECUTION_MODE_AUTONOMOUS, step_goal, StepProgress, InterruptRequired eventsbackend3
T4-3BACKEND: PlanLoop orchestrator (plan→step→observe→reflect→terminate) + StepDriver over ExecuteStepbackend8T4-1, T4-2
T4-4BACKEND: Three-axis Terminator + ProgressEval (no-progress window + load-bearing replan cap, empty-plan guard)backend5T4-1
T4-5BACKEND: Autonomous-mode knob defaults — non-zero compaction_threshold_events, stall window, replan capbackend2T4-4
T4-6BACKEND(python): Worker step_goal consumption + StepProgress emission (+ response_delta_hash)backend5T4-2
T4-7BACKEND: In-step interrupt() approval path — worker raise + executor surface + orchestrator→approval.Service→resume (net-new)backend8T4-3, T4-6
T4-8BACKEND: Idempotency-key enforcement at MCP proxy egress (exactly-once on side-effecting calls) + persist tool_results into interrupt checkpoint + skip-done resume + LoopIteration sub-keybackend8T4-1, T4-7
T4-9DEVOPS: CI assertion — provider server-side tools disabled (forbidden-path guard)devops2
T4-10QA: interrupt-resume no-double-exec — pause-on-gated-tool-mid-batch → recycle → resume + duplicate-resume; assert each side-effecting tool fired exactly once; stall/replan-cap/goal/budget termination matrix; compaction long-run survivalqa8T4-7, T4-8, T4-5
T4-11BACKEND: Plan lifecycle metrics + spans + audit (terminations by axis, stall reason, interrupt outcomes, sync-fallback alert)backend3T4-3

Dependency graph (critical path): T4-1 → T4-3 → T4-7 → T4-8 → T4-10. T4-2/T4-6 feed the worker side; T4-4/T4-5 feed termination; T4-9/T4-11 are independent and parallelizable.


14. Open items (pinned for implementation PRs, not blockers)

  1. Exact INTERRUPT_FLAG bit layout for the interrupt-checkpoint LoopIteration sub-key (§8.3) — fix in T4-8, assert in a checkpointstore test.
  2. Idempotency dedup-table TTL + storage (Redis vs PG) — fix in T4-8; default Redis with session-lifetime+grace TTL.
  3. Stall-window / replan-cap final defaults per execution_mode beyond the autonomous seed (window 3, cap 4) — tune in T4-5 with QA data from T4-10.
  4. Completion-sentinel format the model emits for goal-complete confirmation — fix in T4-3/T4-6; orchestrator re-confirms against todo state regardless (§5.2 empty-plan guard).
  5. The two ADR-0019 freshness gaps (mid-session guardrail re-pin; policy-bundle hash / decision receipt #1184 per plan step) — tracked separately; not gating T4 v1.

15. References

  • Parent HLD: docs/hld/HLD-autonomous-loop-langgraph.md (#1422)
  • De-risk spike findings: docs/hld/spikes/T4-autonomous-loop-spike-findings.md (#1435, GO-WITH-CHANGES)
  • ADR-0019: docs/adrs/ADR-0019-adopt-and-wrap-autonomous-runtime.md
  • Merged deps: #1427 (real governed tools), #1426 (compaction revived)
  • Substrate: internal/runtime/checkpointstore/store.go, internal/context/assembly/pipeline.go, services/agent-worker/src/agent_worker/graph/, internal/runtime/server/lifecycle_pause.go, proto/upsquad/runtime/v1/runtime.proto
  • Deferred dep: ADR-0015 / A2A #1226 (multi-agent plan delegation, post-v1)