Skip to content

Programmatic Tool Calling in Yolo: Running LLM JavaScript in a QuickJS WASM Sandbox

Harry

TL;DR: When an AI agent must query live data and then perform conditional or repeated actions, repeatedly returning intermediate results to the model can add latency and consume context. In Yolo, my personal AI-native productivity app, I implemented a local form of Programmatic Tool Calling: the model generates a small JavaScript program, and Yolo executes it inside a QuickJS WebAssembly sandbox. The program can orchestrate only explicitly exposed application tools, while the host application continues to enforce schema validation, permissions, confirmation, execution limits, and undo policies.

1. The Real Bottleneck: Model-Mediated Orchestration

A common AI-agent architecture uses an iterative model–tool loop:

User Request
    β”‚
    β–Ό
   LLM ──► Tool Call ──► Host Application
    β–²                         β”‚
    └────── Tool Result β—„β”€β”€β”€β”€β”€β”˜

This is often described as a ReAct-style loop, although ReAct has a more specific meaning: it interleaves model-generated reasoning traces, actions, and observations from an external environment. See ReAct: Synergizing Reasoning and Acting in Language Models.

The pattern works well for simple actions:

β€œCreate a task called β€˜Submit the report’ for Friday.”

The model can produce one tool call, the application executes it, and the model returns the result.

The situation becomes more interesting when later actions depend on live data:

β€œFind all overdue tasks in my β€˜Work’ category and reschedule them to next Monday.”

A direct tool-calling workflow may look like this:

  1. The model calls list_tasks.
  2. Yolo returns the matching tasks.
  3. The model inspects those results and creates the required update_task calls.
  4. Yolo executes the updates.
  5. The model summarizes the outcome.

Modern tool-calling APIs can return multiple independent tool calls in one model response, so updating 15 tasks does not necessarily require 15 model turns. Independent calls may be issued together and executed in parallel.

The core bottleneck is sequential dependency depth.

Because the model cannot construct data-dependent calls until it receives intermediate query results, each dependent step forces another model turn:

Thus, model turns scale with the number of sequential decision pointsβ€”not the volume of data records.

Where the overhead comes from

A Quick Real-World Sidebar: I ran into this exact issue when setting up periodic log analysis with autonomous agents like Hermes Agent. Whenever the agent pulled raw, uncleaned logs directly into its context, it burned through massive token budgets just sifting through noise. The fix was simple: having a local script scrub and filter the logs first, then passing only the condensed summary back to the model. Programmatic tool calling takes that exact pattern and bakes it into a first-class, automated runtime capability.

2. Programmatic Tool Calling

Instead of asking the model to select every operation across multiple inference phases, Yolo exposes one meta-tool:

execute_program

The model writes a short JavaScript program that can:

The generated program becomes an executable control plan.

This approach draws on the concept of Programmatic Tool Calling as documented by Anthropic, in which a model writes code that invokes tools inside a code-execution environment while intermediate results remain outside the model’s context.

Cloudflare describes a closely related architecture as Code Mode: a model receives a code-execution tool and writes a program that composes typed tools, processes their results, and returns only the information needed for the response.

The broader idea also appears in research. CodeAct treats executable code as an agent action space, while PAL delegates deterministic computation to an interpreter instead of requiring the language model to perform every reasoning step itself.

Yolo adapts this paradigm into a local, model-agnostic runtime using JavaScript and QuickJS, allowing different configured LLMs to use the same orchestration runtime, while security remains enforced by the host application.

3. A Concrete Example

Suppose the user asks:

β€œFind all overdue Work tasks and move them to next Monday.”

Yolo asks the model to generate a program body. The execution engine wraps that body inside an asynchronous function, so both await and return are valid statements within the generated code.

A simplified program body generated by the model looks like this:

const targetDate = "2026-07-27";

const tasks = await list_tasks({
  scope: "overdue",
  category: "Work",
});

if (!tasks.ok) {
  return {
    ok: false,
    error: tasks.error,
  };
}

let applied = 0;
let queued = 0;
const failures = [];

for (const task of tasks.data) {
  const update = await update_task({
    task_id: task.id,
    due_date: targetDate,
  });

  if (!update.ok) {
    failures.push({
      taskId: task.id,
      error: update.error,
    });
  } else if (update.queued) {
    queued += 1;
  } else {
    applied += 1;
  }
}

log(
  `Matched ${tasks.data.length} tasks: ` +
    `${applied} applied, ${queued} queued, ` +
    `${failures.length} failed.`,
);

return {
  ok: failures.length === 0,
  matched: tasks.data.length,
  applied,
  queued,
  failed: failures.length,
  failures,
  targetDate,
};

The host execution engine evaluates this generated body by wrapping it inside an immediately invoked asynchronous function:

(async () => {
  // generated program body
})();

From the model’s perspective, the tool interface is ordinary asynchronous JavaScript:

const tasks = await list_tasks({ scope: "overdue" });

The typical execution path contains two model inference phases:

  1. Generate the program.
  2. Summarize the program result.

The important distinction is not that 15 database writes become one database write. The example still executes update_task once per task.

Instead, Yolo moves the loop and intermediate decisions out of repeated model inference and into an explicitly structured program executed locally.

A purpose-built bulk operation such as bulk_update_tasks could reduce domain-tool calls further. Programmatic tool calling is most useful when the workflow requires flexible filtering, branching, composition, or error handling that cannot be expressed cleanly through one fixed bulk API.

4. Architecture

Yolo is a desktop application built with Tauri v2, React, and TypeScript.

Below the model, the programmatic runtime has four main layers:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         AI Model                          β”‚
β”‚             Generates a JavaScript program               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Program Validation Layer                 β”‚
β”‚      Size limits, syntax checks, allowed entry point      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                QuickJS WebAssembly Sandbox                β”‚
β”‚                                                           β”‚
β”‚  Loops Β· Conditions Β· Temporary State Β· JSON Processing   β”‚
β”‚                                                           β”‚
β”‚  Exposed capabilities:                                    β”‚
β”‚  list_tasks Β· update_task Β· log                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚ Explicit host-function bridge
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 Yolo Standard Tool Registry               β”‚
β”‚                                                           β”‚
β”‚  Schema validation Β· Permissions Β· Confirmation Β· Undo    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 SQLite / Tauri / App State                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

A. Capability containment

Executing model-generated JavaScript through eval in Yolo’s normal frontend environment would give that code access to anything already available in the same JavaScript realm.

Depending on the application, that might include:

Tauri still applies its own capability and runtime-authority checks to IPC commands. A webview does not automatically gain unrestricted native access merely because JavaScript is running inside it.

However, executing generated code in the main application realm would unnecessarily expose a much larger attack surface.

Yolo therefore executes the program inside a separate QuickJS instance compiled to WebAssembly.

WebAssembly modules have no ambient access to their host environment. The embedder determines which capabilities become available by controlling the imported functions and objects.

The QuickJS program does not automatically receive access to:

It receives only the host functions that Yolo explicitly installs.

This is capability containment: the program retains normal language features such as loops, variables, arrays, and conditions, but its external authority is restricted to a small allowlist.

B. Bridging asynchronous host tools

The generated program uses standard async/await, but the underlying bridge requires explicit handling.

A plain quickjs-emscripten host callback cannot simply return a native JavaScript Promise or a normal JavaScript object and expect QuickJS to adopt it automatically.

One supported approach is:

  1. Create a promise inside the QuickJS context.
  2. Start the asynchronous host operation.
  3. Convert the result into a QuickJS value.
  4. Resolve or reject the QuickJS promise.
  5. Execute pending QuickJS jobs so the guest program can resume.

The library also provides Asyncify-based builds for cases where the entire WebAssembly execution must suspend around asynchronous host work. Asyncified builds have additional size, performance, reentrancy, and suspension constraints, so the choice should be made deliberately.

The following is a simplified version of the deferred-promise bridge:

type JsonValue =
  | null
  | boolean
  | number
  | string
  | JsonValue[]
  | { [key: string]: JsonValue };

type ToolExecutor = (
  args: Record<string, JsonValue>,
  options: { signal: AbortSignal },
) => Promise<JsonValue>;

function installJsonTool(
  name: string,
  execute: ToolExecutor,
  signal: AbortSignal,
): void {
  const hostFunctionName = `__host_${name}`;

  const hostFunction = vm.newFunction(
    hostFunctionName,
    (argsHandle) => {
      const args = vm.dump(argsHandle) as Record<string, JsonValue>;
      const deferred = vm.newPromise();

      void execute(args, { signal }).then(
        (result) => {
          try {
            const jsonString = JSON.stringify(result ?? null);
            const resultHandle = vm.newString(jsonString);
            deferred.resolve(resultHandle);
            resultHandle.dispose();
          } catch (err: unknown) {
            const message =
              err instanceof Error ? err.message : String(err);
            const errorHandle = vm.newString(message);
            deferred.reject(errorHandle);
            errorHandle.dispose();
          }
        },
        (error: unknown) => {
          const message =
            error instanceof Error
              ? error.message
              : String(error);

          const errorHandle = vm.newString(message);

          deferred.reject(errorHandle);
          errorHandle.dispose();
        },
      );

      deferred.settled.then(() => {
        vm.runtime.executePendingJobs();
      });

      return deferred.handle;
    },
  );

  vm.setProp(
    vm.global,
    hostFunctionName,
    hostFunction,
  );

  hostFunction.dispose();

  const toolNameLiteral = JSON.stringify(name);
  const hostNameLiteral = JSON.stringify(hostFunctionName);

  const wrapperResult = vm.evalCode(`
    globalThis[${toolNameLiteral}] = async (args) => {
      const json = await globalThis[${hostNameLiteral}](args);
      return JSON.parse(json);
    };
  `);

  vm.unwrapResult(wrapperResult).dispose();
}

From the model’s perspective, this bridge mechanism is entirely invisible. The LLM simply writes idiomatic asynchronous JavaScript using schema-described tool interfaces (such as await list_tasks(...)), while the host runtime handles handle lifecycle management, JSON serialization, and job queue awakening.

5. Preserving Application Safety Policies

The programmatic runtime is an alternative orchestration mechanism. It is not a privileged back door.

Every exposed function routes through Yolo’s existing tool registry.

Schema validation

The registry validates tool arguments before executing the underlying operation.

The sandbox cannot bypass a tool’s input schema merely because the call originated from generated code.

Permission modes

Yolo supports different execution modes:

The same mode applies whether the tool was called directly by the model or through execute_program.

Human confirmation

In Plan or Ask mode, a mutating tool can return:

{
  "ok": true,
  "queued": true
}

Yolo then renders a preview card instead of silently changing application data.

Destructive operations still require confirmation.

Undo and version checks

Successful mutations can return an UndoOp containing enough information to reverse the operation.

Yolo also records the post-write updated_at value. Before applying an undo operation, it can verify that the record has not subsequently changed.

This prevents an old undo action from overwriting a newer user edit.

Implemented safeguards

To prevent infinite loops or excessive resource consumption, Yolo enforces strict runtime safeguards:

6. Security Model

A WebAssembly sandbox is useful, but it is only one layer of the security design.

The real security boundary consists of:

  1. The QuickJS/Wasm isolation layer.
  2. The set of injected host functions.
  3. Tool argument validation.
  4. Application permission checks.
  5. Confirmation policies.
  6. Resource quotas.
  7. Tauri capabilities.
  8. Audit and undo behavior.

The most important rule is:

Generated code must never receive more authority than the user and the application policy intended to grant.

The host-function bridge is therefore part of the attack surface.

For example, a safe update_task function should not accept an arbitrary SQL fragment. It should accept a narrow, validated object:

{
  task_id: string;
  due_date: string;
}

The sandbox limits where the code can execute. The tool registry limits what the code can do.

Both are necessary.

7. Partial Failures and Idempotency

A program may execute several mutations before one of them fails.

For example:

Task 1 updated
Task 2 updated
Task 3 updated
Task 4 failed
Task 5 not yet attempted

The runtime must not report this as a simple success or failure. It should return a structured execution summary:

{
  "matched": 5,
  "applied": 3,
  "queued": 0,
  "failed": 1,
  "notAttempted": 1
}

Future hardening work

For reliable bulk workflows, Yolo also needs to consider:

A sandbox can contain the program, but it cannot automatically make the business operation transactional.

8. Direct Tool Calling vs. Programmatic Tool Calling

Let:

DimensionDirect Tool CallingProgrammatic Tool Calling
Model inference phasesUsually grows with dependency depth (D), not necessarily item count (N)Commonly one phase to generate the program and one to summarize it
Domain-tool executionsO(N), unless a bulk tool is availableO(N), unless a bulk tool is available
Parallel operationsSupported when calls are independentPossible when the host bridge and business rules permit it
Intermediate resultsFrequently enter the model contextCan remain inside the execution environment
Control flowDistributed across model responsesExpressed explicitly through JavaScript
Model latencyAdded at every dependent decision phaseReduced when local code handles intermediate decisions
Tool latencyStill presentStill present
Execution overheadTool serialization and model orchestrationTool serialization, sandbox startup, and VM execution
Failure handlingModel or host coordinates each phaseProgram can aggregate failures, but generated logic may itself be wrong
Security surfaceTool schemas and host policiesTool schemas and host policies, plus a sandbox and code bridge
Best use caseSmall, fixed, easily reviewed actionsData-dependent loops, filtering, branching, and multi-tool composition

Programmatic tool calling is therefore not automatically faster in every situation.

Its primary benefit appears when:

9. When Not to Use It

Direct tool calling is usually better when:

For example, the best implementation of:

β€œMark every selected task as complete.”

may simply be:

complete_tasks({
  task_ids: selectedTaskIds,
});

There is no reason to generate a program when a narrow, well-tested domain operation already expresses the user’s intent.

Programmatic tool calling should complement good tool design, not replace it.

Conclusion

Programmatic tool calling does not turn N database operations into one operation.

It moves data-dependent orchestration out of repeated model inference and into an explicitly structured program executed within a constrained environment.

In Yolo, QuickJS provides the JavaScript runtime, WebAssembly helps establish an isolated execution boundary, and the existing tool registry preserves permissions, validation, confirmation, and undo behavior.

The result is a hybrid architecture:

For complex agent workflows, that separation is more important than simply reducing the number of tool calls.

References

  1. Yao et al., β€œReAct: Synergizing Reasoning and Acting in Language Models”
  2. Anthropic, β€œProgrammatic Tool Calling”
  3. Anthropic, β€œIntroducing Advanced Tool Use on the Claude Developer Platform”
  4. Anthropic, β€œCode Execution with MCP: Building More Efficient AI Agents”
  5. Cloudflare, β€œCode Mode”
  6. Cloudflare, β€œCreate a Durable Code Mode Runtime”
  7. Wang et al., β€œExecutable Code Actions Elicit Better LLM Agents”
  8. Gao et al., β€œPAL: Program-Aided Language Models”
  9. quickjs-emscripten Documentation
  10. WebAssembly Core Specification
  11. Tauri v2 Capability Reference
  12. Tauri v2 Runtime Authority
Previous
How I Reclaimed 23 GB of Storage from Rust & Tauri Projects in 5 Minutes
Next
Yolo Devlog 02: From Recording a Day to Seeing It Clearly