Skip to content

feat: add tool-calling primitives (0.11.0)#3

Merged
marceloceccon merged 1 commit intomainfrom
feat/tool-calling
Apr 30, 2026
Merged

feat: add tool-calling primitives (0.11.0)#3
marceloceccon merged 1 commit intomainfrom
feat/tool-calling

Conversation

@marceloceccon
Copy link
Copy Markdown
Member

Summary

Adds engine-orchestrated tool calling so consumers can plug a toolExecutor and let participants invoke tools mid-turn. 100% backward compatible with 0.10.x — every new field is optional, and absence of toolExecutor preserves 0.10 behaviour byte-for-byte.

This is the upstream half of the work in ai-consensus-mcp to add tool-calling support to the MCP wrapper.

What changes

Surface additions (all optional):

  • Participant.tools?: ToolDefinition[] — per-participant tool inventory
  • ConsensusOptions.toolExecutor?: ToolExecutor — host-supplied dispatcher
  • ConsensusOptions.maxToolIterations?: number — loop cap (default 8, clamped to [1, 32])
  • ModelCallRequest.tools?, ModelCallRequest.toolCallTurns? — forwarded verbatim to the caller
  • ModelCallResponse.toolCalls? — caller may return tool-call requests
  • New types: ToolDefinition, ToolCall, ToolCallTurn, ToolCallContext, ToolExecutionResult, ToolExecutor
  • New events: toolCallStart, toolCallComplete (with ok, durationMs, preview), toolError
  • Schema export: ToolDefinitionSchema
  • Constant export: MAX_TOOL_ITERATIONS_CAP (= 32)

Engine internals:

  • New private helper #runParticipantTurn drives the per-turn tool loop
  • New private helper #dispatchToolCalls runs one iteration's calls and emits the corresponding events
  • Token usage is summed across loop iterations
  • After the loop terminates (or hits the cap), the last response's content becomes the participant turn's output

Documentation:

  • New "Tool calling" section in the README with the full integration recipe and ModelCaller responsibilities
  • CHANGELOG.md initialised, with 0.10.0 retro-entry and the 0.11.0 entry

Why this shape

  • Pure plumbing. The library never parses tool arguments, never invokes a tool, never decides what tools a participant has. Hosts own all of that. The engine just routes the request from model → host executor → back to model.
  • Per-participant tool inventory. Different participants can have different tools (a domain expert can read files; a pessimist cannot). Participant.tools enables this.
  • Per-turn loop, not per-round. The tool loop sits inside a single participant's turn. One participant's tool calls do not leak into another's context. Round-level state is unchanged.
  • Caller translates the contract. ModelCallRequest.tools and toolCallTurns are forwarded verbatim; the caller maps them to OpenAI's tools/tool_calls, Anthropic's tools/tool_use, etc. This keeps the library zero-coupled to provider-specific shapes.
  • Engine drives events, not result-state. ParticipantResponse and ConsensusResult are unchanged — tool history is observable via events but not persisted on the response. Stable JSON shape for existing consumers.

Backward compatibility

  • Every new field is optional.
  • With no toolExecutor, the engine ignores any toolCalls on the response (treats content as the final turn — same as 0.10).
  • All 130 existing tests pass without modification.

Test plan

  • All 130 pre-existing tests pass unchanged
  • 12 new tool-calling tests covering:
    • Happy path: tool dispatch → results fed back → final content
    • No executor configured: toolCalls ignored (0.10 backward compat preserved)
    • No tools declared: tools not passed to caller
    • Executor exception: captured as { error }, conversation continues, toolError event fires
    • Executor returns { error }: forwarded to toolCallComplete (ok: false) and toolError events
    • Iteration cap: model loops forever, engine breaks after N
    • maxToolIterations clamping to [1, 32] (passing 0 yields 1 iteration)
    • Abort propagation: executor throws AbortError → run finalises with stopReason: \"aborted\"
    • Turn isolation: toolCallTurns does not leak across participants
    • Iteration counters increase monotonically per turn
    • Token usage summed across iterations
    • toolCallTurns payload mirrors what the executor actually returned
  • npm run typecheck clean
  • npm run build clean
  • npm run test 142/142 pass

Follow-ups (not in this PR)

  • A future PR could add ParticipantResponse.toolHistory?: ToolCallTurn[] if we want non-event observers to see what happened. Deferred until there's a concrete consumer asking for it.
  • The ai-consensus-mcp PR depending on this lands separately (Phase 3 of the consumer-side plan).

Adds engine-orchestrated tool calling. Participants declare a `tools`
list; the host plugs in a `ToolExecutor`; the engine drives the per-turn
dispatch loop. 100% backward compatible with 0.10.x — every new field
is optional, and absence of `toolExecutor` preserves byte-for-byte
behaviour.

Public surface (all optional):
- Participant.tools?: ToolDefinition[]
- ConsensusOptions.toolExecutor?: ToolExecutor
- ConsensusOptions.maxToolIterations?: number (default 8, clamped [1,32])
- ModelCallRequest.tools?, ModelCallRequest.toolCallTurns?
- ModelCallResponse.toolCalls?
- New types: ToolDefinition, ToolCall, ToolCallTurn, ToolCallContext,
  ToolExecutionResult, ToolExecutor
- New events: toolCallStart, toolCallComplete, toolError
- ToolDefinitionSchema (zod)
- MAX_TOOL_ITERATIONS_CAP constant

Engine internals: new private #runParticipantTurn drives the loop;
#dispatchToolCalls runs one iteration's calls and emits the events.
Token usage is summed across iterations; the final response.content
(after the loop terminates or hits the cap) becomes the turn's output.

Tests: 130 → 142 (12 new — happy path, no-executor backward compat,
exception capture, executor-returned errors, abort propagation, max-
iteration cap with clamping, turn isolation, payload integrity,
multi-call dispatch).

README gets a new "Tool calling" section with the full integration
recipe and ModelCaller responsibilities. CHANGELOG.md initialised.
@marceloceccon marceloceccon merged commit 870420e into main Apr 30, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant