agent-worker
Programmer agent — executes coding tasks
agent-worker is a generic worker agent for the KĀDI multi-agent orchestration platform. It supports multiple roles (artist, programmer, designer) via runtime configuration and role manifests. The agent connects to a Kadi broker, registers itself with the “quest” service, listens for task assignments, executes tasks using configured providers (primary/fallback), and publishes task lifecycle events (task.completed, task.failed, task.rejected). It centralizes heartbeat/registration, provider selection, vault credential loading, and role-specific behavior via RoleLoader.
Architecture
Section titled “Architecture”- Key components
- BaseWorkerAgent (referenced): extends BaseAgent; manages broker connection, provider and memory services.
- RoleLoader (./roles/RoleLoader.js): loads role-specific config from config/roles/{role}.toml and supplies role metadata (role name, provider overrides, capabilities).
- ProviderManager & MemoryService (inherited from BaseAgent / agents-library): manage LLM providers and persistent memory storage.
- KadiClient (via agents-library): single broker connection used for RPC and pub/sub.
- Vault loader (loadVaultCredentials): aggregates vault secrets for runtime credentials.
- Data flow
- Agent reads config.toml and loads vault credentials.
- Agent connects to broker URL(s) and joins configured networks.
- Agent registers with quest via an RPC call (quest_quest_register_agent).
- Agent listens for task.assigned events filtered by role and executes tasks using the configured provider(s).
- Agent emits task.completed / task.failed / task.rejected events and periodically sends heartbeats via quest_quest_agent_heartbeat.
- How it fits in AGENTS ecosystem
- Acts as the worker/executor node for “quest” orchestration: registers and heartbeats with quest RPCs.
- Uses providers (model-manager primary, anthropic fallback by default) to run LLM workloads.
- Communicates via the Kadi broker networks (configured under broker.local / broker.remote).
- Integrates with abilities declared in agent.json (secret-ability, ability-file-local, ability-log) to access vaults, local filesystem, and logging.
Tools / API
Section titled “Tools / API”The package exposes and uses the following utilities (functions imported from agents-library and local module functions):
| Tool / Function | Source | Description | Key parameters |
|---|---|---|---|
| createWorkerAgent | agents-library | Factory for creating the worker agent (not shown in full source excerpt) | (options) — broker URL, networks, role |
| loadVaultCredentials | agents-library | Loads vault secrets (encrypted secrets) and returns an object with keys | none — returns Promise<Record<string,string>> |
| readConfig | agents-library | Reads config.toml into a cfg helper with typed getters | none — returns cfg with .string/.has/.strings |
| setLogLevel | agents-library | Sets global logging level | level: string |
| setAgentTag | agents-library | Set agent tag/id for logger | tag: string |
| logger | agents-library | Structured logger used across the agent | methods: info, warn, error, debug |
| timer | agents-library | Simple timer helpers used for elapsed time logging | timer.start(name), timer.elapsed(name) |
| RoleLoader | ./roles/RoleLoader.js | Loads role manifest/config from disk | new RoleLoader(cwd).loadRole(roleName) |
| registerAgent(client, role, capabilities, maxConcurrentTasks) | local | Registers this agent with quest via RPC quest_quest_register_agent | client: Kadi RPC client, role: string, capabilities: string[], maxConcurrentTasks: number |
| sendHeartbeat(client, role) | local | Invokes quest_quest_agent_heartbeat RPC to indicate availability | client, role |
| startHeartbeat(client, role) | local | Starts 30s interval to send heartbeat | client, role — returns NodeJS.Timeout |
| unregisterAgent(client, role) | local | Graceful unregister via quest_quest_unregister_agent | client, role |
If/when createWorkerAgent or other agent lifecycle helpers are exported in this package, follow the same pattern: supply client and role and hook into register/unregister/heartbeat.
Configuration
Section titled “Configuration”Configuration is primarily driven by config.toml and environment variables. Secrets should live in vault(s) (secrets.toml encrypted) and are loaded via loadVaultCredentials.
Key config.toml fields (from config.toml):
- [agent]
- ID: “agent-worker” — agent identity tag
- ROLE: default role (e.g., “programmer”)
- VERSION: version string
- [logging]
- LEVEL: “info” — logger level
- [broker.local] / [broker.remote]
- URL: broker URL (wss://…)
- NETWORKS: array of network names the agent should join (e.g., [“programmer”,“git”,“qa”,“file”,“utility”,“quest”,“global”])
- [provider]
- PRIMARY: “model-manager” — primary LLM provider id
- FALLBACK: “anthropic” — fallback provider id
- [provider.model-manager]
- MODEL: default model name (e.g., “gpt-5-mini”)
- [provider.anthropic]
- MODEL: default Claude model string (e.g., “claude-haiku-4-5-20251001”)
- [memory]
- DATA_PATH: ”./data/memory” — local memory storage path
- [secrets]
- VAULTS: [“anthropic”,“model-manager”,“arcadedb”] — vault list loaded by loadVaultCredentials
- KEYS: list of keys that will be read from vaults
- [arcadedb]
- HOST, PORT, USERNAME, DATABASE — used for logging/storage to ArcadeDB
Environment variables (take precedence in many cases):
- AGENT_ROLE — overrides role read from config (used at start to select role loader)
- KADI_BROKER_URL_LOCAL — override broker.local.URL
- KADI_BROKER_URL_REMOTE — override broker.remote.URL
- KADI_NETWORK_LOCAL — comma separated networks to override broker.local.NETWORKS
- KADI_NETWORK_REMOTE — comma separated networks to override broker.remote.NETWORKS
- ANTHROPIC_API_KEY — vault override for Anthropics
- MODEL_MANAGER_API_KEY — vault override for model-manager API key
- MODEL_MANAGER_BASE_URL — vault override for model-manager base URL
- ARCADE_HOST, ARCADE_PORT — optional runtime host/port (used in deploy examples)
- NODE_ENV — “production” recommended in Docker deployments
Secrets and Vaults:
- agent.json deploy sections declare three vaults: “anthropic”, “model-manager”, “arcadedb”
- Required keys (as listed): ANTHROPIC_API_KEY, MODEL_MANAGER_API_KEY, MODEL_MANAGER_BASE_URL, ARCADE_USERNAME, ARCADE_PASSWORD
- Secrets delivery method in agent.json: “delivery”: “broker” (secrets are pulled by kadi secret receive before starting)
- loadVaultCredentials() is used at runtime to merge environment variables with vault values.
Code Examples
Section titled “Code Examples”Below are key patterns and real snippets taken from the package source. Use these when modifying lifecycle, registration, or heartbeat behavior.
Imports + config reading:
import fs from 'fs';import { createWorkerAgent, loadVaultCredentials, readConfig, setLogLevel, setAgentTag, logger, timer,} from 'agents-library';import { RoleLoader } from './roles/RoleLoader.js';
const cfg = readConfig();Agent identity + logging setup:
const agentId = cfg.string('agent.ID');const agentVersion = cfg.string('agent.VERSION');const logLevel = cfg.has('logging.LEVEL') ? cfg.string('logging.LEVEL') : 'info';setLogLevel(logLevel);setAgentTag(agentId);Vault & broker resolution (priority: env vars -> config.toml -> vault):
// Broker resolution: at least one of local/remote requiredconst hasLocal = cfg.has('broker.local.URL');const hasRemote = cfg.has('broker.remote.URL');if (!hasLocal && !hasRemote) { throw new Error('At least one broker required: set [broker.local] or [broker.remote] in config.toml');}
const brokerUrl = hasLocal ? (process.env.KADI_BROKER_URL_LOCAL ?? cfg.string('broker.local.URL')) : (process.env.KADI_BROKER_URL_REMOTE ?? cfg.string('broker.remote.URL'));const networks = hasLocal ? (process.env.KADI_NETWORK_LOCAL?.split(',') ?? cfg.strings('broker.local.NETWORKS')) : (process.env.KADI_NETWORK_REMOTE?.split(',') ?? cfg.strings('broker.remote.NETWORKS'));
const additionalBrokerUrl = hasLocal && hasRemote ? (process.env.KADI_BROKER_URL_REMOTE ?? cfg.string('broker.remote.URL')) : undefined;const additionalBrokerNetworks = hasLocal && hasRemote ? (process.env.KADI_NETWORK_REMOTE?.split(',') ?? cfg.strings('broker.remote.NETWORKS')) : undefined;
// Credentials: env vars take priority over vaultconst vault = await loadVaultCredentials();const anthropicApiKey = process.env.ANTHROPIC_API_KEY || vault.ANTHROPIC_API_KEY;const modelManagerBaseUrl = process.env.MODEL_MANAGER_BASE_URL || vault.MODEL_MANAGER_BASE_URL;const modelManagerApiKey = process.env.MODEL_MANAGER_API_KEY || vault.MODEL_MANAGER_API_KEY;Register / heartbeat helpers:
async function registerAgent(client: any, role: string, capabilities: string[], maxConcurrentTasks: number): Promise<void> { try { logger.info(agentId, 'Registering with quest server...', timer.elapsed('main')); const result = await client.invokeRemote('quest_quest_register_agent', { agentId: `agent-worker-${role}`, name: `${role.charAt(0).toUpperCase() + role.slice(1)} Agent`, role, capabilities, maxConcurrentTasks, }); const resultText = result.content[0].text; const registrationData = JSON.parse(resultText); logger.info(agentId, `Registered: ${registrationData.message}`, timer.elapsed('main')); } catch (error: any) { logger.error(agentId, `Registration failed: ${error.message}`, timer.elapsed('main'), error); }}
async function sendHeartbeat(client: any, role: string): Promise<void> { try { await client.invokeRemote('quest_quest_agent_heartbeat', { agentId: `agent-worker-${role}`, status: 'available', currentTasks: [], timestamp: new Date().toISOString(), }); } catch (error: any) { logger.warn(agentId, `Heartbeat failed: ${error.message}`, timer.elapsed('main')); }}
function startHeartbeat(client: any, role: string): NodeJS.Timeout { logger.debug(agentId, 'Starting heartbeat (30s interval)', timer.elapsed('main')); sendHeartbeat(client, role).catch(() => {}); return setInterval(() => sendHeartbeat(client, role).catch(() => {}), 30000);}
async function unregisterAgent(client: any, role: string): Promise<void> { try { logger.info(agentId, 'Unregistering from quest server...', timer.elapsed('main')); await client.invokeRemote('quest_quest_unregister_agent', { agentId: `agent-worker-${role}`, reason: 'Graceful shutdown', }); logger.info(agentId, 'Unregistered', timer.elapsed('main')); } catch (error: any) { logger.error(agentId, `Unregister failed: ${error.message}`, timer.elapsed('main'), error); }}Main startup role loading excerpt:
async function main(): Promise<void> { timer.start('main');
try { // Load role configuration const roleLoader = new RoleLoader(process.cwd()); const roleName = process.env.AGENT_ROLE || cfg.string('agent.ROLE'); const roleConfig = roleLoader.loadRole(roleName);
// Provider config: role overrides config.toml defaults const primaryProvider = cfg.string('provider.PRIMARY'); const primaryModel = roleConfig.provider?.model || cfg.string(`provider.${primaryProvider}.MODEL`); const fallbackProvider = cfg.has('provider.FALLBACK') ? cfg.string('provider.FALLBACK') : undefined; const fallbackModel = fallbackProvider ? cfg.string(`provider.${fallbackProvider}.MODEL`) : undefined;
// Startup summary const workerAgentId = `agent-worker-${roleConfig.role}`; logger.info(agentId, `Starting ${workerAgentId} v${agentVersion} (role: ${roleConfig.role})`, timer.elapsed('main'));When modifying behaviour, ensure any RPC names remain unchanged:
- quest_quest_register_agent
- quest_quest_agent_heartbeat
- quest_quest_unregister_agent
Dependencies
Section titled “Dependencies”Declared runtime dependencies (from package metadata):
- @anthropic-ai/sdk — Anthropics SDK for Claude provider
- @kadi.build/core — Kadi core bindings (broker client)
- @modelcontextprotocol/sdk — model context utilities
- agents-library — shared helpers (readConfig, createWorkerAgent, logger, timer, loadVaultCredentials)
- zod — runtime schema validation
Dev dependencies:
- typescript, tsx, vitest, eslint, @typescript-eslint, etc.
Declared abilities in agent.json (what this agent expects to use):
- secret-ability — access to vault secrets (kadi secret receive & loadVaultCredentials)
- ability-file-local — read/write to local filesystem (playground mounts)
- ability-log — structured logging
What depends on agent-worker:
- “quest” orchestration service (RPC registration & heartbeat) expects worker agents to register
- any higher-level orchestrator or orchestration UIs that publish task.assigned events to broker networks the agent joins
Notes for developers
Section titled “Notes for developers”- Role-specific behaviour is encapsulated under roles/ and loaded via RoleLoader. Add or modify roles by editing config/roles/{role}.toml and RoleLoader parsing logic.
- Brokers: the agent will require at least one broker URL (local or remote) in config.toml; environment variables can override.
- Secrets: in production deploys, kadi secret receive is invoked before starting the agent in Docker entrypoint (see agent.json deploy commands). Ensure the required vault keys are provisioned.
- RPC payloads: registerAgent expects the quest RPC to return a content[0].text JSON string — maintain that contract when changing quest server RPCs.
- Heartbeat interval is fixed at 30s (startHeartbeat). Adjust with care if quest server expects a different cadence.
If you need documentation for RoleLoader, provider selection, or task execution flow beyond the startup/heartbeat code here, inspect ./roles/RoleLoader.js, provider manager usage in agents-library, and the remaining src files that implement task handlers.