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-enginepackage 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 detailedagent.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. Supersedesdocs/superpowers/plans/2026-06-27-in-app-coding-agent-phase1.md(which shared only the inner loop+tools — would have shipped two agents). Seedocs/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 ports — Workspace (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 orunknown. - The engine has ZERO platform dependencies — no
@oxagen/database,@oxagen/billing,@oxagen/ai, Neo4j, Inngest. Onlyai,zod, and@oxagen/agent-engine's own ports. This keeps the CLI lean, installable, and offline-capable. - The engine never calls
streamText/generateObjectfromaidirectly in its loop/pipeline — it calls the injectedAgentAiport. (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 runpnpm test/turbo run test/pnpm gate/any all-package suite. Restate this verbatim to every subagent. - Worktree: all work in
/Users/macanderson/oxagen-coding-runneronfeat/in-app-coding-agent. Never touch/Users/macanderson/oxagen-monorepo(another session). Commit + push frequently; never tomain. - Migration files (platform stage) in
packages/database/atlas/migrations/; every new table ENABLE+FORCE RLS +tenant_isolationpolicy +oxagen_appgrants. - Inngest (platform stage): every
step.runtouching the DB wrapsrunInTenantScope.
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.jsonset"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; thenpnpm --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) -
CodeGraphProviderandWorkspacealready exist insrc/types.ts; re-export from ports for one import site. -
Step 1: Write
ports.tswith the interfaces above (theAgentAi.streamreturn type is exactly the AI SDKStreamTextResult, so both astreamTextand astreamAgentReplywrapper satisfy it). -
Step 2: Write
ports.test.ts— a compile-level test constructing a fakeAgentAiwhosestreamreturns a minimal object and asserting structural assignability; a fakeMemoryProvider. -
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 intypes.ts): addai: AgentAi(required), keepworkspace,instruction,model,system?,history?,maxSteps?,readOnly?,codeGraph?,signal?,onEvent?. Remove any directaiimport for the call. -
runCodingAgent(opts) => Promise<RunCodingAgentResult>— same loop as the superseded plan's Task 4, but the model call isopts.ai.stream({ model, system, messages, tools, stopWhen: stepCountIs(maxSteps ?? 32), abortSignal, onError, onStepFinish }). After the loop,workspace.diff()→changedFilesFromDiff→final-diffevent. ExportchangedFilesFromDiff. -
Step 1: Write
engine.test.ts— inject a fakeAgentAiwhosestreamreturns{ textStream, steps, usage, response }and exercises a tool (edit_file) then yields text; assert the diff + changedFiles + usage +final-diffevent. (Novi.mock("ai")— inject the fake instead, which is the whole point of the port.) -
Step 2: Run → FAIL.
-
Step 3: Implement
engine.tsusingopts.ai.stream(...). ImportstepCountIs/ModelMessagefromaifor types/stop-condition only (not for the call). -
Step 4: Run → PASS. Uncomment the
runCodingAgentexport insrc/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.ts → packages/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 itsprocess.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.tsas pure data +estimateCost(usage, model). (Follow-up ticket: have@oxagen/billingimport 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}.ts → packages/agent-engine/src/pipeline/; move their __tests__.
- Step 1: In
evaluator.ts/judge.ts, replace directgenerateObjectfromaiwithai.generateObject(...)via an injectedAgentAiparam. Keep the heuristic fallbacks intact. - Step 2: In
prompt-enhancer.ts, replace the directqueryCodeGraph(cwd,…)+FleetMemoryreads with the injectedCodeGraphProvider+MemoryProviderports. - Step 3: Port their tests (the existing
evaluator.test.ts/judge.test.tsuse fakes already — adapt to the injectedAgentAi). - 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}.ts → packages/agent-engine/src/trace/; move system-prompt.ts → packages/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.TurnTraceis the type referenced by theTraceStoreport (A2). - Step 2: Move
buildSystemPrompt(pure) into the engine; keep its signature. - Step 3: Port
trace-formattests. - 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 callsrunCodingAgent({ ai, workspace, codeGraph, … })(A3) instead of the CLIrunAgent; EVALUATE/ROUTE/JUDGE use the injectedAgentAi+ router (A4/A5); ENHANCE uses the ports.runTurngains anAgentEngineContextparam bundling the ports ({ ai, workspace, codeGraph, memory, trace, tierSlugs }). - Step 2:
planner.ts: routegenerateObjectthroughAgentAi;enhancePromptvia ports. - Step 3:
fleet/orchestrator.ts: the scheduler (dependency graph + file-lock concurrency) is pure; its runner is already injectable — make it accept arunTask(task, ctx)callback so the CLI fulfills tasks with local concurrency and the platform can fulfill via Inngest later. Movefleet/memory.tsbehindMemoryProvider;fleet/store.tsbehind a smallPlanStoreport (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'stools.ts/fs logic — already specced in the superseded plan's Task 5; reusesrc/internal/glob.ts).LocalCodeGraphProvider implements CodeGraphProviderwrappingqueryCodeGraph(cwd,…)(the daemon index, ADR-016).DuckdbMemoryProvider implements MemoryProviderwrapping today'smemory.ts/engram.LocalTraceStore implements TraceStorewrapping today'strace-store.tsJSON 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
AgentAiover rawaistreamText/generateObjectwith the user's gateway key (ensureGatewayKey). No metering. - Test: a unit test asserting
stream/generateObjectdelegate (mockai). - 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 callrunTurn(...). MaponStage/onText/onToolCallto Ink/stdout as today. - Delete the now-duplicated engine files from
apps/cli/src/agent; update all importers; ensuregrep -rl "agent/pipeline\|agent/model-router\|agent/judge\|agent/evaluator\|agent/planner\|agent/tools\|agent/trace" apps/cli/srcall 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'sMemoryProvider/CodeGraphProvidercan 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 Workspaceover a persistent Vercel Sandbox (from the superseded plan's Task 6 — extendpackages/sandboxwithcreateWorkspace()).PlatformAgentAi implements AgentAiwrappingstreamAgentReply(stream) +@oxagen/aigenerateObject(metered). Telemetry/credits/tenant-scope captured here.Neo4jCodeGraphProviderviagraph.node.search.PlatformMemoryProviderviaagent.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/githubOctokit 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_runwebhooks 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
AgentAiport; 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.