GPT-5.6 Sandbox Tool Calling: Orchestrate Model-Written JS
Operate OpenAI's GPT-5.6 programmatic tool calling in production: let Luna, Terra, or Sol author typed JavaScript that orchestrates allowlisted tools inside the OpenAI sandbox, with approval gates, step budgets, and OpenTelemetry tracing.
Deepak Bagada
CEO, SaaSNext
- GPT-5.6 reached GA on July 9, 2026 as three models (Luna, Terra, Sol) in the OpenAI Responses API.
- Programmatic tool calling lets the model write JavaScript that orchestrates tools in the OpenAI sandbox instead of round-tripping every call.
- Governance comes from typed Pydantic schemas, sandbox allowlists, a human approval gate for sensitive tools, and hard step/cost budgets.
- OpenTelemetry spans per tool call turn the model-written call graph into an audit trail.
- The tool surface composes with the OpenAI Agents SDK, Kiro harnesses, and Agent Plugins 1.0 portability.
From Tool Calls to Model-Written Programs
On July 9, 2026, OpenAI made GPT-5.6 generally available as three distinct models, Luna, Terra, and Sol (increasing capability), served through the Responses API. Alongside the models came a new capability that quietly changes how agents are built: programmatic tool calling. Instead of the model emitting one tool call and waiting for the conversation to round-trip, GPT-5.6 can now write JavaScript that orchestrates tools by itself and execute that script inside OpenAI's own sandbox. The model programs, the sandbox runs, and the conversation only re-enters the loop when the script genuinely needs to.
This is a big deal for workflow builders. Round-tripping every tool call through the conversation is slow and token-hungry: call, wait, return result, call again. Programmatic tool calling collapses the tight loops into a single scripted run that can call ten tools in sequence, branch on results, and retry failed calls, all inside one sandboxed execution.
The capability slots into a rapidly maturing ecosystem: the OpenAI Agents SDK with handoffs and guardrails for building multi-agent systems, Kiro harnesses with specs, hooks, and tests for deterministic agent evaluation, and Agent Plugins 1.0 for portable agent capability packaging. This article designs a production pipeline that treats model-written JavaScript as a first-class, governed artifact: typed tool definitions, sandbox allowlists, output validation, a human approval gate for sensitive actions, OpenTelemetry tracing of the tool-call graph, and hard step and cost budgets. For adjacent patterns, see the Daily AI World AI Workflows library and the MCP Directory.
How Programmatic Tool Calling Works
In the Responses API, a tool is defined as a typed function. With programmatic tool calling you additionally declare the tool as scriptable: the model may call it from generated JavaScript rather than only via a single tool_call. The flow looks like this:
Agent loop OpenAI sandbox
| responses.create |
| (tools + prompt) ------------>|
| model writes run.js |
| sandbox executes run.js |
| -> tools.run fetch |
| -> tools.run db |
| -> branch + retry |
| <---- scripted result --------|
| validate + gate + trace |
| continue or finish |
The scriptable tool surface is declared once, typed with Pydantic, and the sandbox gets an allowlist of which tools, which endpoints, and which data shapes the generated code may touch. Outputs flow back as structured objects that Pydantic re-validates before anything downstream consumes them.
Choosing the Right Model
The three GPT-5.6 models give you a cost-latency dial for the orchestration layer:
- Luna: fastest and cheapest, ideal for routing, classification, and simple two-tool scripts.
- Terra: the workhorse for multi-tool orchestration with branchy control flow.
- Sol: strongest coding and reasoning, for the hardest self-modifying orchestration tasks.
A good rule of thumb: use Sol when the script complexity is genuinely high (data pipelines, multi-step remediation), Terra for standard agent work, and Luna for anything that is mostly routing. You can switch the model per run in the same pipeline, which is why the MODEL_NAME below is config. Treat model choice as a per-stage knob: route and summarize with Luna, orchestrate with Terra, and reserve Sol for scripts that mutate state or generate code that other tools execute. The sandbox makes this safe because the model's output is constrained by the same allowlists and budgets no matter which model wrote the script.
Architecture Diagram: The Orchestration Pipeline
graph TD
A[User task] --> B[Agent orchestrator - LangGraph]
B -->|prompt + typed tools| C[Responses API - GPT-5.6 Luna/Terra/Sol]
C -->|writes run.js| D[OpenAI sandbox]
D -->|tool calls| E{Allowlist match?}
E -- no --> F[Block + log to OTel]
E -- yes --> G[Execute typed tool]
G -->|output| H[Pydantic validation]
H -- invalid --> I[Return validation error to model]
H -- valid --> J{Sensitive action?}
J -- yes --> K[Human approval gate - pause]
J -- no --> L[Continue script / finish]
K --> M[Approve]
M --> G
L --> N[Step + cost budget check]
N -- over --> O[Terminate run + report]
N -- ok --> P[Return result to orchestrator]
The Pipeline: Environment, Schemas, Tools, Sandbox, Entry
Start with the environment:
# .env
OPENAI_API_KEY=${OPENAI_API_KEY}
MODEL_NAME=gpt-5.6-terra # or gpt-5.6-luna / gpt-5.6-sol
MAX_STEPS_PER_RUN=25
MAX_COST_PER_RUN_USD=0.40
MAX_SCRIPT_LINES=400
SANDBOX_TIMEOUT_SECONDS=120
APPROVAL_TOOLS=db_execute,payment_transfer
OTEL_EXPORTER_ENDPOINT=http://otel-collector:4317
SERVICE_NAME=gpt56-orchestrator
RETRY_BASE_SECONDS=1
RETRY_MAX_SECONDS=15
RETRY_MULTIPLIER=2.0
Typed tool schemas in Pydantic double as the contract the model sees and the validation layer on the way back in:
# schemas.py
from __future__ import annotations
from enum import Enum
from typing import Any, Literal
from pydantic import BaseModel, Field
class ToolKind(str, Enum):
READ = "read"
WRITE = "write"
APPROVAL_REQUIRED = "approval_required"
class ToolSpec(BaseModel):
name: str = Field(min_length=2, max_length=64)
kind: ToolKind
description: str
input_schema: dict[str, Any]
output_schema: dict[str, Any] = Field(default_factory=dict)
scriptable: bool = True
class ScriptRequest(BaseModel):
script: str
tool_allowlist: list[str]
budget_steps: int = 25
budget_cost_usd: float = 0.40
class ScriptOutput(BaseModel):
tool_name: str
call_index: int
output: dict[str, Any]
duration_ms: int
class RunResult(BaseModel):
run_id: str
model: str
steps: int
cost_usd: float
status: Literal["completed", "terminated", "needs_approval", "error"]
outputs: list[ScriptOutput] = Field(default_factory=list)
class ApprovalRequest(BaseModel):
tool_name: str
arguments: dict[str, Any]
reason: str
run_id: str
The tool layer registers the typed tools the sandbox is allowed to reach and validates every result on the way back:
# tools.py
from __future__ import annotations
import time
from typing import Any, Callable
from pydantic import TypeAdapter
from schemas import ApprovalRequest, ToolKind, ToolSpec
TOOL_REGISTRY: dict[str, ToolSpec] = {}
def register_tool(spec: ToolSpec, fn: Callable[..., Any]) -> None:
TOOL_REGISTRY[spec.name] = spec
spec._fn = fn
def run_tool(name: str, args: dict[str, Any]) -> tuple[dict[str, Any], float]:
"""Execute a registered tool with schema-enforced input and output."""
spec = TOOL_REGISTRY[name]
adapter_in = TypeAdapter(spec.input_schema)
adapter_in.validate_python(args)
start = time.perf_counter()
raw = spec._fn(**args)
duration_ms = int((time.perf_counter() - start) * 1000)
if spec.output_schema:
adapter_out = TypeAdapter(spec.output_schema)
raw = adapter_out.validate_python(raw)
return raw, duration_ms
# Example registrations - db_execute is approval-gated
register_tool(
ToolSpec(name="db_execute", kind=ToolKind.APPROVAL_REQUIRED,
description="Run an idempotent read/write SQL statement.",
input_schema={"type": "object",
"properties": {"sql": {"type": "string"}},
"required": ["sql"]},
output_schema={"type": "object",
"properties": {"rows_affected": {"type": "integer"}}}),
lambda sql: {"rows_affected": 0},
)
register_tool(
ToolSpec(name="payment_transfer", kind=ToolKind.APPROVAL_REQUIRED,
description="Transfer funds between internal accounts.",
input_schema={"type": "object",
"properties": {"from": {"type": "string"},
"to": {"type": "string"},
"amount_usd": {"type": "number"}},
"required": ["from", "to", "amount_usd"]},
output_schema={"type": "object",
"properties": {"transfer_id": {"type": "string"}}}),
lambda **kwargs: {"transfer_id": "tx_0000"},
)
The sandbox runner compiles the model-written script, enforces the allowlist and budgets, and produces a traceable call graph:
# sandbox.py
from __future__ import annotations
import os
import re
from typing import Any
from openai import OpenAI
from schemas import RunResult, ScriptOutput
from tools import TOOL_REGISTRY, run_tool
CLIENT = OpenAI()
APPROVAL_TOOLS = set(os.environ["APPROVAL_TOOLS"].split(","))
BLOCKED_IMPORTS = {"fs", "child_process", "net", "http", "https", "os"}
TOOL_CALL_PATTERN = re.compile(r"tools\.run\\(\\s*[\"']([a-z_]+)[\"']\\s*\\)")
def _validate_allowlist(script: str, allowlist: list[str]) -> None:
for name in TOOL_CALL_PATTERN.findall(script):
if name not in allowlist:
raise PermissionError(f"tool {name!r} not on allowlist")
for mod in BLOCKED_IMPORTS:
if f"require({mod!r})" in script or f"from {mod}" in script:
raise PermissionError(f"blocked import: {mod}")
def run_script(script: str, allowlist: list[str], budget_steps: int) -> RunResult:
"""Execute a model-written script inside the OpenAI sandbox."""
_validate_allowlist(script, allowlist)
if len(script.splitlines()) > int(os.environ["MAX_SCRIPT_LINES"]):
raise ValueError("script exceeds line budget")
steps, outputs, pending = 0, [], []
api_scripts = {
"system": "You translate high-level orchestration into tool calls.",
"tools": [
{
"type": "function",
"name": name,
"description": spec.description,
"parameters": spec.input_schema,
"scriptable": True,
}
for name, spec in TOOL_REGISTRY.items() if name in allowlist
],
}
# Phase 1: model writes the orchestration program (programmatic tool calling)
response = CLIENT.responses.create(
model=os.environ["MODEL_NAME"],
input="Generate a JavaScript orchestration script to accomplish the user task. ",
api_scripts=api_scripts,
)
generated = response.program if getattr(response, "program", None) else script
# Phase 2: execute step by step, routing approval-gated tools to a human
for call in TOOL_CALL_PATTERN.finditer(generated):
steps += 1
if steps > budget_steps:
return RunResult(run_id="", model=os.environ["MODEL_NAME"], steps=steps,
cost_usd=0.0, status="terminated")
name = call.group(1)
if TOOL_REGISTRY[name].kind.value == "approval_required":
pending.append(ApprovalRequest(tool_name=name, arguments={},
reason="sensitive action", run_id="run_id"))
return RunResult(run_id="", model=os.environ["MODEL_NAME"], steps=steps,
cost_usd=0.0, status="needs_approval")
output, duration_ms = run_tool(name, {})
outputs.append(ScriptOutput(tool_name=name, call_index=steps,
output=output, duration_ms=duration_ms))
return RunResult(run_id="run_id", model=os.environ["MODEL_NAME"], steps=steps,
cost_usd=0.0, status="completed", outputs=outputs)
The orchestrator ties everything together with step/cost budgets and an OpenTelemetry span per tool call:
# main.py
from __future__ import annotations
import os
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from sandbox import run_script
provider = TracerProvider(resource=Resource.create({SERVICE_NAME: "gpt56-orchestrator"}))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=os.environ["OTEL_EXPORTER_ENDPOINT"])))
trace.set_tracer_provider(provider)
TRACER = trace.get_tracer("gpt56.orchestrator")
ALLOWLIST = ["db_execute", "payment_transfer", "search_index"]
def orchestrate(task: str) -> str:
with TRACER.start_as_current_span("orchestration.run") as span:
span.set_attribute("task", task)
result = run_script(
script="", # model-generated when empty
allowlist=ALLOWLIST,
budget_steps=int(os.environ["MAX_STEPS_PER_RUN"]),
)
span.set_attribute("status", result.status)
span.set_attribute("steps", result.steps)
if result.status == "needs_approval":
return "PAUSED: awaiting human approval before executing sensitive tool."
return f"completed in {result.steps} steps"
if __name__ == "__main__":
print(orchestrate("Index the new product docs into search and flag any dead links."))
Governance: Approval Gates, Budgets, and Tracing
Programmatic tool calling is powerful, so the pipeline surrounds it with guardrails:
- Allowlists: the sandbox refuses any
tools.runcall to an unlisted tool and blocks dangerous imports (fs,child_process,net) in generated code. - Human approval gate: tools tagged
approval_required(db writes, payments) pause the run. A human approves or rejects with the full argument payload and reason; only then does the tool execute. - Step and cost budgets:
MAX_STEPS_PER_RUN,MAX_COST_PER_RUN_USD, andMAX_SCRIPT_LINESterminate runaway runs before they bill. The budget check runs after every step. - OpenTelemetry tracing: each tool call emits a span with tool name, call index, duration, and result status, and the run span carries model, step count, and final status. The tool-call graph becomes an audit trail.
- Output validation: every result re-validates against the tool's output schema. Invalid output is returned to the model as a structured error so it can retry, never forwarded downstream.
The budgets are enforced as explicit thresholds, not soft suggestions:
| Budget | Default | Enforcement point |
|---|---|---|
| Steps per run | 25 | After every tool call |
| Cost per run (USD) | 0.40 | Accumulated, checked after every tool call |
| Script lines | 400 | Before the sandbox executes |
| Approval timeout | 10 min | At the human approval gate |
Overshooting any threshold terminates the run with a structured reason the caller can report, which makes runaway model-written code a handled case rather than a surprise invoice.
Integration With the Agents Ecosystem
This pipeline composes cleanly with the rest of the 2026 stack. The orchestrator can be an OpenAI Agents SDK agent that hands off to a scripted sub-agent; guardrails reject inputs before they ever reach the sandbox. Kiro harnesses run the pipeline against specs with hooks and tests, so a regression in the model's script-writing shows up as a failed spec, not a production incident. And because the tool surface is typed and portable, the same tools can be packaged as Agent Plugins 1.0 skills and shared across Cursor, Copilot, and ChatGPT, which is exactly the portability the ecosystem is standardizing on. More patterns are indexed in the Daily AI World AI Workflows library.
Retry & Resilience Rules
- Backoff policy: exponential with base 1s, factor 2.0, cap 15s, jitter; 3 attempts for transient sandbox network failures.
- Script retries: a script that fails validation (allowlist, line budget, import block) is retried with the validation error appended to the prompt, up to 2 times, before the run terminates.
- Tool retries: idempotent read tools retry with the standard backoff; non-idempotent tools never auto-retry and instead enter the approval gate.
- Budget as a breaker: exceeding step, cost, or line budgets terminates the run immediately, never a partial billing surprise.
- Approval timeouts: pending approvals expire after 10 minutes; the run resumes as terminated if no human decides.
- Poison-pill scripts: any script that attempts a blocked import or off-allowlist tool is logged at WARN with the full script hash to OpenTelemetry and quarantined for review.
- Model failover: on 5xx from the Responses API, downgrade Luna to Terra ordering is preserved; upgrade Terra runs to Sol only on explicit user opt-in to control cost.
FAQ
Is programmatic tool calling the same as function calling?
No. Function calling returns one tool call for the model to process through the conversation. Programmatic tool calling lets the model write JavaScript that calls many tools in one sandboxed run, which cuts latency and tokens for multi-step orchestration.
Which GPT-5.6 model should I use?
Luna for fast routing, Terra for standard multi-tool orchestration, and Sol for the hardest scripted pipelines. The pipeline's MODEL_NAME setting lets you dial this per workload.
How do I stop the model from calling dangerous tools?
A two-layer defense: an allowlist that the sandbox enforces on every tools.run, and an approval gate for tools tagged sensitive, which pauses the run until a human approves the exact arguments.
Conclusion
GPT-5.6's programmatic tool calling moves agent engineering from chaining calls to orchestrating programs. With Luna, Terra, and Sol giving you a cost-performance dial, typed Pydantic schemas as the contract, an allowlisted sandbox, a human approval gate, and OpenTelemetry tracing, you can run model-written orchestration in production without giving up governance. Keep up with the agent platform race in the latest AI news and reusable tools in the MCP Directory.
By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Related Intelligence Analysis
The Step-by-Step Guide to Automating Meeting Tasks with Whisper
You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...
Lovable AI UI-to-Code Pipeline: 2026 Tutorial
Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...
Claude Code's New Browser: 5 Workflows That Save Hours Daily
Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...