# Models and core: file tools, mid-run steering, /model switches, and new OpenRouter models

## File tools: read_file, edit_file, write_file

The builtin toolset gains three file tools. `read_file` returns a `cat -n`-style line-numbered view with `offset`/`limit` paging (2000 lines by default), reading incrementally instead of slurping the file: an abort-aware scan with a hard 8MB cap that retains only the requested window, detects CRLF, and truncates overlong single lines. Its output cap is 64000 characters against the 16000 default of the other builtins, and the output is self-budgeted so the trailing "file has N lines total; showing X–Y" continuation note always survives truncation. It refuses binary content with a pointer to the shell and image tools, and refuses `.vault.toml` / `.project_config.toml` by basename — it is auto-approved under read-only approval, so the secret files need their own guard. `edit_file` performs exact `old_string` → `new_string` replacement: the string must occur exactly once — zero occurrences and duplicates both fail with an explanation — `replace_all` covers bulk changes, and success answers with a summary line plus a git-style unified diff: one hunk per replacement site with nearby sites merged, capped at 5 hunks on a `replace_all` storm with an "…and N more replacements" note, self-budgeted so the summary and note survive truncation. `write_file` writes a whole file, creating parent directories as needed, and reports whether it created or overwrote — an overwrite appends a real line diff against the previous content (a `+X/−Y` one-liner once the diff passes ~60 lines), a created file carries none — and the CLI colors the `+`/`-`/`@@` lines of both tools' diff outputs. Both writers land atomically (a temp file in the target directory, then a rename that preserves existing permission bits), and the approval prompt in both UIs shows the decoded edit or write payload, so the user approves the actual change. All three resolve relative paths against the Workspace and accept absolute paths under the same trust model as the shell tool, which remains the general-purpose fallback for everything else. The docs site's framing — it used to present "no file tools at all" as a design tenet — was rewritten around the nine-tool builtin set (zh + en); the landing blog's earlier posts keep their historical six-tool description. New agents get the file tools; an existing agent's frozen tool list does not, until its config is restored to the current defaults — see [backward compatibility](2026-07-24-backward-compatibility.md).

## Model-written call descriptions

The four command/subagent tools — `exec_command`, `input_command`, `run_subagent`, `input_subagent` — take an optional `description` argument: one model-written sentence, in the user's language, saying what the call is doing, shown in the UIs while it runs. The parameter lives in the four entries' schemas in `system_config.yaml` like any other tool parameter, and it is required there while the entry's `call_description` toggle — missing means enabled — is on, so a tool that offers a description always carries one and the frontends can pick a call's display form up front instead of guessing mid-stream; switching the toggle off filters the property and its `required` entry out of the assembled schema, never rewriting the stored YAML. The web Tools tab switches it per row. The file tools take no `description`: their path argument is self-describing. Every schema leads with its most user-visible argument — `description` first on these four tools, `file_path` first on the file tools — so it streams first into the CLI and web previews.

## Exact CLI call and output formats

Tool calls in the CLI render as `name <- description (payload)`, or plain `name <- payload` for a tool whose schema offers no description — the form is settled up front from the session's assembled tool list rather than guessed as arguments stream, so the plain form streams live while a described call waits for its sentence and only ever appears in the described form. Call and output lines share an identical `[tool-NNN] name` prefix, so `[tool-NNN] exec_command <- $ date` pairs with `[tool-NNN] exec_command -> output`; an output whose call was never seen keeps the bare `[tool-NNN] ->` tag. File-tool previews shorten the path to one parent directory plus filename, and the chat and run startup banner now prints the version and the Agent, Workspace and Model each on its own line.

## Steering: message a running task

A running task used to be out of reach — anything sent mid-run just waited for the next idle. `Session.steer` and `POST /api/sessions/:sessionId/steer` now deliver a message into a RUNNING task: the text is queued and handed over at the next input assembly as a standalone user message wrapped in a paired `[user_steering]` marker, sent alongside that turn's tool outputs — written to Trace as real user input, streamed at that same point, and never splitting the Task in the UIs. Tool outputs are never rewritten, and steering authority comes from the message's user role — a tool printing the marker inside its own output cannot impersonate the user. The queue drains at every input assembly, including immediately after a compaction (steering that arrives during a long summarize request is delivered right behind the summary), and is discarded only when the run exits; a turn that ends with the queue still non-empty continues the loop with the queued text through the same mechanism. The marker is documented in the default prompt's System markers section.

A busy session can also hold ordinary follow-ups: `POST /api/sessions/:sessionId/tasks` accepts `queueIfBusy`, answering 202 and queuing the input server-side; when the run ends — abort included — each queued input auto-starts as a normal task, in order, carrying the per-turn thinking level stored alongside it rather than falling back to the session default, with the queued count carried on task-state events and the session snapshot. In the CLI, a line typed while a task runs becomes steering (the acknowledgment echoes the text), the renderer holds streamed output while the user is composing a line and flushes it on submit, and a steer that loses the completion race is re-submitted as the next normal prompt.

## System markers move to paired square brackets

Every system-synthesized marker switches from the angle-bracket to the paired square-bracket form: `[turn_aborted]`, `[turn_retried]`, `[context_summary]`, `[summary]`, `[use_skills]`, `[handoff_from]`, `[scheduled_task]`, `[developer_instructions]`, plus the inner transcript tags inside synthetic blocks. Producers emit only the new form, while the parsers keep reading the legacy angle form — see [backward compatibility](2026-07-24-backward-compatibility.md). Every marker's producers and parsers moved into one core module, `omnimessage/markers/`, also reachable as the `@prismshadow/penguin-core/markers` subpath, so the tag literals and the dual-form reading rule have a single home instead of being spread across core, server and the frontends. The docs' marker literals were updated accordingly (zh + en).

## Thinking level is per-request; session_meta keeps invariants only

`thinking_level` is removed from `session_meta`: the meta now holds only per-session invariants, and anything the user can change mid-conversation is either a per-turn parameter or a new session. The level threads through as a per-request parameter instead — `GenerativeModelParameters`, `RunOptions` and `TaskCreateRequest` — with the construction-time value acting as just the default; compaction requests deliberately keep the default. Traces that recorded the field are still honored on resume — see [backward compatibility](2026-07-24-backward-compatibility.md).

## Switching models: a new session that reads the source trace

Continuing a conversation on another model injects no history into the new session: thinking payloads and provider fidelity are model-bound and cannot be replayed faithfully across models, so the earlier history-carrying fork design was dropped for the handoff pattern. The web's `/model` command opens a NEW session for the same agent on the picked model through the ordinary session-creation API — deliberately reusing the source session's Workspace, so files the conversation refers to stay reachable, and its approval mode — and posts a first task whose input opens with a paired `[model_switch_from]` source block — the source session id, the absolute path of its latest trace file, the workspace, and the previous model pair — following the square-bracket marker convention. The model reads the source trace itself when it needs the earlier context. `SessionInfo` gains an optional `tracePath` (the latest shard, populated on the single-session GET only), and the marker is stripped from generated titles.

## The prompt presents the project dir as the App Data Dir; restoring defaults is the adoption path

Models kept mistaking the Environment section's `Project Dir` for the user's working directory (which is `CWD`) — treating its contents as task input and writing task deliverables there. The default prompt now presents the same value under a name that says what it is: the Environment line reads App Data Dir (the `{{PROJECT_DIR}}` placeholder is unchanged, so existing custom prompts are unaffected), and the File system section states the semantics explicitly — PenguinHarness's application data root, holding every agent's data files plus the project-level data files; not the current task's directory, not user-provided input, never a place for task deliverables (the working folder is `CWD`). Prompt paths (Agent State, other agents' assets, scratchpad, skills) are spelled `<app_data_dir>/agents/…`, and the secrets rule points at `.project_config.toml` directly under the App Data Dir. Six of the builtin skills read the App Data Dir field from the Environment section — the eval-family's project-root derivation simplifies to the App Data Dir itself — and the web system-prompt editor's placeholder hints document `{{PROJECT_DIR}}` with the app-data-root description, in the prompt's order with the full set.

Default changes never reach existing agents by themselves, since a stored config is loaded verbatim. The adoption path is a new "Restore default configuration" operation — `POST /api/projects/:projectId/agents/:agentId/config/reset` and the matching action on the agent settings Overview tab, behind a confirm dialog — the config-side analogue of a skill update; what it preserves and what it overwrites is spelled out under [backward compatibility](2026-07-24-backward-compatibility.md).

## OpenRouter additions: three free models and Claude Opus 5

The catalog gains three $0 OpenRouter rows: `inclusionai/ling-3.0-flash:free` and `poolside/laguna-m.1:free` — free tiers of Ling 3.0 Flash (a 124B-parameter MoE) and Laguna M.1, both with a 262,144 context — and `openrouter/free`, the Free Models Router that sends each request to a random free model. The router deliberately claims no vision support, since the harness must not send images to a router whose target may be text-only, and records a conservative 128,000-token context floor, so the 75% compaction clamp fires at 96,000 tokens: with no window recorded, the clamp was silently disabled, and a long Session routed to a small-window target would hard-fail instead of compacting early. The catalog test's free-pricing invariant covers exactly the `:free` suffix and the router id, the web UI badges such rows as Free, and the models docs (zh + en) note the free variants, their zero cost, and OpenRouter's free-tier rate limits and data policy.

Requested alongside the free lineup, `anthropic/claude-opus-5` joins the OpenRouter block as a paid flagship: context 1,000,000, vision, and per-million pricing of $5 input / $25 output with the real published cache prices ($0.50 read, $6.25 write) rather than the repeat-input convention — positioned newest-of-series first, right after `claude-fable-5`.
