my-pi-agent 0.1.0
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.
- package/README.md +318 -0
- package/package.json +45 -0
- package/pyproject.toml +50 -0
- package/src/my_agent_core/__init__.py +123 -0
- package/src/my_agent_core/agent.py +441 -0
- package/src/my_agent_core/background.py +121 -0
- package/src/my_agent_core/context.py +505 -0
- package/src/my_agent_core/events.py +153 -0
- package/src/my_agent_core/extensions/__init__.py +9 -0
- package/src/my_agent_core/extensions/core.py +197 -0
- package/src/my_agent_core/hooks.py +130 -0
- package/src/my_agent_core/loop.py +709 -0
- package/src/my_agent_core/main.py +134 -0
- package/src/my_agent_core/memory.py +241 -0
- package/src/my_agent_core/message_queue.py +110 -0
- package/src/my_agent_core/plugins.py +212 -0
- package/src/my_agent_core/registry.py +186 -0
- package/src/my_agent_core/session/__init__.py +79 -0
- package/src/my_agent_core/session/entries.py +197 -0
- package/src/my_agent_core/session/jsonl.py +60 -0
- package/src/my_agent_core/session/memory.py +137 -0
- package/src/my_agent_core/session/session.py +400 -0
- package/src/my_agent_core/session/storage.py +245 -0
- package/src/my_agent_core/session/store.py +131 -0
- package/src/my_agent_core/session/tree.py +86 -0
- package/src/my_agent_core/skills.py +149 -0
- package/src/my_agent_core/subagent_tasks.py +170 -0
- package/src/my_agent_core/subagents.py +148 -0
- package/src/my_agent_core/task_store.py +248 -0
- package/src/my_agent_core/tool_history.py +189 -0
- package/src/my_agent_core/tools/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/task.py +30 -0
- package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
- package/src/my_agent_core/tools/core.py +239 -0
- package/src/my_agent_llm/__init__.py +45 -0
- package/src/my_agent_llm/auth/__init__.py +46 -0
- package/src/my_agent_llm/auth/antigravity.py +209 -0
- package/src/my_agent_llm/auth/manager.py +259 -0
- package/src/my_agent_llm/auth/quota.py +56 -0
- package/src/my_agent_llm/auth/schema.py +94 -0
- package/src/my_agent_llm/client.py +116 -0
- package/src/my_agent_llm/config.py +17 -0
- package/src/my_agent_llm/events.py +84 -0
- package/src/my_agent_llm/models.py +195 -0
- package/src/my_agent_llm/providers/__init__.py +4 -0
- package/src/my_agent_llm/providers/_base.py +94 -0
- package/src/my_agent_llm/providers/anthropic.py +298 -0
- package/src/my_agent_llm/providers/antigravity.py +480 -0
- package/src/my_agent_llm/providers/deepseek.py +196 -0
- package/src/my_agent_llm/providers/openai.py +364 -0
- package/src/my_agent_llm/providers/registry.py +16 -0
- package/src/my_agent_llm/stream.py +218 -0
- package/src/my_coding_agent/__init__.py +66 -0
- package/src/my_coding_agent/agent.py +208 -0
- package/src/my_coding_agent/cli.py +78 -0
- package/src/my_coding_agent/file_reference.py +80 -0
- package/src/my_coding_agent/macro.py +408 -0
- package/src/my_coding_agent/mcp.py +243 -0
- package/src/my_coding_agent/mutation_queue.py +37 -0
- package/src/my_coding_agent/paths.py +119 -0
- package/src/my_coding_agent/permissions.py +84 -0
- package/src/my_coding_agent/prompt.py +54 -0
- package/src/my_coding_agent/rpc_server.py +2817 -0
- package/src/my_coding_agent/settings.py +126 -0
- package/src/my_coding_agent/tools/__init__.py +55 -0
- package/src/my_coding_agent/tools/base.py +58 -0
- package/src/my_coding_agent/tools/bash.py +206 -0
- package/src/my_coding_agent/tools/edit.py +226 -0
- package/src/my_coding_agent/tools/find.py +118 -0
- package/src/my_coding_agent/tools/grep.py +177 -0
- package/src/my_coding_agent/tools/ls.py +112 -0
- package/src/my_coding_agent/tools/read.py +113 -0
- package/src/my_coding_agent/tools/write.py +72 -0
- package/tui/README.md +27 -0
- package/tui/bin/my-agent.js +98 -0
- package/tui/dist/app.d.ts +41 -0
- package/tui/dist/app.js +110 -0
- package/tui/dist/bridge/event-translator.d.ts +92 -0
- package/tui/dist/bridge/event-translator.js +216 -0
- package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
- package/tui/dist/bridge/kernel-bridge.js +132 -0
- package/tui/dist/client.d.ts +63 -0
- package/tui/dist/client.js +239 -0
- package/tui/dist/components/assistant-message.d.ts +19 -0
- package/tui/dist/components/assistant-message.js +90 -0
- package/tui/dist/components/compaction-summary-message.d.ts +19 -0
- package/tui/dist/components/compaction-summary-message.js +46 -0
- package/tui/dist/components/custom-editor.d.ts +18 -0
- package/tui/dist/components/custom-editor.js +56 -0
- package/tui/dist/components/dynamic-border.d.ts +9 -0
- package/tui/dist/components/dynamic-border.js +14 -0
- package/tui/dist/components/footer.d.ts +39 -0
- package/tui/dist/components/footer.js +199 -0
- package/tui/dist/components/header.d.ts +4 -0
- package/tui/dist/components/header.js +21 -0
- package/tui/dist/components/keys.d.ts +5 -0
- package/tui/dist/components/keys.js +12 -0
- package/tui/dist/components/login-selector.d.ts +26 -0
- package/tui/dist/components/login-selector.js +181 -0
- package/tui/dist/components/logout-selector.d.ts +19 -0
- package/tui/dist/components/logout-selector.js +88 -0
- package/tui/dist/components/model-selector.d.ts +40 -0
- package/tui/dist/components/model-selector.js +268 -0
- package/tui/dist/components/session-selector.d.ts +54 -0
- package/tui/dist/components/session-selector.js +393 -0
- package/tui/dist/components/settings-selector.d.ts +24 -0
- package/tui/dist/components/settings-selector.js +146 -0
- package/tui/dist/components/status-indicator.d.ts +25 -0
- package/tui/dist/components/status-indicator.js +60 -0
- package/tui/dist/components/theme-selector.d.ts +14 -0
- package/tui/dist/components/theme-selector.js +77 -0
- package/tui/dist/components/thinking-selector.d.ts +21 -0
- package/tui/dist/components/thinking-selector.js +128 -0
- package/tui/dist/components/tool-execution.d.ts +31 -0
- package/tui/dist/components/tool-execution.js +206 -0
- package/tui/dist/components/tree-selector.d.ts +40 -0
- package/tui/dist/components/tree-selector.js +173 -0
- package/tui/dist/components/user-message-selector.d.ts +21 -0
- package/tui/dist/components/user-message-selector.js +103 -0
- package/tui/dist/components/user-message.d.ts +5 -0
- package/tui/dist/components/user-message.js +15 -0
- package/tui/dist/index.d.ts +11 -0
- package/tui/dist/index.js +11 -0
- package/tui/dist/interactive/chat-viewport.d.ts +19 -0
- package/tui/dist/interactive/chat-viewport.js +41 -0
- package/tui/dist/interactive/components.d.ts +1 -0
- package/tui/dist/interactive/components.js +1 -0
- package/tui/dist/interactive/interactive-mode.d.ts +89 -0
- package/tui/dist/interactive/interactive-mode.js +1625 -0
- package/tui/dist/interactive/theme.d.ts +1 -0
- package/tui/dist/interactive/theme.js +1 -0
- package/tui/dist/interactive/tui-renderer.d.ts +8 -0
- package/tui/dist/interactive/tui-renderer.js +10 -0
- package/tui/dist/protocol.d.ts +78 -0
- package/tui/dist/protocol.js +1 -0
- package/tui/dist/theme/dark.json +54 -0
- package/tui/dist/theme/light.json +71 -0
- package/tui/dist/theme/theme.d.ts +20 -0
- package/tui/dist/theme/theme.js +86 -0
- package/tui/package.json +25 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# pyright: reportImportCycles=false
|
|
2
|
+
"""单层 Agent —— 状态 + 循环 + 工具执行全在一个类(原生异步驱动)。
|
|
3
|
+
|
|
4
|
+
模型调用 → 检查 tool_calls → 执行工具 → 观察结果写回消息 → 循环,
|
|
5
|
+
直到模型不再发起工具调用(经典退出条件:tool_calls 为空 → 结束)。
|
|
6
|
+
|
|
7
|
+
模型边界交给 my-agent-llm 的 LLM 门面;消息状态是 Message 对象列表。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import contextlib
|
|
14
|
+
import inspect
|
|
15
|
+
from collections.abc import AsyncIterator, Callable, Sequence
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Literal
|
|
18
|
+
|
|
19
|
+
from my_agent_core.background import ( # pyright: ignore[reportMissingImports]
|
|
20
|
+
BackgroundRunner,
|
|
21
|
+
)
|
|
22
|
+
from my_agent_core.context import ContextManager, ContextSessionBridge
|
|
23
|
+
from my_agent_core.events import (
|
|
24
|
+
AgentEnd,
|
|
25
|
+
AgentStart,
|
|
26
|
+
ContextCompacted,
|
|
27
|
+
Event,
|
|
28
|
+
MessageEnd,
|
|
29
|
+
TurnEnd,
|
|
30
|
+
)
|
|
31
|
+
from my_agent_core.extensions import ExtensionManager
|
|
32
|
+
from my_agent_core.hooks import ( # pyright: ignore[reportMissingImports]
|
|
33
|
+
AgentStartHook,
|
|
34
|
+
HookRegistry,
|
|
35
|
+
HookResult,
|
|
36
|
+
UserInputHook,
|
|
37
|
+
)
|
|
38
|
+
from my_agent_core.loop import CancellationToken, run_agent_loop
|
|
39
|
+
from my_agent_core.memory import MemoryStore, make_memory_tool
|
|
40
|
+
from my_agent_core.message_queue import MessageQueue
|
|
41
|
+
from my_agent_core.plugins import PluginManager
|
|
42
|
+
from my_agent_core.registry import ToolRegistry
|
|
43
|
+
from my_agent_core.session import Session
|
|
44
|
+
from my_agent_core.skills import Skill, SkillManager
|
|
45
|
+
from my_agent_core.subagents import SubagentManager
|
|
46
|
+
from my_agent_core.task_store import TaskStore # pyright: ignore[reportMissingImports]
|
|
47
|
+
from my_agent_core.tool_history import repair_tool_history
|
|
48
|
+
from my_agent_core.tools import Tool
|
|
49
|
+
from my_agent_core.tools.builtin.task import (
|
|
50
|
+
make_task_tool, # pyright: ignore[reportMissingImports]
|
|
51
|
+
)
|
|
52
|
+
from my_agent_core.tools.builtin.task_tools import ( # pyright: ignore[reportMissingImports]
|
|
53
|
+
TaskGuardHook,
|
|
54
|
+
make_task_tools,
|
|
55
|
+
)
|
|
56
|
+
from my_agent_llm import LLM, Message # pyright: ignore[reportMissingImports]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Agent:
|
|
60
|
+
"""单层 Agent:持有 llm / 工具注册表 / 消息,内联 ReAct 异步循环。"""
|
|
61
|
+
|
|
62
|
+
# ── 构造与装配 ──────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
*,
|
|
67
|
+
llm: LLM,
|
|
68
|
+
tools: list[Tool],
|
|
69
|
+
session: Session,
|
|
70
|
+
system_prompt: str | None = None,
|
|
71
|
+
max_iterations: int | None = None,
|
|
72
|
+
context_budget: int | None = None,
|
|
73
|
+
keep_recent_tokens: int | None = None,
|
|
74
|
+
skill_dirs: Sequence[str | Path] | None = None,
|
|
75
|
+
model: str | None = None,
|
|
76
|
+
subagent_dirs: Sequence[str | Path] | None = None,
|
|
77
|
+
extension_dirs: Sequence[str | Path] | None = None,
|
|
78
|
+
plugin_dirs: Sequence[str | Path] | None = None,
|
|
79
|
+
memory_dir: str | Path | None | Literal[False] = None,
|
|
80
|
+
task_store: TaskStore | Path | str | None | Literal[False] = None,
|
|
81
|
+
steering_mode: Literal["one-at-a-time", "all"] = "one-at-a-time",
|
|
82
|
+
followup_mode: Literal["one-at-a-time", "all"] = "one-at-a-time",
|
|
83
|
+
hooks: list[tuple[type, Callable[..., Any]]] | None = None,
|
|
84
|
+
):
|
|
85
|
+
"""各参数语义见框架设计文档 §4.3(hook 通过 register_hook 挂载)。
|
|
86
|
+
|
|
87
|
+
session 必填:run() 内每条消息落盘;构造时从 session 当前路径恢复纯对话,
|
|
88
|
+
并用 system_prompt + skill 清单 + subagent 清单拼 system(消息首条)。
|
|
89
|
+
context_budget 为 context 预算:None → 用 ContextManager 默认(100k);显式传 → 覆盖。
|
|
90
|
+
context 默认启用(每次 llm.chat 前 prepare 压缩视图)。
|
|
91
|
+
skill_dirs 为 skill 机制来源:None → 探测 <cwd>/.agents/skills(不存在则空);
|
|
92
|
+
[] → 显式禁用;非空 list → 只扫这些目录。构造 skill_manager 追加清单块进
|
|
93
|
+
system;self.skill_manager 公开可读,self.skills 为动态计算属性(@property 代理 manager.list())。
|
|
94
|
+
正文由宿主 invoke_skill 显式注入(模型侧无 read 工具)。
|
|
95
|
+
subagent_dirs 三态同 skill_dirs:None → 探测 <cwd>/.agents/agents;[] → 禁用;
|
|
96
|
+
非空 → 只扫这些目录。有 agent 时清单追加进 system,且自动装配 task 工具。
|
|
97
|
+
extension_dirs 三态同 skill_dirs/subagent_dirs:None → 探测 <cwd>/.agents/extensions;
|
|
98
|
+
[] → 禁用;非空 → 只扫这些目录。extension 在 _register_tools 之后加载,注册的工具可覆盖
|
|
99
|
+
内置工具(对齐 pi);hook 注册进 hooks(先于构造参数 hooks 触发);命令存 extension_manager,
|
|
100
|
+
上层 CLI 调 handle_command 派发。
|
|
101
|
+
plugin_dirs 为 Claude Code 格式插件目录:None → 探测 <cwd>/.agents/plugins;
|
|
102
|
+
[] → 显式禁用;非空 list → 扫各插件目录并自动解构其 skills/、agents/ 注入对应管理器。
|
|
103
|
+
memory_dir 为持久化记忆存储目录:None → 探测 <cwd>/.my_agent_core/memory(存在才启用);
|
|
104
|
+
False → 显式禁用;str | Path → 显式指定目录。启用时构造 MemoryStore 并冻结快照,
|
|
105
|
+
自动注入 <MEMORY_CONTEXT> 块进 system,并自动注册 memory 工具(add/replace/remove)。
|
|
106
|
+
"""
|
|
107
|
+
self.llm = llm
|
|
108
|
+
self.model = model # 缺省 inherit:None 时 llm.chat 用 LLM 自身配置
|
|
109
|
+
self.max_iterations = max_iterations
|
|
110
|
+
self.session = session
|
|
111
|
+
self._system_prompt = system_prompt # 保存,reset 重拼用
|
|
112
|
+
self._aborted = False # 中止状态标记
|
|
113
|
+
self._current_signal: CancellationToken | None = None
|
|
114
|
+
self._subscribers: list[Callable[[Event], Any]] = []
|
|
115
|
+
self.hooks = HookRegistry()
|
|
116
|
+
self.registry = ToolRegistry()
|
|
117
|
+
self.plugin_manager = PluginManager(plugin_dirs)
|
|
118
|
+
self.skill_manager = SkillManager(
|
|
119
|
+
skill_dirs, extra_dirs=self.plugin_manager.get_skill_dirs()
|
|
120
|
+
) # None→探测默认 / []→禁用 / 显式→目录
|
|
121
|
+
self.subagent_manager = SubagentManager(
|
|
122
|
+
subagent_dirs, extra_dirs=self.plugin_manager.get_subagent_dirs()
|
|
123
|
+
) # 三态同 skill_dirs
|
|
124
|
+
|
|
125
|
+
self.memory_store = self._init_memory_store(memory_dir) # memory 装配与快照冻结
|
|
126
|
+
self.task_store = self._init_task_store(task_store) # 任务看板仓库装配
|
|
127
|
+
self.message_queue = MessageQueue(
|
|
128
|
+
steering_mode=steering_mode, followup_mode=followup_mode
|
|
129
|
+
) # 动态干预消息队列 (Pi-style steer & followup)
|
|
130
|
+
self.background_runner = BackgroundRunner(self.message_queue) # 后台异步执行器与孤儿进程防御调度引擎
|
|
131
|
+
|
|
132
|
+
self._register_tools(tools) # ① 工具注册统一(用户 + 内置 task + 内置 memory)
|
|
133
|
+
self.extension_manager = ExtensionManager(self, extension_dirs) # extension 装配
|
|
134
|
+
self._extensions_loaded = False
|
|
135
|
+
self.messages = self._init_messages(session, system_prompt) # ② 拼 system + 恢复
|
|
136
|
+
self._init_context(session, context_budget, keep_recent_tokens) # ③ context 装配
|
|
137
|
+
self._register_hooks(hooks) # ④ hooks 批量注册
|
|
138
|
+
|
|
139
|
+
def _init_task_store(self, task_store: TaskStore | Path | str | None | Literal[False]) -> TaskStore | None:
|
|
140
|
+
"""解析 task_store 三态并初始化 TaskStore:
|
|
141
|
+
False → 显式禁用;
|
|
142
|
+
TaskStore 实例 → 直接复用;
|
|
143
|
+
str | Path → 指定工作区初始化;
|
|
144
|
+
None → 探测 <cwd>/.my_agent_core/tasks.json(存在才启用)。
|
|
145
|
+
"""
|
|
146
|
+
if isinstance(task_store, bool) and not task_store:
|
|
147
|
+
return None
|
|
148
|
+
if isinstance(task_store, TaskStore):
|
|
149
|
+
return task_store
|
|
150
|
+
if task_store is not None:
|
|
151
|
+
return TaskStore(task_store)
|
|
152
|
+
default_file = Path.cwd() / ".my_agent_core" / "tasks.json"
|
|
153
|
+
if default_file.exists() and default_file.is_file():
|
|
154
|
+
return TaskStore(Path.cwd())
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def _init_memory_store(self, memory_dir: str | Path | None | Literal[False]) -> MemoryStore | None:
|
|
158
|
+
"""解析 memory_dir 三态并初始化 MemoryStore:
|
|
159
|
+
False → 显式禁用;
|
|
160
|
+
None → 探测 <cwd>/.my_agent_core/memory(存在才启用);
|
|
161
|
+
str | Path → 显式指定目录。
|
|
162
|
+
"""
|
|
163
|
+
if isinstance(memory_dir, bool) and not memory_dir:
|
|
164
|
+
return None
|
|
165
|
+
if memory_dir is not None:
|
|
166
|
+
store = MemoryStore(memory_dir)
|
|
167
|
+
store.load_from_disk()
|
|
168
|
+
return store
|
|
169
|
+
default_dir = Path.cwd() / ".my_agent_core" / "memory"
|
|
170
|
+
if default_dir.exists() and default_dir.is_dir():
|
|
171
|
+
store = MemoryStore(default_dir)
|
|
172
|
+
store.load_from_disk()
|
|
173
|
+
return store
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
def _register_tools(self, tools: list[Tool]) -> None:
|
|
177
|
+
"""注册用户工具 + 内置 task 工具 + 内置 memory 工具(撞名 ValueError)。"""
|
|
178
|
+
for t in tools:
|
|
179
|
+
self.registry.register(t)
|
|
180
|
+
if self.subagent_manager:
|
|
181
|
+
if self.registry.get("task") is not None:
|
|
182
|
+
raise ValueError("Tool name 'task' conflicts with the built-in subagent delegation tool")
|
|
183
|
+
self.registry.register(make_task_tool(self.subagent_manager, self))
|
|
184
|
+
if self.memory_store:
|
|
185
|
+
if self.registry.get("memory") is not None:
|
|
186
|
+
raise ValueError("Tool name 'memory' conflicts with the built-in memory tool")
|
|
187
|
+
self.registry.register(make_memory_tool(self.memory_store))
|
|
188
|
+
if self.task_store:
|
|
189
|
+
for task_tool in make_task_tools(self.task_store):
|
|
190
|
+
if self.registry.get(task_tool.name) is not None:
|
|
191
|
+
raise ValueError(f"Tool name '{task_tool.name}' conflicts with built-in task tool")
|
|
192
|
+
self.registry.register(task_tool)
|
|
193
|
+
|
|
194
|
+
def _init_messages(self, session: Session, system_prompt: str | None) -> list[Message]:
|
|
195
|
+
"""拼 system(Agent 配置)+ 恢复 session 纯对话,合成初始 messages。"""
|
|
196
|
+
mem_prompt = self.memory_store.format_all_for_system_prompt() if self.memory_store else None
|
|
197
|
+
parts = [
|
|
198
|
+
p
|
|
199
|
+
for p in (
|
|
200
|
+
system_prompt or "",
|
|
201
|
+
self.skill_manager.format_prompt(),
|
|
202
|
+
self.subagent_manager.format_prompt(),
|
|
203
|
+
mem_prompt,
|
|
204
|
+
)
|
|
205
|
+
if p
|
|
206
|
+
]
|
|
207
|
+
messages = session.get_full_history_messages() # 纯对话(不含 system)
|
|
208
|
+
if parts:
|
|
209
|
+
messages.insert(0, Message(role="system", content="\n\n".join(parts)))
|
|
210
|
+
return messages
|
|
211
|
+
|
|
212
|
+
def _init_context(
|
|
213
|
+
self,
|
|
214
|
+
session: Session,
|
|
215
|
+
context_budget: int | None,
|
|
216
|
+
keep_recent_tokens: int | None,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""装配 context 管理(默认启用):context_budget None → 用 ContextManager 默认 budget。"""
|
|
219
|
+
self._ctx_bridge = ContextSessionBridge(session)
|
|
220
|
+
self._ctx = ContextManager(
|
|
221
|
+
llm=self.llm,
|
|
222
|
+
keep_recent_tokens=keep_recent_tokens,
|
|
223
|
+
results_dir=self._ctx_bridge.results_dir(),
|
|
224
|
+
**({} if context_budget is None else {"budget": context_budget}),
|
|
225
|
+
)
|
|
226
|
+
self._ctx_bridge.restore_cache(self._ctx)
|
|
227
|
+
|
|
228
|
+
def _register_hooks(self, hooks: list[tuple[type, Callable[..., Any]]] | None) -> None:
|
|
229
|
+
"""构造时批量注册 hooks / 决策回调(对称 _register_tools)。"""
|
|
230
|
+
if self.task_store:
|
|
231
|
+
guard = TaskGuardHook(self.task_store, self.steer)
|
|
232
|
+
self.subscribe(lambda ev: guard.on_agent_start(ev) if isinstance(ev, AgentStart) else None)
|
|
233
|
+
self.subscribe(lambda ev: guard.on_turn_end(ev) if isinstance(ev, TurnEnd) else None)
|
|
234
|
+
for target, callback in hooks or []:
|
|
235
|
+
self.hooks.register(target, callback)
|
|
236
|
+
|
|
237
|
+
# ── 公共 API ────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def context_manager(self) -> ContextManager:
|
|
241
|
+
"""底层上下文管理器。"""
|
|
242
|
+
return self._ctx
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def skills(self) -> list[Skill]:
|
|
246
|
+
"""动态获取当前注册的全部技能列表。"""
|
|
247
|
+
return self.skill_manager.list()
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def system_prompt(self) -> str | None:
|
|
251
|
+
"""Agent 配置的初始系统提示词。"""
|
|
252
|
+
return self._system_prompt
|
|
253
|
+
|
|
254
|
+
def abort(self) -> None:
|
|
255
|
+
"""中止当前运行中的任务(取消流式输出,丢弃未完成半截文本并清空干预队列)。"""
|
|
256
|
+
self._aborted = True
|
|
257
|
+
if self._current_signal is not None:
|
|
258
|
+
self._current_signal.cancel()
|
|
259
|
+
self.message_queue.clear()
|
|
260
|
+
asyncio.create_task(self.background_runner.cancel_all())
|
|
261
|
+
|
|
262
|
+
def subscribe(self, listener: Callable[[Event], Any]) -> Callable[[], None]:
|
|
263
|
+
"""订阅所有生命周期事件通知,返回取消订阅的回调函数。"""
|
|
264
|
+
self._subscribers.append(listener)
|
|
265
|
+
|
|
266
|
+
def unsubscribe() -> None:
|
|
267
|
+
if listener in self._subscribers:
|
|
268
|
+
self._subscribers.remove(listener)
|
|
269
|
+
|
|
270
|
+
return unsubscribe
|
|
271
|
+
|
|
272
|
+
def steer(self, message: str) -> None:
|
|
273
|
+
"""注入即时转向指令(在下一个安全点打断/干预模型执行路线)。"""
|
|
274
|
+
self.message_queue.add_steering(message)
|
|
275
|
+
|
|
276
|
+
def follow_up(self, message: str) -> None:
|
|
277
|
+
"""追加排队追问指令(在当前任务彻底完成后自动开启下一段任务)。"""
|
|
278
|
+
self.message_queue.add_followup(message)
|
|
279
|
+
|
|
280
|
+
def _get_steering_messages(self) -> Sequence[str]:
|
|
281
|
+
"""为底层循环提取当前排队的 steer 消息。"""
|
|
282
|
+
if self.message_queue.has_steering():
|
|
283
|
+
return [m.content for m in self.message_queue.get_steering_messages()]
|
|
284
|
+
return []
|
|
285
|
+
|
|
286
|
+
def _get_follow_up_messages(self) -> Sequence[str]:
|
|
287
|
+
"""为底层循环提取当前排队的 follow-up 消息。"""
|
|
288
|
+
if self.message_queue.has_followup():
|
|
289
|
+
return [m.content for m in self.message_queue.get_followup_messages()]
|
|
290
|
+
return []
|
|
291
|
+
|
|
292
|
+
async def prompt_stream(self, user_input: str) -> AsyncIterator[Event]:
|
|
293
|
+
"""原生事件流一等公民接口:调用 run_agent_loop 执行 ReAct 循环,逐一产生生命周期事件,并更新 session 与 messages。"""
|
|
294
|
+
self._aborted = False
|
|
295
|
+
self._current_signal = CancellationToken()
|
|
296
|
+
|
|
297
|
+
if not self._extensions_loaded:
|
|
298
|
+
await self.extension_manager.load()
|
|
299
|
+
self._extensions_loaded = True
|
|
300
|
+
|
|
301
|
+
# ── Hook 1: UserInputHook 拦截与改写(在进入 Session 和消息历史之前触发)
|
|
302
|
+
user_input_decision = await self.hooks.emit(UserInputHook(input_text=user_input))
|
|
303
|
+
if isinstance(user_input_decision, HookResult):
|
|
304
|
+
if user_input_decision.block:
|
|
305
|
+
reason = f": {user_input_decision.reason}" if user_input_decision.reason else ""
|
|
306
|
+
end_ev = AgentEnd(
|
|
307
|
+
messages=list(self.messages),
|
|
308
|
+
final_text=f"(blocked{reason})",
|
|
309
|
+
iterations=0,
|
|
310
|
+
stop_reason="blocked",
|
|
311
|
+
)
|
|
312
|
+
await self._notify(end_ev)
|
|
313
|
+
yield end_ev
|
|
314
|
+
return
|
|
315
|
+
if user_input_decision.updated_input is not None:
|
|
316
|
+
user_input = user_input_decision.updated_input
|
|
317
|
+
|
|
318
|
+
# 同步到 session 当前指针:rewind 后同 Agent 续跑时,内存 transcript 以文件为准。
|
|
319
|
+
system = [m for m in self.messages if m.role == "system"]
|
|
320
|
+
restored = system + self.session.get_full_history_messages()
|
|
321
|
+
# 对齐 Tau: 执行对话历史自愈,保证送入模型的会话转录本没有悬空断头 ToolCall
|
|
322
|
+
self.messages = list(repair_tool_history(restored).messages)
|
|
323
|
+
|
|
324
|
+
# 准备 system_prompt
|
|
325
|
+
system_prompt = self.system_prompt or ""
|
|
326
|
+
system_msgs = [m for m in self.messages if m.role == "system"]
|
|
327
|
+
if system_msgs:
|
|
328
|
+
system_prompt = system_msgs[0].content
|
|
329
|
+
|
|
330
|
+
# ── Hook 2: AgentStartHook 拦截启动或动态重写 system_prompt
|
|
331
|
+
start_decision = await self.hooks.emit(AgentStartHook(system_prompt=system_prompt))
|
|
332
|
+
if isinstance(start_decision, HookResult):
|
|
333
|
+
if start_decision.block:
|
|
334
|
+
reason = f": {start_decision.reason}" if start_decision.reason else ""
|
|
335
|
+
end_ev = AgentEnd(
|
|
336
|
+
messages=list(self.messages),
|
|
337
|
+
final_text=f"(blocked{reason})",
|
|
338
|
+
iterations=0,
|
|
339
|
+
stop_reason="blocked",
|
|
340
|
+
)
|
|
341
|
+
await self._notify(end_ev)
|
|
342
|
+
yield end_ev
|
|
343
|
+
return
|
|
344
|
+
if start_decision.updated_system_prompt is not None:
|
|
345
|
+
system_prompt = start_decision.updated_system_prompt
|
|
346
|
+
if self.messages and self.messages[0].role == "system":
|
|
347
|
+
self.messages[0] = Message(role="system", content=system_prompt)
|
|
348
|
+
elif system_prompt:
|
|
349
|
+
self.messages.insert(0, Message(role="system", content=system_prompt))
|
|
350
|
+
|
|
351
|
+
# 委托核心 ReAct 纯函数微内核驱动事件流
|
|
352
|
+
loop_gen = run_agent_loop(
|
|
353
|
+
llm=self.llm,
|
|
354
|
+
messages=self.messages,
|
|
355
|
+
tools=self.registry,
|
|
356
|
+
context_manager=self._ctx,
|
|
357
|
+
model=self.model,
|
|
358
|
+
system=system_prompt,
|
|
359
|
+
prompts=[Message(role="user", content=user_input)],
|
|
360
|
+
max_iterations=self.max_iterations,
|
|
361
|
+
signal=self._current_signal,
|
|
362
|
+
get_steering_messages=self._get_steering_messages,
|
|
363
|
+
get_follow_up_messages=self._get_follow_up_messages,
|
|
364
|
+
before_model_call=self.hooks.emit,
|
|
365
|
+
before_tool_call=self.hooks.emit,
|
|
366
|
+
after_tool_call=self.hooks.emit,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
async for event in loop_gen:
|
|
370
|
+
# 同步 Session 状态与压缩写回
|
|
371
|
+
if isinstance(event, MessageEnd):
|
|
372
|
+
msg = event.message
|
|
373
|
+
is_cancelled_partial_text = (
|
|
374
|
+
msg.role == "assistant"
|
|
375
|
+
and not (msg.metadata and msg.metadata.get("tool_calls"))
|
|
376
|
+
and (bool(msg.metadata and msg.metadata.get("stop_reason") == "cancelled") or self._aborted)
|
|
377
|
+
)
|
|
378
|
+
if msg.role != "system" and not is_cancelled_partial_text:
|
|
379
|
+
self.session.add_message(msg.role, msg.content, **(msg.metadata or {}))
|
|
380
|
+
elif isinstance(event, ContextCompacted):
|
|
381
|
+
self._ctx_bridge.write_compaction(self._ctx)
|
|
382
|
+
|
|
383
|
+
# 分发到订阅者
|
|
384
|
+
await self._notify(event)
|
|
385
|
+
|
|
386
|
+
yield event
|
|
387
|
+
|
|
388
|
+
async def run(self, user_input: str) -> str | None:
|
|
389
|
+
"""追加 user 消息 → 内部消费 prompt_stream 事件流 → 返回最终文本。"""
|
|
390
|
+
final_text = None
|
|
391
|
+
async for event in self.prompt_stream(user_input):
|
|
392
|
+
if isinstance(event, AgentEnd):
|
|
393
|
+
if event.stop_reason == "cancelled":
|
|
394
|
+
return "(cancelled)"
|
|
395
|
+
if event.stop_reason == "blocked":
|
|
396
|
+
return event.final_text or "(blocked)"
|
|
397
|
+
if event.stop_reason == "error":
|
|
398
|
+
raise RuntimeError(event.final_text or "Error during model stream")
|
|
399
|
+
final_text = event.final_text
|
|
400
|
+
return final_text
|
|
401
|
+
|
|
402
|
+
async def invoke_skill(self, name: str, instructions: str = "") -> str | None:
|
|
403
|
+
"""显式调用:skill_manager.format_invocation 包装(未知名 ValueError)→
|
|
404
|
+
self.run(包装文本) 跑一轮。"""
|
|
405
|
+
return await self.run(self.skill_manager.format_invocation(name, instructions))
|
|
406
|
+
|
|
407
|
+
def reset(self) -> None:
|
|
408
|
+
"""清空对话(保留 system)。session 树清空 + 重载 memory 快照并重拼 system,清空干预队列。"""
|
|
409
|
+
self.message_queue.clear()
|
|
410
|
+
self.session.reset()
|
|
411
|
+
if self.memory_store:
|
|
412
|
+
self.memory_store.load_from_disk()
|
|
413
|
+
self.messages = self._init_messages(self.session, self._system_prompt)
|
|
414
|
+
self._ctx.reset()
|
|
415
|
+
|
|
416
|
+
async def compact(self, instructions: str | None = None) -> None:
|
|
417
|
+
"""手动触发压缩:无条件执行一次 L4 摘要(写缓存 + 事件),不动 messages。"""
|
|
418
|
+
await self._ctx.compact(self.messages, instructions=instructions)
|
|
419
|
+
await self._handle_compaction()
|
|
420
|
+
|
|
421
|
+
# ── 内部实现 ─────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
async def _notify(self, event: Event) -> None:
|
|
424
|
+
"""将生命周期事件安全广播给所有旁路订阅者(对标 Tau AgentHarness._notify)。"""
|
|
425
|
+
for sub in list(self._subscribers):
|
|
426
|
+
with contextlib.suppress(Exception):
|
|
427
|
+
res = sub(event)
|
|
428
|
+
if inspect.isawaitable(res):
|
|
429
|
+
await res
|
|
430
|
+
|
|
431
|
+
async def _handle_compaction(self) -> None:
|
|
432
|
+
"""prepare/force_compact 触发压缩后:写回 session(桥)+ 事件。"""
|
|
433
|
+
self._ctx_bridge.write_compaction(self._ctx)
|
|
434
|
+
info = self._ctx.pending_compaction
|
|
435
|
+
if info is not None:
|
|
436
|
+
ev = ContextCompacted(
|
|
437
|
+
tokens_before=info.tokens_before,
|
|
438
|
+
tokens_after=info.tokens_after,
|
|
439
|
+
summarized_count=info.summarized_count,
|
|
440
|
+
)
|
|
441
|
+
await self._notify(ev)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""后台异步任务执行器与孤儿进程防御调度引擎。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import atexit
|
|
7
|
+
import contextlib
|
|
8
|
+
import os
|
|
9
|
+
import signal
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING, Literal
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from my_agent_core.message_queue import ( # pyright: ignore[reportMissingImports]
|
|
18
|
+
MessageQueue,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class BackgroundJob:
|
|
24
|
+
"""单个后台作业状态。"""
|
|
25
|
+
|
|
26
|
+
id: str
|
|
27
|
+
description: str
|
|
28
|
+
status: Literal["running", "completed", "failed", "cancelled"] = "running"
|
|
29
|
+
result: str | None = None
|
|
30
|
+
exit_code: int | None = None
|
|
31
|
+
process: asyncio.subprocess.Process | None = None
|
|
32
|
+
started_at: float = field(default_factory=time.time)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _kill_process_tree(proc: asyncio.subprocess.Process | None) -> None:
|
|
36
|
+
"""递归杀死子进程及其整个子进程树,防止孤儿进程遗留。"""
|
|
37
|
+
if proc is None or proc.returncode is not None or not proc.pid:
|
|
38
|
+
return
|
|
39
|
+
with contextlib.suppress(Exception):
|
|
40
|
+
if os.name == "nt":
|
|
41
|
+
subprocess.run(
|
|
42
|
+
["taskkill", "/F", "/T", "/PID", str(proc.pid)],
|
|
43
|
+
stdout=subprocess.DEVNULL,
|
|
44
|
+
stderr=subprocess.DEVNULL,
|
|
45
|
+
check=False,
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BackgroundRunner:
|
|
52
|
+
"""管理后台异步作业生命周期并自动向 MessageQueue 投递完成通知。"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, message_queue: MessageQueue) -> None:
|
|
55
|
+
self.message_queue = message_queue
|
|
56
|
+
self.jobs: dict[str, BackgroundJob] = {}
|
|
57
|
+
self._counter = 0
|
|
58
|
+
atexit.register(self._sync_cleanup)
|
|
59
|
+
|
|
60
|
+
def _sync_cleanup(self) -> None:
|
|
61
|
+
"""进程退出时强制清理所有存活的子进程树,杜绝孤儿进程。"""
|
|
62
|
+
for job in self.jobs.values():
|
|
63
|
+
if (
|
|
64
|
+
job.status == "running"
|
|
65
|
+
and job.process
|
|
66
|
+
and job.process.returncode is None
|
|
67
|
+
):
|
|
68
|
+
_kill_process_tree(job.process)
|
|
69
|
+
|
|
70
|
+
async def run_process(
|
|
71
|
+
self, command: str, cwd: Path | str, description: str = ""
|
|
72
|
+
) -> str:
|
|
73
|
+
"""异步启动操作系统后台子进程,立即返回 job_id。"""
|
|
74
|
+
self._counter += 1
|
|
75
|
+
job_id = f"bg_{self._counter:06x}"
|
|
76
|
+
job = BackgroundJob(
|
|
77
|
+
id=job_id, description=description or command, status="running"
|
|
78
|
+
)
|
|
79
|
+
self.jobs[job_id] = job
|
|
80
|
+
|
|
81
|
+
async def _worker() -> None:
|
|
82
|
+
try:
|
|
83
|
+
proc = await asyncio.create_subprocess_shell(
|
|
84
|
+
command,
|
|
85
|
+
cwd=str(cwd),
|
|
86
|
+
stdout=asyncio.subprocess.PIPE,
|
|
87
|
+
stderr=asyncio.subprocess.PIPE,
|
|
88
|
+
)
|
|
89
|
+
job.process = proc
|
|
90
|
+
stdout, stderr = await proc.communicate()
|
|
91
|
+
output = (
|
|
92
|
+
stdout.decode(errors="replace") + stderr.decode(errors="replace")
|
|
93
|
+
).strip()
|
|
94
|
+
job.exit_code = proc.returncode
|
|
95
|
+
job.result = output[:20000] if output else "(no output)"
|
|
96
|
+
job.status = "completed" if proc.returncode == 0 else "failed"
|
|
97
|
+
except Exception as e:
|
|
98
|
+
job.status = "failed"
|
|
99
|
+
job.result = str(e)
|
|
100
|
+
job.exit_code = 1
|
|
101
|
+
|
|
102
|
+
notification = (
|
|
103
|
+
f'<task_notification id="{job.id}">\n'
|
|
104
|
+
f"Background task {job.id} ({job.description}) {job.status} (exit code {job.exit_code}):\n"
|
|
105
|
+
f"{job.result}\n"
|
|
106
|
+
f"</task_notification>"
|
|
107
|
+
)
|
|
108
|
+
self.message_queue.add_followup(notification)
|
|
109
|
+
|
|
110
|
+
asyncio.create_task(_worker())
|
|
111
|
+
return job_id
|
|
112
|
+
|
|
113
|
+
async def cancel_all(self) -> None:
|
|
114
|
+
"""取消所有正在运行的后台子进程并递归杀死进程树。"""
|
|
115
|
+
for job in self.jobs.values():
|
|
116
|
+
if job.status == "running":
|
|
117
|
+
job.status = "cancelled"
|
|
118
|
+
_kill_process_tree(job.process)
|
|
119
|
+
if job.process:
|
|
120
|
+
with contextlib.suppress(Exception):
|
|
121
|
+
await job.process.wait()
|