| Navigation: Root AGENTS.md | CLAUDE.md → Agent System |
Code in
@nodetool-ai/agentsfollows the canonical standards in DEVELOPMENT_STANDARDS.md — in particular, the rules for TypeScript (§1), Error handling (§18), Observability (§17), and Security/Sandboxing (§16). This document describes the architecture; the standards doc describes the rules.
The agent system (@nodetool-ai/agents) gives LLMs the ability to decompose complex objectives into steps, execute those steps with tools, and return structured results. It powers the Agent, Research Agent, and Control Agent nodes in the workflow editor, as well as the standalone Agent CLI.
Architecture Overview
Objective (user goal)
│
▼
┌── Agent ──────────────────────────────────────────────┐
│ │
│ 1. Skill resolution (load SKILL.md files) │
│ 2. Planning phase (TaskPlanner → Task with Steps)│
│ 3. Execution phase (TaskExecutor → StepExecutor) │
│ │
└────────────────────────────────────────────────────────┘
│
▼
Structured result (validated against output JSON schema)
Agent Classes
| Class | When to Use | Planning | Source |
|---|---|---|---|
| Agent | Multi-step objectives needing decomposition | Full DAG planning | packages/agents/src/agent.ts |
| SimpleAgent | Single-step tasks with known output schema | No planning | packages/agents/src/simple-agent.ts |
| AgentExecutor | Lightweight value extraction | No planning | packages/agents/src/agent-executor.ts |
All agents extend BaseAgent:
abstract class BaseAgent {
readonly name: string;
readonly objective: string;
readonly provider: BaseProvider; // LLM provider (OpenAI, Anthropic, Ollama, etc.)
readonly model: string;
readonly tools: Tool[];
abstract execute(context: ProcessingContext): AsyncGenerator<ProcessingMessage>;
abstract getResults(): unknown;
}
Planning Phase
When you use the full Agent, the first thing it does is call TaskPlanner to decompose the objective into a Task — an ordered DAG of Steps with dependency edges.
interface Task {
id: string;
title: string;
description?: string;
steps: Step[];
}
interface Step {
id: string;
instructions: string;
dependsOn: string[]; // IDs of prerequisite steps (forms a DAG)
tools?: string[]; // restrict available tools for this step
outputSchema?: string; // JSON schema for step output validation
mode?: "discover" | "process" | "aggregate";
perItemInstructions?: string; // template for fan-out processing
completed: boolean;
}
The planner sends the objective to the LLM with a create_task tool. The response is parsed, validated as a DAG (no circular dependencies), and retried up to three times on failure.
You can skip planning entirely by passing a pre-built task object to the Agent constructor.
Execution Phase
TaskExecutor walks the step DAG, respecting dependency order. For each step, it creates a StepExecutor that runs a tool-calling loop:
- Build messages — system prompt, user instructions, upstream step results
- Stream the LLM response
- Collect and execute tool calls in parallel
- Append tool results to conversation history
- Repeat until the LLM calls
finish_stepor max iterations are reached - Validate the result against the step’s output schema
Token Management
StepExecutor estimates token usage and enters a “conclusion stage” at 90% of the budget. In this stage only the finish_step tool is available, forcing the LLM to wrap up. Older messages are summarized to stay within limits.
Fan-Out Execution
Steps can use three modes for batch processing:
| Mode | Purpose | Example |
|---|---|---|
| discover | Produce a list of items | “Find all CSV files in the workspace” |
| process | Create sub-step per item (runs in parallel) | “Analyze each CSV file” |
| aggregate | Collect per-item results into final output | “Summarize all analyses” |
Memory
Every ProcessingContext carries an AgentMemory at context.memory — the single namespaced store for results shared between steps, tasks, sub-agents, and tools. There are no parallel result maps; all executors write and read through the same API.
import { memoryKeys } from "@nodetool-ai/runtime";
context.memory.set({
key: memoryKeys.task("research"),
kind: "task_result",
value: { findings: ["alpha", "beta"] },
source: "research",
title: "Research findings"
});
context.memory.getValue(memoryKeys.task("research"));
| Namespace | Helper | Used For |
|---|---|---|
step:<id> |
memoryKeys.step(id) |
Per-step results |
task:<id> |
memoryKeys.task(id) |
Per-task results |
input:<key> |
memoryKeys.input(key) |
Caller-supplied inputs and edge inputs |
shared:<key> |
memoryKeys.shared(key) |
Cross-agent communication, tool-published facts |
Access pattern — progressive disclosure via tool calls: memory contents are NOT auto-injected into prompts. The agent uses three auto-attached tools:
| Tool | Purpose |
|---|---|
memory_list |
Discover available entries (metadata only — keys, titles, kinds, byte sizes) |
memory_read |
Fetch full values for specific keys |
memory_write |
Publish a value under shared:<key> for other agents to discover |
The default execution system prompt explains these tools; the user message names only the specific upstream keys the planner declared as required for the step. Values are pulled on demand.
Multi-agent teams mirror TaskBoard task_completed events into context.memory, so sub-agents see each other’s work through memory_list.
For the full API, tool schemas, propagation flow, examples, and troubleshooting, see Agent Memory System.
Tool System
Every tool extends a single base class:
abstract class Tool {
abstract readonly name: string;
abstract readonly description: string;
abstract readonly inputSchema: Record<string, unknown>; // JSON Schema
abstract process(
context: ProcessingContext,
params: Record<string, unknown>,
): Promise<unknown>;
toProviderTool(): ProviderTool; // convert to LLM tool-call format
}
Built-In Tools
| Category | Tools | Source File |
|---|---|---|
| Step control | FinishStepTool |
finish-step-tool.ts |
| Workflow control | ControlNodeTool |
control-tool.ts |
| File system | ReadFileTool, WriteFileTool, ListDirectoryTool |
filesystem-tools.ts |
| Web | BrowserTool, ScreenshotTool |
browser-tools.ts |
| HTTP | HttpRequestTool, DownloadFileTool |
http-tools.ts |
| Search | GoogleSearchTool, GoogleNewsTool, GoogleImagesTool |
search-tools.ts |
| Code execution | RunCodeTool, MiniJSAgentTool |
code-tools.ts, js-code-tool.ts |
| Math | CalculatorTool, StatisticsTool, GeometryTool, TrigonometryTool, ConversionTool |
math-tools.ts, calculator-tool.ts |
| OpenAI | OpenAIWebSearchTool, OpenAIImageGenerationTool, OpenAITextToSpeechTool |
openai-tools.ts |
GoogleGroundedSearchTool, GoogleImageGenerationTool |
google-tools.ts |
|
| Vector DB | VecTextSearchTool, VecIndexTool, VecHybridSearchTool, and more |
vector-tools.ts |
ExtractPDFTextTool, ConvertPDFToMarkdownTool, and more |
pdf-tools.ts |
|
SearchEmailTool, ArchiveEmailTool, AddLabelToEmailTool |
email-tools.ts |
|
| Workspace | WorkspaceReadTool, WorkspaceWriteTool, WorkspaceListTool |
workspace-tools.ts |
| Assets | SaveAssetTool, ReadAssetTool |
asset-tools.ts |
| MCP | ListWorkflowsTool, RunWorkflowTool, SearchNodesTool, and more |
mcp-tools.ts |
JavaScript Sandbox
The MiniJSAgentTool and the CodeNode workflow node both run user JavaScript inside a QuickJS WebAssembly sandbox (packages/agents/src/js-sandbox.ts). QuickJS runs in its own WASM instance with a separate heap, providing a true memory/CPU boundary — unlike Node’s node:vm which shares the V8 heap.
Limits enforced:
| Limit | Value |
|---|---|
| Execution timeout | 30 s |
| Guest heap | 64 MB |
| Guest stack | 512 KB |
| Max output size | 100 KB |
| Max loop iterations | 10 000 |
Max fetch calls |
20 |
| Max response body | 1 MB |
The sandbox exposes a curated surface: vanilla JavaScript plus bridge functions (fetch, workspace, getSecret, uuid, sleep, console). Third-party libraries (lodash, dayjs, etc.) are intentionally excluded — use dedicated workflow nodes instead.
Tool Registry
Register custom tools so they can be resolved by name:
import { registerTool, resolveTool, getAllTools } from "@nodetool-ai/agents";
registerTool(new MyCustomTool());
const tool = resolveTool("my_custom_tool");
const allTools = getAllTools(); // returns all registered tools
Writing a Custom Tool
import { Tool } from "@nodetool-ai/agents";
import type { ProcessingContext } from "@nodetool-ai/runtime";
class WeatherTool extends Tool {
readonly name = "get_weather";
readonly description = "Get current weather for a city.";
readonly inputSchema = {
type: "object",
properties: {
city: { type: "string", description: "City name" },
},
required: ["city"],
};
async process(context: ProcessingContext, params: Record<string, unknown>) {
const city = String(params.city);
const res = await fetch(`https://api.example.com/weather?q=${encodeURIComponent(city)}`);
return await res.json();
}
}
Rules for custom tools:
- Always validate params before use (the schema provides type hints to the LLM, but doesn’t enforce at runtime).
- Return serializable values (JSON-compatible objects).
- Handle errors within
process— throwErrorobjects with descriptive messages. - Use
contextfor secret resolution, storage access, and provider calls.
Skills
Skills are markdown files (SKILL.md) that inject domain-specific instructions into the agent’s system prompt.
Skill Format
---
name: data-analysis
description: Analyze CSV datasets and produce summary statistics
---
When working with data analysis tasks:
1. Load the dataset with the file read tool
2. Examine column types and null counts
3. Compute summary statistics
...
Skill Discovery
The agent searches these directories (in order):
- Directories passed to the constructor (
skillDirs) - Paths in the
NODETOOL_AGENT_SKILL_DIRSenvironment variable ./.claude/skills~/.claude/skills~/.codex/skills
Skill Resolution
- Explicit — set
NODETOOL_AGENT_SKILLS=skill-a,skill-bor passskills: ["skill-a"]in the constructor - Auto-select — the agent matches words in the objective against skill descriptions (disable with
NODETOOL_AGENT_AUTO_SKILLS=0)
Matched skill instructions are prepended to the system prompt under an # Agent Skills header.
Workflow Nodes
The agent system surfaces in the workflow editor through several node types defined in base-nodes/src/nodes/agents.ts:
| Node | Purpose |
|---|---|
| AgentNode | General-purpose agent with streaming output, tool access, and control edges |
| SummarizerNode | Summarize text with streaming output |
| ExtractorNode | Extract structured data from text |
| ClassifierNode | Classify text into categories |
| CreateThreadNode | Manage multi-turn conversation threads |
Control Edges
When an agent node has outgoing control edges, ControlNodeTool instances are automatically added to its tool list. The agent can call these tools to trigger downstream nodes with specific parameter values:
AgentNode ──control edge──> ImageGeneratorNode
│
└─ LLM calls "image_generator" tool with { prompt: "sunset over mountains" }
→ ImageGeneratorNode receives prompt override and executes
Using Agents Programmatically
Full Agent with Planning
import { Agent } from "@nodetool-ai/agents";
import { BrowserTool, GoogleSearchTool, WriteFileTool } from "@nodetool-ai/agents";
const agent = new Agent({
name: "researcher",
objective: "Research TypeScript ORMs and write a comparison report",
provider: openaiProvider,
model: "gpt-4o",
tools: [new GoogleSearchTool(), new BrowserTool(), new WriteFileTool()],
workspace: "/tmp/research-output",
maxSteps: 10,
maxStepIterations: 5,
});
for await (const message of agent.execute(context)) {
if (message.type === "chunk") {
process.stdout.write(message.content);
}
}
const result = agent.getResults();
Simple Agent (Single Step)
import { SimpleAgent } from "@nodetool-ai/agents";
const agent = new SimpleAgent({
name: "extractor",
objective: "Extract all email addresses from this text: ...",
provider: openaiProvider,
model: "gpt-4o",
tools: [],
outputSchema: {
type: "object",
properties: {
emails: { type: "array", items: { type: "string" } },
},
},
});
for await (const message of agent.execute(context)) {
// handle streaming messages
}
const { emails } = agent.getResults() as { emails: string[] };
Configuration Reference
| Option | Default | Description |
|---|---|---|
name |
required | Agent identifier |
objective |
required | Goal to achieve |
provider |
required | LLM provider instance (BaseProvider) |
model |
required | Model ID (e.g. "gpt-4o") |
planningModel |
same as model |
Alternative model for the planning phase |
reasoningModel |
same as model |
Alternative model for reasoning-heavy steps |
tools |
[] |
Array of Tool instances |
systemPrompt |
"" |
Custom system instructions |
maxTokenLimit |
128000 |
Token budget per step |
maxSteps |
10 |
Maximum number of steps in a task |
maxStepIterations |
5 |
Maximum LLM round-trips per step |
outputSchema |
— | JSON schema for the final result |
workspace |
auto-generated | Directory for file artifacts |
skills |
— | Explicit skill names to load |
skillDirs |
— | Additional directories to search for skills |
task |
— | Pre-planned task (skips planning phase) |
Related Pages
- Agent Memory System — Unified memory across all agent types: API, propagation, examples
- Global Chat & Agents — Using agents in the chat interface
- Agent CLI — Running agents from the command line
- Agent Configuration Schema — YAML configuration reference
- Custom Nodes Guide — Building custom workflow nodes