Module: flow canvas — visual multi-agent orchestration
Status: Phase 0 + 1 implemented, Phase 2 in progress (Tool nodes shipped). Module
name flow. A node-graph canvas — n8n / ComfyUI style — where you drag, drop,
and wire elements to compose multi-agent orchestrations that actually run:
trigger → agents → tools → logic → output, executed on the backend with live
per-node streaming on the canvas.
What ships today: the Orchestration workflow layout (library · canvas); a
@xyflow/react canvas (packages/core/src/modules/flow/canvas/FlowCanvas.tsx,
the only file importing the engine) with a draggable node palette, a node
inspector, and a Run button; the spine node types Prompt trigger, Agent, Output, plus Tool
(Phase 2); per-flow persistence over /api/flows; and the backend graph
executor (backend/modules/flow/executor.py) that topo-walks the DAG, runs each
node, and streams flow-channel telemetry that lights up nodes/edges live. An
Agent node is one run_agent_loop — the orchestrator loop extracted in Phase 0
(backend/modules/agent/orchestrator.py) and shared with the chat turn. A Tool
node is one manifest tool call: its picker is populated from serializeManifest()
(every pane's agentTools + agent commands), and it executes through the same
permission gate + frontend relay an agent's tool call uses — so any pane's
capability is draggable onto the canvas with no extra plumbing. The relay handler is
now always-on (initAgentRelay), shared by chat turns and flow runs. Verified
end to end: a Prompt → Agent → Output flow runs against the local model, and a
Prompt → Tool → Output flow relays a real tool result downstream.
The headline bet: this is not a new agent runtime. A flow is a graph over the
seams that already exist — the agent orchestrator loop, the
agent tool surface + permission gate, the
provider layer, the /ws socket, and the opaque-blob persistence pattern from the
workspace store. An Agent node is
one orchestrator turn; a Tool node is one gated manifest tool call. The canvas
is the new part; the execution borrows everything.
Why it fits the architecture
| Flow needs… | Reuses existing… |
|---|---|
| Run an LLM step with tools | the orchestrator tool-calling loop (run_agent_turn, refactored reusable) |
| Call one tool deterministically | the frontend tool catalog + executeTool relay (tool-exec.ts) |
| Gate side effects | the permission engine + approval round-trip (_gate) |
| Talk to a model | providers.chat_stream (Ollama / LM Studio / vLLM) |
| Persist a flow | the workspace-store pattern (server stores the graph opaquely) |
| Stream progress to the UI | the shared /ws socket, new flow channel |
| Surface in the UI | a registry Panel hosted by the dockable workspace |
So the build is mostly: a canvas panel, a node registry, and a graph executor that schedules nodes and delegates each node's work to machinery that already runs.
The elements you drag (node taxonomy)
Nodes resolve against a frontend-owned node registry (mirroring how agent tools are frontend-owned and pushed in the manifest), so a plugin can contribute node types with no backend change.
- Triggers (entry points): Manual / Run-button, Prompt input, Schedule (cron), Webhook, File-change, WS event.
- Agent node: model + system prompt + a chosen subset of the live tool catalog + a permission mode. Internally one orchestrator turn. Input = context/messages, output = final answer + any structured fields it produced. This is the multi-agent primitive — wire several, each with its own role/tools/model.
- Tool node: a single deterministic call to one existing agent tool or command
(
files.read,terminal.exec,database.query,open_pane,visualizer.*…). No LLM. Goes through the same gate. The inspector renders the tool's JSON-schema params as fields, and an "Upstream output fills" selector maps the previous node's output onto one chosen param (defaulting to a param namedinput, else the sole required one, else the first) — so afiles.readnode wires the upstream value intopath, not a strayinput. The mapped field is shown disabled (