Skip to main content
Version: 1.0.0

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.

SettingJavaScriptPython
AttestationhumanApprovalHosthuman_approval_host
Agent decisionsagentDecisionProvideragent_decision_provider
Attestation timeout, mshumanApprovalAuthorityTimeoutMshuman_approval_authority_timeout_ms
Pure-function detailtracePureFunctionstrace_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 inputsJavaScript requestPython 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​

program.tb
module sdk.engine
strategy Review
takes value as Text
gives Text
do
give value
run.mjs
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:

Structure
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​

app.mjs
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​

ElementRole
projectRoothost project root used to resolve relative paths
source.tb file compiled by the CLI before execution
air or airJsonexplicit alternative to source if AIR already exists
entryRefAIR entry to execute, for example a skill or strategy
manifesttool binding manifest
secretssecret-handle file
resourceBindingstouba-resource-bindings-v1 file passed to touba run --resource-bindings
requireResourceBindingsrequests refusal if a required logical resource has no active binding
inputsinput object passed to the runtime
agentDecisionsexplicit agent decisions, bounded by uses
agentDecisionProviderpath to a local provider called by the CLI to choose an agent capability
agentDecisionProviderInitialModesummary or capabilities, explicitly relayed to the CLI
agentDecisionProviderContentModeinline or opaque_refs, without implicit data conversion
maxAgentStepslocal step limit for a provider-driven agentic loop
humanCheckpointOutprivate checkpoint path, written only when the run awaits approval
humanResumeCheckpointinput checkpoint path; combine with humanCheckpointOut to save a possible subsequent pause
parallelCheckpointOut, parallelResumeCheckpointprivate paths for saving and resuming parallel group progress
parallelLimitshost ceilings for groups and the run; see the CLI reference
userInputsexplicit ask answers keyed by interaction reference; use occurrence references in parallel branches
humanDecisionsmap from approval references to runtime decisions
effectJournaldurable effect journal path; keep the same journal on resume
triggerEventnormalized event: trigger_ref, event_id, source_kind and optional named arguments; not an ordinary entrypoint
triggerStatepath ending in .triggers.sqlite, for persistent deduplication and quotas
supplyChainLock, requireSupplyChainLockexplicit lock and mandatory pre-execution validation
runLimits, maxProviderCallsexplicit 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​

handler.mjs
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​

search_datasets.handler.mjs
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.

ElementRole
tool:module.name/ToolNamereference of the tool expected by the handler
requestbusiness input when the tool declares a single input
context.argumentscomplete structured arguments sent by the runtime
context.grantsgrants provided to the handler
context.secretHandlessecret handles exposed to the handler
context.limitsruntime limits for the handler
returned valueresult 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:

handler.mjs
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:

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

tests/decision-provider.mjs
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:

APIRole
createAiSdkDecisionProviderGeneric adapter for an AI SDK-compatible model.
createOpenAiDecisionProviderOpenAI helper based on the AI SDK OpenAI provider.
createOllamaDecisionProviderOllama helper for an OpenAI-compatible endpoint.

Generic Shape​

decision-provider.mjs
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:

decision-provider.mjs
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​

decision-provider.mjs
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.

ControlAuthority
Catalog and usesBuilt and rechecked by the Touba runtime
Arguments and output typesSDK schema, then runtime validation
Agent delegationRuntime callable_by checks
Grants, memory, secretsAIR, manifest, active-owner scopes, and secret store
Human reviewActual human decision and source checkpoints
Budgets, effects, recoveryRuntime 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​

ActionMomentEffect
answer_directlyAll modesReturns a value matching the agent output type.
request_capabilitiesorientationAsks the runtime for the authorized catalog.
select_capabilitycapability_catalogSelects a capability and supplies arguments.
request_human_reviewcapability_catalogRequests 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, and inputs explicitly.
  • Pass resourceBindings and requireResourceBindings explicitly when a tool depends on a memory, decision provider, or event source wired by the host.
  • Pass agentDecisionProvider when an agent must choose a capability during execution.
  • Set maxAgentSteps to explicitly bound multi-step agentic scenarios.
  • Keep agentDecisions for reproducible tests and deterministic scenarios.
  • Use createAiSdkDecisionProvider to 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.trace rather 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.

JavaScriptPython
ToubaEngine, ToubaEngineError, ToubaHandlerErrorToubaEngine, ToubaEngineError, ToubaHandlerError
projectRoot, entryRef, pendingInteractionproject_root, entry_ref, pending_interaction
pendingInteractionspending_interactions
compile({ source }) → airJson, airDocumentcompile(source) → air_json, air_document
run({ airJson })run(air_json=...)
defineTool, createToolHandler, handleToolRequestdefine_tool, create_tool_handler, handle_tool_request
createToolResult, createEffectReceipt, createCompensationReceiptcreate_tool_result, create_effect_receipt, create_compensation_receipt
toubaSdkMetadatatouba_sdk_metadata
@touba/sdk/testingtouba_sdk.testing
createScriptedDecisionProvider, assertRunSucceeded, assertRunRefused, assertTraceEventscreate_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.

SettingJavaScriptPython
Schema encodingschemaEncodingschema_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.