Skip to content

FleetManager API Reference

The FleetManager class is the primary entry point for using herdctl programmatically. It provides a simple, high-level API to initialize and run a fleet of agents with minimal configuration.

Terminal window
npm install @herdctl/core
# or
pnpm add @herdctl/core
import { FleetManager } from '@herdctl/core';
const manager = new FleetManager({
configPath: './herdctl.yaml',
stateDir: './.herdctl',
});
await manager.initialize();
await manager.start();
// Subscribe to events
manager.on('job:created', (payload) => {
console.log(`Job ${payload.job.id} created for ${payload.agentName}`);
});
// Graceful shutdown
process.on('SIGINT', async () => {
await manager.stop();
process.exit(0);
});

Creates a new FleetManager instance.

const manager = new FleetManager(options: FleetManagerOptions);
ParameterTypeRequiredDefaultDescription
options.stateDirstringYesPath to the state directory (e.g., .herdctl). Created if it doesn’t exist. Stores job artifacts, session state, and logs.
options.configPathstringNoAuto-discoverPath to herdctl.yaml. Can be absolute, relative, or a directory path. If not provided, searches up from cwd.
options.loggerFleetManagerLoggerNoConsole loggerCustom logger with debug, info, warn, error methods.
options.checkIntervalnumberNo1000Interval in milliseconds between scheduler checks.
import { FleetManager } from '@herdctl/core';
// Minimal configuration
const manager = new FleetManager({
stateDir: './.herdctl',
});
// Full configuration
const manager = new FleetManager({
configPath: './config/herdctl.yaml',
stateDir: './.herdctl',
checkInterval: 5000, // 5 seconds
logger: {
debug: (msg) => console.debug(`[DEBUG] ${msg}`),
info: (msg) => console.info(`[INFO] ${msg}`),
warn: (msg) => console.warn(`[WARN] ${msg}`),
error: (msg) => console.error(`[ERROR] ${msg}`),
},
});

Initializes the fleet manager by loading configuration and preparing the state directory.

await manager.initialize(): Promise<void>

This method:

  1. Loads and validates the configuration file
  2. Initializes the state directory structure
  3. Prepares the scheduler (but does not start it)

After initialization, the fleet manager is ready to start.

ErrorCondition
FleetManagerStateErrorAlready initialized or running
FleetManagerConfigErrorConfiguration is invalid or not found
FleetManagerStateDirErrorState directory cannot be created
const manager = new FleetManager({
configPath: './herdctl.yaml',
stateDir: './.herdctl',
});
try {
await manager.initialize();
console.log(`Loaded ${manager.state.agentCount} agents`);
} catch (error) {
if (error instanceof FleetManagerConfigError) {
console.error('Configuration error:', error.message);
}
}

Starts the fleet manager scheduler, which begins processing agent schedules.

await manager.start(): Promise<void>

This begins the scheduler, which will:

  1. Check agent schedules at the configured interval
  2. Trigger agents when their schedules are due
  3. Track schedule state in the state directory
ErrorCondition
FleetManagerStateErrorNot initialized
await manager.initialize();
await manager.start();
// The manager is now running and processing schedules
manager.on('schedule:triggered', (payload) => {
console.log(`Triggered ${payload.agentName}/${payload.scheduleName}`);
});

Gracefully stops the fleet manager.

await manager.stop(options?: FleetManagerStopOptions): Promise<void>
ParameterTypeRequiredDefaultDescription
options.waitForJobsbooleanNotrueWait for running jobs to complete before stopping
options.timeoutnumberNo30000Maximum time in ms to wait for jobs to complete
options.cancelOnTimeoutbooleanNofalseCancel jobs that don’t complete within timeout
options.cancelTimeoutnumberNo10000Time in ms for each job to respond to SIGTERM before SIGKILL

This will:

  1. Signal the scheduler to stop accepting new triggers
  2. Wait for running jobs to complete (with timeout)
  3. If timeout is reached and cancelOnTimeout is true, cancel remaining jobs
  4. Persist all state before shutdown completes
  5. Emit 'stopped' event when complete
ErrorCondition
FleetManagerShutdownErrorShutdown times out and cancelOnTimeout is false
// Normal shutdown - wait for jobs with default 30s timeout
await manager.stop();
// Shutdown with custom timeout
await manager.stop({ timeout: 60000 });
// Shutdown without waiting for jobs (not recommended)
await manager.stop({ waitForJobs: false });
// Cancel jobs if they don't complete in time
await manager.stop({
timeout: 30000,
cancelOnTimeout: true,
cancelTimeout: 10000,
});

Hot-reloads configuration without restarting the fleet.

await manager.reload(): Promise<ConfigReloadedPayload>
interface ConfigReloadedPayload {
agentCount: number; // Number of agents in new config
agentNames: string[]; // Names of agents in new config
configPath: string; // Path to reloaded config file
changes: ConfigChange[]; // List of detected changes
timestamp: string; // ISO timestamp of reload
}
interface ConfigChange {
type: 'added' | 'removed' | 'modified';
category: 'agent' | 'schedule' | 'defaults';
name: string; // Agent name or "agent/schedule"
details?: string; // What changed (for modifications)
}

This method provides hot configuration reload capability:

  1. Loads and validates the new configuration
  2. If validation fails, keeps the old configuration (fails gracefully)
  3. Running jobs continue with their original configuration
  4. New jobs will use the new configuration
  5. Updates the scheduler with new agent definitions
  6. Emits a 'config:reloaded' event with change details
ErrorCondition
InvalidStateErrorFleet manager is not initialized
FleetManagerConfigErrorNew configuration is invalid
// Reload configuration
const result = await manager.reload();
console.log(`Reloaded with ${result.changes.length} changes`);
for (const change of result.changes) {
console.log(` ${change.type} ${change.category}: ${change.name}`);
}
// Subscribe to reload events
manager.on('config:reloaded', (payload) => {
console.log(`Config reloaded: ${payload.agentCount} agents`);
});

Returns a comprehensive snapshot of fleet state.

await manager.getFleetStatus(): Promise<FleetStatus>
interface FleetStatus {
state: FleetManagerStatus; // Current state
uptimeSeconds: number | null; // Time since started
initializedAt: string | null; // ISO timestamp
startedAt: string | null; // ISO timestamp
stoppedAt: string | null; // ISO timestamp
counts: FleetCounts; // Summary counts
scheduler: {
status: 'stopped' | 'running' | 'stopping';
checkCount: number; // Total checks performed
triggerCount: number; // Total triggers fired
lastCheckAt: string | null; // ISO timestamp
checkIntervalMs: number;
};
lastError: string | null;
}
interface FleetCounts {
totalAgents: number;
idleAgents: number;
runningAgents: number;
errorAgents: number;
totalSchedules: number;
runningSchedules: number;
runningJobs: number;
}
const status = await manager.getFleetStatus();
console.log(`Fleet: ${status.state}`);
console.log(`Uptime: ${status.uptimeSeconds}s`);
console.log(`Agents: ${status.counts.totalAgents} total, ${status.counts.runningAgents} running`);
console.log(`Jobs: ${status.counts.runningJobs} running`);

Returns information about all configured agents.

await manager.getAgentInfo(): Promise<AgentInfo[]>
interface AgentInfo {
name: string; // Agent local name
qualifiedName: string; // Dot-separated qualified name (e.g., "myfleet.agent-name")
fleetPath: string[]; // Fleet hierarchy path (empty for root agents)
description?: string; // From configuration
status: 'idle' | 'running' | 'error';
currentJobId: string | null; // Currently running job
lastJobId: string | null; // Last completed job
maxConcurrent: number; // Max concurrent instances
runningCount: number; // Currently running instances
errorMessage: string | null; // Error if status is 'error'
scheduleCount: number; // Number of schedules
schedules: ScheduleInfo[]; // Schedule details
model?: string; // Model from config
working_directory?: string; // Working directory path
chat?: Record<string, AgentChatStatus>; // Chat connector statuses by platform
}
interface AgentChatStatus {
configured: boolean;
connectionStatus?: 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'disconnecting' | 'error';
botUsername?: string;
lastError?: string;
}
const agents = await manager.getAgentInfo();
for (const agent of agents) {
console.log(`${agent.name}: ${agent.status}`);
console.log(` Running: ${agent.runningCount}/${agent.maxConcurrent}`);
console.log(` Schedules: ${agent.scheduleCount}`);
}

Returns information about a specific agent.

await manager.getAgentInfoByName(name: string): Promise<AgentInfo>
ParameterTypeRequiredDescription
namestringYesThe agent name to look up
ErrorCondition
AgentNotFoundErrorNo agent with that name exists
try {
const agent = await manager.getAgentInfoByName('my-agent');
console.log(`Status: ${agent.status}`);
console.log(`Running: ${agent.runningCount}/${agent.maxConcurrent}`);
} catch (error) {
if (error instanceof AgentNotFoundError) {
console.log(`Agent "${error.agentName}" not found`);
console.log(`Available: ${error.availableAgents?.join(', ')}`);
}
}

Returns all schedules across all agents.

await manager.getSchedules(): Promise<ScheduleInfo[]>
interface ScheduleInfo {
name: string; // Schedule name
agentName: string; // Owning agent
type: string; // 'interval', 'cron', etc.
interval?: string; // e.g., "5m", "1h"
expression?: string; // Cron expression
status: 'idle' | 'running' | 'disabled';
lastRunAt: string | null; // ISO timestamp
nextRunAt: string | null; // ISO timestamp
lastError: string | null;
}
const schedules = await manager.getSchedules();
for (const schedule of schedules) {
console.log(`${schedule.agentName}/${schedule.name}: ${schedule.status}`);
console.log(` Next run: ${schedule.nextRunAt}`);
}

Returns a specific schedule by agent and schedule name.

await manager.getSchedule(agentName: string, scheduleName: string): Promise<ScheduleInfo>
ErrorCondition
AgentNotFoundErrorAgent doesn’t exist
ScheduleNotFoundErrorSchedule doesn’t exist for agent
const schedule = await manager.getSchedule('my-agent', 'hourly');
console.log(`Status: ${schedule.status}`);
console.log(`Last run: ${schedule.lastRunAt}`);
console.log(`Next run: ${schedule.nextRunAt}`);

Enables a previously disabled schedule.

await manager.enableSchedule(agentName: string, scheduleName: string): Promise<ScheduleInfo>

Enables a schedule that was previously disabled, allowing it to trigger again on its configured interval. The enabled state is persisted and survives restarts.

const schedule = await manager.enableSchedule('my-agent', 'hourly');
console.log(`Schedule status: ${schedule.status}`); // 'idle'

Disables a schedule temporarily.

await manager.disableSchedule(agentName: string, scheduleName: string): Promise<ScheduleInfo>

Disables a schedule, preventing it from triggering on its configured interval. The schedule remains in the configuration but won’t run until re-enabled. The disabled state is persisted and survives restarts.

// Disable a schedule temporarily
const schedule = await manager.disableSchedule('my-agent', 'hourly');
console.log(`Schedule status: ${schedule.status}`); // 'disabled'
// Later, re-enable it
await manager.enableSchedule('my-agent', 'hourly');

Registers an agent at runtime without writing YAML or reloading configuration.

await manager.addAgent(
agent: AgentConfig | (Record<string, unknown> & { name: string }),
options?: AddAgentOptions
): Promise<AgentInfo>
ParameterTypeRequiredDescription
agentAgentConfigYesAgent configuration object
options.baseDirstringNoBase directory for resolving relative working_directory paths (defaults to config directory or cwd)
options.mergeDefaultsbooleanNoWhether to merge fleet defaults into the agent (default: true)
options.replacebooleanNoReplace existing agent with same name instead of throwing (default: false)
interface AgentInfo {
name: string;
qualifiedName: string;
status: 'idle' | 'running' | 'error';
// ... (see getAgentInfo for full type)
}

This method provides programmatic agent registration without the write-yaml-then-reload round trip. The agent configuration is validated against the agent schema, merged with fleet defaults (unless disabled), and integrated into the in-memory config. The resolved agent is immediately triggerable and visible in fleet status. A config:reloaded event is emitted describing the change.

Programmatic agents are always registered at the root fleet level (fleetPath: []), so their qualified name equals their local name.

ErrorCondition
InvalidStateErrorFleet manager not initialized
ConfigurationErrorValidation fails or qualified name collides with existing agent (and replace is not set)
// Register a new agent
const agent = await manager.addAgent({
name: 'my-agent',
prompt: 'You are a helpful assistant',
model: 'claude-opus-4-5',
working_directory: '/path/to/project',
schedules: [
{
name: 'hourly',
interval: '1h',
prompt: 'Check for updates',
},
],
});
console.log(`Added agent: ${agent.qualifiedName}`);
// Replace an existing agent
await manager.addAgent(
{
name: 'my-agent',
prompt: 'Updated prompt',
},
{ replace: true }
);
// Add agent without merging defaults
await manager.addAgent(
{
name: 'standalone-agent',
prompt: 'I have my own config',
},
{ mergeDefaults: false }
);

Unregisters an agent at runtime.

await manager.removeAgent(name: string): Promise<boolean>
ParameterTypeRequiredDescription
namestringYesAgent qualified name or local name to remove

Returns true if an agent was removed, false if no match was found.

Removes the agent from the in-memory config and the scheduler. Accepts a qualified name (e.g., "sub.agent") or a local name; qualified names are matched first. Running jobs are unaffected — the scheduler simply stops triggering the removed agent’s schedules.

ErrorCondition
InvalidStateErrorFleet manager not initialized
// Remove by name
const removed = await manager.removeAgent('my-agent');
if (removed) {
console.log('Agent removed');
} else {
console.log('Agent not found');
}
// Remove by qualified name
await manager.removeAgent('myfleet.my-agent');

Lists discovered Claude Code sessions for an agent.

await manager.getAgentSessions(
name: string,
options?: { limit?: number }
): Promise<DiscoveredSession[]>
ParameterTypeRequiredDescription
namestringYesAgent qualified name or local name
options.limitnumberNoOptional limit for top-N enrichment
interface DiscoveredSession {
sessionId: string;
lastModified: string; // ISO timestamp
customName?: string; // Custom display name if set
firstPrompt?: string; // First user message
lastAssistant?: string; // Last assistant response
messageCount?: number; // Total messages
createdByJobId?: string; // Originating job ID
// ... (additional enrichment fields)
}

Derives the agent’s working directory and Docker mode from the loaded config. Sessions are returned sorted by modification time (newest first).

Note: Sessions are keyed by working directory. This method uses the agent’s configured working_directory. If you triggered the agent with a per-trigger workingDirectory override, the resulting sessions live under that override directory and will NOT appear here.

ErrorCondition
InvalidStateErrorFleet manager not initialized
AgentNotFoundErrorAgent doesn’t exist
const sessions = await manager.getAgentSessions('my-agent');
for (const session of sessions) {
console.log(`${session.sessionId}: ${session.customName || session.firstPrompt}`);
console.log(` Last modified: ${session.lastModified}`);
console.log(` Messages: ${session.messageCount}`);
}
// Get top 10 sessions
const recent = await manager.getAgentSessions('my-agent', { limit: 10 });

Gets the parsed chat messages for one of an agent’s sessions.

await manager.getAgentSessionMessages(
name: string,
sessionId: string
): Promise<ChatMessage[]>
ParameterTypeRequiredDescription
namestringYesAgent qualified name or local name
sessionIdstringYesSession ID to read
interface ChatMessage {
role: 'user' | 'assistant';
content: string;
timestamp?: string;
// ... (additional fields)
}
ErrorCondition
InvalidStateErrorFleet manager not initialized
AgentNotFoundErrorAgent doesn’t exist
const messages = await manager.getAgentSessionMessages(
'my-agent',
'session-abc123'
);
for (const msg of messages) {
console.log(`[${msg.role}] ${msg.content}`);
}

Gets token usage data for one of an agent’s sessions.

await manager.getAgentSessionUsage(
name: string,
sessionId: string
): Promise<SessionUsage>
ParameterTypeRequiredDescription
namestringYesAgent qualified name or local name
sessionIdstringYesSession ID to read
interface SessionUsage {
inputTokens: number; // Last turn's input + cache tokens
turnCount: number; // Total turns in session
hasData: boolean; // Whether usage data exists
}

Reads the session transcript and returns the most recent context-window fill level (last assistant turn’s input + cache tokens) plus a turn count. This lets a UI show “context used” for a chat loaded from history — before any new turn streams a fresh usage value.

ErrorCondition
InvalidStateErrorFleet manager not initialized
AgentNotFoundErrorAgent doesn’t exist
const usage = await manager.getAgentSessionUsage('my-agent', 'session-abc123');
if (usage.hasData) {
console.log(`Context used: ${usage.inputTokens} tokens`);
console.log(`Turn count: ${usage.turnCount}`);
}

Deletes one of an agent’s Claude Code session transcripts from disk.

await manager.deleteSession(
name: string,
sessionId: string
): Promise<boolean>
ParameterTypeRequiredDescription
namestringYesAgent qualified name or local name
sessionIdstringYesSession ID whose transcript should be removed

Returns true if a file was removed, false if no transcript existed (or the agent has no working directory).

Resolves the agent’s working directory and Docker mode from the loaded config, computes the CLI (or Docker) transcript file path, deletes it, and invalidates the session-discovery cache so a subsequent getAgentSessions() no longer lists it.

The sessionId is validated (only [A-Za-z0-9-] is allowed) to prevent path traversal.

ErrorCondition
InvalidStateErrorFleet manager not initialized
AgentNotFoundErrorAgent doesn’t exist
ErrorsessionId contains invalid characters
const deleted = await manager.deleteSession('my-agent', 'session-abc123');
if (deleted) {
console.log('Session deleted');
} else {
console.log('Session not found');
}

setSessionName(name, sessionId, customName)

Section titled “setSessionName(name, sessionId, customName)”

Sets (or clears) the custom display name for one of an agent’s sessions.

await manager.setSessionName(
name: string,
sessionId: string,
customName: string | null
): Promise<void>
ParameterTypeRequiredDescription
namestringYesAgent qualified name or local name
sessionIdstringYesSession ID to (re)name
customNamestring | nullYesCustom name to set, or null/empty to clear it

Writes through the fleet’s shared session metadata store so a subsequent getAgentSessions() reflects the new customName immediately. Passing null or an empty/whitespace string clears any existing custom name.

ErrorCondition
InvalidStateErrorFleet manager not initialized
AgentNotFoundErrorAgent doesn’t exist
// Set custom name
await manager.setSessionName('my-agent', 'session-abc123', 'Bug Fix Discussion');
// Clear custom name
await manager.setSessionName('my-agent', 'session-abc123', null);

Drops the cached session listing for an agent so the next getAgentSessions() call rebuilds it from disk.

manager.invalidateSessions(name: string): void
ParameterTypeRequiredDescription
namestringYesAgent qualified name or local name

The underlying session discovery service caches each working directory’s listing for up to its TTL (default 30s). That cache is now mtime-aware, so newly created transcript files are normally picked up immediately. Call this when you want to force a fresh listing regardless — e.g., after each chat turn, or on filesystems whose directory mtime has coarse (1-second) granularity.

Resolves the agent’s working directory and Docker mode from the loaded config. A no-op when the agent has no working directory.

ErrorCondition
InvalidStateErrorFleet manager not initialized
AgentNotFoundErrorAgent doesn’t exist
// Force refresh session listing
manager.invalidateSessions('my-agent');
// Next call will rebuild from disk
const sessions = await manager.getAgentSessions('my-agent');

Lists the slash commands available to an agent — for populating a command palette or autocomplete — in a single call.

await manager.listAgentCommands(
agentName: string,
options?: ChatSessionOptions
): Promise<SlashCommand[]>
ParameterTypeRequiredDescription
agentNamestringYesAgent qualified name or local name
options.workingDirectorystringNoPer-call working-directory override so the list reflects the intended project context
options.injectedMcpServersRecord<string, InjectedMcpServerDef>NoMCP servers to inject, so their commands appear in the list
interface SlashCommand {
name: string; // Command name, without the leading slash
description: string; // Human-readable description
argumentHint: string; // Hint for arguments (e.g. "<pr>"), "" if none
}

Returns the full command list — built-ins plus project .claude/commands plus any MCP-provided commands — exactly as the CLI reports them for the resolved session’s cwd and config.

A one-shot convenience over openChatSession(): it opens a streaming session, reads its command list, and always closes the session (in a finally, even if the listing throws), so consumers never have to manage the underlying claude subprocess lifecycle themselves.

Accepts the same ChatSessionOptions as openChatSession. The session runs on the SDK runtime regardless of the agent’s configured runtime, so this works for cli-runtime (Claude-subscription) agents too.

Cost: each call spawns and tears down a claude subprocess (~seconds). The command list is essentially static per project, so callers that query it repeatedly should cache the result.

ErrorCondition
InvalidStateErrorFleet manager not initialized
AgentNotFoundErrorAgent doesn’t exist
StreamingSessionUnsupportedErrorAgent is Docker-wrapped (surfaced unchanged from openChatSession)
import type { SlashCommand } from '@herdctl/core';
const commands: SlashCommand[] = await manager.listAgentCommands('my-agent');
for (const cmd of commands) {
console.log(`/${cmd.name} ${cmd.argumentHint}${cmd.description}`);
}

trigger(agentName, scheduleName?, options?)

Section titled “trigger(agentName, scheduleName?, options?)”

Manually triggers an agent outside its normal schedule.

await manager.trigger(
agentName: string,
scheduleName?: string,
options?: TriggerOptions
): Promise<TriggerResult>
ParameterTypeRequiredDescription
agentNamestringYesName of the agent to trigger
scheduleNamestringNoSchedule to use for configuration (prompt, work source)
options.promptstringNoOverride the prompt for this trigger
options.workItemsWorkItem[]NoCustom work items instead of fetching from work source
options.bypassConcurrencyLimitbooleanNoForce trigger even if agent is at capacity (default: false)
options.workingDirectorystringNoOverride the agent’s configured working directory for this trigger only
interface TriggerResult {
jobId: string; // Unique job identifier
agentName: string;
scheduleName: string | null;
startedAt: string; // ISO timestamp
prompt?: string; // Prompt used for trigger
}
ErrorCondition
InvalidStateErrorFleet manager not initialized
AgentNotFoundErrorAgent doesn’t exist
ScheduleNotFoundErrorSpecified schedule doesn’t exist
ConcurrencyLimitErrorAgent at capacity and bypass not enabled
// Trigger with agent defaults
const job = await manager.trigger('my-agent');
console.log(`Created job: ${job.jobId}`);
// Trigger a specific schedule
const job = await manager.trigger('my-agent', 'hourly');
// Trigger with custom prompt
const job = await manager.trigger('my-agent', undefined, {
prompt: 'Review the latest security updates',
});
// Force trigger even at capacity
const job = await manager.trigger('my-agent', undefined, {
bypassConcurrencyLimit: true,
});
// Trigger with custom working directory
const job = await manager.trigger('my-agent', undefined, {
workingDirectory: '/path/to/specific/project',
prompt: 'Analyze this project',
});

Cancels a running job gracefully.

await manager.cancelJob(
jobId: string,
options?: { timeout?: number }
): Promise<CancelJobResult>
ParameterTypeRequiredDefaultDescription
jobIdstringYesID of the job to cancel
options.timeoutnumberNo10000Time in ms to wait for graceful shutdown before SIGKILL
interface CancelJobResult {
jobId: string;
success: boolean;
terminationType: 'graceful' | 'forced' | 'already_stopped';
canceledAt: string; // ISO timestamp
}

Cancels a running job by first sending SIGTERM to allow graceful shutdown. If the job doesn’t terminate within the timeout, it will be forcefully killed with SIGKILL.

ErrorCondition
InvalidStateErrorFleet manager not initialized
JobNotFoundErrorJob doesn’t exist
// Cancel with default timeout (10s)
const result = await manager.cancelJob('job-2024-01-15-abc123');
console.log(`Cancelled: ${result.terminationType}`);
// Cancel with custom timeout
const result = await manager.cancelJob('job-2024-01-15-abc123', {
timeout: 30000, // 30 seconds
});

Creates a new job based on an existing job’s configuration.

await manager.forkJob(
jobId: string,
modifications?: JobModifications
): Promise<ForkJobResult>
ParameterTypeRequiredDescription
jobIdstringYesID of the job to fork
modifications.promptstringNoOverride the prompt
modifications.schedulestringNoOverride the schedule name
modifications.workItemsWorkItem[]NoReplace work items
interface ForkJobResult {
jobId: string; // New job ID
forkedFromJobId: string; // Original job ID
agentName: string;
startedAt: string; // ISO timestamp
prompt?: string;
}

Creates a new job based on an existing job’s configuration. The new job will have the same agent and can optionally have modifications applied. If the original job has a session ID, the new job will fork from that session, preserving conversation context.

ErrorCondition
InvalidStateErrorFleet manager not initialized
JobNotFoundErrorOriginal job doesn’t exist
JobForkErrorJob cannot be forked
// Fork with same configuration
const result = await manager.forkJob('job-2024-01-15-abc123');
console.log(`Forked to: ${result.jobId}`);
// Fork with modified prompt
const result = await manager.forkJob('job-2024-01-15-abc123', {
prompt: 'Continue the previous task but focus on testing',
});
// Fork with different schedule
const result = await manager.forkJob('job-2024-01-15-abc123', {
schedule: 'nightly',
});

Streams all fleet logs as an async iterable.

manager.streamLogs(options?: LogStreamOptions): AsyncIterable<LogEntry>
ParameterTypeRequiredDefaultDescription
options.levelLogLevelNo'info'Minimum log level ('debug', 'info', 'warn', 'error')
options.agentNamestringNoFilter to specific agent
options.jobIdstringNoFilter to specific job
options.includeHistorybooleanNotrueReplay history before streaming
options.historyLimitnumberNo1000Max historical entries
interface LogEntry {
timestamp: string; // ISO timestamp
level: 'debug' | 'info' | 'warn' | 'error';
source: 'fleet' | 'agent' | 'job' | 'scheduler';
agentName?: string;
jobId?: string;
scheduleName?: string;
message: string;
data?: Record<string, unknown>;
}
// Stream all info+ logs
for await (const log of manager.streamLogs()) {
console.log(`[${log.level}] ${log.message}`);
}
// Stream only errors for a specific agent
for await (const log of manager.streamLogs({
level: 'error',
agentName: 'my-agent',
})) {
console.error(log.message);
}

Streams output from a specific job.

manager.streamJobOutput(jobId: string): AsyncIterable<LogEntry>

For completed jobs, this will replay the job’s history and then complete. For running jobs, it will continue streaming until the job completes.

ErrorCondition
JobNotFoundErrorJob doesn’t exist
for await (const log of manager.streamJobOutput('job-2024-01-15-abc123')) {
console.log(`[${log.level}] ${log.message}`);
}

Streams logs for a specific agent.

manager.streamAgentLogs(agentName: string): AsyncIterable<LogEntry>
ErrorCondition
AgentNotFoundErrorAgent doesn’t exist
for await (const log of manager.streamAgentLogs('my-agent')) {
console.log(`[${log.jobId}] ${log.message}`);
}

Returns the final output of a completed job as a string.

await manager.getJobFinalOutput(jobId: string): Promise<string>
ParameterTypeRequiredDescription
jobIdstringYesID of the job to get output from

Returns a Promise<string> containing the job’s final output. Throws JobNotFoundError if the job doesn’t exist.

const output = await manager.getJobFinalOutput('job-2024-01-15-abc123');
console.log(output);
// "I checked the prices for office chairs..."

Returns the current fleet manager state (read-only property).

manager.state: FleetManagerState
interface FleetManagerState {
status: FleetManagerStatus;
initializedAt: string | null;
startedAt: string | null;
stoppedAt: string | null;
agentCount: number;
lastError: string | null;
}
type FleetManagerStatus =
| 'uninitialized' // Initial state
| 'initialized' // After initialize()
| 'starting' // During start()
| 'running' // Scheduler active
| 'stopping' // During stop()
| 'stopped' // After stop()
| 'error'; // Error occurred
console.log(`Status: ${manager.state.status}`);
console.log(`Agents: ${manager.state.agentCount}`);
console.log(`Started at: ${manager.state.startedAt}`);

Returns the loaded configuration.

manager.getConfig(): ResolvedConfig | null

Returns null if not initialized.


Returns the loaded agents array.

manager.getAgents(): ResolvedAgent[]

Returns empty array if not initialized.


The FleetManager extends EventEmitter and provides strongly-typed events.

interface FleetManagerEventMap {
// Lifecycle
'initialized': [];
'started': [];
'stopped': [];
'error': [error: Error];
'config:reloaded': [payload: ConfigReloadedPayload];
// Agent events
'agent:started': [payload: AgentStartedPayload];
'agent:stopped': [payload: AgentStoppedPayload];
// Schedule events
'schedule:triggered': [payload: ScheduleTriggeredPayload];
'schedule:skipped': [payload: ScheduleSkippedPayload];
// Job events
'job:created': [payload: JobCreatedPayload];
'job:output': [payload: JobOutputPayload];
'job:completed': [payload: JobCompletedPayload];
'job:failed': [payload: JobFailedPayload];
'job:cancelled': [payload: JobCancelledPayload];
'job:forked': [payload: JobForkedPayload];
}
interface JobCreatedPayload {
job: JobMetadata;
agentName: string;
scheduleName?: string;
timestamp: string;
}
interface JobOutputPayload {
jobId: string;
agentName: string;
output: string;
outputType: 'stdout' | 'stderr' | 'assistant' | 'tool' | 'system';
timestamp: string;
}
interface JobCompletedPayload {
job: JobMetadata;
agentName: string;
exitReason: ExitReason;
durationSeconds: number;
timestamp: string;
}
interface JobFailedPayload {
job: JobMetadata;
agentName: string;
error: Error;
exitReason: ExitReason;
durationSeconds?: number;
timestamp: string;
}
interface JobCancelledPayload {
job: JobMetadata;
agentName: string;
terminationType: 'graceful' | 'forced' | 'already_stopped';
durationSeconds?: number;
timestamp: string;
}
interface JobForkedPayload {
job: JobMetadata;
originalJob: JobMetadata;
agentName: string;
timestamp: string;
}
const manager = new FleetManager({ stateDir: './.herdctl' });
// Lifecycle events
manager.on('initialized', () => {
console.log('Fleet initialized');
});
manager.on('started', () => {
console.log('Fleet started');
});
manager.on('stopped', () => {
console.log('Fleet stopped');
});
manager.on('error', (error) => {
console.error('Fleet error:', error.message);
});
// Job events
manager.on('job:created', (payload) => {
console.log(`Job ${payload.job.id} created for ${payload.agentName}`);
});
manager.on('job:output', (payload) => {
process.stdout.write(payload.output);
});
manager.on('job:completed', (payload) => {
console.log(`Job completed in ${payload.durationSeconds}s`);
});
manager.on('job:failed', (payload) => {
console.error(`Job failed: ${payload.error.message}`);
});
// Schedule events
manager.on('schedule:triggered', (payload) => {
console.log(`${payload.agentName}/${payload.scheduleName} triggered`);
});
manager.on('schedule:skipped', (payload) => {
console.log(`${payload.agentName}/${payload.scheduleName} skipped: ${payload.reason}`);
});
// Config reload
manager.on('config:reloaded', (payload) => {
console.log(`Config reloaded with ${payload.changes.length} changes`);
});

All errors extend FleetManagerError and include error codes for programmatic handling.

FleetManagerError (base)
├── ConfigurationError // Config invalid/not found
├── AgentNotFoundError // Agent not in config
├── JobNotFoundError // Job doesn't exist
├── ScheduleNotFoundError // Schedule not configured
├── InvalidStateError // Operation invalid for state
├── ConcurrencyLimitError // Agent at max capacity
├── JobCancelError // Job cancellation failed
├── JobForkError // Job fork failed
├── FleetManagerConfigError // Legacy config error
├── FleetManagerStateError // Legacy state error
├── FleetManagerStateDirError // State directory init failed
└── FleetManagerShutdownError // Shutdown failed
const FleetManagerErrorCode = {
FLEET_MANAGER_ERROR: 'FLEET_MANAGER_ERROR',
CONFIGURATION_ERROR: 'CONFIGURATION_ERROR',
CONFIG_LOAD_ERROR: 'CONFIG_LOAD_ERROR',
AGENT_NOT_FOUND: 'AGENT_NOT_FOUND',
JOB_NOT_FOUND: 'JOB_NOT_FOUND',
SCHEDULE_NOT_FOUND: 'SCHEDULE_NOT_FOUND',
INVALID_STATE: 'INVALID_STATE',
STATE_DIR_ERROR: 'STATE_DIR_ERROR',
CONCURRENCY_LIMIT: 'CONCURRENCY_LIMIT',
SHUTDOWN_ERROR: 'SHUTDOWN_ERROR',
JOB_CANCEL_ERROR: 'JOB_CANCEL_ERROR',
JOB_FORK_ERROR: 'JOB_FORK_ERROR',
};
import {
isFleetManagerError,
isConfigurationError,
isAgentNotFoundError,
isJobNotFoundError,
isScheduleNotFoundError,
isInvalidStateError,
isConcurrencyLimitError,
isJobCancelError,
isJobForkError,
} from '@herdctl/core';
try {
await manager.trigger('unknown-agent');
} catch (error) {
if (isAgentNotFoundError(error)) {
console.log(`Agent "${error.agentName}" not found`);
console.log(`Available: ${error.availableAgents?.join(', ')}`);
} else if (isConcurrencyLimitError(error)) {
console.log(`At capacity: ${error.currentJobs}/${error.limit}`);
}
}
class AgentNotFoundError extends FleetManagerError {
agentName: string; // The agent that wasn't found
availableAgents?: string[]; // List of valid agents
}

Here’s a complete example showing typical usage patterns:

import { FleetManager, isAgentNotFoundError } from '@herdctl/core';
async function main() {
const manager = new FleetManager({
configPath: './herdctl.yaml',
stateDir: './.herdctl',
checkInterval: 5000,
});
// Set up event handlers
manager.on('initialized', () => console.log('Fleet initialized'));
manager.on('started', () => console.log('Fleet started'));
manager.on('stopped', () => console.log('Fleet stopped'));
manager.on('job:created', (payload) => {
console.log(`Job ${payload.job.id} created for ${payload.agentName}`);
});
manager.on('job:output', (payload) => {
process.stdout.write(payload.output);
});
manager.on('job:completed', (payload) => {
console.log(`Job ${payload.job.id} completed in ${payload.durationSeconds}s`);
});
manager.on('job:failed', (payload) => {
console.error(`Job ${payload.job.id} failed: ${payload.error.message}`);
});
// Initialize and start
await manager.initialize();
await manager.start();
// Get fleet status
const status = await manager.getFleetStatus();
console.log(`Running ${status.counts.totalAgents} agents`);
// List all agents
const agents = await manager.getAgentInfo();
for (const agent of agents) {
console.log(`${agent.name}: ${agent.status}`);
}
// Manually trigger an agent
try {
const result = await manager.trigger('my-agent', 'check-issues', {
prompt: 'Check for urgent issues only',
});
console.log(`Triggered job: ${result.jobId}`);
} catch (error) {
if (isAgentNotFoundError(error)) {
console.error(`Agent not found: ${error.agentName}`);
}
}
// Handle shutdown
process.on('SIGINT', async () => {
console.log('Shutting down...');
await manager.stop({
timeout: 30000,
cancelOnTimeout: true,
});
process.exit(0);
});
}
main().catch(console.error);

The @herdctl/core package is written in TypeScript and provides full type definitions. All types are exported from the main package:

import type {
// Options and configuration
FleetManagerOptions,
FleetManagerLogger,
// State types
FleetManagerState,
FleetManagerStatus,
// Query result types
FleetStatus,
FleetCounts,
AgentInfo,
ScheduleInfo,
// Action types
TriggerOptions,
TriggerResult,
JobModifications,
CancelJobResult,
ForkJobResult,
// Stop options
FleetManagerStopOptions,
// Log streaming types
LogLevel,
LogSource,
LogEntry,
LogStreamOptions,
// Event types
FleetManagerEventMap,
FleetManagerEventName,
ConfigReloadedPayload,
ConfigChange,
JobCreatedPayload,
JobOutputPayload,
JobCompletedPayload,
JobFailedPayload,
JobCancelledPayload,
JobForkedPayload,
ScheduleTriggeredPayload,
ScheduleSkippedPayload,
} from '@herdctl/core';