SDK JavaScript
Host packages and execution options
Both SDKs accept a module path or a
pkg:organization.package/identifier selector. Declare the module in
the installed package's hosts. Installation verifies content; activation
is an explicit application choice.
| Setting | JavaScript | Python |
|---|---|---|
| Attestation | humanApprovalHost | human_approval_host |
| Agent decisions | agentDecisionProvider | agent_decision_provider |
| Attestation timeout, ms | humanApprovalAuthorityTimeoutMs | human_approval_authority_timeout_ms |
| Pure-function detail | tracePureFunctions | trace_pure_functions |
The timeout defaults to 5,000 ms, with a range of 1 to 60,000 ms. Detail defaults
to full; calls keeps calls and governance with fewer internal events.
See packages and traces.
The JavaScript SDK calls the same governed CLI as the Python SDK. Both guides follow the same path, with the same concepts and introductory examples.
Handler callback arguments
JavaScript calls run({ request, context }); Python calls
run(request, context). The declared input signature determines request.
| Declared inputs | JavaScript request | Python request |
|---|---|---|
| None | {} | {} |
One: value as Text | "Invoice 42" | "Invoice 42" |
Two: left as Int, right as Int | { left: 2, right: 3 } | {"left": 2, "right": 3} |
With one input, read the value directly from request. Context contains
execution metadata and helpers; business inputs are never siblings of it.
Without an explicit input signature, the handler receives the arguments map.
Installation
The Touba distribution includes a self-contained npm archive. No public registry or project publication is required. Node.js 24 and npm are required. From the JavaScript project root, run:
Choose the JavaScript SDK:
touba sdk install javascript
Touba verifies the bundled archive, copies it into
vendor/touba-sdk/<hash>/, then runs npm offline with install scripts disabled.
It creates package.json if needed and checks the ToubaEngine import.
Projects using pnpm, Yarn or Bun keep their package manager: this command
refuses to change their lockfile. touba sdk path javascript prints the
verified archive path to copy into the project and add with that manager.
The archive includes provider dependencies and their licenses. Installation
requires no network access. Keep the archive, package.json and lockfile so
the project can be installed again. One @touba/sdk dependency per project
serves all handlers and providers. Do not link to the mutable CLI installation:
upgrading the CLI must not change the project's SDK. SDK upgrades require an
explicit installation of a compatible archive.
touba init, touba prepare and touba handler stub neither download packages
nor change JavaScript dependencies. For projects with tools, prepare reports
sdk_dependency, distinguishing a missing SDK, incompatible version, missing
provider dependency and undeclared dependency. Its generation status does not
replace this availability check. An unfinished stub refuses execution with
HandlerNotImplemented.
On Windows, the SDK recognizes the installed touba.cmd launcher and calls
the bundled Node executable with the CLI entry point, without a command shell.
Paths with spaces and arguments remain literal values. Custom .cmd or .bat
scripts are refused: use an executable with separate arguments. This discovery
does not verify publisher signatures.
Updates
Updating the CLI does not replace project-local copies of @touba/sdk. Check from each application:
npm ls @touba/sdk
node --input-type=module -e 'console.log(import.meta.resolve("@touba/sdk"))'
After upgrading the CLI, explicitly install the matching SDK:
touba sdk install javascript
Archives are content-addressed even while V1 keeps version 1.0.0. The new dependency refers to the project copy; other applications keep their SDK.
Review and retain the updated lockfile, then rerun business tests. Do not delete checkpoints or project data. For triggers use triggerEvent and triggerState; the entry reference option is entryRef, not entry.
Imports
import { ToubaEngine, ToubaEngineError, ToubaHandlerError } from "@touba/sdk";
import { assertRunSucceeded } from "@touba/sdk/testing";
Install the @touba/sdk package. Test helpers are in its testing submodule.
Run a program
module sdk.engine
strategy Review
takes value as Text
gives Text
do
give value
import { ToubaEngine } from "@touba/sdk";
const engine = new ToubaEngine({ projectRoot: "." });
const run = await engine.run({ source: "program.tb", inputs: { value: "Invoice 42" } });
console.log(run.status, run.reason, run.result);
CLI Engine
The engine consumes the complete JSON envelope returned by the CLI, including large results and show messages. Invalid or truncated JSON remains an error: a successful-looking trace is never substituted for a missing result. Use the documented status, reason and exit code to distinguish failure, waiting, rejection and cancellation.
Role
ToubaEngine lets a JavaScript application call Touba without manipulating the internal runtime directly.
It drives the Touba CLI:
JavaScript app -> @touba/sdk ToubaEngine -> Touba CLI -> AIR/runtime -> trace
The CLI remains the execution contract. The SDK does not become the authority of the language.
Shape
import { ToubaEngine } from "@touba/sdk";
const engine = new ToubaEngine({
projectRoot: ".",
});
const run = await engine.run({
source: "src/program.tb",
entryRef: "skill:examples.project/Run",
manifest: "touba.manifest.json",
secrets: "touba.secrets.dev.json",
inputs: {
request: {
name: "Bamba",
},
},
});
console.log(run.status);
console.log(run.result);
console.log(run.trace);
Visible Parameters
| Element | Role |
|---|---|
projectRoot | host project root used to resolve relative paths |
source | .tb file compiled by the CLI before execution |
air or airJson | explicit alternative to source if AIR already exists |
entryRef | AIR entry to execute, for example a skill or strategy |
manifest | tool binding manifest |
secrets | secret-handle file |
resourceBindings | touba-resource-bindings-v1 file passed to touba run --resource-bindings |
requireResourceBindings | requests refusal if a required logical resource has no active binding |
inputs | input object passed to the runtime |
agentDecisions | explicit agent decisions, bounded by uses |
agentDecisionProvider | path to a local provider called by the CLI to choose an agent capability |
agentDecisionProviderInitialMode | summary or capabilities, explicitly relayed to the CLI |
agentDecisionProviderContentMode | inline or opaque_refs, without implicit data conversion |
maxAgentSteps | local step limit for a provider-driven agentic loop |
humanCheckpointOut | private checkpoint path, written only when the run awaits approval |
humanResumeCheckpoint | input checkpoint path; combine with humanCheckpointOut to save a possible subsequent pause |
parallelCheckpointOut, parallelResumeCheckpoint | private paths for saving and resuming parallel group progress |
parallelLimits | host ceilings for groups and the run; see the CLI reference |
userInputs | explicit ask answers keyed by interaction reference; use occurrence references in parallel branches |
humanDecisions | map from approval references to runtime decisions |
effectJournal | durable effect journal path; keep the same journal on resume |
triggerEvent | normalized event: trigger_ref, event_id, source_kind and optional named arguments; not an ordinary entrypoint |
triggerState | path ending in .triggers.sqlite, for persistent deduplication and quotas |
supplyChainLock, requireSupplyChainLock | explicit lock and mandatory pre-execution validation |
runLimits, maxProviderCalls | explicit execution and provider-call bounds |
A ToubaEngine instance represents a project or session context. It can run several files or entries; there is no need to create one instance per .tb file.
Compile a program
const compiled = await engine.compile({ source: "program.tb" });
const compiledRun = await engine.run({ airJson: compiled.airJson, inputs: { value: "Invoice 42" } });
// compiled.airDocument is the parsed object; airJson also accepts this object.
This extends run.mjs. compile returns airJson (text) and airDocument (object). Supply exactly one input to run: source, air (file), or airJson (text or object).
Results and errors
pendingInteractions (JavaScript) and pending_interactions (Python) expose
all currently pending questions as {interaction_ref, prompt} objects, or [].
The singular field identifies the first question for user_input_pending.
Answer using userInputs / user_inputs keyed by occurrence, then resume with
parallelResumeCheckpoint / parallel_resume_checkpoint. Private continuation
state is not part of this public list.
For a nontechnical outcome, run.status is success, stopped, or
cancelled. run.reason distinguishes completion, waiting, explicit stop,
human refusal, expiration, and cancellation. Waiting or human refusal does not
throw a technical exception. run.messages exposes show messages as
{ interaction_ref, text }; run.pendingInteraction exposes a pending input
prompt. Business content is not copied into run.trace.
Compilation, admission, runtime, and launch failures throw ToubaEngineError.
Inspect error.details.exitCode, status, reason, trace, and stderr
when available. Launch failures also expose code. Never convert the exception
into success. Public results exclude private checkpoints. Human decisions must
target references from the AIR actually executed, not another program.
Store execution traces
const run = await engine.run({ source: "program.tb", traceStore: ".touba/traces.sqlite" });
SQLite stores traces in .touba/traces.sqlite at the engine’s project root
by default. The path above can be customized. Each segment remains available
after the process exits; traceOut / trace_out also produces a JSON export.
See trace retention for archival.
For a long trace, choose traceMode: "reference". Once the journal is finalized,
the response contains the business result, traceReference, and
trace set to null. Read events in pages:
const traceStore = ".touba/traces.sqlite";
const run = await engine.run({
source: "program.tb", traceStore, traceMode: "reference",
});
let after;
while (true) {
const page = await engine.readTrace({
traceStore, reference: run.traceReference, after, limit: 100,
});
for (const event of page.events) console.log(event.kind);
if (page.done) break;
after = page.next;
}
readTrace uses the read-only Core reader. limit accepts 1 to 1,000 events;
event content in each page is capped at 1 MiB. Runtime errors carry their reference
in error.details.traceReference. References support reading; execution resumes
from its checkpoint. Keep the reference and journal
to resume reading after a restart. Start without a cursor to verify the full
segment digest; subsequent pages check their events and order.
In inline mode, trace contains the complete document. The 32 MiB transport
limit applies to SDK responses, including business results and messages.
A long trace can be stored and read in pages without increasing that limit.
See the trace reference.
Write a handler
After touba prepare --mode write or touba handler stub, preserve the
run, TOUBA_HANDLER_SDK and TOUBA_HANDLER_GENERATED_BY exports, the signature
and the file's execution guard. Replace only the business body of run, then
run the generated *.handler.contract.test.mjs test. The defineTool examples
below show the standalone API, not complete replacements for generated files.
With one declared parameter, request is that parameter's value. With zero or
multiple parameters, it is the named-argument object. inputSignature contains
unique names and their type_ref; outputType is the declared type or null
for a tool without a result.
Role
The SDK simplifies:
- reading the request sent by the runtime;
- JSON parsing;
- defensive verification of
tool_ref; - serialization of the return value;
- serialization of controlled errors.
Shape
import { defineTool } from "@touba/sdk";
await defineTool({
toolRef: "tool:module.name/ToolName",
inputSignature: [{ name: "request", type_ref: "type:module.name/Input" }],
outputType: "type:module.name/Output",
requiredGrants: ["network"],
run: async ({ request, context }) => {
return await runHostCode(request, context);
},
});
defineTool identifies the expected tool, defensively verifies received grants and secret handles, then calls the business function run({ request, context }). The function returns a value conforming to the tool gives.
Example
import { defineTool } from "@touba/sdk";
await defineTool({
toolRef: "tool:examples.search/SearchDatasets",
inputSignature: [{ name: "request", type_ref: "type:examples.search/SearchRequest" }],
outputType: "type:examples.search/SearchResult",
requiredGrants: ["network"],
run: async ({ request }) => {
return await searchDatasets(request);
},
});
Visible Parameters
For packs, engine.run({ skillPacks: ["skills/dataset-brief"], activeSkillPackRefs: ["skill_pack:project/dataset-brief"], ... }) passes explicit
paths resolved from projectRoot. Without activation, only hints are offered.
skillPackDiscoveryMaxBytes, skillPackActivationMaxBytes, and
agentModelInputMaxBytes set budgets.
See the complete workflow.
| Element | Role |
|---|---|
tool:module.name/ToolName | reference of the tool expected by the handler |
request | business input when the tool declares a single input |
context.arguments | complete structured arguments sent by the runtime |
context.grants | grants provided to the handler |
context.secretHandles | secret handles exposed to the handler |
context.limits | runtime limits for the handler |
| returned value | result that must respect the tool gives type |
When the tool declares a single input, request is directly the value of that
input. For a tool takes note as ResearchNote, the handler therefore reads
request.topic, not request.note.topic. The full argument object remains
available through context.arguments:
export async function run({ request, context }) {
const note = request;
const sameNote = context.arguments.note;
return {
topic: note.topic,
summary: sameNote.summary,
};
}
When the tool declares several inputs, request remains the named argument
object.
The public path is defineTool.
Follow the complete JavaScript handlers tutorial.
Effects and provenance
Use an explicit envelope to return memory provenance separately from business data:
import { createToolResult } from "@touba/sdk";
return createToolResult(record, {
memoryMetadata: [{ memory_ref: "memory:app/Records", operation: "read" }],
});
This excerpt belongs inside the handler body; record is its business value.
Additional provenance fields depend on the declared memory contract. An ordinary
business object containing a memory_metadata property remains business data,
not an envelope. The SDK rejects malformed metadata; the runtime checks the tool,
scope and permissions without granting any additional authority.
createEffectReceipt and createCompensationReceipt produce operation-correlated receipts. Use context helpers after the actual effect; a receipt grants no permissions and does not replace external service idempotency.
Testing
Test handler business behavior
Generated *.handler.contract.test.mjs files test metadata and admission
refusals, not business outputs or effects. Add positive tests of the exported
run function and failure cases using isolated services. For a handler
normalizing a text ticket:
import test from "node:test";
import assert from "node:assert/strict";
import { run } from "./normalize_ticket.handler.mjs";
test("normalizes the real single-input request", async () => {
assert.equal(await run({ request: " SSO unavailable ", context: {} }),
"SSO unavailable");
});
With one takes ticket as Text, request is the text, not { ticket }.
Multiple inputs form a named object. Unhandled exceptions expose a safe class
and reproduction guidance, not the raw message that could contain secrets.
Use ToubaHandlerError for an intentionally safe public diagnostic.
Deterministic tests with the real engine
The @touba/sdk/testing submodule provides four helpers, without a parallel
runtime or automatic permissions:
| Helper | Purpose |
|---|---|
createScriptedDecisionProvider(steps) | Deterministic proposals with explicit mode, agent_ref and/or step_index expectations |
assertRunSucceeded(result, expected) | Check status, completion reason and exact business result |
assertRunRefused(() => engine.run(options), code) | Require an exact runtime refusal; compilation and launch errors do not count |
assertTraceEvents(trace, { kind, count, where }) | Check exact event counts with an optional synchronous Boolean filter |
A scripted provider replaces model proposals, not Touba enforcement. It may propose a forbidden capability to test the actual refusal. It neither selects a default capability nor treats prior use as exhaustion. Adapt these references to your program:
import { createScriptedDecisionProvider } from "@touba/sdk/testing";
export default createScriptedDecisionProvider([
{
expect: { mode: "capabilities", step_index: 0 },
decision: {
action: "select_capability",
target_ref: "tool:archive_testing/NormalizeTicket",
arguments: { ticket: " T-1 " },
source: "scripted-test",
},
},
{
expect: { mode: "summary", step_index: 1 },
decision: { action: "answer_directly", result: "T-1", source: "scripted-test" },
},
]);
Pass the module to engine.run({ ...options, agentDecisionProvider: "tests/decision-provider.mjs", agentDecisionProviderInitialMode: "capabilities" }).
Steps are ordered; an unexpected request or extra call fails. Within the same
process, provider.assertConsumed() also checks unused steps. Engine loads the
provider in its CLI process, so check the outcome, events and effects from
the calling test. provider.calls retains only mode, agent reference and
step index, not prompts, inputs or observations.
import assert from "node:assert/strict";
import { assertRunSucceeded, assertRunRefused, assertTraceEvents } from "@touba/sdk/testing";
const result = await engine.run(options);
assertRunSucceeded(result, "T-1");
assertTraceEvents(result.trace, { kind: "run_completed", count: 1 });
// forbiddenOptions selects a provider proposing a capability outside uses.
const refused = await assertRunRefused(
() => engine.run(forbiddenOptions),
"AgentCapabilityNotAllowed",
);
assert.equal(refused.reason, "AgentCapabilityNotAllowed");
Independently verify the absence of payments, messages or writes in your isolated test service or journal. A trace of an attempt does not prove an external effect. Keep integration tests against your real test services; scripted proposals are not model evaluation. The helpers enable no paid network call, but a real handler retains its authorized effects.
Generated admission tests include an explicit business-coverage todo.
Replace it with positive and negative domain assertions; passing metadata
checks does not make a handler functional. The distributed SDK is tested
outside the checkout with a process handler, refusal before invocation and the
incorrect request.ticket access regression for a single-input tool.
Resumes and parallelism
Successive approvals
After a pause, supply humanResumeCheckpoint (input checkpoint), humanCheckpointOut (new output file) and humanDecisions (decision for this pause) together. A second pause retains the run and advances its cursor without replaying earlier instructions or effects. Reuse the same effectJournal for external writes. Decisions for another approval are refused. Terminal outcomes, including rejection, do not create a new checkpoint. Do not restart the program from the beginning with accumulated decisions to bypass resume.
Optional pauses and Engine triggers
For a request trigger, triggerEvent.arguments contains named arguments matching takes, such as { topic: "Dataset review" }. The SDK does not apply payload.map to a raw HTTP body. The webhook service performs that mapping; the runtime then validates typed arguments.
humanCheckpointOut does not require a pause. A completed path retains its
result and success without writing a checkpoint or deleting an existing file
at that path. Use a separate path per run and trust status and reason,
not file existence alone. Resume with the same operation's checkpoint and journal, without supplying new inputs.
To dispatch an event already received by your application:
const result = await engine.run({
air: "daily_research.air.json",
manifest: "touba.manifest.json",
triggerEvent: {
trigger_ref: "trigger:examples.daily_research/DailyResearchSchedule",
event_id: "watch-event-42",
source_kind: "schedule",
},
triggerState: "watch.triggers.sqlite",
});
Persistent state refuses repeated identifiers and enforces quotas. The Engine
does not start a scheduler or webhook server: use touba trigger run or
touba trigger serve. CLI preflight without execution uses
--trigger-dry-run, without --state.
See governed triggers and the
server API.
Native parallelism with Engine
Source parallel blocks run inside Touba, without a separate JavaScript business
scheduler. engine.run() forwards parallelCheckpointOut,
parallelResumeCheckpoint and parallelLimits to the CLI. Run budgets remain
shared by all branches.
const outcome = await engine.run({
source: "diagnostics.tb",
entryRef: "strategy:parallel.diagnostics/Run",
inputs: { incidents: ["billing", "shipping"] },
parallelCheckpointOut: "private/parallel.json",
parallelLimits: { maxExternalCalls: 4, maxBranches: 64 },
});
Configure the provider and bindings required by the source as for sequential
execution. Resume replaces inputs with parallelResumeCheckpoint and supplies
humanDecisions for actual pending occurrences. An unanswered ask pauses the group with user_input_pending. The host
reads pendingInteractions (JavaScript) or pending_interactions (Python),
saves the parallel checkpoint and supplies occurrence-specific answers through
userInputs / user_inputs. Resumption retains completed branches and
consumed budgets. Each answer belongs to one occurrence.
userInputProvider remains an embedded-runtime callback; the Engine uses
explicit userInputs answers.
See the complete tutorial.
For an executable success/refusal walkthrough, see Test a Touba program.
Monetary budgets
Import createSqliteProviderBudget from @touba/sdk/budget/sqlite.
createProviderCostFetch, from @touba/sdk, reserves an upper bound before each
HTTP attempt. Use a verified host tariff, a stable ID and the same ledger for
descendants and resumptions. See the provider cost guide
and no-paid-call tutorial.
Additional capabilities
Model adapters, conversation history and the MCP bridge have APIs in both SDKs. Persistent Engine transport between runs is specific to JavaScript; Python uses a new process for each run. The Python guide lists the corresponding names and options.
Agentic Decision Provider
Role
A decision provider receives the touba-agent-decision-v1 request produced by the runtime. It does not receive final authority: it proposes a direct answer, requests the catalog, or selects a capability. The Touba runtime then verifies the decision.
The SDK provides three paths:
| API | Role |
|---|---|
createAiSdkDecisionProvider | Generic adapter for an AI SDK-compatible model. |
createOpenAiDecisionProvider | OpenAI helper based on the AI SDK OpenAI provider. |
createOllamaDecisionProvider | Ollama helper for an OpenAI-compatible endpoint. |
Generic Shape
import { createAiSdkDecisionProvider } from "@touba/sdk";
import { model } from "./model-provider.mjs";
export const decideAgentCapability = createAiSdkDecisionProvider({
model,
});
model is supplied by the project with the chosen AI SDK provider. The default
structured interface generates the output schema, calls generateObject,
then returns a normalized decision to the runtime.
Optional Native Interface
A model that supports strict function calls and structured outputs can use the native interface:
import { createAiSdkDecisionProvider } from "@touba/sdk";
import { model } from "./model-provider.mjs";
export const decideAgentCapability = createAiSdkDecisionProvider({
model,
decisionInterface: "native",
instructions: "Use only observed data and capabilities authorized by the Touba runtime.",
});
During the catalog phase, the SDK exposes one native function per authorized
capability. It gives the provider no executor: the model proposes a call, then
Touba revalidates and executes the capability. .tb source, AIR, uses,
callable_by, types, secrets, memories, human validation, budgets, effects,
and resume behavior keep their roles.
Some reasoning models reject explicit temperature or forced function calls.
Set temperature: null to omit temperature and, when needed,
nativeToolChoice: "auto". Defaults remain 0 and "required", respectively.
Even with "auto", Touba requires exactly one valid proposal in the catalog
phase: a response without a call is rejected, not converted into permission or
a final answer. Provider-specific reasoning settings belong in providerOptions;
verify support in the actual connector and model. maxOutputTokens must also
cover reasoning tokens when the provider includes them in that limit.
Orientation and finalization remain structured. A capability may be reused while its contracts and budgets allow it. Follow Use native calls under Touba authority for the complete path.
OpenAI Shape
import { createOpenAiDecisionProvider } from "@touba/sdk";
export const decideAgentCapability = createOpenAiDecisionProvider({
model: "gpt-4.1-nano",
});
The helper reads TOUBA_AGENT_DECISION_OPENAI_API_KEY or OPENAI_API_KEY. This value must remain a local or environment secret, never a committed value.
Reuse, Typed Results, and Completion
All authorized capabilities remain available on subsequent steps, including
previously used ones, subject to runtime quotas. History does not force a final
answer. Each observation associates the validated named arguments with the real
result of that invocation; repeated calls with different parameters remain
distinguishable. opaque_refs hides raw values without weakening secret or
volume checks.
Each target keeps its own argument schema. Direct answers respect the agent's
declared gives, including scalars, lists, and locally described records.
Legacy agents without gives use the helper's text default, not a
runtime-wide restriction. Unresolved or recursive nominal descriptors are
explicitly rejected by the helper.
Both interfaces check completion metadata before accepting a decision. The
HTTP helpers require a Responses body with status: "completed", no
error, and no incomplete_details. Partial, pending, cancelled, or failed
responses produce IncompleteModelDecision, even when their JSON looks
valid. An injected generateObject must provide the same completion metadata
as the real SDK. Returning only { object } is rejected.
Native Interface Authority
At the catalog stage, the SDK exposes one native function per authorized capability. Capabilities may be functions, tools, skills, agents, or strategies; the transport does not turn them all into Touba tools. Exactly one proposal is accepted, without supplying any executor to the model SDK. Unknown, multiple, already provider-executed, ill-typed, or incomplete proposals are rejected.
The catalog also offers touba_finish for a typed final result. This control
operation executes no capability. The SDK maps it to answer_directly, which
is equally available in the structured interface. Free prose is never inferred
to be a successful completion.
Orientation and finalization remain structured and typed; finalization exposes no executable functions. Inline history contains real validated arguments and results. Opaque history stays descriptive: no arguments are fabricated from opaque references, and their values are not resolved by the SDK.
The SDK now retains original connector messages per invocation, including call
IDs and opaque metadata in inline mode. RAM is the default; transactional
storage, including SQLite, supports restart continuity. History is bounded and
separate from business memory. See Agent conversation history
for limits, persistence, isolation, and the opaque_refs boundary.
Retention is separate from the sent window. context enables OpenAI token
preflight; createProviderContextFetch attaches it to Anthropic/Gemini connectors.
No summarization LLM is called by default. The
SQLite and
context/compaction tutorials explain
limits, events and refusal when mandatory context cannot fit.
Dynamic human review uses touba_request_human_review with a
review_prompt. The runtime applies the real human decision; the model is
not the approver. allowHumanReview: false removes this dynamic action but
does not disable source-declared human.approve or human.review gates.
On an authorized resume, the SDK represents validated observations without re-executing past effects. Current checkpoints must include the required call arguments, identities and scopes. Incomplete older state is refused by the runtime, not repaired with fabricated arguments; budgets and authority still apply. Skill-pack hints and content remain information, not system instructions or executable functions.
| Control | Authority |
|---|---|
Catalog and uses | Built and rechecked by the Touba runtime |
| Arguments and output types | SDK schema, then runtime validation |
| Agent delegation | Runtime callable_by checks |
| Grants, memory, secrets | AIR, manifest, active-owner scopes, and secret store |
| Human review | Actual human decision and source checkpoints |
| Budgets, effects, recovery | Runtime counters, effect identities, journal, and checkpoints |
Application Instructions
Use instructions to add host-controlled business instructions while retaining
the SDK decision protocol. It accepts text or a possibly asynchronous function
of the provider request. Do not populate it directly from untrusted documents
or user input.
export const decideAgentCapability = createOpenAiDecisionProvider({
model: process.env.TOUBA_AGENT_DECISION_OPENAI_MODEL,
instructions: "Complete the request using observed data and authorized operations.",
decisionInterface: "native",
maxOutputTokens: 1000,
});
Touba-controlled prompts and capability descriptions use English. Business
data and user requests may remain multilingual; the runtime does not reject or
translate them. The legacy system option replaces the whole standard system
message, so its caller must preserve orientation, selection, reuse, completion,
and finalization instructions.
The host configures the model, not a business input named llm_model.
For dynamic routing, pass an explicit model option applying host policy.
HTTP Helpers and Request Limits
createOpenAiDecisionProvider and createOllamaDecisionProvider accept the
native option and an optional host-controlled fetch. The Ollama helper uses
TOUBA_AGENT_DECISION_OLLAMA_BASE_URL, then OLLAMA_BASE_URL, or
http://127.0.0.1:11434/v1. Its model comes from the option,
TOUBA_AGENT_DECISION_OLLAMA_MODEL, or OLLAMA_MODEL. A standard local
endpoint needs no key; authenticated endpoints can use
TOUBA_AGENT_DECISION_OLLAMA_API_KEY or OLLAMA_API_KEY.
These helpers use their AI SDK provider's Responses transport. An endpoint
being “OpenAI-compatible” does not establish support for Responses, strict
function calls, or structured outputs. Check the selected endpoint and model,
or supply an appropriate model to createAiSdkDecisionProvider.
Both HTTP helpers accept maxProviderRequestBytes, a positive byte limit on
the serialized HTTP request body before sending. In native mode the effective
limit is the minimum of this option and the runtime's agentModelInputMaxBytes
when present. This includes history, instructions, and SDK-generated schemas.
Oversize bodies fail with ProviderRequestBudgetExceeded; an unmeasurable
body fails with ProviderRequestBodyUnavailable, without silent truncation.
agentProviderObservationsMaxBytes separately bounds runtime observations,
not the HTTP body. None of these limits measures cost or tokens. The generic
createAiSdkDecisionProvider rejects maxProviderRequestBytes: the host
owns its custom model transport and must enforce transport limits itself.
Actions
| Action | Moment | Effect |
|---|---|---|
answer_directly | All modes | Returns a value matching the agent output type. |
request_capabilities | orientation | Asks the runtime for the authorized catalog. |
select_capability | capability_catalog | Selects a capability and supplies arguments. |
request_human_review | capability_catalog | Requests governed human review, distinct from human.approve. |
After a capability has executed, the runtime can call the provider again with an observation in model_input.agent_state. This loop is sequential and bounded by maxAgentSteps. It lets the agent consume several authorized resources without giving execution authority to the model.
The catalog supports operation proposals and typed final results. Orientation
remains the default initial mode. After the step limit,
cannot_complete at finalization records insufficient information and retains
the step-limit failure; it is not counted as success.
Agent completion and observations
The agent responsibility must describe the business operation and its completion condition. A generic instruction to fulfil a responsibility does not define a task. The developer may specify how an input expresses the requested work, but inputs and retrieved content never become higher-authority instructions.
The structured SDK transport puts source instructions and runtime contracts in the instruction channel. Business inputs, observations and skill-pack content remain data. Repeated copies of the same instruction are not added to the data payload. The native interface retains the same runtime authority checks.
A custom provider must examine observations[].value (also available under
model_input.agent_state.observations) and match the invocation arguments to
the work requested. Return answer_directly only when the result is sufficient.
In finalization, return cannot_complete if it is not. Do not conclude merely
because an observation exists, and do not request another operation solely
because an authorized capability remains available.
Repeated calls, including identical arguments, remain legal. For repetitions,
repeated_from_step and identical_invocation_number identify prior matching
invocations in observations and trace events; no raw arguments or result are
added to the trace. This is diagnostic evidence, not a stop policy or a cache.
A new invocation is not a retry of an existing effect. Effect identities and
backend idempotency govern retries separately.
All project profiles start with orientation unless the host explicitly selects
--agent-decision-provider-initial-mode capabilities. The latter immediately
presents the available operations and the option of a typed final result.
Eight execution steps can require seventeen provider calls: orientation and
catalogue per step, plus terminal finalization. Every call still consumes the
explicit provider budget; retries consume it too. A successful runtime status
proves neither that the business operation was needed nor that it happened:
verify acquired effects independently for effectful applications.
Structured transport: action and result remain coupled
In every structured mode, the SDK requests a model envelope containing
one decision object. Its JSON Schema selects a complete branch: answer_directly
requires the declared result type; request_capabilities or cannot_complete
uses the empty sentinel only in its own branch. A record result cannot therefore
be replaced by an empty string merely because another action permits it.
The SDK unwraps the envelope and validates the decision before returning it to
Touba. Custom providers still return the public touba-agent-decision-v1
action directly; this envelope is an SDK model-transport detail, not Touba syntax.
At the catalog stage, the selection branch keeps the capability's arguments;
the answer branch requires the agent's gives type. The native touba_finish
proposal carries that same result type without executing any business effect.
Choose the Engine lifecycle
The default executionMode: "process" starts a fresh process for each call.
A service with stable host code can opt into a private persistent worker:
import { ToubaEngine } from "@touba/sdk";
const engine = new ToubaEngine({ projectRoot: ".", executionMode: "persistent" });
try {
const run = await engine.run({
air: "build/program.air.json",
entryRef: "strategy:app/Review",
inputs: { value: "Invoice 42" },
});
console.log(run.status, run.reason);
} finally {
await engine.close();
}
Both modes use the same CLI admission. Each run has fresh runtime values, budgets, secrets and permissions; AIR and authority files are reread. The persistent worker has four active calls and sixteen queue slots. It has no network endpoint. Excess work is refused.
Trusted JavaScript host modules, including providers and identity adapters, stay loaded and may retain connections and globals. Close and recreate the Engine after changing those modules or their dependencies. Use the default mode when host-process isolation per call is required. Tool handlers retain their own processes and restrictions.
A source file is compiled on every call; supplying compiled AIR avoids that
cost. A local Mac arm64 / Node 24.15.0 probe measured about 348 ms versus 2 ms
median for a pure 64-byte input operation, over 20 warm calls. The first
persistent call took about 196 ms; worker RSS after the probe was about
103 MiB. These are local overhead measurements, not model or business-task
latencies. engine.workerMetrics reports worker RSS and cumulative CPU,
excluding child handlers.
Pass an AbortSignal as run({ ..., signal: controller.signal }).
Cooperative cancellation targets one request. If host code stays blocked,
the persistent worker is killed after five seconds, interrupting its other
calls too. Process mode stops its dedicated process. Crashes and cancellation
never retry an operation automatically: an external effect may already have
happened. Check the effect journal before resuming, and call close() when
the application shuts down.
For Python applications, see the Python SDK.
Providers and history
createAiSdkDecisionProvider, createOpenAiDecisionProvider and
createOllamaDecisionProvider have Python equivalents with the same contracts
and decisionInterface: "structured" or "native".
createMemoryDecisionHistoryStore comes from @touba/sdk;
createSqliteDecisionHistoryStore from @touba/sdk/history/sqlite.
RAM is the default; SQLite preserves the same invocation identities and recovery
requirements in both SDKs. Structural compaction is local. Only an optional
host summarize callback produces a text summary.
See history and context.
Attach spending controls to the final transport through
cost: { budget, ...adapter } in createOpenAiDecisionProvider.
Context size and spending are separate controls; combining them requires
cost.counting with explicit quote and readUsage callbacks for count requests.
See tariff adapters.
MCP
createMcpStdioToolBridge({ mcpToolName, config }) returns a
bridge({ request, context }) handler. The touba-mcp-bridge-config-v1
configuration is shared with Python. After Core tool admission, the bridge
starts the stdio server, enforces time/size limits, passes only bound secrets
and closes the server process. The process grant remains necessary where
required by OS confinement.
Authority boundary
The SDK is not the primary authority.
The runtime verifies before dispatch:
- known tool;
- granted capabilities;
- available secrets;
- agent and memory scopes;
- payload limits;
- payload constraints when declared;
- input shape;
- agentic selection against
uses; - provider-supplied arguments against the AIR signature;
- resource bindings associated with resources used by the called tool.
After handler return, the runtime also verifies output shape.
For the CLI engine, the SDK calls the Touba CLI. It does not automatically read manifests, secrets, handlers, or .env files. It also does not discover resource bindings: the application must explicitly pass resourceBindings when a run depends on a logical resource.
Best Practices
For handlers:
- Keep the handler short.
- Do not read the entire host environment.
- Never write a secret to standard output.
- Return a value conforming to the tool
gives. - Let the runtime refuse invalid contract cases.
For the engine:
- Reuse one instance per project or session.
- Pass
manifest,secrets, andinputsexplicitly. - Pass
resourceBindingsandrequireResourceBindingsexplicitly when a tool depends on amemory, decision provider, or event source wired by the host. - Pass
agentDecisionProviderwhen an agent must choose a capability during execution. - Set
maxAgentStepsto explicitly bound multi-step agentic scenarios. - Keep
agentDecisionsfor reproducible tests and deterministic scenarios. - Use
createAiSdkDecisionProviderto avoid hand-rolling a provider-specific adapter. - Keep the model key out of inputs and traces.
- Do not pass secret values in
inputs. - Read traces returned by
run.tracerather than parsing logs. - Keep live tests with network or API keys opt-in.
Switch between SDKs
Class names and concepts are shared. Functions and options use each language's usual casing. AIR, manifest and trace protocol keys remain unchanged.
| JavaScript | Python |
|---|---|
ToubaEngine, ToubaEngineError, ToubaHandlerError | ToubaEngine, ToubaEngineError, ToubaHandlerError |
projectRoot, entryRef, pendingInteraction | project_root, entry_ref, pending_interaction |
pendingInteractions | pending_interactions |
compile({ source }) → airJson, airDocument | compile(source) → air_json, air_document |
run({ airJson }) | run(air_json=...) |
defineTool, createToolHandler, handleToolRequest | define_tool, create_tool_handler, handle_tool_request |
createToolResult, createEffectReceipt, createCompensationReceipt | create_tool_result, create_effect_receipt, create_compensation_receipt |
toubaSdkMetadata | touba_sdk_metadata |
@touba/sdk/testing | touba_sdk.testing |
createScriptedDecisionProvider, assertRunSucceeded, assertRunRefused, assertTraceEvents | create_scripted_decision_provider, assert_run_succeeded, assert_run_refused, assert_trace_events |
JavaScript uses await; Python provides synchronous ToubaEngine and AsyncToubaEngine with await. Business functions receive run({ request, context }) in JavaScript and run(request, context) in Python. These are calling conventions, not different responsibilities.
Follow the same steps in the JavaScript SDK, Python SDK, JavaScript Engine / Python Engine and JavaScript handlers / Python handlers tutorials.
Typed outputs and provider diagnostics
Date, format, length, min and max follow the same path in both
SDKs: provider schema, response validation, then runtime validation.
Invalid data is refused before use.
| Setting | JavaScript | Python |
|---|---|---|
| Schema encoding | schemaEncoding | schema_encoding |
| Automatic selection (default) | "auto" | "auto" |
| Touba representation | "canonical" | "canonical" |
| Flattened optionals | "flat_optionals" | "flat_optionals" |
Automatic mode adapts Optional fields for Anthropic models: it reduces unions
in the transmitted schema, validates the response, then restores Touba's
{present, value} encoding. Both validations stay active in structured and
native interfaces. Malformed values are never converted to absence.
Schema limits depend on the chosen model; qualify business types with that model.
In a host module, export initialModes (JS) or initial_modes (Python) to
select summary or capabilities per agent reference:
export const initialModes = {
"agent:app.invoices/Reader": "capabilities",
};
initial_modes = {"agent:app.invoices/Reader": "capabilities"}
Without an explicit setting, an agent with no capabilities starts directly with
its result contract. When routing by agent, the SDK reports the resolved model;
the agent_decision_provider_model_resolved event identifies the provider and
model selected for the call.
Token-count failures retain the HTTP status and a bounded diagnostic.
Interrupted generation exposes finish_reason and, when supplied,
incomplete_reason. Partial results are never executed.
A write handler's public error code is preserved; missing or invalid receipts are reported separately. See External effects.
Route several providers
A host router forwards both arguments to the selected adapter:
import { providers } from "./models.mjs";
export function decideAgentCapability(request, execution) {
const provider = providers.get(request.agent_ref);
if (!provider) throw new Error("No provider configured for this agent");
return provider(request, execution);
}
providers is a host-configured Map. execution carries cancellation and cost
reporting. Forward it unchanged, separately from the prompt. Retain provider
instances between decisions to preserve their history. The Python guide covers
the same routing with context propagated through its host channel.