Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Changelog

All notable changes to `ai-consensus-core` will be documented here.
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), [SemVer](https://semver.org/spec/v2.0.0.html).

## [0.11.0] — 2026-04-30

### Added — tool calling

The engine can now drive a per-participant tool-call loop. Hosts plug in a `toolExecutor`; participants declare a `tools` list; the engine dispatches each `ToolCall` from the model, feeds the results back, and re-invokes the caller until the model returns final content or `maxToolIterations` is hit.

- `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?: ToolDefinition[]` — forwarded verbatim to the caller
- `ModelCallRequest.toolCallTurns?: ToolCallTurn[]` — accumulated history on follow-up calls
- `ModelCallResponse.toolCalls?: ToolCall[]` — caller may return tool-call requests
- New types: `ToolDefinition`, `ToolCall`, `ToolCallTurn`, `ToolCallContext`, `ToolExecutionResult`, `ToolExecutor`
- New events: `toolCallStart`, `toolCallComplete`, `toolError`
- Schema export: `ToolDefinitionSchema`
- Constant export: `MAX_TOOL_ITERATIONS_CAP` (= 32)

See README "Tool calling" section for the full contract and integration recipe.

### Backward compatibility

100% backward compatible with 0.10.x. Every new field is optional; absence of `toolExecutor` ⇒ engine behaviour is byte-identical to 0.10. Existing tests pass without modification (130/130) plus 12 new tool-calling tests (142 total).

### Internal

- New private engine helper: `#runParticipantTurn` (drives the tool loop)
- New private engine helper: `#dispatchToolCalls` (per-iteration dispatch + events)
- Token usage is summed across loop iterations; the final response's `content` is the participant turn's output

## [0.10.0] — 2026-04-24

- Removed the seven Roundtable personas from the library; they live in `docs/personas.md` for callers to copy. Only `JUDGE_PERSONA` remains in code.
- Added `ConsensusOptions.judge.systemPrompt` to override the synthesis prompt (with documented contract on the `## Majority Position` / `## Synthesis Confidence` markers).
- Replaced ReDoS-prone regex parsers with linear string scans.

(Earlier history is in git; this CHANGELOG starts at 0.10.0.)
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,73 @@ export interface ModelCallResponse {
4. **Don't swallow other errors.** Throw. The engine captures the error into `ParticipantResponse` and keeps running.
5. **Return the full content verbatim.** Do not strip the trailing `CONFIDENCE:` line — the parser needs it.

## Tool calling

Participants can invoke tools mid-turn. The library never executes a tool itself — it plumbs the request from the model to a host-supplied `ToolExecutor`, feeds the results back, and re-invokes the caller until the model returns final content (or `maxToolIterations` is exhausted).

```ts
import { ConsensusEngine, type ToolDefinition, type ToolExecutor } from "ai-consensus-core";

const READ_FILE_TOOL: ToolDefinition = {
name: "read_file",
description: "Read a file by absolute path.",
parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
};

const participants = [
{
id: "domain",
modelId: "claude-sonnet-4-6",
persona: domainExpertPersona,
tools: [READ_FILE_TOOL], // declared per-participant
},
// …
];

const toolExecutor: ToolExecutor = async (call, ctx) => {
// ctx: { participantId, round, phase, signal? }
if (call.name === "read_file") {
const args = call.arguments as { path: string };
return { content: await readFile(args.path, "utf8") };
// …or { error: "permission denied" } to feed an error back into the conversation
}
return { error: `unknown tool ${call.name}` };
};

const engine = new ConsensusEngine(modelCaller);
const result = await engine.run({
question: "What does the build output say?",
participants,
toolExecutor,
maxToolIterations: 8, // optional, default 8, clamped to [1, 32]
});
```

**ModelCaller responsibilities** (when `tools` is present on the request):

- Translate the `tools` array into whatever the underlying provider expects (OpenAI's `tools`, Anthropic's `tools`, etc.).
- Translate `toolCallTurns` (when present, on follow-up calls) into the conversation history the provider expects — typically: assistant message with tool_calls, then tool messages with results, in order.
- Parse the model's response and surface `toolCalls` on `ModelCallResponse` if the model wants to dispatch tools. Each `ToolCall` carries `{ id, name, arguments }` where `arguments` is **already JSON-parsed**. The library never parses the model's raw argument string.

**Engine guarantees:**

- The tool loop runs **per participant turn**, separately for each model call. Tool history does not leak between participants or between rounds.
- The executor receives a fresh `ToolCallContext` (with `participantId`, `round`, `phase`, `signal`) per call.
- An exception thrown by the executor is captured as a `{ error: message }` result and forwarded back into the conversation — the participant turn does not abort.
- `AbortError` thrown by the executor (or `signal` triggered) propagates up and aborts the whole run with `stopReason: "aborted"`.
- Hitting `maxToolIterations` breaks the loop and uses the last response's `content` — even if the model still wants more tools.
- Without `toolExecutor`, the engine ignores any `toolCalls` on the response: 0.10 behaviour preserved exactly.

**Events:**

```ts
engine.on("toolCallStart", (e: ToolCallStartEvent) => void);
engine.on("toolCallComplete", (e: ToolCallCompleteEvent) => void); // ok: boolean, durationMs, preview (≤ 200 chars)
engine.on("toolError", (e: ToolErrorEvent) => void); // fires when ok === false
```

`iteration` (1-based) on these events disambiguates round-trips within a single participant turn.

## Events

```ts
Expand All @@ -248,6 +315,9 @@ engine.on("synthesisStart", (e: SynthesisStartEvent) => void);
engine.on("synthesisToken", (e: SynthesisTokenEvent) => void);
engine.on("synthesisComplete", (e: SynthesisCompleteEvent) => void);
engine.on("finalResult", (e: FinalResultEvent) => void);
engine.on("toolCallStart", (e: ToolCallStartEvent) => void); // per dispatch
engine.on("toolCallComplete", (e: ToolCallCompleteEvent) => void);
engine.on("toolError", (e: ToolErrorEvent) => void); // ok === false
engine.on("error", (err: Error) => void);
```

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ai-consensus-core",
"version": "0.10.0",
"version": "0.11.0",
"description": "Dependency-light TypeScript implementation of the Consensus Validation Protocol (CVP): multi-model debate with confidence-weighted scoring, disagreement detection, and optional judge synthesis.",
"keywords": [
"consensus",
Expand Down
Loading
Loading