> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sensecode.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Tool-Calling

> ask_with_tools(prompt, tools) — a real model chooses which tool to call, still gated by the same capability checks a hand-written call goes through.

## The shape of it

```sns theme={null}
tool get_weather(city: String) returns String requires weather.read "get the current weather for a city":
    return "sunny, 22C"

policy:
    allow weather.read

model claude = inference("anthropic")
set delegation = claude

result = ask_with_tools("what's the weather like in Paris right now?", [get_weather])
print(result.value)
```

`ask_with_tools(prompt, tools)` hands the model a list of
[`tool`](/ai-native/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

<Note>
  **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.
</Note>

```sns theme={null}
tool charge_card(amount: Int) returns String requires payment.execute "charge a customer's card":
    return "charged " + str(amount)

policy:
    deny payment.execute

result = ask_with_tools("charge $50 to the card", [charge_card])
print(result.value)   # something like "I don't have permission to do that" --
                       # the model was told the call was denied, but as a
                       # fed-back tool result, not a crash
```

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()`](/safety/actions#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

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="It can't be an async tool">
    An [`async tool`](/ai-native/tool#concurrency-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.
  </Step>

  <Step title="It can also be a Skill or an MCP tool">
    A [`skill(...)`](/ai-native/skills) or a tool from
    [`connect_mcp(...)`](/ai-native/mcp) works the same way here — see
    those pages for what's different about each.
  </Step>
</Steps>

<Info>
  The loop is capped at 10 iterations, not yet configurable — a model that
  never produces a final answer raises `SenseRuntimeError` rather than
  looping forever.
</Info>

## 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:

```sns theme={null}
persistent memory profile

tool remember_fact(key: String, value: String) returns String "remember a fact about the user":
    profile.remember(key, value)
    return "ok"

tool recall_fact(key: String) returns String "recall a previously remembered fact":
    result = profile.recall(key)
    if result == nil:
        return "(unknown)"
    return result

result = ask_with_tools("remember my favorite color is blue, then tell me what it is", [remember_fact, recall_fact])
```

For the read-only counterpart — giving a model what's already in memory
*without* a tool call — see
[Memory: reasoning with memory](/ai-native/memory#reasoning-with-memory-ask_with_memory).

<Warning>
  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.
</Warning>

## What this doesn't do (yet)

<AccordionGroup>
  <Accordion title="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).
  </Accordion>
</AccordionGroup>

## Continue

<CardGroup cols={2}>
  <Card title="Tool" icon="wrench" href="/ai-native/tool">
    What gets handed to `ask_with_tools` — declaration, capability
    checks, and the description requirement.
  </Card>

  <Card title="Skills" icon="layer-group" href="/ai-native/skills">
    Bundling several tools under one reusable, named description.
  </Card>

  <Card title="MCP" icon="plug" href="/ai-native/mcp">
    Handing a model tools from an external process instead of Sense code.
  </Card>

  <Card title="Memory" icon="brain" href="/ai-native/memory">
    The read-only counterpart to write-back: giving a model what's already
    known without a tool call.
  </Card>
</CardGroup>
