Skip to content

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.

  • 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
    1. Agent reads config.toml and loads vault credentials.
    2. Agent connects to broker URL(s) and joins configured networks.
    3. Agent registers with quest via an RPC call (quest_quest_register_agent).
    4. Agent listens for task.assigned events filtered by role and executes tasks using the configured provider(s).
    5. 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.

The package exposes and uses the following utilities (functions imported from agents-library and local module functions):

Tool / FunctionSourceDescriptionKey parameters
createWorkerAgentagents-libraryFactory for creating the worker agent (not shown in full source excerpt)(options) — broker URL, networks, role
loadVaultCredentialsagents-libraryLoads vault secrets (encrypted secrets) and returns an object with keysnone — returns Promise<Record<string,string>>
readConfigagents-libraryReads config.toml into a cfg helper with typed gettersnone — returns cfg with .string/.has/.strings
setLogLevelagents-librarySets global logging levellevel: string
setAgentTagagents-librarySet agent tag/id for loggertag: string
loggeragents-libraryStructured logger used across the agentmethods: info, warn, error, debug
timeragents-librarySimple timer helpers used for elapsed time loggingtimer.start(name), timer.elapsed(name)
RoleLoader./roles/RoleLoader.jsLoads role manifest/config from disknew RoleLoader(cwd).loadRole(roleName)
registerAgent(client, role, capabilities, maxConcurrentTasks)localRegisters this agent with quest via RPC quest_quest_register_agentclient: Kadi RPC client, role: string, capabilities: string[], maxConcurrentTasks: number
sendHeartbeat(client, role)localInvokes quest_quest_agent_heartbeat RPC to indicate availabilityclient, role
startHeartbeat(client, role)localStarts 30s interval to send heartbeatclient, role — returns NodeJS.Timeout
unregisterAgent(client, role)localGraceful unregister via quest_quest_unregister_agentclient, 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 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.

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 required
const 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 vault
const 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

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
  • 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.