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.
Files changed (141) hide show
  1. package/README.md +318 -0
  2. package/package.json +45 -0
  3. package/pyproject.toml +50 -0
  4. package/src/my_agent_core/__init__.py +123 -0
  5. package/src/my_agent_core/agent.py +441 -0
  6. package/src/my_agent_core/background.py +121 -0
  7. package/src/my_agent_core/context.py +505 -0
  8. package/src/my_agent_core/events.py +153 -0
  9. package/src/my_agent_core/extensions/__init__.py +9 -0
  10. package/src/my_agent_core/extensions/core.py +197 -0
  11. package/src/my_agent_core/hooks.py +130 -0
  12. package/src/my_agent_core/loop.py +709 -0
  13. package/src/my_agent_core/main.py +134 -0
  14. package/src/my_agent_core/memory.py +241 -0
  15. package/src/my_agent_core/message_queue.py +110 -0
  16. package/src/my_agent_core/plugins.py +212 -0
  17. package/src/my_agent_core/registry.py +186 -0
  18. package/src/my_agent_core/session/__init__.py +79 -0
  19. package/src/my_agent_core/session/entries.py +197 -0
  20. package/src/my_agent_core/session/jsonl.py +60 -0
  21. package/src/my_agent_core/session/memory.py +137 -0
  22. package/src/my_agent_core/session/session.py +400 -0
  23. package/src/my_agent_core/session/storage.py +245 -0
  24. package/src/my_agent_core/session/store.py +131 -0
  25. package/src/my_agent_core/session/tree.py +86 -0
  26. package/src/my_agent_core/skills.py +149 -0
  27. package/src/my_agent_core/subagent_tasks.py +170 -0
  28. package/src/my_agent_core/subagents.py +148 -0
  29. package/src/my_agent_core/task_store.py +248 -0
  30. package/src/my_agent_core/tool_history.py +189 -0
  31. package/src/my_agent_core/tools/__init__.py +5 -0
  32. package/src/my_agent_core/tools/builtin/__init__.py +5 -0
  33. package/src/my_agent_core/tools/builtin/task.py +30 -0
  34. package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
  35. package/src/my_agent_core/tools/core.py +239 -0
  36. package/src/my_agent_llm/__init__.py +45 -0
  37. package/src/my_agent_llm/auth/__init__.py +46 -0
  38. package/src/my_agent_llm/auth/antigravity.py +209 -0
  39. package/src/my_agent_llm/auth/manager.py +259 -0
  40. package/src/my_agent_llm/auth/quota.py +56 -0
  41. package/src/my_agent_llm/auth/schema.py +94 -0
  42. package/src/my_agent_llm/client.py +116 -0
  43. package/src/my_agent_llm/config.py +17 -0
  44. package/src/my_agent_llm/events.py +84 -0
  45. package/src/my_agent_llm/models.py +195 -0
  46. package/src/my_agent_llm/providers/__init__.py +4 -0
  47. package/src/my_agent_llm/providers/_base.py +94 -0
  48. package/src/my_agent_llm/providers/anthropic.py +298 -0
  49. package/src/my_agent_llm/providers/antigravity.py +480 -0
  50. package/src/my_agent_llm/providers/deepseek.py +196 -0
  51. package/src/my_agent_llm/providers/openai.py +364 -0
  52. package/src/my_agent_llm/providers/registry.py +16 -0
  53. package/src/my_agent_llm/stream.py +218 -0
  54. package/src/my_coding_agent/__init__.py +66 -0
  55. package/src/my_coding_agent/agent.py +208 -0
  56. package/src/my_coding_agent/cli.py +78 -0
  57. package/src/my_coding_agent/file_reference.py +80 -0
  58. package/src/my_coding_agent/macro.py +408 -0
  59. package/src/my_coding_agent/mcp.py +243 -0
  60. package/src/my_coding_agent/mutation_queue.py +37 -0
  61. package/src/my_coding_agent/paths.py +119 -0
  62. package/src/my_coding_agent/permissions.py +84 -0
  63. package/src/my_coding_agent/prompt.py +54 -0
  64. package/src/my_coding_agent/rpc_server.py +2817 -0
  65. package/src/my_coding_agent/settings.py +126 -0
  66. package/src/my_coding_agent/tools/__init__.py +55 -0
  67. package/src/my_coding_agent/tools/base.py +58 -0
  68. package/src/my_coding_agent/tools/bash.py +206 -0
  69. package/src/my_coding_agent/tools/edit.py +226 -0
  70. package/src/my_coding_agent/tools/find.py +118 -0
  71. package/src/my_coding_agent/tools/grep.py +177 -0
  72. package/src/my_coding_agent/tools/ls.py +112 -0
  73. package/src/my_coding_agent/tools/read.py +113 -0
  74. package/src/my_coding_agent/tools/write.py +72 -0
  75. package/tui/README.md +27 -0
  76. package/tui/bin/my-agent.js +98 -0
  77. package/tui/dist/app.d.ts +41 -0
  78. package/tui/dist/app.js +110 -0
  79. package/tui/dist/bridge/event-translator.d.ts +92 -0
  80. package/tui/dist/bridge/event-translator.js +216 -0
  81. package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
  82. package/tui/dist/bridge/kernel-bridge.js +132 -0
  83. package/tui/dist/client.d.ts +63 -0
  84. package/tui/dist/client.js +239 -0
  85. package/tui/dist/components/assistant-message.d.ts +19 -0
  86. package/tui/dist/components/assistant-message.js +90 -0
  87. package/tui/dist/components/compaction-summary-message.d.ts +19 -0
  88. package/tui/dist/components/compaction-summary-message.js +46 -0
  89. package/tui/dist/components/custom-editor.d.ts +18 -0
  90. package/tui/dist/components/custom-editor.js +56 -0
  91. package/tui/dist/components/dynamic-border.d.ts +9 -0
  92. package/tui/dist/components/dynamic-border.js +14 -0
  93. package/tui/dist/components/footer.d.ts +39 -0
  94. package/tui/dist/components/footer.js +199 -0
  95. package/tui/dist/components/header.d.ts +4 -0
  96. package/tui/dist/components/header.js +21 -0
  97. package/tui/dist/components/keys.d.ts +5 -0
  98. package/tui/dist/components/keys.js +12 -0
  99. package/tui/dist/components/login-selector.d.ts +26 -0
  100. package/tui/dist/components/login-selector.js +181 -0
  101. package/tui/dist/components/logout-selector.d.ts +19 -0
  102. package/tui/dist/components/logout-selector.js +88 -0
  103. package/tui/dist/components/model-selector.d.ts +40 -0
  104. package/tui/dist/components/model-selector.js +268 -0
  105. package/tui/dist/components/session-selector.d.ts +54 -0
  106. package/tui/dist/components/session-selector.js +393 -0
  107. package/tui/dist/components/settings-selector.d.ts +24 -0
  108. package/tui/dist/components/settings-selector.js +146 -0
  109. package/tui/dist/components/status-indicator.d.ts +25 -0
  110. package/tui/dist/components/status-indicator.js +60 -0
  111. package/tui/dist/components/theme-selector.d.ts +14 -0
  112. package/tui/dist/components/theme-selector.js +77 -0
  113. package/tui/dist/components/thinking-selector.d.ts +21 -0
  114. package/tui/dist/components/thinking-selector.js +128 -0
  115. package/tui/dist/components/tool-execution.d.ts +31 -0
  116. package/tui/dist/components/tool-execution.js +206 -0
  117. package/tui/dist/components/tree-selector.d.ts +40 -0
  118. package/tui/dist/components/tree-selector.js +173 -0
  119. package/tui/dist/components/user-message-selector.d.ts +21 -0
  120. package/tui/dist/components/user-message-selector.js +103 -0
  121. package/tui/dist/components/user-message.d.ts +5 -0
  122. package/tui/dist/components/user-message.js +15 -0
  123. package/tui/dist/index.d.ts +11 -0
  124. package/tui/dist/index.js +11 -0
  125. package/tui/dist/interactive/chat-viewport.d.ts +19 -0
  126. package/tui/dist/interactive/chat-viewport.js +41 -0
  127. package/tui/dist/interactive/components.d.ts +1 -0
  128. package/tui/dist/interactive/components.js +1 -0
  129. package/tui/dist/interactive/interactive-mode.d.ts +89 -0
  130. package/tui/dist/interactive/interactive-mode.js +1625 -0
  131. package/tui/dist/interactive/theme.d.ts +1 -0
  132. package/tui/dist/interactive/theme.js +1 -0
  133. package/tui/dist/interactive/tui-renderer.d.ts +8 -0
  134. package/tui/dist/interactive/tui-renderer.js +10 -0
  135. package/tui/dist/protocol.d.ts +78 -0
  136. package/tui/dist/protocol.js +1 -0
  137. package/tui/dist/theme/dark.json +54 -0
  138. package/tui/dist/theme/light.json +71 -0
  139. package/tui/dist/theme/theme.d.ts +20 -0
  140. package/tui/dist/theme/theme.js +86 -0
  141. 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()