[FEATURE] Speculative tool-call sequences with expected results (branch prediction for tool use)

Status Open
Reported on v2.1.280
Maintainer reply None cached
Activity 1 comment · opened Sep 23, 2026

Related: #33761 (programmatic tool calling; closed as stale and locked, so we can't comment on it), #22264 (parallel tool calls fail together when one errors).

Background

In Claude Code every tool call is a full model round trip. The model emits tool_use and stops, the harness runs the tool, the next request re-sends the whole context (mostly as a cache read), and the model decodes again. "Check the file exists, read line N, run the test, commit" costs at least 4 round trips, even though the model usually knows the whole plan and the likely result of each step up front.

Measured:

  • Context re-read per call: our general-purpose subagents start at a median of 44,422 tokens (n=795 transcripts), and even a Bash-only agent at 7,538 (n=48). Every extra call re-reads at least that, plus everything accumulated since.
  • Per-call wall time and token cost: ? (measurement in progress; we'll add it as a comment).

Today's alternatives, and why none of them is this:

  1. Several tool_use blocks in one message. Parallel only: no conditions and no ordering, and one failure takes down the batch (#22264).
  2. A single Bash a && b && c. Shell only, with ad-hoc grep -q glue. It can't sequence Edit, Read or MCP calls, and permissions see one opaque command.
  3. The Workflow tool. Agent orchestration: heavier, and not built for one agent's next few calls.
  4. API programmatic tool calling. It runs in Anthropic's remote container, isn't exposed in Claude Code (#33761), and the code has to handle every branch itself.

Goals

  • Predictable N-step sequences cost 1 round trip on the expected path, not N.
  • Each call still goes through permissions, hooks and the sandbox, and each result lands in the transcript as a normal tool_result.
  • A misprediction is cheap and safe: the transcript at the stop point is exactly what the model would have had issuing the calls one by one.

Non-goals

  • A scripting language or loops. On a mismatch, the model branches, not the harness.
  • Transactional rollback of side effects. A write that already succeeded stays done.
  • Replacing parallel tool calls or Workflow.

Solution

Add an optional expect field on each tool_use, and an ordered, speculative sequence in one message:

tool_use Bash {command: "test -f pkg/x.go && echo ok"}        expect: {stdout_matches: "^ok$", exit: 0}
tool_use Read {file_path: ".../x.go", offset: 40, limit: 20}  expect: {contains: "func Foo("}
tool_use Edit {...}                                            expect: {success: true}
tool_use Bash {command: "go test ./pkg/..."}                  expect: {exit: 0}

The harness:

  1. Runs the calls in order, each through permissions, hooks and the sandbox as today.
  2. Checks each result against its expect. Start with a small predicate set: exit code, regex or contains on the output, success, a number range.
  3. If all expectations hold, appends every result and makes one model request.
  4. On the first mismatch, appends results up to and including that call, marks the rest not executed (speculation aborted at step k), drops the model's speculative tokens after step k, and makes one model request.
  5. Writes are fired optimistically and interrupt only if they fail. Anything irreversible stays gated by the permission prompt, which also stops the chain.

Why this: every call stays visible and checked, which is what Bash chaining and programmatic tool calling give up. It needs no remote container. Without expect it behaves exactly as today. A wrong prediction costs what the call would have cost anyway, one round trip, plus the wasted speculative output.

Open questions: the predicate vocabulary, billing for discarded speculative tokens, and whether this belongs in the API (tool_use.expect) or only the harness.

Claude Code 2.1.280, Linux.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗