The shape of it
ask_with_tools(prompt, tools) hands the model a list of
tool values and lets it decide whether — and which
one — to call, instead of Sense code deciding by hand. It returns a
Answer, same as a plain ask(...) call.
When to use this
Reach forask_with_tools instead of a plain ask() call whenever
the answer might require looking something up or taking an action you’ve
already written as a tool — a search, a lookup, a calculation — and you
want the model itself to decide whether that’s needed, rather than
hand-coding “if the prompt mentions weather, call get_weather.” Use a
plain ask() call when there’s nothing for the model to invoke, just
a question to answer from what it already knows.
Every tool call still goes through the same enforcement path
The model proposes,
_call_tool still disposes. A tool the model
decides to call goes through the identical arity/capability/type-check/
audit path a hand-written call reaches — nothing is trusted more just
because a model asked for it.charge_card, the call genuinely never runs — same
guarantee as a denied hand-written call, and the denial lands in
audit_log() right alongside every call the
model was allowed to make. The error is fed back to the model as the
tool result, not re-raised to Sense code — ask_with_tools() still
returns normally.
Requirements on a tool passed here
1
It needs a description
tool takes one more optional trailing string for this — backward
compatible (every existing tool keeps working undescribed exactly as
before); it only becomes required at the point a tool is handed to
ask_with_tools (SenseRuntimeError naming the tool if missing).
It has to stay on the same line as the rest of the signature — Sense’s
indentation-based lexer has no bracket-style newline suppression
spanning past it.2
Its parameter types become a JSON schema
Int→integer, Float→number, String→string, Bool→boolean,
Array→array, no annotation/Any→ unconstrained. Every declared
param is required — Sense has no optional parameters.3
It can't be an async tool
An
async tool in the list
raises an error — its result is a pending Future, not a value, and
feeding that back to a model needs its own design, refused rather than
half-supported.4
It can also be a Skill or an MCP tool
A
skill(...) or a tool from
connect_mcp(...) works the same way here — see
those pages for what’s different about each.The loop is capped at 10 iterations, not yet configurable — a model that
never produces a final answer raises
SenseRuntimeError rather than
looping forever.Letting a model manage its own memory
Wrapping<memory>.remember(...) in a tool and handing it to
ask_with_tools lets a model decide what’s worth remembering — no new
interpreter feature, just composition of what’s already built, gated and
audited exactly like any other tool call:
What this doesn’t do (yet)
async tool support, provider-agnostic tool-calling
async tool support, provider-agnostic tool-calling
ask_with_tools refuses an async tool outright rather than
half-supporting it, and its message format is Anthropic-shaped only
for now — to be generalized once a second provider needs tool-calling
too. Also not built: multi-turn history across separate
ask_with_tools calls, a configurable iteration cap, and parallel
tool-call execution within one turn (sequential for now).Continue
Tool
What gets handed to
ask_with_tools — declaration, capability
checks, and the description requirement.Skills
Bundling several tools under one reusable, named description.
MCP
Handing a model tools from an external process instead of Sense code.
Memory
The read-only counterpart to write-back: giving a model what’s already
known without a tool call.

