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/cli/update.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Upgrade an installed application using its package manager."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import importlib.metadata
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
15
|
+
from packaging.version import InvalidVersion, Version
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def select_release(document: dict, channel: str) -> str:
|
|
19
|
+
"""Select a published, non-yanked release from the requested channel."""
|
|
20
|
+
candidates = []
|
|
21
|
+
current_python = ".".join(str(part) for part in sys.version_info[:3])
|
|
22
|
+
for text, files in document.get("releases", {}).items():
|
|
23
|
+
try:
|
|
24
|
+
version = Version(text)
|
|
25
|
+
except InvalidVersion:
|
|
26
|
+
continue
|
|
27
|
+
eligible = {
|
|
28
|
+
"stable": not version.is_prerelease and not version.is_devrelease,
|
|
29
|
+
"preview": version.is_prerelease and not version.is_devrelease,
|
|
30
|
+
"nightly": version.is_devrelease,
|
|
31
|
+
}[channel]
|
|
32
|
+
if not eligible:
|
|
33
|
+
continue
|
|
34
|
+
for artifact in files:
|
|
35
|
+
if artifact.get("yanked", False):
|
|
36
|
+
continue
|
|
37
|
+
try:
|
|
38
|
+
supported = SpecifierSet(artifact.get("requires_python") or "")
|
|
39
|
+
except InvalidSpecifier:
|
|
40
|
+
continue
|
|
41
|
+
if supported.contains(current_python, prereleases=True):
|
|
42
|
+
candidates.append(version)
|
|
43
|
+
break
|
|
44
|
+
if not candidates:
|
|
45
|
+
raise ValueError(f"No compatible, non-yanked {channel} release is published")
|
|
46
|
+
return str(max(candidates))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run(argv: list[str]) -> int:
|
|
50
|
+
if argv[:1] == ["--run"]:
|
|
51
|
+
from rlmagent.managed_release import run as managed_run
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
return managed_run(argv[1:])
|
|
55
|
+
except (OSError, ValueError) as error:
|
|
56
|
+
print(str(error), file=sys.stderr)
|
|
57
|
+
return 1
|
|
58
|
+
parser = argparse.ArgumentParser(prog="rlm-agent update")
|
|
59
|
+
parser.add_argument("--manifest", type=Path, help="Verified offline wheel-bundle manifest")
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--managed", action="store_true", help="Use isolated installations and atomic activation"
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument("--drain", action="store_true", help="Let workers finish before activation")
|
|
64
|
+
parser.add_argument("--wait", type=float, default=60)
|
|
65
|
+
parser.add_argument("--restart-workers", action="store_true")
|
|
66
|
+
parser.add_argument("--apply", action="store_true", help="Run the upgrade command.")
|
|
67
|
+
choice = parser.add_mutually_exclusive_group()
|
|
68
|
+
choice.add_argument("--check", action="store_true", help="Compare against the package index.")
|
|
69
|
+
choice.add_argument("--version", dest="target", help="Install a specific package version.")
|
|
70
|
+
choice.add_argument(
|
|
71
|
+
"--rollback", action="store_true", help="Reinstall the previously recorded version."
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument("--channel", choices=("stable", "preview", "nightly"), default="stable")
|
|
74
|
+
args = parser.parse_args(argv)
|
|
75
|
+
if args.managed or args.manifest is not None:
|
|
76
|
+
if args.target is not None or args.check or (args.manifest is not None and args.rollback):
|
|
77
|
+
parser.error(
|
|
78
|
+
"Use one managed manifest or --managed --rollback; omit --version and --check"
|
|
79
|
+
)
|
|
80
|
+
from rlmagent import managed_release
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
if not args.apply:
|
|
84
|
+
if args.manifest is not None:
|
|
85
|
+
print(json.dumps(managed_release.manifest(args.manifest), indent=2))
|
|
86
|
+
print("Use --apply to install or roll back the reviewed managed release.")
|
|
87
|
+
return 0
|
|
88
|
+
if args.rollback:
|
|
89
|
+
record = managed_release.rollback(
|
|
90
|
+
drain=args.drain, wait=args.wait, restart=args.restart_workers
|
|
91
|
+
)
|
|
92
|
+
elif args.manifest is not None:
|
|
93
|
+
record = managed_release.install(
|
|
94
|
+
args.manifest, drain=args.drain, wait=args.wait, restart=args.restart_workers
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError("Managed installation needs --manifest, or --rollback")
|
|
98
|
+
print(f"Active managed release: {record['version']} ({record['channel']})")
|
|
99
|
+
return 0
|
|
100
|
+
except (OSError, ValueError, subprocess.SubprocessError) as error:
|
|
101
|
+
print(f"Managed update failed: {error}", file=sys.stderr)
|
|
102
|
+
return 1
|
|
103
|
+
if args.drain or args.restart_workers:
|
|
104
|
+
parser.error("Worker drain and restart require a managed installation")
|
|
105
|
+
try:
|
|
106
|
+
package = importlib.metadata.distribution("rlmness-cli")
|
|
107
|
+
except importlib.metadata.PackageNotFoundError:
|
|
108
|
+
parser.exit(1, "rlmagent must be installed to update it.\n")
|
|
109
|
+
if package.read_text("direct_url.json"):
|
|
110
|
+
parser.exit(1, "Update this local or URL installation from its original source.\n")
|
|
111
|
+
|
|
112
|
+
from rlmagent.plugins import rlm_agent_home
|
|
113
|
+
|
|
114
|
+
receipt = rlm_agent_home() / "updates" / "previous.json"
|
|
115
|
+
target = args.target
|
|
116
|
+
if args.rollback:
|
|
117
|
+
try:
|
|
118
|
+
target = json.loads(receipt.read_text(encoding="utf-8"))["version"]
|
|
119
|
+
except (OSError, ValueError, KeyError):
|
|
120
|
+
parser.exit(1, "No successful earlier upgrade has been recorded.\n")
|
|
121
|
+
if target is not None and (
|
|
122
|
+
not isinstance(target, str) or not re.fullmatch(r"[0-9][A-Za-z0-9.!+_-]*", target)
|
|
123
|
+
):
|
|
124
|
+
parser.error("invalid package version")
|
|
125
|
+
if args.check or (args.channel != "stable" and target is None):
|
|
126
|
+
import os
|
|
127
|
+
|
|
128
|
+
if os.environ.get("RLM_AGENT_OFFLINE") == "1":
|
|
129
|
+
parser.exit(1, "Update checks are disabled in offline startup mode.\n")
|
|
130
|
+
from rlmagent.model.transport.client import fetch_json
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
latest = select_release(
|
|
134
|
+
fetch_json("https://pypi.org/pypi/rlmness-cli/json", timeout=15), args.channel
|
|
135
|
+
)
|
|
136
|
+
except ValueError as error:
|
|
137
|
+
parser.exit(1, str(error) + "\n")
|
|
138
|
+
except Exception:
|
|
139
|
+
parser.exit(1, "Could not check the package index.\n")
|
|
140
|
+
if args.check:
|
|
141
|
+
print(f"Installed rlmagent {package.version}; index {args.channel} release {latest}")
|
|
142
|
+
return 0
|
|
143
|
+
target = latest
|
|
144
|
+
|
|
145
|
+
prefix = Path(sys.prefix)
|
|
146
|
+
if (prefix / "uv-receipt.toml").is_file():
|
|
147
|
+
command = ["uv", "tool", "upgrade", "rlmness-cli"]
|
|
148
|
+
elif (prefix / "pipx_metadata.json").is_file():
|
|
149
|
+
command = ["pipx", "upgrade", "rlmness-cli"]
|
|
150
|
+
elif (package.read_text("INSTALLER") or "").strip() == "uv":
|
|
151
|
+
command = ["uv", "pip", "install", "--python", sys.executable, "--upgrade", "rlmness-cli"]
|
|
152
|
+
else:
|
|
153
|
+
command = [sys.executable, "-m", "pip", "install", "--upgrade", "rlmness-cli"]
|
|
154
|
+
|
|
155
|
+
if target is not None:
|
|
156
|
+
spec = f"rlmness-cli=={target}"
|
|
157
|
+
if command[:3] == ["uv", "tool", "upgrade"]:
|
|
158
|
+
command = ["uv", "tool", "install", "--force", spec]
|
|
159
|
+
elif command[:2] == ["pipx", "upgrade"]:
|
|
160
|
+
command = ["pipx", "install", "--force", spec]
|
|
161
|
+
else:
|
|
162
|
+
command[-1] = spec
|
|
163
|
+
if args.channel in ("preview", "nightly"):
|
|
164
|
+
if command[0] != "pipx":
|
|
165
|
+
command.extend(["--prerelease", "allow"] if command[0] == "uv" else ["--pre"])
|
|
166
|
+
|
|
167
|
+
print(f"Installed rlmagent {package.version}", flush=True)
|
|
168
|
+
if not args.apply:
|
|
169
|
+
print("Run rlm-agent update --apply with the same options to install the requested update.")
|
|
170
|
+
return 0
|
|
171
|
+
executable = shutil.which(command[0])
|
|
172
|
+
if executable is None:
|
|
173
|
+
parser.exit(1, f"The installer {command[0]} is missing from PATH.\n")
|
|
174
|
+
try:
|
|
175
|
+
status = subprocess.run([executable, *command[1:]], check=False).returncode
|
|
176
|
+
if status == 0:
|
|
177
|
+
receipt.parent.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
from uuid import uuid4
|
|
179
|
+
|
|
180
|
+
staged = receipt.with_suffix(f".{uuid4().hex}.tmp")
|
|
181
|
+
try:
|
|
182
|
+
staged.write_text(json.dumps({"version": package.version}), encoding="utf-8")
|
|
183
|
+
staged.replace(receipt)
|
|
184
|
+
finally:
|
|
185
|
+
staged.unlink(missing_ok=True)
|
|
186
|
+
return status
|
|
187
|
+
except OSError as exc:
|
|
188
|
+
parser.exit(1, f"Could not start the installer: {exc}\n")
|
rlmagent/clipboard.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Native clipboard writes with explicit terminal-delivery fallback."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def sequence(text: str, *, tmux=False) -> str:
|
|
11
|
+
encoded = base64.b64encode(text.encode()).decode()
|
|
12
|
+
value = f"\x1b]52;c;{encoded}\x07"
|
|
13
|
+
return "\x1bPtmux;" + value.replace("\x1b", "\x1b\x1b") + "\x1b\\" if tmux else value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def copy(text: str, terminal_write) -> str:
|
|
17
|
+
if len(text.encode()) > 1024 * 1024:
|
|
18
|
+
raise ValueError("Clipboard content exceeds the 1 MiB limit")
|
|
19
|
+
remote = bool(
|
|
20
|
+
os.environ.get("SSH_CONNECTION") or os.environ.get("SSH_TTY") or os.environ.get("TMUX")
|
|
21
|
+
)
|
|
22
|
+
command = None
|
|
23
|
+
if not remote:
|
|
24
|
+
if sys.platform == "win32":
|
|
25
|
+
shell = shutil.which("powershell.exe")
|
|
26
|
+
if shell:
|
|
27
|
+
command = [
|
|
28
|
+
shell,
|
|
29
|
+
"-NoProfile",
|
|
30
|
+
"-NonInteractive",
|
|
31
|
+
"-Command",
|
|
32
|
+
"[Console]::InputEncoding = [Text.UTF8Encoding]::new(); "
|
|
33
|
+
"Set-Clipboard -Value ([Console]::In.ReadToEnd())",
|
|
34
|
+
]
|
|
35
|
+
else:
|
|
36
|
+
for candidate in (
|
|
37
|
+
("pbcopy",) if sys.platform == "darwin" else ("wl-copy", "xclip", "xsel")
|
|
38
|
+
):
|
|
39
|
+
executable = shutil.which(candidate)
|
|
40
|
+
if executable:
|
|
41
|
+
command = [executable]
|
|
42
|
+
if candidate == "xclip":
|
|
43
|
+
command += ["-selection", "clipboard"]
|
|
44
|
+
elif candidate == "xsel":
|
|
45
|
+
command += ["--clipboard", "--input"]
|
|
46
|
+
break
|
|
47
|
+
if command is not None:
|
|
48
|
+
try:
|
|
49
|
+
result = subprocess.run(
|
|
50
|
+
command,
|
|
51
|
+
input=text.encode(),
|
|
52
|
+
stdout=subprocess.DEVNULL,
|
|
53
|
+
stderr=subprocess.DEVNULL,
|
|
54
|
+
timeout=5,
|
|
55
|
+
check=False,
|
|
56
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0,
|
|
57
|
+
)
|
|
58
|
+
if result.returncode == 0:
|
|
59
|
+
return "Copied to the native clipboard."
|
|
60
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
61
|
+
pass
|
|
62
|
+
terminal_write(sequence(text, tmux=bool(os.environ.get("TMUX"))))
|
|
63
|
+
return "Clipboard delivery requested from the terminal; delivery is unconfirmed."
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""rlm-agent config: application configuration and provider selection."""
|
|
2
|
+
|
|
3
|
+
from rlmagent.config.config import (
|
|
4
|
+
list_providers,
|
|
5
|
+
resolve_provider,
|
|
6
|
+
select_provider,
|
|
7
|
+
setup_provider,
|
|
8
|
+
)
|
|
9
|
+
from rlmagent.config.loader import (
|
|
10
|
+
apply_config,
|
|
11
|
+
load_api_key,
|
|
12
|
+
load_model,
|
|
13
|
+
load_provider,
|
|
14
|
+
)
|
|
15
|
+
from rlmagent.config.store import (
|
|
16
|
+
RlmAgentConfig,
|
|
17
|
+
config_path,
|
|
18
|
+
load_config,
|
|
19
|
+
mask_key,
|
|
20
|
+
save_config,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"RlmAgentConfig",
|
|
25
|
+
"apply_config",
|
|
26
|
+
"config_path",
|
|
27
|
+
"list_providers",
|
|
28
|
+
"load_api_key",
|
|
29
|
+
"load_config",
|
|
30
|
+
"load_model",
|
|
31
|
+
"load_provider",
|
|
32
|
+
"mask_key",
|
|
33
|
+
"resolve_provider",
|
|
34
|
+
"save_config",
|
|
35
|
+
"select_provider",
|
|
36
|
+
"setup_provider",
|
|
37
|
+
]
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Provider resolution, catalog, and environment-based auto-detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import replace
|
|
8
|
+
|
|
9
|
+
from rlmagent.harness.provider.base import ModelProvider
|
|
10
|
+
from rlmagent.model.settings import (
|
|
11
|
+
AnthropicProfile,
|
|
12
|
+
ConfigError,
|
|
13
|
+
OpenAIProfile,
|
|
14
|
+
load_anthropic_profile,
|
|
15
|
+
load_openai_profile,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Known provider identifiers
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
_KNOWN_PROVIDERS = ("anthropic", "openai", "openrouter", "ollama", "huggingface", "scripted", "cli", "vertex",
|
|
23
|
+
"google", "mistral", "azure", "codex", "copilot", "claude-subscription", "xai", "xai-subscription")
|
|
24
|
+
|
|
25
|
+
# OpenAI-compatible backends reuse the OpenAI adapter with a different base URL.
|
|
26
|
+
_OPENAI_COMPATIBLE = ("openai", "openrouter", "ollama", "huggingface")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Detection
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _detect_provider_name() -> str | None:
|
|
35
|
+
"""Infer the provider name from environment variables, then saved config."""
|
|
36
|
+
explicit = os.environ.get("RLM_AGENT_PROVIDER")
|
|
37
|
+
if explicit:
|
|
38
|
+
return explicit.strip().lower()
|
|
39
|
+
|
|
40
|
+
from rlmagent.config.loader import project_settings
|
|
41
|
+
from rlmagent.config.store import load_config
|
|
42
|
+
|
|
43
|
+
project = project_settings().get("provider")
|
|
44
|
+
if project:
|
|
45
|
+
return project.lower()
|
|
46
|
+
|
|
47
|
+
saved = load_config().provider
|
|
48
|
+
if saved:
|
|
49
|
+
return saved.strip().lower()
|
|
50
|
+
|
|
51
|
+
if os.environ.get("ANTHROPIC_API_KEY"):
|
|
52
|
+
return "anthropic"
|
|
53
|
+
if os.environ.get("OPENAI_API_KEY"):
|
|
54
|
+
return "openai"
|
|
55
|
+
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Builder helpers (one per provider)
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _build_anthropic(profile: AnthropicProfile) -> ModelProvider:
|
|
65
|
+
"""Instantiate an Anthropic adapter from a loaded profile."""
|
|
66
|
+
try:
|
|
67
|
+
from rlmagent.model.claude import AnthropicProvider # type: ignore[import-not-found]
|
|
68
|
+
except ImportError:
|
|
69
|
+
raise ConfigError(
|
|
70
|
+
"The 'anthropic' provider requires the httpx extra.\n"
|
|
71
|
+
" Install it with: pip install rlmness-cli[providers]"
|
|
72
|
+
) from None
|
|
73
|
+
return AnthropicProvider(profile) # type: ignore[return-value]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_openai(profile: OpenAIProfile) -> ModelProvider:
|
|
77
|
+
"""Instantiate an OpenAI-compatible adapter from a loaded profile."""
|
|
78
|
+
try:
|
|
79
|
+
from rlmagent.model.oai_compatible import OpenAIProvider # type: ignore[import-not-found]
|
|
80
|
+
except ImportError:
|
|
81
|
+
raise ConfigError(
|
|
82
|
+
"The 'openai' provider requires the httpx extra.\n"
|
|
83
|
+
" Install it with: pip install rlmness-cli[providers]"
|
|
84
|
+
) from None
|
|
85
|
+
return OpenAIProvider(profile) # type: ignore[return-value]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Resolution
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def resolve_provider(name: str | None = None) -> ModelProvider:
|
|
94
|
+
"""Return a ready-to-use ``ModelProvider`` for *name*, with any fallbacks."""
|
|
95
|
+
from rlmagent.model.failover import with_fallbacks
|
|
96
|
+
|
|
97
|
+
return with_fallbacks(_resolve_one(name), _resolve_one)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve_one(name: str | None = None) -> ModelProvider:
|
|
101
|
+
"""Build the provider for *name* alone."""
|
|
102
|
+
resolved = name or _detect_provider_name()
|
|
103
|
+
if resolved is None:
|
|
104
|
+
raise ConfigError(
|
|
105
|
+
"No provider configured.\n"
|
|
106
|
+
" Set RLM_AGENT_PROVIDER or export an API key env var.\n"
|
|
107
|
+
" Run 'rlm-agent' to pick a provider and model, or 'rlm-agent config'."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
resolved = resolved.strip().lower()
|
|
111
|
+
if resolved == "copilot":
|
|
112
|
+
from rlmagent.model.copilot import CopilotProvider
|
|
113
|
+
return CopilotProvider()
|
|
114
|
+
if resolved == "xai-subscription":
|
|
115
|
+
from rlmagent.model.registered_oauth import BASE, resolve
|
|
116
|
+
from rlmagent.model.settings import Credential
|
|
117
|
+
|
|
118
|
+
return _build_openai(OpenAIProfile(name=resolved, credential=Credential(None, BASE),
|
|
119
|
+
resolver=resolve, endpoint="chat"))
|
|
120
|
+
if resolved == "claude-subscription":
|
|
121
|
+
from rlmagent.model.external import ExternalAgentProvider
|
|
122
|
+
return ExternalAgentProvider()
|
|
123
|
+
|
|
124
|
+
if resolved == "codex":
|
|
125
|
+
from rlmagent.model.settings import Credential
|
|
126
|
+
from rlmagent.model.subscription import RESOURCE, resolve
|
|
127
|
+
|
|
128
|
+
transport = os.environ.get("RLM_AGENT_CODEX_TRANSPORT", "http")
|
|
129
|
+
if transport not in ("http", "websocket"):
|
|
130
|
+
raise ConfigError("RLM_AGENT_CODEX_TRANSPORT must be http or websocket")
|
|
131
|
+
return _build_openai(OpenAIProfile(
|
|
132
|
+
name="codex", credential=Credential(None, RESOURCE),
|
|
133
|
+
resolver=resolve, endpoint="responses", transport=transport,
|
|
134
|
+
))
|
|
135
|
+
|
|
136
|
+
if resolved == "anthropic":
|
|
137
|
+
profile = load_anthropic_profile()
|
|
138
|
+
return _build_anthropic(profile)
|
|
139
|
+
|
|
140
|
+
if resolved == "google":
|
|
141
|
+
from rlmagent.model.google import build_google
|
|
142
|
+
|
|
143
|
+
return build_google()
|
|
144
|
+
|
|
145
|
+
if resolved in ("mistral", "azure", "xai"):
|
|
146
|
+
from rlmagent.config.loader import load_api_key
|
|
147
|
+
from rlmagent.config.store import load_config, provider_info
|
|
148
|
+
from rlmagent.model.settings import Credential
|
|
149
|
+
|
|
150
|
+
info = provider_info(resolved)
|
|
151
|
+
key = load_api_key(resolved)
|
|
152
|
+
if not key:
|
|
153
|
+
raise ConfigError(f"{info.key_env} is not set")
|
|
154
|
+
base = os.environ.get("AZURE_OPENAI_BASE_URL" if resolved == "azure" else resolved.upper() + "_BASE_URL")
|
|
155
|
+
base = base or load_config().base_urls.get(resolved) or info.base_url
|
|
156
|
+
if not base:
|
|
157
|
+
raise ConfigError("Set AZURE_OPENAI_BASE_URL to your deployed /openai/v1 endpoint")
|
|
158
|
+
credential = (Credential(None, base, (("api-key", key),)) if resolved == "azure"
|
|
159
|
+
else Credential(key, base))
|
|
160
|
+
return _build_openai(OpenAIProfile(name=resolved, credential=credential,
|
|
161
|
+
endpoint="responses" if resolved == "azure" else "chat"))
|
|
162
|
+
|
|
163
|
+
if resolved in _OPENAI_COMPATIBLE:
|
|
164
|
+
# Named for the chosen provider, so replies, stats, and errors say
|
|
165
|
+
# "openrouter" or "ollama" rather than the shared adapter's "openai".
|
|
166
|
+
profile = replace(load_openai_profile(), name=resolved)
|
|
167
|
+
return _build_openai(profile)
|
|
168
|
+
|
|
169
|
+
if resolved == "vertex":
|
|
170
|
+
from rlmagent.model.vertex import build_vertex
|
|
171
|
+
return build_vertex()
|
|
172
|
+
|
|
173
|
+
if resolved == "cli":
|
|
174
|
+
from rlmagent.model.external import ExternalAgentProvider
|
|
175
|
+
|
|
176
|
+
return ExternalAgentProvider() # type: ignore[return-value]
|
|
177
|
+
|
|
178
|
+
if resolved == "scripted":
|
|
179
|
+
from rlmagent.model.scripted import ReplayProvider
|
|
180
|
+
|
|
181
|
+
return ReplayProvider([]) # type: ignore[return-value]
|
|
182
|
+
|
|
183
|
+
raise ConfigError(
|
|
184
|
+
f"Unknown provider: {resolved!r}.\n"
|
|
185
|
+
f" Known providers: {', '.join(_KNOWN_PROVIDERS)}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# Catalog
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _probe_status(provider_name: str) -> str:
|
|
195
|
+
"""Return a short status string for a provider: 'ready', 'no key', etc."""
|
|
196
|
+
if provider_name == "codex":
|
|
197
|
+
return "subscription (rlm-agent provider login codex)"
|
|
198
|
+
if provider_name in ("google", "mistral", "azure"):
|
|
199
|
+
from rlmagent.config.loader import load_api_key
|
|
200
|
+
from rlmagent.config.store import provider_info
|
|
201
|
+
|
|
202
|
+
return "key configured" if load_api_key(provider_name) else (
|
|
203
|
+
f"no key (set {provider_info(provider_name).key_env})")
|
|
204
|
+
if provider_name == "anthropic":
|
|
205
|
+
if not os.environ.get("ANTHROPIC_API_KEY"):
|
|
206
|
+
return "no key (set ANTHROPIC_API_KEY)"
|
|
207
|
+
try:
|
|
208
|
+
load_anthropic_profile()
|
|
209
|
+
return "ready"
|
|
210
|
+
except ConfigError as exc:
|
|
211
|
+
return f"misconfigured ({exc})"
|
|
212
|
+
|
|
213
|
+
if provider_name == "openai":
|
|
214
|
+
if not os.environ.get("OPENAI_API_KEY"):
|
|
215
|
+
return "no key (set OPENAI_API_KEY)"
|
|
216
|
+
try:
|
|
217
|
+
load_openai_profile()
|
|
218
|
+
return "ready"
|
|
219
|
+
except ConfigError as exc:
|
|
220
|
+
return f"misconfigured ({exc})"
|
|
221
|
+
|
|
222
|
+
if provider_name == "scripted":
|
|
223
|
+
return "available (test only)"
|
|
224
|
+
|
|
225
|
+
return "unknown"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def list_providers() -> None:
|
|
229
|
+
"""Print available providers and their configuration status to stderr."""
|
|
230
|
+
active = _detect_provider_name()
|
|
231
|
+
sys.stderr.write("Providers:\n")
|
|
232
|
+
for name in _KNOWN_PROVIDERS:
|
|
233
|
+
marker = "*" if name == active else " "
|
|
234
|
+
status = _probe_status(name)
|
|
235
|
+
sys.stderr.write(f" {marker} {name:<12} {status}\n")
|
|
236
|
+
sys.stderr.flush()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def setup_provider() -> None:
|
|
240
|
+
"""Print environment-variable guidance for configuring each provider."""
|
|
241
|
+
sys.stderr.write(
|
|
242
|
+
"Provider setup\n"
|
|
243
|
+
"\nCodex subscription:\n"
|
|
244
|
+
" Install rlmagent[subscription], then run rlm-agent provider login codex.\n"
|
|
245
|
+
" Use --provider codex; tokens stay in the platform credential store.\n"
|
|
246
|
+
"\nNative providers:\n"
|
|
247
|
+
" GOOGLE_API_KEY, MISTRAL_API_KEY, or AZURE_OPENAI_API_KEY\n"
|
|
248
|
+
" Azure also requires AZURE_OPENAI_BASE_URL ending in /openai/v1.\n"
|
|
249
|
+
"\n"
|
|
250
|
+
"Set the following environment variables for your chosen provider.\n"
|
|
251
|
+
"\n"
|
|
252
|
+
"Anthropic:\n"
|
|
253
|
+
" ANTHROPIC_API_KEY (required)\n"
|
|
254
|
+
" ANTHROPIC_BASE_URL (default: https://api.anthropic.com)\n"
|
|
255
|
+
" ANTHROPIC_TIMEOUT (seconds, default: 120)\n"
|
|
256
|
+
" ANTHROPIC_MAX_TOKENS (default: 16384)\n"
|
|
257
|
+
" ANTHROPIC_EXTRA_HEADERS (semicolon-separated Key: Value pairs)\n"
|
|
258
|
+
"\n"
|
|
259
|
+
"OpenAI-compatible:\n"
|
|
260
|
+
" OPENAI_API_KEY (required)\n"
|
|
261
|
+
" OPENAI_BASE_URL (default: https://api.openai.com/v1)\n"
|
|
262
|
+
" OPENAI_TIMEOUT (seconds, default: 120)\n"
|
|
263
|
+
" OPENAI_ORG_ID (optional)\n"
|
|
264
|
+
" OPENAI_EXTRA_HEADERS (semicolon-separated Key: Value pairs)\n"
|
|
265
|
+
"\n"
|
|
266
|
+
"Google Vertex:\n"
|
|
267
|
+
" Install rlmagent[vertex]; configure Application Default Credentials.\n"
|
|
268
|
+
" GOOGLE_CLOUD_PROJECT (required unless ADC supplies a project)\n"
|
|
269
|
+
" GOOGLE_CLOUD_LOCATION (default: global)\n"
|
|
270
|
+
"\n"
|
|
271
|
+
"Shared:\n"
|
|
272
|
+
" RLM_AGENT_PROVIDER (anthropic | openai | scripted)\n"
|
|
273
|
+
" RLM_AGENT_MODEL (model identifier override)\n"
|
|
274
|
+
" RLM_AGENT_MAX_RETRIES (default: 2)\n"
|
|
275
|
+
" RLM_AGENT_MAX_RETRY_DELAY (seconds, default: 30)\n"
|
|
276
|
+
" RLM_AGENT_THINKING_ENABLED (true/false, default: false)\n"
|
|
277
|
+
" RLM_AGENT_THINKING_BUDGET (token count, optional)\n"
|
|
278
|
+
)
|
|
279
|
+
sys.stderr.flush()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def select_provider(name: str) -> None:
|
|
283
|
+
"""Validate a provider name for selection."""
|
|
284
|
+
normalized = name.strip().lower()
|
|
285
|
+
if normalized not in _KNOWN_PROVIDERS:
|
|
286
|
+
raise ConfigError(
|
|
287
|
+
f"Unknown provider: {name!r}.\n"
|
|
288
|
+
f" Known providers: {', '.join(_KNOWN_PROVIDERS)}"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
__all__ = [
|
|
293
|
+
"list_providers",
|
|
294
|
+
"resolve_provider",
|
|
295
|
+
"select_provider",
|
|
296
|
+
"setup_provider",
|
|
297
|
+
]
|