Skip to content

Orchestrator API

The OrchestratorAPI extends the Quarry API with agent orchestration capabilities. It’s the primary interface for managing agents, dispatching tasks, and coordinating the orchestration loop.

Initialization

import { createOrchestratorAPI } from '@stoneforge/smithy';
import { createStorage, initializeSchema } from '@stoneforge/storage';
const storage = createStorage({ path: '.stoneforge/stoneforge.db' });
initializeSchema(storage);
const api = createOrchestratorAPI(storage);

The OrchestratorAPI inherits all methods from QuarryAPI — you can use api.create(), api.list(), api.ready(), etc. alongside orchestration-specific methods.


Agent registration

Register a Director

const director = await api.registerDirector({
name: 'MainDirector',
createdBy: humanEntityId,
maxConcurrentTasks: 1, // Default: 1
provider: 'claude-code', // Optional
model: 'sonnet', // Optional
executablePath: 'claude', // Optional
});

Register a Worker

const worker = await api.registerWorker({
name: 'Worker-1',
workerMode: 'ephemeral', // 'ephemeral' or 'persistent'
createdBy: directorEntityId,
reportsTo: directorEntityId, // Optional: management hierarchy
maxConcurrentTasks: 2, // Default: 1
roleDefinitionRef: roleDefId, // Optional: link to role definition
});

Register a Steward

// Merge steward (triggered by events)
const steward = await api.registerSteward({
name: 'MergeSteward',
stewardFocus: 'merge', // 'merge' | 'docs' | 'recovery' | 'custom'
triggers: [
{ type: 'event', event: 'task_completed' },
],
createdBy: directorEntityId,
maxConcurrentTasks: 1,
});
// Custom steward with cron schedule
const customSteward = await api.registerSteward({
name: 'CleanupSteward',
stewardFocus: 'custom',
playbook: '## Stale Branch Cleanup\n\n1. List branches older than 14 days\n2. Archive those with no open tasks',
triggers: [
{ type: 'cron', schedule: '0 2 * * *' },
],
createdBy: directorEntityId,
});

Agent queries

// Get specific agent
const agent = await api.getAgent(entityId);
const agentByName = await api.getAgentByName('Worker-1');
// Get the director
const director = await api.getDirector();
// List and filter agents
const allAgents = await api.listAgents();
const workers = await api.listAgents({ role: 'worker' });
const ephemeralWorkers = await api.listAgents({
role: 'worker',
workerMode: 'ephemeral',
});
// Role-specific queries
const stewards = await api.getStewards();
const availableWorkers = await api.getAvailableWorkers();
const allWorkers = await api.getAgentsByRole('worker');

Disabled agents

Agents carry an optional disabled: boolean flag on BaseAgentMetadata. When true, the agent stays visible in queries and listings but is skipped by:

  • getAvailableDirector (returns the next director that is running AND not disabled)
  • The dispatch daemon’s worker, steward, and orphan-recovery candidate-selection loops
  • The steward scheduler’s registerSteward (returns false for disabled stewards, so cron and event triggers are not registered)
  • Session start and resume routes (POST /api/agents/:id/start and POST /api/agents/:id/resume return 409 AGENT_DISABLED)

The flag does NOT terminate in-flight sessions: an agent disabled while running keeps its session until it completes or is explicitly stopped. Only future work is blocked.

Read the flag through helpers rather than touching metadata directly:

import { isAgentDisabled, isAgentEnabled, getAgentMetadata } from '@stoneforge/smithy';
const agent = await api.getAgent(agentId);
if (agent && isAgentDisabled(agent)) {
// Skip dispatch / start / register
}
// Equivalent inverse:
if (agent && isAgentEnabled(agent)) { /* ... */ }

Toggle the flag through updateAgentMetadata. To enable an agent, omit the property entirely (do not set disabled: false) so the JSON-serialised metadata stays in the canonical absent-means-enabled shape:

// Disable
await api.updateAgentMetadata(agentId, { disabled: true });
// Enable (delete the property)
const meta = getAgentMetadata(agent);
const next = { ...meta };
delete next.disabled;
await api.updateAgentMetadata(agentId, next);

The HTTP route PATCH /api/agents/:id accepts { "disabled": true | false } and applies the same canonical shape internally; it additionally reconciles a running steward scheduler (calls registerSteward on enable, unregisterSteward on disable) so the change takes effect immediately.


Agent channels

Each agent gets a dedicated channel on registration. Used for dispatch notifications and inter-agent messaging.

// OrchestratorAPI returns just the ChannelId
const channelId: ChannelId | undefined = await api.getAgentChannel(entityId);
// AgentRegistry returns full Channel object
import { createAgentRegistry } from '@stoneforge/smithy';
const registry = createAgentRegistry(api);
const channel: Channel | undefined = await registry.getAgentChannel(agentId);
const channelId2: ChannelId | undefined = await registry.getAgentChannelId(agentId);
// Channel name utilities
import { generateAgentChannelName, parseAgentChannelName } from '@stoneforge/smithy';
generateAgentChannelName('Worker-1'); // 'agent-Worker-1'
parseAgentChannelName('agent-Worker-1'); // 'Worker-1' or null

Task assignment

Basic assignment

// Auto-generates branch and worktree names
const task = await api.assignTaskToAgent(taskId, workerId);
// With explicit options
const task = await api.assignTaskToAgent(taskId, workerId, {
branch: 'agent/worker-1/task-feat-auth',
worktree: '.stoneforge/.worktrees/worker-1-feat-auth',
sessionId: 'claude-session-123',
markAsStarted: true, // Also sets task status to 'in_progress'
});

Orchestrator task metadata

Each task carries orchestration metadata — branch, worktree, merge status, handoff history, etc.

// Get metadata
const meta = await api.getTaskOrchestratorMeta(taskId);
// Replace entire metadata
await api.setTaskOrchestratorMeta(taskId, fullMeta);
// Partial update (merge)
await api.updateTaskOrchestratorMeta(taskId, {
mergeStatus: 'pending',
});
interface OrchestratorTaskMeta {
assignedAgent?: EntityId;
branch?: string;
worktree?: string;
targetBranch?: string; // Target branch for merging (inherited from director at dispatch)
sessionId?: string;
owningDirector?: EntityId; // Director that created/owns this task
startedAt?: Timestamp;
completedAt?: Timestamp;
mergedAt?: Timestamp;
mergeStatus?: MergeStatus;
mergeFailureReason?: string;
testRunCount?: number;
lastTestResult?: TestResult;
reconciliationCount?: number;
stuckMergeRecoveryCount?: number;
stewardRecoveryCount?: number;
resumeCount?: number;
reportedIssues?: readonly string[];
// Handoff context
handoffBranch?: string;
handoffWorktree?: string;
lastSessionId?: string;
handoffAt?: Timestamp;
handoffFrom?: EntityId;
handoffHistory?: HandoffHistoryEntry[];
// Merge request info
mergeRequestUrl?: string;
mergeRequestId?: number;
mergeRequestProvider?: string;
completionSummary?: string;
lastCommitHash?: string;
// Session history
sessionHistory?: readonly TaskSessionHistoryEntry[];
// Branch sync
lastSyncResult?: SyncResultMeta;
}

MergeStatus

StatusDescription
pendingTask completed, awaiting merge
testingSteward is running tests
mergingTests passed, merge in progress
mergedSuccessfully merged
conflictMerge conflict detected
test_failedTests failed
failedMerge failed (other reason)
not_applicableNo merge needed
awaiting_approvalPR created, waiting for human approval/merge

Session management

// Update agent session state
await api.updateAgentSession(agentId, 'session-123', 'running');

Session states:

type SessionState = 'idle' | 'running' | 'suspended' | 'terminated';

Agent types

AgentEntity

A typed wrapper for entities that have agent metadata:

import { getAgentMetadata } from '@stoneforge/smithy';
// AgentEntity extends Entity with typed agent metadata
interface AgentEntity extends Entity {
metadata: { agent: AgentMetadata } & Record<string, unknown>;
}
// Extract agent metadata from an entity (returns undefined if not an agent)
const agentMeta = getAgentMetadata(entity);
if (agentMeta) {
console.log(agentMeta.agentRole); // 'director' | 'worker' | 'steward'
}

AgentRole

type AgentRole = 'director' | 'worker' | 'steward';

AgentMetadata

Agent metadata is a discriminated union based on agentRole:

interface BaseAgentMetadata {
agentRole: AgentRole;
channelId?: ChannelId;
sessionId?: string;
worktree?: string;
sessionStatus?: 'idle' | 'running' | 'suspended' | 'terminated';
lastActivityAt?: Timestamp;
maxConcurrentTasks?: number;
roleDefinitionRef?: ElementId;
provider?: string;
model?: string;
executablePath?: string;
}
interface DirectorMetadata extends BaseAgentMetadata {
agentRole: 'director';
targetBranch?: string; // Git branch for task merges (falls back to config or auto-detect)
}
interface WorkerMetadata extends BaseAgentMetadata {
agentRole: 'worker';
workerMode: 'ephemeral' | 'persistent';
branch?: string;
}
interface StewardMetadata extends BaseAgentMetadata {
agentRole: 'steward';
stewardFocus: 'merge' | 'docs' | 'recovery' | 'custom';
triggers?: StewardTrigger[];
playbook?: string; // @deprecated - use playbookId instead
playbookId?: string;
lastExecutedAt?: Timestamp;
nextScheduledAt?: Timestamp;
}

StewardTrigger

interface CronTrigger {
type: 'cron';
schedule: string; // e.g., '0 2 * * *'
}
interface EventTrigger {
type: 'event';
event: string; // e.g., 'task_completed'
condition?: string;
}
type StewardTrigger = CronTrigger | EventTrigger;

Type guards

import {
isDirectorMetadata,
isWorkerMetadata,
isStewardMetadata,
isCronTrigger,
isEventTrigger,
} from '@stoneforge/smithy';
if (isWorkerMetadata(agent.metadata.agent)) {
console.log(agent.metadata.agent.workerMode);
}
if (isStewardMetadata(agent.metadata.agent)) {
console.log(agent.metadata.agent.stewardFocus);
}