Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ no standalone entry point: outside a session the call raises.

All runtime configuration enters through the `ai.prime.rlm/runtime-v1` contract object
(model, provider credentials, execution policy, prompt configuration, skills, kernel
environment, search credential). Recursive children inherit the parent's configuration
environment, search credential). Prompt configuration is role-aware: optional
`subagent_append_to_system_prompt` (nodes that can still recurse) and
`leaf_append_to_system_prompt` (depth == max_depth) override `append_to_system_prompt`
for sub-agents, each falling back to the next-more-general tier. Recursive children inherit the parent's configuration
in-memory (`model_copy`); nothing is re-read from the process environment.

The process environment configures only process infrastructure:
Expand Down
4 changes: 4 additions & 0 deletions src/rlm/acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ class _RuntimeMetadata(_ContractModel):
policy: ExecutionPolicy
system_prompt_path: str | None
append_to_system_prompt: str | None
subagent_append_to_system_prompt: str | None = None
leaf_append_to_system_prompt: str | None = None
skills: list[Annotated[str, Field(min_length=1)]]
kernel_env: dict[str, str]
search_api_key: str | None
Expand Down Expand Up @@ -201,6 +203,8 @@ def _runtime_config(meta_kwargs: Any) -> tuple[RuntimeConfig, str]:
policy=payload.policy,
system_prompt_path=payload.system_prompt_path,
append_to_system_prompt=payload.append_to_system_prompt,
subagent_append_to_system_prompt=payload.subagent_append_to_system_prompt,
leaf_append_to_system_prompt=payload.leaf_append_to_system_prompt,
Comment thread
cursor[bot] marked this conversation as resolved.
skills=tuple(payload.skills),
kernel_env=tuple(payload.kernel_env.items()),
search_api_key=payload.search_api_key,
Expand Down
25 changes: 25 additions & 0 deletions src/rlm/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,31 @@ class RuntimeConfig(_ConfigModel):
policy: ExecutionPolicy
system_prompt_path: str | None = None
append_to_system_prompt: str | None = None
subagent_append_to_system_prompt: str | None = None
leaf_append_to_system_prompt: str | None = None
skills: tuple[str, ...] = ()
kernel_env: tuple[tuple[str, str], ...] = Field(default=(), repr=False)
search_api_key: str | None = Field(default=None, repr=False)

@property
def resolved_append_to_system_prompt(self) -> str | None:
"""The append for this engine's role in the session tree.

root (depth 0) -> append_to_system_prompt
node (depth >= 1, can still recurse) -> subagent_append_to_system_prompt
leaf (depth == max_depth, cannot recurse) -> leaf_append_to_system_prompt

Each tier falls back to the next-more-general one (leaf -> subagent -> root),
so any unset append preserves the prior single-append behavior. Mirrors the
allow_recursion gating that build_system_prompt uses for the built-in rlm hint.
"""
if self.invocation.depth == 0:
return self.append_to_system_prompt
if (
self.invocation.depth >= self.policy.max_depth
and self.leaf_append_to_system_prompt is not None
):
return self.leaf_append_to_system_prompt
if self.subagent_append_to_system_prompt is not None:
return self.subagent_append_to_system_prompt
return self.append_to_system_prompt
4 changes: 3 additions & 1 deletion src/rlm/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def __init__(
self.summarize_at_tokens = config.policy.summarize_at_tokens
self.max_compactions = config.policy.max_compactions
self.system_prompt_path = config.system_prompt_path
self.append_to_system_prompt = config.append_to_system_prompt
self.append_to_system_prompt = config.resolved_append_to_system_prompt
self.max_depth = config.policy.max_depth
self.depth = config.invocation.depth
self.allow_git = config.policy.allow_git
Expand Down Expand Up @@ -1001,6 +1001,8 @@ def _load_system_prompt(self, active_tools: list[BuiltinTool]) -> str:
self.cwd,
str(SKILLS_DIR) if SKILLS_DIR is not None else None,
discover_skills(self.session.dir),
depth=self.depth,
session_dir=str(self.session.dir),
allow_recursion=self.depth < self.max_depth,
allow_git=self.allow_git,
active_tools=active_tools,
Expand Down
52 changes: 37 additions & 15 deletions src/rlm/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
"Run shell commands with `%%bash` as the very first line of a code cell "
"(no comments, imports, or statements before it). " + PROJECT_ENV_PROMPT
)
KERNEL_PACKAGES_PROMPT = (
"Pre-installed in the kernel venv: " + ", ".join(BASE_TOOLKIT) + ". "
"Install extra packages with `!uv pip install <pkg>` in a code cell — that "
"targets the kernel venv (a uv-managed venv with no pip module)."
)
BASH_SKILL_PROMPT = (
"Run shell with `out = await bash('''command here''')` — always "
"triple-quote the command so shell quotes and multi-line scripts never "
Expand Down Expand Up @@ -85,22 +90,31 @@ def build_system_prompt(
skills_dir: str | None,
installed_skills: list[str],
*,
depth: int = 0,
session_dir: str | None = None,
allow_recursion: bool,
allow_git: bool,
active_tools: list[BuiltinTool],
shell_skills: list[str] | None = None,
) -> str:
"""Build the system prompt.

Layout: role → environment (cwd, log path, skills) → capabilities
(recursion) → tool API. Keep it tight: the model also receives the
per-tool schemas, so redundant tool guidance here just inflates
Layout: role → environment (cwd, log path, skills, kernel venv) →
capabilities (recursion) → guards. Keep it tight: the model also receives
the per-tool schemas, so redundant tool guidance here just inflates
every request.
"""
has_bash = _has_tool(active_tools, "bash")
has_edit = _has_tool(active_tools, "edit")
has_ipython = _has_tool(active_tools, "ipython")
role = "You are a coding agent."
if depth > 0:
role = (
"You are a coding agent, spawned as a sub-agent: your caller "
"delegated a single task to you and sees none of your work. Do "
"exactly that task; don't widen the scope."
)
else:
role = "You are a coding agent."
if has_bash:
role += " You have access to a bash tool for running shell commands."
if has_edit:
Expand All @@ -113,14 +127,22 @@ def build_system_prompt(
" You also have an ipython tool: a persistent Python REPL "
"(variables, imports, and function definitions persist across calls)."
)
if depth > 0:
done_line = (
"When the task is done, stop calling tools and state your final "
"answer. It is the only thing your caller receives, so make it a "
"complete, self-contained result — the answer plus the evidence "
"needed to trust it (sources, file paths, values)."
)
else:
done_line = "When you are done, stop calling tools and state your final answer."
log_dir = session_dir or "$RLM_SESSION_DIR"
parts: list[str] = [
role,
"When you are done, stop calling tools and state your final answer.",
done_line,
"",
f"Working directory: {cwd}",
"Conversation log: $RLM_SESSION_DIR/messages.jsonl",
f"Pre-installed Python packages: {', '.join(BASE_TOOLKIT)}.",
"Install additional packages with `uv pip install <pkg>` (this is a uv-managed venv with no pip module).",
f"Conversation log: {log_dir}/messages.jsonl",
]

skill_lines: list[str] = []
Expand All @@ -132,8 +154,8 @@ def build_system_prompt(
installed = ", ".join(f"`{skill}`" for skill in installed_skills)
skill_lines.append(f"Installed skills (pre-imported): {installed}.")
skill_lines.append(
"Each skill is an async function by the same name. "
"Inspect with `help(<skill>)` or `inspect.signature(<skill>.run)`."
"Each skill is an async function by the same name; "
"inspect one with `help(<skill>)`."
)
shell_skill_set = set(shell_skills or [])
if shell_skill_set:
Expand All @@ -150,6 +172,11 @@ def build_system_prompt(
if skill_lines:
parts.extend(["", *skill_lines])

if has_ipython and not has_bash and "bash" not in (installed_skills or []):
parts.extend(["", IPYTHON_CONTROL_PROMPT, KERNEL_PACKAGES_PROMPT])
elif has_ipython:
parts.extend(["", PROJECT_ENV_PROMPT, KERNEL_PACKAGES_PROMPT])

if allow_recursion:
parts.extend(
[
Expand All @@ -159,11 +186,6 @@ def build_system_prompt(
]
)

if has_ipython and not has_bash and "bash" not in (installed_skills or []):
parts.extend(["", IPYTHON_CONTROL_PROMPT])
elif has_ipython:
parts.extend(["", PROJECT_ENV_PROMPT])

if _should_include_git_history_guard(active_tools, allow_git):
parts.extend(["", GIT_HISTORY_GUARD_PROMPT])

Expand Down
2 changes: 1 addition & 1 deletion src/rlm/tools/ipython.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ def _inject_startup(self):
skill_names = discover_skills(self.session.dir if self.session else None)

setup_code = f"""\
import os, sys, types, json, time, functools, inspect
import os, sys, asyncio, types, json, time, functools, inspect
os.chdir({self.cwd!r})
if {bool(session_dir)!r}:
sys.path.append({session_dir!r})
Expand Down
Loading