LLD: Autonomous Loop — LangGraph adopt-and-wrap
| Field | Value |
|---|---|
| Status | Draft — architect/agent-level (NOT founder-gated; HLD direction was the founder gate, approved via PR #1416 / ADR-0019) |
| Version | 1.0 |
| Date | 2026-06-18 |
| Author | Principal Technical Architect Agent |
| Issue | #1422 (T4) |
| Parent HLD | HLD-autonomous-loop-langgraph.md (approved + merged) |
| De-risk spike | spikes/T4-autonomous-loop-spike-findings.md (#1435) — verdict GO-WITH-CHANGES |
| Ratified by | ADR-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)
| # | Component | Location (new unless noted) | Language |
|---|---|---|---|
| C1 | PlanLoop — macro orchestrator loop (plan → step → observe → reflect → terminate) | internal/runtime/autoloop/loop.go | Go |
| C2 | Plan state model + codec (encode/decode into checkpoint blob, backward-compat) | internal/runtime/autoloop/plan.go | Go |
| C3 | Terminator — three-axis termination evaluator (goal / stall / budget) | internal/runtime/autoloop/terminate.go | Go |
| C4 | ProgressEval — stall detection (no-progress window + replan cap) | internal/runtime/autoloop/progress.go | Go |
| C5 | StepDriver — wraps the ExecuteStep RPC, maps step_goal in / StepProgress + interrupt out | internal/runtime/autoloop/driver.go | Go |
| C6 | Checkpoint schema bump (plan-bearing blob, OQ-5 header) | internal/runtime/checkpointstore/store.go (extend) | Go |
| C7 | Proto additions — step_goal, StepProgress, interrupt_required event, EXECUTION_MODE_AUTONOMOUS | proto/upsquad/runtime/v1/runtime.proto (extend) | proto |
| C8 | In-step interrupt mapping → approval.Service + resume | internal/runtime/server/lifecycle_pause.go (extend), C5 | Go |
| C9 | Worker: step_goal consumption + StepProgress emission + interrupt() raise on requires_approval | services/agent-worker/.../graph/{nodes,edges,state}.py, executor/langgraph_executor.py (extend) | Python |
| C10 | MCP proxy: idempotency-key enforcement (exactly-once on side-effecting calls) | internal/mcp/middleware/ (extend) | Go |
| C11 | Autonomous-mode knob defaults (compaction_threshold_events, stall window, replan cap) | internal/context/.../knobs + internal/runtime/autoloop/config.go | Go |
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.pybeyond the existing budget-onlyshould_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_hashnovelty signal; the replan cap is the only backstop that catches novel-output thrash (the thrash worker terminated atstalled:replan_cap, notno_progress). - Where it lives:
Plan.replanscounter (C2), evaluated byProgressEval(C4) and surfaced toTerminator(C3). The cap is a config valueReplanCap(C11),execution_mode-tunable, seed default = 4 (spike seed; "~5" budget envelope). Incremented exactly once per accepted re-plan event inPlanLoop.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 furtherExecuteStep). This is distinct fromno_progressand frombudget_exhaustedso operators can tell novel-thrash apart from genuine stall. - Both stall sub-axes are mandatory (spike R1):
no_progress(flat window) ANDreplans > 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–708—0means "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— theEffectiveForTenantresolution gains anexecution_mode-aware default so thatexecution_mode = autonomousresolvesCompactionThresholdEventsto 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 (
2×). Persisting executedtool_resultsinto 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_resultsfor the in-flight cycle. On resume, the worker skips anytool_call_idalready present intool_resultsand 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 falseHashMatch=falseP1 (the checkpointstore unique constraint is on(session_id, loop_iteration); seestore.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_approvalhard-fails-closed —_governance_block_tool(nodes.py:816) returns an errorToolResult; there is nointerrupt()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
Planrides the checkpoint blob, behind the existing OQ-5 schema-version header (PrependSchemaVersion/StripSchemaVersion,store.go). The checkpointSchemaVersionconstant bumps from1 → 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.Progressis a bounded ring capped toStallWindow(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
| Axis | Condition | Disposition (Status / reason) | Today? |
|---|---|---|---|
| Goal-complete | All todos done (or model emits completion sentinel and orchestrator confirms no pending/active todos) | complete; emit final response | No |
| No-progress / stall | ProgressEval flat over StallWindow OR Replans > ReplanCap | stalled + stall_reason ∈ {no_progress, replan_cap}; emit best-effort partial + structured reason | No |
| Budget | LoopsUsed >= LoopsMax (existing max_loops) OR tokens/wall exceeded | budget_exhausted (maps to today's loop_limit_handler semantics) | Yes (only this) |
- Empty-plan guard (spike 3.1):
goal_completeMUST NOT fire on an empty/zero-todo plan (otherwise a failed decompose looks "complete").Terminatorrequireslen(Todos) > 0 && no pending/activefor 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) consumesstep_goal; when set, the planning/pursuit sub-goal seeds the cycle's intent instead of the rawuser_message.- The graph emits a
StepProgressevent atingest_result(nodes.py:639) with the five fields;response_delta_hashis 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:
- Worker raises
interrupt()carryingInterruptRequired{tool_call_id, tool_name, idempotency_key, pending_tool_intent}instead of returning the fail-closed error result. The already-executed siblingtool_resultsin this cycle are written into the in-step checkpoint (§3.3) so resume skips them. - Executor surfaces the interrupt as an
InterruptRequiredevent on theExecuteStepstream; the cycle suspends (LangGraph in-step checkpointer, scoped to this step only). - Orchestrator (
PlanLoop+ C8) routespending_tool_intentthroughapproval.Service(the existing approval/pause lifecycle). - 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 normalStepProgressis returned. The macro plan state is untouched and still in Go. - 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
| Concern | Mechanism |
|---|---|
| Suspend point | LangGraph interrupt() inside execute_tools parallel-tool loop, per-call (not per batch — ADR-0019 §3a / HLD §7.4 gotcha) |
| Approval transport | approval.Service + PauseManager (reused, unchanged) |
| In-step state survival | LangGraph checkpointer scoped to the single step; distinct LoopIteration sub-key (§8.3) so it never collides with the step checkpoint (OQ-3 guard) |
| Resume correctness | skip tool_call_id already in persisted tool_results; re-run only the gated call (§3.3) |
| Double-exec backstop | idempotency 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 mode | Mechanism | Locus |
|---|---|---|
| Clean resume after in-step interrupt re-runs already-done siblings | persist executed tool_results + skip-done | interrupt checkpoint (C6/C9) |
| Duplicate resume delivery (pod churn / at-least-once) re-fires a side-effecting tool | idempotency key dedup | MCP 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 andHashMatch(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
keyrecords(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
| Tenet | How this LLD preserves it | Enforcing artifact |
|---|---|---|
| Stateless worker | Plan 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 session | Session 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-hop | All 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 gate | Preserved as one of three axes; LoopsMax == max_loops semantics intact; tokens + wall added. | §5.2 |
| Approval pause/resume | Macro (between-step) reuses existing lifecycle; micro (in-step) maps interrupt() → approval.Service, net-new but over the same service. | §7 |
| Audit | Each step, plan mutation, termination decision, and interrupt emits to the existing audit chain + OTel spans (orchestrator-side). | §11 |
| No provider server-side tools | Forbidden 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 risk | Status after spike | LLD mitigation |
|---|---|---|
| R1 re-plan thrash | RETIRED | ReplanCap load-bearing, both stall sub-axes (§3.1, §5.3) |
| R2 stall false-positive | Mitigated | surface as stalled not error; tunable window (§5.3) |
| R3 compaction timing | RETIRED | non-zero autonomous default + sync fallback net (§3.2, §9.2) |
| R4 checkpoint bloat | Mitigated | progress ring + todo summaries (§4.3) |
| R5 interrupt double-exec | RETIRED | persist tool_results + skip-done and idempotency keys (§3.3, §3.4, §8) |
| R6 server-side tools | Open → gated | CI assertion (T4-9), review discipline (§9.1) |
| R7 termination split-brain | Closed by design | all axes Go-only; worker reports signals (§2, §5.2) |
| R8 two runtime paths | Accepted (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.
| ID | Title | Track | Pts | Blocked by |
|---|---|---|---|---|
| T4-1 | BACKEND: Plan state model + checkpoint schema v2 (plan-bearing blob, backward-compat decode) | backend | 5 | — |
| T4-2 | BACKEND: Proto additions — EXECUTION_MODE_AUTONOMOUS, step_goal, StepProgress, InterruptRequired events | backend | 3 | — |
| T4-3 | BACKEND: PlanLoop orchestrator (plan→step→observe→reflect→terminate) + StepDriver over ExecuteStep | backend | 8 | T4-1, T4-2 |
| T4-4 | BACKEND: Three-axis Terminator + ProgressEval (no-progress window + load-bearing replan cap, empty-plan guard) | backend | 5 | T4-1 |
| T4-5 | BACKEND: Autonomous-mode knob defaults — non-zero compaction_threshold_events, stall window, replan cap | backend | 2 | T4-4 |
| T4-6 | BACKEND(python): Worker step_goal consumption + StepProgress emission (+ response_delta_hash) | backend | 5 | T4-2 |
| T4-7 | BACKEND: In-step interrupt() approval path — worker raise + executor surface + orchestrator→approval.Service→resume (net-new) | backend | 8 | T4-3, T4-6 |
| T4-8 | BACKEND: Idempotency-key enforcement at MCP proxy egress (exactly-once on side-effecting calls) + persist tool_results into interrupt checkpoint + skip-done resume + LoopIteration sub-key | backend | 8 | T4-1, T4-7 |
| T4-9 | DEVOPS: CI assertion — provider server-side tools disabled (forbidden-path guard) | devops | 2 | — |
| T4-10 | QA: 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 survival | qa | 8 | T4-7, T4-8, T4-5 |
| T4-11 | BACKEND: Plan lifecycle metrics + spans + audit (terminations by axis, stall reason, interrupt outcomes, sync-fallback alert) | backend | 3 | T4-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)
- Exact
INTERRUPT_FLAGbit layout for the interrupt-checkpointLoopIterationsub-key (§8.3) — fix in T4-8, assert in a checkpointstore test. - Idempotency dedup-table TTL + storage (Redis vs PG) — fix in T4-8; default Redis with session-lifetime+grace TTL.
- Stall-window / replan-cap final defaults per
execution_modebeyond the autonomous seed (window 3, cap 4) — tune in T4-5 with QA data from T4-10. - 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).
- 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)