Skip to content

feat(kernel): agent graphs — profiles as nodes, optimizable directives as edges (subsumes the driver/loop/supervisor/multishot families) #694

Description

@drewstone

One sentence

Nodes are AgentProfiles, edges are typed and carry optimizable prompt directives, a loop is just a cyclic graph — and every existing orchestration family (driverAgent, supervisorAgent, runLoop/loopDispatch, agent-eval's AgentDriver + multishot, VB's hand-rolled shot loop) becomes an instance of this one primitive instead of a sibling implementation.

Why now — measured, not aesthetic

A real 8-round driven run (VB, arc-smart-wallet-product, glm-5.2, 2026-08-01) produced the evidence:

  • Rounds 5–7 received a 241-char generic fallback instead of steering (rounds 1–4: ~1,700 chars). The anti-Goodhart filter stripped the driver's instruction and silently substituted boilerplate. In graph terms: the analysis edge worked (the driver's diagnosis was sharp — "the worker is stuck in a self-verification loop"), the delegation edge was severed, and nothing observed the edge. Half the run was unsteered and no artifact said so.
  • The driver was structurally incapable of investigating: a single callLlmJson, no tools, no turns, one-sentence system prompt — while driverAgent (with observe_agent, run_analyst, spawn-by-profile, conserved budgets) sat unused in the installed runtime.
  • The driver's prompt was assembled by functions (buildWorkerDriverSystemPrompt et al.), so the only GEPA-optimizable surface was a one-sentence registry file while ~200 lines of doctrine sat hardcoded in TypeScript. A role expressed as code is a role that can never improve.

The primitive

type NodeId = string

interface GraphNode {
  id: NodeId
  profile: AgentProfile          // the ONLY way a node is described. No role-builder functions.
}

type GraphEdge =
  // Work flows down. The delegation directive is DATA -> versionable, sweepable, optimizable.
  | { kind: 'delegates'; from: NodeId; to: NodeId; directive: PromptHandle }
  // Findings flow anywhere: an analyst lens over N nodes' traces, delivered to ONE node,
  // wrapped in a directive telling the recipient what to do with the analysis.
  | { kind: 'analyzes'; analyst: string; over: NodeId[]; to: NodeId; directive: PromptHandle }

interface AgentGraph {
  nodes: GraphNode[]
  edges: GraphEdge[]
  deliverable: DeliverableSpec   // termination is mandatory, not optional
  budget: Budget                 // conserved pool across the whole graph
}

runGraph(graph, opts): Promise<GraphResult>

Driver↔worker is the two-node cyclic instance. "Agent 3 analyzes outputs of 1 and 2 and reports to 1 only" is one edge, not a framework:

{ kind: 'analyzes', analyst: 'convergence', over: ['w1', 'w2'], to: 'driver', directive: h('convergence-report/v0') }

Acceptance test for the whole design: an agent can author a working topology as plain data in ≤20 LOC (code-mode teachable). If a scheduler, DAG validator, builder API, or DSL appears, we have rebuilt ADC's workflow engine — which stays on the paid platform and is explicitly not this.

Semantics that are load-bearing (each one paid for in incidents)

  1. Edge delivery is observable. Every traversal appends to an edge ledger: delivered | stripped | fell-back | empty, with byte counts. The 241-char incident is the motivating case — an unobservable edge cannot be trusted and its directive cannot be optimized. (Generalizes VB's new steering-integrity.jsonl.)
  2. Oracles are environment, not nodes. Graders/verifiers must not be addressable in the graph — an edge to them leaks the rubric (VB's rubric leak needed a 5-site chokepoint redaction precisely because the grader's vocabulary reached the driver). "Everything is a profile" has exactly one exception and this is it. Analysts may read the environment's verdicts; nodes may not read the environment's internals.
  3. Conserved budget or no cycles. A cyclic graph without conserved-pool semantics never terminates. Keep driverAgent's existing model: per-child reservation from one pool (perWorker, maxLiveWorkers), deliverable as the only success exit, loopUntil semantics for bounded retry.
  4. Directives live in the prompt registry (prompts/<surface>/v<n>.md pattern), one surface per edge. That is what makes every edge a GEPA/DSPy optimization target with the downstream node's outcome delta as feedback — createDspyRlmTraceEngine (agent-eval ≥0.140) is the intended engine. The GEPA→DSPy migration must be finished first (see deletions).

What this subsumes — the deletion/absorption ledger

At least four sibling orchestration families exist across two packages plus VB. Post-graph, each is either an instance (thin wrapper allowed during migration) or deleted:

agent-runtime/kernel (245 exports today):

  • driverAgent → the star-topology graph instance. Its analyzeOnSettle hardwires findings→spawning-driver; generalizing that destination to to: NodeId + directive IS the analyzes-edge. Keep the internals, retire the bespoke surface.
  • supervisorAgent, supervise, superviseSurface, createSupervisor → a supervisor is a driver profile with a different directive. Candidate for instance-of-graph; audit for anything genuinely unique (span recording stays).
  • runLoop, loopUntil, loopDispatch, loopCampaignDispatch, routerToolLoop — five loop entry points. runGraph + deliverable should leave at most runLoop (the executor) + loopUntil (the combinator); the dispatch pair looks like campaign-layer sugar that belongs beside campaigns, not in kernel.
  • dumbDriver, naiveDriver → these are directives (information-poor steering policies), not drivers. Re-express as registry prompts on a delegates-edge; delete the functions. (VB independently duplicates them as buildDumbContinuationPrompt/buildNaiveContinuationPrompt — same knowledge, third copy.)
  • runTree, pipeline, replaySpawnTree, worktreeFanout → audit: tree = acyclic graph; if runTree is not expressible as runGraph we have two graph runtimes.

agent-eval:

  • AgentDriver (a SECOND driver, persona-plays-a-user) → a driver profile whose directive is the persona. Instance, then delete the class.
  • buildDriverSystemPrompt, buildWorkerDriverSystemPrompt, decideNextUserTurndelete the functions, keep what they know: the harness capability brief and the "never write a thin steer" contract become seed data in the initial directive prompts, which GEPA then optimizes away from. Role-as-function is the anti-pattern this issue exists to end.
  • ./multishot (runMultishot, runMultishotMatrix, MultishotDriverEmptyError, computeCellComposite, runJudge) → a third loop family, added while VB forked its own. The matrix/judge/composite parts are eval-layer and stay; the loop should be a graph instance. MultishotDriverEmptyError becomes an edge-ledger empty event.
  • PairwiseSteeringOptimizer + the GEPA remnants → confirm createDspyRlmTraceEngine fully supersedes; delete what it replaced (AxGepaSteeringOptimizer is already gone in 0.140.1; VB's gepa-reflective-proposer.ts targets the deleted class and is dead code walking).

VB (blueprint-agent scripts/experiments) — the proof-of-subsumption consumer:

  • lib/sandbox-driver/shot-loop.ts + lib/shot-reviewer.ts + shot-reviewer/ (~2,000+ lines) → two profiles + a 2-node graph. The doctrine in context.ts (200 hardcoded lines) becomes the driver directive's seed prompt. VB keeps ONLY: realness gate, search arms, verticals (per its own charter §5/§6).
  • VB_USE_RUNTIME_CHAT half-migration (transport only, default off) → subsumed by the above.

Ship plan

  • P0 — spec + skeleton: AgentGraph types, runGraph executing the 2-node cyclic case by delegating to driverAgent internals; edge ledger; directives resolved via prompt registry. No deletions yet.
  • P1 — proof on a real cell: VB's arc-smart-wallet-product run replayed as a graph (same worker profile, driver doctrine as directive seed) side-by-side vs the legacy shot loop. Replacement is measured, not asserted: pass-rate, realness, edge-ledger completeness, tokens.
  • P2 — analysts as edges: generalize analyzeOnSettle destination; route a real TraceAnalyst's findings to a named node with a directive; edge-directive GEPA/DSPy pass on the driver edge.
  • P3 — absorption: supervisor/multishot/AgentDriver become instances; deletion ledger executed package by package with a discriminating test per deletion (a green suite that cannot tell the old path is gone is not evidence — proven twice this week).

Open questions

  1. runTree vs runGraph — one runtime or two? (Audit pending.)
  2. Where does the edge ledger persist — per-run artifacts (VB pattern) or the OTLP trace itself (agent-trace-contract now has span links with agent.link.kind; steered_by is already specced)? Leaning OTLP: one trace carries topology + delivery.
  3. Budget semantics for analyzes edges (analyst spend attribution — the analyzing node, the observed node, or the graph?).
  4. Does DeliverableSpec suffice as the only termination, or do cyclic graphs need per-edge traversal caps as a backstop? (Leaning: yes, cap per edge, fail loud.)

Living issue — updated as the VB substrate-reconciliation survey and the kernel audit land. Evidence trail: VB run artifacts oc-glm52@generic-arc-smart-wallet-product-r0 (prompt-shot-N.txt, reviewer-memory.jsonl, steering-integrity work), agent-trace-contract@1.0.2 span-link semantics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions