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
ai_agent/__init__.py ADDED
@@ -0,0 +1,66 @@
1
+ from ai_agent.agent import Agent
2
+ from ai_agent.app import AgentApp, AgentSession, RunInputPacket, RunOutputPacket
3
+ from ai_agent.harness import Harness
4
+ from ai_agent.memory import BuiltMemoryContext, MemoryConfig, MemorySystem
5
+ from ai_agent.skill import BuiltinToolRegistry, SkillKit, SkillManager
6
+ from ai_agent.context import (
7
+ AgentContext,
8
+ ChatMessage,
9
+ RunContext,
10
+ RunPhase,
11
+ RunPhaseKind,
12
+ RunStatus,
13
+ ToolInvocation,
14
+ )
15
+ from ai_agent.listener import AgentListener
16
+ from ai_agent.mcp_config import McpConfig, McpStdioServerConfig, parse_mcp_config
17
+ from ai_agent.mcp_loader import MCPToolLoader
18
+ from ai_agent.plan import (
19
+ Plan,
20
+ PlanParseError,
21
+ parse_plan_text,
22
+ PlanPlanner,
23
+ PlanRunResult,
24
+ PlanRunner,
25
+ PlanStep,
26
+ PlanStepFailedError,
27
+ )
28
+ from ai_agent.rule import RuleSet
29
+ from ai_agent.tools import Tool
30
+
31
+ __all__ = [
32
+ "Agent",
33
+ "AgentApp",
34
+ "AgentSession",
35
+ "RunInputPacket",
36
+ "RunOutputPacket",
37
+ "BuiltMemoryContext",
38
+ "Harness",
39
+ "MemoryConfig",
40
+ "MemorySystem",
41
+ "BuiltinToolRegistry",
42
+ "SkillKit",
43
+ "SkillManager",
44
+ "AgentContext",
45
+ "ChatMessage",
46
+ "AgentListener",
47
+ "McpConfig",
48
+ "McpStdioServerConfig",
49
+ "MCPToolLoader",
50
+ "RunContext",
51
+ "RunPhase",
52
+ "RunPhaseKind",
53
+ "RunStatus",
54
+ "Tool",
55
+ "ToolInvocation",
56
+ "parse_mcp_config",
57
+ "RuleSet",
58
+ "Plan",
59
+ "PlanParseError",
60
+ "parse_plan_text",
61
+ "PlanPlanner",
62
+ "PlanRunResult",
63
+ "PlanRunner",
64
+ "PlanStep",
65
+ "PlanStepFailedError",
66
+ ]
ai_agent/agent.py ADDED
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from pathlib import Path
5
+
6
+ from openai import AsyncOpenAI
7
+
8
+ from ai_agent.context import (
9
+ AgentContext,
10
+ ChatMessage,
11
+ RunContext,
12
+ RunPhase,
13
+ RunPhaseKind,
14
+ )
15
+ from ai_agent.listener import AgentListener, normalize_listeners
16
+ from ai_agent.llm import LLMClient
17
+ from ai_agent.llm_openai import OpenAILLM
18
+ from ai_agent.loop import ReactLoop
19
+ from ai_agent.rule import RuleSet
20
+ from ai_agent.tools import Tool, ToolRegistry
21
+
22
+
23
+ class Agent:
24
+ """
25
+ ReAct 运行入口:构造时组装 ``AgentContext`` 并驱动多步循环。
26
+
27
+ 系统提示由构造时的 ``rule_paths`` 指定文件读入;``run`` 只接收消息列表。
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ api_key: str,
34
+ model: str,
35
+ base_url: str,
36
+ temperature: float | None = None,
37
+ max_tokens: int | None = None,
38
+ thinking_enabled: bool = False,
39
+ tools: list[Tool] | ToolRegistry | None = None,
40
+ rule_paths: list[str] | list[Path] | tuple[str, ...] | tuple[Path, ...] | None = None,
41
+ max_steps: int = 20,
42
+ listeners: AgentListener | Iterable[AgentListener] | None = None,
43
+ ) -> None:
44
+ if not api_key.strip():
45
+ raise ValueError("api_key 不能为空")
46
+ if not model.strip():
47
+ raise ValueError("model 不能为空")
48
+ if not base_url.strip():
49
+ raise ValueError("base_url 不能为空")
50
+
51
+ api_key = api_key.strip()
52
+ model = model.strip()
53
+ base_url = base_url.strip().rstrip("/")
54
+
55
+ client = AsyncOpenAI(api_key=api_key, base_url=base_url)
56
+ llm: LLMClient = OpenAILLM(
57
+ client,
58
+ model=model,
59
+ base_url=base_url,
60
+ temperature=temperature,
61
+ max_tokens=max_tokens,
62
+ thinking_enabled=thinking_enabled,
63
+ )
64
+ if isinstance(tools, ToolRegistry):
65
+ registry = tools
66
+ else:
67
+ registry = ToolRegistry(tools)
68
+ self.context = AgentContext(
69
+ llm=llm,
70
+ tools=registry,
71
+ max_steps=max_steps,
72
+ listeners=normalize_listeners(listeners),
73
+ )
74
+ self._rules = RuleSet(rule_paths)
75
+
76
+ @property
77
+ def rules(self) -> RuleSet:
78
+ """构造时绑定的规则集。"""
79
+ return self._rules
80
+
81
+ async def run(
82
+ self,
83
+ *,
84
+ messages: list[ChatMessage],
85
+ ) -> str:
86
+ """
87
+ 运行一轮 ReAct 循环,返回最终回答文本。
88
+ """
89
+ return await self.run_with_system(
90
+ system_prompt=self._rules.build_system_prompt(),
91
+ messages=messages,
92
+ )
93
+
94
+ async def run_with_system(
95
+ self,
96
+ *,
97
+ system_prompt: str,
98
+ messages: list[ChatMessage],
99
+ phase: RunPhase | None = None,
100
+ ) -> str:
101
+ """
102
+ 使用给定系统提示运行一轮(供会话层叠加计划步说明等)。
103
+
104
+ Args:
105
+ system_prompt: 本轮送入模型的完整系统提示
106
+ messages: 用户与助手历史及本轮输入
107
+ """
108
+ run = RunContext(
109
+ system_prompt=system_prompt,
110
+ messages=list(messages),
111
+ phase=phase or RunPhase(kind=RunPhaseKind.DIRECT),
112
+ )
113
+ if self.context.on_run_begin is not None:
114
+ self.context.on_run_begin(run)
115
+ try:
116
+ loop = ReactLoop(self.context)
117
+ async for updated in loop.run(run):
118
+ run = updated
119
+ return run.output
120
+ finally:
121
+ if self.context.on_run_end is not None:
122
+ self.context.on_run_end(run)
@@ -0,0 +1,10 @@
1
+ from ai_agent.app.app import AgentApp
2
+ from ai_agent.app.packet import RunInputPacket, RunOutputPacket
3
+ from ai_agent.app.session import AgentSession
4
+
5
+ __all__ = [
6
+ "AgentApp",
7
+ "AgentSession",
8
+ "RunInputPacket",
9
+ "RunOutputPacket",
10
+ ]
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ _SESSIONS_DIR = "sessions"
7
+ SESSION_HARNESS_SUBDIR = "harness"
8
+ SESSION_MEMORY_SUBDIR = "memory"
9
+
10
+
11
+ def resolve_app_sandbox_root(sandbox: Path | str) -> Path:
12
+ """
13
+ 解析并创建总沙箱根目录。
14
+
15
+ Args:
16
+ sandbox: 总沙箱路径
17
+
18
+ Returns:
19
+ 规范化后的绝对路径
20
+ """
21
+ root = Path(sandbox).expanduser().resolve()
22
+ root.mkdir(parents=True, exist_ok=True)
23
+ if not root.is_dir():
24
+ raise ValueError(f"沙箱根须为目录: {root}")
25
+ return root
26
+
27
+
28
+ def validate_session_id(session_id: str) -> str:
29
+ """
30
+ 校验会话标识,拒绝路径穿越与分隔符。
31
+
32
+ Args:
33
+ session_id: 调用方提供的会话 id
34
+
35
+ Returns:
36
+ 去首尾空白后的 id
37
+ """
38
+ cleaned = session_id.strip()
39
+ if not cleaned:
40
+ raise ValueError("session_id 不能为空")
41
+ if cleaned in (".", ".."):
42
+ raise ValueError(f"非法 session_id: {session_id}")
43
+ for char in ("/", "\\", "\0"):
44
+ if char in cleaned:
45
+ raise ValueError(f"session_id 不能包含路径分隔符: {session_id}")
46
+ return cleaned
47
+
48
+
49
+ def session_workspace(sandbox_root: Path, session_id: str) -> Path:
50
+ """
51
+ 在总沙箱下为某会话分配子工作区目录。
52
+
53
+ Args:
54
+ sandbox_root: 总沙箱根(已 resolve)
55
+ session_id: 会话 id
56
+
57
+ Returns:
58
+ 该会话专属子目录(已创建)
59
+ """
60
+ label = validate_session_id(session_id)
61
+ target = (sandbox_root / _SESSIONS_DIR / label).resolve()
62
+ try:
63
+ target.relative_to(sandbox_root)
64
+ except ValueError as exc:
65
+ raise ValueError(f"会话工作区越出总沙箱: {session_id}") from exc
66
+ target.mkdir(parents=True, exist_ok=True)
67
+ if not target.is_dir():
68
+ raise ValueError(f"无法创建会话工作区: {session_id}")
69
+ return target
70
+
71
+
72
+ def _validate_session_subdir_name(name: str, *, label: str) -> str:
73
+ cleaned = name.strip()
74
+ if not cleaned:
75
+ raise ValueError(f"{label} 不能为空")
76
+ if cleaned in (".", ".."):
77
+ raise ValueError(f"非法 {label}: {name}")
78
+ for char in ("/", "\\", "\0"):
79
+ if char in cleaned:
80
+ raise ValueError(f"{label} 不能包含路径分隔符: {name}")
81
+ return cleaned
82
+
83
+
84
+ def session_harness_workspace(session_root: Path) -> Path:
85
+ """
86
+ 在会话工作区下分配 Harness 隔离子目录。
87
+
88
+ Args:
89
+ session_root: 会话根目录(已 resolve)
90
+
91
+ Returns:
92
+ Harness 工作区路径(已创建)
93
+ """
94
+ root = session_root.resolve()
95
+ sub = _validate_session_subdir_name(
96
+ SESSION_HARNESS_SUBDIR,
97
+ label="harness 子目录",
98
+ )
99
+ target = (root / sub).resolve()
100
+ try:
101
+ target.relative_to(root)
102
+ except ValueError as exc:
103
+ raise ValueError("Harness 子目录越出会话工作区") from exc
104
+ target.mkdir(parents=True, exist_ok=True)
105
+ if not target.is_dir():
106
+ raise ValueError("无法创建 Harness 工作区")
107
+ return target
108
+
109
+
110
+ def reset_session_subdirs(session_root: Path) -> None:
111
+ """
112
+ 清空会话内 Harness 与 memory 子目录并重建。
113
+
114
+ Args:
115
+ session_root: 会话根目录
116
+ """
117
+ root = session_root.resolve()
118
+ for sub_name in (SESSION_HARNESS_SUBDIR, SESSION_MEMORY_SUBDIR):
119
+ sub = _validate_session_subdir_name(sub_name, label="子目录")
120
+ target = (root / sub).resolve()
121
+ try:
122
+ target.relative_to(root)
123
+ except ValueError as exc:
124
+ raise ValueError(f"子目录越出会话工作区: {sub_name}") from exc
125
+ if target.is_dir():
126
+ shutil.rmtree(target)
127
+ target.mkdir(parents=True, exist_ok=True)
ai_agent/app/app.py ADDED
@@ -0,0 +1,321 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Mapping, Sequence
4
+ from pathlib import Path
5
+
6
+ from ai_agent.app._workspace import (
7
+ SESSION_MEMORY_SUBDIR,
8
+ reset_session_subdirs,
9
+ resolve_app_sandbox_root,
10
+ session_harness_workspace,
11
+ session_workspace,
12
+ validate_session_id,
13
+ )
14
+ from ai_agent.app.harness_io import (
15
+ format_input_files_context,
16
+ resolve_output_files,
17
+ stage_input_files,
18
+ )
19
+ from ai_agent.app.output_format import parse_structured_run_output
20
+ from ai_agent.app.packet import RunInputPacket, RunOutputPacket
21
+ from ai_agent.app.session_store import (
22
+ clear_conversation,
23
+ load_conversation,
24
+ save_conversation,
25
+ )
26
+ from ai_agent.app.session import (
27
+ AgentSession,
28
+ build_session,
29
+ normalize_session_listeners,
30
+ )
31
+ from ai_agent.listener import AgentListener, notify_app_run_end
32
+ from ai_agent.memory import MemoryConfig, MemorySystem
33
+ from ai_agent.tools import Tool
34
+
35
+
36
+ class AgentApp:
37
+ """
38
+ 多会话应用入口:在总沙箱下按会话标识分配子目录,并装配隔离工作区、技能与对话代理。
39
+
40
+ 各会话下固定 harness 子目录供工具读写、memory 子目录供分层记忆;语言模型与记忆压缩
41
+ 模型在 AgentApp 构造时配置一次,会话间工作区与对话历史相互隔离。
42
+
43
+ Args:
44
+ sandbox: 总沙箱根目录
45
+ skill_roots: 只读技能仓库根;键为根键,值为目录路径;未配置则不挂载技能
46
+ rule_paths: 系统规则文本文件路径列表,固定拼入各轮系统提示
47
+ api_key: 主对话语言模型 API 密钥
48
+ model: 主对话模型名
49
+ base_url: 主对话 API 地址
50
+ temperature: 主对话采样温度;未设则由服务端默认
51
+ max_tokens: 主对话单次补全 token 上限;未设则由服务端默认
52
+ thinking_enabled: 主对话是否向兼容接口开启思考模式(``enable_thinking``)
53
+ max_steps: 单轮 ReAct 最大步数
54
+ extra_tools: 各会话共用的额外工具(如 MCP),共享同一列表引用
55
+ listeners: 运行时事件监听;可传单个或序列
56
+ memory_api_key: 记忆压缩用模型 API 密钥;三者缺一则不启用记忆
57
+ memory_model: 记忆压缩用模型名
58
+ memory_base_url: 记忆压缩用 API 地址
59
+ memory_short_term_max_messages: 短期记忆条数上限
60
+ memory_short_term_overflow_batch: 每次从短期弹出的条数
61
+ memory_date_memory_days: 日期记忆保留天数
62
+ memory_date_memory_max_entries_per_day: 单日日期记忆条目上限
63
+ memory_long_term_max_chunks: 长期记忆块数上限
64
+ memory_important_max_entries: 重要记忆条目上限
65
+ harness_enabled: 是否向模型注册 Harness 沙箱工具;默认关闭
66
+ current_time_tool_enabled: 是否注册 ``builtin__current_time``;默认开启
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ sandbox: Path | str,
72
+ *,
73
+ skill_roots: (
74
+ Mapping[str, Path | str]
75
+ | Sequence[Path | str]
76
+ | Path
77
+ | str
78
+ | None
79
+ ) = None,
80
+ rule_paths: Sequence[Path | str] | None = None,
81
+ api_key: str,
82
+ model: str,
83
+ base_url: str,
84
+ temperature: float | None = None,
85
+ max_tokens: int | None = None,
86
+ thinking_enabled: bool = False,
87
+ max_steps: int = 20,
88
+ extra_tools: list[Tool] | None = None,
89
+ listeners: AgentListener | Iterable[AgentListener] | None = None,
90
+ memory_api_key: str | None = None,
91
+ memory_model: str | None = None,
92
+ memory_base_url: str | None = None,
93
+ memory_short_term_max_messages: int = 20,
94
+ memory_short_term_overflow_batch: int = 5,
95
+ memory_date_memory_days: int = 7,
96
+ memory_date_memory_max_entries_per_day: int = 50,
97
+ memory_long_term_max_chunks: int = 30,
98
+ memory_important_max_entries: int = 20,
99
+ harness_enabled: bool = False,
100
+ current_time_tool_enabled: bool = True,
101
+ ) -> None:
102
+ self._sandbox_root = resolve_app_sandbox_root(sandbox)
103
+ self._skill_roots = skill_roots
104
+ self._rule_paths = tuple(rule_paths) if rule_paths else None
105
+ self._api_key = api_key
106
+ self._model = model
107
+ self._base_url = base_url
108
+ self._temperature = temperature
109
+ self._max_tokens = max_tokens
110
+ self._thinking_enabled = thinking_enabled
111
+ self._max_steps = max_steps
112
+ self._shared_extra_tools: list[Tool] = list(extra_tools or [])
113
+ self._listeners = normalize_session_listeners(listeners)
114
+ self._memory_api_key = memory_api_key
115
+ self._memory_model = memory_model
116
+ self._memory_base_url = memory_base_url
117
+ self._memory_config = MemoryConfig(
118
+ short_term_max_messages=memory_short_term_max_messages,
119
+ short_term_overflow_batch=memory_short_term_overflow_batch,
120
+ date_memory_days=memory_date_memory_days,
121
+ date_memory_max_entries_per_day=memory_date_memory_max_entries_per_day,
122
+ long_term_max_chunks=memory_long_term_max_chunks,
123
+ important_max_entries=memory_important_max_entries,
124
+ )
125
+ self._harness_enabled = harness_enabled
126
+ self._current_time_tool_enabled = current_time_tool_enabled
127
+ self._sessions: dict[str, AgentSession] = {}
128
+
129
+ @property
130
+ def shared_extra_tools(self) -> list[Tool]:
131
+ """各会话共用的已加载工具(如 MCP),运行时共享同一列表引用。"""
132
+ return self._shared_extra_tools
133
+
134
+ def set_shared_extra_tools(self, tools: list[Tool]) -> None:
135
+ """
136
+ 替换各会话共用的工具表(须在 ``run`` 之前完成,例如 MCP ``load`` 之后)。
137
+
138
+ Args:
139
+ tools: 新工具列表;会替换内部列表内容
140
+ """
141
+ self._shared_extra_tools.clear()
142
+ self._shared_extra_tools.extend(tools)
143
+
144
+ @property
145
+ def sandbox_root(self) -> Path:
146
+ """总沙箱根目录。"""
147
+ return self._sandbox_root
148
+
149
+ @property
150
+ def harness_enabled(self) -> bool:
151
+ """各会话是否向模型注册 Harness 沙箱工具。"""
152
+ return self._harness_enabled
153
+
154
+ @property
155
+ def current_time_tool_enabled(self) -> bool:
156
+ """各会话是否向模型注册 ``builtin__current_time``。"""
157
+ return self._current_time_tool_enabled
158
+
159
+ @property
160
+ def thinking_enabled(self) -> bool:
161
+ """主对话是否在请求中开启思考模式。"""
162
+ return self._thinking_enabled
163
+
164
+ def list_session_ids(self) -> tuple[str, ...]:
165
+ """当前已打开(在内存中)的会话 id。"""
166
+ return tuple(sorted(self._sessions))
167
+
168
+ def has_session(self, session_id: str) -> bool:
169
+ """是否已有该会话实例。"""
170
+ label = validate_session_id(session_id)
171
+ return label in self._sessions
172
+
173
+ def get_session(self, session_id: str) -> AgentSession | None:
174
+ """
175
+ 获取已打开的会话。
176
+
177
+ Args:
178
+ session_id: 会话 id
179
+
180
+ Returns:
181
+ 已存在则返回会话,否则 ``None``
182
+ """
183
+ label = validate_session_id(session_id)
184
+ return self._sessions.get(label)
185
+
186
+ def open_session(
187
+ self,
188
+ session_id: str,
189
+ *,
190
+ memory: MemorySystem | None = None,
191
+ ) -> AgentSession:
192
+ """
193
+ 打开或复用一会话:分配子沙箱并安装 Harness 与 Agent。
194
+
195
+ Args:
196
+ session_id: 会话 id;重复打开返回同一 ``AgentSession`` 实例
197
+ memory: 外部已构造的记忆系统;未传且 AgentApp 已配置记忆模型时自动创建
198
+
199
+ Returns:
200
+ 该会话实例
201
+ """
202
+ label = validate_session_id(session_id)
203
+ existing = self._sessions.get(label)
204
+ if existing is not None:
205
+ return existing
206
+ session_root = session_workspace(self._sandbox_root, label)
207
+ harness_workspace = session_harness_workspace(session_root)
208
+ session_memory = memory
209
+ if session_memory is None:
210
+ session_memory = self._build_default_memory(session_root)
211
+ session = build_session(
212
+ session_id=label,
213
+ workspace=session_root,
214
+ harness_workspace=harness_workspace,
215
+ skill_roots=self._skill_roots,
216
+ rule_paths=self._rule_paths,
217
+ api_key=self._api_key,
218
+ model=self._model,
219
+ base_url=self._base_url,
220
+ temperature=self._temperature,
221
+ max_tokens=self._max_tokens,
222
+ thinking_enabled=self._thinking_enabled,
223
+ max_steps=self._max_steps,
224
+ extra_tools=self._shared_extra_tools,
225
+ listeners=self._listeners,
226
+ memory=session_memory,
227
+ harness_enabled=self._harness_enabled,
228
+ current_time_tool_enabled=self._current_time_tool_enabled,
229
+ )
230
+ self._sessions[label] = session
231
+ return session
232
+
233
+ async def run(self, packet: RunInputPacket) -> RunOutputPacket:
234
+ """
235
+ 运行入口:接收输入数据包,规划并逐步执行后返回输出数据包。
236
+
237
+ 单次调用内打开会话、处理完毕后从内存移除会话实例;Harness 与对话状态落在
238
+ ``sessions/<session_id>/``。运行时事件通过构造时传入的 ``listeners`` 回调。
239
+
240
+ Args:
241
+ packet: 用户名、会话 id、要求、附件路径与是否清空会话状态等
242
+
243
+ Returns:
244
+ 结构化回答与须交还用户的文件路径列表
245
+ """
246
+ label = validate_session_id(packet.session_id)
247
+ user_name = packet.user_name.strip()
248
+ if not user_name:
249
+ raise ValueError("user_name 不能为空")
250
+ session_root = session_workspace(self._sandbox_root, label)
251
+ if packet.clear:
252
+ reset_session_subdirs(session_root)
253
+ clear_conversation(session_root)
254
+ self.close_session(label)
255
+ session = self.open_session(label)
256
+ try:
257
+ if not packet.clear and session.memory is None:
258
+ session.replace_messages(load_conversation(session_root))
259
+ if packet.input_files and not self._harness_enabled:
260
+ raise ValueError(
261
+ "已传入 input_files 但 AgentApp.harness_enabled 为 False;"
262
+ "请启用 Harness 或勿附带附件",
263
+ )
264
+ staged = stage_input_files(packet.input_files, session.harness.workspace)
265
+ file_context = format_input_files_context(staged)
266
+ user_message = _compose_plan_user_message(packet.request, file_context)
267
+ plan_result = await session.run_with_plan(
268
+ user_message=user_message,
269
+ speaker=user_name,
270
+ extra_planning_context=file_context,
271
+ )
272
+ answer, rel_files = parse_structured_run_output(plan_result.final_output)
273
+ output_files = resolve_output_files(rel_files, session.harness.workspace)
274
+ if session.memory is None:
275
+ save_conversation(session_root, list(session.messages))
276
+ packet = RunOutputPacket(
277
+ user_name=user_name,
278
+ session_id=label,
279
+ answer=answer,
280
+ output_files=output_files,
281
+ )
282
+ await notify_app_run_end(self._listeners, packet)
283
+ return packet
284
+ finally:
285
+ self.close_session(label)
286
+
287
+ def _build_default_memory(self, workspace: Path) -> MemorySystem | None:
288
+ key = self._memory_api_key
289
+ model = self._memory_model
290
+ base = self._memory_base_url
291
+ if not key or not model or not base:
292
+ return None
293
+ storage = workspace / SESSION_MEMORY_SUBDIR
294
+ return MemorySystem(
295
+ storage,
296
+ api_key=key,
297
+ model=model,
298
+ base_url=base,
299
+ config=self._memory_config,
300
+ )
301
+
302
+ def close_session(self, session_id: str) -> bool:
303
+ """
304
+ 从应用中移除会话实例(不删除磁盘上的子沙箱内容)。
305
+
306
+ Args:
307
+ session_id: 会话 id
308
+
309
+ Returns:
310
+ 若原先存在并已移除则为 True
311
+ """
312
+ label = validate_session_id(session_id)
313
+ return self._sessions.pop(label, None) is not None
314
+
315
+
316
+ def _compose_plan_user_message(request: str, file_context: str) -> str:
317
+ """拼规划与记忆用的用户原文(不含最终交付 JSON 说明)。"""
318
+ parts: list[str] = [request.strip()]
319
+ if file_context.strip():
320
+ parts.append(file_context.strip())
321
+ return "\n\n".join(parts)