Python-first Agent: Code Is Transient, Workflows Should Be Replayable
Agent can already call tools well. The usual problem is not whether it can call, but how many rounds of calls a task requires.
Suppose you need to read a paginated API until there is no next page, then fill in missing information from a second source, filter out failed items, join on several fields, and finally keep only five results. If every step goes back to the model, the execution looks like this:
LLM decides next call
→ tool call
→ full result enters context
→ LLM reads it
→ LLM decides next call
→ ...
Parallel tool calls can compress a batch of known independent requests, but they do not solve data-dependent control flow. Whether there is a next page, whether failed items should be retried, and which records should be joined often depends on previous results.
I have recently been experimenting with a different boundary: let the model generate a temporary Python snippet, and keep loops, pagination, filtering, joins, and bounded retries within one run; external access still has to go through Host-provided capability.
This code is not meant to enter a long-lived codebase. It is more like an executable plan for this task, disposable after use.
Python-first does not mean Python gets all authority
The easiest mistake is to give the Agent a container, shell, and secrets, then say “code is more flexible.” That just replaces tool calling with a much larger attack surface.
The structure I want is:
Agent harness
│ code + inputs + output schema
▼
CPython/WASI guest
│ typed Host call
▼
Capability broker
│ aliases + credentials + budgets + policy
▼
External providers
A request generated by the model has only four fields: an untrusted label, Python code, JSON inputs, and an optional output schema. It cannot specify credentials, timeout, memory limit, target origin, or grant in the request.
These authority-bearing settings are provided separately by the Host. The guest only sees opaque aliases like catalog and weather, not URLs with tokens.
A simplified workflow can be written as:
from agent_runtime import tools
items = tools.fetch_many([
{
"request_id": "p1",
"target": "catalog",
"path": "/items?page=1",
},
{
"request_id": "p2",
"target": "catalog",
"path": "/items?page=2",
},
])
rows = [
x["body"] for x in items
if x["status"] == "ok"
]
result = normalize_and_rank(rows)[:5]
The guest has no raw socket and does not get the Authorization header. The Host maps aliases to origins, injects credentials, enforces path, timeout, call count, response bytes, and concurrency, and writes a receipt for each operation.
Python is control flow, not the authority boundary.
Why not continue using direct tool calls
Simple actions should still call tools directly. Checking weather once, reading a file, and turning on a light are not worth generating Python first.
Python is better for these kinds of tasks:
- Call count is determined by intermediate data, for example, paginating until completion.
- Intermediate results are large, but only a small aggregation is needed in the end.
- Multiple sources need local filtering, joining, deduplicating, or sorting.
- Failure handling is deterministic, such as retrying timeout entries once and then returning a partial result.
The rough cost of direct tool loop is:
k ×(LLM turn + tool serialization + growing context)
A Python workflow is closer to:
1 × code generation
+ 1 × bounded runtime
+ m × provider operation
It does not remove provider calls. What it reduces is the LLM round trip and the amount of intermediate data entering model context.
This is still an economic hypothesis, not a proven token-saving percentage. To validate it, the same task needs to be compared by completion rate, number of model invocations, input/output tokens, provider calls, wall time, and repair cost after failure. Measuring runtime latency alone cannot answer whether it is cheaper.
There is also a reverse cost: generated code can contain bugs. If a task needs only two fixed calls, having the model write code, execute it, read a traceback, and fix it once may be more expensive than direct tool call. Python-first should be an execution mode chosen by the harness, not a mandatory ABI for every action.
Why fetch_many is managed by the Host for concurrency
The guest can generate a batch of read requests, but it should not determine concurrency limits.
The current prototype divides requests into bounded waves in input order. Each wave runs up to the Host grant limit; worker threads fetch concurrently, then after the wave finishes, a single-threaded reducer processes results by original operation index:
input-order wave
→ concurrent provider calls
→ join
→ ordered byte-budget admission
→ ordered receipts
This gives up a bit of fastest-completion head-of-line returns, but gains two useful properties:
- completion order no longer changes which responses are accepted by the aggregate byte budget.
- receipt order is not affected by network jitter.
In a fixed 2 ms/provider synthetic fixture, 20 in-order requests take about 50.2 ms; Host concurrency 8 takes about 8.0 ms, about 6.3×. This is not a production network benchmark, but it is enough to show that concurrency should be managed in bulk within one call by the Host instead of letting the model spin up unbounded threads.
Python-first does not suspend MCP
Python is control flow on the Agent side; MCP, REST, and local adapters can still be Host-side integration backends.
A simple call can still go through model → MCP tool. When pagination, joins, or bounded retries are needed, the path becomes Python guest → capability broker → MCP client adapter. The guest does not hold MCP server credentials or inherit a full MCP session directly.
The key point is that both entry points are generated from the same canonical capability spec: same target alias, argument schema, budget, policy, and receipt. Otherwise, direct tool and Python binding have separate authorization logic, and behavior plus auditing quickly diverge.
I also will not open arbitrary MCP sampling in the temporary runtime. That would let a seemingly deterministic Python make further model calls and hide a new token budget plus recursive Agent loop. When semantic judgment is needed, the flow should explicitly return to the harness.
Playback is not “just running the outside world again”
If the code is temporary workflow, it is actually easier to record. A run can bind:
exact generated code + digest
input payload + digest
runtime / artifact identity
capability request sequence
bounded recorded observations
Host receipts
final output digest
The deterministic playback I want is rerunning the same Python while the Host capability no longer calls real providers and instead replays observations from the journal as recorded then. That makes it possible to reproduce why the Agent got a particular result and run regression tests after changing runtime, parser, or output schema.
Digest alone is not enough. A digest can confirm identity but does not reconstruct response bodies. To make playback meaningful, the journal must store policy-trimmed observations or a content-addressed object reference; both need separate handling for retention, sensitive fields, and access control. Current receipts only provide bounded evidence and are not a full replay log.
Determinism here has limits. It does not promise byte-for-byte identical CPython execution across machines, and it does not pretend today’s API state is fresh data.
- pure computation can be recomputed directly.
- read effects can return historical observations, but recorded time must be included.
- time, random, provider responses, and capability errors must be fixed or recorded by the Host.
- if the code depends on unsorted collections, platform floating-point differences, or undeclared external state, playback can still differ.
Read-only runtime already has Host-authored receipts, but durable journal and playback mode remain design work; having a digest alone is not enough to claim “already replayable.”
Write operations cannot pretend playback means rollback
Re-reading old weather data may be stale; sending duplicate email, charging again, or posting again has real-world effects.
So if write capability is added in the future, a Python call should not directly mean “operation succeeded.” A better return shape is an immutable intent:
Python proposes action
→ Host canonicalizes arguments
→ journal stores intent + digest
→ policy chooses DENY / COMMIT / APPROVE
→ Effect Kernel calls provider
→ Host stores receipt or reconciliation state
COMMIT cannot happen in the same Python run as the proposal; otherwise generated code could immediately approve itself. The first round should only generate a staged intent. A later Agent turn or trusted human then decides on the same digest.
For playback, applied effects should return only the original receipt and must not call providers again. If a provider timeout happens in a state where the provider may have accepted but the Host has not received a response, the state should be reconciliation_required, not blind retry.
This entire layer is not a V1 feature yet. The current implementation is only read-only fetch_many. I include it in the design because once deterministic replay involves side effects, the problem shifts from “sandboxing” to request identity, idempotency, and durable state machine design.
Fresh does not mean always starting cold
Once Python is in WASI, there is a practical issue: CPython initialization is not cheap. The previous post explained how the evaluator runtime handles a post-import snapshot; the runtime state used by the Agent runtime is different and cannot be copied directly.
The most conservative implementation is to create a fresh instance for each run and discard it afterward. During structural audit of the current experimental artifact, I found that copying linear memory alone is not enough for safe reuse: the module also has unexported mutable globals, and wazero has no public API to fully clone/restore module state.
So I did not hard-build snapshots. The default configuration is still fresh instantiate per run, then discard afterward; a pool capacity greater than zero only enables an optional optimization with clearer boundaries: a single-use preinitialised instance.
background:
instantiate → _initialize → runtime_init → ready queue
request:
checkout never-served instance
→ attach fresh Host broker
→ trusted request prepare
→ execute once
→ close forever
It is not a persistent notebook. An interpreter used once is never handed to the next Agent.
In the same Linux synthetic fixture:
- fresh execute median: about 4.607 s
- prepared steady execute median: about 1.854 ms
- factory-to-ready: about 7.492 s
- refill
runtime_init: about 4.753 s - each queued instance reserves at least 128 MiB guest linear memory
The 2484× ratio is striking, but cannot stand alone. Initialization just moves to readiness and background refill; a capacity of 4 alone reserves 512 MiB guest memory, not yet counting compiled code, Go heap, and WASI state. Pool misses still fall back to the fresh path.
This optional optimization trades request latency, not free compute.
I split this into three layers in the end
Python-first for me is not “the Agent can write code, so it should always write code.” What I want is clear division:
LLM
handles semantic judgment and workflow generation
handles exceptions that truly need reasoning
Python
handles loops and data transforms for the current task
executes deterministic control flow
Host
handles authority, credentials, and budgets
manages identity, receipts, and effects
Simple actions continue with direct tool calls. Decisions requiring human judgment still stop for input. Temporary code only takes over parts that do not need repeated model reasoning but would otherwise repeatedly cost token and latency.
If this economic model holds, it will not be because Python is “smarter” than the LLM. On the contrary, many workflows simply do not require the model to think at every step. Keeping the model only where judgment is needed and letting ordinary disposable, constrained, and replayable code handle the rest may be the cheaper and more explainable Agent runtime.