rlmness-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rlmagent/__init__.py +1 -0
- rlmagent/__main__.py +8 -0
- rlmagent/acp/__init__.py +1 -0
- rlmagent/acp/runtime.py +71 -0
- rlmagent/acp/server.py +598 -0
- rlmagent/acp/updates.py +128 -0
- rlmagent/agentnet.py +237 -0
- rlmagent/agents/__init__.py +1 -0
- rlmagent/agents/approval.py +90 -0
- rlmagent/agents/bridge.py +109 -0
- rlmagent/agents/budget.py +212 -0
- rlmagent/agents/child_handles.py +46 -0
- rlmagent/agents/child_mailbox.py +30 -0
- rlmagent/agents/kernel_api.py +156 -0
- rlmagent/agents/output.py +34 -0
- rlmagent/agents/patch.py +107 -0
- rlmagent/agents/prompts.py +156 -0
- rlmagent/agents/trace.py +290 -0
- rlmagent/agents/tree.py +682 -0
- rlmagent/agents/wasm_api.py +105 -0
- rlmagent/agents/workspace.py +357 -0
- rlmagent/autopilot.py +447 -0
- rlmagent/backend.py +946 -0
- rlmagent/backend_context.py +74 -0
- rlmagent/cli/__init__.py +5 -0
- rlmagent/cli/config_cmd.py +177 -0
- rlmagent/cli/health.py +156 -0
- rlmagent/cli/main.py +1194 -0
- rlmagent/cli/update.py +188 -0
- rlmagent/clipboard.py +63 -0
- rlmagent/config/__init__.py +37 -0
- rlmagent/config/config.py +297 -0
- rlmagent/config/loader.py +119 -0
- rlmagent/config/prompts.py +124 -0
- rlmagent/config/secure_input.py +213 -0
- rlmagent/config/store.py +275 -0
- rlmagent/context/__init__.py +47 -0
- rlmagent/context/budget.py +557 -0
- rlmagent/contract.py +311 -0
- rlmagent/contract_run.py +232 -0
- rlmagent/conversation.py +2547 -0
- rlmagent/cron.py +77 -0
- rlmagent/directives.py +893 -0
- rlmagent/discovery.py +356 -0
- rlmagent/execution_log.py +66 -0
- rlmagent/factory.py +189 -0
- rlmagent/factory_graph.py +413 -0
- rlmagent/harness/__init__.py +63 -0
- rlmagent/harness/contracts/__init__.py +1 -0
- rlmagent/harness/contracts/stream.py +129 -0
- rlmagent/harness/contracts/tooling.py +127 -0
- rlmagent/harness/contracts/transcript/__init__.py +68 -0
- rlmagent/harness/contracts/transcript/_base.py +29 -0
- rlmagent/harness/contracts/transcript/diagnostics.py +52 -0
- rlmagent/harness/contracts/transcript/entries.py +180 -0
- rlmagent/harness/contracts/transcript/segments.py +64 -0
- rlmagent/harness/contracts/values.py +6 -0
- rlmagent/harness/driver.py +397 -0
- rlmagent/harness/engine.py +489 -0
- rlmagent/harness/mending.py +153 -0
- rlmagent/harness/provider/__init__.py +1 -0
- rlmagent/harness/provider/base.py +27 -0
- rlmagent/harness/provider/wire.py +189 -0
- rlmagent/harness/py.typed +0 -0
- rlmagent/harness/session/__init__.py +5 -0
- rlmagent/harness/session/index.py +218 -0
- rlmagent/harness/session/lease.py +66 -0
- rlmagent/harness/session/records.py +118 -0
- rlmagent/harness/session/replay.py +165 -0
- rlmagent/harness/session/store.py +155 -0
- rlmagent/harness/session/summary.py +46 -0
- rlmagent/hints.py +47 -0
- rlmagent/history.py +92 -0
- rlmagent/hooks/__init__.py +1 -0
- rlmagent/hooks/hooks.py +1 -0
- rlmagent/instructions.py +157 -0
- rlmagent/job_runner.py +117 -0
- rlmagent/jobs.py +365 -0
- rlmagent/kept_agents.py +397 -0
- rlmagent/kernel/__init__.py +5 -0
- rlmagent/kernel/session.py +205 -0
- rlmagent/kernel/wasm.py +169 -0
- rlmagent/logs.py +208 -0
- rlmagent/managed_release.py +368 -0
- rlmagent/mcp.py +411 -0
- rlmagent/mcp_oauth.py +213 -0
- rlmagent/model/__init__.py +29 -0
- rlmagent/model/_claude/__init__.py +1 -0
- rlmagent/model/_claude/courier.py +117 -0
- rlmagent/model/_claude/emitter.py +378 -0
- rlmagent/model/_oai/__init__.py +1 -0
- rlmagent/model/_oai/helpers.py +256 -0
- rlmagent/model/_oai/normalize.py +263 -0
- rlmagent/model/_oai/parsers.py +317 -0
- rlmagent/model/_oai/payloads.py +226 -0
- rlmagent/model/_oai/transport.py +127 -0
- rlmagent/model/claude.py +552 -0
- rlmagent/model/copilot.py +142 -0
- rlmagent/model/correlation.py +21 -0
- rlmagent/model/deadline.py +19 -0
- rlmagent/model/discovery.py +181 -0
- rlmagent/model/external/__init__.py +11 -0
- rlmagent/model/external/acp.py +202 -0
- rlmagent/model/external/acp_install.py +109 -0
- rlmagent/model/external/cli.py +248 -0
- rlmagent/model/external/textcalls.py +57 -0
- rlmagent/model/failover.py +129 -0
- rlmagent/model/google.py +199 -0
- rlmagent/model/limits.py +46 -0
- rlmagent/model/oai_compatible.py +414 -0
- rlmagent/model/preferences.py +56 -0
- rlmagent/model/py.typed +0 -0
- rlmagent/model/registered_oauth.py +195 -0
- rlmagent/model/scripted.py +55 -0
- rlmagent/model/settings.py +271 -0
- rlmagent/model/subscription.py +398 -0
- rlmagent/model/token_bounds.py +15 -0
- rlmagent/model/transport/__init__.py +1 -0
- rlmagent/model/transport/backoff.py +87 -0
- rlmagent/model/transport/client.py +87 -0
- rlmagent/model/transport/faults.py +65 -0
- rlmagent/model/transport/responses_socket.py +223 -0
- rlmagent/model/vertex.py +93 -0
- rlmagent/network.py +123 -0
- rlmagent/packages.py +246 -0
- rlmagent/planning.py +84 -0
- rlmagent/playbook/__init__.py +1 -0
- rlmagent/playbook/apply.py +106 -0
- rlmagent/playbook/digest.py +63 -0
- rlmagent/playbook/improver.py +272 -0
- rlmagent/playbook/plan.py +99 -0
- rlmagent/playbook/store.py +67 -0
- rlmagent/plugins.py +327 -0
- rlmagent/policy.py +105 -0
- rlmagent/prompt_refs.py +57 -0
- rlmagent/prompts.py +331 -0
- rlmagent/prune.py +60 -0
- rlmagent/py.typed +0 -0
- rlmagent/reasoning.py +135 -0
- rlmagent/refinement/__init__.py +1 -0
- rlmagent/refinement/controller.py +448 -0
- rlmagent/refinement/engine.py +220 -0
- rlmagent/refinement/evidence.py +131 -0
- rlmagent/refinement/scoped.py +90 -0
- rlmagent/refinement/store.py +193 -0
- rlmagent/refresh.py +202 -0
- rlmagent/routing.py +52 -0
- rlmagent/runtime.py +575 -0
- rlmagent/safety.py +267 -0
- rlmagent/session_html.py +57 -0
- rlmagent/session_import.py +52 -0
- rlmagent/session_share.py +55 -0
- rlmagent/sessions.py +249 -0
- rlmagent/shell_sandbox.py +88 -0
- rlmagent/skillset.py +320 -0
- rlmagent/steering.py +148 -0
- rlmagent/stops.py +62 -0
- rlmagent/subscriptions.py +219 -0
- rlmagent/tasks.py +98 -0
- rlmagent/toolhost.py +310 -0
- rlmagent/tools/__init__.py +56 -0
- rlmagent/tools/_shared.py +210 -0
- rlmagent/tools/files.py +462 -0
- rlmagent/tools/kernel.py +93 -0
- rlmagent/tools/patch_tool.py +68 -0
- rlmagent/tools/search.py +188 -0
- rlmagent/trust.py +383 -0
- rlmagent/tui/__init__.py +1 -0
- rlmagent/tui/adapter.py +506 -0
- rlmagent/tui/app.py +711 -0
- rlmagent/tui/approval_screen.py +57 -0
- rlmagent/tui/choice_screen.py +47 -0
- rlmagent/tui/completions.py +116 -0
- rlmagent/tui/components.py +25 -0
- rlmagent/tui/composer.py +40 -0
- rlmagent/tui/factory_view.py +86 -0
- rlmagent/tui/inspect.py +193 -0
- rlmagent/tui/messages.py +215 -0
- rlmagent/tui/model_screen.py +307 -0
- rlmagent/tui/prompt.py +98 -0
- rlmagent/tui/question_screen.py +52 -0
- rlmagent/tui/replay.py +141 -0
- rlmagent/tui/rlm_agent.tcss +405 -0
- rlmagent/tui/search_box.py +91 -0
- rlmagent/tui/sidebar.py +182 -0
- rlmagent/tui/status.py +49 -0
- rlmagent/tui/steps.py +283 -0
- rlmagent/tui/suggest.py +778 -0
- rlmagent/tui/transcript.py +172 -0
- rlmagent/tui/trust_dialog.py +58 -0
- rlmagent/tui/worker_attach.py +141 -0
- rlmagent/ui/__init__.py +1 -0
- rlmagent/ui/render.py +211 -0
- rlmagent/undo.py +109 -0
- rlmagent/utils/__init__.py +1 -0
- rlmagent/websearch.py +75 -0
- rlmagent/worker.py +1275 -0
- rlmagent/worker_service.py +203 -0
- rlmness_cli-0.2.0.dist-info/METADATA +882 -0
- rlmness_cli-0.2.0.dist-info/RECORD +203 -0
- rlmness_cli-0.2.0.dist-info/WHEEL +4 -0
- rlmness_cli-0.2.0.dist-info/entry_points.txt +2 -0
- rlmness_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
rlmagent/runtime.py
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
"""Shared session construction for every rlm-agent frontend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from rlmagent.conversation import CodingSession
|
|
12
|
+
from rlmagent.trust import Asker, Choice, TrustOutcome, TrustRequest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def build_session(ns: argparse.Namespace, *, session_id: str | None = None) -> CodingSession:
|
|
16
|
+
"""Resolve configuration and return a ready ``CodingSession``."""
|
|
17
|
+
from uuid import uuid4
|
|
18
|
+
|
|
19
|
+
from rlmagent.cli.main import _die, _sessions_dir
|
|
20
|
+
from rlmagent.harness.session.lease import SessionBusy, acquire
|
|
21
|
+
|
|
22
|
+
if getattr(ns, "no_session", False):
|
|
23
|
+
return await _build_session(ns)
|
|
24
|
+
session_id = getattr(ns, "resume", None) or session_id or uuid4().hex[:12]
|
|
25
|
+
try:
|
|
26
|
+
lease = acquire(_sessions_dir(getattr(ns, "session_dir", None)), session_id)
|
|
27
|
+
except (SessionBusy, ValueError) as exc:
|
|
28
|
+
_die(str(exc))
|
|
29
|
+
try:
|
|
30
|
+
session = await _build_session(ns, session_id=session_id)
|
|
31
|
+
except BaseException:
|
|
32
|
+
lease.close()
|
|
33
|
+
raise
|
|
34
|
+
session._writer_lease = lease
|
|
35
|
+
return session
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def _build_session(ns: argparse.Namespace, *, session_id: str | None = None) -> CodingSession:
|
|
39
|
+
from rlmagent.cli.main import (
|
|
40
|
+
_die,
|
|
41
|
+
_info,
|
|
42
|
+
_install_hooks,
|
|
43
|
+
_install_safety,
|
|
44
|
+
_load_extensions,
|
|
45
|
+
_load_prompt_templates,
|
|
46
|
+
_load_skills,
|
|
47
|
+
_load_tools,
|
|
48
|
+
_resolve_model,
|
|
49
|
+
_resolve_provider,
|
|
50
|
+
_resolve_provider_name,
|
|
51
|
+
_resolve_system,
|
|
52
|
+
_sessions_dir,
|
|
53
|
+
)
|
|
54
|
+
from rlmagent.config.loader import apply_config, load_model
|
|
55
|
+
from rlmagent.conversation import CodingSession
|
|
56
|
+
from rlmagent.agents.prompts import root_system
|
|
57
|
+
from rlmagent.safety import ApprovalPolicy
|
|
58
|
+
|
|
59
|
+
# Trust first: a trusted project's settings may choose the provider.
|
|
60
|
+
await settle_project_trust(ns, ask=getattr(ns, "trust_ask", None))
|
|
61
|
+
provider_name = _resolve_provider_name(getattr(ns, "provider", None))
|
|
62
|
+
# Export saved credentials for this process so the provider loaders
|
|
63
|
+
# (which read the environment) see them. Real env vars keep precedence.
|
|
64
|
+
apply_config(provider_name)
|
|
65
|
+
model = (
|
|
66
|
+
getattr(ns, "model", None)
|
|
67
|
+
or load_model(provider_name)
|
|
68
|
+
or _resolve_model(None, provider_name)
|
|
69
|
+
)
|
|
70
|
+
provider = _resolve_provider(getattr(ns, "provider", None))
|
|
71
|
+
|
|
72
|
+
verbose = getattr(ns, "verbose", False)
|
|
73
|
+
|
|
74
|
+
no_session = getattr(ns, "no_session", False)
|
|
75
|
+
sessions_dir = None if no_session else _sessions_dir(getattr(ns, "session_dir", None))
|
|
76
|
+
|
|
77
|
+
# The root agent and every sub-agent share one tree: one budget, one
|
|
78
|
+
# channel back from the kernels, one place that shuts them all down.
|
|
79
|
+
from rlmagent.tools._shared import set_workspace
|
|
80
|
+
|
|
81
|
+
set_workspace(os.getcwd())
|
|
82
|
+
from rlmagent.undo import Shadow
|
|
83
|
+
|
|
84
|
+
tree = make_agent_tree(ns, provider, provider_name, model, sessions_dir, cwd=os.getcwd(),
|
|
85
|
+
backend=getattr(ns, "engine", "session") == "rlmness")
|
|
86
|
+
from rlmagent.policy import DEFAULT_OUTPUT_BYTES, Policy, git_refusal
|
|
87
|
+
|
|
88
|
+
output_limit = getattr(ns, "max_tool_output", None)
|
|
89
|
+
policy = tree.policy = Policy(
|
|
90
|
+
allow_git=not getattr(ns, "deny_git", False),
|
|
91
|
+
max_tool_output_bytes=DEFAULT_OUTPUT_BYTES if output_limit is None else output_limit,
|
|
92
|
+
max_compactions=getattr(ns, "max_compactions", None),
|
|
93
|
+
agent_max_tokens=getattr(ns, "agent_max_tokens", None),
|
|
94
|
+
search_block=list(getattr(ns, "search_block", None) or []),
|
|
95
|
+
shell_access=getattr(ns, "shell_access", "workspace"),
|
|
96
|
+
shell_network=getattr(ns, "shell_network", False),
|
|
97
|
+
)
|
|
98
|
+
await tree.start()
|
|
99
|
+
kernel = tree.root_kernel()
|
|
100
|
+
raw_provider = provider
|
|
101
|
+
provider = tree.provider_for("root")
|
|
102
|
+
if getattr(ns, "engine", "session") == "rlmness":
|
|
103
|
+
from rlmagent.backend import BackendProvider
|
|
104
|
+
from rlmagent.model.external import with_external
|
|
105
|
+
provider = BackendProvider(with_external(raw_provider), provider, tree)
|
|
106
|
+
|
|
107
|
+
from rlmagent.plugins import (
|
|
108
|
+
as_tool, contributions, load_extensions, reload_extensions, rlm_agent_home,
|
|
109
|
+
)
|
|
110
|
+
from rlmagent.tasks import TaskList
|
|
111
|
+
|
|
112
|
+
# Harness functions every agent gets: cells call them, the session engine sees tools.
|
|
113
|
+
tasks = TaskList()
|
|
114
|
+
from pathlib import Path
|
|
115
|
+
|
|
116
|
+
from rlmagent.mcp import McpHub, load_configs
|
|
117
|
+
from rlmagent.trust import project_inputs_allowed
|
|
118
|
+
|
|
119
|
+
here = os.getcwd()
|
|
120
|
+
configs, problems = load_configs(rlm_agent_home(),
|
|
121
|
+
Path(here) if project_inputs_allowed(here) else None)
|
|
122
|
+
supplied = getattr(ns, "acp_mcp", {})
|
|
123
|
+
if conflicts := sorted(configs.keys() & supplied.keys()):
|
|
124
|
+
_die("Editor MCP names already configured: " + ", ".join(conflicts))
|
|
125
|
+
configs.update(supplied)
|
|
126
|
+
hub = McpHub(configs, here, tree.approver, problems)
|
|
127
|
+
from rlmagent.agentnet import AgentNet
|
|
128
|
+
|
|
129
|
+
net = AgentNet("", here)
|
|
130
|
+
from rlmagent.autopilot import Autopilot
|
|
131
|
+
|
|
132
|
+
pilot = Autopilot(cwd=here)
|
|
133
|
+
from rlmagent.websearch import make_web_search
|
|
134
|
+
|
|
135
|
+
built: dict = {} # the session, once it exists; these functions run only after that
|
|
136
|
+
|
|
137
|
+
async def context_usage() -> dict:
|
|
138
|
+
"""How full the conversation context is: used and limit in tokens, and percent."""
|
|
139
|
+
usage = built["session"].usage
|
|
140
|
+
limit = max(usage.context_limit, 1)
|
|
141
|
+
return {"used": usage.estimated_context_tokens, "limit": usage.context_limit,
|
|
142
|
+
"percent": round(100 * usage.estimated_context_tokens / limit)}
|
|
143
|
+
|
|
144
|
+
async def compact() -> str:
|
|
145
|
+
"""Summarize older conversation when this turn ends, to free context for more work."""
|
|
146
|
+
built["session"].compact_requested = True
|
|
147
|
+
return "older conversation will be summarized when this turn ends"
|
|
148
|
+
|
|
149
|
+
from rlmagent.agents.approval import Request, check
|
|
150
|
+
from rlmagent.history import make_history
|
|
151
|
+
from rlmagent.jobs import Jobs
|
|
152
|
+
|
|
153
|
+
async def _approve_job(command: str) -> None:
|
|
154
|
+
request = Request("run", command)
|
|
155
|
+
check(await tree.approver.approve(request), request)
|
|
156
|
+
|
|
157
|
+
async def factory_run(name: str) -> dict:
|
|
158
|
+
"""Run a named factory from the user's library after workspace approval."""
|
|
159
|
+
import shlex
|
|
160
|
+
|
|
161
|
+
from rlmagent.agents.workspace import Workspace
|
|
162
|
+
from rlmagent.factory import template
|
|
163
|
+
|
|
164
|
+
manifest = template(name)
|
|
165
|
+
if not manifest.is_file():
|
|
166
|
+
raise ValueError(f"no factory named {name!r}")
|
|
167
|
+
argv = [sys.executable, "-m", "rlmagent.cli.main", "factory", str(manifest)]
|
|
168
|
+
request = Request("run", shlex.join(argv))
|
|
169
|
+
check(await tree.approver.approve(request), request)
|
|
170
|
+
workspace = Workspace(here, tree.approver)
|
|
171
|
+
return await workspace._execute_argv(argv, timeout=1800)
|
|
172
|
+
|
|
173
|
+
from uuid import uuid4
|
|
174
|
+
session_id = getattr(ns, "resume", None) or session_id or uuid4().hex[:12]
|
|
175
|
+
jobs = Jobs(here, (sessions_dir or rlm_agent_home()) / "jobs" / session_id, _approve_job,
|
|
176
|
+
lambda command: git_refusal(command, policy),
|
|
177
|
+
shell_access=policy.shell_access, shell_network=policy.shell_network)
|
|
178
|
+
tools_at = getattr(ns, "tools_at", None)
|
|
179
|
+
if tools_at:
|
|
180
|
+
if getattr(ns, "engine", "session") != "rlmness":
|
|
181
|
+
_die("--tools-at needs the default engine (--engine rlmness).")
|
|
182
|
+
from rlmagent.toolhost import connect
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
tree.remote_tools = await connect(tools_at)
|
|
186
|
+
except (OSError, ValueError, ConnectionError) as exc:
|
|
187
|
+
_die(f"Cannot reach the tool host at {tools_at}: {exc}")
|
|
188
|
+
tree.host_functions = {"todo": tasks.todo, "mcp": hub.mcp, "mcp_tools": hub.mcp_tools,
|
|
189
|
+
**net.functions(), **pilot.functions(),
|
|
190
|
+
**jobs.functions(inbox=lambda: net.take()),
|
|
191
|
+
"context_usage": context_usage, "compact": compact,
|
|
192
|
+
"factory_run": factory_run,
|
|
193
|
+
"web_search": make_web_search(lambda: tree.network, policy),
|
|
194
|
+
"history": make_history(
|
|
195
|
+
lambda: getattr(getattr(built.get("session"), "_vault", None),
|
|
196
|
+
"path", None),
|
|
197
|
+
lambda: getattr(getattr(built.get("session"), "backend", None),
|
|
198
|
+
"event_path", None))}
|
|
199
|
+
builtin_tools = [as_tool(name, fn) for name, fn in tree.host_functions.items()]
|
|
200
|
+
extended: dict = {"tools": [], "functions": {}}
|
|
201
|
+
|
|
202
|
+
def _take_extensions(result) -> None:
|
|
203
|
+
for failure in result.failures:
|
|
204
|
+
print(f"rlm-agent: extension {failure.name!r} failed: {failure.error}", file=sys.stderr)
|
|
205
|
+
extra, functions = contributions(result)
|
|
206
|
+
extended["tools"] = [*extra, *(as_tool(name, fn) for name, fn in functions.items())]
|
|
207
|
+
extended["functions"] = functions
|
|
208
|
+
tree.extension_functions = functions
|
|
209
|
+
|
|
210
|
+
_load_extensions(verbose=verbose)
|
|
211
|
+
_take_extensions(load_extensions())
|
|
212
|
+
tools = [*_load_tools(verbose=verbose, kernel=kernel), *builtin_tools, *extended["tools"]]
|
|
213
|
+
skills = _load_skills(os.getcwd(), verbose=verbose)
|
|
214
|
+
system = root_system(
|
|
215
|
+
_resolve_system(getattr(ns, "system_prompt", None), tools=tools, skills=skills),
|
|
216
|
+
can_delegate=tree.allowance.max_depth > 0,
|
|
217
|
+
playbook=True,
|
|
218
|
+
sandboxed=tree.kernel_kind == "wasm",
|
|
219
|
+
network=tree.network,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def _tools_loader():
|
|
223
|
+
_take_extensions(reload_extensions())
|
|
224
|
+
return [*_load_tools(kernel=kernel), *builtin_tools, *extended["tools"]]
|
|
225
|
+
|
|
226
|
+
def _skills_loader():
|
|
227
|
+
return _load_skills(os.getcwd())
|
|
228
|
+
|
|
229
|
+
def _templates_loader():
|
|
230
|
+
return _load_prompt_templates(os.getcwd())
|
|
231
|
+
|
|
232
|
+
resume = getattr(ns, "resume", None)
|
|
233
|
+
if resume:
|
|
234
|
+
if sessions_dir is None:
|
|
235
|
+
_die("Cannot use --resume together with --no-session.")
|
|
236
|
+
problem = await _resume_folder_problem(ns, resume, sessions_dir)
|
|
237
|
+
if problem:
|
|
238
|
+
_die(problem)
|
|
239
|
+
try:
|
|
240
|
+
session = await CodingSession.resume(
|
|
241
|
+
resume,
|
|
242
|
+
provider=provider,
|
|
243
|
+
provider_name=provider_name,
|
|
244
|
+
model=model,
|
|
245
|
+
system=system,
|
|
246
|
+
tools=tools,
|
|
247
|
+
sessions_dir=sessions_dir,
|
|
248
|
+
tools_loader=_tools_loader,
|
|
249
|
+
skills_loader=_skills_loader,
|
|
250
|
+
templates_loader=_templates_loader,
|
|
251
|
+
keep_model=bool(getattr(ns, "model", None)),
|
|
252
|
+
)
|
|
253
|
+
except FileNotFoundError:
|
|
254
|
+
_die(f"Session not found: {resume}")
|
|
255
|
+
if verbose:
|
|
256
|
+
_info(f"Resumed {len(session.transcript)} messages from {resume}")
|
|
257
|
+
if session.interrupted:
|
|
258
|
+
_info("The last turn of this session did not finish; its work may be incomplete. "
|
|
259
|
+
"Send a message, or /continue, to carry on.")
|
|
260
|
+
else:
|
|
261
|
+
session = await CodingSession.create(
|
|
262
|
+
session_id=session_id,
|
|
263
|
+
provider=provider,
|
|
264
|
+
provider_name=provider_name,
|
|
265
|
+
model=model,
|
|
266
|
+
system=system,
|
|
267
|
+
tools=tools,
|
|
268
|
+
sessions_dir=sessions_dir,
|
|
269
|
+
tools_loader=_tools_loader,
|
|
270
|
+
skills_loader=_skills_loader,
|
|
271
|
+
templates_loader=_templates_loader,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if getattr(ns, "engine", "session") == "rlmness":
|
|
275
|
+
from rlmness.session import Session as BackendState
|
|
276
|
+
provider.instructions = _resolve_system(
|
|
277
|
+
getattr(ns, "system_prompt", None), tools=[], skills=skills)
|
|
278
|
+
if sessions_dir is not None:
|
|
279
|
+
provider.event_path = sessions_dir / f"{session.session_id}.backend.events.jsonl"
|
|
280
|
+
provider.state = BackendState.load(
|
|
281
|
+
sessions_dir / f"{session.session_id}.backend.json")
|
|
282
|
+
if getattr(ns, "engine", "session") == "rlmness":
|
|
283
|
+
session.backend = provider
|
|
284
|
+
elif sessions_dir is not None:
|
|
285
|
+
tree.event_path = sessions_dir / f"{session.session_id}.backend.events.jsonl"
|
|
286
|
+
session.agent_tree = tree
|
|
287
|
+
session.shadow = Shadow(session.cwd, rlm_agent_home())
|
|
288
|
+
session.load_undo()
|
|
289
|
+
session.tasks = tasks
|
|
290
|
+
session.mcp = hub
|
|
291
|
+
session.agentnet = net
|
|
292
|
+
session.jobs = jobs
|
|
293
|
+
jobs.subscriptions.start()
|
|
294
|
+
session.remote_tools = getattr(tree, "remote_tools", None)
|
|
295
|
+
session.policy = policy
|
|
296
|
+
built["session"] = session
|
|
297
|
+
session.autopilot = pilot
|
|
298
|
+
pilot.path = sessions_dir / f"{session.session_id}.goal.json" if sessions_dir else None
|
|
299
|
+
pilot.used_tokens = lambda: (session.usage.total_input_tokens
|
|
300
|
+
+ session.usage.total_output_tokens)
|
|
301
|
+
pilot.load()
|
|
302
|
+
net.me = f"s-{session.session_id[:8]}"
|
|
303
|
+
if sessions_dir is not None:
|
|
304
|
+
tree.tracer.persist(sessions_dir / f"{session.session_id}.requests.jsonl")
|
|
305
|
+
net.register()
|
|
306
|
+
|
|
307
|
+
def _progress(text: str) -> None:
|
|
308
|
+
backend = getattr(session, "backend", None)
|
|
309
|
+
if backend is not None and hasattr(backend, "record_event"):
|
|
310
|
+
backend.record_event({"kind": "progress", "text": text})
|
|
311
|
+
|
|
312
|
+
net.on_progress = _progress
|
|
313
|
+
tasks.attach(sessions_dir / f"{session.session_id}.tasks.json" if sessions_dir else None)
|
|
314
|
+
tree.attach(tree.root_id(), session)
|
|
315
|
+
from rlmagent.playbook.improver import Improver
|
|
316
|
+
from rlmagent.plugins import rlm_agent_home
|
|
317
|
+
from rlmagent.refinement.controller import project_folder
|
|
318
|
+
from rlmagent.refinement.evidence import EvidenceLog
|
|
319
|
+
|
|
320
|
+
session.evidence = EvidenceLog(
|
|
321
|
+
None if no_session else project_folder(rlm_agent_home(), session.cwd) / "evidence.jsonl"
|
|
322
|
+
)
|
|
323
|
+
local = sessions_dir / f"{session.session_id}.playbook.json" if sessions_dir else None
|
|
324
|
+
session.improver = tree.improver = Improver(
|
|
325
|
+
session, local_path=local, shared_path=rlm_agent_home() / "playbook.json"
|
|
326
|
+
)
|
|
327
|
+
session.improver.auto = getattr(ns, "auto_improve", True)
|
|
328
|
+
session.improver.hold_auto = getattr(ns, "review_auto_improve", True)
|
|
329
|
+
session.improver.every = getattr(ns, "improve_every", 5)
|
|
330
|
+
session.improver.cooldown = getattr(ns, "improve_cooldown", 900.0)
|
|
331
|
+
session.on_compacted = lambda: (tree.tracer.compacted("root"), session.improver.compacted())
|
|
332
|
+
session.improver.show_digest()
|
|
333
|
+
|
|
334
|
+
max_turns = getattr(ns, "max_turns", None)
|
|
335
|
+
if max_turns is not None:
|
|
336
|
+
session.harness.settings.max_turns = max_turns
|
|
337
|
+
|
|
338
|
+
_install_hooks(session.harness, verbose=verbose)
|
|
339
|
+
_install_safety(session.harness, ApprovalPolicy.AUTO)
|
|
340
|
+
from rlmagent.refinement.controller import attach
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
attach(session, rlm_agent_home(), persist=not no_session)
|
|
344
|
+
except Exception:
|
|
345
|
+
await session.shutdown()
|
|
346
|
+
raise
|
|
347
|
+
attach_logger(session)
|
|
348
|
+
session._apply_model_preferences()
|
|
349
|
+
return session
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
async def _resume_folder_problem(ns, session_id: str, sessions_dir) -> str | None:
|
|
353
|
+
"""Why this session must not resume here: it was started for another project."""
|
|
354
|
+
from pathlib import Path
|
|
355
|
+
|
|
356
|
+
from rlmagent.harness.session.index import SessionCatalog
|
|
357
|
+
|
|
358
|
+
meta = SessionCatalog(sessions_dir).get(session_id)
|
|
359
|
+
if meta is None or not meta.cwd or getattr(ns, "resume_anywhere", False):
|
|
360
|
+
return None
|
|
361
|
+
if Path(meta.cwd).resolve() == Path(os.getcwd()).resolve():
|
|
362
|
+
return None
|
|
363
|
+
text = f"Session {session_id} belongs to {meta.cwd}, not {os.getcwd()}."
|
|
364
|
+
if not _is_interactive(ns):
|
|
365
|
+
return f"{text} Run it from that folder, or pass --resume-anywhere."
|
|
366
|
+
import asyncio
|
|
367
|
+
|
|
368
|
+
answer = await asyncio.get_running_loop().run_in_executor(
|
|
369
|
+
None, input, f"{text} Resume it here anyway? [y/N] ")
|
|
370
|
+
return None if answer.strip().lower().startswith("y") else "Resume cancelled."
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def make_agent_tree(ns, provider, provider_name, model, sessions_dir, *, cwd, backend=False):
|
|
374
|
+
"""Build the tree of agents a run uses, with the limits from the command line."""
|
|
375
|
+
from rlmness import Allowance
|
|
376
|
+
|
|
377
|
+
from rlmagent.agents.prompts import child_first_message, child_system
|
|
378
|
+
from rlmagent.agents.tree import AgentTree
|
|
379
|
+
from rlmagent.cli.main import _resolve_system
|
|
380
|
+
|
|
381
|
+
call_limit = getattr(ns, "max_calls", None)
|
|
382
|
+
if call_limit is None:
|
|
383
|
+
call_limit = 50 if str(model).startswith("acp:") else 200
|
|
384
|
+
allowance = Allowance(
|
|
385
|
+
max_depth=getattr(ns, "max_depth", 2),
|
|
386
|
+
max_calls=call_limit,
|
|
387
|
+
max_cost=getattr(ns, "max_cost", 2.0),
|
|
388
|
+
max_live=getattr(ns, "max_live", 8),
|
|
389
|
+
max_seconds=getattr(ns, "max_seconds", None),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
from rlmagent.agents.approval import Approver
|
|
393
|
+
from rlmagent.kernel.wasm import available
|
|
394
|
+
|
|
395
|
+
kind = getattr(ns, "kernel", "local")
|
|
396
|
+
if kind == "wasm" and not available():
|
|
397
|
+
raise ValueError("The WebAssembly sandbox is not installed. Run `rlmness --setup` "
|
|
398
|
+
"or explicitly select --kernel local for host Python execution.")
|
|
399
|
+
network = getattr(ns, "network", True)
|
|
400
|
+
if getattr(ns, "auto_approve", False):
|
|
401
|
+
policy = "auto"
|
|
402
|
+
elif getattr(ns, "print_mode", False) or getattr(ns, "prompt", None) is not None:
|
|
403
|
+
policy = "deny"
|
|
404
|
+
else:
|
|
405
|
+
policy = "ask"
|
|
406
|
+
|
|
407
|
+
def system_for(node, tools):
|
|
408
|
+
base = _resolve_system(None, tools=tools, skills=[])
|
|
409
|
+
return child_system(base, node, node.depth < allowance.max_depth, sandboxed=kind == "wasm",
|
|
410
|
+
network=network)
|
|
411
|
+
|
|
412
|
+
if backend:
|
|
413
|
+
from rlmagent.backend_context import BackendContext
|
|
414
|
+
tree_type = BackendContext
|
|
415
|
+
else:
|
|
416
|
+
tree_type = AgentTree
|
|
417
|
+
tree = tree_type(
|
|
418
|
+
provider=provider,
|
|
419
|
+
provider_name=provider_name,
|
|
420
|
+
model=model,
|
|
421
|
+
cwd=cwd,
|
|
422
|
+
allowance=allowance,
|
|
423
|
+
sessions_dir=sessions_dir,
|
|
424
|
+
system_for=system_for,
|
|
425
|
+
first_message_for=child_first_message,
|
|
426
|
+
cell_timeout=getattr(ns, "cell_timeout", 300.0),
|
|
427
|
+
max_agents=getattr(ns, "max_agents", 50),
|
|
428
|
+
kernel=kind,
|
|
429
|
+
restore_kernel=getattr(ns, "restore_kernel", False),
|
|
430
|
+
approver=Approver(policy),
|
|
431
|
+
network=network,
|
|
432
|
+
)
|
|
433
|
+
tree.allow_private_fetch = getattr(ns, "allow_private_fetch", False)
|
|
434
|
+
import math
|
|
435
|
+
request_timeout = getattr(ns, "request_timeout", 300.0)
|
|
436
|
+
if not math.isfinite(request_timeout) or request_timeout <= 0:
|
|
437
|
+
raise ValueError("--request-timeout must be finite and positive")
|
|
438
|
+
tree.request_timeout = request_timeout
|
|
439
|
+
return tree
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _is_interactive(ns: argparse.Namespace) -> bool:
|
|
443
|
+
"""A human is at a terminal and this run is not one-shot or piped."""
|
|
444
|
+
import sys
|
|
445
|
+
|
|
446
|
+
if getattr(ns, "print_mode", False) or getattr(ns, "prompt", None) is not None:
|
|
447
|
+
return False
|
|
448
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
async def settle_project_trust(
|
|
452
|
+
ns: argparse.Namespace, *, ask: Asker | None = None,
|
|
453
|
+
) -> TrustOutcome:
|
|
454
|
+
"""Decide whether this folder's project inputs load, before any are read."""
|
|
455
|
+
from rlmagent.cli.main import _die, _info, _warn
|
|
456
|
+
from rlmagent.config.store import load_config
|
|
457
|
+
from rlmagent.trust import StartupCancelled, resolve_project_trust
|
|
458
|
+
|
|
459
|
+
override: bool | None = None
|
|
460
|
+
if getattr(ns, "approve", False):
|
|
461
|
+
override = True
|
|
462
|
+
elif getattr(ns, "no_approve", False):
|
|
463
|
+
override = False
|
|
464
|
+
if ask is None and _is_interactive(ns):
|
|
465
|
+
ask = ask_on_console
|
|
466
|
+
default = load_config().project_trust or "ask"
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
outcome = await resolve_project_trust(
|
|
470
|
+
os.getcwd(), override=override, default=default, ask=ask, # type: ignore[arg-type]
|
|
471
|
+
)
|
|
472
|
+
except StartupCancelled:
|
|
473
|
+
_die("No project trust decision made; exiting.")
|
|
474
|
+
if outcome.notice:
|
|
475
|
+
_warn(outcome.notice)
|
|
476
|
+
if not outcome.trusted and outcome.inputs is not None and not outcome.inputs.empty:
|
|
477
|
+
_info(
|
|
478
|
+
f"Project inputs not loaded ({outcome.inputs.describe()}): {outcome.reason}. "
|
|
479
|
+
"Run with --approve to load them this once."
|
|
480
|
+
)
|
|
481
|
+
return outcome
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def trust_choices(request: TrustRequest) -> list[tuple[Choice, str]]:
|
|
485
|
+
from rlmagent.trust import Choice
|
|
486
|
+
|
|
487
|
+
menu = [(Choice.TRUST_FOLDER, "Trust this folder")]
|
|
488
|
+
if request.parent is not None:
|
|
489
|
+
menu.append((Choice.TRUST_PARENT, f"Trust parent folder ({request.parent})"))
|
|
490
|
+
menu += [
|
|
491
|
+
(Choice.TRUST_ONCE, "Trust for this run only"),
|
|
492
|
+
(Choice.DISTRUST_FOLDER, "Do not trust this folder"),
|
|
493
|
+
(Choice.DISTRUST_ONCE, "Do not trust for this run only"),
|
|
494
|
+
]
|
|
495
|
+
return menu
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def trust_question(request: TrustRequest) -> str:
|
|
499
|
+
"""The explanation shown above the choices, shared by every frontend."""
|
|
500
|
+
lines = [
|
|
501
|
+
f"{request.folder} contains project inputs: {request.inputs.describe()}.",
|
|
502
|
+
"They shape the agent's instructions and can add plugins that run as code.",
|
|
503
|
+
"This controls project inputs; it is not a sandbox.",
|
|
504
|
+
]
|
|
505
|
+
if request.store_problem:
|
|
506
|
+
lines.append(f"Saved decisions are unavailable: {request.store_problem}")
|
|
507
|
+
return "\n".join(lines)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
async def ask_on_console(request: TrustRequest) -> Choice | None:
|
|
511
|
+
"""Ask on the terminal; EOF or Ctrl+C cancels."""
|
|
512
|
+
import asyncio
|
|
513
|
+
import sys
|
|
514
|
+
|
|
515
|
+
menu = trust_choices(request)
|
|
516
|
+
sys.stderr.write(trust_question(request) + "\n")
|
|
517
|
+
for number, (_choice, label) in enumerate(menu, start=1):
|
|
518
|
+
sys.stderr.write(f" {number}. {label}\n")
|
|
519
|
+
loop = asyncio.get_running_loop()
|
|
520
|
+
while True:
|
|
521
|
+
try:
|
|
522
|
+
answer = await loop.run_in_executor(None, input, f"Choose 1-{len(menu)}: ")
|
|
523
|
+
except (EOFError, KeyboardInterrupt):
|
|
524
|
+
return None
|
|
525
|
+
answer = answer.strip()
|
|
526
|
+
if answer.isdigit() and 1 <= int(answer) <= len(menu):
|
|
527
|
+
return menu[int(answer) - 1][0]
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def attach_logger(session: CodingSession) -> None:
|
|
531
|
+
"""Subscribe a ``RunLogger`` to the session's event stream."""
|
|
532
|
+
from rlmagent.logs import RunLogger
|
|
533
|
+
|
|
534
|
+
logger = RunLogger.create(
|
|
535
|
+
session.session_id,
|
|
536
|
+
provider=session.provider_name,
|
|
537
|
+
model=session.model,
|
|
538
|
+
cwd=session.cwd,
|
|
539
|
+
)
|
|
540
|
+
if logger is None:
|
|
541
|
+
return
|
|
542
|
+
logger.stats_source = lambda: session.usage
|
|
543
|
+
session.harness.on_event(logger.record_event)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def git_branch(cwd: str | None = None) -> str | None:
|
|
547
|
+
"""Current git branch name, or ``None`` outside a repository."""
|
|
548
|
+
import subprocess
|
|
549
|
+
|
|
550
|
+
try:
|
|
551
|
+
out = subprocess.run(
|
|
552
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
553
|
+
cwd=cwd or os.getcwd(),
|
|
554
|
+
capture_output=True,
|
|
555
|
+
text=True,
|
|
556
|
+
timeout=2,
|
|
557
|
+
encoding="utf-8",
|
|
558
|
+
errors="replace",
|
|
559
|
+
)
|
|
560
|
+
except (OSError, subprocess.SubprocessError):
|
|
561
|
+
return None
|
|
562
|
+
if out.returncode != 0:
|
|
563
|
+
return None
|
|
564
|
+
branch = out.stdout.strip()
|
|
565
|
+
return branch or None
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
__all__ = [
|
|
569
|
+
"ask_on_console",
|
|
570
|
+
"build_session",
|
|
571
|
+
"git_branch",
|
|
572
|
+
"settle_project_trust",
|
|
573
|
+
"trust_choices",
|
|
574
|
+
"trust_question",
|
|
575
|
+
]
|