Skip to content

The agent runtime

This page explains what happens inside a turn: the Pi session, the tools the model can call, the guard every call passes, and how the prompt is assembled.

The short version: src/runtime runs each turn in a Pi session. Every tool call goes through one turn guard. Results from the outside world reach the model wrapped as untrusted data.

Pi is the agent harness nod runs on. It keeps the transcript, calls the model and runs tools. nod's adapter is in src/providers/pi/. The runtime in src/runtime/index.ts runs sessions in parallel. Each session runs one turn at a time:

  • Conversation turns share the thread's saved session. The server hands it to one turn at a time.
  • Background sessions save their transcript too. A reply or secure form continues that session.
  • Subagents use temporary in-memory Pi sessions and run in parallel.

Recent chat history goes into the prompt only when Pi has no saved session. A resumed session gets new background messages since its previous turn started.

Each Pi execution has one scope: thread ID, session ID, turn ID, run ID, parent run ID, and browser ID. The main run uses the turn ID. Each subagent gets a new run ID and uses it as its browser ID. Tools and model usage keep this scope. turn_events records execution and tool activity without saving raw tool arguments or results.

model_usage is the only cost ledger. Each model request reports one usage ID, including subagent, compaction, review, and memory requests. Repeated reports with the same ID create no extra charge record. Parent runs never add child costs again. All totals come from these ledger rows. Missing prices stay unknown.

Mechanism Rule
Compaction Pi compacts at 256k tokens or 16,384 tokens below the model's context window, whichever is smaller, and keeps the newest third of that in full, which leaves the summary request room even when a chat fills the window.
Screenshot pruning Above twelve screenshots, the oldest are replaced in steps of eight with a note to take a new one. The prompt cache breaks only at those steps.
Attached images PNG, JPEG and WebP images of up to 3.75 MiB each, and 16 MiB together per message, reach the model with the message. Others, and every image for a model that can't read images, stay workspace files marked as not shown.
Provider errors Reach Pi with only the connection key redacted, so Pi's overflow recovery and retries see the real error.

When a loaded session ends with tool calls that have no result, each call gets the result "This action was interrupted. Its outcome is unknown; check before doing it again."

Tool definitions live in src/runtime/tools.ts.

Tool What it does
read, bash, edit, write, find, ls Pi's native tools. They run inside the workspace container.
skills Reads a guide or learned skill; only owner-started work writes one.
send_message Sends a message now. The only delivery in background turns.
follow_up In a chat or saved task, queues one short line that goes out as its own message right after the reply, such as an offer to keep doing a job.
ask_user Asks one question and ends the turn as waiting.
react_to_message Now and then, reacts to the user's message with one emoji, on web, Telegram and WhatsApp. At most one every five minutes.
browser Drives the private browser: open, snapshot, click, fill, evaluate, download and more.
sign_in Enters a Vault login or code into the page without showing it to the model.
pay Pays at a checkout with a Vault card after your approval.
browser_session Creates a browser profile to sign in to, or opens a saved one after the same check as a sign-in.
apps Runs an action on a connected account, after the trust check.
manage_apps Lists, discovers, connects or proposes app connections.
manage_personal_context Schedules, notification preferences, memory, history and usage.
task Starts saved task sessions, sends owner updates, reports progress and stops a task. It grants no permission.
share_file Attaches a workspace file to nod's next reply or question, or sends it alone when a turn finishes or fails without one.
create_audio Turns text into speech and sends it to you.
web_search, fetch_content, get_search_content, source_check Web research from the pinned pi-web-access extension. It never reads local files.
codemode Runs a model-written script that calls workspace and web tools.

The pi-web-access extension runs in the server process with the server's permissions, but has no direct access to workspace files and never uses the server's own logins, such as a GitHub CLI session. Its connections go only to public addresses, checked when each connection opens, IP addresses included. nod drops any proxy, whether the model names it or the settings file sets it. Its settings live in NOD_DATA_ROOT/pi/web-search.json.

Every tool call, including every call a codemode script makes, passes the same guard.

flowchart TD
  call[Tool call] --> live{{Turn still<br/>running?}}
  live -- no --> refuse[Refused]:::stop
  live -- yes --> run[Run the tool]:::go
  run --> wrap[Wrap outside results<br/>as untrusted data]
  wrap --> pi[Back to Pi]:::accent
  • Cancellation: a stopped or finished turn can't start a call. Server callbacks also check withTurnLock before they run.
  • Observations: the guard reports each call to an optional observer. Only the capability evals and the voice eval script set one; the capability evals redact typed text and memory arguments.
  • App actions: apps also passes the server's trust check.

apps and manage_personal_context calls run in parallel with each other and with subagents. The trust check covers app actions, changes to schedules, notification settings and spending limits, and forgetting a note or turning memory on, off or clearing it, and those checks run in parallel too. Other manage_personal_context operations, such as saving a note and reads, have no trust check and count as decided as soon as they reach the server.

A call acts only once every apps or manage_personal_context call issued before it in the turn is decided, and each earlier one that asks has opened its question or failed. If one of them opened a question, the later call is refused. A turn has at most one open confirmation: while one waits for the owner, approvals refuse to open another.

Pi's codemode tool runs model-written JavaScript in a QuickJS sandbox with no network, files or timers.

  • Scripts can call only the workspace and pi-web-access tools, in parallel, plus Pi's image and classifier models. That model spend goes to the usage ledger.
  • Every nod tool is model-only, so scripts can't reach apps, the browser, credentials, messages or shared files.
  • Calls inside a script stay raw so the script can parse them. Codemode's output is wrapped when it returns.

Results from apps, the browser, the web and codemode reach Pi inside one untrusted_tool_result block with a data-not-instructions notice. Workspace tool results aren't wrapped, and errors reach Pi unwrapped. See Untrusted content.

The prompt has three layers, built in src/runtime/prompt.ts.

Layer Contains Sent
Static prompt nod's personality and rules, the conversation and background rules, the core guides, the skill catalog and Pi's tool guidelines Once per session
Owner context Account details, date format, the memory index and the learned skill index Again only when it changes
Turn message Mode, time, channel, message ID, reply target, recent conversation and the instruction Every turn

The core guides are connected-apps, schedules-and-watches, artifacts and video-links. Pi's own tool guidelines are appended to the static prompt, because nod's custom prompt would otherwise drop them.

The owner context is a message of its own, never part of the system prompt, so a memory change leaves the system prompt and the conversation before it untouched:

  • A session gets the full current owner context with its first turn, and again with the first turn after it changes. The newest copy replaces earlier ones.
  • Earlier copies stay where they are, so the cached conversation after them stays valid.
  • Compaction summaries leave the owner context out. A session that compacts it away gets it again with its next request.

Providers that take a prompt cache key, such as OpenAI, get the same one from every session of a thread: a keyed hash of the thread ID that changes when nod restarts. When a session's system prompt and tools match a recent request from the same thread, as they do for back-to-back background runs even after memory changes, the provider reads them from its cache and bills them at its cheaper cache-read rate. The code is in src/providers/pi/index.ts.

A chat turn posts its opening line, then only what the model writes between tool calls after the user has heard nothing for a while, then its final reply. The window is 30 seconds in nod web and 3 minutes in chat apps. A task's first message is its acknowledgement, so the task posts only its questions, files, deliberate send_message updates and result. The rule lives in src/runtime/index.ts; the prompt asks for what each message should contain.

Moment What nod does Why
Start One line on what it will do, only when the work takes more than a few seconds Unexplained waits feel longer (Maister 1985); feedback matters past about 10 seconds (Nielsen)
During One line at a change of step: what it found and what's next, without tool names, percentages or time estimates Intermediate results beat silence on perceived speed and trust (Kirmayr et al. 2026); progress that slips makes waits worse (Conrad et al. 2010); updates land best at breakpoints (Iqbal and Bailey 2008)
Chat apps Waits longer before an update Every phone notification costs attention (Stothart et al. 2015), and people often look minutes later (Pielot et al. 2014)
End One reply that leads with the result and doesn't repeat updates Summaries help; full traces hurt (Fernandes et al. 2026)
After At most one concrete offer, not repeated after a no Frequent proactive suggestions annoy (Chen et al. 2025); keep requests minimal and easy to dismiss (PAIR)

Workspace images include a pinned RTK. Supported simple Bash commands are rewritten to RTK inside the workspace for compact Git, file, search, test and build output. Pipes, redirection, machine-output flags, bare git log and find stay raw. RTK's find skips hidden and ignored folders such as .nod, where nod keeps its working files. Unsupported commands fall back before they run, and executed commands are never retried. rtk proxy <command> gives exact output. The rules are in src/providers/pi/sandbox.ts.