Skip to main content

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 for ask_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.
If the model requests 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

Intinteger, Floatnumber, Stringstring, Boolboolean, Arrayarray, 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:
For the read-only counterpart — giving a model what’s already in memory without a tool call — see Memory: reasoning with memory.
Automatic write-back (parsing a model’s free-text response to guess what to persist) was deliberately not attempted — it would mean unreliably inventing facts to store, silently, with no capability check on what gets written. The tool-based approach above gets the same outcome honestly: the model decides explicitly, through a call that’s gated and auditable like everything else in this language.

What this doesn’t do (yet)

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.