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.
Installation
Section titled “Installation”npm install @herdctl/core# orpnpm add @herdctl/coreQuick Start
Section titled “Quick Start”import { FleetManager } from '@herdctl/core';
const manager = new FleetManager({ configPath: './herdctl.yaml', stateDir: './.herdctl',});
await manager.initialize();await manager.start();
// Subscribe to eventsmanager.on('job:created', (payload) => { console.log(`Job ${payload.job.id} created for ${payload.agentName}`);});
// Graceful shutdownprocess.on('SIGINT', async () => { await manager.stop(); process.exit(0);});Constructor
Section titled “Constructor”new FleetManager(options)
Section titled “new FleetManager(options)”Creates a new FleetManager instance.
const manager = new FleetManager(options: FleetManagerOptions);Parameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
options.stateDir | string | Yes | — | Path to the state directory (e.g., .herdctl). Created if it doesn’t exist. Stores job artifacts, session state, and logs. |
options.configPath | string | No | Auto-discover | Path to herdctl.yaml. Can be absolute, relative, or a directory path. If not provided, searches up from cwd. |
options.logger | FleetManagerLogger | No | Console logger | Custom logger with debug, info, warn, error methods. |
options.checkInterval | number | No | 1000 | Interval in milliseconds between scheduler checks. |
options.configOverrides | FleetConfigOverrides | No | — | Runtime overrides applied after the config file is loaded and parsed — lets CLI flags or programmatic callers override specific fleet-level values. Currently supports web (enabled, port, host). |
interface FleetConfigOverrides { web?: { enabled?: boolean; // Enable/disable the web dashboard port?: number; // Override the web dashboard port host?: string; // Override the web dashboard host };}Example
Section titled “Example”import { FleetManager } from '@herdctl/core';
// Minimal configurationconst manager = new FleetManager({ stateDir: './.herdctl',});
// Full configurationconst 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}`), },});Lifecycle Methods
Section titled “Lifecycle Methods”initialize()
Section titled “initialize()”Initializes the fleet manager by loading configuration and preparing the state directory.
await manager.initialize(): Promise<void>Description
Section titled “Description”This method:
- Loads and validates the configuration file
- Initializes the state directory structure
- Prepares the scheduler (but does not start it)
After initialization, the fleet manager is ready to start.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Already initialized or running |
ConfigurationError | Configuration is invalid or not found |
FleetManagerStateDirError | State directory cannot be created |
Example
Section titled “Example”import { FleetManager, ConfigurationError } from '@herdctl/core';
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 ConfigurationError) { console.error('Configuration error:', error.message); }}initializeWebOnly(options?)
Section titled “initializeWebOnly(options?)”Initializes the fleet manager in web-only mode, without a configuration file.
await manager.initializeWebOnly(options?: { port?: number; host?: string }): Promise<void>Parameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
options.port | number | No | 3232 | Port for the web dashboard |
options.host | string | No | 'localhost' | Host for the web dashboard |
Description
Section titled “Description”Builds a minimal in-memory config with zero agents and the web dashboard enabled, so the dashboard can serve session data from ~/.claude/ without requiring a herdctl.yaml fleet configuration. Any constructor configOverrides (e.g. web.port) are still applied on top of the built-in defaults.
Like initialize(), this prepares the state directory, session lifecycle, and scheduler, and emits 'initialized' on success.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Not in uninitialized, stopped, or error state |
Example
Section titled “Example”const manager = new FleetManager({ stateDir: './.herdctl' });
// No herdctl.yaml needed — just the dashboard over ~/.claude sessionsawait manager.initializeWebOnly({ port: 4000 });await manager.start();start()
Section titled “start()”Starts the fleet manager scheduler, which begins processing agent schedules.
await manager.start(): Promise<void>Description
Section titled “Description”This begins the scheduler, which will:
- Check agent schedules at the configured interval
- Trigger agents when their schedules are due
- Track schedule state in the state directory
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Not initialized |
Example
Section titled “Example”await manager.initialize();await manager.start();
// The manager is now running and processing schedulesmanager.on('schedule:triggered', (payload) => { console.log(`Triggered ${payload.agentName}/${payload.scheduleName}`);});stop(options?)
Section titled “stop(options?)”Gracefully stops the fleet manager.
await manager.stop(options?: FleetManagerStopOptions): Promise<void>Parameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
options.waitForJobs | boolean | No | true | Wait for running jobs to complete before stopping |
options.timeout | number | No | 30000 | Maximum time in ms to wait for jobs to complete |
options.cancelOnTimeout | boolean | No | false | Cancel jobs that don’t complete within timeout |
options.cancelTimeout | number | No | 10000 | Passed through to each job’s cancelJob() call; currently unused — cancellation aborts runs immediately |
Description
Section titled “Description”This will:
- Signal the scheduler to stop accepting new triggers
- Wait for running jobs to complete (with timeout)
- If timeout is reached and
cancelOnTimeoutis true, cancel remaining jobs - Persist all state before shutdown completes
- Emit
'stopped'event when complete
Throws
Section titled “Throws”| Error | Condition |
|---|---|
FleetManagerShutdownError | Shutdown times out and cancelOnTimeout is false |
Example
Section titled “Example”// Normal shutdown - wait for jobs with default 30s timeoutawait manager.stop();
// Shutdown with custom timeoutawait manager.stop({ timeout: 60000 });
// Shutdown without waiting for jobs (not recommended)await manager.stop({ waitForJobs: false });
// Cancel jobs if they don't complete in timeawait manager.stop({ timeout: 30000, cancelOnTimeout: true, cancelTimeout: 10000,});reload()
Section titled “reload()”Hot-reloads configuration without restarting the fleet.
await manager.reload(): Promise<ConfigReloadedPayload>Returns
Section titled “Returns”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)}Description
Section titled “Description”This method provides hot configuration reload capability:
- Loads and validates the new configuration
- If validation fails, keeps the old configuration (fails gracefully)
- Running jobs continue with their original configuration
- New jobs will use the new configuration
- Updates the scheduler with new agent definitions
- Emits a
'config:reloaded'event with change details
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager is not initialized |
ConfigurationError | New configuration is invalid |
Example
Section titled “Example”// Reload configurationconst 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 eventsmanager.on('config:reloaded', (payload) => { console.log(`Config reloaded: ${payload.agentCount} agents`);});Query Methods
Section titled “Query Methods”getFleetStatus()
Section titled “getFleetStatus()”Returns a comprehensive snapshot of fleet state.
await manager.getFleetStatus(): Promise<FleetStatus>Returns
Section titled “Returns”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;}Example
Section titled “Example”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`);getAgentInfo()
Section titled “getAgentInfo()”Returns information about all configured agents.
await manager.getAgentInfo(): Promise<AgentInfo[]>Returns
Section titled “Returns”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;}Example
Section titled “Example”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}`);}getAgentInfoByName(name)
Section titled “getAgentInfoByName(name)”Returns information about a specific agent.
await manager.getAgentInfoByName(name: string): Promise<AgentInfo>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The agent name to look up |
Throws
Section titled “Throws”| Error | Condition |
|---|---|
AgentNotFoundError | No agent with that name exists |
Example
Section titled “Example”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(', ')}`); }}getSchedules()
Section titled “getSchedules()”Returns all schedules across all agents.
await manager.getSchedules(): Promise<ScheduleInfo[]>Returns
Section titled “Returns”interface ScheduleInfo { name: string; // Schedule name agentName: string; // Owning agent type: string; // 'interval', 'cron', 'webhook', 'chat' interval?: string; // e.g., "5m", "1h" (interval schedules) cron?: string; // Cron expression (cron schedules) status: 'idle' | 'running' | 'disabled'; lastRunAt: string | null; // ISO timestamp nextRunAt: string | null; // ISO timestamp lastError: string | null;}Example
Section titled “Example”const schedules = await manager.getSchedules();for (const schedule of schedules) { console.log(`${schedule.agentName}/${schedule.name}: ${schedule.status}`); console.log(` Next run: ${schedule.nextRunAt}`);}getSchedule(agentName, scheduleName)
Section titled “getSchedule(agentName, scheduleName)”Returns a specific schedule by agent and schedule name.
await manager.getSchedule(agentName: string, scheduleName: string): Promise<ScheduleInfo>Throws
Section titled “Throws”| Error | Condition |
|---|---|
AgentNotFoundError | Agent doesn’t exist |
ScheduleNotFoundError | Schedule doesn’t exist for agent |
Example
Section titled “Example”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}`);enableSchedule(agentName, scheduleName)
Section titled “enableSchedule(agentName, scheduleName)”Enables a previously disabled schedule.
await manager.enableSchedule(agentName: string, scheduleName: string): Promise<ScheduleInfo>Description
Section titled “Description”Enables a schedule that was previously disabled, allowing it to trigger again on its configured interval. The enabled state is persisted and survives restarts.
Example
Section titled “Example”const schedule = await manager.enableSchedule('my-agent', 'hourly');console.log(`Schedule status: ${schedule.status}`); // 'idle'disableSchedule(agentName, scheduleName)
Section titled “disableSchedule(agentName, scheduleName)”Disables a schedule temporarily.
await manager.disableSchedule(agentName: string, scheduleName: string): Promise<ScheduleInfo>Description
Section titled “Description”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.
Example
Section titled “Example”// Disable a schedule temporarilyconst schedule = await manager.disableSchedule('my-agent', 'hourly');console.log(`Schedule status: ${schedule.status}`); // 'disabled'
// Later, re-enable itawait manager.enableSchedule('my-agent', 'hourly');Programmatic Agent Management
Section titled “Programmatic Agent Management”addAgent(agent, options?)
Section titled “addAgent(agent, options?)”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>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
agent | AgentConfig | Yes | Agent configuration object |
options.baseDir | string | No | Base directory for resolving relative working_directory paths (defaults to config directory or cwd) |
options.mergeDefaults | boolean | No | Whether to merge fleet defaults into the agent (default: true) |
options.replace | boolean | No | Replace existing agent with same name instead of throwing (default: false) |
Returns
Section titled “Returns”interface AgentInfo { name: string; qualifiedName: string; status: 'idle' | 'running' | 'error'; // ... (see getAgentInfo for full type)}Description
Section titled “Description”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.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
ConfigurationError | Validation fails or qualified name collides with existing agent (and replace is not set) |
Example
Section titled “Example”// Register a new agentconst 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 agentawait manager.addAgent( { name: 'my-agent', prompt: 'Updated prompt', }, { replace: true });
// Add agent without merging defaultsawait manager.addAgent( { name: 'standalone-agent', prompt: 'I have my own config', }, { mergeDefaults: false });removeAgent(name)
Section titled “removeAgent(name)”Unregisters an agent at runtime.
await manager.removeAgent(name: string): Promise<boolean>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Agent qualified name or local name to remove |
Returns
Section titled “Returns”Returns true if an agent was removed, false if no match was found.
Description
Section titled “Description”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.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
Example
Section titled “Example”// Remove by nameconst removed = await manager.removeAgent('my-agent');if (removed) { console.log('Agent removed');} else { console.log('Agent not found');}
// Remove by qualified nameawait manager.removeAgent('myfleet.my-agent');Session Management Methods
Section titled “Session Management Methods”getAgentSessions(name, options?)
Section titled “getAgentSessions(name, options?)”Lists discovered Claude Code sessions for an agent.
await manager.getAgentSessions( name: string, options?: { limit?: number }): Promise<DiscoveredSession[]>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Agent qualified name or local name |
options.limit | number | No | Optional limit for top-N enrichment |
Returns
Section titled “Returns”interface DiscoveredSession { sessionId: string; workingDirectory: string; // Directory the session belongs to mtime: string; // Last-modified time (ISO 8601) origin: SessionOrigin; // 'web' | 'discord' | 'slack' | 'schedule' | 'native' agentName: string | undefined; // Attributed agent, if known resumable: boolean; // Whether the session can be resumed customName: string | undefined; // Custom display name if set autoName: string | undefined; // Auto-generated name (from JSONL summary) preview: string | undefined; // First-message preview (only if metadata was loaded)}Description
Section titled “Description”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.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
AgentNotFoundError | Agent doesn’t exist |
Example
Section titled “Example”const sessions = await manager.getAgentSessions('my-agent');for (const session of sessions) { console.log(`${session.sessionId}: ${session.customName ?? session.autoName ?? session.preview}`); console.log(` Last modified: ${session.mtime}`); console.log(` Origin: ${session.origin}, resumable: ${session.resumable}`);}
// Get top 10 sessionsconst recent = await manager.getAgentSessions('my-agent', { limit: 10 });getAgentSessionMessages(name, sessionId)
Section titled “getAgentSessionMessages(name, sessionId)”Gets the parsed chat messages for one of an agent’s sessions.
await manager.getAgentSessionMessages( name: string, sessionId: string): Promise<ChatMessage[]>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Agent qualified name or local name |
sessionId | string | Yes | Session ID to read |
Returns
Section titled “Returns”interface ChatMessage { role: 'user' | 'assistant' | 'tool'; content: string; timestamp: string; // ISO 8601 toolCall?: ChatToolCall; // Present for role: 'tool' (paired tool_use/tool_result) uuid?: string; // Stable id from the source transcript entry}The uuid field is the stable ID of the message’s source JSONL transcript entry. It is assigned when the line is written, is append-only, and remains stable across reloads and session forks — making it suitable for keying per-message UI state (e.g. React list keys, collapse state, deep links). For a paired tool message, it is the originating tool_use entry’s uuid, so the ID is deterministic even when several tool results share one user line. It is undefined when the source line carried no uuid.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
AgentNotFoundError | Agent doesn’t exist |
Example
Section titled “Example”const messages = await manager.getAgentSessionMessages( 'my-agent', 'session-abc123');
for (const msg of messages) { console.log(`[${msg.role}] ${msg.content}`);}getAgentSessionUsage(name, sessionId)
Section titled “getAgentSessionUsage(name, sessionId)”Gets token usage data for one of an agent’s sessions.
await manager.getAgentSessionUsage( name: string, sessionId: string): Promise<SessionUsage>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Agent qualified name or local name |
sessionId | string | Yes | Session ID to read |
Returns
Section titled “Returns”interface SessionUsage { inputTokens: number; // Last turn's input + cache tokens turnCount: number; // Total turns in session hasData: boolean; // Whether usage data exists}Description
Section titled “Description”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.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
AgentNotFoundError | Agent doesn’t exist |
Example
Section titled “Example”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}`);}deleteSession(name, sessionId)
Section titled “deleteSession(name, sessionId)”Deletes one of an agent’s Claude Code session transcripts from disk.
await manager.deleteSession( name: string, sessionId: string): Promise<boolean>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Agent qualified name or local name |
sessionId | string | Yes | Session ID whose transcript should be removed |
Returns
Section titled “Returns”Returns true if a file was removed, false if no transcript existed (or the agent has no working directory).
Description
Section titled “Description”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.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
AgentNotFoundError | Agent doesn’t exist |
Error | sessionId contains invalid characters |
Example
Section titled “Example”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>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Agent qualified name or local name |
sessionId | string | Yes | Session ID to (re)name |
customName | string | null | Yes | Custom name to set, or null/empty to clear it |
Description
Section titled “Description”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.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
AgentNotFoundError | Agent doesn’t exist |
Example
Section titled “Example”// Set custom nameawait manager.setSessionName('my-agent', 'session-abc123', 'Bug Fix Discussion');
// Clear custom nameawait manager.setSessionName('my-agent', 'session-abc123', null);invalidateSessions(name)
Section titled “invalidateSessions(name)”Drops the cached session listing for an agent so the next getAgentSessions() call rebuilds it from disk.
manager.invalidateSessions(name: string): voidParameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Agent qualified name or local name |
Description
Section titled “Description”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.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
AgentNotFoundError | Agent doesn’t exist |
Example
Section titled “Example”// Force refresh session listingmanager.invalidateSessions('my-agent');
// Next call will rebuild from diskconst sessions = await manager.getAgentSessions('my-agent');openChatSession(agentName, options?)
Section titled “openChatSession(agentName, options?)”Opens a long-lived, multi-turn streaming chat session with an agent and returns a live RuntimeSession handle.
await manager.openChatSession( agentName: string, options?: ChatSessionOptions): Promise<RuntimeSession>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
agentName | string | Yes | Agent qualified name or local name |
options.prompt | string | No | Optional initial user turn to send when the session opens. When omitted, the session opens idle and the first turn is sent via session.send(...) |
options.resume | string | null | No | Session ID to resume. string resumes that session; null explicitly starts fresh (skips agent-level fallback); undefined falls back to the agent’s stored session |
options.workingDirectory | string | No | Override the agent’s configured working directory for this session only |
options.injectedMcpServers | Record<string, InjectedMcpServerDef> | No | MCP servers to inject at runtime (in-process SDK servers) |
options.systemPromptAppend | string | No | Text to append to the agent’s system prompt for this session |
options.manageLifecycle | boolean | No | Opt in to herdctl-managed session lifecycle (reap-on-idle + durable wakes). See Session Lifecycle Methods. Default: false (caller owns close()) |
Returns
Section titled “Returns”A live RuntimeSession:
interface RuntimeSession { // The live SDK message stream for the session. Iterate to receive output. readonly messages: AsyncIterable<SDKMessage>;
// Send a user turn. A leading-slash string (e.g. "/compact", "/clear") is // dispatched by Claude Code as a slash command — commands are just user // messages whose text is the command. send(text: string): Promise<void>;
// Interrupt the current turn without closing the session. // Further send() calls remain valid. interrupt(): Promise<void>;
// List the slash commands available in this session // (name, description, argument hint). Does not run anything. listCommands(): Promise<SlashCommand[]>;
// Change the model used for subsequent turns in this session. setModel(model?: string): Promise<void>;
// Close the session, ending the input stream and shutting down the query. close(): Promise<void>;}Description
Section titled “Description”Unlike trigger(), openChatSession() does not create a job record and does not drain the run to completion — the caller drives the session across turns and must call session.close() when finished. This is the primitive behind interactive chat UIs: multi-turn streaming input, mid-turn interrupt(), and slash-command discovery via listCommands().
The session always runs on the SDK runtime, regardless of the agent’s configured runtime. This is safe for cli-configured agents (same CLAUDE_CODE_OAUTH_TOKEN auth, shared on-disk session store) and is what unlocks the SDK control requests (interrupt, listCommands, streaming send), which require streaming input/output mode.
Session resume follows the same precedence as trigger(): an explicit options.resume string wins; null forces a fresh session; undefined falls back to the agent’s stored session.
The session ID is not a property of the handle — it arrives inside the message stream, on the system/init message.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
AgentNotFoundError | Agent doesn’t exist |
StreamingSessionUnsupportedError | Agent is Docker-wrapped (docker.enabled: true) — streaming sessions require the SDK runtime |
Example
Section titled “Example”const session = await manager.openChatSession('my-agent', { prompt: 'Summarize the open PRs',});
// Consume the output streamconst reader = (async () => { for await (const message of session.messages) { if (message.type === 'system' && message.subtype === 'init') { console.log(`Session started: ${message.session_id}`); } // ... render assistant text, tool calls, results }})();
// Send follow-up turns — slash commands are just user messagesawait session.send('Now focus on the oldest one');await session.send('/compact');
// Stop a runaway turn without losing the sessionawait session.interrupt();await session.send('Never mind, try a shorter answer');
// Discover available slash commandsconst commands = await session.listCommands();
// Always close when doneawait session.close();await reader;listAgentCommands(agentName, options?)
Section titled “listAgentCommands(agentName, options?)”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[]>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
agentName | string | Yes | Agent qualified name or local name |
options.workingDirectory | string | No | Per-call working-directory override so the list reflects the intended project context |
options.injectedMcpServers | Record<string, InjectedMcpServerDef> | No | MCP servers to inject, so their commands appear in the list |
Returns
Section titled “Returns”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.
Description
Section titled “Description”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.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
AgentNotFoundError | Agent doesn’t exist |
StreamingSessionUnsupportedError | Agent is Docker-wrapped (surfaced unchanged from openChatSession) |
Example
Section titled “Example”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}`);}Session Lifecycle Methods
Section titled “Session Lifecycle Methods”When a streaming chat session is opened with manageLifecycle: true, herdctl manages its lifecycle: the session is reaped (closed) the instant it goes idle — instead of accumulating warm claude processes at roughly 300 MB each — and any timer-class wakeups the agent scheduled in-session (ScheduleWakeup, CronCreate) are captured as durable wake entries and re-fired through the fleet’s scheduler, resuming the session with the wake’s prompt. Wake entries persist in state.yaml under session_wakes, so they survive fleet restarts. See State Persistence for the on-disk format and Sessions for the concept.
getSessionLifecycle()
Section titled “getSessionLifecycle()”Returns the fleet’s session-lifecycle manager, or null before initialize().
manager.getSessionLifecycle(): SessionLifecycleManager | nullDescription
Section titled “Description”The SessionLifecycleManager is the facade over the session reaper and wake registry. It is constructed during initialize() and wired into the scheduler loop (due wakes are dispatched once per scheduler tick). Most applications only need setSessionWakeHandler(), but the manager is exposed for advanced use:
class SessionLifecycleManager { readonly registry: WakeRegistry; // Durable wake entries (reconcile/remove/dispatchDue) readonly reaper: SessionReaper; // Reap-on-idle policy over managed sessions
manage(session: RuntimeSession, agent: string): ManagedSession; dispatchDue(now?: Date): Promise<SessionWakeEntry[]>; setSessionWakeHandler(handler: SessionWakeHandler | undefined): void;}The reap policy is a single rule: a session is kept alive if and only if it holds live background work (running shells, subagents, monitors); otherwise it is reaped as soon as its turn ends. Pending wakeups do not keep a session alive — they are captured durably and re-triggered later. Resuming recovers the full conversation, and Claude’s prompt cache is server-side, so a reap costs only about half a second of respawn time on the next wake.
Example
Section titled “Example”const lifecycle = manager.getSessionLifecycle();if (lifecycle) { // Inspect or force-dispatch due wakes (normally done by the scheduler tick) const fired = await lifecycle.dispatchDue(); console.log(`Fired ${fired.length} due wakes`);}setSessionWakeHandler(handler)
Section titled “setSessionWakeHandler(handler)”Registers the consumer hook that receives sessions resumed by a fired wake.
manager.setSessionWakeHandler( handler: SessionWakeHandler | undefined): void
type SessionWakeHandler = ( session: RuntimeSession, entry: SessionWakeEntry,) => void | Promise<void>;Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
handler | SessionWakeHandler | undefined | Yes | Called with the already-resumed, lifecycle-managed session and the wake entry that fired. Pass undefined to clear |
Description
Section titled “Description”When a wake fires, herdctl resumes the session via openChatSession({ resume, prompt, manageLifecycle: true }) — the wake’s prompt is already injected. If a handler is registered, it receives the live RuntimeSession and is responsible for consuming its message stream (e.g. delivering the woken turn to a chat UI). If no handler is registered, the manager drains the stream headlessly so recurring wakes keep firing.
Because the resumed session is itself lifecycle-managed, the reaper closes it again when the woken turn goes idle, so the handler should treat the message stream ending as normal. Safe to call before or after initialize().
The SessionWakeEntry describes the wake that fired:
interface SessionWakeEntry { id: string; // SDK cron id — the reconciliation key across turns agent: string; // Qualified agent name that owns the session sessionId: string; // Session resumed when the wake fired schedule: string; // Cron expression (a one-shot wakeup encodes a single fire time) recurring: boolean; // false = one-shot; true = re-fires on every match prompt: string; // Prompt injected into the resumed session nextRunAt: string; // Resolved absolute next fire time (ISO) createdAt: string; // ISO capture time — anchors the 7-day recurring expiry}Example
Section titled “Example”manager.setSessionWakeHandler(async (session, entry) => { console.log(`Wake ${entry.id} fired for ${entry.agent} (${entry.sessionId})`); // Deliver the woken turn to your UI for await (const message of session.messages) { renderMessage(message); }});Action Methods
Section titled “Action Methods”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>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
agentName | string | Yes | Name of the agent to trigger |
scheduleName | string | No | Schedule to use for configuration (prompt, work source) |
options.prompt | string | No | Override the prompt for this trigger |
options.resume | string | null | No | Session ID to resume for conversation continuity. string resumes that session; null explicitly starts fresh (skips agent-level fallback); undefined uses the agent-level session fallback |
options.fork | string | No | Session ID to fork: the run resumes this session’s transcript as context but writes all new turns to a brand-new session ID (Claude Code --fork-session), leaving the source session untouched. Mutually exclusive with resume — when both are set, fork takes precedence |
options.forkedFrom | string | No | Parent job ID recorded as the new job’s forked_from lineage. Only meaningful alongside fork; purely informational |
options.workItems | WorkItem[] | No | Custom work items instead of fetching from work source |
options.bypassConcurrencyLimit | boolean | No | Force trigger even if agent is at capacity (default: false) |
options.workingDirectory | string | No | Override the agent’s configured working directory for this trigger only |
options.triggerType | string | No | How the trigger was initiated ("discord", "slack", "web", "manual", …) — connectors set this to identify the source platform (default: "manual") |
options.systemPromptAppend | string | No | Text appended to the agent’s system prompt for this trigger only |
options.injectedMcpServers | Record<string, InjectedMcpServerDef> | No | MCP servers merged with the agent’s config-declared servers at execution time, for runtime tool injection |
options.onMessage | (message: SDKMessage) => void | Promise<void> | No | Callback invoked for each SDK message during execution, for real-time streaming of output |
options.onJobCreated | (jobId: string) => void | Promise<void> | No | Callback invoked as soon as the job ID is created — useful for immediate job control (e.g. enabling cancel while output streams) |
Returns
Section titled “Returns”The returned promise resolves only after the run completes — trigger() executes the job to completion (streaming output via options.onMessage along the way), it does not return a handle to a still-running job. Use options.onJobCreated if you need the job ID while the run is still streaming.
interface TriggerResult { jobId: string; // Unique job identifier agentName: string; scheduleName: string | null; startedAt: string; // ISO timestamp prompt?: string; // Prompt used for trigger success: boolean; // Whether the job completed successfully sessionId?: string; // Claude Agent SDK session ID — the continuity // handle: pass it as options.resume on a later // trigger to continue the conversation. Only // trust it when success is true error?: Error; // Error if the job failed errorDetails?: RunnerErrorDetails; // Detailed error info for programmatic access}Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
AgentNotFoundError | Agent doesn’t exist |
ScheduleNotFoundError | Specified schedule doesn’t exist |
ConcurrencyLimitError | Agent at capacity and bypass not enabled |
InvalidWorkingDirectoryOverrideError | options.workingDirectory is not a non-empty string |
Note that a job that starts but fails during execution does not reject the promise — it resolves with success: false plus error/errorDetails.
Example
Section titled “Example”// Trigger with agent defaults — the await resolves when the run completesconst job = await manager.trigger('my-agent');console.log(`Job ${job.jobId} ${job.success ? 'completed' : 'failed'}`);
// Continue the same conversation later using the returned session IDif (job.success && job.sessionId) { await manager.trigger('my-agent', undefined, { resume: job.sessionId, prompt: 'Follow up on your last answer', });}
// Trigger a specific scheduleconst job = await manager.trigger('my-agent', 'hourly');
// Trigger with custom promptconst job = await manager.trigger('my-agent', undefined, { prompt: 'Review the latest security updates',});
// Force trigger even at capacityconst job = await manager.trigger('my-agent', undefined, { bypassConcurrencyLimit: true,});
// Trigger with custom working directoryconst job = await manager.trigger('my-agent', undefined, { workingDirectory: '/path/to/specific/project', prompt: 'Analyze this project',});
// Fork an existing session into an independent child conversationconst job = await manager.trigger('my-agent', undefined, { fork: 'session-abc123', prompt: 'Try a different approach from here',});cancelJob(jobId, options?)
Section titled “cancelJob(jobId, options?)”Cancels a running job gracefully.
await manager.cancelJob( jobId: string, options?: { timeout?: number }): Promise<CancelJobResult>Parameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
jobId | string | Yes | — | ID of the job to cancel |
options.timeout | number | No | 10000 | Accepted for backwards compatibility but currently unused — cancellation aborts the run immediately (see Description) |
Returns
Section titled “Returns”interface CancelJobResult { jobId: string; success: boolean; terminationType: 'graceful' | 'forced' | 'already_stopped'; canceledAt: string; // ISO timestamp}Description
Section titled “Description”Cancels a running job by aborting its in-flight execution and marking the job record cancelled. The fleet manager keeps an AbortController registered for every job it is currently running; cancelJob() fires that controller’s abort signal, which genuinely interrupts the run:
- CLI runtime — the
claudesubprocess is killed. - SDK runtime — the in-flight SDK query is aborted.
The aborted run is finalized with status: cancelled and exit_reason: cancelled (not failed), and a job:cancelled event is emitted.
If the job is already finished, the call returns early with terminationType: 'already_stopped'. If the job was started by a different process (the registry is per-process), only the job’s status file is updated — the cancellation is best-effort in that case.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
JobNotFoundError | Job doesn’t exist |
Example
Section titled “Example”// Cancel with default timeout (10s)const result = await manager.cancelJob('job-2024-01-15-abc123');console.log(`Cancelled: ${result.terminationType}`);
// Cancel with custom timeoutconst result = await manager.cancelJob('job-2024-01-15-abc123', { timeout: 30000, // 30 seconds});forkJob(jobId, modifications?)
Section titled “forkJob(jobId, modifications?)”Creates a new job based on an existing job’s configuration.
await manager.forkJob( jobId: string, modifications?: JobModifications): Promise<ForkJobResult>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
jobId | string | Yes | ID of the job to fork |
modifications.prompt | string | No | Override the prompt |
modifications.schedule | string | No | Override the schedule name |
modifications.workItems | WorkItem[] | No | Replace work items |
Returns
Section titled “Returns”interface ForkJobResult { jobId: string; // New job ID forkedFromJobId: string; // Original job ID agentName: string; startedAt: string; // ISO timestamp prompt?: string;}Description
Section titled “Description”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.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
InvalidStateError | Fleet manager not initialized |
JobNotFoundError | Original job doesn’t exist |
JobForkError | Job cannot be forked |
Example
Section titled “Example”// Fork with same configurationconst result = await manager.forkJob('job-2024-01-15-abc123');console.log(`Forked to: ${result.jobId}`);
// Fork with modified promptconst result = await manager.forkJob('job-2024-01-15-abc123', { prompt: 'Continue the previous task but focus on testing',});
// Fork with different scheduleconst result = await manager.forkJob('job-2024-01-15-abc123', { schedule: 'nightly',});Log Streaming Methods
Section titled “Log Streaming Methods”streamLogs(options?)
Section titled “streamLogs(options?)”Streams all fleet logs as an async iterable.
manager.streamLogs(options?: LogStreamOptions): AsyncIterable<LogEntry>Parameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
options.level | LogLevel | No | 'info' | Minimum log level ('debug', 'info', 'warn', 'error') |
options.agentName | string | No | — | Filter to specific agent |
options.jobId | string | No | — | Filter to specific job |
options.includeHistory | boolean | No | true | Replay history before streaming |
options.historyLimit | number | No | 1000 | Max historical entries |
Returns
Section titled “Returns”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>;}Example
Section titled “Example”// Stream all info+ logsfor await (const log of manager.streamLogs()) { console.log(`[${log.level}] ${log.message}`);}
// Stream only errors for a specific agentfor await (const log of manager.streamLogs({ level: 'error', agentName: 'my-agent',})) { console.error(log.message);}streamJobOutput(jobId)
Section titled “streamJobOutput(jobId)”Streams output from a specific job.
manager.streamJobOutput(jobId: string): AsyncIterable<LogEntry>Description
Section titled “Description”For completed jobs, this will replay the job’s history and then complete. For running jobs, it will continue streaming until the job completes.
Throws
Section titled “Throws”| Error | Condition |
|---|---|
JobNotFoundError | Job doesn’t exist |
Example
Section titled “Example”for await (const log of manager.streamJobOutput('job-2024-01-15-abc123')) { console.log(`[${log.level}] ${log.message}`);}streamAgentLogs(agentName)
Section titled “streamAgentLogs(agentName)”Streams logs for a specific agent.
manager.streamAgentLogs(agentName: string): AsyncIterable<LogEntry>Throws
Section titled “Throws”| Error | Condition |
|---|---|
AgentNotFoundError | Agent doesn’t exist |
Example
Section titled “Example”for await (const log of manager.streamAgentLogs('my-agent')) { console.log(`[${log.jobId}] ${log.message}`);}getJobFinalOutput(jobId)
Section titled “getJobFinalOutput(jobId)”Returns the final output of a completed job as a string.
await manager.getJobFinalOutput(jobId: string): Promise<string>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
jobId | string | Yes | ID of the job to get output from |
Returns
Section titled “Returns”Returns a Promise<string> containing the job’s final output — the last assistant message with text content, falling back to the last meaningful tool_result. This method never throws: if the job doesn’t exist, its output file can’t be read, or no meaningful output is found, it resolves to an empty string "" (a warning is logged).
Example
Section titled “Example”const output = await manager.getJobFinalOutput('job-2024-01-15-abc123');if (output) { console.log(output); // "I checked the prices for office chairs..."} else { console.log('No output found (unknown job or empty transcript)');}State Accessors
Section titled “State Accessors”Returns the current fleet manager state (read-only property).
manager.state: FleetManagerStateReturns
Section titled “Returns”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 occurredExample
Section titled “Example”console.log(`Status: ${manager.state.status}`);console.log(`Agents: ${manager.state.agentCount}`);console.log(`Started at: ${manager.state.startedAt}`);getConfig()
Section titled “getConfig()”Returns the loaded configuration.
manager.getConfig(): ResolvedConfig | nullReturns null if not initialized.
getAgents()
Section titled “getAgents()”Returns the loaded agents array.
manager.getAgents(): ResolvedAgent[]Returns empty array if not initialized.
Events
Section titled “Events”The FleetManager extends EventEmitter and provides strongly-typed events.
Event Types
Section titled “Event Types”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];
// 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];}Event Payloads
Section titled “Event Payloads”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;}interface ScheduleTriggeredPayload { agentName: string; scheduleName: string; schedule: Schedule; timestamp: string;}interface ConfigReloadedPayload { agentCount: number; agentNames: string[]; configPath: string; changes: ConfigChange[]; timestamp: string;}
interface ConfigChange { type: 'added' | 'removed' | 'modified'; category: 'agent' | 'schedule' | 'defaults'; name: string; details?: string;}Example: Event Subscription
Section titled “Example: Event Subscription”const manager = new FleetManager({ stateDir: './.herdctl' });
// Lifecycle eventsmanager.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 eventsmanager.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 eventsmanager.on('schedule:triggered', (payload) => { console.log(`${payload.agentName}/${payload.scheduleName} triggered`);});
// Config reloadmanager.on('config:reloaded', (payload) => { console.log(`Config reloaded with ${payload.changes.length} changes`);});Error Classes
Section titled “Error Classes”All errors extend FleetManagerError and include error codes for programmatic handling.
Error Hierarchy
Section titled “Error Hierarchy”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├── InvalidWorkingDirectoryOverrideError // Bad per-trigger workingDirectory override├── StreamingSessionUnsupportedError // Agent runtime can't open streaming sessions├── FleetManagerStateDirError // State directory init failed├── FleetManagerShutdownError // Shutdown failed└── SchedulerError // Scheduler subsystem errors ├── IntervalParseError // Invalid interval expression (e.g. "5 minutes") ├── CronParseError // Invalid cron expression ├── ScheduleTriggerError // Schedule trigger failed └── SchedulerShutdownError // Scheduler shutdown failedError Codes
Section titled “Error Codes”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', INVALID_WORKING_DIRECTORY_OVERRIDE: 'INVALID_WORKING_DIRECTORY_OVERRIDE', STREAMING_SESSION_UNSUPPORTED: 'STREAMING_SESSION_UNSUPPORTED', // ... plus agent-distribution codes (SOURCE_PARSE_ERROR, AGENT_INSTALL_ERROR, ...)};Type Guards
Section titled “Type Guards”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}`); }}Error Properties
Section titled “Error Properties”class AgentNotFoundError extends FleetManagerError { agentName: string; // The agent that wasn't found availableAgents?: string[]; // List of valid agents}class ScheduleNotFoundError extends FleetManagerError { agentName: string; scheduleName: string; // The schedule that wasn't found availableSchedules?: string[]; // List of valid schedules}class ConcurrencyLimitError extends FleetManagerError { agentName: string; currentJobs: number; // Currently running limit: number; // Max allowed
isAtLimit(): boolean; // Helper method}class InvalidStateError extends FleetManagerError { operation: string; // The attempted operation currentState: string; // Current fleet state expectedState: string | string[]; // Required state(s)}class StreamingSessionUnsupportedError extends FleetManagerError { runtime?: string; // Runtime that can't stream (e.g. "docker")}Complete Example
Section titled “Complete Example”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);TypeScript Support
Section titled “TypeScript Support”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, FleetConfigOverrides, 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,} from '@herdctl/core';