Wallfacer embeds the topos runtime SDK (latere.ai/x/topos) as an in-process multi-agent execution engine. A topos run compiles a set of agents into a topos.Region, executes it inside the wallfacer server process, and returns a final text plus a lineage graph of which agent did what and who delegated to whom. This is the execution substrate behind the Agent Graph page (/agent-graph), agentic flows, and the native topos harness.
The integration is deliberately experimental and opt-in. The built-in implement flow does not run through it: an ordinary task keeps the multi-turn subprocess turn loop documented in Task Lifecycle. Only a flow explicitly marked agentic, or a task explicitly pinned to the topos harness, reaches this runtime. Current constraints: static API-key credentials only (Bearer/OAuth deferred), transparent fallback to a deterministic fake model when no credential is configured, no session resume, no MCP, and no commit/verification parity with the subprocess harnesses on the native path. The milestone history lives in specs/local/topos-runtime-integration.md.
internal/agentgraph is the only wallfacer package that imports topos. Everything the rest of the codebase consumes is a topos-free mirror type defined in the seam:
| Seam type | Mirrors | Purpose |
|---|---|---|
agentgraph.Runner |
topos.Runner |
Run(ctx, region, task) executes a region and returns the result |
agentgraph.Result |
topos.RunResult |
Final text + Lineage graph |
agentgraph.Lineage / Node / Edge |
topos.Lineage |
Renderable run graph; marshals to the same JSON shape |
agentgraph.TraceEvent |
topos.Event |
One live observation (assistant text, delegation, tool use); PayloadJSON stays opaque |
agentgraph.ModelConfig / ModelMode |
topos.ModelOptions / ModelKind |
Host-side model selection; mapped in exactly one function, modelOptions |
Only the curated root package latere.ai/x/topos is a supported surface. TestWallfacerImportsOnlyRootTopos (internal/agentgraph/boundary_test.go) runs go list over every package and fails if any wallfacer package imports a topos engine subpackage (latere.ai/x/topos/...). This keeps the runtime an implementation detail: the engine can restructure internally without touching wallfacer, and no second seam can grow by accident.
Consumers on the wallfacer side read the seam's exported entrypoints: RunFlowWithModel (multi-agent), RunAgent (single agent), RunFlowFake (explicit fake-model entrypoint for tests), and the assertion helpers ModelOptions / RunOptions that expose the option mapping without running a model.
ModelMode (internal/agentgraph/model.go) selects how a run reaches a model:
ModelModeFake("", the zero value): the deterministic, network-free fake model. Any unconfigured or credential-less config lands here.ModelModeLux("lux"): a provider reached through Lux, the model gateway.BaseURLpoints at a Lux endpoint andAPIKeyis a Lux virtual key (lux_*).ModelModeDirect("direct"): a provider endpoint reached directly with a BYO key.
The runner derives the config in Runner.agenticModelConfig (internal/runner/agentic.go) from the same .env file the subprocess harnesses read:
- No env file, unparseable env file, or no
ANTHROPIC_API_KEY: zeroModelConfig, which maps to the fake model. Tests and no-credential development keep working. ANTHROPIC_API_KEYset andANTHROPIC_BASE_URLset:ModelModeLuxthrough the gateway.ANTHROPIC_API_KEYset, no base URL:ModelModeDirectagainst the provider.
CLAUDE_DEFAULT_MODEL supplies the model id; the provider is always anthropic today. The fake fallback is centralized in modelOptions: a real-mode config missing a credential also degrades to fake rather than building a guaranteed-401 adapter, so callers never pre-check.
Only the static x-api-key credential is wired. Bearer-style credentials (ANTHROPIC_AUTH_TOKEN gateway tokens, CLAUDE_CODE_OAUTH_TOKEN) require a per-call BearerSource and are deferred; a Claude subscription login therefore does not light up the agent-graph runtime, only a raw API key or Lux key does.
FromFlow (internal/agentgraph/adapter.go) compiles a wallfacer flow.Flow plus the agents registry into a topos.Region. The flow's first step becomes the region entry, the remaining steps the ordered peer chain. Each step's AgentSlug resolves through the registry into an agents.Role and maps onto a topos.AgentSpec: the slug is the stable identity (lineage node ids are <session>/<slug>), PromptTmpl becomes the system prompt, and Capabilities become permission scopes. Built-in roles leave PromptTmpl empty (they render through the prompts package); an empty system prompt is legal for the fake model and the headless path.
Three flow fields shape the region's autonomy (internal/flow/flow.go):
Agenticroutes the flow through this runtime at all. Default false keeps every existing flow on the legacy engine.Dynamicopts an agentic flow into model-driven delegation (topos.Dynamic): the entry agent gets a delegate tool over the peer directory and the model decides handoffs. Default is the deterministic pinned chain (topos.Pinned), whereOptional/RunInParallelWithhints are ignored.Topologygates who may delegate on a dynamic flow:orchestrator-worker(default, only the entry agent delegates) ormesh(any agent delegates recursively, bounded byMaxHandoffDepth; zero passes through and topos applies its default of 3).
toTopology in the adapter is the only place that names the topos topology constants, and runOptions the only place that names topos.Options.
Runner.Run (internal/runner/execute.go) dispatches a task in priority order:
- Agentic flow. After resolving the task's flow slug, if
r.flowBySlug(flowSlug)returns a flow withAgentic == true, the task runs throughrunAgenticFlow(internal/runner/agentic.go), which callsagentgraph.RunFlowWithModelwith the compiled region. This branch runs before the flow-engine branch: an agentic flow has a non-implementslug and would otherwise be swallowed by the legacy engine path. - Legacy flow engine. Non-
implement, non-agentic flows walk the linear flow engine (subprocess agents). Unrelated to topos. - Native topos harness. An
implement-path task whose resolved harness is in-process (harness.InProcess(r.sandboxForTask(task)), true only fortopos) runs throughrunNativeTopos: a single agent as a one-node pinned region viaagentgraph.RunAgent. Test runs (task.IsTestRun) are excluded and keep the subprocess verification path. The dispatch happens after worktree setup sotopos.Options.Workdircan point at the task's real worktree (firstWorktreePath); an empty worktree falls back to the topos temp-dir sandbox. The periodic oversight worker is skipped for native runs, which produce their own live trace and never enter the turn loop. - Turn loop. Everything else, including the built-in
implementflow, stays on the subprocess turn loop.
Because flow/builtins.go does not set Agentic on implement, and harness.Default() is still Claude (internal/harness/registry.go), the runtime today only executes user-authored agentic flows and tasks explicitly pinned to the topos harness. Flipping the default harness to topos is the direction tracked in the topos-native-harness spec; commit and verification parity are the prerequisites, since runNativeTopos does not yet make a durable git commit or run a verification pass.
Both paths converge on driveToposRun (internal/runner/agentic.go), which:
- applies the task timeout (
task.Timeoutminutes, falling back toconstants.DefaultTaskTimeout); - forwards live trace events onto the task timeline. The topos observer is called synchronously on the run's goroutines, so it must not block: events are pushed into a 256-slot buffered channel and drained into the store by a separate goroutine, dropping on overflow rather than backpressuring the run;
- on error, respects an already-cancelled task, classifies the failure (
classifyFailure), attemptstryAutoRetry, and otherwise fails the task with an error event; - on success, persists the final text (
UpdateTaskResultwith stop reasonend_turn) and the JSON-marshalled lineage (UpdateTaskLineage) before transitioning, so the durable record is complete the moment the task reaches done; - walks
in_progress -> waiting -> committing -> donewith a state-change event per hop. The state machine forbids a directin_progress -> donetransition, so this mirrors the flow-engine path.
store.Task.Lineage (internal/store/models.go) holds the JSON-marshalled lineage as an opaque *string, so the store never depends on topos types. Nil for every non-agentic task.
GET /api/tasks/{id}/lineage (internal/handler/tasks_lineage.go, TaskLineage) reparses the stored string into the thin frontend shape: nodes (id, name, role, status running|done|failed, grants, sandbox) and edges (from, to, kind delegate|deliver|next). The stored JSON uses capitalised keys with no tags; json.Unmarshal matches case-insensitively, so it binds directly to the lowercase wire fields. A task with no lineage returns 200 with empty arrays, never null, so the client renders nothing without special casing. AgentLineage.vue draws the graph in the task detail modal.
agenticTraceEvent (internal/runner/agentic.go) maps a TraceEvent onto a task-timeline event, returning ok=false for anything that should not surface (lifecycle bookkeeping, empty payloads). Exactly three topos event names surface today:
| Topos event | kind |
Rendered line |
|---|---|---|
AssistantMessage |
assistant |
<agent>: <text> (dropped when text is empty) |
SubagentStart |
delegate |
delegated to <agent> |
PostToolUse |
tool |
<agent> used <tool> (dropped when the tool name is missing) |
Each surfaces as a store.EventTypeSystem event whose data carries result (the human-readable line, so the events tab reads naturally), source: "agentgraph" (marks it as an agent-graph trace), kind, node (the lineage node id, the join key back to graph nodes), agent, and text. The agent label prefers AgentID and falls back to Node. This is what makes an agent-graph run visible as it proceeds, not only as a lineage graph at the end.
internal/harness/topos.go registers topos as a full registry citizen so the config UI selector, default resolution, and per-task pinning treat it uniformly with the five CLI harnesses. Its execution is not subprocess-shaped, so every subprocess-facing method is a guard:
harness.InProcess(id)reports whether an id runs in-process; the runner consults it to choose the agent-graph path overBuildArgv+ the executor. Today onlyToposqualifies.BuildArgvreturnsErrInProcess; any caller that reaches it has routed an in-process harness down the subprocess path by mistake.ParseEventreturnsKindUnknown(events come from the topos observer, not NDJSON stdout), recording rather than crashing on an accidental subprocess-path call.AuthEnvreturns an empty map: credentials resolve in-process through the model config, not via subprocess env injection.CapabilitiesreportsSupportsSystemPromptandEmitsUsageonly. No resume, no MCP: a native topos task cannot continue a previous session, and MCP servers configured for CLI harnesses do not apply.
The Agent Graph page defines agents and composes graphs against two CRUD surfaces:
/api/agents(internal/handler/agents.go): the merged built-in + user-authored agents registry (preferring the runner's registry so the handler sees exactly the catalog the dispatcher executes against).AgentResponseintentionally hides runner plumbing: an earlier shape leakedmount_mode/single_turn/activity; the stable vocabulary iscapabilitiesplus advisorymultiturn.harnesscarries a per-agent harness pin. List responses include the inlineprompt_tmplonly for user-authored agents; built-in prompt bodies render on demand via the per-agent GET./api/flows(internal/handler/flows.go): the merged flow registry.FlowResponseresolves each step'sagent_nameserver-side and carries the agentic execution fields (agentic,dynamic,topology,max_handoff_depth) withomitempty, so ordinary flows serialize byte-identically to their pre-topos shape.
A flow created here with agentic: true is the only way to route a board task through the multi-agent runtime; a routine can then spawn it on a schedule via RoutineSpawnFlow.