"They asked me to compute the RMS of a 50 kHz vibration window in my head. I offered instead to call the function that does it correctly, and everyone was much happier."
A Self-Aware AI Agent
Prerequisites
This section assumes you know the numerical instruments an agent will call: the linear-phase and IIR filters of Chapter 6, the recursive state estimators of Chapter 9, the change and anomaly detectors of Chapter 12, and the zero-shot forecasters of Chapter 19. It follows Section 22.1, which framed the agentic loop, and Section 22.2, which built the evidence packet the agent reasons over. Here the agent stops merely reading precomputed evidence and starts choosing which computation to run next.
The Big Picture
A language model is a superb router and a terrible calculator. Ask it to estimate a spectral centroid from raw samples and it will produce a confident, wrong number; give it a compute_spectrum function and it will call it with the right arguments and read the answer correctly. Tool use is the design pattern that puts each job where it belongs: the deterministic, calibrated, testable numerical work stays in ordinary code (a bandpass filter, a CUSUM detector, a Kalman smoother, a forecasting foundation model), and the model's role is to decide which tool to invoke, with what parameters, in what order, and how to act on the result. The agent becomes an orchestrator of the entire toolbox this book has built, not a replacement for any single tool in it. This section is about wiring that toolbox to a model safely: how to describe a detector as a callable function, how the observe-reason-act loop drives a multi-step diagnosis, and how to keep every number the agent utters traceable to a tool that actually produced it.
A tool is a typed instrument with a contract
In agent terminology a tool is a function the model may call, described to it by a machine-readable schema: a name, a one-line purpose, and a typed parameter list. The model never sees the implementation; it sees only the contract and decides whether calling it advances the task. For sensing, the natural tools are the operators you already trust. A bandpass tool wraps a Butterworth filter with cutoff arguments in hertz. A detect_change tool wraps a CUSUM or a residual test and returns a changepoint index with a score. A forecast tool wraps a model such as TimesFM or Chronos and returns a horizon of predictions with quantiles. Each is a small, deterministic, unit-tested piece of code; the intelligence added by the agent is purely in the choreography.
The contract matters more than the code. A good sensing tool declares its units (hertz, milliseconds, g, not dimensionless integers), returns structured results rather than prose, and carries its own uncertainty so the model cannot silently drop it. If detect_change returns just an index, the agent may report a changepoint as certain fact; if it returns \(\{\text{index}, \text{score}, p\}\), the agent can reason about whether the evidence clears an action threshold. Design tool outputs the way you would design a lab instrument's readout: a value is never useful without its scale and its error bar, a discipline that traces straight back to the calibration language of Chapter 18.
Key Insight
Never let the model do arithmetic it can delegate. Every quantitative claim an agent makes should be the return value of a tool call, not a token the model generated. This single rule eliminates the most damaging failure mode in sensing agents: fluent hallucinated numbers. A model that says "the bearing fault frequency is 162 Hz" from memory is guessing; a model that calls peak_frequency(band=[120,220]) and reports what came back is measuring. Ground numbers in tools, and the agent's confidence becomes an honest reflection of the instruments underneath it.
The observe-reason-act loop
Tool use turns a one-shot prompt into an iterative loop, usually the ReAct pattern: the model emits a short reasoning trace, then a structured tool call; the runtime executes the call and appends the result; the model observes the result and either calls another tool or finishes. A diagnosis is rarely one call. A vibration agent might first bandpass a raw accelerometer window to isolate the shaft-order band, then compute_spectrum on the filtered signal, then peak_frequency to locate the dominant line, then forecast the trend of that peak's amplitude to estimate how many days remain before it crosses an alarm limit. No single tool answers "should I schedule maintenance"; the chain does, and the model's contribution is knowing that a spectral peak on a filtered signal is the right next step after a raw window looks suspicious.
Two properties make this loop trustworthy. Tools should be idempotent and deterministic so the same call yields the same result, which makes runs reproducible and cacheable (and keeps cost bounded, the subject of Section 22.7). And every call plus its result should be logged as a transcript, giving you an auditable record of exactly which filter ran with which cutoffs on which window. When an operator later asks "why did you say the pump was failing," the answer is a replayable sequence of tool calls, not an opaque paragraph.
Building the sensing toolbox
Below is a minimal registry that exposes three sensing tools to a function-calling model. The @tool decorator captures each function's typed signature and docstring and emits the JSON schema that model APIs expect, so adding a new instrument is a matter of writing one plain Python function. The dispatch helper is what the runtime calls when the model returns a tool request; it validates arguments and returns a structured result the model reads on the next turn.
import numpy as np, json
REGISTRY = {}
def tool(fn):
"""Register a plain function as an agent-callable tool."""
REGISTRY[fn.__name__] = fn
return fn
@tool
def bandpass(signal: list, fs_hz: float, low_hz: float, high_hz: float) -> dict:
"""Zero-phase bandpass filter. Returns the filtered window."""
x = np.asarray(signal, float)
f = np.fft.rfftfreq(len(x), 1 / fs_hz)
keep = (f >= low_hz) & (f <= high_hz)
y = np.fft.irfft(np.fft.rfft(x) * keep, n=len(x))
return {"filtered": y.round(4).tolist(), "band_hz": [low_hz, high_hz]}
@tool
def detect_change(signal: list, k: float = 0.5, h: float = 5.0) -> dict:
"""Two-sided CUSUM change detector. Returns index, score, and a flag."""
x = np.asarray(signal, float)
z = (x - x.mean()) / (x.std() + 1e-9)
s_hi = np.maximum.accumulate(np.clip(np.cumsum(z - k), 0, None))
idx = int(np.argmax(s_hi)) if s_hi.max() > h else -1
return {"changepoint": idx, "score": float(s_hi.max()), "fired": idx >= 0}
@tool
def peak_frequency(signal: list, fs_hz: float) -> dict:
"""Dominant spectral line and its relative prominence."""
x = np.asarray(signal, float)
mag = np.abs(np.fft.rfft(x * np.hanning(len(x))))
f = np.fft.rfftfreq(len(x), 1 / fs_hz)
i = int(np.argmax(mag[1:]) + 1)
return {"peak_hz": float(f[i]), "prominence": float(mag[i] / (mag.mean() + 1e-9))}
def dispatch(call: dict) -> dict:
"""Execute a model-issued tool call: {'name':..., 'args':{...}}."""
fn = REGISTRY[call["name"]]
return fn(**call["args"])
# What the model emits on turn 2, after seeing a raw window looks odd:
model_call = {"name": "bandpass",
"args": {"signal": list(np.random.randn(256)),
"fs_hz": 2048, "low_hz": 120, "high_hz": 220}}
print(json.dumps(dispatch(model_call))[:80], "...")
dispatch runs the trusted numerical code and returns a structured result the model reads on its next turn.The registry pattern scales cleanly: a kalman_smooth tool from Chapter 9 and a forecast tool from Chapter 19 drop in the same way, and the model composes them without any new orchestration code. The hard engineering is not the wiring, it is routing: teaching the model, through tool descriptions and a few worked examples, which instrument fits which symptom. A crisp description ("use detect_change for step or drift shifts in slow telemetry; use peak_frequency for periodic mechanical faults in fast vibration") does most of the routing work.
Practical Example: the wind-turbine gearbox agent
A utility runs an agent over nacelle accelerometers on 400 turbines. An alert fires on turbine 118. The agent calls bandpass(120, 220) to isolate the gear-mesh band, then peak_frequency, which returns 162 Hz with high prominence; a lookup tool maps that to the intermediate-shaft gear-mesh line. It then pulls the last 90 days of that peak's amplitude and calls forecast, which projects the trend crossing the vibration alarm limit in roughly eleven days with an 80 percent interval of six to nineteen. The agent writes a work order: "Intermediate-shaft gear-mesh amplitude trending up, projected alarm in about 11 days (6 to 19), inspect at next low-wind window." Every number in that sentence is a tool return, not a guess. The maintenance planner schedules a crane during a calm forecast window instead of after a failure, and the whole chain is replayable from the logged transcript.
Routing, grounding, and graceful failure
Three disciplines separate a demo from a deployable agent. Routing must degrade safely: if no tool fits, the agent should say so and escalate rather than force an irrelevant call, and you constrain the space by exposing only the tools relevant to the asset class. Grounding must be enforced, not hoped for: a post-hoc check can verify that every numeral in the agent's final message appears in some tool result, rejecting outputs that smuggle in invented figures. Failure must be a first-class return: tools should surface errors as structured objects (a saturated sensor, a too-short window, an out-of-range cutoff) so the model can react, perhaps by widening the window and retrying, instead of receiving a stack trace it cannot interpret. An agent that handles a "signal too short for this band" error by requesting more data is behaving like an engineer; one that hallucinates a result to fill the gap is the reason people distrust these systems.
Library Shortcut
Writing the schema-generation, argument validation, retry, and transcript plumbing by hand runs to roughly 200 lines and is easy to get subtly wrong. Frameworks that expose native function calling (the OpenAI and Anthropic tool APIs, or orchestration layers such as LangGraph and LlamaIndex) reduce the registry-and-loop wiring above to about 15 lines: you decorate each Python function, hand the list to the client, and the framework handles JSON-schema emission, the multi-turn tool loop, parallel calls, and result marshalling. Keep writing the detectors and filters yourself, since those are your trusted numerical core; let the library own only the model-to-tool glue.
Exercise
Extend the registry with a forecast(signal, horizon) tool that returns a point path plus 10th and 90th percentile bands (a naive drift model is fine). Then write a grounding checker that scans an agent's final text, extracts every number, and flags any that does not appear in the concatenated tool results within a tolerance. Feed it a message that contains one hallucinated figure and confirm the checker catches it. Which numeric formats (scientific notation, units, rounding) break naive matching, and how would you normalize them?
Self-Check
1. Why is delegating arithmetic to tools, rather than letting the model compute, the central safety move in this pattern?
2. A tool returns only a changepoint index with no score. What reasoning failure does that invite, and what would you add to the return value?
3. Give a concrete sensing task where the correct behavior is for the agent to call no tool and escalate to a human instead.
What's Next
In Section 22.5, we let several tool-using agents collaborate: a recursion-of-thought structure in which specialist agents call their own toolboxes and pass partial conclusions to a coordinator, turning the single diagnostic chain of this section into a full multi-agent root-cause investigation.