Code Mode
Code mode lets an Agent solve a multi-step Tool task in one small program. The model can gather independent results, filter them, and combine them before it returns to the conversation. Your Ruby Tools keep their usual permission checks.
Start by adding code mode to an Agent that already has a Tool:
class HelpCenterLookupTool < LittleGhost::Tool tool_name "help_center_lookup" description "Find a support answer by topic." input_schema( type: "object", properties: {query: {type: "string"}}, required: ["query"], additionalProperties: false ) def call(input) entries = { "refund policy" => "Refunds are available within 30 days.", "shipping policy" => "Standard shipping takes three to five days." } entries.fetch(input.fetch("query"), "No matching entry.") end end class ResearchAgent < LittleGhost::Agent tools HelpCenterLookupTool code_mode end
The language adapter that runs the program is called an engine. By default, LittleGhost uses its Ruby engine and the native Sandbox for the host operating system. It fails closed when that Sandbox is unavailable.
Code mode adds three control Tools to the conversation. exec starts a program, wait checks on a program that is still working, and stop ends work that is no longer needed. The model can now send Ruby like this to exec:
results = tools.parallel( -> { tools.help_center_lookup(query: "refund policy") }, -> { tools.help_center_lookup(query: "shipping policy") } ) text(results.join("\n"))
LittleGhost turns the Agent’s Tool schemas into Ruby method signatures. The model sees those signatures and the available Tool names in its instructions. Those names, descriptions, and signatures form the code-mode Tool catalog. The model can compose the results with ordinary Ruby values instead of guessing how to call each Tool.
See what runs where
The program runs in a child interpreter. The Tools do not.
model
│ writes a program
â–Ľ
exec ──> sandboxed Ruby process
│ tools.help_center_lookup(...)
â–Ľ
Tool broker in the parent Ruby process
│ normal Tool execution
â–Ľ
HelpCenterLookupTool#call
The Tool broker receives interpreter calls in the parent Ruby process. It accepts only Tools registered on the Agent, then sends each call through the same validation, permission checks, limits, callbacks, events, and tracing used by a direct Tool call.
Public streams show the brokered Tools by name. They omit the exec, wait, and stop bookkeeping. Traces still record the control operation around its nested Tool calls, so you can follow the complete execution.
Code mode does not change what a Tool can do. A Tool still runs as application code, while the generated program runs in the configured Sandbox. Read Tools for Tool permission checks and Workspaces and Sandboxes before giving generated programs file or process access.
Compose Tool calls with Ruby
Each exec starts a fresh Ruby process. Local variables, constants, and globals do not carry into a later exec. Within one program, the model can use:
-
Named methods such as
tools.help_center_lookup(query: "refund policy"). -
tools.call(name, arguments)when the Tool name is dynamic. -
tools.parallelfor independent calls whose results should preserve input order. -
ALL_TOOLSto inspect the complete runtime catalog. -
text(value)to add user-visible output. -
Ordinary Ruby output from
puts,print,printf, andp, which is captured as user-visible output and combined into bounded chunks. -
The program’s final expression as the completed value returned by
execor a laterwait. -
finish(value)to complete early.
The dynamic form accepts either the catalog name ("help_center_lookup") or the matching method name ("tools.help_center_lookup").
JSON Tool results arrive as ordinary Ruby hashes, arrays, strings, numbers, booleans, or nil. When a Tool returns Tool::Result, code mode uses its Ruby value. Artifact bytes are not copied into program variables; the artifacts return to the model once with the surrounding exec or wait result. Stored references appear only when native media delivery exceeds its limit. A Tool failure raises inside the program so its Ruby code can handle the failure or return an error.
Fresh processes keep interpreter state from leaking across programs. Each Ruby program also gets a temporary Workspace. LittleGhost removes it when the program ends, so files created directly by the interpreter do not carry into a later exec.
A brokered filesystem Tool uses the Agent Run’s separate Workspace. Files written through that Tool follow the Run Workspace’s cleanup rules and may persist.
Check on work that takes longer
Most programs finish while exec is watching them, so their result is ready in the same Tool call. If a program is still active after one minute, exec returns still_working. The program keeps running. The model can call wait to watch for up to another minute or stop when it no longer needs the result.
Both exec and wait return as soon as the program finishes. The one-minute window is a maximum observation time, not a delay added to every call.
wait does not resume, restart, or extend the program. It returns only output produced since the previous exec or wait. The returned status tells the model what to do next:
-
still_workingmeans the program is active. Callwaitagain when its result is still needed, or callstopto end it. -
completed,error, andterminatedare final. There is no program to wait for after one of these statuses.
The built-in engines give each program a total lifetime of one hour by default. That deadline begins at exec and does not reset when the model calls wait. The engine ends and cleans up an expired program even if the model never checks on it again. Applications can configure a shorter total lifetime with wall_seconds; the one-minute observation window remains fixed.
A code-mode session owns the engine’s active child process and related resources. It accepts only one exec, wait, or stop operation at a time. The Agent closes the session when its current call ends, including after a failure or cancellation. Cleanup failures raise because LittleGhost cannot claim that the child process and its resources ended cleanly.
Keep a Tool in the conversation
With code mode enabled, ordinary Agent Tools move into the program catalog. The model-facing controls become exec, wait, and stop. Use except when an application Tool should remain available to the conversational model instead of moving into the program:
class ResearchAgent < LittleGhost::Agent tools HelpCenterLookupTool, ConfirmTool code_mode except: ["confirm_tool"] end
Exclude a Tool when the conversational model should call it as a distinct decision—for example, a final confirmation that must remain visible as its own step. except uses each Tool’s model-visible name; ConfirmTool defaults to confirm_tool. Calls made inside and outside code mode share the Agent’s Tool-call limit. The exec, wait, and stop controls manage execution. They do not count toward that application Tool limit themselves.
Subagent controls also stay in the conversation. They are orchestration choices for the parent model, not functions available inside a code-mode program. Code-mode wait watches an interpreter program; wait_for_subagents checks on delegated Agents. Core Concepts explains model-directed delegation.
Set limits for the work you expect
The Ruby engine sets limits for source and output size, memory, total and CPU time, file size, the number of programs, Tool calls, concurrency, and cleanup. Override only the limits your workload needs to change:
LittleGhost.configure do |config| config.code_mode = { engine: :ruby, sandbox: :native, limits: { programs: 16, wall_seconds: 900, cleanup_seconds: 5 } } end
Bubblewrap owns the program’s process tree but does not cap its process or thread count. Use an outer cgroup or container supervisor when generated code needs a hard task-count limit.
Application defaults apply to every Agent that declares code_mode. An Agent can override the engine, Sandbox, limits, or excluded Tools in its own declaration.
The operating-system Sandbox contains the interpreter. The parent Ruby process starts it, brokers Tool calls, and cleans it up. Language restrictions alone cannot contain native extensions, interpreter bugs, files, subprocesses, or sockets.
Use an enforcing Sandbox backend for model-written code. Before production, test the deployed backend against the files, child processes, networking, and resource pressure your application expects. Also test cancellation and cleanup on the deployed host.
Opt into JavaScript when it fits
The JavaScript engine is optional. It uses MiniRacer and gives each program its own V8 global state. The core gem does not require or load MiniRacer:
# Gemfile gem "mini_racer", "~> 0.21" # application setup require "little_ghost/code_mode/javascript_engine" LittleGhost.configure do |config| config.code_mode = {engine: :javascript, sandbox: :native} end
The JavaScript program has no Node.js APIs, filesystem, network, console, WebAssembly, or process-spawning API. Tool methods return Promises, and the generated instructions include TypeScript declarations derived from each Tool schema. Use await or Promise.all, text(value) for output, and exit() to complete early. Call text(value) first when the value should become user-visible output.
MiniRacer’s language-level restrictions are useful, but the operating-system Sandbox still contains the program. The Ruby parent owns the Tool catalog, permission checks, Tool-call limits, events, tracing, and resource cleanup.
Build a custom engine
Applications can register another CodeMode::Engine. An engine names its language, writes the instructions shown to the model, and opens a CodeMode::Session. The session implements #execute, #wait, #stop, and #close. The first three operations return a CodeMode::ProgramResult.
LittleGhost gives the engine a Tool broker and a Sandbox factory. The broker stays in the parent Ruby process. The factory creates the Sandbox that contains model-written code. An engine may request named runtime paths for its interpreter libraries. Those paths are visible to the child process, but they never become filesystem grants available through Tools.
The session owns every Workspace, Sandbox, child process, background task, and communication channel it creates. It closes those resources in reverse order. One registered engine can open several sessions concurrently on threads or fibers. Keep each program’s mutable state inside the returned session, and synchronize any state the engine shares across sessions.
A sandboxed engine must use a backend that owns the complete child process tree or can prevent child processes. An explicitly unrestricted backend may run an engine, but the generated program then has the same host access as the parent.
See LittleGhost::CodeMode::Engine, LittleGhost::CodeMode::Session, and LittleGhost::CodeMode::ProgramResult for the extension contract. Continue with Integrations to connect Runs to MCP tools, AG-UI, and OpenTelemetry.