Skip to content

Orchestration ​

Build composable multi-step LLM workflows with chains and agents.

Overview ​

The orchestration module provides primitives for creating complex LLM workflows:

  • Chains: Compose steps into pipelines with type safety
  • Steps: Reusable building blocks for common operations
  • Agents: ReAct-style loops that autonomously use tools
  • Hooks: Observe and monitor workflow execution

Quick Start ​

typescript
import {
  Chain,
  llmStep,
  mapStep,
  userMessageStep,
  createContext
} from 'meloqui/orchestration';
import { ChatClient } from 'meloqui';
import type { Message } from 'meloqui';

const client = new ChatClient({
  provider: 'openai',
  model: 'gpt-4o'
});

// Create a chain that adds a user message, calls the LLM, then extracts data
const chain = Chain.create(
  'add-user-message',
  userMessageStep((ctx) => `Summarize this text: ${ctx.data.text}`).execute
)
  .pipe('summarize', llmStep(client).execute)
  .pipe('extract', mapStep('extract-data', (data: { messages: Message[] }) => {
    // After userMessageStep + llmStep, messages array contains the conversation
    const assistantMessage = data.messages.find(m => m.role === 'assistant');
    return {
      summary: assistantMessage?.content || '',
      wordCount: (assistantMessage?.content || '').split(' ').length
    };
  }).execute);

// Execute
const result = await chain.run(createContext({
  text: 'Your input text here'
}));

if (result.ok) {
  console.log('Summary:', result.value.data.summary);
  console.log('Words:', result.value.data.wordCount);
}

Core Concepts ​

Chains ​

A chain is a sequence of steps that execute in order. Each step transforms the context and passes it to the next step.

typescript
const chain = Chain.create('step1', step1)
  .pipe('step2', step2)
  .pipe('step3', step3);

Chains are immutable – each method returns a new chain.

Context ​

Context flows through the chain immutably:

typescript
interface ChainContext<T> {
  readonly data: T;              // Your data
  readonly messages: Message[];  // LLM conversation
  readonly metadata: {           // Execution metadata
    readonly chainId: string;
    readonly stepIndex: number;
    readonly startTime: number;
  };
}

Create initial context:

typescript
const ctx = createContext({ myData: 'value' });

Steps ​

Steps are functions that transform context:

typescript
type Step<TIn, TOut> = (
  ctx: ChainContext<TIn>
) => Promise<ChainResult<ChainContext<TOut>>>;

Built-in steps:

typescript
import {
  userMessageStep,
  systemMessageStep,
  mapStep,
  llmStep,
  withRetry,
  validateStep
} from 'meloqui/orchestration';

Built-in Steps ​

userMessageStep ​

Add a user message to the context:

typescript
Chain.create('add-message', userMessageStep('Hello, AI!').execute)

Dynamic content:

typescript
userMessageStep((ctx) => `Summarize: ${ctx.data.text}`).execute

systemMessageStep ​

Add a system message:

typescript
systemMessageStep('You are a helpful assistant.').execute

mapStep ​

Transform data:

typescript
mapStep('extract-names', (data) => ({
  ...data,
  names: extractNames(data.text)
})).execute

llmStep ​

Call the LLM:

typescript
llmStep(client).execute
llmStep(client, { temperature: 0.3 }).execute
llmStep(client, { systemPrompt: 'You are an expert.' }).execute

withRetry ​

Add retry logic to any step:

typescript
withRetry(llmStep(client).execute, {
  maxAttempts: 3,
  initialBackoffMs: 1000,
  backoffMultiplier: 2
})

validateStep ​

Validate data against JSON Schema:

typescript
validateStep('validate-output', {
  type: 'object',
  properties: {
    name: { type: 'string' },
    age: { type: 'number' }
  },
  required: ['name', 'age']
}).execute

Conditional Execution ​

when() ​

Execute step only if condition is true:

typescript
Chain.create('init', initStep)
  .when(
    (ctx) => ctx.data.value > 100,
    'high-value-handler',
    handleHighValue
  )
  .when(
    (ctx) => ctx.data.value <= 100,
    'low-value-handler',
    handleLowValue
  )

branch() ​

If/else branching:

typescript
chain.branch(
  (ctx) => ctx.data.type === 'question',
  { name: 'answer', step: answerStep },
  { name: 'acknowledge', step: acknowledgeStep }
)

Observability ​

Add hooks to monitor execution:

typescript
const hooks = {
  onStepStart: (name, ctx) => {
    console.log(`Starting: ${name}`);
  },
  onStepEnd: (name, ctx, durationMs) => {
    console.log(`Completed: ${name} (${durationMs}ms)`);
  },
  onStepError: (name, error) => {
    console.error(`Failed: ${name}`, error);
  },
  onChainEnd: (result, totalMs) => {
    console.log(`Chain complete (${totalMs}ms)`);
  }
};

const result = await chain.withHooks(hooks).run(ctx);

Agents ​

Agents are ReAct-style loops that autonomously use tools to complete tasks.

Creating an Agent ​

typescript
import { runAgent } from 'meloqui/orchestration';
import { ChatClient, ToolRegistry } from 'meloqui';

// Set up tools
const tools = new ToolRegistry();
tools.registerTool('search', searchFn, {
  description: 'Search the web',
  parameters: { /* schema */ }
});

// Create client with tools
const client = new ChatClient({
  provider: 'openai',
  model: 'gpt-4o',
  tools
});

// Run agent
const result = await runAgent(
  {
    client,
    maxIterations: 10,
    systemPrompt: 'You are a research assistant.'
  },
  'What is the population of Tokyo?'
);

if (result.ok) {
  console.log('Answer:', result.value.response);
  console.log('Iterations:', result.value.iteration);
}

Agent Configuration ​

typescript
interface AgentConfig {
  client: ChatClient;           // Client with tools
  maxIterations?: number;       // Max iterations (default: 10)
  shouldStop?: (ctx, iteration) => boolean;  // Custom stop
  systemPrompt?: string;        // System instructions
}

Agent State ​

typescript
interface AgentState {
  iteration: number;            // Current iteration
  done: boolean;                // Completed?
  response?: string;            // Final response
  toolCalls?: ToolCall[];       // Tool calls made
}

How Agents Work ​

  1. Send messages to LLM with tools
  2. If LLM returns tool calls, execute them
  3. Add tool results to messages
  4. Repeat until:
    • Max iterations reached
    • Custom stop condition returns true
    • LLM provides final response (no tool calls)

Error Handling ​

Chains use Result types for explicit error handling:

typescript
const result = await chain.run(ctx);

if (result.ok) {
  // Success
  console.log(result.value.data);
} else {
  // Error
  console.error(result.error.message);
  console.error(result.error.stepName);
  console.error(result.error.stepIndex);
}

Errors include full context:

typescript
class ChainError extends ChatError {
  readonly stepName: string;
  readonly stepIndex: number;
  readonly context: ChainContext;
}

Advanced Patterns ​

Composing Chains ​

Chains can be converted to steps and composed:

typescript
const preprocessChain = Chain.create('clean', cleanStep)
  .pipe('validate', validateStep('validate', schema).execute);

const analysisChain = Chain.create('analyze', analyzeStep)
  .pipe('extract', extractStep);

// Compose into pipeline
const pipeline = Chain.create('preprocess', preprocessChain.asStep().execute)
  .pipe('analyze', analysisChain.asStep().execute)
  .pipe('format', formatStep);

Custom Steps ​

Create your own steps:

typescript
const customStep: Step<InputType, OutputType> = async (ctx) => {
  try {
    const result = await myOperation(ctx.data);
    return {
      ok: true as const,
      value: {
        ...ctx,
        data: result
      }
    };
  } catch (error) {
    return {
      ok: false as const,
      error: new ChainError(
        'Custom step failed',
        'custom-step',
        ctx.metadata.stepIndex,
        ctx,
        error as Error
      )
    };
  }
};

Workflows ​

The workflow module provides graph-based orchestration for complex multi-agent systems.

Overview ​

Workflows support two execution modes:

  • Graph-driven: Static workflow graphs with explicit node connections
  • LLM-driven: Dynamic routing where an LLM supervisor decides which specialist to call

Both modes share the same specialist definitions for consistency.

Defining Specialists ​

Specialists are focused agents with constrained capabilities:

typescript
import { defineSpecialist } from 'meloqui/orchestration/workflow';

const planner = defineSpecialist({
  name: 'planner',
  systemPrompt: `You create detailed implementation plans.
    Current task: {{state.input}}`,
  tools: ['read_file', 'search_code'],
  maxIterations: 5
});

const coder = defineSpecialist({
  name: 'coder',
  systemPrompt: `You write clean, tested code.
    Plan: {{state.planner}}`,
  tools: ['read_file', 'write_file', 'run_tests'],
  maxIterations: 10
});

System prompts support template interpolation with {{state.nodeId}} syntax.

Graph-Driven Workflows ​

Define static workflow graphs:

typescript
import { defineWorkflow, WorkflowRunner } from 'meloqui/orchestration/workflow';
import { ChatClient } from 'meloqui';

// Define the workflow
const workflow = defineWorkflow({
  specialists: { planner, coder },
  nodes: [
    { id: 'input', type: 'input' },
    { id: 'plan', type: 'specialist', specialist: 'planner' },
    { id: 'review', type: 'checkpoint', config: {
      message: 'Review the plan before coding'
    }},
    { id: 'code', type: 'specialist', specialist: 'coder' },
    { id: 'output', type: 'output' }
  ],
  edges: [
    { from: 'input', to: 'plan' },
    { from: 'plan', to: 'review' },
    { from: 'review', to: 'code' },
    { from: 'code', to: 'output' }
  ]
});

// Run the workflow
const client = new ChatClient({ provider: 'openai', model: 'gpt-4o' });
const runner = new WorkflowRunner({
  client,
  checkpointHandler: async (state, message) => {
    console.log('Checkpoint:', message);
    return { action: 'approve' };
  }
});

const result = await runner.run(workflow, {
  input: 'Build a todo app',
  onProgress: (event) => console.log(event.type, event.nodeId)
});

LLM-Driven Routing (Supervisor) ​

For dynamic workflows where an LLM decides routing:

typescript
import { createSupervisor, defineSpecialist } from 'meloqui/orchestration/workflow';
import { ChatClient } from 'meloqui';

const supervisor = createSupervisor({
  client: new ChatClient({ provider: 'openai', model: 'gpt-4o' }),
  specialists: {
    planner: defineSpecialist({ name: 'planner', systemPrompt: 'You create plans' }),
    coder: defineSpecialist({ name: 'coder', systemPrompt: 'You write code' }),
    reviewer: defineSpecialist({ name: 'reviewer', systemPrompt: 'You review code' })
  },
  maxIterations: 10
});

const result = await supervisor.run('Build a todo app', {
  onProgress: (event) => console.log(event.type, event.nodeId)
});

if (result.success) {
  console.log('Output:', result.output);
}

The supervisor uses a router pattern:

  1. Router decides which specialist to call next
  2. Selected specialist executes
  3. Control returns to router
  4. Router can return "done" to complete

Node Types ​

TypePurpose
inputEntry point for workflow
outputExit point for workflow
specialistCalls a specialist agent
conditionBranch based on state
checkpointHuman-in-the-loop approval

Condition Nodes ​

Route based on state:

typescript
{
  id: 'check-quality',
  type: 'condition',
  config: {
    expression: 'state.coder?.includes("error") === false'
  }
}

With conditional edges:

typescript
{ from: 'check-quality', to: 'output', when: 'pass' },
{ from: 'check-quality', to: 'coder', when: 'fail' }

Fluent Builder API ​

Build workflows programmatically:

typescript
import { Workflow } from 'meloqui/orchestration/workflow';

const workflow = Workflow.create()
  .addSpecialist('planner', { systemPrompt: 'You plan' })
  .addSpecialist('coder', { systemPrompt: 'You code' })
  .addNode({ id: 'input', type: 'input' })
  .addNode({ id: 'plan', type: 'specialist', specialist: 'planner' })
  .addNode({ id: 'code', type: 'specialist', specialist: 'coder' })
  .addNode({ id: 'output', type: 'output' })
  .addEdge({ from: 'input', to: 'plan' })
  .addEdge({ from: 'plan', to: 'code' })
  .addEdge({ from: 'code', to: 'output' })
  .build();

Progress Events ​

Monitor workflow execution:

typescript
interface WorkflowProgressEvent {
  type: 'node_start' | 'node_end' | 'checkpoint' | 'error';
  nodeId: string;
  state: WorkflowState;
  timestamp: number;
  duration?: number;
  error?: string;
}

Dev Specialist Presets ​

Common specialists for development workflows:

typescript
import { devSpecialists } from 'meloqui/orchestration/workflow';

const supervisor = createSupervisor({
  client,
  specialists: devSpecialists
});

Available presets: planner, coder, reviewer, researcher.

Examples ​

See the examples directory for complete working examples:

  • Multi-step summarization
  • Classification and routing
  • Agent with tools
  • Error handling and recovery
  • Workflow orchestration

API Reference ​

See the API documentation for complete type definitions and detailed API reference.

Released under the MIT License.