Skip to main content

5. Async tool calling: writing one async def does not mean you have it

5.1 Four kinds of “async” that are easy to confuse

The current API expresses the last case via async: true on function or custom tools; the tool is still executed on the application side, and the result is returned with the original call_id. It differs from Background mode, and the current docs do not allow using such async tools in PTC; multi-agent mode has additional composition restrictions. Async tool calling

5.2 A concrete example

Task: “Query the CI result and check whether the README documents the install steps.”
While CI is pending, the model can point out the README’s gaps, but cannot say “CI has passed.” The correct dependency graph is:

5.3 Why might it be faster?

Assume the model’s independent work takes M, tools take T, and coordination overhead is O:
This is only a latency model. If the next step must know the tool result, you still wait; if all tools contend for the same database lock, so-called parallelism will not speed things up noticeably. Measure critical path, p50/p95, and failure rate, not just “how many asyncs were called.”

5.4 What must the harness fill in?

A minimal pending registry needs to record:
State transitions can be designed like this:
You also need to handle: tool failures returning structured errors; duplicate submissions not re-executing; the same ID with different arguments being rejected; concurrency limits; whether the timeout includes queue time or only execution; late results not being misused for a new requirement; and cancellation propagating to the real executor.

5.5 Why launch tools from the stream as early as possible?

If the application waits for responses.create() to receive the full response before launching tools, the model may already have generated an independent answer, and I/O and generation will not have overlapped much in practice. A streaming implementation should launch a task once the complete tool-call item has been generated, not from an incomplete JSON argument fragment. In the appendix, live_api.py async registers a worker on response.output_item.done and posts the result back after the stream ends. It is a real API wiring example, but this run did not call a paid model.
Keep the original call_id when returning the result, and attach it to the latest response chain so you do not drop conversation added while the tool was running.

5.6 The relationship between wait and yield

You can have a tool first return an awaitable task identifier, let the model continue working, and wait only when it truly depends on the result. This mechanism can be implemented at the application layer and does not necessarily require the API’s async flag. But job_handle and the API call_id are not the same thing: the former may be a name in your business registry; the latter is the identifier the protocol uses to match results. Do not treat a model-defined handle as a low-level ID the model already knows. The Codex Code Mode source shows the paths for launching a cell, initial output, keeping the cell alive after yield, and subsequent wait/terminate handling. This proves that non-blocking execution needs full lifecycle management, not just a tool-declaration change. execute handler, wait handler

5.7 What does the local demo verify?

runtime_demo.py launches two simulated tools at the same time, records independent work before they finish, and handles one user change. The tests do not use a flaky timing threshold to judge parallelism; instead they verify that the start event of the second tool appears before the completion event of the first. It does not call an LLM, so what it proves is Python’s scheduling and state-management mechanism, not Astra inference capability. The output of about 0.123s in this run is just simulated wait time, and cannot be written up as “Codex performance improved by xx%.“

6. Mid-turn steering: how to stay consistent when requirements change mid-run

6.1 Why is a basic harness bad at this?

A basic loop typically only reads the next user input “after a turn ends.” The user wants to say “do not commit, only analyze,” but the model still follows the original plan. Simply cancel-and-restart also has costs: losing intermediate state, repeating tools, and being unable to precisely judge whether an external action has already happened. The goal of steering is to preserve completed work and attach the new requirement to the running task.

6.2 Two layers of steering — do not mix them

The App Server’s protocol tests include behavioral checks such as turn ID; its client interface and the Responses API’s model-service interface do not share a schema. You cannot casually send a turn/steer field to a regular HTTP /responses. App Server steering tests App Server example:
expectedTurnId acts like a misdelivery check: the turn the client believes is still running may already be finished, and the server must not silently apply the update to the next turn.

6.3 Where the Responses API takes effect

The current official guide says Astra supports WebSocket steering. After the request is accepted, it does not modify output already emitted, nor does it undo actions already executed. It stitches into a successor response at an appropriate boundary; the successor’s response.created is the commit point for the update. When it depends on application tool results or approvals, it goes pending. Steering guide
When there are client-owned tools or an approval is pending:
Do not resend steering content that has already been accepted, and do not re-run side-effectful tools just to fill required_input. A disconnect also does not mean the request was not accepted. Precise restrictions also include: only supported user inputs are allowed; the current reference does not support binding to a conversation or combining with automatic compaction. Responses WebSocket event definitions

6.4 “Change while generating” is not the same as rewriting already-sampled tokens

The interview answer should be: the server incorporates the new input into subsequent continuation, taking effect at the safety boundary the protocol promises. You cannot infer from interaction behavior that it modifies the KV cache, current neural-network activations, or already-generated tokens in place.

6.5 What about writes already sent?

Suppose the old plan was “modify config and publish” and the new requirement is “read-only analysis.” A reasonable design is:
  1. On receiving a new requirement, increment the business revision.
  2. Undispatched change plans are invalidated and reassessed.
  3. Read-only tools already running may keep results, but check the scope of applicability.
  4. Uncommitted writes must recheck revision and permissions before execution.
  5. Completed external writes can only be handled by compensation or rollback; they cannot be claimed as “undone by steering.”
The appendix Runtime’s commit() checks the revision; tests prove that an old plan with revision 0 cannot be committed after revision 1. It only records a teaching proposal and does not operate any real external system. Real database writes should place version check and write inside the same transaction/CAS boundary; remote calls should use idempotency keys and reconciliation. “Check then call HTTP” inside the application still has a race window.

6.6 Interview answer

The hard part of mid-turn steering is not just receiving a new message, but defining the input’s acceptance point, effect point, and the boundary of already-completed side effects. I use active-turn checks, requirement revisions, and pre-execution authorization checks, keep completed tool results, and reconcile after a disconnect rather than blindly retrying.