Skip to main content

ADR-0019: Adopt-and-wrap autonomous runtime (LangGraph + embedded governance)

  • Status: Accepted (founder sign-off via PR #1416 on 2026-06-17)
  • Date: 2026-06-17
  • Decision owners: Founder (sign-off), Principal Architect (author), Product Manager (downstream PRDs)
  • Related:
    • ADR-0012 — operationalizes its "adopt runtime for cloud tenants / keep ours for on-prem + multi-tenant" reshape line
    • ADR-0013 — the runtime is an open primitive (direct adopt); model backends and cloud services stay behind interfaces
    • ADR-0015 / upsquad-ai/upsquad-core#1226 — A2A as the cross-agent wire protocol (dependency for cross-agent/handoff interaction)
    • upsquad-ai/upsquad-core#1184 — Agent Decision Receipt (policy-bundle hash; closes a freshness gap below)
    • upsquad-ai/upsquad-core#1375 / #1377 — knowledge / RAG-as-attachable-entity (memory backend the runtime calls out to)
    • Compaction code: internal/context/compaction (service), internal/context/assembly/pipeline.go:365 (assemble-time hook stub), cmd/context-engine/main.go (worker never started)

Context

Verified on origin/main: today an UpsQuad agent is a governed, stateless, single-turn LangGraph relay. The shell is strong — layered context assembly, hybrid RAG, a versioning DAG, the 5-tier clearance + cascade governance, approvals pause/resume, immutable sessions + checkpoints, the audit chain, multi-tenancy, and BYOK. But the core is hollow:

  1. Tool execution is stubbed. services/agent-worker/.../graph/nodes.py execute_tools returns [stub]; MCP_STUB_MODE defaults true; mcp_client is never injected in the running path. Agents cannot actually do anything.
  2. Compaction is dead code. internal/context/compaction exists, but the assemble-time hook is stubbed (assembly/pipeline.go:365 logs "compaction check stubbed") and the background worker is never instantiated in cmd/context-engine/main.go — no producer ever enqueues work.
  3. The loop is a relay, not autonomous. One turn in, one turn out. No plan→act→observe iteration, zero built-in tools.

To deliver autonomous agents we need a real iterating runtime with real tool execution and revived compaction — without giving up the governance shell that is the moat (ADR-0012). This ADR records the validated design from a multi-pass investigation.

Decision

1. Adopt-and-wrap, not rebuild

Keep the Go shell as the outer authority. Replace only the worker's single-turn relay graph with an adopted autonomous runtime that calls out to our existing layers:

ConcernCalls out to
ModelBYOK gateway (per-tenant keys)
MemoryContext Engine (assembly + RAG + compaction)
ToolsMCP proxy
Governanceembedded governance engine

This is not a rewrite. Every moat layer stays exactly where it is; we swap the inner loop.

2. Runtime = LangGraph

LangGraph is the adopted runtime: MIT-licensed, model-agnostic, self-hostable (on-prem-safe), and offers the best interception surface via interrupt() for our pause/resume/approval flows. Claude and Gemini are pluggable model backends behind BYOK — never the runtime. The Claude Agent SDK and Google ADK are rejected as the foundation: each couples us to one provider (Claude-locked / Vertex gravity), breaks model-agnosticism, and breaks the air-gap moat (ADR-0012, ADR-0013).

3. Tool-path governance — every tool must travel a path we can intercept

There are exactly two governed tool paths and one forbidden path:

  • (a) Our LangGraph ToolNode — code we own. Wrap it so governance.Check() always fires: the LLM picks intent, the wrapper enforces the decision. Wrap gotchas to honour in the LLD: gate per-call inside the parallel-tool loop (not once for the batch); you cannot gate post-stream without buffering; the embedded PDP removes per-check network latency.
  • (b) MCP proxy egress — the network choke point and un-bypassable PEP.
  • (FORBIDDEN) The runtime's / provider's own internal tool executor — built-in tools or model-server-side tools. These are invisible to both governed paths. They must be disabled, or the runtime is disqualified.

LangGraph has no hidden native tools (ToolNode runs only registered tools), so wrapping (a) suffices. Rule: keep all tools MCP-registered, and do not enable provider server-side tools.

4. Revive compaction (mandatory for autonomy)

Wire a threshold-triggered producer at the assemble Step-5 stub (assembly/pipeline.go:365) and start the compaction worker in cmd/context-engine/main.go. Without this, long autonomous runs hit the context ceiling and get truncated, never summarized — autonomy is impossible past the window.

5. Governance locus = embedded PDP + gateway backstop

The governance.Engine is already an embeddable Go library (internal/governance/engine.go — "in-process library… callers import it and call engine.Check()"). Embed it inline at the pre-tool and pre-handoff hooks for low-latency in-process decisions, and keep the MCP-proxy egress + Coordinator per-hop checks as the un-bypassable PEP — both over the same engine. This eases on-prem (no second governance service to deploy).

  • Freshness: policy/guardrail evaluation is read-through-per-call (always fresh, no cache) — keep read-through.
  • Two freshness gaps to close in the downstream HLD (not in this ADR):
    1. Mid-session guardrail re-pin — guardrail hashes are pinned at session init, so a mid-run guardrail change is currently ignored.
    2. Policy-bundle hash / decision receipt (#1184, unbuilt) — attest which policy version judged each action.

These are presented as the decision but are flagged for the founder because they touch strategy bound by ADR-0012:

  • Additive, not replacement. The adopted LangGraph-deep autonomous runtime is additive. The existing runtime path is kept for on-prem / multi-tenant per ADR-0012; the autonomous runtime is one option, not a wholesale replacement. Treating it as a replacement would reopen ADR-0012.
  • Hard on-prem rule. No runtime adoption that breaks air-gapped on-prem. The autonomous runtime must be self-hostable OSS. Hyperscaler-managed runtimes (Vertex Agent Engine, hosted SDK control planes) are cloud-opt-in only, never the foundation.

Consequences

Build now — safe under every signed path

These are correct regardless of how the founder rules on the scope decisions, because they make the current runtime real and governed:

  1. Make tools real — inject mcp_client, flip MCP_STUB_MODE off in the running path, replace the execute_tools stub with the real MCP-proxy call, gated by governance.Check() per call inside the parallel-tool loop.
  2. Revive compaction — threshold producer at pipeline.go:365 + start the worker in cmd/context-engine/main.go.
  3. Deterministic governance wrapper — embed the engine at the pre-tool/pre-handoff hooks; keep the MCP-proxy egress + Coordinator per-hop as the backstop, over the same engine.

Gated on founder sign-off

  1. Adopting LangGraph as the autonomous-runtime foundation (the deep loop replacing the relay graph).
  2. The additive-vs-replacement position (above).
  3. The hard on-prem rule (above).

Dependencies

  • Cross-agent / A2A interaction depends on ADR-0015 (#1226). Handoff governance hooks in this design assume A2A as the wire protocol between agents; this ADR does not re-decide that.
  • The two freshness gaps (mid-session re-pin; policy-bundle hash via #1184) are handed to the downstream HLD, not resolved here.

Negative — what we accept

  1. A second runtime path to maintain (relay + autonomous) until the autonomous path is proven, per the additive decision.
  2. Wrapper-correctness burden: per-call gating inside the parallel-tool loop and the no-post-stream-gate-without-buffering constraint must be enforced in review.
  3. Discipline burden: provider server-side tools must stay disabled forever, enforced in code review (a single enabled built-in tool reopens the forbidden path).

Alternatives considered

  1. Build our own autonomous runtime from scratch. Rejected — contradicts ADR-0012 ("stop describing UpsQuad as an agent runtime"), duplicates GEAP's strongest table-stakes axis, and burns months on the inner loop instead of the moat layers.
  2. Adopt the Claude Agent SDK as the foundation. Rejected — Claude-locked, breaks model-agnosticism, and its hosted control-plane variants break the air-gap moat.
  3. Adopt Google ADK / Vertex Agent Engine as the foundation. Rejected — Vertex gravity, breaks model-agnosticism and air-gap on-prem (ADR-0013 names managed runtimes as cloud-opt-in only).
  4. Replace the existing runtime wholesale with the autonomous one. Rejected — reopens ADR-0012's "keep ours for on-prem + multi-tenant" commitment; the additive path preserves it.

Sign-off

  • Founder (Vaisakh / Ashik) — accept this ADR; rule on additive-vs-replacement; rule on the hard on-prem rule
  • Principal Architect — authored; downstream HLD/LLD will enforce the tool-path and freshness-gap items
  • Product Manager — downstream PRDs for the autonomous runtime + compaction revival
  • Backend lead — implementation implications for services/agent-worker, internal/context/compaction, internal/governance