Skip to content

Agent Design

Multi-tool agent with constrained output and runtime schema reasoning.

Tool Architecture

The agent has 6 tools: one output tool (submit_slots) and five investigation tools for runtime ontology exploration. The LLM never writes SPARQL — it fills structured slots or queries the schema graph.

submit_slots Schema

typescript
submit_slots({
  slots: {
    domains: string[],                                       // Asset types to search
    filters: Record<string, string | string[]>,              // Enum filters (any sh:in property,
                                                              //   including country / region /
                                                              //   license — all keyed by SHACL
                                                              //   leaf local name, no special-case
                                                              //   `location` or `license` slots)
    ranges: Record<string, { min?: number; max?: number }>,  // Numeric ranges
    references?: { domain: string }                          // Cross-domain JOIN to another
                                                              //   asset class (SHACL-discovered)
  },
  interpretation: string,                                    // Human-readable summary
  gaps: [{ term, reason, suggestions? }]                     // Unresolvable terms; suggestions
                                                              //   come from tokenised match
                                                              //   against the real vocabulary
})

Slot shape changed in the 2026-05-26 audit: there are no longer top-level location or license objects — both flow through filters keyed by the SHACL leaf local name (e.g. country, region, license). The new references slot binds one cross-domain JOIN whose target is a SHACL-discovered asset class. When the LLM nominates multiple cross-domain references, the dropped ones surface as honest OntologyGaps explaining the single-slot constraint.

Forced tool choice

The agent runs with toolChoice: { type: 'tool', toolName: 'submit_slots' } — the LLM commits to structured output on step 1. The five investigation tools are still wired in (the prompt mentions them) but are no longer the path of least resistance: the full SHACL is embedded in the system prompt, so investigation calls are typically redundant. Forcing the choice eliminated a class of failures where cautious models (Haiku, in particular) exhausted LLM_MAX_AGENT_STEPS on discover_* calls and never reached submit_slots.

Context Engineering

The system prompt is auto-generated from raw SHACL shapes at startup. The LLM reads native Turtle directly:

Why raw SHACL in the prompt

The LLM natively understands SHACL constraint vocabulary:

  • sh:in (...) — allowed values → synonym resolution
  • sh:pattern — format constraints (ISO codes, etc.)
  • sh:datatype xsd:integer → range queries
  • sh:description — semantic context for disambiguation

No properties can be missed because the LLM sees the full shapes. The investigation tools supplement this with on-demand exploration for edge cases.

Post-LLM Validation

Three corrections run after the LLM submits slots:

CorrectionLogicExample
FilterExact → case-insensitive → substring → edit-distance ≤ 4 → gap"motoway""motorway"
DomainProperty→domain map; add missing, keep validscenario + roadTypes → adds hdmap
ConfidenceRecompute from match quality, not LLM self-assessmentExact = high, fuzzy = medium

Investigation Tools (RDF Reasoning)

Five tools query the schema graph (<urn:graph:schema>) at runtime, giving the LLM on-demand ontology exploration:

ToolPurposeReturns
discover_domainsList searchable asset types[{domain, classIri}]
discover_propertiesFilterable properties for a domain[{localName, datatype, hasEnum}]
discover_valuesAllowed sh:in values for a property["motorway", "rural", ...]
discover_connectionsCross-domain references[{from, to}]
investigate_schemaArbitrary SPARQL SELECT on schema[{var1, var2, ...}]

Safety: Read-only, SELECT-only, 50-row cap, no interactive permissions.

When used: Most queries resolve from the static prompt alone. Tools activate for niche properties, unfamiliar concepts, or complex cross-domain exploration.

Provider Flexibility

ProviderSDKUse Case
GitHub Copilot@github/copilot-sdkEnterprise, GitHub-integrated
OpenAIVercel AI SDKCloud, highest quality
Anthropic@ai-sdk/anthropicDirect Claude API access
claude-cli@ai-sdk/anthropic + CLIReuses the local claude CLI's OAuth session (no API key)
vibe-cli@ai-sdk/openai-compatRoutes through the local vibe CLI (Mistral models)
OllamaVercel AI SDKLocal, privacy-first

All providers share the same validation pipeline. Selected via the AI_PROVIDER env var; the model is selected by AI_MODEL.

Tuning knobs

Env varDefaultNotes
LLM_TEMPERATURE0Slot filling is extraction, not generation. Variance is just noise — default is greedy decoding.
LLM_THINKING_BUDGET0Token budget for Anthropic's thinking block (claude-cli/anthropic only). Other providers select reasoning by model name.
LLM_MAX_AGENT_STEPS3Hard cap on tool-call rounds. With toolChoice forcing submit_slots, the typical query needs 1 step.

Reasoning mode by provider: Mistral uses the magistral-* family, OpenAI uses the o-series model names (o1, o4-mini), Anthropic exposes a typed thinking block — LLM_THINKING_BUDGET is the only var that surfaces it explicitly.