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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Frontend metadata and policies for a backend-owned execution tree."""
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
|
|
4
|
+
from rlmagent.agents.approval import Approver
|
|
5
|
+
from rlmagent.agents.budget import BudgetedProvider
|
|
6
|
+
from rlmagent.agents.trace import TracedProvider, Tracer
|
|
7
|
+
from rlmagent.model.external import with_external
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BackendContext:
|
|
11
|
+
def __init__(self, *, provider, provider_name, model, cwd, allowance,
|
|
12
|
+
sessions_dir, kernel="wasm", approver=None, network=False,
|
|
13
|
+
cell_timeout=300, max_agents=50, tracer=None, **metadata):
|
|
14
|
+
self.provider = BudgetedProvider(with_external(provider), allowance)
|
|
15
|
+
self.provider_name, self.model, self.cwd = provider_name, model, cwd
|
|
16
|
+
self.allowance, self.sessions_dir = allowance, sessions_dir
|
|
17
|
+
self.kernel_kind, self.network = kernel, network
|
|
18
|
+
self.approver = approver or Approver("deny")
|
|
19
|
+
self.cell_timeout, self.max_agents = cell_timeout, max_agents
|
|
20
|
+
self.tracer = tracer or Tracer(None)
|
|
21
|
+
self.improver = None
|
|
22
|
+
self.records, self.backend_agents = {}, {}
|
|
23
|
+
self.nodes = {"root": SimpleNamespace(context=None)}
|
|
24
|
+
self._finished = False
|
|
25
|
+
self.__dict__.update(metadata)
|
|
26
|
+
|
|
27
|
+
async def start(self):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def root_id(self):
|
|
31
|
+
return "root"
|
|
32
|
+
|
|
33
|
+
def root_kernel(self):
|
|
34
|
+
# Tool metadata identifies the coding route. The solver owns execution.
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
async def run(self, code, timeout=None):
|
|
38
|
+
raise RuntimeError("submit code through the backend solver")
|
|
39
|
+
|
|
40
|
+
def attach(self, agent, session):
|
|
41
|
+
self.session = session
|
|
42
|
+
|
|
43
|
+
def provider_for(self, agent):
|
|
44
|
+
return TracedProvider(self, agent)
|
|
45
|
+
|
|
46
|
+
def use_provider(self, provider, provider_name):
|
|
47
|
+
self.provider = BudgetedProvider(with_external(provider), self.allowance)
|
|
48
|
+
self.provider_name = provider_name
|
|
49
|
+
return self.provider
|
|
50
|
+
|
|
51
|
+
def _role(self, name):
|
|
52
|
+
if self.improver is None:
|
|
53
|
+
raise RuntimeError("playbook unavailable")
|
|
54
|
+
entry = self.improver.entry(name)
|
|
55
|
+
if entry['kind'] != 'role':
|
|
56
|
+
raise ValueError("entry is not a role")
|
|
57
|
+
return entry['content']
|
|
58
|
+
|
|
59
|
+
def child_session_ids(self, parent):
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
def finish_trace(self, complete=True):
|
|
63
|
+
if self._finished:
|
|
64
|
+
return None
|
|
65
|
+
self._finished = True
|
|
66
|
+
agents = [{"agent": key, "parent": value.get("parent_run_id"),
|
|
67
|
+
"task": value.get("instruction"), "status": value.get("status"),
|
|
68
|
+
"session": getattr(getattr(self, "session", None), "session_id", None)
|
|
69
|
+
if key == "root" else None}
|
|
70
|
+
for key, value in self.backend_agents.items()]
|
|
71
|
+
return self.tracer.finish(agents, self.provider.refusal, complete)
|
|
72
|
+
|
|
73
|
+
async def close(self):
|
|
74
|
+
self.finish_trace()
|
rlmagent/cli/__init__.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""``rlm-agent config`` — interactive provider configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from rlmagent.config import prompts
|
|
10
|
+
from rlmagent.config.store import (
|
|
11
|
+
PROVIDERS,
|
|
12
|
+
config_path,
|
|
13
|
+
load_config,
|
|
14
|
+
mask_key,
|
|
15
|
+
provider_info,
|
|
16
|
+
save_config,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
21
|
+
p = argparse.ArgumentParser(prog="rlm-agent config")
|
|
22
|
+
p.add_argument("--provider", default=None, help="Provider key (skips the menu).")
|
|
23
|
+
p.add_argument("--model", default=None, help="Model identifier (skips the menu).")
|
|
24
|
+
p.add_argument(
|
|
25
|
+
"--api-key", default=None,
|
|
26
|
+
help="API key (skips the prompt). Also read from RLM_AGENT_API_KEY.",
|
|
27
|
+
)
|
|
28
|
+
p.add_argument(
|
|
29
|
+
"--visible", action="store_true",
|
|
30
|
+
help="Type the API key visibly, for terminals without hidden input.",
|
|
31
|
+
)
|
|
32
|
+
sub = p.add_subparsers(dest="action")
|
|
33
|
+
sub.add_parser("show", help="Show the saved configuration (keys masked).")
|
|
34
|
+
sub.add_parser("path", help="Print the configuration file path.")
|
|
35
|
+
sub.add_parser("doctor", help="Report this terminal's hidden-input support.")
|
|
36
|
+
return p
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _doctor() -> int:
|
|
40
|
+
"""Print terminal capability diagnostics."""
|
|
41
|
+
from rlmagent.config.secure_input import describe_terminal
|
|
42
|
+
|
|
43
|
+
prompts.info("Terminal hidden-input diagnostics:\n")
|
|
44
|
+
for line in describe_terminal():
|
|
45
|
+
prompts.info(f" {line}")
|
|
46
|
+
prompts.info(
|
|
47
|
+
"\nIf hidden input is unavailable, use one of:\n"
|
|
48
|
+
" rlm-agent config --visible\n"
|
|
49
|
+
" rlm-agent config --provider openai --model gpt-5 --api-key sk-...\n"
|
|
50
|
+
" $env:RLM_AGENT_API_KEY='sk-...'; rlm-agent config --provider openai"
|
|
51
|
+
)
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _save_noninteractive(
|
|
56
|
+
provider: str, model: str | None, api_key: str | None,
|
|
57
|
+
) -> int:
|
|
58
|
+
"""Save configuration without prompting."""
|
|
59
|
+
try:
|
|
60
|
+
info = provider_info(provider.strip().lower())
|
|
61
|
+
except KeyError:
|
|
62
|
+
valid = ", ".join(p.key for p in PROVIDERS)
|
|
63
|
+
prompts.info(f"Unknown provider {provider!r}. Choose one of: {valid}")
|
|
64
|
+
return 1
|
|
65
|
+
|
|
66
|
+
cfg = load_config()
|
|
67
|
+
key = (api_key or "").strip()
|
|
68
|
+
if not key and info.needs_key and info.key not in cfg.api_keys:
|
|
69
|
+
prompts.info(f"{info.label} requires an API key (--api-key or RLM_AGENT_API_KEY).")
|
|
70
|
+
return 1
|
|
71
|
+
|
|
72
|
+
cfg.provider = info.key
|
|
73
|
+
cfg.model = model or cfg.model or info.models[0]
|
|
74
|
+
if key:
|
|
75
|
+
cfg.api_keys[info.key] = key
|
|
76
|
+
path = save_config(cfg)
|
|
77
|
+
|
|
78
|
+
prompts.info(f"Configuration saved to {path}")
|
|
79
|
+
prompts.info(f"Provider: {info.label}")
|
|
80
|
+
prompts.info(f"Model: {cfg.model}")
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
#: Environment variables that take precedence over the saved file for a run.
|
|
85
|
+
OVERRIDES = ("RLM_AGENT_PROVIDER", "RLM_AGENT_MODEL", "RLM_AGENT_API_KEY", "RLM_AGENT_HOME")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _show() -> int:
|
|
89
|
+
"""Print the saved configuration with API keys masked, and what the environment overrides."""
|
|
90
|
+
import os
|
|
91
|
+
|
|
92
|
+
cfg = load_config()
|
|
93
|
+
path = config_path()
|
|
94
|
+
overrides = [(name, os.environ[name]) for name in OVERRIDES if os.environ.get(name)]
|
|
95
|
+
if not path.exists():
|
|
96
|
+
prompts.info(f"No configuration at {path}. Run 'rlm-agent config' to create one.")
|
|
97
|
+
else:
|
|
98
|
+
prompts.info(f"Config: {path}")
|
|
99
|
+
prompts.info(f"Provider: {cfg.provider or '(unset)'}")
|
|
100
|
+
prompts.info(f"Model: {cfg.model or '(unset)'}")
|
|
101
|
+
for name in sorted(cfg.api_keys):
|
|
102
|
+
prompts.info(f" {name}: {mask_key(cfg.api_keys[name])}")
|
|
103
|
+
if overrides:
|
|
104
|
+
prompts.info("Set in the environment, used instead of the file:")
|
|
105
|
+
for name, value in overrides:
|
|
106
|
+
shown = mask_key(value) if name.endswith("_KEY") else value
|
|
107
|
+
prompts.info(f" {name}={shown}")
|
|
108
|
+
return 0 if path.exists() or overrides else 1
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _configure(*, visible: bool = False) -> int:
|
|
112
|
+
"""Run the interactive setup flow."""
|
|
113
|
+
cfg = load_config()
|
|
114
|
+
|
|
115
|
+
labels = [p.label for p in PROVIDERS]
|
|
116
|
+
default = next(
|
|
117
|
+
(i for i, p in enumerate(PROVIDERS) if p.key == cfg.provider), 0,
|
|
118
|
+
)
|
|
119
|
+
info = PROVIDERS[prompts.choose("Select provider", labels, default=default)]
|
|
120
|
+
|
|
121
|
+
prompts.info(f"\n{info.label}")
|
|
122
|
+
|
|
123
|
+
existing = cfg.api_keys.get(info.key)
|
|
124
|
+
key = prompts.ask_api_key(
|
|
125
|
+
info.label,
|
|
126
|
+
required=info.needs_key,
|
|
127
|
+
has_existing=bool(existing),
|
|
128
|
+
visible=visible,
|
|
129
|
+
)
|
|
130
|
+
if key is None:
|
|
131
|
+
key = existing or ""
|
|
132
|
+
if info.needs_key and not key.strip():
|
|
133
|
+
prompts.info("API key must not be empty. Nothing saved.")
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
current_model = cfg.model if cfg.provider == info.key else None
|
|
137
|
+
model = prompts.choose_model(list(info.models), current=current_model)
|
|
138
|
+
|
|
139
|
+
if not prompts.confirm("Save as default?"):
|
|
140
|
+
prompts.info("Nothing saved.")
|
|
141
|
+
return 1
|
|
142
|
+
|
|
143
|
+
cfg.provider = info.key
|
|
144
|
+
cfg.model = model
|
|
145
|
+
if key.strip():
|
|
146
|
+
cfg.api_keys[info.key] = key.strip()
|
|
147
|
+
path = save_config(cfg)
|
|
148
|
+
|
|
149
|
+
prompts.info(f"\nConfiguration saved to {path}")
|
|
150
|
+
prompts.info(f"Provider: {info.label}")
|
|
151
|
+
prompts.info(f"Model: {model}")
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def handle_config(argv: Sequence[str]) -> int:
|
|
156
|
+
"""Entry point for the ``config`` subcommand."""
|
|
157
|
+
ns = _build_parser().parse_args(list(argv))
|
|
158
|
+
if ns.action == "show":
|
|
159
|
+
return _show()
|
|
160
|
+
if ns.action == "path":
|
|
161
|
+
prompts.info(str(config_path()))
|
|
162
|
+
return 0
|
|
163
|
+
if ns.action == "doctor":
|
|
164
|
+
return _doctor()
|
|
165
|
+
|
|
166
|
+
api_key = ns.api_key or os.environ.get("RLM_AGENT_API_KEY")
|
|
167
|
+
if ns.provider and (api_key or ns.model):
|
|
168
|
+
return _save_noninteractive(ns.provider, ns.model, api_key)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
return _configure(visible=ns.visible)
|
|
172
|
+
except prompts.PromptAborted:
|
|
173
|
+
prompts.info("\nCancelled. Nothing saved.")
|
|
174
|
+
return 130
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = ["handle_config"]
|
rlmagent/cli/health.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""`rlm-agent status` and `rlm-agent doctor`: what is set up, what is running, what is stale."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Check:
|
|
13
|
+
name: str
|
|
14
|
+
ok: bool
|
|
15
|
+
detail: str
|
|
16
|
+
fix: str = "" # what doctor --fix would do; empty when it cannot fix this
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _config() -> Check:
|
|
20
|
+
from rlmagent.config.store import config_path, load_config
|
|
21
|
+
|
|
22
|
+
path = config_path()
|
|
23
|
+
if not path.exists():
|
|
24
|
+
env = next((n for n in ("OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY",
|
|
25
|
+
"RLM_AGENT_API_KEY") if os.environ.get(n)), None)
|
|
26
|
+
if env:
|
|
27
|
+
return Check("provider", True, f"no saved configuration; {env} is set")
|
|
28
|
+
return Check("provider", False, "no configuration; run rlm-agent to pick a provider, or rlm-agent config")
|
|
29
|
+
cfg = load_config()
|
|
30
|
+
has_key = bool(cfg.api_keys.get(cfg.provider or "", ""))
|
|
31
|
+
return Check("provider", bool(cfg.provider),
|
|
32
|
+
f"{cfg.provider or '(unset)'} / {cfg.model or '(unset)'}, "
|
|
33
|
+
f"key {'saved' if has_key else 'not saved (environment or none)'}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _sandbox() -> Check:
|
|
37
|
+
try:
|
|
38
|
+
from rlmness.wasm_runtime import wasm_available
|
|
39
|
+
except ImportError:
|
|
40
|
+
return Check("sandbox", False, "rlmness is not installed")
|
|
41
|
+
if shutil.which("node") is None:
|
|
42
|
+
return Check("sandbox", False, "Node.js 18+ is not on PATH; install it for --kernel wasm")
|
|
43
|
+
if not wasm_available():
|
|
44
|
+
return Check("sandbox", False, "Pyodide is not installed", "install Pyodide (rlmness --setup)")
|
|
45
|
+
return Check("sandbox", True, "Node and Pyodide found")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _git() -> Check:
|
|
49
|
+
ok = shutil.which("git") is not None
|
|
50
|
+
return Check("git", ok, "found; /undo and /diff work" if ok else "not on PATH; /undo and /diff are off")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _workers() -> list[Check]:
|
|
54
|
+
from rlmagent import worker
|
|
55
|
+
|
|
56
|
+
base = worker.root()
|
|
57
|
+
rows = []
|
|
58
|
+
for path in sorted(base.iterdir()) if base.is_dir() else []:
|
|
59
|
+
if not path.is_dir():
|
|
60
|
+
continue
|
|
61
|
+
meta = worker.state_of(path.name)
|
|
62
|
+
recorded = meta.get("state", "?")
|
|
63
|
+
if meta["alive"]:
|
|
64
|
+
rows.append(Check(f"worker {path.name}", True, f"{recorded}, session {meta.get('session')}"))
|
|
65
|
+
elif recorded in ("starting", "idle", "busy"):
|
|
66
|
+
rows.append(Check(f"worker {path.name}", False,
|
|
67
|
+
f"recorded as {recorded} but its process is gone", "mark it stopped"))
|
|
68
|
+
else:
|
|
69
|
+
rows.append(Check(f"worker {path.name}", True, recorded))
|
|
70
|
+
return rows
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _sessions_open() -> list[Check]:
|
|
74
|
+
from rlmagent.agentnet import _home, _read
|
|
75
|
+
from rlmagent.worker import _pid_alive
|
|
76
|
+
|
|
77
|
+
base = _home()
|
|
78
|
+
rows = []
|
|
79
|
+
for path in sorted(base.iterdir()) if base.is_dir() else []:
|
|
80
|
+
meta = _read(path / "meta.json")
|
|
81
|
+
if meta is None:
|
|
82
|
+
continue
|
|
83
|
+
if meta.get("pid") and _pid_alive(int(meta["pid"])):
|
|
84
|
+
rows.append(Check(f"session {path.name}", True, f"open in {meta.get('cwd')}"))
|
|
85
|
+
else:
|
|
86
|
+
rows.append(Check(f"session {path.name}", False, "registered but its process is gone",
|
|
87
|
+
"remove the registration"))
|
|
88
|
+
return rows
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _mcp() -> Check:
|
|
92
|
+
from rlmagent.mcp import load_configs
|
|
93
|
+
from rlmagent.plugins import rlm_agent_home
|
|
94
|
+
|
|
95
|
+
servers, problems = load_configs(rlm_agent_home(), None)
|
|
96
|
+
if problems:
|
|
97
|
+
return Check("mcp", False, "; ".join(problems))
|
|
98
|
+
return Check("mcp", True, f"{len(servers)} server(s) in the home mcp.json")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _search() -> Check:
|
|
102
|
+
from rlmagent.websearch import SERVICES, configured
|
|
103
|
+
|
|
104
|
+
name = configured()
|
|
105
|
+
return Check("web search", True,
|
|
106
|
+
f"{name} is set" if name else f"off; set one of {', '.join(SERVICES)}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def checks() -> list[Check]:
|
|
110
|
+
return [_config(), _sandbox(), _git(), _mcp(), _search(), *_workers(), *_sessions_open()]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _fix(check: Check) -> str:
|
|
114
|
+
if check.name == "sandbox":
|
|
115
|
+
from rlmness import home
|
|
116
|
+
|
|
117
|
+
return "installed Pyodide" if home.setup() == 0 else "Pyodide install failed"
|
|
118
|
+
if check.name.startswith("worker "):
|
|
119
|
+
from rlmagent import worker
|
|
120
|
+
|
|
121
|
+
name = check.name.split(" ", 1)[1]
|
|
122
|
+
meta = worker.state_of(name)
|
|
123
|
+
meta.update(state="stopped", pid=None)
|
|
124
|
+
meta.pop("alive", None)
|
|
125
|
+
worker._write(worker.folder(name) / "meta.json", meta)
|
|
126
|
+
return "marked stopped"
|
|
127
|
+
if check.name.startswith("session "):
|
|
128
|
+
from rlmagent.agentnet import _home
|
|
129
|
+
|
|
130
|
+
(_home() / check.name.split(" ", 1)[1] / "meta.json").unlink(missing_ok=True)
|
|
131
|
+
return "removed"
|
|
132
|
+
return "nothing done"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def run(argv: list[str], *, doctor: bool) -> int:
|
|
136
|
+
from rlmagent.cli.main import get_version
|
|
137
|
+
from rlmagent.plugins import rlm_agent_home
|
|
138
|
+
|
|
139
|
+
fix = "--fix" in argv
|
|
140
|
+
out = sys.stdout
|
|
141
|
+
out.write(f"rlm-agent {get_version()}, home {rlm_agent_home()}\n")
|
|
142
|
+
found = checks()
|
|
143
|
+
problems = 0
|
|
144
|
+
for check in found:
|
|
145
|
+
mark = "ok " if check.ok else "!! "
|
|
146
|
+
out.write(f"{mark}{check.name}: {check.detail}\n")
|
|
147
|
+
if not check.ok:
|
|
148
|
+
problems += 1
|
|
149
|
+
if doctor and check.fix:
|
|
150
|
+
out.write(f" {'fixing: ' + _fix(check) if fix else 'doctor --fix would ' + check.fix}\n")
|
|
151
|
+
if doctor:
|
|
152
|
+
out.write("no problems found\n" if not problems else f"{problems} problem(s)\n")
|
|
153
|
+
return 1 if doctor and problems and not fix else 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = ["Check", "checks", "run"]
|