Skip to main content

Registering Specialists

This guide covers how to register specialists using the dialai library. For the conceptual foundation, see Specialists.

Specialists are registered using one of two functions: registerProposer or registerArbiter. Each function accepts configuration for how the specialist produces its output.

Proposer Registration

A proposer analyzes the current state and suggests what transition should happen next.

import { registerProposer } from "dialai";

registerProposer({
specialistId: "ai-proposer-1",
machineName: "my-task",
strategyFn: async (ctx) => {
const name = Object.keys(ctx.transitions)[0];
return {
transitionName: name,
toState: ctx.transitions[name],
reasoning: "First available transition",
};
},
});

Arbiter Registration

An arbiter evaluates consensus and determines winning proposals.

import { registerArbiter } from "dialai";

registerArbiter({
specialistId: "consensus-arbiter",
machineName: "my-task",
strategyFnName: "alignmentMargin",
threshold: 0.5,
});

Arbiters support three execution modes: strategyFn, strategyWebhookUrl, and strategyFnName. They do not support LLM-based modes because arbitration must be deterministic.

See Consensus Strategies for details on the built-in arbiter strategies.

Execution Modes

Proposers support five execution modes. Arbiters support three (no LLM modes). They are mutually exclusive.

1. strategyFn -- Local Function

You provide an async function. The orchestrator calls it with the appropriate context and expects a complete proposal back.

registerProposer({
specialistId: "my-proposer",
machineName: "document-review",
strategyFn: async (ctx) => ({
transitionName: "approve",
toState: "approved",
reasoning: "Document meets all criteria",
}),
});

What happens inside the function is entirely up to you. Call your own LLM, apply rules, run deterministic logic. The orchestrator only checks that the return value matches the expected shape.

Required parameters: strategyFn Forbidden parameters: contextFn, contextWebhookUrl, strategyWebhookUrl, modelId, webhookTokenName

2. strategyWebhookUrl -- Remote Function

The orchestrator POSTs the full context to a URL and expects a proposal response. Authentication is HTTP Basic Auth: the username is the machineName, the password is the value of the environment variable named by webhookTokenName.

registerProposer({
specialistId: "remote-proposer",
machineName: "document-review",
strategyWebhookUrl: "https://my-service.example.com/propose",
webhookTokenName: "MY_SERVICE_TOKEN",
});
POST https://my-service.example.com/propose
Authorization: Basic base64("document-review:${MY_SERVICE_TOKEN}")
Content-Type: application/json

{ ...ProposerContext }

Response Handling: 55-Second Window

The orchestrator waits up to 55 seconds for the webhook to respond.

  • If the webhook responds with a JSON body containing a valid proposal, the orchestrator submits it on the specialist's behalf.

    Proposer response:

    { "transitionName": "approve", "toState": "approved", "reasoning": "Meets criteria" }
  • If the webhook does not respond within 55 seconds, the request throws an error.

  • If the webhook responds with 202 Accepted, an error is thrown (async webhook processing is not yet supported).

Required parameters: strategyWebhookUrl, webhookTokenName Forbidden parameters: strategyFn, contextFn, contextWebhookUrl, modelId

3. contextFn + modelId -- Local Context, Orchestrator Calls LLM

You provide an async function that returns a context string. The orchestrator sends that string to the LLM specified by modelId along with the decision prompt and parses the response into a proposal.

registerProposer({
specialistId: "context-proposer",
machineName: "document-review",
modelId: "openai/gpt-4o-mini",
contextFn: async (ctx) => {
const doc = await readFile(ctx.prompt);
return `Document contents:\n${doc}\n\nReview criteria: completeness, accuracy`;
},
});

Your function only provides the context string. The orchestrator handles prompt assembly, the API call, response parsing, and validation.

Required parameters: contextFn, modelId Forbidden parameters: strategyFn, strategyWebhookUrl, contextWebhookUrl, webhookTokenName

4. contextWebhookUrl + modelId -- Remote Context, Orchestrator Calls LLM

The orchestrator POSTs the context request to a URL, then sends the returned context to the LLM.

registerProposer({
specialistId: "webhook-context-proposer",
machineName: "document-review",
modelId: "openai/gpt-4o-mini",
contextWebhookUrl: "https://my-service.example.com/context",
webhookTokenName: "MY_SERVICE_TOKEN",
});

The webhook response should contain a context field with the context string:

{ "context": "Document contents:\n..." }

The orchestrator waits up to 55 seconds for the response. If the webhook does not respond in time, the request throws an error.

Required parameters: contextWebhookUrl, webhookTokenName, modelId Forbidden parameters: strategyFn, strategyWebhookUrl, contextFn, strategyFnName

5. strategyFnName -- Built-in Named Strategy

Reference a built-in strategy by name. The orchestrator loads the strategy from its internal registry and invokes it with the appropriate context.

registerArbiter({
specialistId: "consensus-arbiter",
machineName: "document-review",
strategyFnName: "alignmentMargin",
threshold: 0.5,
});

Built-in strategies are stored in src/dialai/strategies.ts and loaded by name at runtime. The threshold parameter is passed to the strategy and its meaning varies by strategy type.

See Default Strategies below for the complete list and documentation.

Required parameters: strategyFnName Optional parameters: threshold Forbidden parameters: strategyFn, strategyWebhookUrl, contextFn, contextWebhookUrl, modelId

Validation Rules

Valid parameter combinations:

ModestrategyFnstrategyFnNamestrategyWebhookUrlcontextFncontextWebhookUrlmodelIdwebhookTokenNamethreshold
1. Local strategyrequired
2. Webhook strategyrequiredrequired
3. Local context + LLMrequiredrequired
4. Webhook context + LLMrequiredrequiredrequired
5. Built-in strategyrequiredoptional

Invalid configurations are rejected at registration time with descriptive error messages:

  • strategyFn + modelId -- "modelId is only used with contextFn or contextWebhookUrl. A strategyFn returns proposals directly and does not need a model."
  • strategyFn + strategyFnName -- "Provide either strategyFn (custom function) or strategyFnName (built-in strategy), not both."
  • strategyFnName + modelId -- "modelId is only used with contextFn or contextWebhookUrl. A strategyFnName references a built-in strategy and does not need a model."
  • contextFn without modelId -- "contextFn provides context for an LLM to generate proposals. You must also specify modelId."
  • strategyFn + contextFn -- "Provide either strategyFn (you handle everything) or contextFn + modelId (orchestrator calls the LLM), not both."
  • contextWebhookUrl without webhookTokenName -- "Webhook URLs require webhookTokenName for authentication."
  • Arbiter with contextFn or contextWebhookUrl -- "Arbiters cannot use LLM-based modes. Arbitration must be deterministic."
  • No execution parameters at all -- "Specialist must specify one of: strategyFn, strategyFnName, strategyWebhookUrl, contextFn + modelId, or contextWebhookUrl + modelId."

Context Shapes

ProposerContext

interface ProposerContext {
sessionId: string;
currentState: string;
prompt: string;
transitions: Record<string, string>;
history: TransitionRecord[];
metaJson?: Record<string, unknown>;
}

Specialist ID Conventions

Any naming scheme works, but including the purpose is helpful:

ai-proposer-1
human-reviewer
human-approver-jane

To allow a specialist to force arbitration decisions, set isHuman: true when registering:

// This allows forcing arbitration:
registerProposer({
specialistId: "reviewer-jane",
machineName: "document-review",
isHuman: true,
strategyFn: async (ctx) => ({ ... }),
});

Human Specialists

Human specialists are registered with isHuman: true. Their proposals count like any other proposal during consensus evaluation. The key difference is that only human specialists can force a transition when consensus isn't reached. Human specialists have alignment = 1.0, giving their proposals maximum weight in consensus evaluation. Additionally, human specialists can force transitions directly via submitArbitration with an explicit transitionName, bypassing consensus entirely.

Human specialists can have strategy functions that encode human preferences, or proposals can be submitted directly:

// Register a human specialist with a strategy
registerProposer({
specialistId: "reviewer-jane",
machineName: "document-review",
isHuman: true,
strategyFn: async (ctx) => ({
transitionName: "approve",
toState: "approved",
reasoning: "Reviewed and meets all criteria",
}),
});

Humans can also bypass the entire decision cycle using submitArbitration with an explicit transition:

import { submitArbitration } from "dialai";

// Human override - directly execute a transition
await submitArbitration({
sessionId: session.sessionId,
roundId: session.currentRoundId,
specialistId: "human-reviewer", // must be registered with isHuman: true
transitionName: "approve",
reasoning: "Reviewed and approved by manager",
metaJson: { approvedBy: "manager@example.com" },
});

Registration Options Reference

FieldTypeRequiredDefaultDescription
specialistIdstringYes--Unique identifier for the specialist
machineNamestringYes--Which machine this specialist participates in
isHumanbooleanNofalseSet to true to allow forcing arbitration decisions (proposers only)
strategyFnasync (context) => resultMode 1--Local function that returns a proposal or consensus result
strategyFnNamestringMode 5--Built-in strategy name (see Default Strategies)
strategyWebhookUrlstringMode 2--URL to POST context to; expects proposal/consensus response
contextFnasync (context) => stringMode 3--Local function that returns context for the LLM (proposers only)
contextWebhookUrlstringMode 4--URL to POST context request to; expects context response (proposers only)
modelIdstringModes 3, 4--LLM model identifier (e.g., "openai/gpt-4o-mini")
webhookTokenNamestringModes 2, 4--Env var name holding the webhook auth token
thresholdnumberNovariesStrategy-specific threshold (see Consensus Strategies)

Default Strategies

DIAL provides built-in strategies for all specialist roles. These are referenced by name via strategyFnName and stored in src/dialai/strategies.ts.

Proposer Strategies

StrategyDescriptionThreshold
firstAvailableProposes the first available transition (by insertion order)--
lastAvailableProposes the last available transition (by insertion order)--
randomProposes a random available transition--

firstAvailable

function firstAvailable(ctx: ProposerContext) -> Proposal:
transitions = list(ctx.transitions.keys()) // insertion order
name = transitions[0]
return {
transitionName: name,
toState: ctx.transitions[name],
reasoning: "Choosing first available transition: " + name
}

lastAvailable

function lastAvailable(ctx: ProposerContext) -> Proposal:
transitions = list(ctx.transitions.keys()) // insertion order
name = transitions[len(transitions) - 1]
return {
transitionName: name,
toState: ctx.transitions[name],
reasoning: "Choosing last available transition: " + name
}

random

function random(ctx: ProposerContext) -> Proposal:
transitions = list(ctx.transitions.keys())
name = random_choice(transitions)
return {
transitionName: name,
toState: ctx.transitions[name],
reasoning: "Randomly selected transition: " + name
}

Arbiter Strategies

StrategyDescriptionKey Parameter
alignmentMarginDefault. Alignment-weighted margin; consensus when margin exceeds thresholdthreshold (float, default: 1)
firstProposalImmediately selects the first proposal received--

alignmentMargin (Default)

The default strategy. Groups proposals by transition, scores by alignment-weighted margin. Consensus when the margin exceeds the threshold.

See Consensus Strategies for full documentation.

firstProposal

The simplest arbiter: immediately declares consensus on the first proposal received.

See Consensus Strategies for full documentation.

Strategy Combinations

Common combinations of proposer and arbiter strategies:

Use CaseProposerArbiter
Testing/DevfirstAvailablefirstProposal
Production(LLM)alignmentMargin (threshold=0.5)
High-stakes(LLM)alignmentMargin (threshold=0.8+)

Implementing Custom Strategies

You can implement custom strategies using strategyFn:

registerProposer({
specialistId: "custom-proposer",
machineName: "my-task",
strategyFn: async (ctx) => {
// Your custom logic
const best = analyzeTransitions(ctx.transitions, ctx.history);
return {
transitionName: best.name,
toState: best.target,
reasoning: best.explanation,
};
},
});

Custom strategies can call external services, use ML models (for proposers), or implement any deterministic logic (for arbiters).