Here is the decision, up front: if a task in your agent chains together a bunch of related tool calls — fetch these ten records, filter them, summarize the survivors — GPT-5.6's new Programmatic Tool Calling will almost certainly make it faster and cheaper than the classic tool loop. If the task is one or two calls, or a human needs to approve each action, the classic loop is still the right tool. The rest of this piece is why, with the code.
The classic loop, and where it hurts#
The tool loop you already know works one call at a time. The model emits a tool call; your code runs it; you hand the result back; the model reads it and decides what to do next. It looks like this:
let input = [{ role: "user", content: "Which of my 10 open tickets are stale?" }];
while (true) {
const res = await client.responses.create({ model: "gpt-5.6-sol", tools, input });
const calls = res.output.filter(o => o.type === "function_call");
if (!calls.length) break; // model is done
for (const call of calls) {
const result = await runTool(call.name, call.arguments);
input.push({ type: "function_call_output", call_id: call.call_id, output: result });
}
input = input.concat(res.output); // every output re-enters the context
}
Two costs are hiding in that last line. First, every round is a model round-trip — ten chained calls is (roughly) ten times through the model, and latency stacks. Second, and less obvious: every tool output lands back in the context window. If runTool returns a 40 KB JSON blob and the model only needed one field from it, you paid input tokens for all 40 KB — and you pay again on the next turn, because it's still in the transcript.
The classic loop's real tax isn't the number of calls. It's that every intermediate result has to travel through the model's context, whether the model needed it or not.
What programmatic tool calling changes#
Programmatic Tool Calling flips who writes the orchestration. Instead of the model emitting one call and waiting, it writes a small JavaScript program that runs in a hosted, isolated V8 sandbox — and that program calls your tools directly, in parallel, with loops and conditionals, keeping the intermediate results in the runtime. Only the final answer comes back to the model's context.
You opt a tool in with the allowed_callers field:
const tools = [{
type: "function",
name: "get_ticket",
description: "Fetch a ticket by id",
parameters: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
allowed_callers: ["programmatic"], // let the model orchestrate this from generated JS
}];
const res = await client.responses.create({ model: "gpt-5.6-sol", tools, input });
When the model chooses the programmatic path, the response output array carries three things worth knowing about: a program item (the JavaScript the model generated, plus a call_id and an opaque fingerprint), the function_call items the program actually made, and a program_output item with the final result and a status. The model's generated program is doing, inside the sandbox, what your while loop did in your process — except the ten ticket fetches happen in parallel and the ten 40 KB blobs never enter the context. Only "3 tickets are stale: #12, #40, #57" does.
That is the whole value in one sentence: the orchestration and the bulky intermediate data move out of the context window and into a sandbox. On GPT-5.6 Sol at $5 per million input tokens, the blobs you stop re-sending are the line item you stop paying.
The sandbox is deliberately small#
The runtime is a fresh, isolated V8 with JavaScript and top-level await — and almost nothing else. No Node.js, no npm install, no direct network access, no general filesystem, no subprocess, no console, and no state that persists between program runs. That sounds restrictive until you see the point: your tools are the program's only door to the outside world. Everything the program can touch, it touches through a function call you defined and can audit. That's what keeps the feature Zero-Data-Retention compatible with no extra container cost — the sandbox never phones home on its own.
It also draws a clean security line. The model writes code, but it can't fetch() an arbitrary URL or read a file — it can only call get_ticket, close_ticket, and whatever else you allowed. This is the same tradeoff we walked through for MCP's code-execution-vs-direct-tool-calls debate: letting the model write orchestration code is enormously more efficient than a chatty call-by-call loop, provided the code runs somewhere it can't hurt you. OpenAI's answer and MCP's answer land in the same place — a locked sandbox whose only capabilities are the tools you hand it.
So which one#
Pick by the shape of the task, not the vibe:
- Reach for programmatic tool calling when a task fans out to many related calls, when tool outputs are large and the model needs only a slice, or when the control flow (filter, retry, aggregate) is real logic rather than one step. This is where the token and latency savings are largest.
- Stay with the classic loop when the task is one or two calls (there is nothing to orchestrate), when a human must approve each action — the program runs autonomously, so per-action gating gets awkward — or when you need to stream each call and show progress as it happens.
The mistake to avoid is reaching for the fancier path by default. A one-call tool wrapped in a generated program is strictly more moving parts for zero benefit. Programmatic tool calling earns its keep exactly when the classic loop was going to make you pay, over and over, for data the model was only going to glance at. If your bill is dominated by prompt size rather than tool chatter, the other lever is GPT-5.6's explicit prompt-cache breakpoints — different problem, same instinct: stop paying twice for tokens the model has already seen.



