claxcode 0.1.1__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.
- claxcode/__init__.py +3 -0
- claxcode/api/__init__.py +1 -0
- claxcode/api/chat.py +51 -0
- claxcode/api/session.py +66 -0
- claxcode/api/subagents.py +82 -0
- claxcode/api/tools.py +38 -0
- claxcode/cli.py +1214 -0
- claxcode/cli_completion.py +174 -0
- claxcode/config.py +54 -0
- claxcode/core/__init__.py +1 -0
- claxcode/core/agent.py +327 -0
- claxcode/core/checkpoint.py +100 -0
- claxcode/core/context.py +114 -0
- claxcode/core/models.py +446 -0
- claxcode/extensions/__init__.py +1 -0
- claxcode/extensions/hooks.py +131 -0
- claxcode/extensions/mcp.py +13 -0
- claxcode/extensions/plugins.py +10 -0
- claxcode/extensions/skills.py +205 -0
- claxcode/main.py +55 -0
- claxcode/memory/__init__.py +23 -0
- claxcode/memory/auto_memory.py +68 -0
- claxcode/memory/long_term.py +196 -0
- claxcode/memory/project_context.py +20 -0
- claxcode/memory/project_memory.py +194 -0
- claxcode/memory/session_store.py +206 -0
- claxcode/models/__init__.py +1 -0
- claxcode/models/session.py +25 -0
- claxcode/models/subagent.py +25 -0
- claxcode/models/tool.py +24 -0
- claxcode/security/__init__.py +1 -0
- claxcode/security/audit.py +54 -0
- claxcode/security/permissions.py +29 -0
- claxcode/skills/bug-fix/SKILL.md +33 -0
- claxcode/skills/code-review/SKILL.md +35 -0
- claxcode/skills/python-refactor/SKILL.md +22 -0
- claxcode/skills/task-workflow/SKILL.md +38 -0
- claxcode/subagents/__init__.py +1 -0
- claxcode/subagents/base.py +108 -0
- claxcode/subagents/executor.py +16 -0
- claxcode/subagents/explore.py +25 -0
- claxcode/subagents/planner.py +25 -0
- claxcode/tools/__init__.py +14 -0
- claxcode/tools/admin.py +170 -0
- claxcode/tools/base.py +19 -0
- claxcode/tools/browser.py +499 -0
- claxcode/tools/code_intelligence.py +68 -0
- claxcode/tools/custom_example.py +76 -0
- claxcode/tools/execution.py +114 -0
- claxcode/tools/file_operations.py +287 -0
- claxcode/tools/memory_tool.py +29 -0
- claxcode/tools/registry.py +35 -0
- claxcode/tools/search.py +157 -0
- claxcode/tools/ssh.py +534 -0
- claxcode/tools/web.py +83 -0
- claxcode-0.1.1.dist-info/METADATA +359 -0
- claxcode-0.1.1.dist-info/RECORD +60 -0
- claxcode-0.1.1.dist-info/WHEEL +4 -0
- claxcode-0.1.1.dist-info/entry_points.txt +3 -0
- claxcode-0.1.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Task Workflow
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
通用任务处理方法论:接到任务时按"分类 → 探索 → 计划 → 小步执行 → 验证 → 汇报"的流程工作,避免臆测和返工。
|
|
6
|
+
|
|
7
|
+
## Instructions
|
|
8
|
+
|
|
9
|
+
处理任何任务时,先判断类型,再按对应流程执行:
|
|
10
|
+
|
|
11
|
+
**1. 任务分类(先做)**
|
|
12
|
+
- 问答/探索:只需读懂代码 → 直接用搜索和读取工具回答,不改文件。
|
|
13
|
+
- 修改/新功能:需要动代码 → 走完整流程。
|
|
14
|
+
- 调试:复现优先(见 bug-fix 方法论)。
|
|
15
|
+
|
|
16
|
+
**2. 探索先行(修改前必做)**
|
|
17
|
+
- 用 glob/grep 定位相关文件,用 read 读懂现状;绝不猜测路径、函数签名或已有实现。
|
|
18
|
+
- 找不到时换关键词再搜,而不是编造。
|
|
19
|
+
|
|
20
|
+
**3. 计划(超过 3 步的任务必做)**
|
|
21
|
+
- 用编号列出步骤,每步可独立验证;先易后难,先根因后表象。
|
|
22
|
+
- 计划内只包含任务要求的事,不顺带重构、不扩大范围。
|
|
23
|
+
|
|
24
|
+
**4. 小步执行**
|
|
25
|
+
- 一次改一个步骤,改完立刻验证(读回文件/跑相关测试),再进行下一步。
|
|
26
|
+
- 写代码遵循项目已有风格和编码规范:最少代码、最简单实现、扁平不嵌套。
|
|
27
|
+
|
|
28
|
+
**5. 验证(汇报前必做)**
|
|
29
|
+
- 有测试跑测试;没测试至少读回改动确认。
|
|
30
|
+
- 验证失败就继续修,不要说"应该没问题"。
|
|
31
|
+
|
|
32
|
+
**6. 汇报**
|
|
33
|
+
- 说清改了哪些文件、验证结果是什么;没验证的部分明确标注"未验证"。
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
- `task`: 按完整方法论处理一个任务(用法:/task <任务描述>)
|
|
38
|
+
- `plan`: 只输出执行计划,不动手修改(用法:/plan <任务描述>)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Subagent system for ClaxCode."""
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Base subagent with isolated context and restricted tools."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from typing import Any
|
|
5
|
+
from uuid import UUID, uuid4
|
|
6
|
+
|
|
7
|
+
from claxcode.core.agent import DEFAULT_SYSTEM_PROMPT
|
|
8
|
+
from claxcode.core.context import ContextManager
|
|
9
|
+
from claxcode.core.models import get_provider
|
|
10
|
+
from claxcode.tools.registry import registry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Subagent:
|
|
14
|
+
"""Isolated subagent for delegated tasks."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
session_id: UUID,
|
|
19
|
+
agent_type: str,
|
|
20
|
+
instruction: str,
|
|
21
|
+
allowed_tools: list[str] | None = None,
|
|
22
|
+
max_iterations: int = 5,
|
|
23
|
+
):
|
|
24
|
+
self.id = uuid4()
|
|
25
|
+
self.session_id = session_id
|
|
26
|
+
self.agent_type = agent_type
|
|
27
|
+
self.instruction = instruction
|
|
28
|
+
self.allowed_tools = allowed_tools
|
|
29
|
+
self.max_iterations = max_iterations
|
|
30
|
+
self.provider = get_provider()
|
|
31
|
+
self.context = ContextManager(
|
|
32
|
+
system_prompt=f"{DEFAULT_SYSTEM_PROMPT}\n\n你是一个 {agent_type} 子代理。"
|
|
33
|
+
f"专注于你的专业领域并简洁地报告发现。"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def run(self) -> AsyncIterator[dict[str, Any]]:
|
|
37
|
+
"""Run the subagent and stream progress events."""
|
|
38
|
+
yield {"type": "status", "status": "running"}
|
|
39
|
+
self.context.add_message("user", self.instruction)
|
|
40
|
+
|
|
41
|
+
for i in range(self.max_iterations):
|
|
42
|
+
tools = self._build_tools()
|
|
43
|
+
content_buffer = ""
|
|
44
|
+
pending_tool_calls = []
|
|
45
|
+
|
|
46
|
+
async for chunk in self.provider.stream_chat(
|
|
47
|
+
messages=self.context.get_messages(),
|
|
48
|
+
tools=tools if tools else None,
|
|
49
|
+
):
|
|
50
|
+
if isinstance(chunk, str):
|
|
51
|
+
content_buffer += chunk
|
|
52
|
+
elif isinstance(chunk, dict) and chunk.get("type") == "tool_call":
|
|
53
|
+
pending_tool_calls.append(chunk)
|
|
54
|
+
|
|
55
|
+
if not pending_tool_calls:
|
|
56
|
+
self.context.add_message("assistant", content_buffer)
|
|
57
|
+
yield {"type": "result", "content": content_buffer}
|
|
58
|
+
yield {"type": "status", "status": "completed"}
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
import json
|
|
62
|
+
assistant_tool_calls = []
|
|
63
|
+
for tc in pending_tool_calls:
|
|
64
|
+
assistant_tool_calls.append(
|
|
65
|
+
{
|
|
66
|
+
"id": tc["id"],
|
|
67
|
+
"type": "function",
|
|
68
|
+
"function": {
|
|
69
|
+
"name": tc["name"],
|
|
70
|
+
"arguments": json.dumps(tc["arguments"]),
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
self.context.add_assistant_tool_calls(assistant_tool_calls)
|
|
75
|
+
|
|
76
|
+
for tc in pending_tool_calls:
|
|
77
|
+
result = await self._execute_tool(tc["name"], tc["arguments"])
|
|
78
|
+
self.context.add_tool_result(tc["id"], tc["name"], result)
|
|
79
|
+
|
|
80
|
+
final_messages = self.context.get_messages()
|
|
81
|
+
final_content = "\n".join(m.get("content", "") for m in final_messages if m["role"] == "assistant")
|
|
82
|
+
yield {"type": "result", "content": final_content}
|
|
83
|
+
yield {"type": "status", "status": "completed"}
|
|
84
|
+
|
|
85
|
+
def _build_tools(self) -> list[dict[str, Any]]:
|
|
86
|
+
all_tools = registry.list_tools()
|
|
87
|
+
if self.allowed_tools:
|
|
88
|
+
all_tools = [t for t in all_tools if t.name in self.allowed_tools]
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
"type": "function",
|
|
92
|
+
"function": {
|
|
93
|
+
"name": t.name,
|
|
94
|
+
"description": t.description,
|
|
95
|
+
"parameters": t.input_schema,
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
for t in all_tools
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
async def _execute_tool(self, name: str, args: dict) -> str:
|
|
102
|
+
t = registry.get(name)
|
|
103
|
+
if t is None:
|
|
104
|
+
return f"错误:工具 {name} 不存在"
|
|
105
|
+
try:
|
|
106
|
+
return await t.execute(**args)
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
return f"错误:{exc}"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Executor subagent — full-capability multi-step task executor."""
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from claxcode.subagents.base import Subagent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExecutorSubagent(Subagent):
|
|
9
|
+
def __init__(self, session_id: UUID, instruction: str):
|
|
10
|
+
super().__init__(
|
|
11
|
+
session_id=session_id,
|
|
12
|
+
agent_type="executor",
|
|
13
|
+
instruction=instruction,
|
|
14
|
+
allowed_tools=None, # all tools
|
|
15
|
+
max_iterations=10,
|
|
16
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Explore subagent — read-only codebase explorer."""
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from claxcode.subagents.base import Subagent
|
|
6
|
+
|
|
7
|
+
READONLY_TOOLS = [
|
|
8
|
+
"glob_search",
|
|
9
|
+
"grep_search",
|
|
10
|
+
"explore_codebase",
|
|
11
|
+
"read_file",
|
|
12
|
+
"find_definition",
|
|
13
|
+
"find_references",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExploreSubagent(Subagent):
|
|
18
|
+
def __init__(self, session_id: UUID, instruction: str):
|
|
19
|
+
super().__init__(
|
|
20
|
+
session_id=session_id,
|
|
21
|
+
agent_type="explore",
|
|
22
|
+
instruction=instruction,
|
|
23
|
+
allowed_tools=READONLY_TOOLS,
|
|
24
|
+
max_iterations=8,
|
|
25
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Planner subagent — research-oriented for plan-mode analysis."""
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from claxcode.subagents.base import Subagent
|
|
6
|
+
|
|
7
|
+
READONLY_TOOLS = [
|
|
8
|
+
"glob_search",
|
|
9
|
+
"grep_search",
|
|
10
|
+
"explore_codebase",
|
|
11
|
+
"read_file",
|
|
12
|
+
"find_definition",
|
|
13
|
+
"find_references",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PlannerSubagent(Subagent):
|
|
18
|
+
def __init__(self, session_id: UUID, instruction: str):
|
|
19
|
+
super().__init__(
|
|
20
|
+
session_id=session_id,
|
|
21
|
+
agent_type="planner",
|
|
22
|
+
instruction=instruction,
|
|
23
|
+
allowed_tools=READONLY_TOOLS,
|
|
24
|
+
max_iterations=5,
|
|
25
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Tool system for ClaxCode."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import pkgutil
|
|
5
|
+
|
|
6
|
+
from claxcode.tools.base import Tool # noqa: F401
|
|
7
|
+
from claxcode.tools.registry import registry, tool # noqa: F401
|
|
8
|
+
|
|
9
|
+
_EXCLUDED = {"base", "registry", "__init__"}
|
|
10
|
+
|
|
11
|
+
# Auto-discover and register all built-in tools
|
|
12
|
+
for _mod_info in pkgutil.iter_modules(__path__):
|
|
13
|
+
if _mod_info.name not in _EXCLUDED:
|
|
14
|
+
importlib.import_module(f"claxcode.tools.{_mod_info.name}")
|
claxcode/tools/admin.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Admin tools for runtime extension."""
|
|
2
|
+
|
|
3
|
+
import textwrap
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from claxcode.tools.base import Tool
|
|
7
|
+
from claxcode.tools.registry import registry, tool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@tool
|
|
11
|
+
class AdminAddTool(Tool):
|
|
12
|
+
"""Dynamically add a new tool at runtime by providing its Python source."""
|
|
13
|
+
|
|
14
|
+
name = "admin_add_tool"
|
|
15
|
+
description = (
|
|
16
|
+
"动态注册新工具。"
|
|
17
|
+
"提供工具名称、描述、JSON 输入模式和 execute() 方法体作为字符串。"
|
|
18
|
+
"代码在受限环境中运行,仅包含基本内置函数。"
|
|
19
|
+
)
|
|
20
|
+
input_schema = {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {
|
|
23
|
+
"name": {"type": "string", "description": "唯一的工具标识符"},
|
|
24
|
+
"description": {"type": "string", "description": "工具的功能描述"},
|
|
25
|
+
"category": {"type": "string", "default": "custom"},
|
|
26
|
+
"required_permissions": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {"type": "string"},
|
|
29
|
+
"default": [],
|
|
30
|
+
},
|
|
31
|
+
"input_schema": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"description": "工具参数的 JSON Schema",
|
|
34
|
+
},
|
|
35
|
+
"code": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": (
|
|
38
|
+
"execute() 方法体的 Python 源代码。"
|
|
39
|
+
"仅使用 'self' 和标准库。返回一个字符串。"
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
"required": ["name", "description", "code"],
|
|
44
|
+
}
|
|
45
|
+
category = "admin"
|
|
46
|
+
required_permissions = ["write"]
|
|
47
|
+
|
|
48
|
+
async def execute(
|
|
49
|
+
self,
|
|
50
|
+
name: str,
|
|
51
|
+
description: str,
|
|
52
|
+
code: str,
|
|
53
|
+
category: str = "custom",
|
|
54
|
+
required_permissions: list[str] | None = None,
|
|
55
|
+
input_schema: dict[str, Any] | None = None,
|
|
56
|
+
**kwargs: Any,
|
|
57
|
+
) -> str:
|
|
58
|
+
if registry.get(name):
|
|
59
|
+
return f"错误:工具 '{name}' 已存在。"
|
|
60
|
+
|
|
61
|
+
# Sanitize name
|
|
62
|
+
if not name.replace("_", "").isalnum():
|
|
63
|
+
return "错误:工具名称只能包含字母数字或下划线。"
|
|
64
|
+
|
|
65
|
+
safe_name = "".join(ch if ch.isalnum() else "_" for ch in name)
|
|
66
|
+
class_name = f"Dyn{safe_name.title()}"
|
|
67
|
+
|
|
68
|
+
# Build the full class source
|
|
69
|
+
schema = input_schema or {"type": "object", "properties": {}, "required": []}
|
|
70
|
+
perms = required_permissions or []
|
|
71
|
+
perms_repr = repr(perms)
|
|
72
|
+
schema_repr = repr(schema)
|
|
73
|
+
|
|
74
|
+
body = textwrap.dedent(code).strip()
|
|
75
|
+
# Indent to 12 spaces so it sits inside the try block (try is at 8 spaces)
|
|
76
|
+
wrapped_code = textwrap.indent(body, " ")
|
|
77
|
+
|
|
78
|
+
class_source = f"""
|
|
79
|
+
class {class_name}(Tool):
|
|
80
|
+
name = {repr(name)}
|
|
81
|
+
description = {repr(description)}
|
|
82
|
+
input_schema = {schema_repr}
|
|
83
|
+
category = {repr(category)}
|
|
84
|
+
required_permissions = {perms_repr}
|
|
85
|
+
|
|
86
|
+
async def execute(self, **kwargs) -> str:
|
|
87
|
+
try:
|
|
88
|
+
{wrapped_code}
|
|
89
|
+
except Exception as _exc:
|
|
90
|
+
return f"{repr(name)} 中出错: {{_exc}}"
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
# Restricted execution environment
|
|
94
|
+
allowed_builtins = {
|
|
95
|
+
"__build_class__": __build_class__,
|
|
96
|
+
"Exception": Exception,
|
|
97
|
+
"BaseException": BaseException,
|
|
98
|
+
"ValueError": ValueError,
|
|
99
|
+
"TypeError": TypeError,
|
|
100
|
+
"KeyError": KeyError,
|
|
101
|
+
"IndexError": IndexError,
|
|
102
|
+
"AttributeError": AttributeError,
|
|
103
|
+
"RuntimeError": RuntimeError,
|
|
104
|
+
"len": len,
|
|
105
|
+
"str": str,
|
|
106
|
+
"int": int,
|
|
107
|
+
"float": float,
|
|
108
|
+
"bool": bool,
|
|
109
|
+
"list": list,
|
|
110
|
+
"dict": dict,
|
|
111
|
+
"set": set,
|
|
112
|
+
"tuple": tuple,
|
|
113
|
+
"range": range,
|
|
114
|
+
"enumerate": enumerate,
|
|
115
|
+
"zip": zip,
|
|
116
|
+
"map": map,
|
|
117
|
+
"filter": filter,
|
|
118
|
+
"sum": sum,
|
|
119
|
+
"min": min,
|
|
120
|
+
"max": max,
|
|
121
|
+
"sorted": sorted,
|
|
122
|
+
"reversed": reversed,
|
|
123
|
+
"any": any,
|
|
124
|
+
"all": all,
|
|
125
|
+
"type": type,
|
|
126
|
+
"callable": callable,
|
|
127
|
+
"abs": abs,
|
|
128
|
+
"round": round,
|
|
129
|
+
"print": print,
|
|
130
|
+
"repr": repr,
|
|
131
|
+
"format": format,
|
|
132
|
+
"isinstance": isinstance,
|
|
133
|
+
"hasattr": hasattr,
|
|
134
|
+
"getattr": getattr,
|
|
135
|
+
"setattr": setattr,
|
|
136
|
+
"open": open,
|
|
137
|
+
"Path": __import__("pathlib").Path,
|
|
138
|
+
"json": __import__("json"),
|
|
139
|
+
"re": __import__("re"),
|
|
140
|
+
"textwrap": __import__("textwrap"),
|
|
141
|
+
"datetime": __import__("datetime"),
|
|
142
|
+
"typing": __import__("typing"),
|
|
143
|
+
"httpx": __import__("httpx"),
|
|
144
|
+
"math": __import__("math"),
|
|
145
|
+
"random": __import__("random"),
|
|
146
|
+
"hashlib": __import__("hashlib"),
|
|
147
|
+
"base64": __import__("base64"),
|
|
148
|
+
"urllib": __import__("urllib"),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
namespace = {
|
|
152
|
+
"__builtins__": allowed_builtins,
|
|
153
|
+
"__name__": "__claxcode_dynamic__",
|
|
154
|
+
"__doc__": None,
|
|
155
|
+
"Tool": Tool,
|
|
156
|
+
"Any": Any,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
exec(class_source, namespace) # noqa: S102
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
return f"编译工具时出错: {exc}\n\n生成的源代码:\n{class_source}"
|
|
163
|
+
|
|
164
|
+
tool_cls = namespace.get(class_name)
|
|
165
|
+
if tool_cls is None or not isinstance(tool_cls, type) or not issubclass(tool_cls, Tool):
|
|
166
|
+
return "错误:编译的类不是有效的 Tool 子类。"
|
|
167
|
+
|
|
168
|
+
instance = tool_cls()
|
|
169
|
+
registry.register(instance)
|
|
170
|
+
return f"✅ 工具 '{name}' 已成功注册到类别 '{category}'。"
|
claxcode/tools/base.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Tool base class."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Tool(ABC):
|
|
8
|
+
"""Base class for all ClaxCode tools."""
|
|
9
|
+
|
|
10
|
+
name: str = ""
|
|
11
|
+
description: str = ""
|
|
12
|
+
input_schema: dict[str, Any] = {}
|
|
13
|
+
category: str = "general"
|
|
14
|
+
required_permissions: list[str] = []
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
async def execute(self, **kwargs: Any) -> str:
|
|
18
|
+
"""Execute the tool and return a string result."""
|
|
19
|
+
...
|