A self-improving RLM agent for coding workflows and long-running autonomous tasks.
  • Rust 89.2%
  • Python 9.4%
  • Shell 0.6%
  • JavaScript 0.3%
  • PowerShell 0.3%
Find a file
Seth Karten 74591cddef
feat(kernel): rlm.end_turn() — the sanctioned terminal call (#3830)
* feat(kernel): rlm.end_turn() - sanctioned terminal programmatic call ends the turn

OpenAI-convention-trained models end assistant messages with a tool call,
and the PTC surface has exactly one tool (python_repl): on turns with
nothing to do, models invent degenerate no-op cells (print(".")), the
loop executes them, and another assistant cycle runs (the churn pinned at
0.10.2-beta.10 / c4c238ca, pre-#3410; the eval grader's
v3e_noop_filler_cell class). Measured on real sessions, 75% of assistant
messages end with a tool call (48k sampled) - text endings are the
minority pattern, so the explicit verb makes the native convention legal.

Wire the dormant machinery:

- Kernel runtime: rlm.end_turn() in the persistent REPL makes a host
  request (the same registration lane bash.completed uses); the host
  marks the executing cell's result with the ended_turn detail; without a
  registered handler the call raises, never silently no-ops.
- Kernel->host protocol: ExecuteResult carries the ended_turn flag
  (kernel shared types + manager execution path).
- Tool mapping: the flagged ExecuteResult maps to a terminating
  AgentToolResult with a short "turn ended" confirmation in the
  transcript.
- Batch rule (the core): should_terminate_tool_batch now terminates on
  ANY end_turn result instead of requiring every result to terminate;
  other calls in the same message still execute first.
- Loop: results pushed, TurnEnd emitted, no further assistant response;
  end_turn never queues a completion notice or self-wakes.
- Replay: the toolResult->user shape (no assistant between) is covered by
  cross-provider convert tests (openai-completions, anthropic, google,
  responses); transform_messages already handles tool-result runs.
- Prompts: one core.md line registering end_turn(); usage.md's
  text-only/no-op bullets name the sanctioned verb; pinned by a
  prompt_guards.rs structural test, mutation-checked red on removal.
- Changelog: .changes/rlm-end-turn.md (#3410 convention).

* test(pa-ai): collapse the ended-turn replay fixtures into one transform_messages pin

The four per-provider JSON fixtures (anthropic, google, openai-completions,
openai-responses) each replayed the same ended-turn shape ([user,
assistant(tool call), tool result, user]) to prove their provider converts
it. The shared transform every provider runs is the contract that matters:
pin it once - the matched tool result stays, no synthetic result is inserted
for its answered call, and the following user message survives as its own
message - and drop the per-provider JSON (the size directive's one-pin
rule).

* refactor(kernel): answer rlm.end_turn in the host-request dispatch

Delete the manager's self-registration plumbing for the end-turn handler
(register_end_turn_handler plus the OnceLock<Weak<Inner>> late-binding
slot): the marking now lives in the host-request dispatch, the same lane
bash.completed rides. A host-registered handler still wins; the manager
answers natively when none is registered, and a harness without this
manager keeps the not-available error, so the kernel call raises instead
of silently no-opping. Behavior is unchanged for daemon and daemonless
sessions.

* test: tighten the end-turn prompt guard to the structural scan

Same contract, less code: exactly one core-layer rlm.end_turn bullet that
registers it as ending the turn, and every #3410 turn-end bullet in the
usage layer names the sanctioned verb.

* fix(runtime): expose rlm.end_turn on the cell-facing rlm namespace

The bootstrap binds the cell-facing rlm name to the _RLMNamespace instance, not the module, so the module-level end_turn was unreachable from cells: await rlm.end_turn() raised AttributeError and the sanctioned terminal call never reached the host (Cursor Bugbot high-severity finding on the PR). Bind the namespace method to the module function, pin the delegation with a runtime-suite test, and require both surfaces in the kernel-venv probe.

* docs(kernel): state the end-turn raise condition as host support, not registration

The host-request dispatch answers rlm.end_turn natively (a registered
host handler still wins), so the prompt bullet and the docstring's
'raises when the host has not registered the handler' no longer matched
the shipped behavior: in an ordinary session no handler is registered
and the call still succeeds. Word the raise as host support (the
foreign-harness case), keeping the never-a-silent-no-op contract. The
structural prompt guard is unchanged: the core bullet still registers
end_turn as ending the turn.

* fix(prompts): restore #3410's turn-end bullets and drop the wording pin

Sebastian's thread on usage.md: the PR's rewording made #3410's rule
contradict itself - a no-op tool call offered as 'the whole turn' for
nothing-to-do frames. #3410's two bullets return verbatim (the text-only
turn stays the sanctioned ending for no-action messages), and the
structural wording pin is deleted per his prompt_guards thread: exact
wording coverage was relinquished in #3433, and the HARNESS_TOKENS scan
already requires the rlm.end_turn bullet through the core layer.

* fix(pa-ai): join consecutive user turns so an ended turn replays on Bedrock

Sebastian's Bedrock thread: rlm.end_turn() ends a turn on a tool
result, the history becomes [.., toolResult, user], and Bedrock
Converse rejects two user messages in a row - every end_turn() on
Bedrock broke the next request. Includes #3683's push_user_turn at the
conversion seam (tool results stay first) and adds the Bedrock
convert_messages pin for the ended-turn history. The transform_messages
replay pin is deleted per his thread on it: nothing in the shared
transform changed, so the pin passed on main without the PR - the
Bedrock convert_messages test replaces it where the history fails.

* refactor(kernel): answer rlm.end_turn before the registry lookup

Sebastian's host_requests thread: no host registers rlm.end_turn, so
the registry-first order and the registry-wins comment describe cases
that do not exist. The dispatch answers END_TURN_REQUEST_TYPE before
the lookup and the comments here and on the constant state what the
code does; the raise-condition wording in the core bullet and the
runtime docstring stays as landed.

* fix(daemon): pass the new cwd field in the factory spawn test initializer

The rebase brought RlmSpawnRequest.cwd to main; the F20 watch test's
factory-tagged initializer predates it and failed the windows-target
compile and shard 3.

* test: position the F20 cwd field after thinking like the spawn_child helper

Main's cwd fix (#3868) adds the same line after thinking in this
initializer; matching that position makes the two additions
byte-identical, so merging this PR after the fix keeps a single cwd
field (no E0062).

* fix(kernel): fail end_turn on a settled cell and drop the dead Bedrock join

Stack review of the 40331f797 delta (deepseek, no functional blockers):

- end_turn cloned the active execution and wrote ended_turn with no
  re-check: a cell settling in between (force_abort with
  clear_active=false) answered ok while its already-delivered result
  carried ended_turn=false. Under the buffers lock, error on settled:
  the call never silently no-ops.
- push_user_turn's toolResults arm was unreachable (tool results always
  follow their assistant turn through the transform); back to a plain
  push, doc comment trimmed to the observable user arm.
- tools.rs' batch-rule comment dropped the counterfactual narration;
  the runtime end_turn docstring states the cell-attribution rule
  (marks the cell executing when the request arrives).

* test(kernel): pin the end-turn host-request round trip

The kernel lane had zero pins: deleting the END_TURN dispatch, the flag
write, or the registry-first order left every suite green. Extend the
deterministic kernel round-trip test with an end_turn cell: it needs no
registration (the manager answers before the registry lookup), returns
{'endedTurn': True}, and the settled ExecuteResult carries ended_turn.
Mutation-checked: dispatch deleted -> the cell fails with the
unavailable-handler error; flag write deleted -> the settled-result
assert fails; restored -> green. Local venv python; no model call.

* fix(core): answer headless print and child collects with the ended batch's text

The opus-5.5 pass found a user-visible regression vs main: a turn ended
on rlm.end_turn() settles on the batch's toolResult, and the terminal
selections answered with nothing.

- select_headless_terminal_result steps back over the ended batch's
  toolResult rows to the assistant message that issued the calls, so
  prime-agent -p prints the answer it carries.
- handle_get_last_assistant_text reads the newest NON-EMPTY assistant
  text (the file fallback's rule): the final assistant row may hold
  only the tool call, and the answer sits in the last row with text,
  so rlm.collect shows the child's answer again.

Headless regression test: [user, assistant(text+toolCall), toolResult]
answers with the assistant text.

* test(kernel): pin the end-to-end end-turn arm

The kernel round-trip pin covered the dispatch and the settled flag;
the loop arm (ExecuteResult -> ipython terminate -> tool_bridge ->
batch rule) had no pin: reverting terminate at ipython.rs or
tool_bridge.rs still passed every suite.

end_turn_kernel.rs drives the full stack on a real kernel with the
ScriptedProvider (no model call): one python_repl turn runs
await rlm.end_turn(), a second scripted turn exists to catch the
revert, and the test asserts one provider call plus the 'turn ended'
tool result. Mutation-checked: terminate reverted at ipython.rs or
tool_bridge.rs -> RED (two provider calls); restored -> green.

Also from the pass: details.endedTurn had no reader anywhere (the
'turn ended' text already records the end), so the key is gone; the
start_host_request doc now states that the manager answers
rlm.end_turn itself.

* test(kernel): pin the ended turn's background-work ruling

The settled design ruling: end_turn returns the turn to the user;
background work keeps running; the turn fires when the batch's end-turn
call finishes Ok (the .any rule stays); the remaining jobs keep running
and their completion notices collect the results.

- The live-kernel verifier now starts a detached bash() job in the
  end-turn cell: the turn returns while the job still runs (no notice
  yet), and its [bash-done] completion row lands on the agent AFTER the
  turn returned, exit 0.
- core.md's rlm.end_turn bullet and the runtime end_turn docstring state
  the ruling verbatim in substance.

* test(daemon): pin the collect read's newest-non-empty rule

Cursor's thread on commands.rs: get_last_assistant_text's step over
empty assistant rows (58656a68b) landed with no regression test, so
reverting the filter failed no suite - the rlm.collect lane could
silently regress to answering nothing after a turn ended on
rlm.end_turn().

The worker test appends the ended turn's suffix to a created session's
store - the assistant row with the answer text, then the final
assistant row holding only the python_repl call - and asserts the read
returns the earlier row's text. Mutation-checked: filter reverted to
the pre-58656a68b find -> RED (reads "" where the answer sits); the
restored filter -> green.

* fix(core): headless answers with the ended batch's text when the end call was its own cell

---------

Co-authored-by: sethkarten <seth@karten.io>
2026-11-04 23:27:39 +01:00
.cargo send usage telemetry from the rust build (#3288) 2026-10-02 15:48:56 -07:00
.changes feat(kernel): rlm.end_turn() — the sanctioned terminal call (#3830) 2026-11-04 23:27:39 +01:00
.cursor bugbot: flag unwired scripts and tests, committed artifacts, and test bloat (#3817) 2026-10-29 21:15:14 -04:00
.github Render ipython edit diffs in the TUI (#3744) 2026-10-30 16:41:46 +01:00
crates feat(kernel): rlm.end_turn() — the sanctioned terminal call (#3830) 2026-11-04 23:27:39 +01:00
docs fix(mcp): log in to Google Workspace MCP servers with a user OAuth client (#3676) 2026-10-30 16:41:24 +01:00
packaging/homebrew prepare the homebrew-core release: src tarball, formula, update guard (#3372) 2026-10-06 23:07:05 -07:00
prime-agent-runtime feat(kernel): rlm.end_turn() — the sanctioned terminal call (#3830) 2026-11-04 23:27:39 +01:00
scripts count the cost of refinement, review, and side-question model calls (#3776) 2026-11-03 11:26:35 -05:00
skills [factory] pa-tui: factory view arrows walk the states that ran; Enter/→ opens the session (#3798) 2026-10-29 23:21:36 -04:00
vendor/crossterm keep a windows multi-line paste one paste instead of one message per line (#3755) 2026-10-29 11:14:36 +01:00
.gitignore Port Prime Agent to Rust (#2524) 2026-09-29 18:00:25 -07:00
AGENTS.md ci: restore changelog-fragment check (#3790) 2026-10-29 21:15:20 -04:00
Cargo.lock fix(rlm): agent_observe.recent_messages reads members no worker holds (#3778) 2026-10-29 21:15:00 -04:00
Cargo.toml release: v0.10.2 (#3789) 2026-10-28 00:30:39 -07:00
CHANGELOG.md release: v0.10.2 (#3789) 2026-10-28 00:30:39 -07:00
CONTRIBUTING.md promote the windows jobs and add the contributor trust gate (#3004) 2026-10-02 11:23:09 -07:00
deny.toml answer @ file completion from a per-session in-memory file index (#3505) 2026-10-12 12:57:54 -07:00
install-rust.sh clean up installer shellcheck warnings (#3301) 2026-10-29 11:14:09 +01:00
install.ps1 prepare prime agent 0.10.0 release (#3441) 2026-10-09 13:32:14 -07:00
LICENSE license: MIT with Prime Intellect - the TS product license restored + the Zechner attribution (the derivative notice) (#3173) 2026-09-29 18:27:22 -07:00
Makefile release: bundle the live catalog instead of the fixture snapshot (#3763) 2026-10-25 14:23:47 +01:00
README.md fix(mcp): log in to Google Workspace MCP servers with a user OAuth client (#3676) 2026-10-30 16:41:24 +01:00

Prime Intellect

Prime Agent: A Self-Improving RLM Harness

Documentation • Verifiers • PRIME-RL

CI arXiv

PrimeIntellect-ai%2Fprime-agent | Trendshift

Prime Agent is an open-source coding and research agent for general and long-running work. It is designed around two core abstractions:

  • The Recursive Language Model (RLM) treats context as variables (prompt-as-a-variable) and tools like recursive subagents as function calls (programmatic tool /sub-agent calling) inside a persistent REPL.
  • The Continual Harness stores supplemental prompts, memories, skill descriptions, and reusable subagent specifications as durable state that Prime Agent can refine through small, evidence-backed updates, local to the session by default.

Install

Install the latest stable version with the one-command installer:

curl -fsSL https://app.primeintellect.ai/prime-agent/install.sh | sh

On Windows, install from PowerShell:

irm https://app.primeintellect.ai/prime-agent/install.ps1 | iex

The Windows installer downloads the latest stable Windows x64 build and verifies its checksum before installation.

Why Prime Agent

Prime Agent combines a persistent Python control environment with durable harness state, so useful working context and reusable operating patterns can outlive a single chat window.

  • Everything is programmatic: a persistent Python REPL is the built-in model tool; file operations, shell commands, tool use, subagents, and context management happen through code.
  • Subagents are built in: rlm.spawn(...) spawns real child agents for parallel or background work and returns their results programmatically.
  • The harness can improve: /refine reviews the current trajectory and can apply small, evidence-backed updates to supplemental harness state. It never rewrites the immutable base system prompt, and recorded snapshots support rollback.
  • Skills are executable: skills are importable Python packages, and the built-in skill creator can turn recurring workflows into project or personal skills.
  • Sessions run in the background: daemon-backed agents keep running when the terminal disconnects and can be reattached later.
  • Agents communicate directly: running agents can exchange messages and orchestrate one another without routing everything through the user.
  • Long tasks keep moving: automatic compaction, persistent goals, heartbeats, schedules, autonomous mode, and retained subagents preserve progress across turns and terminal sessions.

Slack

Connect Slack from /mcp or run /mcp login slack inside Prime Agent to search your workspace and use Slack tools. See the Slack connector guide for OAuth setup and troubleshooting.

Google Drive and Google Sheets

Connect Google's official Drive and Sheets MCP servers with your own Google OAuth client: add them to mcpServers with clientId, clientSecret, callbackPort, and scopes, then run /mcp login <name>. See the Google Drive and Google Sheets guide for the Google Cloud setup and the tool mapping.

Clipboard over SSH and tmux

Prime Agent uses the local clipboard tool (such as pbcopy) when available. In tmux it asks the attached tmux client to forward a clipboard buffer if that client advertises the Ms capability. This works with tmux's default set-clipboard external, unlike application-origin OSC 52. Without tmux, remote copies request delivery using OSC 52. Terminal requests cannot confirm that your local clipboard changed, so Prime Agent reports them as unconfirmed.

Nested tmux servers and terminals may still block delivery. An outer tmux with set-clipboard external can reject the inner server's OSC 52. In that case, run tmux set -s set-clipboard on on the outer server, or use native terminal selection (Shift-drag in most terminals; Option-drag in iTerm2) followed by Cmd-C. macOS Terminal.app does not support OSC 52; iTerm2 requires its “Applications in terminal may access clipboard” setting.

Custom certificate authorities

HTTPS requests trust your operating system's trust store together with the bundled Mozilla roots. For servers behind a custom CA, point NODE_EXTRA_CA_CERTS at a PEM bundle of your CA certificates — the same variable the Node/TypeScript build honors:

export NODE_EXTRA_CA_CERTS=/path/to/your-ca.pem

Each transport reads the variable once when its first client is initialized. The background daemon keeps the environment it started with, so run prime-agent shutdown after changing it.

Getting Started

Start Prime Agent from the repository or directory you want it to work in:

cd /path/to/project
prime-agent

On first launch, run /login to choose a subscription or API-key provider. Prime Agent works in the current directory and can run commands and modify files there. Use a disposable clone, clean worktree, or another checkpoint you can inspect and restore.

Warning

Prime Agent executes model-generated Python and project commands with your user permissions. Its worker and kernel processes improve lifecycle isolation and recovery; they are not a security sandbox. Review changes and use trusted repositories, instructions, and skills only. Run untrusted code or instructions in an external sandbox or restricted environment.

Useful commands:

prime-agent agents                   # Browse running, idle, and saved sessions
prime-agent attach <agent>           # Reattach to a running session
prime-agent --resume [path|id]       # Browse sessions or resume one directly
prime-agent status                   # Inspect background service state
prime-agent doctor [--fix]           # Inspect or repair background services
prime-agent update [--force]         # Update Prime Agent
prime-agent shutdown [--force]       # Stop every agent, worker, and background service

Built for Long-Running Work

Prime Agent is built for long-running work, especially for evaluations in research. These features are available in the TUI, and when run autonomously.

  • Continual Harness: /refine can persist focused, reviewable lessons as supplemental prompts, memories, reusable skill descriptions, or subagent specifications, with recorded refinement history. It does not replace packaging and reviewing new executable skills.
  • Direct agent-to-agent communication: running agents and retained subagents can discover one another, exchange messages, and steer active work.
  • Daemon-backed continuity: active sessions, Python REPL state, schedules, and subagents keep running when the terminal detaches and can be reattached later.
  • Heartbeats and schedules: /heartbeat, rlm_heartbeat, and prime-agent schedule can re-enter a session periodically or at a specific time.
  • Persistent goals: /goal keeps an objective and its progress active across turns until it is completed, paused, or cleared.
  • Bounded autonomous mode: /autonomous continues within configured turn, token, and time budgets and can run user-defined quality gates. A passed gate checks only what that gate verifies; reaching a limit does not imply task success.

Contributing

Start with a GitHub Discussion for general questions, bug reports, and feature requests. Maintainers promote accepted work into Issues, and pull requests are reviewed from maintainers and vouched contributors.

License

Prime Agent is fully open source and released under the MIT License.

Citation

If you use this codebase in your research, please cite Prime Agent:

@article{karten2026prime,
  title={Prime Agent: A Self-Improving RLM Harness},
  author={Karten, Seth and Zhang, Alex L. and Thomas, Kevin and Müller, Sebastian and Bakouch, Elie and Auras, Daniel and Senghaas, Mika and Obeid, Fares and Dunas, Konstantin and Hagemann, Johannes and Jaghouar, Sami},
  journal={arXiv preprint arXiv:2608.23552},
  year={2026}
}

Available at https://arxiv.org/abs/2608.23552.

Automatic compaction settings

/settings exposes the global automatic compaction percentage in five-point steps, with a 95% default. A saved off-grid percentage stays selectable. The effective trigger also reserves room for retained history and summaries; the model's input and requested-output ceiling always takes priority. Manual compaction budgets are unchanged. The underlying compaction.thresholdPercent setting accepts 1–100, including fractional values; project settings can override the global setting.

For per-model early compaction, use the existing provider-scoped modelOverrides.<id>.contextWindow option in models.json, for example:

{
  "providers": {
    "prime-inference": {
      "modelOverrides": {
        "your-model-id": { "contextWindow": 524288 }
      }
    }
  }
}

Use the actual model ID for your provider. This sets that model's effective context window; it does not change every model's global compaction percentage.