Worker
Worker architecture, configuration, model routing, agent building, and the understanding filter.
Overview
The worker is an event-driven Rust process that executes LLM-backed agents. It has no HTTP server of its own -- all communication flows through a WebSocket connection to the backend, which relays events from the frontend via Redis pub/sub.
On startup, the worker:
- Loads configuration from
~/.nenjo/config.toml. - Bootstraps by fetching all platform data from
GET /api/v1/agents/bootstrap. - Caches the bootstrap data to
~/.nenjo/data/as individual JSON files. - Builds a
RouterContextcontaining the in-memory snapshot, provider registry, memory store, and security policy. - Connects to the backend WebSocket and waits for events.
Once running, the worker processes events like chat.message, ticket.execute, chat.mode_enter, bootstrap.changed, and cron.fire, dispatching each to the appropriate handler.
Configuration
The worker reads its configuration from ~/.nenjo/config.toml. The file is organized into several sections:
Core connection settings
backend_api_url = "http://localhost:8080"
backend_ws_url = "ws://localhost:8080/ws"
backend_api_key = "your-api-key"
nats_url = "nats://localhost:4222"
[model_provider_api_keys]
openai = "sk-..."
anthropic = "sk-ant-..."
google = "..."Autonomy and security
Controls what agents are allowed to do. See Security for full details.
[autonomy]
workspace_only = true
max_actions_per_hour = 1000
max_cost_per_day_cents = 500
require_approval_for_medium_risk = true
block_high_risk_commands = true
blocked_commands = ["rm", "sudo", "curl", "ssh"]
forbidden_paths = ["/etc", "/root", "~/.ssh", "~/.aws"]Runtime
Choose between native execution or Docker-sandboxed execution:
[runtime]
kind = "native" # or "docker"
[runtime.docker]
image = "alpine:3.20"
network = "none"
memory_limit_mb = 512
cpu_limit = 1.0
read_only_rootfs = true
mount_workspace = trueAgent behavior
[agent]
max_tool_iterations = 100
max_history_messages = 50
parallel_tools = false
tool_dispatcher = "auto"
max_delegation_depth = 3
compact_context = false # true for small models (13B or less)Memory
[memory]
backend = "sqlite" # or "none"
auto_inject = true
injection_token_budget = 5000
fallback_model = "gpt-4o-mini"
fallback_provider = "openai"
time_decay_half_life_days = 7.0
embedding_provider = "none" # "openai" or "custom:URL"
embedding_model = "text-embedding-3-small"
embedding_dimensions = 1536
vector_weight = 0.7
keyword_weight = 0.3Reliability
[reliability]
provider_retries = 2
provider_backoff_ms = 500
fallback_providers = ["anthropic", "openai"]
model_fallbacks = { "claude-opus-4-20250514" = ["claude-sonnet-4-20250514", "gpt-4o"] }Cost tracking
[cost]
enabled = false
daily_limit_usd = 10.0
monthly_limit_usd = 100.0
warn_at_percent = 80Model routing
Route task hints to specific provider and model combinations:
[[model_routes]]
hint = "reasoning"
provider = "openrouter"
model = "anthropic/claude-opus-4-20250514"
[[model_routes]]
hint = "fast"
provider = "groq"
model = "llama-3.3-70b-versatile"Usage: set hint:reasoning as the model parameter on an agent to route through the configured provider.
Web search and fetch
[web_search]
enabled = true
provider = "duckduckgo" # or "brave"
max_results = 5
[web_fetch]
enabled = true
allowed_domains = ["*"] # or specific domains
blocked_domains = []
max_response_size = 500000Agent building
The build_agent function is the core assembly point. It is called every time a pipeline step needs to execute or a chat message needs to be handled. Here is what it does, in order:
1. Resolve provider and model
Looks up the agent's model_provider and model_name in the ProviderRegistry. The registry creates provider instances lazily -- the first time a provider/model combination is needed, it is initialized from the configured API keys.
2. Resolve role
The role is determined by priority:
- Explicit
override_role_id(set by the pipeline step'srole_id). - First role assigned to the agent via
role_assignments.
The role's prompt_config provides the system prompt, developer prompt, templates, output templates, and memory profile.
3. Build core tools
All agents start with the full set of built-in tools: shell, file_read, file_write, file_edit, web_search, web_fetch, git_operations, memory_store, memory_query, and delegation.
4. Apply understanding filter
Immediately after building the tool set, the worker applies the understanding filter. This filter removes all Write-category tools, keeping only Read and ReadWrite tools. The result is that agents in their default state can observe and analyze but cannot make changes.
Write tools are re-added when a mode is activated that explicitly includes them in its allow list or additional tools configuration.
5. Add delegation tool
If the agent has a DelegationContext and has not exceeded max_delegation_depth, a DelegateToTool is added. This allows the agent to delegate subtasks to other roles in the pipeline.
6. Resolve skill tools
For each skill assigned to the agent's role:
nenjo-readandnenjo-writeskills are skipped (replaced by MCP platform tools).- Skills that require credentials (
auth_type != "none") are only registered as tools if credentials are resolved. Skills without credentials are still included for prompt injection (instructions and guidelines).
Credential resolution follows a chain: local file (~/.nenjo/credentials.toml) -> environment variables (NENJO_SKILL_{NAME}_{FIELD}) -> backend API.
7. Register MCP platform tools
If the role has platform_scopes (e.g., ["tickets:read", "pipelines:write"]), the worker calls the backend's MCP endpoint to fetch available tools, then filters them by the role's scopes. This gives the agent access to Nenjo platform operations (listing projects, creating pipelines, etc.).
8. Register external MCP tools
If the role has external MCP server assignments (via role_mcp_assignments), the worker connects to those servers through the ExternalMcpPool and registers their tools.
9. Build memory context
A MemoryContext is created with the project ID, role name, and agent ID. This scopes all memory operations to the correct namespace, ensuring that memories are isolated per project and role.
10. Assemble the agent instance
Finally, an AgentBuilder assembles the agent with:
- Agent ID, name, model, and temperature
- LLM provider
- All resolved tools
- Memory store and memory context
- Prompt config (system prompt, developer prompt, templates)
- Stream sender for real-time token streaming
Event handling
The RouterContext dispatches incoming WebSocket events to specialized handlers:
| Event | Handler |
|---|---|
chat.message | Direct chat or pipeline-routed chat |
chat.mode_enter | Activate a mode session for a role |
chat.mode_exit | Deactivate a mode session |
chat.cancel | Cancel an in-progress chat |
ticket.execute | Run a pipeline for a ticket |
bootstrap.changed | Re-fetch and swap bootstrap data |
cron.fire | Execute a cron-triggered pipeline |
tools.registry | Return available tools for a project |
See Bootstrap and Hot-Swap for how bootstrap.changed events trigger live reconfiguration.