Agent Node
The Agent Node defines an LLM call. It's where you pick the model, set the parameters, scaffold the conversation pre-history, and decide how the agent's identity ties back to /org.
This is the most-configurable node in Keen. The behaviour also depends on how it's called — mainline (normal) vs as a tool (__callContext = 'tool') — and on whether the surrounding flow is parallelized.
Visual
The node renders with an atom-like icon, labelled "Agent". The empty-state subtitle reads "Configure agent".
Two dots: IN (top) and OUT (bottom). No error edge — failures route through the parent flow's standard mechanisms.
Three tabs in the settings panel
Click the node to open the Agent panel. Three tabs in the left rail:
- Prompts — pre-history (system / user / assistant turns)
- Settings — identity, LLM, parameters, files, skip flags
- Chat — preview / test surface (see below)
A Save Configuration button at the bottom of each tab persists changes.
Settings tab
The bulk of the config lives here.
Identity fields
| Field | Purpose |
|---|---|
| Agent Name | Visual label only — tells you (the developer) which agent this node represents on the canvas. Has no runtime meaning. |
| Agent ID | The string that ties this node to its responses + prompts in the per-flow stack. Must match the Agent ID in /org/agents/<id> for the main agent of the flow. Sub-agents (additional Agent Nodes that aren't the user-facing one) can use any ID. |
| Session ID | Optional. Correlation ID for the prompt/response lookup. See Session ID and Request ID. |
| Request ID | Optional. Sub-correlation for the same lookup. See Session ID and Request ID. |
Both Session ID and Request ID accept template values like ${session.sessionID} or ${dictionary.requestID} — resolved at runtime against session / dictionary.
LLM Selection
A picker showing the current selection (e.g. "google / gemini-2.5-pro") with an Edit link to change it. Selecting an LLM populates company, model, and modelId on the underlying node JSON.
Model Parameters
Three sliders / inputs:
| Param | Default range | Effect |
|---|---|---|
| Temperature | 0–1 (e.g. 0.8) | Creativity / randomness. Higher = more variability. |
| Top P | 0–1 (e.g. 0.8) | Nucleus sampling cutoff. |
| Max Tokens | int (e.g. 65536) | Hard cap on output length. |
File Paths (Optional)
A text area — one path per line. Files attached here become part of the pre-history available to the LLM call.
Lower-priority feature for most flows. Use it when an agent needs persistent reference material (PDFs, structured docs) baked into its conversation.
Skip flags (bottom of the panel)
Two checkboxes:
| Flag | Effect when checked |
|---|---|
| Skip Internal Database Interactions | The agent's response and chat history are not saved to the DB. Each call starts fresh — new context plus the pre-history. "One-time agent." |
| Skip Internal Tooling | The agent cannot call tools, even if tools are registered. The tool-call wire format won't be injected into its system prompt. |
Prompts tab
Defines the pre-history — the scripted system / user / assistant turns the LLM sees before any live messages.
Conversation Roles + Conversation Prompts
The left column lists turns by role: System (top), then a sequence of User / Assistant turns you add via the + Add Turn button.
Click a turn to edit its content in the right panel:
- "Write the conversation turn." placeholder
- Delete Prompt option per turn
Each turn is one role + one piece of content. Order matters — turns run from top to bottom.
Role grouping (important runtime detail)
The engine collapses consecutive same-role turns into one at runtime. So if you author:
system, assistant, assistant, assistant, user, user, assistant, user, user
…the LLM actually sees:
system, assistant, user, assistant, user
The three sibling assistant turns are joined into one assistant message; the two consecutive users are joined into one user message; etc. This matches what most LLM APIs require (alternating roles after the system prompt).
Plan your pre-history with this in mind. If you want three separate assistant points, intersperse with a user turn between them — otherwise they're concatenated.
Chat tab
The pre-prompt chat history for this agent — the conversation memory it carries forward, like every LLM has.
The Prompts tab defines the scripted turns the agent always sees (system instructions + scripted opener turns). The Chat tab holds the dynamic running conversation — built up over real interactions and replayed back to the model on every request.
So each call to the LLM looks like:
[Prompts tab — scripted system / user / assistant turns]
+
[Chat tab — accumulated chat history]
+
[the latest live prompt]
In normal operation this history grows as the agent and user converse. When the Skip Internal Database Interactions flag is checked, the response and history are not persisted — so the Chat tab effectively starts fresh each call (Prompts + the latest live prompt only). Useful for stateless / one-time agents where carrying history would just be noise.
How an Agent Node executes — the prompt/response stack
The runtime model behind the node is straightforward but worth understanding.
How prompts and responses are stored
Prompts are stored grouped by agentID. Each agent has an array of prompt objects:
type AgentPrompt = {
agentID: string;
sessionID: string;
requestID: string;
prompt: string;
timestamp: number;
agentChat?: boolean;
role: LLMEngine.PromptRole;
files?: SocketFileData[];
filePaths?: LLMEngine.FilePath[];
images?: LLMEngine.ImagePath[];
};
// Storage shape:
{
'my-agent': [AgentPrompt, AgentPrompt, ...],
'next-agent': [AgentPrompt, AgentPrompt, ...],
// …one bucket per agentID
}
Responses are stored as a single flat collection — every response from every agent in chronological order:
type AgentResponse = {
sessionID: string;
requestID: string;
agentID: string;
agentName: string;
response: string;
timestamp: number;
};
// Storage shape:
agentCollection = [AgentResponse, AgentResponse, AgentResponse, ...];
The Agent API (in Script Nodes) reads from these stores: getAgentLastResponse(agentID), getAgentLastPrompt(agentID), etc.
Normal mode (not a tool call)
- The engine looks for the latest prompt for this Agent ID — sorted by timestamp, narrowed by whichever IDs are configured on the node (see Lookup precedence below).
- The agent answers that prompt.
- The response is appended to the agent response collection with the current timestamp. From now on it's the latest response for this agent ID.
There can be thousands of prompts and responses for one agent ID across the request — the agent always answers the most recent prompt and writes the most recent response, by timestamp.
The "same agent ID, distinct prompts" constraint
For one Agent ID, two prompt or response objects cannot share the same Session ID and Request ID — context would collide. At least one of (sessionID, requestID) must differ between any two prompts/responses for the same agent.
Session ID and Request ID — what they're for
| ID | What it correlates |
|---|---|
| Session ID | The chat-level conversation. A new sessionID = the user typed a new message in chat. All internal traffic for that user message binds to this. |
| Request ID | A sub-correlation inside one session. Necessary when you call the same agent many times in parallel — you need a per-call ID so each parallel run targets the right prompt/response. |
The canonical scenario for Request ID: you call one agent 1000 times with Parallel Context. All 1000 share the same agentID and sessionID. You must generate a unique requestID per item and provide it via the context — then on the Agent Node set Request ID = ${dictionary.requestID}. Each parallel run binds to its own prompt/response pair instead of fighting over a shared one.
Lookup precedence
The engine fetches prompts based on which IDs you've set on the node:
| Set on the node | Engine fetches |
|---|---|
sessionID only | All prompts for agentID + sessionID → picks latest by timestamp. |
requestID only | All prompts for agentID + requestID → picks latest by timestamp. |
agentID only (default) | All prompts for agentID → picks latest by timestamp. |
sessionID + requestID | Looks for the exact prompt matching both → returns it or null. |
The sessionID + requestID combo is how you target a specific prompt object precisely — useful inside parallel branches.
__callContext = 'tool' — where the response goes
The agent's response goes to one of two places, decided by dictionary.__callContext:
dictionary.__callContext | Response lands in |
|---|---|
'tool' | dictionary.result (a string — the LLM-generated text) |
| anything else (or unset) | The agent response collection (the default mechanism — same place setAgentResponse writes to and getAgentLastResponse reads from) |
That's the only thing __callContext controls. It does NOT change how prompts are looked up, whether the agent runs, or what the response itself contains — only where the answer is delivered.
Nomenclature note: the agent's answer (and a flow tool's return) lands in
dictionary.resultwhen__callContext = 'tool'. Some older notes called thisdictionary.response; that key is not the contract — under Parallel Context fan-out it made agents collide on a shared key. Usedictionary.result.
Engine auto-injection: Parallel Context sets __callContext: 'tool' on every parallel run by default, so the parallel-run's agent response lands in that run's dictionary.result. The parent flow can then map / filter / join the array of returned dictionaries via a Script Node.
For Parallel (with Parallel Flow branches), __callContext is not auto-injected — set it manually with an Assignment Node at the top of each branch if you want the same semantic.
How this connects to tool-calling
The same dictionary.result field is also the contract a tool uses to return its result to the agent — the tool sets dictionary.result = '<text>' inside its isolated dictionary scope, and the engine wraps it as <SYSTEM RESPONSE>...</SYSTEM RESPONSE> for the agent's next turn. See TOOLING / How tool results come back.
Calling Tools
When Skip Internal Tooling is OFF and tools are registered, the agent can emit a <SYSTEM CALL> block to invoke a tool mid-conversation.
The mechanism (full details in Tooling):
- Agent emits a
<SYSTEM CALL>block instead of a normal response. - Engine intercepts the block — it doesn't go to the chat.
- Engine runs the tool (script or flow) named in
!*tool. - Engine sets a new user prompt for this agent containing the tool's result.
- Cycle 2 fires — the agent now sees:
prev question + system-call + tool resultand writes a real response. - That final response goes to the response stack (or
dictionary.resultif__callContext = 'tool').
The tool-call wire format is injected into the agent's system prompt automatically (when tools are available and Skip Internal Tooling is off). See TOOLING for the canonical prompt and the script-vs-flow distinction.
The "fake response" trick
Sometimes you want a flow to end with the agent appearing to respond — but you DON'T need a real LLM call because the answer is already constructed. Example: at the end of a flow you've concatenated some results in a Script Node and want THAT to be the user-visible answer.
The trick:
- Define the agent in
/orgas the user-facing one. - DON'T put a corresponding Agent Node in the flow.
- In a Script Node, use
AgentMgr.setAgentResponse(...)to set a response under that Agent ID directly. - When the flow ends, the engine looks for the latest response for the org-defined agent → finds the one you set → returns it to the chat.
If no response exists for that agent ID, the chat shows "there is no response from the agent X" — so this trick requires the Script Node setting the response to actually run before the flow ends.
The "fake prompt" trick — answering the last pre-history user turn
When you want the Agent Node to answer the last user turn from its pre-history instead of a prompt object from the stack:
- Set
Session IDandRequest IDto dummy values (e.g.XXXandYYY) that don't match any existing prompt. - The prompt-stack lookup returns
null(no matching object found). - The engine falls back to answering the last
userturn from the Prompts tab (typically authored as${dictionary.prompt}or similar so it pulls live data).
Where the answer goes is controlled by __callContext like any other Agent Node — dictionary.result if __callContext = 'tool', otherwise the agent response collection.
This is a common pattern for tool-style agent nodes that should respond to whatever's currently in dictionary.prompt without depending on the upstream having seeded a real prompt object. Pair the dummy IDs + the templated last user turn + __callContext: 'tool' and you have a self-contained "answer this question" node.
Common patterns
One main agent + many sub-agents
The user-facing agent matches an Agent ID in /org. You also drop additional Agent Nodes with arbitrary IDs that aren't in /org — these are internal helpers (classifiers, planners, summarizers). Their responses live in the stack but aren't returned to the chat at the end.
Same agent, parallel calls (e.g. summarize 100 docs)
Use Parallel Context. For each item, write a unique requestID into the context. Set Agent Node's Session ID = ${session.sessionID} and Request ID = ${dictionary.requestID}. Each parallel run resolves to its own prompt/response pair; results land in dictionary.result (since __callContext: 'tool' is auto-set in Parallel Context).
Tool-using agent
Leave Skip Internal Tooling off. Register tools in your project. Configure pre-history that explains what tools are available (see Tooling for the prompt template). The agent decides when to call them.
One-time stateless agent
Check Skip Internal Database Interactions. Each call starts fresh — useful for classifiers or stateless transformations where session memory would just be noise.
Execution and streaming
The Agent Node is authored and wired today — the model, prompts, tools, identity, and response routing are all expressible. The agent's prompts and responses live in the per-request agent-state store (the prompt/response stacks the Agent API reads, scoped to the connection and channel-aware), and the LLM turns are orchestrated through that store plus the tool-dispatch loop. Token-level streaming to the chat is wired through the relay (the same token stream the client already consumes) and surfaced to scripts via the writeAgentStream / writeAgentProgress helpers on system/chat.
Related
- TOOLING — how tools work, the wire format, retries, errors.
- Agent API in Script Nodes —
AgentMgr.getAgentLastResponse,setAgentResponse,getAgentLastPrompt, etc. - Parallel Context — the canonical parallel-agent-call pattern.
- Parallel Node / Parallel Flow — fixed-set parallel agents.
- Organization → Agents — the
/orgside of an agent's identity.
Still TBD
-
File Paths— file format expectations, max size, whether they're sent as attachments to the LLM API or rendered as text into the prompt.