Crux
API Reference@crux/core

Agents

Agent definitions, composition utilities, blackboard, handoff, and delegate for multi-agent coordination.

Composition utilities

import { agent, isAgent, createCompositions } from '@crux/core/agent'

// Pre-bound versions from adapters:
import { parallel, pipeline, consensus, agent } from '@crux/ai'

agent(config)

Bundle a prompt with optional model and tools into a frozen agent instance.

FieldTypeDescription
idstringUnique identifier
descriptionstring?Human-readable description
promptPromptThe prompt this agent executes
modelAnyModel?Default model override (takes precedence over composition-level model)
toolsAnyToolSet?Default tools

Returns: Agent<TOwnInput, TOutput, TContexts> — frozen object with _tag: 'Agent'

isAgent(value)

Type guard — returns true if the value is an Agent instance created by agent().

parallel(options)

Run multiple agents concurrently and return typed results as a named record.

FieldTypeDescription
agentsRecord<string, AgentLike>Named map of agents (or plain functions)
contextunknownContext data passed to all agents
modelAnyModel?Shared model (agent-level model takes precedence)
onError'fail-fast' | 'continue'?Error handling strategy. Default: 'fail-fast'

Returns: ParallelResult

FieldTypeDescription
resultsRecord<name, AgentResult<typed>>Results keyed by agent name, typed from each agent's output schema
durationMsnumberTotal wall-clock duration
settledRecord<name, SettledResult>?Only present with onError: 'continue' — discriminated results for all agents

In continue mode, each entry in settled is a SettledResult<AgentResult>:

  • { status: 'success', value: AgentResult }
  • { status: 'error', error: Error }

pipeline(options)

Chain agents sequentially with typed data flow between immediate steps via a context accumulator. Use flow() when you need suspend/resume or durable gates.

FieldTypeDescription
stepsArray<AgentStep | FnStep>Ordered steps to execute
contextunknownSeed data available to all steps
modelAnyModel?Shared model

Agent step (inline object):

FieldTypeDescription
agentAgentLikeAgent or plain function
namestringStep name (for devtools and error messages)
input(ctx: AccumulatedContext) => unknown?Transform accumulated context to this step's input. ctx contains all previous step outputs keyed by name, plus the seed data.

Plain function step (inline object):

FieldTypeDescription
namestringStep name
fn(ctx: AccumulatedContext) => Promise<unknown>Custom function receiving accumulated context

Returns: PipelineResult

FieldDescription
statusAlways completed when returned
contextAccumulated seed data and named step outputs
finalOutputOutput from the last step
resultsOrdered agent/function step results
durationMsTotal wall-clock duration

consensus(options)

Run multiple agents and determine a winner via voting.

FieldTypeDescription
agentsAgentLike[]Agents to run as voters
contextunknownContext data passed to all agents
modelAnyModel?Shared model
extract(result: AgentResult) => stringExtract a vote string from each result
quorum'majority' | 'unanimous' | number?Quorum requirement. Default: 'majority'

Returns: ConsensusResult

FieldTypeDescription
resultstringWinning vote value
votesRecord<string, number>Vote breakdown
detailsAgentResult[]Each agent's full result
agreementnumberAgreement ratio (0–1)
durationMsnumberTotal duration

Throws ConsensusError when quorum is not met. The error has .votes and .quorum properties.

AgentResult

FieldTypeDescription
agentIdstringAgent identifier
outputunknownThe agent's output
durationMsnumberExecution time
usage{ inputTokens?, outputTokens?, totalTokens? }?Token usage if available

AgentExecutor

SDK-agnostic executor interface. Each adapter implements this to bridge agents to their SDK's generate(). Used by createCompositions(executor) to create pre-bound composition functions.

type AgentExecutor = (agent: Agent, options: ExecuteOptions) => Promise<AgentResult>

createCompositions(executor)

Factory that creates adapter-bound parallel, pipeline, and consensus functions. Called internally by each adapter package.

createFakeAgentExecutor(config?)

A conformant in-memory AgentExecutor for testing how compositions drive the executor without an SDK (the agent-layer analogue of the resolver fakes). Exported from @crux/core/agent and the package root.

import { createFakeAgentExecutor, createPipeline } from '@crux/core/agent'

const executor = createFakeAgentExecutor({
  agents: {
    reviewer: { output: { score: 0.9 } },           // return output
    triage: { transfer: 'billing', reason: 'fees' }, // drive a swarm handoff
    flaky: { throws: 'LLM unavailable' },            // exercise an error path
  },
  fallback: 'echo', // unconfigured agents echo `{ _agent, _input }`
})

await createPipeline(executor)({ context: { seed: 1 }, model: 'm', steps: [{ name: 'r', agent: reviewer }] })

// Every invocation is recorded for assertions:
executor.calls[0].options.input        // { seed: 1 }
executor.calls[0].resolvedModel        // agent.model ?? options.model
executor.calls[0].executionContext     // ambient context observed per call
Config fieldTypeDescription
agentsRecord<string, Behavior | Resolver>Per-agent behavior, keyed by agent.id
fallbackBehavior | Resolver | 'echo'Used when no agents entry matches; omitted ⇒ throws
durationMsnumberReported on every result (default 10)

A Behavior is { output }, { transfer, reason } (executes the generated transfer_to_<id> tool), or { throws }; non-throwing forms may carry usage. A Resolver is (agent, options, callIndex) => Behavior for call-order-dependent fakes (e.g. consensus voters). The returned executor exposes a readonly calls array of { agent, options, resolvedModel, executionContext }.


Building-block primitives

import { blackboard, handoff, delegate } from '@crux/core/agent'

blackboard(config)

Shared typed state for multi-agent coordination.

FieldTypeDescription
idstringUnique identifier
schemaZodTypeSchema for state fields
storeCruxStore?Persistent storage

Returns: Blackboard<T>

MethodDescription
.get(field)Get a single field value
.getAll()Get all fields
.set(field, value)Set a field (Zod-validated)
.patch(partial)Merge partial update
.subscribe(callback)Listen for field changes: (field, value) => void
.asContext(opts?)Context (default priority: 70)
.asTools()Returns { readBlackboard, writeBlackboard, patchBlackboard, clearBlackboard } — focused tools for LLM interaction

Use a board directly in prompt use when the agent should see and update shared state:

prompt({
  use: [board], // context + focused tools
})

Use board.asContext() for context-only injection, and board.asTools() when you want manual tool selection.

Focused tools pattern. .asTools() returns an object of individual tools. Pick which to expose based on the agent's role:

const board = blackboard({ id: 'shared', schema: mySchema, store })

// Read-only agent — can observe shared state but not modify it
const { readBlackboard } = board.asTools()
prompt({ tools: { readBlackboard } })

// Full access agent — can read and write
prompt({ tools: board.asTools() })

When a prompt uses more than one blackboard directly, give each board a prefix:

const research = blackboard({ id: 'research', schema, tools: { prefix: 'research' } })
const writing = blackboard({ id: 'writing', schema, tools: { prefix: 'writing' } })

prompt({ use: [research, writing] })
// readResearchBlackboard, writeResearchBlackboard, readWritingBlackboard, ...

Override descriptions for domain-specific language:

const { readBlackboard, writeBlackboard } = board.asTools()
const tools = {
  getSharedState: { ...readBlackboard, description: 'Read the current analysis results from the shared workspace.' },
  updateSharedState: { ...writeBlackboard, description: 'Write your analysis results to the shared workspace.' },
}

handoff(config)

Structured context transfer between agents. Supports stateless (in-process) and stored (distributed) modes.

FieldTypeDescription
idstringUnique identifier
inputSchemaZodTypeValidates producer output
outputSchemaZodTypeValidates transformed data
transform(input) => outputTransform function (may be async)
summarize{ generate, model, system? }?Optional LLM summarization
storeCruxStore?Persistent storage — enables send() and receive() for distributed agents
fromAgentstring?Name of sending agent (for devtools identification)
toAgentstring?Name of receiving agent (for devtools identification)

Returns: HandoffInstance<TInput, TOutput>

MethodDescription
.prepare(input)Validate, transform, optionally summarize → HandoffPayload { data, summary? }
.send(input)Like prepare() but also persists to store. Requires store.
.receive()Read latest payload from store. Returns null if not sent yet. Requires store.
.asContext(payload)Context from prepared payload (default priority: 80)
.idHandoff identifier

delegate(config)

Orchestration wrapper combining handoff + subagent execution.

FieldTypeDescription
idstringUnique identifier
argsSchemaZodTypeWhat the LLM provides when calling the tool
handoffHandoffInstanceHandoff contract for validation
execute(args: TArgs, ctx: TCtx) => Promise<THandoffInput>Run the subagent with typed context

Generic parameters: delegate<TArgs, THandoffInput, THandoffOutput, TCtx>

TCtx defaults to unknown. When specified, both execute and .run() enforce the typed context — useful for threading framework-specific data (action context, project IDs, etc.).

Returns: Delegate<TArgs, TInput, TOutput, TCtx>

MethodDescription
.run(args, ctx)Execute: run subagent → validate via handoff → return DelegateResult. ctx is required and typed.
.asTools(opts?)Returns { delegate } — focused tool for LLM interaction. For frameworks with custom tool shapes, use .run() directly.

Focused tools pattern. .asTools() returns { delegate } — a single focused tool for triggering the delegation. Override the description to match your domain:

const researchDelegate = delegate({
  id: 'research',
  argsSchema: z.object({ topic: z.string() }),
  handoff: researchHandoff,
  execute: async (args) => runResearchAgent(args),
})

// Default: generic "delegate" tool name
prompt({ tools: researchDelegate.asTools() })

// Custom: rename and describe for your domain
const { delegate } = researchDelegate.asTools()
prompt({
  tools: {
    requestResearch: {
      ...delegate,
      description: 'Assign a research task to the research team. Provide the topic to investigate.',
    },
  },
})

DelegateResult:

FieldTypeDescription
delegateIdstringDelegate identifier
dataTOutputTransformed output from handoff
summarystring?LLM summary if handoff configured one
durationMsnumberExecution time

Three-layer validation: argsSchemahandoff.inputSchemahandoff.outputSchema


Instrumentation

All primitives emit instrumentation events automatically:

EventEmitted byFields
blackboard:update.set(), .patch(), .clear()boardId, fieldsChanged, snapshot
handoff:prepare.prepare(), .send()handoffId, inputSize, outputSize, input, output, fromAgent, toAgent
delegate:start.run()delegateId, handoffId, inputSize, input
delegate:complete.run()delegateId, handoffId, inputSize, outputSize, durationMs, output
composition:startparallel(), pipeline(), consensus()compositionId, kind, agentIds
composition:agentEach agent in a compositioncompositionId, agentId, index, status, durationMs, error?
composition:endparallel(), pipeline(), consensus()compositionId, kind, status, durationMs, agentCount, successCount, errorCount, agreement?

Types

import type {
  // Composition
  Agent, AgentConfig, AgentLike,
  AgentExecutor, AgentResult, ExecuteOptions,
  ParallelOptions, ParallelResult, SettledResult,
  PipelineResult,
  ConsensusOptions, ConsensusResult,
  // Building blocks
  Blackboard, BlackboardConfig,
  HandoffInstance, HandoffConfig, HandoffPayload,
  Delegate, DelegateConfig, DelegateResult,
} from '@crux/core/agent'

import { ConsensusError } from '@crux/core/agent'

On this page