OxagenDocs
Specs & Plans

Unified Agent Engine

Archived spec & plan — status: partially shipped (audited 2026-07-03).

Status: Partially shipped — verified against the codebase on 2026-07-03 by an automated audit.

The unified agent engine core (Stage A) has shipped completely: @oxagen/agent-engine package exists with all ported modules (ports, engine, router, pipeline, evaluator, judge, planner, fleet, trace, prompt). CLI adapters (Stage B1-B3) and platform adapters (Stage C1) are implemented and actively used in agent.repo.edit handler and CLI commands. However, the implementation diverges from the spec's Stage C plan: instead of the detailed agent.coding.session.{start,get,cancel} contract hierarchy with Inngest orchestrator, they implemented a simpler sync endpoint (agent.repo.edit). Secondary deliverables remain incomplete: MCP tool parity for agent.repo.edit, in-app diff tray, and "run in cloud" CLI feature are not verified as shipped.

Implementation evidence

  • /Users/macanderson/Workspaces/oxagen-platform/packages/agent-engine/src/ports.ts — AgentAi/MemoryProvider/TraceStore/GraphSyncProvider interfaces defined
  • /Users/macanderson/Workspaces/oxagen-platform/packages/agent-engine/src/engine.ts — runCodingAgent function implemented
  • /Users/macanderson/Workspaces/oxagen-platform/packages/agent-engine/src/router/model-router.ts — model routing logic migrated
  • /Users/macanderson/Workspaces/oxagen-platform/packages/agent-engine/src/pipeline/index.ts — full 6-stage pipeline (evaluate/enhance/route/execute/judge/revise)
  • /Users/macanderson/Workspaces/oxagen-platform/packages/agent-engine/src/planner/index.ts — task planner
  • /Users/macanderson/Workspaces/oxagen-platform/packages/agent-engine/src/fleet/index.ts — fleet scheduler
  • /Users/macanderson/Workspaces/oxagen-platform/apps/cli/src/agent/adapters/workspace.ts — CLI local workspace adapter
  • /Users/macanderson/Workspaces/oxagen-platform/apps/cli/src/agent/adapters/gateway-agent-ai.ts — CLI BYOK AI runner
  • /Users/macanderson/Workspaces/oxagen-platform/packages/agent/src/adapters/ — platform adapters (code-graph, memory-provider, trace-store, graph-sync)
  • /Users/macanderson/Workspaces/oxagen-platform/packages/handlers/src/agent.repo.edit.ts — handler using runTurn from agent-engine
  • /Users/macanderson/Workspaces/oxagen-platform/apps/cli/src/repl/one-shot.ts — CLI using runTurn
  • /Users/macanderson/Workspaces/oxagen-platform/docs/adr/ADR-019-unified-agent-engine.md — architecture decision recorded

Known gaps at time of archive

  • agent.coding.session.{start,get,cancel} contracts (Stage C3) — agent.repo.edit sync endpoint used instead
  • coding_sessions table/schema (Stage C2)
  • agent.repo.edit MCP tool (Stage C4 parity)
  • In-app diff tray UI surface (Stage C5)
  • CLI 'run in cloud' for connected repos (Stage B5) — unclear if implemented
  • Inngest orchestrator for session management (Stage C3)

Source documents (archived verbatim below)

  • docs/superpowers/plans/2026-06-27-unified-agent-engine-plan.md

Plan — 2026-06-27-unified-agent-engine-plan.md

Unified Agent Engine — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use checkbox (- [ ]) tracking. Supersedes docs/superpowers/plans/2026-06-27-in-app-coding-agent-phase1.md (which shared only the inner loop+tools — would have shipped two agents). See docs/adr/ADR-019-unified-agent-engine.md.

Goal: One coding agent across the CLI and the platform: a single dependency-light @oxagen/agent-engine (the full pipeline/router/judge/planner/fleet/trace brain) that both consumers drive through injected ports, running locally (CLI working dir, BYOK/unmetered) and in the cloud (connected repos, sandboxed/metered).

Architecture: The engine depends only on interface portsWorkspace (filesystem), ModelRunner/AgentAi (the AI call), CodeGraphProvider, MemoryProvider, TraceStore. The CLI supplies local adapters + a BYOK unmetered runner; the platform supplies sandbox + streamAgentReply (metered) + Neo4j + agent.memory + ClickHouse adapters. The engine never imports platform packages and never hardcodes streamText/streamAgentReply.

Tech Stack: TypeScript 6.0.3 (no any), AI SDK ai@6.0.197 (streamText/generateObject/stepCountIs), Zod 3.25.76, Vitest 2.1.9; platform side adds @vercel/sandbox, Drizzle, Inngest, @oxagen/ai.

Global Constraints

  • No any. Precise types or unknown.
  • The engine has ZERO platform dependencies — no @oxagen/database, @oxagen/billing, @oxagen/ai, Neo4j, Inngest. Only ai, zod, and @oxagen/agent-engine's own ports. This keeps the CLI lean, installable, and offline-capable.
  • The engine never calls streamText/generateObject from ai directly in its loop/pipeline — it calls the injected AgentAi port. (On the platform that port is backed by @oxagen/ai, honoring the single-AI-chokepoint rule; in the CLI it's a BYOK wrapper.)
  • Behavior parity: the same pipeline (evaluate→enhance→route→execute→judge→revise), router tiers, planner, and fleet scheduling run in BOTH consumers. Do not add agent intelligence to only one side.
  • Tests: new/changed code needs tests. Run ONLY the narrowest implicated test (pnpm --filter <pkg> test:unit -- <file>); NEVER run pnpm test/turbo run test/pnpm gate/any all-package suite. Restate this verbatim to every subagent.
  • Worktree: all work in /Users/macanderson/oxagen-coding-runner on feat/in-app-coding-agent. Never touch /Users/macanderson/oxagen-monorepo (another session). Commit + push frequently; never to main.
  • Migration files (platform stage) in packages/database/atlas/migrations/; every new table ENABLE+FORCE RLS + tenant_isolation policy + oxagen_app grants.
  • Inngest (platform stage): every step.run touching the DB wraps runInTenantScope.

STAGE A — @oxagen/agent-engine (the shared brain)

Task A1: Rename @oxagen/coding-agent@oxagen/agent-engine

Files: rename dir packages/coding-agent/packages/agent-engine/; update package.json name; update src/index.ts header; pnpm-lock.yaml.

  • Step 1: git mv packages/coding-agent packages/agent-engine (preserves history).
  • Step 2: In packages/agent-engine/package.json set "name": "@oxagen/agent-engine".
  • Step 3: grep -rl "@oxagen/coding-agent" packages apps → update every importer (none expected outside the package yet) to @oxagen/agent-engine.
  • Step 4: pnpm i --no-frozen-lockfile; then pnpm --filter @oxagen/agent-engine typecheck && pnpm --filter @oxagen/agent-engine test:unit (all existing tests green).
  • Step 5: Commit refactor(agent-engine): rename @oxagen/coding-agent → @oxagen/agent-engine.

Task A2: Define the engine ports

Files: Create packages/agent-engine/src/ports.ts; re-export from src/index.ts; Test: packages/agent-engine/src/ports.test.ts.

Interfaces (Produces):

  • interface ModelRunArgs { model: string; system: string; messages: import("ai").ModelMessage[]; tools: import("ai").ToolSet; stopWhen: ReturnType<typeof import("ai").stepCountIs>; abortSignal?: AbortSignal; effort?: "low"|"medium"|"high"; onError?: (e: { error: unknown }) => void; onStepFinish?: (s: { toolCalls?: unknown[] }) => void }

  • type StreamRunResult = import("ai").StreamTextResult<import("ai").ToolSet, never>

  • interface ObjectRunArgs<T> { model: string; system?: string; prompt?: string; messages?: import("ai").ModelMessage[]; schema: import("zod").ZodType<T>; abortSignal?: AbortSignal }

  • interface AgentAi { stream(args: ModelRunArgs): StreamRunResult; generateObject<T>(args: ObjectRunArgs<T>): Promise<{ object: T; usage: { inputTokens?: number; outputTokens?: number; totalTokens?: number } }> }

  • interface MemoryProvider { recallContext(): Promise<string>; remember(kind: string, content: unknown, status?: string): void | Promise<void>; close?(): Promise<void> }

  • interface TraceStore { record(trace: import("./types").TurnTrace): void | Promise<void> } (TurnTrace type migrated in A6)

  • CodeGraphProvider and Workspace already exist in src/types.ts; re-export from ports for one import site.

  • Step 1: Write ports.ts with the interfaces above (the AgentAi.stream return type is exactly the AI SDK StreamTextResult, so both a streamText and a streamAgentReply wrapper satisfy it).

  • Step 2: Write ports.test.ts — a compile-level test constructing a fake AgentAi whose stream returns a minimal object and asserting structural assignability; a fake MemoryProvider.

  • Step 3: pnpm --filter @oxagen/agent-engine typecheck && test:unit -- ports.test.ts.

  • Step 4: Commit feat(agent-engine): define AgentAi/Memory/Trace/CodeGraph ports.

Task A3: runCodingAgent engine with injected AgentAi (NOT hardcoded streamText)

Files: Create packages/agent-engine/src/engine.ts; Test: src/engine.test.ts. (This replaces the superseded plan's Task 4.)

Interfaces:

  • RunCodingAgentOptions (extend the existing type in types.ts): add ai: AgentAi (required), keep workspace, instruction, model, system?, history?, maxSteps?, readOnly?, codeGraph?, signal?, onEvent?. Remove any direct ai import for the call.

  • runCodingAgent(opts) => Promise<RunCodingAgentResult> — same loop as the superseded plan's Task 4, but the model call is opts.ai.stream({ model, system, messages, tools, stopWhen: stepCountIs(maxSteps ?? 32), abortSignal, onError, onStepFinish }). After the loop, workspace.diff()changedFilesFromDifffinal-diff event. Export changedFilesFromDiff.

  • Step 1: Write engine.test.ts — inject a fake AgentAi whose stream returns { textStream, steps, usage, response } and exercises a tool (edit_file) then yields text; assert the diff + changedFiles + usage + final-diff event. (No vi.mock("ai") — inject the fake instead, which is the whole point of the port.)

  • Step 2: Run → FAIL.

  • Step 3: Implement engine.ts using opts.ai.stream(...). Import stepCountIs/ModelMessage from ai for types/stop-condition only (not for the call).

  • Step 4: Run → PASS. Uncomment the runCodingAgent export in src/index.ts.

  • Step 5: Commit feat(agent-engine): runCodingAgent loop over injected AgentAi port.

Task A4: Migrate model-router + model resolution + shared rate card

Files: Move apps/cli/src/agent/model-router.ts + model.tspackages/agent-engine/src/routing/{model-router,model}.ts; create packages/agent-engine/src/routing/rate-card.ts (the cost table currently duplicated in model-router); Tests: move the existing apps/cli/src/agent/__tests__/*model-router* into the package.

  • Step 1: Move the files; the router classifier (classifyTier, routeModel) is pure — keep as-is. Replace its process.env/readConfig() reads with injected params: routeModel(signals, { override?, tierSlugs?: Record<Tier,string> }) so the CLI passes env-derived slugs and the platform passes workspace defaults.
  • Step 2: Extract the rate card into rate-card.ts as pure data + estimateCost(usage, model). (Follow-up ticket: have @oxagen/billing import this instead of its own copy — note it, don't do it here to avoid platform-dep creep.)
  • Step 3: Port the router tests into the package; run them.
  • Step 4: Commit feat(agent-engine): migrate model router + rate card (injected slugs).

Task A5: Migrate evaluator + judge + prompt-enhancer (route through AgentAi)

Files: Move apps/cli/src/agent/{evaluator,judge,prompt-enhancer}.tspackages/agent-engine/src/pipeline/; move their __tests__.

  • Step 1: In evaluator.ts/judge.ts, replace direct generateObject from ai with ai.generateObject(...) via an injected AgentAi param. Keep the heuristic fallbacks intact.
  • Step 2: In prompt-enhancer.ts, replace the direct queryCodeGraph(cwd,…) + FleetMemory reads with the injected CodeGraphProvider + MemoryProvider ports.
  • Step 3: Port their tests (the existing evaluator.test.ts/judge.test.ts use fakes already — adapt to the injected AgentAi).
  • Step 4: Commit feat(agent-engine): migrate evaluator/judge/enhancer onto ports.

Task A6: Migrate trace types + formatter + system-prompt builder

Files: Move apps/cli/src/agent/{trace,trace-format}.tspackages/agent-engine/src/trace/; move system-prompt.tspackages/agent-engine/src/prompt/system-prompt.ts. trace-store.ts becomes a CLI adapter (stays in apps/cli, implements TraceStore). project-context.ts loader stays CLI-side (FS walk); buildSystemPrompt takes the already-loaded context string (already does).

  • Step 1: Move trace types + formatTraceText/formatTraceList (pure) into the engine; export. TurnTrace is the type referenced by the TraceStore port (A2).
  • Step 2: Move buildSystemPrompt (pure) into the engine; keep its signature.
  • Step 3: Port trace-format tests.
  • Step 4: Commit feat(agent-engine): migrate trace + system-prompt builder.

Task A7: Migrate the pipeline orchestrator + planner + fleet scheduler

Files: Move apps/cli/src/agent/{pipeline,planner}.ts and apps/cli/src/agent/fleet/packages/agent-engine/src/; move their __tests__.

  • Step 1: pipeline.ts (runTurn): rewire so EXECUTE calls runCodingAgent({ ai, workspace, codeGraph, … }) (A3) instead of the CLI runAgent; EVALUATE/ROUTE/JUDGE use the injected AgentAi + router (A4/A5); ENHANCE uses the ports. runTurn gains an AgentEngineContext param bundling the ports ({ ai, workspace, codeGraph, memory, trace, tierSlugs }).
  • Step 2: planner.ts: route generateObject through AgentAi; enhancePrompt via ports.
  • Step 3: fleet/orchestrator.ts: the scheduler (dependency graph + file-lock concurrency) is pure; its runner is already injectable — make it accept a runTask(task, ctx) callback so the CLI fulfills tasks with local concurrency and the platform can fulfill via Inngest later. Move fleet/memory.ts behind MemoryProvider; fleet/store.ts behind a small PlanStore port (CLI=JSON file).
  • Step 4: Port the pipeline/fleet tests (pipeline.test.ts, etc.) using fakes for the ports.
  • Step 5: Commit feat(agent-engine): migrate pipeline + planner + fleet scheduler onto ports.

STAGE B — CLI consumer (local adapters + always-linked)

Task B1: CLI local adapters

Files: apps/cli/src/agent/adapters/{local-workspace,local-code-graph,duckdb-memory,local-trace-store,plan-store}.ts.

  • LocalWorkspace implements Workspace (lift today's tools.ts/fs logic — already specced in the superseded plan's Task 5; reuse src/internal/glob.ts).
  • LocalCodeGraphProvider implements CodeGraphProvider wrapping queryCodeGraph(cwd,…) (the daemon index, ADR-016).
  • DuckdbMemoryProvider implements MemoryProvider wrapping today's memory.ts/engram.
  • LocalTraceStore implements TraceStore wrapping today's trace-store.ts JSON file.
  • Tests per adapter (LocalWorkspace gets the temp-dir test from the superseded plan's Task 5).
  • Commit feat(cli): local agent-engine adapters.

Task B2: BYOK AgentAi runner

Files: apps/cli/src/agent/adapters/byok-ai.ts (or a @oxagen/ai unmetered export — prefer a @oxagen/ai export createUnmeteredAi() so the wrapper lives in @oxagen/ai, but the CLI must not pull platform deps; if @oxagen/ai is too heavy for the CLI, keep a thin local streamText/generateObject wrapper here).

  • Implements AgentAi over raw ai streamText/generateObject with the user's gateway key (ensureGatewayKey). No metering.
  • Test: a unit test asserting stream/generateObject delegate (mock ai).
  • Commit feat(cli): BYOK AgentAi runner (unmetered).

Task B3: Refactor apps/cli onto @oxagen/agent-engine

Files: apps/cli/src/agent/loop.ts (becomes a thin wrapper or is deleted), repl/*, the migrated files DELETED from apps/cli/src/agent (now in the engine). Add @oxagen/agent-engine to apps/cli/package.json.

  • The REPL/one-shot build an AgentEngineContext (B1 adapters + B2 runner + router tier slugs from env) and call runTurn(...). Map onStage/onText/onToolCall to Ink/stdout as today.
  • Delete the now-duplicated engine files from apps/cli/src/agent; update all importers; ensure grep -rl "agent/pipeline\|agent/model-router\|agent/judge\|agent/evaluator\|agent/planner\|agent/tools\|agent/trace" apps/cli/src all point at the package.
  • Run the CLI agent tests (the ones that remain CLI-side) + typecheck.
  • Commit refactor(cli): consume @oxagen/agent-engine; delete duplicated engine.

Task B4: oxagen login (always-linked)

Files: apps/cli/src/commands/login.ts, a token store in ~/.config/oxagen/, wire org/workspace into the adapters (so memory/code-graph can use platform contracts when linked).

  • Device-code or token-paste login → store { token, orgId, workspaceId }. When present, the CLI's MemoryProvider/CodeGraphProvider can call platform contracts (agent.memory.recall, graph.node.search) via the API; otherwise fall back to local.
  • Tests for the token store + an auth-required guard.
  • Commit feat(cli): oxagen login (always-linked identity).

Task B5: CLI "run in cloud" for connected repos

Files: apps/cli/src/commands/code.ts (or extend the REPL): when the target is a connected repo (not the local cwd), call agent.coding.session.start (Stage C) over the API and stream/poll progress.

  • Commit feat(cli): run connected-repo coding sessions in the cloud.

STAGE C — Platform adapters + cloud runner

Task C1: Platform adapters (packages/agent)

  • SandboxWorkspace implements Workspace over a persistent Vercel Sandbox (from the superseded plan's Task 6 — extend packages/sandbox with createWorkspace()).
  • PlatformAgentAi implements AgentAi wrapping streamAgentReply (stream) + @oxagen/ai generateObject (metered). Telemetry/credits/tenant-scope captured here.
  • Neo4jCodeGraphProvider via graph.node.search.
  • PlatformMemoryProvider via agent.memory.recall/write.
  • ClickHouseTraceStore.
  • Commit feat(agent): platform agent-engine adapters.

Task C2: coding_sessions schema + migration (RLS + grants) — as superseded plan Task 7.

Task C3: agent.coding.session.{start,get,cancel} contracts + handlers + sandboxed Inngest orchestrator — as superseded plan Tasks 8–9, but the orchestrator runs runTurn/runCodingAgent with the C1 platform adapters (clone connected repo → run the SAME engine → persist diff/events). Fleet fan-out fulfilled via agent.subagent.dispatch (ADR-010).

Task C4: MCP + API parity surfaces — as superseded plan Task 10.

Task C5: In-app diff tray + @-mention repos — as superseded plan Task 11.

Task C6: Docs (capability docs + this ADR), manifest sync, gate, PR — as superseded plan Task 12.


STAGE D — Phase 2/3 (separate specs)

  • Phase 2 VCS write: @oxagen/github Octokit wrapper, OAuth write-scope upgrade, repo.pr.open/get, push branch from sandbox (user token), ready PR. Never push to default branch.
  • Phase 3 CI auto-fix loop: route check_run/workflow_run webhooks to the active session, feed failures back to the engine, fix + re-push, cap N=3, finalize on green.

Self-Review notes

  • Two-agents risk closed: the full pipeline/router/judge/planner/fleet live in @oxagen/agent-engine (Stage A) and are consumed identically by CLI (Stage B) and platform (Stage C). No intelligence lives on only one side.
  • Chokepoint honored: the engine calls only the AgentAi port; platform backs it with @oxagen/ai/streamAgentReply (metered), CLI with a BYOK runner (unmetered) — ADR-019.
  • Dep boundary: engine imports only ai+zod; all platform/local specifics live in adapters in the consumers.
  • Type consistency: ports defined in A2 (AgentAi, MemoryProvider, TraceStore, CodeGraphProvider, Workspace) are the only cross-stage contract; every migrated subsystem (A4–A7) and every adapter (B1–B2, C1) depends on exactly these.

On this page

Plan — 2026-06-27-unified-agent-engine-plan.mdUnified Agent Engine — Implementation PlanGlobal ConstraintsSTAGE A — @oxagen/agent-engine (the shared brain)Task A1: Rename @oxagen/coding-agent@oxagen/agent-engineTask A2: Define the engine portsTask A3: runCodingAgent engine with injected AgentAi (NOT hardcoded streamText)Task A4: Migrate model-router + model resolution + shared rate cardTask A5: Migrate evaluator + judge + prompt-enhancer (route through AgentAi)Task A6: Migrate trace types + formatter + system-prompt builderTask A7: Migrate the pipeline orchestrator + planner + fleet schedulerSTAGE B — CLI consumer (local adapters + always-linked)Task B1: CLI local adaptersTask B2: BYOK AgentAi runnerTask B3: Refactor apps/cli onto @oxagen/agent-engineTask B4: oxagen login (always-linked)Task B5: CLI "run in cloud" for connected reposSTAGE C — Platform adapters + cloud runnerTask C1: Platform adapters (packages/agent)Task C2: coding_sessions schema + migration (RLS + grants) — as superseded plan Task 7.Task C3: agent.coding.session.{start,get,cancel} contracts + handlers + sandboxed Inngest orchestrator — as superseded plan Tasks 8–9, but the orchestrator runs runTurn/runCodingAgent with the C1 platform adapters (clone connected repo → run the SAME engine → persist diff/events). Fleet fan-out fulfilled via agent.subagent.dispatch (ADR-010).Task C4: MCP + API parity surfaces — as superseded plan Task 10.Task C5: In-app diff tray + @-mention repos — as superseded plan Task 11.Task C6: Docs (capability docs + this ADR), manifest sync, gate, PR — as superseded plan Task 12.STAGE D — Phase 2/3 (separate specs)Self-Review notes