Tutorial - Use Native Calls Under Touba Authority
This tutorial shows how to let a model propose a native function call without giving it execution authority. The Touba program keeps its contracts; only the representation used by the provider changes.
At the end, you will have an agent that can select and reuse its skills through the SDK native interface while the runtime still validates every decision.
What Changes and What Does Not
| Element | Behavior |
|---|---|
.tb source | unchanged; no special syntax is required |
| AIR and runtime protocol | unchanged |
| SDK provider | decisionInterface: "native" enables model function calls |
| execution | remains exclusively controlled by the Touba runtime |
| default mode | remains structured until the host explicitly selects native |
The native interface is useful when the selected model supports strict function
calls and structured outputs. It does not turn a skill, function, strategy,
or agent into a Touba tool: it only provides a common representation during
decision making.
Prerequisites
- the
toubacommand; - the
@touba/sdkJavaScript SDK; - Node.js to load the provider module;
- a model that supports function calls and structured outputs;
- a provider key stored in the environment or in a host secret manager.
Install the SDK archive
Use Node.js 24 and npm. In a macOS/Linux terminal:
mkdir -p native-calculator-demo
cd native-calculator-demo
mkdir -p providers vendor
npm init -y
node --input-type=module -e 'import {execFileSync} from "node:child_process"; import {copyFileSync} from "node:fs"; const v=JSON.parse(execFileSync("touba",["version","--json"],{encoding:"utf8"})); copyFileSync(v.distribution.sdk_archive,"vendor/touba-sdk.tgz");'
npm install --offline --ignore-scripts --save-exact ./vendor/touba-sdk.tgz
Step 1 - Write the Touba Program
Create native-calculator.tb:
module examples.native_calculator
skill Double
description "Doubles one integer and returns the result."
takes item as Int
gives Int
do
give item * 2
skill AddTen
description "Adds ten to one integer and returns the result."
takes item as Int
gives Int
do
give item + 10
agent Calculator
description "Selects declared arithmetic skills for the requested calculation."
takes start as Int
gives Int
responsibility "Starting from start, invoke Double on the initial value, invoke Double again on the observed result, then invoke AddTen on that result. Return the final observed integer."
uses Double
uses AddTen
callable_by strategy RunCalculation
strategy RunCalculation
description "Runs one governed calculation."
takes start as Int
gives Int
uses Calculator
do
Calculator
with start as start
into result
give result
The agent scope is declared by uses. The provider can only propose Double or
AddTen. The callable_by rule still reserves entry into Calculator for the
declared strategy.
Compile the program:
touba compile native-calculator.tb --out native-calculator.air.json
Step 2 - Enable the Native Interface in the Provider
Create providers/calculator.provider.mjs:
import { createOpenAiDecisionProvider } from "@touba/sdk";
export const provider_metadata = Object.freeze({
provider_kind: "sdk_ai",
provider_name: "calculator-provider",
provider_version: "1.0.0",
model_family: "openai",
prompt_template_id: "calculator-native-v1",
decision_schema_version: "touba-agent-decision-v1",
});
export const decideAgentCapability = createOpenAiDecisionProvider({
model: "gpt-4.1-nano",
decisionInterface: "native",
instructions: [
"Complete the requested calculation using observed data.",
"Use only capabilities authorized by the Touba runtime.",
"Return a final answer only after the required calculation is complete.",
].join(" "),
maxOutputTokens: 1000,
});
Application-controlled instructions use English to keep the provider protocol consistent. Business inputs and answers may remain in the user's language.
For another AI SDK-compatible provider, use the same option with
createAiSdkDecisionProvider:
Supply a real model object from the installed provider to createAiSdkDecisionProvider({ model, decisionInterface: "native" }); see the SDK guide.
If the model rejects temperature or forced calls, add temperature: null or
nativeToolChoice: "auto", respectively. These are transport options, not
permissions: Touba still rejects a selection without a valid call. Also qualify
the connector's reasoning settings and output limit before running a paid workflow.
Step 3 - Run the Agent
Expose the key in the environment. Do not place it in source, inputs, or the provider file:
: "${TOUBA_AGENT_DECISION_OPENAI_API_KEY:=${OPENAI_API_KEY:?Set OPENAI_API_KEY in your shell or secret manager}}"
export TOUBA_AGENT_DECISION_OPENAI_API_KEY
Run the strategy:
touba run native-calculator.air.json \
--entry strategy:examples.native_calculator/RunCalculation \
--agent-decision-provider providers/calculator.provider.mjs \
--agent-decision-provider-initial-mode capabilities \
--max-agent-steps 3 \
--max-provider-calls 8 \
--inputs-json '{"start":6}' \
--output json \
--trace-out native-calculator.trace.json > result.json
node --input-type=module -e 'import assert from "node:assert/strict"; import {readFileSync} from "node:fs"; const run=JSON.parse(readFileSync("result.json","utf8")); assert.equal(run.status,"success"); assert.equal(run.result,34); assert.deepEqual(run.trace.events.filter(e=>e.kind==="agent_capability_selected").map(e=>e.payload.selected_ref),["skill:examples.native_calculator/Double","skill:examples.native_calculator/Double","skill:examples.native_calculator/AddTen"]); console.log("NATIVE_CALCULATION_OK");'
The capabilities initial mode saves one round trip when you know the task
requires a capability. Keep the default summary mode when the model must be
able to answer without consulting the catalog.
This command performs billable calls to the actual provider. Expected calculation: 6 → 12 → 24 → 34. The same capability is reused. A provider or model failure remains a failed test; do not substitute a precomputed answer.
This sequence requires six provider calls without retries: three selections, two intermediate orientations and one finalization. The limit of eight leaves room for retries; it does not represent eight skill calls.
Step 4 - Understand the Actual Cycle
Touba agent
-> structured orientation, or an explicitly requested initial catalog
-> authorized catalog built by the runtime
-> native functions derived from signatures
-> one proposed operation or typed final result
-> SDK normalization without execution
-> runtime validation, then execution or result return
-> observation associated with the call arguments
-> next decision or structured finalization
During the catalog phase, the SDK exposes one native function per authorized
capability, plus touba_finish for a final result matching gives. The model
must propose exactly one call. The SDK does not provide an
execute function to the provider, so receiving a call has not produced an
effect yet.
The runtime receives a normal Touba decision and checks the target, arguments,
and authority before entering the capability. touba_finish becomes an
answer_directly decision without a business effect; its result must match the
agent's gives type. Free prose without this control is still rejected.
Step 5 - Reuse a Capability
An authorized capability is not consumed by its first call. The agent may
select Double several times with identical or different arguments. Every
invocation is validated and counted separately.
Reuse stops when the task completes or when a limit applies, including:
maxAgentStepsfor agentic steps;max_provider_callsfor provider calls;max_tool_callsfor tools that are actually dispatched;- duration and execution budgets declared by the project.
The used-capabilities history describes what happened. It is not a list of capabilities forbidden during the rest of the run.
Step 6 - Check the Authority Boundary
Conversation history is enabled by default: the SDK keeps connector messages
between steps of this invocation. The two calls to Double remain distinct
operations, each with its runtime result. To observe size, add
history: { onEvent: event => console.info(event) } to the provider configuration.
These events do not contain messages. For size limits and restart persistence
with SQLite, see Agent conversation history.
History storage does not replace checkpoints or the effect journal.
| Model proposal | Touba control |
|---|---|
| selected native function | the target must belong to uses |
| generated arguments | signatures and types are checked again |
| another agent call | callable_by is checked on every invocation |
| memory access | source scopes, grants, and resource bindings remain required |
| secret use | only the authorized handle can cross; the resolved value stays out of the prompt |
| validation request | the decision belongs to the human, not the model |
| external effect | manifest, grants, effect identity, and runtime journal still apply |
| resume | retained observations do not redispatch past effects |
An unknown function name, multiple calls in one proposal, invalid arguments, or an incomplete provider response is refused before capability execution.
Step 7 - Inspect the Result
touba inspect native-calculator.trace.json
The trace distinguishes the decision, selected capability, budget consumption, execution, and terminal answer. It must not contain the provider key or a resolved secret value.
Choose Between structured and native
Choose structured | Choose native |
|---|---|
| portable default behavior | model reliable with strict function calls |
| provider without native-function support | closer fit with the provider's native interface |
existing integration based on generateObject | rich catalog with directly exposed signatures |
Both interfaces use the same runtime protocol and authority boundary. Switching
interfaces does not require a .tb program change.
Common Errors
| Symptom | Check |
|---|---|
| no native call is produced | verify function-call support in the model and transport |
| decision is refused | verify uses, callable_by, descriptions, and argument types |
IncompleteModelDecision response | verify that provider generation actually completed |
| a limit is reached before the answer | adjust budgets without removing required controls |
| observation is not available inline | verify whether opaque_refs was selected intentionally |
For more detail, read the Touba SDK guide and the governed agentic decision concept.