Mode resolution and the visible tool set for Code Mode.
Code Mode lets the model write a program that calls tools — looping,
branching, fanning out — in one round trip, instead of emitting a chain of
individual tool calls. The program runs on a Nous.CodeRuntime provider; the
tools it may call are the same ones a direct call would reach, through the
same permission policy.
Modes
:native— no Code Mode. The model sees the tools and calls them directly. This is what every existing agent does.:code— the model sees onlyrun_code. Tools are reachable from inside a program and nowhere else.:both— the model sees the tools andrun_code, and picks per turn. A singlefile_readis already ideal as a native call; a ten-step search is not.
Resolution order: the agent's :code_mode field, then
config :nous, :code_mode, then :both. As with
Nous.Sandbox.Policy.default_mode/0, a configured value may be a string (it
often comes from an env var) and is matched against a literal whitelist —
never String.to_atom/1.
Degrading without a runtime
:both needs a runtime to be worth advertising: with none configured it
behaves as :native rather than showing the model a run_code that can only
fail, since the native path is right there. :code is not degraded — an
operator who asked for code mode gets run_code, and calling it without a
provider returns an actionable error (see Nous.Tools.RunCode) instead of
silently reverting to a mode they turned off.
Configure a provider with:
config :nous, :code_runtime, {Nous.CodeRuntime.JS, timeout_ms: 30_000}Nous.CodeRuntime.JS ships here and needs the optional :tyrex dependency;
any module implementing Nous.CodeRuntime works in its place.
run_code sits outside the restriction layers
visible_tools/4 injects run_code after Nous.Permissions.filter_tools/2
has run. A restriction that denies every tool still leaves Code Mode's entry
point reachable — otherwise a deny-all policy would not restrict the agent,
it would mute it. That is not a hole: run_code still dispatches through the
whole pipeline, so a pre-tool hook or a permission plugin can inspect the
program text before it executes, and every tool the program calls is filtered
by the same policy through bindings/4.
One permission mechanism
bindings/4 derives the program's callable surface from
Nous.Permissions.filter_tools/2. A granted tool becomes a real closure,
already scoped to the caller; a denied one becomes a stub that returns an
error naming the tool. Both appear, so a program that calls a denied tool
reads a comprehensible refusal instead of crashing on an undefined function —
and there is exactly one place that decides what may run.
Summary
Types
A tool dispatch: what actually runs a sub-call made from inside a program.
How the model reaches tools this run.
Functions
How long one run_code call may take, in milliseconds.
The bindings a program is given: one global, one function per tool.
What the model is told when a call collapses. Names the way back in, so the
next turn is a run_code call rather than the same mistake.
Whether a model-direct call to tool_name collapses to "unknown tool".
The application-wide default mode.
Run one sub-call directly, with no scheduling in front of it.
The language the configured provider runs, as an SDK language.
The three modes.
Normalise a mode given as an atom or a string.
Mint a parent token for one run_code execution.
The mode for an agent: its own :code_mode, else default_mode/0.
The name of the transport tool.
Build the run_code tool for this request.
The configured runtime provider, or an error message explaining what to do.
Whether a usable runtime provider is configured.
Tag a synthesised tool call as transport, so collapsed?/3 lets it through.
Whether a tool call was synthesised by a running program rather than sent by the model.
The tool set the model sees, given mode.
Types
@type dispatch() :: (Nous.Tool.t(), map(), Nous.RunContext.t() -> {:ok, term()} | {:error, term()})
A tool dispatch: what actually runs a sub-call made from inside a program.
Sub-call scheduling, ordering and session-event logging live behind this
function, which is why bindings/4 takes it as an option instead of calling
the executor directly.
@type mode() :: :native | :code | :both
How the model reaches tools this run.
Functions
@spec await_timeout_ms() :: pos_integer()
How long one run_code call may take, in milliseconds.
This is the caller's patience, not a budget the program can ask for: real budgets — wall clock, memory, instruction count — are provider configuration, validated when the provider is configured.
@spec bindings( [Nous.Tool.t()], Nous.Permissions.Policy.t() | nil, Nous.RunContext.t(), keyword() ) :: [ Nous.CodeRuntime.Binding.t() ]
The bindings a program is given: one global, one function per tool.
Granted tools — those that survive Nous.Permissions.filter_tools/2 — get a
closure that dispatches the real tool with the caller's context. Denied tools
get a stub returning an error naming the tool. Both are present, so the
program never hits an undefined function and never gets to distinguish
"denied" from "failed" by the shape of what it caught.
Options
:dispatch— adispatch/0replacing the default,direct_dispatch/3. This is where a sub-call scheduler plugs in: serialisation,max_parallel, argument snapshots and session-event logging all live behind it.
direct_dispatch/3 is the default because it is the only thing a caller who
owns no Nous.Agent.Context can honestly do — but it is not what the
shipped Code Mode path uses. Nous.Tools.RunCode starts a
Nous.CodeMode.Scheduler for each run and passes
Nous.CodeMode.Scheduler.dispatch_fun/1 here, so a real program's sub-calls
are ordered by one lane and audited into the session log. Calling bindings/4
with no :dispatch gets the unscheduled path: right for a test or a direct
caller, wrong for a run.
What the model is told when a call collapses. Names the way back in, so the
next turn is a run_code call rather than the same mistake.
Whether a model-direct call to tool_name collapses to "unknown tool".
Under mode: :code the model was shown exactly one tool, so a call to any
other one — hallucinated, or replayed from a transcript recorded in another
mode — can only fail. It is refused here, before pre-tool hooks run: a
guard must never be asked to approve a call that cannot execute, because an
approval it grants is a decision about nothing.
A sub-dispatch made from inside a program carries a parent token (see
tag_transport/2) marking it as transport rather than model, and is let
through.
@spec default_mode() :: mode()
The application-wide default mode.
@spec direct_dispatch(Nous.Tool.t(), map(), Nous.RunContext.t()) :: {:ok, term()} | {:error, map()}
Run one sub-call directly, with no scheduling in front of it.
This is the default dispatch/0, and the one Nous.CodeMode.Scheduler sits
on top of in the shipped run_code path rather than calling
Nous.ToolExecutor.execute/3 itself: the executor has a three-shape
return ({:ok, result}, {:ok, result, %ContextUpdate{}},
{:error, reason}) and a dispatch must hand back two. Folding that here, in
one place, is what keeps a tool that returns a context update from looking
like a contract breach to everything downstream.
The context update is dropped, loudly. There is no agent context at this
depth to merge it into — a %Nous.RunContext{} is not one — so the honest
options are "drop it and say so" or "lie". That is about a tool's deps
update: applying those from concurrent sub-calls into a context the runner
also owns is the two-writers hazard. The scheduler's own bookkeeping events
are a different thing and do reach the session log, as :log_event
operations on the update Nous.Tools.RunCode returns.
@spec language() :: Nous.CodeMode.Sdk.language()
The language the configured provider runs, as an SDK language.
Falls back to :javascript when nothing is configured or the provider names
a language this repo cannot generate an SDK for — a provider must not be able
to break tool visibility by reporting an exotic language.
@spec modes() :: [mode()]
The three modes.
Normalise a mode given as an atom or a string.
Raises ArgumentError on anything else, at the point of configuration rather
than mid-run.
Examples
iex> Nous.CodeMode.new(:code)
:code
iex> Nous.CodeMode.new("both")
:both
@spec new_parent_token() :: String.t()
Mint a parent token for one run_code execution.
The mode for an agent: its own :code_mode, else default_mode/0.
Takes anything with a :code_mode key, so it works on an agent, a plain map
in a test, or nil.
Examples
iex> Nous.CodeMode.resolve(%{code_mode: :code})
:code
iex> Nous.CodeMode.resolve(nil)
:both
@spec run_code_name() :: String.t()
The name of the transport tool.
@spec run_code_tool([Nous.Tool.t()], [Nous.Tool.t()], keyword()) :: Nous.Tool.t()
Build the run_code tool for this request.
The generated SDK rides in the tool's description, which is where the model
reads it: under mode: :code it is the only declaration of the tools that
reaches the prompt at all. That also makes Nous.CodeMode.Sdk's byte
stability load-bearing — this string is part of the prompt prefix of every
request in the run.
retries: 0 is deliberate. A program has side effects by the time it fails;
re-running it because the model's code threw would repeat them.
The configured runtime provider, or an error message explaining what to do.
Accepts {module, config} or a bare module (config defaults to []), and
checks the module actually implements the behaviour, so a typo is reported
here rather than as an UndefinedFunctionError inside a tool call.
@spec runtime_configured?() :: boolean()
Whether a usable runtime provider is configured.
Tag a synthesised tool call as transport, so collapsed?/3 lets it through.
Whether a tool call was synthesised by a running program rather than sent by the model.
@spec visible_tools(mode(), [Nous.Tool.t()], [Nous.Tool.t()], keyword()) :: [ Nous.Tool.t() ]
The tool set the model sees, given mode.
all_tools is the set before the permission policy ran and granted the
set after; both are needed because the SDK declares what the program may call
(granted) while its bindings also stub what it may not (all).
Injection happens here, after the filter, on purpose — see the moduledoc.
Options
:policy— theNous.Permissions.Policyin force, carried into the bindings so the program is filtered by the same mechanism.