From Prompt to Action: How OpenCode Runs a Coding Task
This chapter follows one OpenCode request from a TUI or API client to a provider turn, a possible local tool call, and the next continuation. The goal is to make one distinction precise: a provider turn is one request-and-response exchange with a model, while a coding task is the larger controlled loop around it. Once that distinction is clear, the roles of the project Location, Session, agent, model, permissions, and tools become observable rather than abstract.
A request is shaped before the model sees it
Imagine asking OpenCode to find every place where an application creates a session, explain the authentication path, and update the error handling. The model cannot safely act on that sentence in isolation. It needs to know which project is active, what files and instructions belong to that project, what role it is operating in, which model is available, and which tools it may use. OpenCode assembles those conditions before it asks the provider for a response.
The first boundary is the client. The terminal UI is the familiar entry point, but the source also defines an HTTP API for sessions, messages, models, providers, agents, permissions, files, and events. The TUI can run through an embedded worker and an in-memory request bridge, or it can use an external server transport. That choice changes how requests travel, not what a session means. Both paths lead into the same server and core contracts.
The request is then associated with a project-scoped Location. In practical terms, a Location identifies the active directory and its project or worktree context, with an optional workspace identity. It is more informative than a shell’s current directory because it is also the scope in which OpenCode resolves configuration, filesystem services, tools, permissions, provider catalog state, skills, references, and the session runner. A request arriving for another directory can therefore receive a different runtime environment even when the same OpenCode process serves it.
The Session supplies continuity. It identifies the project and Location, records the selected agent and model, and gives the request a place in the task’s history. In the current V2 API, prompt admission and execution are separate operations: input can first become durable pending work, then be promoted into model-visible history when the runner reaches a safe boundary. That separation matters when a user sends a steering message while an earlier turn is still running. The new input is not simply appended to a prompt string at an arbitrary moment.
That admission boundary also keeps two kinds of uncertainty separate. OpenCode can know that a user has submitted a request even when the provider is busy, a permission dialog is open, or the runtime cannot yet assemble a complete context. The pending input remains a durable fact that can be promoted later. By contrast, model-visible history should represent what the runner actually admitted into the conversation. This is a small but consequential distinction: it prevents a queued request from looking like work the model has already seen, and it gives steering and queued delivery different meanings instead of making both just another text append.
The agent is the role that interprets the request and the policy that accompanies it. OpenCode’s visible build and plan modes make the idea concrete. A full-access development role and a read-only analysis role may inspect the same repository, but they should not expose the same action surface. Internally, the agent record also carries model selection, system instructions, visibility, mode, step limits, and permissions. The agent is therefore part of request assembly and authorization, not just a label shown in the interface.
Model selection is another independent decision. OpenCode’s catalog separates a provider from a model and records information such as capabilities, input and output modalities, context limits, cost metadata, variants, and availability. Credentials or integrations can make a provider usable; configuration and policy can still narrow what may be selected. The selected model is eventually turned into an executable provider route by an adapter. This lets the session runner ask for a normalized model operation without embedding every provider’s endpoint and request format in its control flow.
Selection is not one lookup performed at startup. A model can be present in catalog data but unavailable because its provider lacks credentials or an active integration. A provider can be configured correctly but denied by policy. A model can be available while a particular route or transport remains unsupported. OpenCode’s design keeps those cases distinct so that “not found,” “not usable,” and “not supported by this execution path” do not collapse into a misleading fallback. That distinction is valuable in a coding task: changing the model can be a deliberate choice, while silently changing the protocol could change how tools, context, or provider-native metadata behave.
Before the provider call, OpenCode combines the selected agent, current context, chronological messages, and the tools that are available for that turn. In the V2 runner, this includes the Location’s system context and selected guidance, the session history after its active context boundary, a resolved model, and materialized tool definitions. In the legacy path, the broad session prompt service performs the analogous work and also adapts MCP tools, instructions, skills, and provider-specific request behavior. The two paths differ in ownership and maturity, but they embody the same causal rule: the model sees a prepared request, not the entire host runtime.
A provider turn begins only after those decisions have been made. It is useful to keep the phrase narrow. One provider turn includes one call to the model provider and the normalized response produced from that call. It may contain text, reasoning, usage, an error, or a request to call one or more tools. It does not, by itself, include the local tool execution and the follow-up call needed to continue the task. OpenCode keeps that larger continuation loop outside the provider abstraction because the runtime must persist and authorize what happens between calls.
Provider-neutral does not mean provider-blind. The LLM package normalizes requests and streamed events, while adapters retain the endpoint, authentication, framing, and wire-format details needed by each provider. The runner can therefore react to a common shape such as a text delta, tool call, provider error, or finish event. When a provider has a special capability or an unsupported transport, the adapter or model resolver can expose that fact at the boundary where it belongs. The session loop does not need to pretend that every provider behaves identically in order to keep its own state transitions consistent.
The sequence makes the most important ownership boundary visible. The provider decides what response to generate next. The runner decides whether that response is actionable, whether a local call is allowed to execute, how its result is represented, and whether another provider turn is required.
A tool call is a controlled handoff, not a jump into the host
Suppose the model decides it needs to search the repository before proposing a change. It emits a tool call with a name and structured input. OpenCode first resolves the effective registration for that name. In the V2 registry, application-level tools and Location-level registrations are combined, with the latest active Location registration taking precedence. The registry also captures the identity of the registration advertised for the provider turn. If an extension replaces or removes that registration before settlement, the old call becomes stale rather than being redirected silently to a different handler.
Next comes the schema boundary. The Tool abstraction owns an input codec and an output codec in addition to its description and executor. The registry decodes the provider’s input before invoking the tool. Invalid input never reaches the executor. After execution, the returned value is encoded and checked against the output contract before it can be treated as a successful tool result. These steps are easy to overlook because a model-facing tool definition resembles a function signature, but the runtime is deliberately separating model data from host behavior.
That validation boundary also gives the runtime a place to reject a changed world. A model may have produced valid JSON for a file edit, yet the file may no longer contain the expected text by the time the tool runs. The tool can return a typed, model-visible failure instead of silently applying a different edit. Similarly, a registration can be valid when advertised and stale when settled. In both cases, the agent receives a statement about the actual current state rather than an apparently successful result produced by guessing.
Authorization belongs inside the tool’s trusted execution path. A built-in tool formulates the permission request with the session, agent, assistant message, and tool-call identities supplied by the runner. A repository search may ask to read a pattern within the active Location. A file write may need both permission to access an external directory and permission to edit the target. A shell command may need approval for the command itself and for an external working directory. The exact checks vary by tool because the resource being authorized varies.
If the permission system returns allow, the tool executes with the services it was designed to use. A read operation resolves and inspects a path, pages large text, and may normalize supported images. An edit operation checks that the expected old text still exists and can reject a stale file instead of overwriting a change made after approval. A shell operation runs with the host user’s filesystem, process, and network authority, subject to the tool’s own timeout and capture handling. These are not equivalent capabilities, so the registry’s shared tool type should not be mistaken for equal authority.
The result then crosses a second presentation boundary. A tool may return structured data for the runtime, model-facing text for the provider, or supported media attachments. The V2 Tool contract allows a pure projection to decide how validated output should be shown to the model. The registry sends the projected result through the output store, which can keep a bounded preview in the session while retaining oversized textual content separately. Structured metadata and media are handled distinctly rather than being flattened into an unbounded text block.
This separation protects more than context length. The durable session needs a truthful account of whether an operation succeeded, while the provider needs a representation it can consume within its request budget. A bounded preview can tell the model that a complete result exists elsewhere without changing the tool’s validated structured value. Conversely, a producer that discarded bytes before the registry received them cannot claim that a later retention step preserved the full output. The source treats domain output, model projection, and storage policy as related but non-identical responsibilities.
Only after this settlement does the runner decide how to continue. The V2 runner records each complete local tool call durably before side effects begin, starts the execution, and awaits the started tool fibers after the provider stream closes. Once the results are published, it reloads the projected history and begins another provider turn. The next request contains the model’s prior tool call and the corresponding result, so the model can use the new evidence to choose its next action or produce an answer.
This explains why one user prompt can produce several provider turns without being several unrelated conversations. Each turn advances the same Session. A model can search, inspect a file, edit it, run a check, interpret the output, and revise the next action because the runtime places each result back into the chronological record that the following turn consumes.
Starting independent tools eagerly is a deliberate latency choice. If one provider response requests multiple local calls that do not depend on one another, OpenCode can begin their settlements without waiting for each to finish serially. The provider stream still has a durable settlement boundary: the runner records, authorizes, settles, and waits for the calls before it commits to the next continuation. Concurrency reduces waiting time; it does not remove the need to know which calls were issued, which succeeded, and which failed.
The stopping conditions are equally important. If the agent has reached its configured step limit, the V2 runner can omit tools for the final step and add a reminder that the model must finish. If a tool fails with an expected model-visible failure, the model may be able to respond by changing its approach. If a call is unknown, invalid, or stale, the runtime reports an explicit settlement error without invoking an arbitrary handler. If the provider fails, the stream is interrupted, or execution is cancelled, OpenCode can mark unfinished tool work as failed rather than pretending that an incomplete side effect was a successful result.
There are two implementation stories in the captured repository, and the distinction matters. The legacy SessionPrompt service uses the AI SDK’s tool execution path, while the V2 runner is built around one explicit normalized llm.stream(request) turn and a core-owned registry. The V2 runner’s comments list completed behavior beside unfinished work, including some provider-aware tool materialization, plugin and MCP integration, cancellation settlement, bounded retries, and post-run maintenance. The broad legacy runtime means OpenCode already has more application behavior than the V2 slice alone, but a V2 design note is not evidence that every planned behavior is the default path today.
The practical result is a better way to inspect an agent’s actions. Do not ask only, “What did the model say?” Ask which Location supplied the context, which agent supplied the policy, which model and provider handled the turn, which tool registration was advertised, what permission decision occurred, what output was persisted, and why the runner did or did not continue. OpenCode’s orchestration is visible in those transitions.
A provider turn is therefore a narrow unit inside a larger control loop. The model proposes the next response; OpenCode supplies the project boundary, state, authority, validation, and continuation. The next question is how that loop remains coherent across many turns—when the user steers a running task, when the process reconnects, and when the context window can no longer hold the full history.