Agent Architecture
Nodus runs an LLM agent against the graph. This page describes the system it
sits in, the loop it executes, and the three memories that feed its prompt.
The agentic system
flowchart TB
subgraph UI["Agent panel (canvas left edge)"]
Prompt[Prompt input]
Transcript[Chat transcript]
Log[Activity log<br/>errors and diagnostics]
Tasks[Task list]
end
subgraph Runner["Agent runner"]
Mode{Mode<br/>explore / plan / execute}
Loop[Iteration loop]
Parse[Tool-call extraction<br/>native + text fallbacks]
end
subgraph Context["Prompt context"]
Sys[System prompt]
Mem[Session / Stack / Facts memory]
Nodes[Selected nodes,<br/>or the whole filtered graph]
Hist[Last 3 exchanges]
end
subgraph Exec["Execution"]
Queue[LLM queue<br/>serialised, cancellable, retrying]
Provider[Provider adapter<br/>Ollama / OpenAI / Anthropic / compatible]
Tools[Tool executor]
end
subgraph State["Application state"]
Stores[Pinia stores<br/>nodes, edges, frames, storylines]
DB[(SQLite)]
Files[(Markdown vault)]
end
Prompt --> Runner
Mode --> Loop
Context --> Queue
Loop --> Queue
Queue --> Provider
Provider --> Parse
Parse --> Loop
Loop --> Tools
Tools --> Stores
Stores --> DB
Stores --> Files
Tools -->|result| Loop
Loop --> Transcript
Loop --> Log
Loop --> Tasks
Mem --> Sys
Sys --> Context
Stores --> Nodes
MCP[External agents<br/>via MCP server] --> Stores
The same capabilities are reachable from both the in-app agent and the MCP
server; a gate test fails when one surface gains a tool, or a field of a
shared tool, that the other lacks.
The execution loop
flowchart TB
Start([User sends a prompt]) --> Enhance[Enhance prompt<br/>+ push user turn to transcript]
Enhance --> Build[Build messages:<br/>system prompt + memory + graph context<br/>+ last 3 exchanges]
Build --> Pin[Pin those messages<br/>so pruning cannot drop them]
Pin --> Iter{Iteration < mode cap?<br/>explore/plan 200, execute 500}
Iter -->|no| Stop([Stop: cap reached])
Iter -->|yes| Prune{Every 10th<br/>iteration?}
Prune -->|yes| DoPrune[Prune to the last 6 messages<br/>keeping pinned ones]
Prune -->|no| Call
DoPrune --> Call[Queue the request<br/>tools filtered by mode]
Call --> Provider[Provider call<br/>retry with backoff]
Provider -->|token limit| Limit([Stop: context too large<br/>reported in transcript and log])
Provider --> Reply{Reply shape?}
Reply -->|native tool_calls| Run[Execute each tool]
Reply -->|text containing<br/>JSON fence, python_tag,<br/>channel marker or raw JSON| Extract[Extract the embedded call]
Extract --> Run
Reply -->|plain text| Say[Append the answer<br/>to the transcript in full]
Say --> Iter
Run --> Record[Record the tool in the turn's<br/>action list; append result]
Record --> Marker{Result marker?}
Marker -->|AGENT_DONE| Done([Done: summary becomes<br/>the assistant turn])
Marker -->|CREATE_PLAN| Plan[Plan created] --> Iter
Marker -->|approval requested| Pause[Pause, save the iteration]
Marker -->|none| Iter
Pause --> Decide{User decides}
Decide -->|approve| Resume[Resume from the saved iteration] --> Iter
Decide -->|reject| Revise[Feed the rejection back] --> Iter
Err[Any error] --> Fail([Fail: message closes the<br/>current turn, log opens])
Loop details
| Concern |
Behavior |
| Modes |
explore (read-only), plan (design for approval), execute (mutate). The mode filters which tools are offered, so a read-only run cannot write |
| Iteration cap |
200 for explore and plan, 500 for execute; reaching it stops the run rather than looping forever |
| Serialisation |
Every model call goes through one queue, so concurrent runs cannot interleave writes; the queue is cancellable, which is what the stop button uses |
| Retry |
Transient provider failures retry with backoff; a context-length error is not retried but reported, since repeating it cannot help |
| Context pruning |
Every 10th iteration the message list is pruned to the last 6, keeping the pinned prompt and graph context |
| Tool-call fallbacks |
Models without native tool calling emit calls inside text; the runner extracts them from JSON fences, python_tag and channel markers, or a bare JSON object |
| Generation guard |
Each run increments a generation counter, so a late reply from a cancelled run cannot mutate the graph |
| Visible output |
Answers land in the transcript in full; tool names collapse into a per-turn action list; errors open the log panel |
The three memories
The system prompt is assembled from three memories with different lifetimes.
flowchart TB
subgraph Memory
Session[Session<br/>goal, progress, steps]
Stack[Stack<br/>LIFO task queue]
Facts[Facts<br/>long-term knowledge]
end
Session --> SP[System Prompt]
Stack --> SP
Facts --> SP
SP --> Agent[Agent Runner]
Session Memory
Tracks current goal and progress. Cleared on completion.
| Field |
Description |
goal |
What the user asked for |
progress |
0-100% |
completed |
Actions done |
current_step |
Current work |
next_steps |
Upcoming work |
blockers |
Issues |
Stack Memory
LIFO todo queue. Persists across refresh.
| Field |
Description |
id |
Unique identifier |
description |
Task description |
priority |
high/medium/low |
context |
Optional data |
Facts Memory
Long-term knowledge (up to 50 per workspace).
Session
| Tool |
Parameters |
set_goal |
goal, steps? |
update_progress |
progress, completed_action? |
complete_goal |
summary |
Stack
| Tool |
Parameters |
push_task |
description, priority?, context? |
pop_task |
- |
peek_stack |
- |
clear_stack |
- |
Facts
| Tool |
Parameters |
remember |
message |
Storage Keys
| Type |
Key |
| Session |
nodus_agent_session_{workspaceId} |
| Stack |
nodus_agent_stack_{workspaceId} |
| Facts |
nodus_memories_{workspaceId} |
Files
src/llm/types.ts - Type definitions
src/lib/storage.ts - Storage functions
src/llm/tools/planningTools.ts - Tool registrations
src/canvas/composables/agent/useLLMTools.ts - Tool handlers
src/canvas/composables/agent/systemPrompt.ts - Prompt builder