Nous.Tools.RunCode (nous v0.17.1)

Copy Markdown View Source

Code Mode's transport tool: runs a model-authored program that calls tools.

One run_code call replaces a chain of individual tool calls — the program loops, branches and fans out in a single round trip, and only what it logs or returns re-enters the conversation. The tools it may call are declared in the SDK carried in this tool's description (see Nous.CodeMode.Sdk), and are reached through bindings derived from the same permission policy that governs a direct call (Nous.CodeMode.bindings/4).

The runtime provider

Nous.CodeRuntime.JS ships in this repository and runs programs in an embedded V8 isolate. It needs the optional :tyrex dependency:

{:tyrex, "~> 0.4"}

config :nous, :code_runtime, {Nous.CodeRuntime.JS, timeout_ms: 30_000}

Any other provider behind Nous.CodeRuntime works the same way. With none configured this tool returns a clear error naming the configuration it needs; it does not crash, and it does not pretend to have run anything. mode: :both quietly behaves as :native when nothing is configured, so that error is only reachable when an operator explicitly asked for mode: :code.

Approval is per sub-call, not per program

Approving a run_code call approves running that program. It does not approve whatever the program then decides to call: each sub-call to a tool with requires_approval: true consults the approval handler on its own, with the real tool name and the real arguments. With no handler in the context such a tool is refused rather than run — the same default-deny Nous.ToolExecutor applies at every other entry point.

Result shape

A run that finishes returns %{logs: [String.t()], result: json}. A run that fails returns %{logs: [String.t()], error: %{kind: String.t(), message: String.t()}} — failure is data, not a tool error, because a program that throws is the normal case: the model reads the failure and writes a better program. {:error, _} is reserved for a call that never ran at all — no provider, a missing description, a request the seam refused.

Logs are returned whether the run succeeded or failed, so a program killed by its deadline still reports what it managed to say.

Sub-calls go through one lane

Every tool call the program makes is submitted to a Nous.CodeMode.Scheduler started for this run and torn down with it: one driver lane with a max_parallel ceiling, an exclusivity barrier for tools that do not declare themselves concurrency_safe?/1, and two independent argument snapshots per call. The bindings never reach Nous.ToolExecutor on their own.

The scheduler records each sub-dispatch as a bookkeeping :tool_call event the moment it starts. A tool is handed a %Nous.RunContext{}, which carries no session log, so those events leave here as :log_event operations on a Nous.Tool.ContextUpdate — this tool's third return shape — and the agent runner appends them to the real Nous.Agent.Context. Being bookkeeping, they project to no message: a program making forty tool calls adds forty audit records and nothing whatsoever to what the model reads.

Approval and audit

description is required and must be non-empty. It is what a human reads in an approval prompt and what an audit log records; an empty one turns both into a shrug, so it is rejected. The tool's own requires_approval is false — gate it through Nous.Permissions (it is an :execute-category tool) rather than here, because every tool the program calls is also gated, and a second unconditional prompt in front of the first buys nothing.

Summary

Functions

Return the full tool schema definition for introspection.

Run a program, with the tool set and dispatch this call is scoped to.

Functions

__tool_schema__()

@spec __tool_schema__() :: map()

Return the full tool schema definition for introspection.

Includes parameter declarations, category, and tags.

run(ctx, args, opts)

@spec run(Nous.RunContext.t(), map(), keyword()) ::
  {:ok, map()}
  | {:ok, map(), Nous.Tool.ContextUpdate.t()}
  | {:error, String.t()}

Run a program, with the tool set and dispatch this call is scoped to.

Nous.CodeMode.run_code_tool/3 builds the closure that supplies opts; the bare execute/2 above is the same call with none, which is what a direct Nous.ToolExecutor.execute/3 outside the runner gets.

Returns {:ok, output, %Nous.Tool.ContextUpdate{}} when the program made at least one sub-call, carrying one :log_event operation per sub-dispatch in submission order; a program that called nothing returns the plain {:ok, output}, because an update with no operations is noise.

Options

  • :tools — every tool in scope before the permission policy filtered it. Granted ones become real closures, denied ones error stubs.
  • :policy — the Nous.Permissions.Policy that decides which is which.
  • :dispatch — a Nous.CodeMode.dispatch/0 the scheduler runs sub-calls through; defaults to Nous.CodeMode.direct_dispatch/3. It replaces what the lane calls, never the lane itself.
  • :call_id — the model's tool-call id, so each sub-dispatch event correlates back to this call. Defaults to a minted per-run token.
  • :runtime{module, config} overriding config :nous, :code_runtime.