07 · Tools¶
Tools are how a Horismos agent affects the outside world — and the sharpest place where Horismos inverts the conventional agent stack. The LLM never chooses a tool, never constructs arguments, never interprets results. Tools are declared contracts, fired by the runtime when workflow preconditions hold.
Declaring a tool¶
:ehr.assign_queue a horis:Tool ;
horis:takes [
horis:arg [ horis:name "patient" ; horis:type :Patient ] ;
horis:arg [ horis:name "queue" ; horis:type :AcuityLevel ]
] ;
horis:effect [
horis:asserts [ horis:property :queuedAt ; horis:value horis:now ] ;
horis:asserts [ horis:property :queueRef ; horis:type xsd:string ]
] ;
horis:binding "python:ehr_client.assign_queue" ;
horis:timeout "5s" ;
horis:idempotent false .
horis:takes— argument names and ontology types. At invocation, the runtime reads argument values from the graph (the ActionStep names the bindings). No text parsing is involved anywhere.horis:effect— the declared shape of what the tool may write back. This is a contract, not documentation: the return value is validated against it, through the World gate (provenance:tool), before any fact lands. An EHR API returning garbage is a rejected transaction and the ActionStep'shoris:onErroredge fires.horis:idempotent— drives replay semantics (doc 09): non-idempotent tools are never re-executed on replay; their recorded effects are substituted.
Bindings¶
| Binding scheme | Example | Notes |
|---|---|---|
python: |
python:ehr_client.assign_queue |
Dotted path or registered callable (tools={...} at Agent construction) |
rust: |
rust:assign_queue |
Registered Tool trait impl |
mcp:// |
mcp://ehr-server/assign_queue |
MCP server tool; Horismos consumes the MCP schema and narrows it to the declared contract |
http: / https: |
https://api…/assign |
JSON POST; response validated like any effect |
MCP deserves emphasis: the ecosystem already exposes domain tools over MCP. Horismos
does not rebuild them — it wraps them in a typed contract. Where the MCP tool's own
schema is looser than the horis:takes/horis:effect declaration, the declaration
wins: out-of-contract calls can't be made, out-of-contract results can't land.
Adapter-mediated external commits¶
Under the bounded-projection pivot (PRPs/PRP-000-vision-positioning.md), a tool effect
that reaches an external system of record commits through an Adapter, not a bare
synchronous call, and the contract above extends with:
- Expected source versions: an adapter-mediated commit carries the source-version vector the World's projection was hydrated against; the external system rejects (or the adapter refuses to attempt) a commit against a resource that has since changed — the same fail-closed discipline as doc 03's conditional-commit semantics.
- Remote receipt/effect reconciliation: a commit's outcome (accepted, conflicted,
failed) is a distinct trace state from "decision produced" — the tool's declared
horis:effectdescribes what Horismos proposes to assert, not a guarantee the remote system accepted it; reconciliation records which actually happened. - Authorization scopes: an adapter-mediated tool binding carries the authorization scope (e.g. a SMART-on-FHIR scope) it commits under, checked before the call, not inferred from a successful response.
- Distributed-failure semantics: no cross-system atomicity claim (doc 03, doc 15) — a multi-effect commit may partially succeed; the trace names exactly which effects committed and which didn't, rather than reporting one aggregate success/failure.
- Where useful, an adapter-mediated tool aligns with an existing operation shape (e.g.
FHIR ActivityDefinition/operations) rather than inventing a parallel one — but this is
not forced: a tool that has no natural standards analog stays a plain
horis:Tool.
None of this changes tools without an external commit (the shipped rust:/python:
in-process bindings) — they keep the synchronous contract above unmodified.
The candidate-artifact boundary¶
See docs/framework/06-llm-boundaries.md §"Candidate-artifact authoring" for the full
statement. In tools terms: a model-generated patch/file/command is never an ordinary
horis:Tool argument built from graph-read values — it is a typed proposal envelope that
passes through deterministic policy validation, sandboxed test execution, and a symbolic
authorization step before an ActionStep-like effect can land. This is captain-adopted
design (decisions/captain-pivot-ruling.md item 3), owned jointly by PRPs/PRP-007-
authoring-agent.md and PRPs/PRP-008-standards-host-integration/01-objective.md.
horismos-host::artifact owns the shipped runtime validation/sandbox/authorization evidence;
PRP-007 owns authoring and generation of the envelope.
Invocation semantics¶
An ActionStep fires its tool when:
- The step is current, and
- every
horis:requirespattern holds in the graph.
Then: arguments resolved from the graph → binding invoked with horis:timeout →
result validated against horis:effect → effects asserted in the step's transaction →
horis:next. On timeout or validation failure: horis:onError. Retries are explicit
(horis:retries, default 0 for non-idempotent tools).
ProposalStep: the constrained escape hatch¶
Some choices are genuinely open — three legal rebooking options, which does the customer prefer articulated in prose? That is a perception-like judgment, and it gets a perception-like contract:
:PickOption a horis:ProposalStep ;
horis:candidates [ horis:query "…legal options pattern…" ] ; # graph-derived
horis:rankBy "customer preference expressed in the request" ;
horis:model "claude:claude-fable-5" ;
horis:confidence 0.8 ;
horis:fallback :AskHuman ;
horis:next :ExecuteOption .
Hard properties:
- Candidates come from a graph query — every candidate already satisfies its preconditions. The LLM ranks; it cannot add options.
- The selection is recorded with confidence, and the execution of the chosen option is a normal ActionStep with normal contracts.
- Low confidence falls back — typically to a human (
WaitStep).
If you find yourself reaching for ProposalStep to pick whether something is allowed, stop: that is a rule, and it belongs in the ontology.
Failure classes, compared¶
| Failure | Conventional stack | Horismos |
|---|---|---|
| Wrong tool called | LLM misjudged | Impossible — no neural tool choice |
| Malformed/hallucinated args | Parse-and-pray | Impossible — args read from typed graph |
| Tool called before its preconditions | Prompted convention | Impossible — precondition gate |
| Bad data returned by tool | Enters context silently | Rejected at effect validation, onError |
| Tool has side effects on retry/replay | Unmodeled | horis:idempotent drives policy |
The remaining honest failure class — the tool itself misbehaves in the real world in ways its contract can't see — is logged with full trace context, which is exactly the context an incident review needs.
Correction (2026-08-24, bounded-projection pivot review): "impossible" in the table
above describes the visible, contract-checked surface — which tool fires, what arguments
it receives, when it's callable — not the tool implementation's full behavior. Contract
narrowing constrains what an ActionStep can ask for and what result can land as a
fact; it does not and cannot prove the bound implementation has no hidden side effect
outside its declared horis:effect (a python:/http: binding that also writes to an
unrelated system, for instance). That residual is the "tool itself misbehaves" row above,
stated precisely rather than implied away by the table's absolutist framing.