python-library-ai-agent 0.1.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.
Files changed (62) hide show
  1. ai_agent/__init__.py +66 -0
  2. ai_agent/agent.py +122 -0
  3. ai_agent/app/__init__.py +10 -0
  4. ai_agent/app/_workspace.py +127 -0
  5. ai_agent/app/app.py +321 -0
  6. ai_agent/app/harness_io.py +109 -0
  7. ai_agent/app/output_format.py +77 -0
  8. ai_agent/app/packet.py +39 -0
  9. ai_agent/app/session.py +742 -0
  10. ai_agent/app/session_store.py +85 -0
  11. ai_agent/builtin_tools/__init__.py +18 -0
  12. ai_agent/builtin_tools/current_time.py +39 -0
  13. ai_agent/builtin_tools/pack.py +20 -0
  14. ai_agent/builtin_tools/prefix.py +11 -0
  15. ai_agent/context.py +151 -0
  16. ai_agent/harness/__init__.py +3 -0
  17. ai_agent/harness/current_time.py +25 -0
  18. ai_agent/harness/harness.py +324 -0
  19. ai_agent/harness/process.py +115 -0
  20. ai_agent/harness/prompts.py +38 -0
  21. ai_agent/harness/sandbox.py +139 -0
  22. ai_agent/json_extract.py +70 -0
  23. ai_agent/listener.py +172 -0
  24. ai_agent/llm.py +39 -0
  25. ai_agent/llm_openai.py +117 -0
  26. ai_agent/loop.py +124 -0
  27. ai_agent/mcp_config.py +54 -0
  28. ai_agent/mcp_loader.py +110 -0
  29. ai_agent/memory/__init__.py +9 -0
  30. ai_agent/memory/compression_work.py +71 -0
  31. ai_agent/memory/compressor.py +339 -0
  32. ai_agent/memory/config.py +40 -0
  33. ai_agent/memory/context_builder.py +57 -0
  34. ai_agent/memory/memory_system.py +561 -0
  35. ai_agent/memory/models.py +76 -0
  36. ai_agent/memory/snapshot_merge.py +158 -0
  37. ai_agent/memory/store.py +107 -0
  38. ai_agent/memory/worker.py +227 -0
  39. ai_agent/plan/__init__.py +15 -0
  40. ai_agent/plan/complete.py +64 -0
  41. ai_agent/plan/delivery.py +41 -0
  42. ai_agent/plan/display.py +46 -0
  43. ai_agent/plan/models.py +44 -0
  44. ai_agent/plan/parse.py +39 -0
  45. ai_agent/plan/planner.py +204 -0
  46. ai_agent/plan/runner.py +281 -0
  47. ai_agent/react_tool_turn.py +39 -0
  48. ai_agent/rule/__init__.py +3 -0
  49. ai_agent/rule/rules.py +36 -0
  50. ai_agent/skill/__init__.py +5 -0
  51. ai_agent/skill/builtin_registry.py +56 -0
  52. ai_agent/skill/catalog.py +104 -0
  53. ai_agent/skill/frontmatter.py +83 -0
  54. ai_agent/skill/manager.py +486 -0
  55. ai_agent/skill/models.py +31 -0
  56. ai_agent/skill/roots.py +150 -0
  57. ai_agent/skill/skill_kit.py +80 -0
  58. ai_agent/skill/tool_declarations.py +68 -0
  59. ai_agent/tools.py +123 -0
  60. python_library_ai_agent-0.1.0.dist-info/METADATA +10 -0
  61. python_library_ai_agent-0.1.0.dist-info/RECORD +62 -0
  62. python_library_ai_agent-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from ai_agent.context import ChatMessage
7
+
8
+ _CONVERSATION_FILE = "conversation.json"
9
+
10
+
11
+ def conversation_path(session_root: Path) -> Path:
12
+ """会话根目录下的对话持久化文件路径。"""
13
+ return session_root / _CONVERSATION_FILE
14
+
15
+
16
+ def load_conversation(session_root: Path) -> list[ChatMessage]:
17
+ """
18
+ 从磁盘读取对话历史。
19
+
20
+ Args:
21
+ session_root: 会话工作区根
22
+
23
+ Returns:
24
+ 无文件或解析失败时返回空列表
25
+ """
26
+ path = conversation_path(session_root)
27
+ if not path.is_file():
28
+ return []
29
+ try:
30
+ raw = json.loads(path.read_text(encoding="utf-8"))
31
+ except (json.JSONDecodeError, OSError):
32
+ return []
33
+ if not isinstance(raw, list):
34
+ return []
35
+ messages: list[ChatMessage] = []
36
+ for item in raw:
37
+ if not isinstance(item, dict):
38
+ continue
39
+ role = item.get("role")
40
+ content = item.get("content")
41
+ if role not in ("system", "user", "assistant", "tool"):
42
+ continue
43
+ if not isinstance(content, str):
44
+ continue
45
+ name = item.get("name")
46
+ if name is not None and not isinstance(name, str):
47
+ name = None
48
+ messages.append(
49
+ ChatMessage(
50
+ role=role,
51
+ content=content,
52
+ name=name,
53
+ ),
54
+ )
55
+ return messages
56
+
57
+
58
+ def save_conversation(session_root: Path, messages: list[ChatMessage]) -> None:
59
+ """
60
+ 将会话对话历史写入磁盘。
61
+
62
+ Args:
63
+ session_root: 会话工作区根
64
+ messages: 待持久化的消息列表
65
+ """
66
+ session_root.mkdir(parents=True, exist_ok=True)
67
+ payload = [
68
+ {
69
+ "role": m.role,
70
+ "content": m.content,
71
+ **({"name": m.name} if m.name else {}),
72
+ }
73
+ for m in messages
74
+ ]
75
+ conversation_path(session_root).write_text(
76
+ json.dumps(payload, ensure_ascii=False, indent=2),
77
+ encoding="utf-8",
78
+ )
79
+
80
+
81
+ def clear_conversation(session_root: Path) -> None:
82
+ """删除会话对话持久化文件(若存在)。"""
83
+ path = conversation_path(session_root)
84
+ if path.is_file():
85
+ path.unlink()
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from ai_agent.builtin_tools.current_time import (
4
+ CURRENT_TIME_SHORT_NAME,
5
+ build_current_time_tool,
6
+ harness_current_time_tool_name,
7
+ )
8
+ from ai_agent.builtin_tools.pack import build_app_builtin_tools
9
+ from ai_agent.builtin_tools.prefix import APP_BUILTIN_PREFIX, builtin_tool_name
10
+
11
+ __all__ = [
12
+ "APP_BUILTIN_PREFIX",
13
+ "CURRENT_TIME_SHORT_NAME",
14
+ "build_app_builtin_tools",
15
+ "build_current_time_tool",
16
+ "builtin_tool_name",
17
+ "harness_current_time_tool_name",
18
+ ]
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ai_agent.builtin_tools.prefix import builtin_tool_name
6
+ from ai_agent.harness.current_time import get_current_time
7
+ from ai_agent.tools import Tool
8
+
9
+ CURRENT_TIME_SHORT_NAME = "current_time"
10
+
11
+ _CURRENT_TIME_PARAMETERS: dict[str, Any] = {
12
+ "type": "object",
13
+ "properties": {
14
+ "timezone": {
15
+ "type": "string",
16
+ "description": "IANA 时区名,如 Asia/Shanghai;省略则使用本机本地时区",
17
+ },
18
+ },
19
+ "additionalProperties": False,
20
+ }
21
+
22
+
23
+ def harness_current_time_tool_name() -> str:
24
+ """Harness 沙箱层同名工具的对外名,用于去重。"""
25
+ return "harness__current_time"
26
+
27
+
28
+ def build_current_time_tool() -> Tool:
29
+ """构造 ``builtin__current_time``,不依赖 Harness 工作区。"""
30
+ return Tool(
31
+ name=builtin_tool_name(CURRENT_TIME_SHORT_NAME),
32
+ description="获取当前日期与时间(ISO 8601),可按 IANA 时区名指定时区。",
33
+ parameters=_CURRENT_TIME_PARAMETERS,
34
+ handler=_run_current_time,
35
+ )
36
+
37
+
38
+ def _run_current_time(*, timezone: str = "") -> str:
39
+ return get_current_time(timezone)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from ai_agent.builtin_tools.current_time import build_current_time_tool
4
+ from ai_agent.tools import Tool
5
+
6
+
7
+ def build_app_builtin_tools(*, current_time: bool) -> list[Tool]:
8
+ """
9
+ 组装 AgentApp 可选的应用级内置工具(与 Harness 沙箱无关)。
10
+
11
+ Args:
12
+ current_time: 是否注册 ``builtin__current_time``
13
+
14
+ Returns:
15
+ 待并入 ToolRegistry 基础层的工具列表
16
+ """
17
+ tools: list[Tool] = []
18
+ if current_time:
19
+ tools.append(build_current_time_tool())
20
+ return tools
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ APP_BUILTIN_PREFIX = "builtin"
4
+
5
+
6
+ def builtin_tool_name(short: str) -> str:
7
+ """生成应用级内置工具对外名,形如 ``builtin__current_time``。"""
8
+ key = short.strip()
9
+ if not key:
10
+ raise ValueError("内置工具短名不能为空")
11
+ return f"{APP_BUILTIN_PREFIX}__{key}"
ai_agent/context.py ADDED
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from collections.abc import Callable
7
+ from typing import TYPE_CHECKING, Any, Literal
8
+
9
+ if TYPE_CHECKING:
10
+ from ai_agent.llm import LLMClient
11
+ from ai_agent.listener import AgentListener
12
+ from ai_agent.tools import ToolRegistry
13
+
14
+ MessageRole = Literal["system", "user", "assistant", "tool"]
15
+
16
+
17
+ class RunStatus(str, Enum):
18
+ """一轮运行的整体状态。"""
19
+
20
+ RUNNING = "running"
21
+ COMPLETED = "completed"
22
+ FAILED = "failed"
23
+ MAX_STEPS = "max_steps"
24
+
25
+
26
+ class RunPhaseKind(str, Enum):
27
+ """监听回调所归属的运行阶段,供前端区分规划与逐步执行。"""
28
+
29
+ PLANNING = "planning"
30
+ STEP = "step"
31
+ DIRECT = "direct"
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class RunPhase:
36
+ """
37
+ 单次 ReAct 或规划补全所处的阶段。
38
+
39
+ 规划阶段仅 ``kind=PLANNING``;逐步执行时带 ``step_index`` 与 ``step_id``。
40
+ """
41
+
42
+ kind: RunPhaseKind
43
+ step_index: int | None = None
44
+ step_id: str | None = None
45
+
46
+
47
+ @dataclass
48
+ class AgentContext:
49
+ """
50
+ Agent 运行环境:语言模型、工具表与 ReAct 步数上限。
51
+
52
+ 由 ``Agent`` 构造时创建;测试可替换 ``llm`` 等成员。
53
+ """
54
+
55
+ llm: LLMClient
56
+ tools: ToolRegistry
57
+ max_steps: int = 20
58
+ listeners: list[AgentListener] = field(default_factory=list)
59
+ on_run_begin: Callable[["RunContext"], None] | None = None
60
+ on_run_end: Callable[["RunContext"], None] | None = None
61
+
62
+
63
+ @dataclass
64
+ class ChatMessage:
65
+ """对话消息;用于历史记录与用户输入。"""
66
+
67
+ role: MessageRole
68
+ content: str
69
+ name: str | None = None
70
+
71
+ def to_api(self) -> dict[str, Any]:
72
+ item: dict[str, Any] = {"role": self.role, "content": self.content}
73
+ if self.name:
74
+ item["name"] = self.name
75
+ return item
76
+
77
+
78
+ @dataclass
79
+ class ToolInvocation:
80
+ """按顺序记录的一次工具调用;运行中更新思考与回答。"""
81
+
82
+ call_id: str
83
+ tool_name: str
84
+ arguments: dict[str, Any] = field(default_factory=dict)
85
+ thinking: str = ""
86
+ answer: str = ""
87
+ ok: bool = True
88
+
89
+
90
+ @dataclass
91
+ class RunContext:
92
+ """
93
+ 单轮运行的上下文:输入、按序工具调用与最终输出。
94
+
95
+ 运行上下文:系统提示与 messages;运行后读取 output 与 tool_invocations。
96
+ """
97
+
98
+ system_prompt: str = ""
99
+ ephemeral_skill_context: str = ""
100
+ messages: list[ChatMessage] = field(default_factory=list)
101
+ tool_invocations: list[ToolInvocation] = field(default_factory=list)
102
+ tool_turns: list[list[ToolInvocation]] = field(default_factory=list)
103
+ thinking: str = ""
104
+ output: str = ""
105
+ status: RunStatus = RunStatus.RUNNING
106
+ phase: RunPhase | None = None
107
+ def api_messages(self) -> list[dict[str, Any]]:
108
+ """组装送入语言模型的消息列表。"""
109
+ out: list[dict[str, Any]] = []
110
+ system = self._effective_system_prompt()
111
+ if system:
112
+ out.append({"role": "system", "content": system})
113
+ out.extend(m.to_api() for m in self.messages)
114
+ for turn in self.tool_turns:
115
+ tool_calls: list[dict[str, Any]] = []
116
+ for inv in turn:
117
+ tool_calls.append(
118
+ {
119
+ "id": inv.call_id,
120
+ "type": "function",
121
+ "function": {
122
+ "name": inv.tool_name,
123
+ "arguments": json.dumps(inv.arguments, ensure_ascii=False),
124
+ },
125
+ }
126
+ )
127
+ assistant: dict[str, Any] = {
128
+ "role": "assistant",
129
+ "content": turn[0].thinking or None,
130
+ "tool_calls": tool_calls,
131
+ }
132
+ out.append(assistant)
133
+ for inv in turn:
134
+ if inv.answer:
135
+ out.append(
136
+ {
137
+ "role": "tool",
138
+ "tool_call_id": inv.call_id,
139
+ "content": inv.answer,
140
+ }
141
+ )
142
+ return out
143
+
144
+ def _effective_system_prompt(self) -> str:
145
+ base = self.system_prompt.strip()
146
+ extra = self.ephemeral_skill_context.strip()
147
+ if base and extra:
148
+ return f"{base}\n\n{extra}"
149
+ if extra:
150
+ return extra
151
+ return base
@@ -0,0 +1,3 @@
1
+ from ai_agent.harness.harness import Harness
2
+
3
+ __all__ = ["Harness"]
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
5
+
6
+
7
+ def get_current_time(timezone: str = "") -> str:
8
+ """
9
+ 返回当前日期与时间。
10
+
11
+ Args:
12
+ timezone: IANA 时区名,如 Asia/Shanghai;留空则用本机本地时区
13
+
14
+ Returns:
15
+ ISO 8601 格式的时间字符串
16
+ """
17
+ if timezone.strip():
18
+ try:
19
+ tz = ZoneInfo(timezone.strip())
20
+ except ZoneInfoNotFoundError as exc:
21
+ raise ValueError(f"未知时区: {timezone}") from exc
22
+ now = datetime.now(tz)
23
+ else:
24
+ now = datetime.now().astimezone()
25
+ return now.isoformat(timespec="seconds")
@@ -0,0 +1,324 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from ai_agent.harness.current_time import get_current_time
8
+ from ai_agent.harness.process import run_python, run_shell
9
+ from ai_agent.harness.sandbox import HarnessSandbox
10
+ from ai_agent.skill.manager import SkillManager
11
+ from ai_agent.skill.skill_kit import SkillKit
12
+ from ai_agent.tools import Tool
13
+
14
+ _HARNESS_TOOL_PREFIX = "harness"
15
+
16
+
17
+ class Harness:
18
+ """
19
+ 隔离工作区上的文件与进程能力;与对话代理核分离,由调用方构造后导出为工具列表。
20
+
21
+ 路径均相对工作区根;可挂载只读技能区。工具说明与返回值不向模型暴露宿主机绝对路径。
22
+
23
+ Args:
24
+ workspace: 沙箱工作区根目录
25
+ skill_roots: 只读技能仓库根;与 skill_kit 二选一
26
+ skill_kit: 已构造的 SkillKit;与 skill_roots 二选一
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ workspace: Path | str,
32
+ *,
33
+ skill_roots: (
34
+ Mapping[str, Path | str] | Sequence[Path | str] | Path | str | None
35
+ ) = None,
36
+ skill_kit: SkillKit | None = None,
37
+ ) -> None:
38
+ if skill_kit is not None and skill_roots is not None:
39
+ raise ValueError("skill_kit 与 skill_roots 不可同时指定")
40
+ self._sandbox = HarnessSandbox(Path(workspace))
41
+ self._prefix = _HARNESS_TOOL_PREFIX
42
+ self._skill_kit: SkillKit | None = None
43
+ if skill_kit is not None:
44
+ self._skill_kit = skill_kit
45
+ elif skill_roots is not None:
46
+ self._skill_kit = SkillKit(skill_roots)
47
+
48
+ @property
49
+ def workspace(self) -> Path:
50
+ """沙箱工作区根目录(供应用侧配置与测试使用,勿写入模型可见文案)。"""
51
+ return self._sandbox.root
52
+
53
+ @property
54
+ def skill(self) -> SkillKit:
55
+ """已配置的 SkillKit;未配置 skill_roots 时访问会报错。"""
56
+ if self._skill_kit is None:
57
+ raise ValueError("未配置 skill_roots")
58
+ return self._skill_kit
59
+
60
+ def _tool_name(self, short: str) -> str:
61
+ return f"{self._prefix}__{short}"
62
+
63
+ def read_file(self, path: str, offset: int = 1, limit: int = 0) -> str:
64
+ """
65
+ 读取工作区内文本文件。
66
+
67
+ Args:
68
+ path: 相对工作区根的路径
69
+ offset: 起始行号,从 1 起
70
+ limit: 最多读取行数;0 表示读到末尾
71
+ """
72
+ return self._sandbox.read_text_file(path, offset=offset, limit=limit)
73
+
74
+ def write_file(self, path: str, content: str, append: bool = False) -> str:
75
+ """
76
+ 写入或追加工作区内文本文件。
77
+
78
+ Args:
79
+ path: 相对工作区根的路径
80
+ content: 文件内容
81
+ append: 为 True 时追加
82
+ """
83
+ return self._sandbox.write_text_file(path, content, append=append)
84
+
85
+ def list_files(
86
+ self,
87
+ path: str = "",
88
+ max_entries: int = 200,
89
+ pattern: str = "",
90
+ ) -> str:
91
+ """
92
+ 扫描工作区内文件与目录。
93
+
94
+ Args:
95
+ path: 相对工作区根的子目录;留空则扫描整个工作区
96
+ max_entries: 最多返回条数
97
+ pattern: 可选 glob,如 *.py
98
+ """
99
+ entries = self._sandbox.list_entries(
100
+ path,
101
+ max_entries=max_entries,
102
+ pattern=pattern,
103
+ )
104
+ if not entries:
105
+ return "(空)"
106
+ truncated = len(entries) >= max_entries
107
+ lines = "\n".join(entries)
108
+ if truncated:
109
+ lines += f"\n...(已达上限 {max_entries} 条)"
110
+ return lines
111
+
112
+ def run_shell(self, command: str, cwd: str = "", timeout_seconds: int = 0) -> str:
113
+ """
114
+ 在工作区内执行 shell 命令。
115
+
116
+ Args:
117
+ command: shell 命令
118
+ cwd: 相对工作区的子目录;留空则用工作区根
119
+ timeout_seconds: 超时秒数;0 则用默认
120
+ """
121
+ work_dir = (
122
+ self._sandbox.root
123
+ if not cwd.strip()
124
+ else self._sandbox.resolve_path(cwd)
125
+ )
126
+ if not work_dir.is_dir():
127
+ raise ValueError(f"cwd 不是目录: {cwd or '.'}")
128
+ return run_shell(
129
+ command,
130
+ work_dir=work_dir,
131
+ timeout_seconds=timeout_seconds,
132
+ )
133
+
134
+ def run_python(self, code: str, timeout_seconds: int = 0) -> str:
135
+ """
136
+ 在工作区根目录下执行 Python 代码片段。
137
+
138
+ Args:
139
+ code: Python 源码
140
+ timeout_seconds: 超时秒数;0 则用默认
141
+ """
142
+ return run_python(
143
+ code,
144
+ work_dir=self._sandbox.root,
145
+ timeout_seconds=timeout_seconds,
146
+ )
147
+
148
+ def workspace_info(self) -> str:
149
+ """说明隔离工作区约束(不包含宿主机绝对路径)。"""
150
+ return (
151
+ "隔离工作区已启用。所有路径均相对于工作区根;"
152
+ "无法读取或写入工作区外的文件。"
153
+ "使用 list_files 查看目录结构。"
154
+ )
155
+
156
+ def current_time(self, timezone: str = "") -> str:
157
+ """
158
+ 返回当前日期与时间。
159
+
160
+ Args:
161
+ timezone: IANA 时区名,如 Asia/Shanghai;留空则用本机本地时区
162
+ """
163
+ return get_current_time(timezone)
164
+
165
+ def build_skill_tools(self) -> list[Tool]:
166
+ """生成 skill 管理 Tool 列表;须已配置 skill_roots。"""
167
+ return self.skill.build_management_tools()
168
+
169
+ def build_all_tools(self) -> list[Tool]:
170
+ """合并沙箱、skill 管理与已启用子工具(未配置 skill_roots 时仅沙箱)。"""
171
+ tools = self.build_tools()
172
+ if self._skill_kit is not None:
173
+ tools = tools + self._skill_kit.build_all_flat_tools()
174
+ return tools
175
+
176
+ @property
177
+ def skill_manager(self) -> SkillManager:
178
+ """已配置的 SkillManager;未配置 skill_roots 时访问会报错。"""
179
+ return self.skill.manager
180
+
181
+ def build_tools(self) -> list[Tool]:
182
+ """生成沙箱区 Tool 列表。"""
183
+ specs: list[tuple[str, str, dict[str, Any], Any]] = [
184
+ (
185
+ "read_file",
186
+ "读取隔离工作区内文本文件,返回带行号的内容。",
187
+ {
188
+ "type": "object",
189
+ "properties": {
190
+ "path": {
191
+ "type": "string",
192
+ "description": "相对工作区根的文件路径",
193
+ },
194
+ "offset": {
195
+ "type": "integer",
196
+ "description": "起始行号,从 1 起,默认 1",
197
+ },
198
+ "limit": {
199
+ "type": "integer",
200
+ "description": "最多读取行数;0 表示读到末尾",
201
+ },
202
+ },
203
+ "required": ["path"],
204
+ "additionalProperties": False,
205
+ },
206
+ self.read_file,
207
+ ),
208
+ (
209
+ "write_file",
210
+ "写入或追加隔离工作区内文本文件,必要时创建父目录。",
211
+ {
212
+ "type": "object",
213
+ "properties": {
214
+ "path": {
215
+ "type": "string",
216
+ "description": "相对工作区根的文件路径",
217
+ },
218
+ "content": {"type": "string", "description": "要写入的文本"},
219
+ "append": {
220
+ "type": "boolean",
221
+ "description": "为 true 时追加,否则覆盖",
222
+ },
223
+ },
224
+ "required": ["path", "content"],
225
+ "additionalProperties": False,
226
+ },
227
+ self.write_file,
228
+ ),
229
+ (
230
+ "list_files",
231
+ "扫描隔离工作区内的文件与目录,仅返回相对路径。",
232
+ {
233
+ "type": "object",
234
+ "properties": {
235
+ "path": {
236
+ "type": "string",
237
+ "description": "相对工作区根的子目录;留空扫描整个工作区",
238
+ },
239
+ "max_entries": {
240
+ "type": "integer",
241
+ "description": "最多返回条数,默认 200",
242
+ },
243
+ "pattern": {
244
+ "type": "string",
245
+ "description": "可选 glob,如 *.txt",
246
+ },
247
+ },
248
+ "additionalProperties": False,
249
+ },
250
+ self.list_files,
251
+ ),
252
+ (
253
+ "run_shell",
254
+ "在隔离工作区内执行 shell 命令,返回退出码与标准输出/错误。",
255
+ {
256
+ "type": "object",
257
+ "properties": {
258
+ "command": {"type": "string", "description": "shell 命令"},
259
+ "cwd": {
260
+ "type": "string",
261
+ "description": "相对工作区的子目录;留空则用工作区根",
262
+ },
263
+ "timeout_seconds": {
264
+ "type": "integer",
265
+ "description": "超时秒数,默认 120,上限 600",
266
+ },
267
+ },
268
+ "required": ["command"],
269
+ "additionalProperties": False,
270
+ },
271
+ self.run_shell,
272
+ ),
273
+ (
274
+ "run_python",
275
+ "在隔离工作区根目录下执行 Python 代码片段。",
276
+ {
277
+ "type": "object",
278
+ "properties": {
279
+ "code": {"type": "string", "description": "Python 源码"},
280
+ "timeout_seconds": {
281
+ "type": "integer",
282
+ "description": "超时秒数,默认 120,上限 600",
283
+ },
284
+ },
285
+ "required": ["code"],
286
+ "additionalProperties": False,
287
+ },
288
+ self.run_python,
289
+ ),
290
+ (
291
+ "workspace_info",
292
+ "查看隔离工作区的使用约束。",
293
+ {
294
+ "type": "object",
295
+ "properties": {},
296
+ "additionalProperties": False,
297
+ },
298
+ self.workspace_info,
299
+ ),
300
+ (
301
+ "current_time",
302
+ "获取当前日期与时间,可按 IANA 时区名指定时区。",
303
+ {
304
+ "type": "object",
305
+ "properties": {
306
+ "timezone": {
307
+ "type": "string",
308
+ "description": "IANA 时区名,如 Asia/Shanghai;省略则使用本机本地时区",
309
+ },
310
+ },
311
+ "additionalProperties": False,
312
+ },
313
+ self.current_time,
314
+ ),
315
+ ]
316
+ return [
317
+ Tool(
318
+ name=self._tool_name(short),
319
+ description=description,
320
+ parameters=parameters,
321
+ handler=handler,
322
+ )
323
+ for short, description, parameters, handler in specs
324
+ ]