agentskit.js
Recipes

Tool composer

Chain N tools into a single macro tool — a fixed recipe the model can invoke with one schema.

Some agent capabilities are always the same multi-step recipe: fetch → parse → rerank → summarize. Letting the model pick each step adds latency and unreliability; baking the recipe into a single tool gives the model one lever and you predictable behavior.

composeTool takes N sub-tools, a mapper per step, and an optional finalizer — and returns one ToolDefinition the model sees as a single tool.

Install

Ships in @agentskit/core under a subpath:

import { composeTool } from '@agentskit/core/compose-tool'

Chain three tools into one

import { composeTool } from '@agentskit/core/compose-tool'
import { defineTool } from '@agentskit/core'
import { fetchUrl, webSearch } from '@agentskit/tools'

const summarize = defineTool({
  name: 'summarize',
  schema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] } as const,
  execute: async ({ text }) => `summary: ${text.slice(0, 80)}...`,
})

const research = composeTool<{ query: string }>({
  name: 'research',
  description: 'Search the web, fetch the top result, summarize it.',
  schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
  steps: [
    {
      tool: webSearch(),
      mapArgs: ({ args }) => ({ query: args.query, limit: 1 }),
    },
    {
      tool: fetchUrl(),
      mapArgs: ({ state }) => ({ url: (state as { results: { url: string }[] }).results[0]!.url }),
    },
    {
      tool: summarize,
      mapArgs: ({ state }) => ({ text: String(state) }),
    },
  ],
})

Step contract

Each steps[i]:

{
  tool,
  mapArgs({ args, state, prior }) => Record<string, unknown>,
  mapResult?(result, { args, state, prior }) => newState,
  stopWhen?(state, { args, prior }) => boolean,  // short-circuit the chain
}
  • args — the macro tool's original input.
  • state — output of the previous step (after mapResult).
  • prior — every intermediate output in declaration order.

Return a finalize({ args, prior, state }) to produce a different return value than the last step's state.

Stop when done

A step can short-circuit the rest of the chain if its stopWhen predicate returns true — useful for early termination in cache-hit scenarios.

{
  tool: cacheCheck,
  mapArgs: ({ args }) => ({ key: args.query }),
  stopWhen: state => state !== null,
}

Observability

composeTool({
  ...,
  onStep: e => logger.debug('[compose]', e),
})

Events: start / end / skip, with step index + tool name.

See also

✎ Edit this page on GitHub·Found a problem? Open an issue →·How to contribute →

On this page