Class ComposableToolBuilder

Builds composite ITool instances by chaining existing tool invocations.

Each invocation is described by a ComposableStep that maps values from a shared pipeline context into the step tool's arguments via a lightweight reference expression syntax:

Expression Resolves to
"$input.foo" args.foo from the composite tool's own input
"$input" the whole input object
"$prev.bar" bar from the previous step's output
"$prev" the previous step's full output
"$steps[0].output.data" output.data from the first step's output
"$steps[0]" the first step's full output
anything else used as a literal value without transformation

Reference expressions nested inside plain objects are resolved recursively, so { query: "$input.topic", limit: 10 } becomes { query: "actual-topic", limit: 10 }.

Every step runs through a StepGate, which is where the host's checks live, and is checked first against the chainability rule: a tool that declares no side effects is chained freely, a tool with side effects only when the host lists it, and a tool that declares nothing by no one.

Example

const builder = new ComposableToolBuilder(
createStepGate({ resolve: (name) => registry.get(name) }),
);
builder.bind({ sideEffectingTools: ['send_email'] });

const spec: ComposableToolSpec = {
mode: 'compose',
steps: [
{ name: 'search', tool: 'web_search', inputMapping: { query: '$input.topic' } },
{ name: 'summarise', tool: 'summarise_text', inputMapping: { text: '$prev.snippet' } },
],
};

const tool = builder.build('research', 'Search then summarise a topic', schema, spec);
const result = await tool.execute({ topic: 'quantum computing' }, ctx);

Constructors

  • Parameters

    • gateOrExecute: StepGate | ((toolName, args, context) => Promise<ToolExecutionResult<any>>)

      A StepGate. A bare (toolName, args, context) callback is still accepted so existing construction keeps working, but nothing can be composed through one: it cannot say what a step tool is, so no step can be checked. Bind a gate with bind.

    • Optional options: {
          strictMode?: boolean;
      }

      Optional builder configuration.

      • Optional strictMode?: boolean

        When true, unresolved reference expressions throw an error instead of falling through as literal strings.

    Returns ComposableToolBuilder

Methods

  • Bind the gate and the host's list of side-effecting tools that may be chained. The engine calls this from its configuration; a host that uses the builder on its own may call it too.

    Parameters

    • options: {
          gate?: StepGate;
          sideEffectingTools?: readonly string[];
      }
      • Optional gate?: StepGate
      • Optional sideEffectingTools?: readonly string[]

    Returns void

  • Build an ITool-compatible object from a ComposableToolSpec.

    The returned tool can be registered directly with any tool orchestrator that accepts ITool. Its execute method runs the step pipeline sequentially, threading outputs through the reference resolution system.

    Parameters

    • name: string

      Machine-readable tool name exposed to the LLM (e.g. "research_topic").

    • description: string

      Natural language description of what the composite tool does.

    • inputSchema: JSONSchemaObject

      JSON Schema describing the arguments the composite tool accepts.

    • spec: ComposableToolSpec

      The composable pipeline specification to execute.

    Returns ITool<any, any>

    A fully-formed ITool instance whose execute method runs the pipeline.

    Example

    const tool = builder.build(
    'fetch_and_summarise',
    'Fetch a URL then return a one-paragraph summary.',
    { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] },
    spec,
    );
  • Run a pipeline. Every step is re-checked against the chainability rule before it runs, because a tool is replaced by name, and the instance that was checked is the one the gate runs. With options.dry (forge-time tests), a step whose tool has side effects, or is itself a composition, is not executed: its output is taken from stepOutputs, and the step is listed as an effect that would have run. options.signal, when given, reaches every step's run.

    Compositions nest no deeper than MAX_COMPOSITION_DEPTH: the depth travels in context.sessionData.emergentDepth, and a run at the limit is refused with nesting_too_deep, a code no composition is suspended for (a chain that long is not a cycle; the engine refuses a composition that reaches itself at its own call).

    A refusal or a failure ends the run; details then names the step, the steps that completed before it, and the effects so far.

    Parameters

    • spec: ComposableToolSpec
    • args: Record<string, unknown>
    • context: ToolExecutionContext
    • options: {
          dry?: {
              stepOutputs: Record<string, unknown>;
          };
          signal?: AbortSignal;
      } = {}
      • Optional dry?: {
            stepOutputs: Record<string, unknown>;
        }
        • stepOutputs: Record<string, unknown>
      • Optional signal?: AbortSignal

    Returns Promise<ToolExecutionResult<any>>

  • Validate a ComposableToolSpec before building.

    Performs structural checks only — it does not verify that the referenced tool names are actually registered in any registry. Use this method to give early, actionable feedback before attempting to build a tool.

    Checks performed:

    1. spec.steps must be a non-empty array.
    2. Every step must have a non-empty tool string.

    Parameters

    Returns {
        valid: boolean;
        errors?: string[];
    }

    { valid: true } when the spec passes all checks, or { valid: false, errors: string[] } with one message per failing check.

    • valid: boolean
    • Optional errors?: string[]

    Example

    const result = builder.validate(spec);
    if (!result.valid) {
    console.error('Invalid spec:', result.errors);
    }

Properties

strictMode: boolean

When true, unresolved $-prefixed reference expressions throw instead of silently passing through as literal strings. Useful for development and testing to catch typos in inputMapping expressions early.