Agent tools
A dispatcher that runs the three tools of bindings/llm-tools.json against the rule server, so an AI agent checks an operation before it calls the API that performs it.
A dispatcher that runs the three tools of bindings/llm-tools.json (bindings/llm-tools.json)
against the rule server, so an AI agent checks an operation before it calls
the API that performs it.
| Tool | Rule server call |
|---|---|
list_rules | GET /rulesets/{ruleset}/manifest?channel=…, filtered here by target.entity and operations. The server has no per-entity listing |
evaluate_rules | POST /evaluations. A dry run: nothing is changed |
explain_rule | GET /rulesets/{ruleset}/rules/{rule_id}, including origin and overriddenBy: which level defined or changed the rule |
import OpenAI from 'openai';
import { createDispatcher, tools } from './src/dispatch.mjs';
const rules = createDispatcher({
baseUrl: 'http://rules.internal:8080',
token: process.env.RULE_SERVER_TOKEN,
actor: { id: session.userId, roles: session.roles }, // from your session, never from the model
});
const response = await new OpenAI().responses.create({ model, input, tools });
for (const item of response.output) {
if (item.type === 'function_call') input.push(item, await rules.handleToolCall(item));
}(The OpenAI client is not a dependency of this example; handleToolCall takes and returns the plain
Responses API items.)
What the dispatcher decides for the model
- The actor is not a tool argument. It comes from the session the dispatcher was created for, so a model cannot claim a role. Resolutions (acknowledgements, risk acceptances) are tool arguments because the user gives them, and the tool description tells the model never to invent them; a risk acceptance still only counts when the actor holds a listed role.
- The token decides what can be listed. With
RULE_SERVER_TOKENthe dispatcher reads the server manifest, which includes server-only rules; without it, the client manifest.POST /evaluationsalways needs the token: the rule server refuses to start withoutRULE_SERVER_TOKENunlessRULE_SERVER_ALLOW_OPEN=1is set. - Nulls are not forwarded. Strict mode makes the model send
nullfor every absent value. The dispatcher drops thenullmembers ofview(none left: every place is evaluated) and anulljustification, asbindings/README.mdasks of every host. - Failures are answers. Bad
data_json, an unknown rule, a401or an unreachable server come back as{ "error": "…" }, which the model can read and act on. Malformed JSON never reaches the server.
As an MCP server
This is not an MCP server, but the mapping is one to one: each entry of tools becomes an MCP tool
with name, description and inputSchema = parameters, and a tools/call handler returns
dispatch(name, arguments) as the text content (with isError: true when it holds error). Create
the dispatcher per MCP session so the actor and token are the connected user's.
Test
npm ci && npm run build # from the repository root
npm test -w rule-cascade-example-agent-toolsThe test starts a real rule server in process (createRuleServer over examples/contracts, with a
token) and drives every tool through it, including a full Responses API function-call round trip. A
second set of tests replaces fetch with a recorder and checks the exact route, method, headers and
body each tool produces, so the mapping above is pinned without a server.
bindings/README.md describes the same three tools against a runtime in
process, in Python; this example is the host for the HTTP rule server.
@yarlisaisolutions/rule-cascade-server
A stateless HTTP service that implements spec/v1/rule-evaluation.openapi.yaml. Use it for callers that cannot embed a runtime. It keeps nothing between requests, so you run as many replicas as you need behind a load balancer.
Go example (net/http)
The payments API of backend-spring-boot, on the standard library's net/http with the Go runtime. It enforces payments-transfer.ruleset.yaml from its compiled bundle.