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/tools/kernel.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""The python tool: run a cell in this agent's persistent kernel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
|
|
8
|
+
from rlmagent.kernel import CellResult, KernelSession
|
|
9
|
+
from rlmagent.tools._shared import (
|
|
10
|
+
CancelToken,
|
|
11
|
+
JValue,
|
|
12
|
+
ProgressNotifier,
|
|
13
|
+
ToolOutcome,
|
|
14
|
+
ToolSpec,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_DESCRIPTION = (
|
|
18
|
+
"Run Python in a persistent IPython kernel whose working directory is the "
|
|
19
|
+
"project root. Variables, imports and functions stay defined between calls. "
|
|
20
|
+
"Run shell commands with a leading `!` or with subprocess. Top-level `await` works."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_PARAMETERS: Mapping[str, JValue] = {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {"code": {"type": "string", "description": "The cell to execute in the kernel."}},
|
|
26
|
+
"required": ["code"],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def format_cell(result: CellResult) -> str:
|
|
31
|
+
parts: list[str] = []
|
|
32
|
+
if result.restarted and not result.timed_out:
|
|
33
|
+
parts.append(
|
|
34
|
+
"[The kernel had stopped and was restarted: variables from before are gone.]"
|
|
35
|
+
)
|
|
36
|
+
if result.output:
|
|
37
|
+
parts.append(result.output.rstrip("\n"))
|
|
38
|
+
if result.error:
|
|
39
|
+
parts.append(result.error)
|
|
40
|
+
if result.timed_out and result.restarted:
|
|
41
|
+
parts.append(
|
|
42
|
+
"[The cell timed out and ignored the interrupt, so the kernel was restarted: "
|
|
43
|
+
"variables from before are gone.]"
|
|
44
|
+
)
|
|
45
|
+
elif result.timed_out:
|
|
46
|
+
parts.append("[The cell timed out and was interrupted; state up to that point is kept.]")
|
|
47
|
+
return "\n".join(parts) if parts else "(no output)"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_CANCEL_GRACE = 3.0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def _cancel(kernel: KernelSession, task: asyncio.Task) -> str:
|
|
54
|
+
"""Stop a running cell for a cancel, and never wait long for it."""
|
|
55
|
+
await kernel.interrupt()
|
|
56
|
+
done, _ = await asyncio.wait({task}, timeout=_CANCEL_GRACE)
|
|
57
|
+
if done:
|
|
58
|
+
return format_cell(task.result()) + "\n[Cancelled by the user.]"
|
|
59
|
+
await kernel.kill()
|
|
60
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
61
|
+
return (
|
|
62
|
+
"[Cancelled by the user. The cell ignored the interrupt, so the kernel was "
|
|
63
|
+
"stopped: variables from before are gone.]"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def make_python_tool(kernel: KernelSession) -> ToolSpec:
|
|
68
|
+
async def run(
|
|
69
|
+
tool_call_id: str,
|
|
70
|
+
arguments: Mapping[str, JValue],
|
|
71
|
+
signal: CancelToken | None = None,
|
|
72
|
+
on_update: ProgressNotifier | None = None,
|
|
73
|
+
) -> ToolOutcome:
|
|
74
|
+
code = str(arguments.get("code", ""))
|
|
75
|
+
task = asyncio.create_task(kernel.run(code))
|
|
76
|
+
try:
|
|
77
|
+
while not task.done():
|
|
78
|
+
if signal is not None and signal.is_cancelled():
|
|
79
|
+
return ToolOutcome(content=await _cancel(kernel, task))
|
|
80
|
+
await asyncio.sleep(0.2)
|
|
81
|
+
except asyncio.CancelledError:
|
|
82
|
+
await kernel.interrupt()
|
|
83
|
+
raise
|
|
84
|
+
return ToolOutcome(content=format_cell(await task))
|
|
85
|
+
|
|
86
|
+
return ToolSpec(
|
|
87
|
+
name="python",
|
|
88
|
+
label="Python",
|
|
89
|
+
description=_DESCRIPTION,
|
|
90
|
+
parameters=_PARAMETERS,
|
|
91
|
+
run=run,
|
|
92
|
+
parallelism="sequential",
|
|
93
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""ApplyPatch: several edits across files from one unified diff, all of them or none."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rlmagent.tools._shared import (
|
|
9
|
+
CancelToken,
|
|
10
|
+
JValue,
|
|
11
|
+
ProgressNotifier,
|
|
12
|
+
ToolOutcome,
|
|
13
|
+
ToolSpec,
|
|
14
|
+
fault,
|
|
15
|
+
require_str,
|
|
16
|
+
text_outcome,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_PARAMS: Mapping[str, JValue] = {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"patch": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": (
|
|
25
|
+
"A unified diff with ---/+++ headers and @@ hunks. Paths are relative to the "
|
|
26
|
+
"working directory; /dev/null creates or deletes a file."
|
|
27
|
+
),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
"required": ["patch"],
|
|
31
|
+
"additionalProperties": False,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def _run_patch(
|
|
36
|
+
tool_call_id: str,
|
|
37
|
+
arguments: Mapping[str, JValue],
|
|
38
|
+
signal: CancelToken | None = None,
|
|
39
|
+
on_update: ProgressNotifier | None = None,
|
|
40
|
+
) -> ToolOutcome:
|
|
41
|
+
from rlmagent.agents.approval import Approver
|
|
42
|
+
from rlmagent.agents.patch import PatchError
|
|
43
|
+
from rlmagent.agents.workspace import Workspace
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
diff = require_str(arguments, "patch")
|
|
47
|
+
# Approval for this tool happens before it runs, like Write and Edit.
|
|
48
|
+
return text_outcome(await Workspace(Path.cwd(), Approver("auto")).apply_patch(diff))
|
|
49
|
+
except (PatchError, ValueError, OSError) as exc:
|
|
50
|
+
return fault(f"Patch not applied: {exc}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def make_patch_tool() -> ToolSpec:
|
|
54
|
+
return ToolSpec(
|
|
55
|
+
name="ApplyPatch",
|
|
56
|
+
label="Apply Patch",
|
|
57
|
+
description=(
|
|
58
|
+
"Apply a unified diff that may change, create or delete several files. Every hunk "
|
|
59
|
+
"is checked first; if any does not match, no file is changed."
|
|
60
|
+
),
|
|
61
|
+
parameters=_PARAMS,
|
|
62
|
+
run=_run_patch,
|
|
63
|
+
parallelism="sequential",
|
|
64
|
+
format_call=lambda a: f"{str(a.get('patch', '')).count('+++ ')} files",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = ["make_patch_tool"]
|
rlmagent/tools/search.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Search tools: Grep for file contents, Glob for file names. Both only read."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rlmagent.tools._shared import (
|
|
12
|
+
CancelToken,
|
|
13
|
+
JValue,
|
|
14
|
+
ProgressNotifier,
|
|
15
|
+
ToolOutcome,
|
|
16
|
+
ToolSpec,
|
|
17
|
+
fault,
|
|
18
|
+
optional_bool,
|
|
19
|
+
optional_int,
|
|
20
|
+
require_str,
|
|
21
|
+
text_outcome,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "__pycache__", ".rlm-agent"}
|
|
25
|
+
MAX_FILE_BYTES = 2_000_000
|
|
26
|
+
MAX_LINE_CHARS = 300
|
|
27
|
+
DEFAULT_MATCHES = 200
|
|
28
|
+
MAX_MATCHES = 1_000
|
|
29
|
+
MAX_NAMES = 1_000
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _start(arguments: Mapping[str, JValue]) -> Path:
|
|
33
|
+
raw = arguments.get("path")
|
|
34
|
+
path = Path(raw) if isinstance(raw, str) and raw.strip() else Path.cwd()
|
|
35
|
+
if not path.is_absolute():
|
|
36
|
+
path = Path.cwd() / path
|
|
37
|
+
if not path.exists():
|
|
38
|
+
raise ValueError(f"Path not found: {path}")
|
|
39
|
+
return path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _files(base: Path, glob: str | None):
|
|
43
|
+
if base.is_file():
|
|
44
|
+
yield base
|
|
45
|
+
return
|
|
46
|
+
for folder, dirs, names in os.walk(base, followlinks=False):
|
|
47
|
+
dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
|
|
48
|
+
for name in sorted(names):
|
|
49
|
+
file = Path(folder) / name
|
|
50
|
+
if glob:
|
|
51
|
+
rel = file.relative_to(base).as_posix()
|
|
52
|
+
if not (fnmatch.fnmatch(name, glob) or fnmatch.fnmatch(rel, glob)):
|
|
53
|
+
continue
|
|
54
|
+
yield file
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
58
|
+
# Grep tool
|
|
59
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
60
|
+
|
|
61
|
+
_GREP_PARAMS: Mapping[str, JValue] = {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"properties": {
|
|
64
|
+
"pattern": {"type": "string", "description": "Regular expression to look for."},
|
|
65
|
+
"path": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": "File or folder to search. Defaults to the working directory.",
|
|
68
|
+
},
|
|
69
|
+
"glob": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"description": "Only search files whose name or relative path matches, e.g. *.py.",
|
|
72
|
+
},
|
|
73
|
+
"ignore_case": {"type": "boolean", "description": "Match without regard to case."},
|
|
74
|
+
"limit": {
|
|
75
|
+
"type": "integer",
|
|
76
|
+
"description": f"Most matching lines to return (default {DEFAULT_MATCHES}).",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
"required": ["pattern"],
|
|
80
|
+
"additionalProperties": False,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def _run_grep(
|
|
85
|
+
tool_call_id: str,
|
|
86
|
+
arguments: Mapping[str, JValue],
|
|
87
|
+
signal: CancelToken | None = None,
|
|
88
|
+
on_update: ProgressNotifier | None = None,
|
|
89
|
+
) -> ToolOutcome:
|
|
90
|
+
try:
|
|
91
|
+
regex = re.compile(
|
|
92
|
+
require_str(arguments, "pattern"),
|
|
93
|
+
re.IGNORECASE if optional_bool(arguments, "ignore_case") else 0,
|
|
94
|
+
)
|
|
95
|
+
base = _start(arguments)
|
|
96
|
+
except (ValueError, re.error) as exc:
|
|
97
|
+
return fault(str(exc))
|
|
98
|
+
glob = arguments.get("glob") if isinstance(arguments.get("glob"), str) else None
|
|
99
|
+
limit = max(1, min(optional_int(arguments, "limit") or DEFAULT_MATCHES, MAX_MATCHES))
|
|
100
|
+
lines: list[str] = []
|
|
101
|
+
for file in _files(base, glob):
|
|
102
|
+
try:
|
|
103
|
+
if file.stat().st_size > MAX_FILE_BYTES:
|
|
104
|
+
continue
|
|
105
|
+
with open(file, encoding="utf-8") as f:
|
|
106
|
+
for number, line in enumerate(f, 1):
|
|
107
|
+
if regex.search(line):
|
|
108
|
+
lines.append(f"{file}:{number}: {line.rstrip()[:MAX_LINE_CHARS]}")
|
|
109
|
+
if len(lines) >= limit:
|
|
110
|
+
lines.append(f"(stopped at {limit} matches; narrow the search)")
|
|
111
|
+
return text_outcome("\n".join(lines))
|
|
112
|
+
except (UnicodeDecodeError, OSError):
|
|
113
|
+
continue
|
|
114
|
+
return text_outcome("\n".join(lines) if lines else "No matches.")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def make_grep_tool() -> ToolSpec:
|
|
118
|
+
return ToolSpec(
|
|
119
|
+
name="Grep",
|
|
120
|
+
label="Search Files",
|
|
121
|
+
description=(
|
|
122
|
+
"Search file contents with a regular expression. Returns path:line: text "
|
|
123
|
+
"for each matching line. Skips .git, virtual environments and binary files."
|
|
124
|
+
),
|
|
125
|
+
parameters=_GREP_PARAMS,
|
|
126
|
+
run=_run_grep,
|
|
127
|
+
parallelism="parallel",
|
|
128
|
+
format_call=lambda a: f"{a.get('pattern', '?')} in {a.get('path') or '.'}",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
133
|
+
# Glob tool
|
|
134
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
135
|
+
|
|
136
|
+
_GLOB_PARAMS: Mapping[str, JValue] = {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"properties": {
|
|
139
|
+
"pattern": {"type": "string", "description": "File name pattern, e.g. **/*.py."},
|
|
140
|
+
"path": {
|
|
141
|
+
"type": "string",
|
|
142
|
+
"description": "Folder to search in. Defaults to the working directory.",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
"required": ["pattern"],
|
|
146
|
+
"additionalProperties": False,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def _run_glob(
|
|
151
|
+
tool_call_id: str,
|
|
152
|
+
arguments: Mapping[str, JValue],
|
|
153
|
+
signal: CancelToken | None = None,
|
|
154
|
+
on_update: ProgressNotifier | None = None,
|
|
155
|
+
) -> ToolOutcome:
|
|
156
|
+
try:
|
|
157
|
+
pattern = require_str(arguments, "pattern")
|
|
158
|
+
base = _start(arguments)
|
|
159
|
+
except ValueError as exc:
|
|
160
|
+
return fault(str(exc))
|
|
161
|
+
names: list[str] = []
|
|
162
|
+
for path in sorted(base.glob(pattern)):
|
|
163
|
+
rel = path.relative_to(base)
|
|
164
|
+
if path.is_file() and not SKIP_DIRS.intersection(rel.parts):
|
|
165
|
+
names.append(str(path))
|
|
166
|
+
if len(names) >= MAX_NAMES:
|
|
167
|
+
names.append(f"(stopped at {MAX_NAMES} files; narrow the pattern)")
|
|
168
|
+
break
|
|
169
|
+
return text_outcome("\n".join(names) if names else "No files found.")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def make_glob_tool() -> ToolSpec:
|
|
173
|
+
return ToolSpec(
|
|
174
|
+
name="Glob",
|
|
175
|
+
label="Find Files",
|
|
176
|
+
description="Find files by name pattern, such as **/*.py or src/**/test_*.py.",
|
|
177
|
+
parameters=_GLOB_PARAMS,
|
|
178
|
+
run=_run_glob,
|
|
179
|
+
parallelism="parallel",
|
|
180
|
+
format_call=lambda a: f"{a.get('pattern', '?')} in {a.get('path') or '.'}",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def search_tools() -> list[ToolSpec]:
|
|
185
|
+
return [make_grep_tool(), make_glob_tool()]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
__all__ = ["make_glob_tool", "make_grep_tool", "search_tools"]
|