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,709 @@
1
+ """纯函数 ReAct 微内核 (loop.py) 与上下文清洗 (_provider_context)。
2
+
3
+ 本模块将 ReAct 循环彻底解耦为无状态、可组合的异步生成器,外部宿主只需消费事件流。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import contextlib
10
+ import inspect
11
+ import logging
12
+ import threading
13
+ from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
14
+ from typing import Any
15
+
16
+ from my_agent_core.events import (
17
+ AgentEnd,
18
+ AgentStart,
19
+ ContextCompacted,
20
+ Event,
21
+ MessageEnd,
22
+ MessageStart,
23
+ MessageUpdate,
24
+ ToolExecutionEnd,
25
+ ToolExecutionStart,
26
+ ToolExecutionUpdate,
27
+ TurnEnd,
28
+ TurnStart,
29
+ )
30
+ from my_agent_core.hooks import ( # pyright: ignore[reportMissingImports]
31
+ BeforeModelCallHook,
32
+ HookResult,
33
+ ToolCallHook,
34
+ ToolResultHook,
35
+ )
36
+ from my_agent_core.registry import ToolRegistry
37
+ from my_agent_core.tool_history import (
38
+ _INTERRUPTED_TOOL_RESULT,
39
+ repair_tool_history,
40
+ )
41
+ from my_agent_core.tools import ToolResult
42
+ from my_agent_llm import Message, StreamChunk, ToolCall
43
+ from my_agent_llm.events import ( # pyright: ignore[reportMissingImports]
44
+ StreamDoneEvent,
45
+ StreamErrorEvent,
46
+ StreamStartEvent,
47
+ TextDeltaEvent,
48
+ ThinkingDeltaEvent,
49
+ ToolCallDoneEvent,
50
+ )
51
+ from my_agent_llm.stream import ( # pyright: ignore[reportMissingImports]
52
+ StreamAccumulator,
53
+ )
54
+
55
+ logger = logging.getLogger(__name__)
56
+
57
+ __all__ = [
58
+ "CancellationToken",
59
+ "_assistant_turn",
60
+ "_execute_tools_turn",
61
+ "_provider_context",
62
+ "_synthesize_interrupted_tool_calls",
63
+ "run_agent_loop",
64
+ ]
65
+
66
+
67
+ class CancellationToken:
68
+ """协作式取消令牌,供宿主在流式过程中从外部主动请求安全中断。"""
69
+
70
+ def __init__(self) -> None:
71
+ self._cancelled: bool = False
72
+
73
+ def is_cancelled(self) -> bool:
74
+ """检查是否已请求取消。"""
75
+ return self._cancelled
76
+
77
+ def cancel(self) -> None:
78
+ """触发协作式取消。"""
79
+ self._cancelled = True
80
+
81
+ @property
82
+ def cancelled(self) -> bool:
83
+ """取消状态属性访问捷径。"""
84
+ return self._cancelled
85
+
86
+
87
+ def _provider_context(messages: Sequence[Message]) -> list[Message]:
88
+ """清洗会话历史以严格满足主流大模型 Provider 的上下文契约。
89
+
90
+ 1. 剥离无正文且以异常中断结尾的终端 assistant 失败轮次(避免 OpenAI/Anthropic 400);
91
+ 2. 串联 repair_tool_history 拓扑修复,自动补齐断头调用并安全丢弃孤儿结果。
92
+ """
93
+ replayable = tuple(
94
+ m
95
+ for m in messages
96
+ if not (
97
+ m.role == "assistant"
98
+ and bool(
99
+ m.metadata
100
+ and m.metadata.get("stop_reason") in {"error", "aborted", "cancelled"}
101
+ )
102
+ and not m.content
103
+ )
104
+ )
105
+ return list(repair_tool_history(replayable).messages)
106
+
107
+
108
+ async def _assistant_turn(
109
+ *,
110
+ llm: Any,
111
+ view: list[Message],
112
+ tool_schemas: list[dict[str, Any]],
113
+ model: str | None = None,
114
+ signal: CancellationToken | None = None,
115
+ context_manager: Any | None = None,
116
+ ) -> AsyncIterator[Event]:
117
+ """专职大模型推理车间:纯粹转译模型层产出的高阶 StreamEvent(对标 Tau _assistant_events)。"""
118
+ if hasattr(llm, "astream_events"):
119
+ event_stream = llm.astream_events(
120
+ messages=view, tools=tool_schemas, model=model, signal=signal
121
+ )
122
+ else:
123
+ acc = StreamAccumulator()
124
+ event_stream = acc.stream(
125
+ llm.achat_stream(messages=view, tools=tool_schemas, model=model),
126
+ signal=signal,
127
+ )
128
+
129
+ async for ev in event_stream:
130
+ if isinstance(ev, StreamStartEvent):
131
+ yield MessageStart(ev.partial)
132
+ elif isinstance(ev, TextDeltaEvent):
133
+ yield MessageUpdate(message=ev.partial, chunk=StreamChunk(content=ev.delta))
134
+ elif isinstance(ev, ThinkingDeltaEvent):
135
+ yield MessageUpdate(
136
+ message=ev.partial,
137
+ chunk=StreamChunk(content="", metadata={"reasoning_content": ev.delta}),
138
+ )
139
+ elif isinstance(ev, ToolCallDoneEvent):
140
+ yield MessageUpdate(
141
+ message=ev.partial,
142
+ chunk=StreamChunk(content="", tool_calls=[ev.tool_call]),
143
+ )
144
+ elif isinstance(ev, StreamDoneEvent):
145
+ if (
146
+ ev.usage
147
+ and context_manager is not None
148
+ and hasattr(context_manager, "record_usage")
149
+ ):
150
+ with contextlib.suppress(Exception):
151
+ context_manager.record_usage(ev.usage)
152
+ yield MessageEnd(ev.message)
153
+ elif isinstance(ev, StreamErrorEvent):
154
+ yield MessageEnd(ev.error)
155
+
156
+
157
+ def _as_messages(items: Sequence[Message | str]) -> list[Message]:
158
+ """安全归一化字符串或消息序列为标准 Message 列表。"""
159
+ return [
160
+ m if isinstance(m, Message) else Message(role="user", content=m) for m in items
161
+ ]
162
+
163
+
164
+ def _synthesize_interrupted_tool_calls(
165
+ tool_calls: Sequence[Any],
166
+ ) -> list[Message]:
167
+ """统一生成标准的中断工具结果,彻底消除多处代码重复。"""
168
+ return [
169
+ Message(
170
+ role="tool",
171
+ content=_INTERRUPTED_TOOL_RESULT,
172
+ metadata={"tool_call_id": _coerce_tool_call(tc).id, "is_error": True},
173
+ )
174
+ for tc in tool_calls
175
+ ]
176
+
177
+
178
+ def _coerce_tool_call(tc: Any) -> ToolCall:
179
+ """安全归一化工具调用为 ToolCall 实体,Never-Throw 捕获畸形入参。"""
180
+ if isinstance(tc, ToolCall):
181
+ return tc
182
+ try:
183
+ if isinstance(tc, dict) and "id" not in tc:
184
+ tc = {**tc, "id": ""}
185
+ return ToolCall.model_validate(tc)
186
+ except Exception as exc:
187
+ raw_id = getattr(tc, "id", None)
188
+ if not raw_id:
189
+ raw_id = tc.get("id", "") if isinstance(tc, dict) else ""
190
+ raw_name = getattr(tc, "name", None)
191
+ if not raw_name:
192
+ raw_name = tc.get("name", "") if isinstance(tc, dict) else ""
193
+ return ToolCall(
194
+ id=str(raw_id if raw_id else ""),
195
+ name=str(raw_name if raw_name else ""),
196
+ error=f"Invalid tool call: {exc}",
197
+ )
198
+
199
+
200
+ async def _fail_tool_calls_from_truncated_message(
201
+ tool_calls: Sequence[Any],
202
+ ) -> AsyncIterator[Event]:
203
+ """阶段 1: 当模型因触达 Token 上限导致输出截断 (stop_reason='length') 时,安全拦截所有工具调用。
204
+
205
+ 防范流式 salvage 拼装出残缺的 JSON 参数导致文件写崩或命令截断。
206
+ 生成清晰的重试提示结果回传给大模型,引导其重新完整发起调用。
207
+ """
208
+ for tc in tool_calls:
209
+ call = _coerce_tool_call(tc)
210
+ yield ToolExecutionStart(
211
+ tool_call_id=call.id,
212
+ tool_name=call.name,
213
+ args=call.args,
214
+ )
215
+ err_msg = (
216
+ f'Tool call "{call.name}" was not executed: the response hit the output '
217
+ "token limit, so its arguments may be truncated. Re-issue the tool call with complete arguments."
218
+ )
219
+ yield ToolExecutionEnd(
220
+ tool_call_id=call.id,
221
+ tool_name=call.name,
222
+ result=err_msg,
223
+ is_error=True,
224
+ )
225
+ tool_msg = Message(
226
+ role="tool",
227
+ content=err_msg,
228
+ metadata={"tool_call_id": call.id, "is_error": True},
229
+ )
230
+ yield MessageStart(tool_msg)
231
+ yield MessageEnd(tool_msg)
232
+
233
+
234
+ async def _execute_tools_turn(
235
+ *,
236
+ tool_calls: Sequence[Any],
237
+ registry: ToolRegistry,
238
+ before_tool_call: (
239
+ Callable[[ToolCallHook], Awaitable[HookResult | None] | HookResult | None]
240
+ | None
241
+ ) = None,
242
+ after_tool_call: (
243
+ Callable[[ToolResultHook], Awaitable[HookResult | None] | HookResult | None]
244
+ | None
245
+ ) = None,
246
+ signal: CancellationToken | None = None,
247
+ ) -> AsyncIterator[Event]:
248
+ """专职工具批处理执行车间:Preflight 广播 -> 审批改参 -> 并发批执行 -> 结果改写 -> 结果广播。
249
+
250
+ 严格遵循 Pi 时序契约:
251
+ 1. Preflight 阶段:在调用 before_tool_call 审查与执行之前,率先按 source order 广播 ToolExecutionStart;
252
+ 2. 审查阶段:调用 before_tool_call 审批与入参改写;
253
+ 3. Execution 阶段:并发批量执行未阻断工具,支持实时流式进度回传 (ToolExecutionUpdate) 与中途取消自愈;
254
+ 4. Completion 阶段:按 source order 执行 after_tool_call 改写并广播 ToolExecutionEnd (含 terminate 状态);
255
+ 5. Message 阶段:按 source order 发射 role="tool" 的 MessageStart / MessageEnd。
256
+ """
257
+ # ── 阶段 A1: Preflight 广播(按 source order 先行发射 ToolExecutionStart)
258
+ parsed_calls: list[ToolCall] = [_coerce_tool_call(tc) for tc in tool_calls]
259
+ for call in parsed_calls:
260
+ yield ToolExecutionStart(
261
+ tool_call_id=call.id, tool_name=call.name, args=call.args
262
+ )
263
+
264
+ # ── 阶段 A2: 前置审查与参数改写 (before_tool_call)
265
+ prepared_calls: list[tuple[int, ToolCall, dict[str, Any]]] = []
266
+ direct_results: dict[int, ToolResult] = {}
267
+
268
+ for idx, call in enumerate(parsed_calls):
269
+ if call.error is not None:
270
+ direct_results[idx] = ToolResult(ok=False, error=call.error)
271
+ continue
272
+
273
+ if signal is not None and signal.is_cancelled():
274
+ direct_results[idx] = ToolResult(ok=False, error=_INTERRUPTED_TOOL_RESULT)
275
+ continue
276
+
277
+ current_args = call.args
278
+ err: str | None = None
279
+ block_terminate: bool = False
280
+ if before_tool_call is not None:
281
+ try:
282
+ decision = before_tool_call(
283
+ ToolCallHook(
284
+ tool_call_id=call.id, tool_name=call.name, args=current_args
285
+ )
286
+ )
287
+ if inspect.isawaitable(decision):
288
+ decision = await decision
289
+ if decision is not None:
290
+ if decision.block:
291
+ err = f"Tool '{call.name}' blocked: {decision.reason or 'blocked by policy'}"
292
+ if decision.terminate is not None:
293
+ block_terminate = decision.terminate
294
+ elif decision.updated_args is not None:
295
+ current_args = decision.updated_args
296
+ except Exception as exc:
297
+ err = f"Error in before_tool_call for '{call.name}': {exc}"
298
+
299
+ if err is not None:
300
+ direct_results[idx] = ToolResult(
301
+ ok=False, error=err, terminate=block_terminate
302
+ )
303
+ else:
304
+ prepared_calls.append((idx, call, current_args))
305
+
306
+ # ── 阶段 B: 并发批执行与实时进度流式广播 (Phase 5)
307
+ if signal is not None and signal.is_cancelled():
308
+ for idx, _, _ in prepared_calls:
309
+ direct_results[idx] = ToolResult(ok=False, error=_INTERRUPTED_TOOL_RESULT)
310
+ elif prepared_calls:
311
+ # 建立线程安全事件队列与哨兵
312
+ queue: asyncio.Queue[Event | object] = asyncio.Queue()
313
+ _SENTINEL = object()
314
+ loop = asyncio.get_running_loop()
315
+ loop_thread_id = threading.get_ident()
316
+
317
+ def safe_put(item: Event | object) -> None:
318
+ if threading.get_ident() == loop_thread_id:
319
+ queue.put_nowait(item)
320
+ else:
321
+ with contextlib.suppress(RuntimeError):
322
+ loop.call_soon_threadsafe(queue.put_nowait, item)
323
+
324
+ def make_on_update(
325
+ call_id: str, tool_name: str, args: dict[str, Any]
326
+ ) -> Callable[[Any], None]:
327
+ def on_update(partial: Any) -> None:
328
+ safe_put(
329
+ ToolExecutionUpdate(
330
+ tool_call_id=call_id,
331
+ tool_name=tool_name,
332
+ args=args,
333
+ partial_result=partial,
334
+ )
335
+ )
336
+
337
+ return on_update
338
+
339
+ calls_to_run = [
340
+ (
341
+ call.name,
342
+ current_args,
343
+ make_on_update(call.id, call.name, current_args),
344
+ call.id,
345
+ )
346
+ for _, call, current_args in prepared_calls
347
+ ]
348
+
349
+ async def _run_batch() -> list[ToolResult]:
350
+ try:
351
+ return await registry.execute_batch(calls_to_run, signal=signal)
352
+ finally:
353
+ safe_put(_SENTINEL)
354
+
355
+ runner = asyncio.create_task(_run_batch())
356
+ try:
357
+ while True:
358
+ item = await queue.get()
359
+ if item is _SENTINEL:
360
+ break
361
+ if isinstance(item, Event):
362
+ yield item
363
+ batch_out = await runner
364
+ for (idx, _, _), res in zip(prepared_calls, batch_out, strict=False):
365
+ direct_results[idx] = res
366
+ except Exception as exc:
367
+ for idx, _, _ in prepared_calls:
368
+ if idx not in direct_results:
369
+ direct_results[idx] = ToolResult(
370
+ ok=False, error=f"Tool execution failed: {exc}"
371
+ )
372
+ finally:
373
+ if not runner.done():
374
+ runner.cancel()
375
+ with contextlib.suppress(asyncio.CancelledError, Exception):
376
+ await runner
377
+
378
+ # ── 阶段 C: 后置改写与 ToolExecutionEnd 广播
379
+ for idx, call in enumerate(parsed_calls):
380
+ res = direct_results.get(
381
+ idx, ToolResult(ok=False, error=_INTERRUPTED_TOOL_RESULT)
382
+ )
383
+ obs = res.serialize()
384
+ is_err = not res.ok
385
+ effective_terminate = res.terminate
386
+
387
+ # 触发决策点 5: tool_result (after_tool_call 结果篡改与熔断介入)
388
+ if after_tool_call is not None and not (
389
+ signal is not None and signal.is_cancelled()
390
+ ):
391
+ try:
392
+ decision = after_tool_call(
393
+ ToolResultHook(
394
+ tool_call_id=call.id,
395
+ tool_name=call.name,
396
+ result=obs,
397
+ is_error=is_err,
398
+ terminate=effective_terminate,
399
+ )
400
+ )
401
+ if inspect.isawaitable(decision):
402
+ decision = await decision
403
+ if decision is not None:
404
+ if decision.block:
405
+ obs = f"Tool '{call.name}' blocked: {decision.reason or 'blocked by policy'}"
406
+ is_err = True
407
+ elif decision.updated_result is not None:
408
+ obs = decision.updated_result
409
+ is_err = False
410
+ if decision.terminate is not None:
411
+ effective_terminate = decision.terminate
412
+ except Exception as exc:
413
+ obs = f"Error in after_tool_call for '{call.name}': {exc}"
414
+ is_err = True
415
+
416
+ yield ToolExecutionEnd(
417
+ tool_call_id=call.id,
418
+ tool_name=call.name,
419
+ result=obs,
420
+ is_error=is_err,
421
+ terminate=effective_terminate,
422
+ )
423
+
424
+ # 阶段 7: 产出配对的 Tool 消息实体并携带关键元数据
425
+ tool_msg = Message(
426
+ role="tool",
427
+ content=obs,
428
+ metadata={
429
+ "tool_call_id": call.id,
430
+ "is_error": is_err,
431
+ "terminate": effective_terminate,
432
+ },
433
+ )
434
+
435
+ # 发射成对的 MessageStart / MessageEnd 事件驱动多层持久化:
436
+ # 1. 内存层:外层 run_agent_loop 监听到 MessageEnd 会将 tool_msg 追加到 messages 列表,供给下一轮大模型推理;
437
+ # 2. 磁盘层:外壳层 Agent.prompt_stream 监听到 MessageEnd 会触发 session.add_message 原子落盘到 JSONL 文件;
438
+ # 3. 熔断层:通过 metadata["terminate"] 向外透传阶段 7 熔断标记,驱动 ReAct 循环终止。
439
+ yield MessageStart(tool_msg)
440
+ yield MessageEnd(tool_msg)
441
+
442
+
443
+ async def run_agent_loop(
444
+ *,
445
+ llm: Any,
446
+ messages: list[Message],
447
+ tools: ToolRegistry | Sequence[Any] | None = None,
448
+ context_manager: Any | None = None,
449
+ model: str | None = None,
450
+ system: str = "",
451
+ prompts: Sequence[Message | str] = (),
452
+ max_turns: int | None = None,
453
+ max_iterations: int | None = None,
454
+ signal: CancellationToken | None = None,
455
+ get_steering_messages: Callable[[], Sequence[Message | str]] | None = None,
456
+ get_follow_up_messages: Callable[[], Sequence[Message | str]] | None = None,
457
+ before_model_call: (
458
+ Callable[
459
+ [BeforeModelCallHook], Awaitable[HookResult | None] | HookResult | None
460
+ ]
461
+ | None
462
+ ) = None,
463
+ before_tool_call: (
464
+ Callable[[ToolCallHook], Awaitable[HookResult | None] | HookResult | None]
465
+ | None
466
+ ) = None,
467
+ after_tool_call: (
468
+ Callable[[ToolResultHook], Awaitable[HookResult | None] | HookResult | None]
469
+ | None
470
+ ) = None,
471
+ ) -> AsyncIterator[Event]:
472
+ """对标 Tau 的极简纯函数异步微内核,主状态机约 110 行。"""
473
+ if isinstance(tools, ToolRegistry):
474
+ registry = tools
475
+ else:
476
+ registry = ToolRegistry()
477
+ if isinstance(tools, Sequence):
478
+ for t in tools:
479
+ registry.register(t)
480
+
481
+ effective_max = max_turns if max_turns is not None else max_iterations
482
+
483
+ # 初始化协作取消检查
484
+ if signal is not None and signal.is_cancelled():
485
+ yield AgentEnd(
486
+ messages=list(messages),
487
+ final_text=None,
488
+ iterations=0,
489
+ stop_reason="cancelled",
490
+ )
491
+ return
492
+
493
+ # system prompt 初始化
494
+ if system and (not messages or messages[0].role != "system"):
495
+ messages.insert(0, Message(role="system", content=system))
496
+ elif not system and messages and messages[0].role == "system":
497
+ system = messages[0].content
498
+
499
+ # prompts 规范化
500
+ converted_prompts = _as_messages(prompts)
501
+
502
+ user_input = converted_prompts[0].content if converted_prompts else ""
503
+
504
+ # 1. 注入初始 prompts 并发射事件
505
+ yield AgentStart(system_prompt=system, user_input=user_input)
506
+ for p_msg in converted_prompts:
507
+ messages.append(p_msg)
508
+ yield MessageStart(p_msg)
509
+ yield MessageEnd(p_msg)
510
+
511
+ iteration = 0
512
+ final_text: str | None = None
513
+ pending_messages: list[Message] = []
514
+ if get_steering_messages is not None:
515
+ init_steer = get_steering_messages()
516
+ if init_steer:
517
+ pending_messages.extend(_as_messages(init_steer))
518
+
519
+ # ══════════════════════════════════════════════════════════
520
+ # 【外层循环】:Follow-up 宏观任务接力
521
+ # ══════════════════════════════════════════════════════════
522
+ while True:
523
+ has_more_tools = True
524
+
525
+ # ──────────────────────────────────────────────────────
526
+ # 【内层循环】:微观 ReAct 迭代与 Steer 即时转向
527
+ # ──────────────────────────────────────────────────────
528
+ while has_more_tools or pending_messages:
529
+ iteration += 1
530
+
531
+ # 轮次开端:先注入并清空 pending_messages (Steering)
532
+ if pending_messages:
533
+ for p_msg in pending_messages:
534
+ messages.append(p_msg)
535
+ yield MessageStart(p_msg)
536
+ yield MessageEnd(p_msg)
537
+ pending_messages = []
538
+
539
+ # 检查最大轮次熔断截断
540
+ if effective_max is not None and iteration > effective_max:
541
+ yield AgentEnd(
542
+ messages=list(messages),
543
+ final_text=final_text,
544
+ iterations=iteration,
545
+ stop_reason="max_iterations",
546
+ )
547
+ return
548
+
549
+ yield TurnStart(iteration)
550
+
551
+ # 前置清洗与上下文准备
552
+ clean_messages = _provider_context(messages)
553
+ view = (
554
+ await context_manager.prepare(clean_messages)
555
+ if context_manager
556
+ else clean_messages
557
+ )
558
+
559
+ # 派发上下文压缩事件(若触发了 L4/L2 压缩)
560
+ if (
561
+ context_manager is not None
562
+ and getattr(context_manager, "pending_compaction", None) is not None
563
+ ):
564
+ info = context_manager.pending_compaction
565
+ yield ContextCompacted(
566
+ tokens_before=info.tokens_before,
567
+ tokens_after=info.tokens_after,
568
+ summarized_count=info.summarized_count,
569
+ )
570
+
571
+ # Hook 3: BeforeModelCallHook (context 审查)
572
+ if before_model_call is not None:
573
+ try:
574
+ decision = before_model_call(
575
+ BeforeModelCallHook(messages=list(view), iteration=iteration)
576
+ )
577
+ if inspect.isawaitable(decision):
578
+ decision = await decision
579
+ if decision is not None:
580
+ if decision.block:
581
+ reason = f": {decision.reason}" if decision.reason else ""
582
+ yield TurnEnd(message=None, tool_results=[])
583
+ yield AgentEnd(
584
+ messages=list(messages),
585
+ final_text=f"(blocked{reason})",
586
+ iterations=iteration,
587
+ stop_reason="blocked",
588
+ )
589
+ return
590
+ if decision.updated_messages is not None:
591
+ view = decision.updated_messages
592
+ except Exception as exc:
593
+ logger.warning("Error in before_model_call callback: %s", exc)
594
+
595
+ # 委托模型车间
596
+ assistant: Message | None = None
597
+ async for ev in _assistant_turn(
598
+ llm=llm,
599
+ view=view,
600
+ tool_schemas=registry.get_schemas(),
601
+ model=model,
602
+ signal=signal,
603
+ context_manager=context_manager,
604
+ ):
605
+ yield ev
606
+ if isinstance(ev, MessageEnd):
607
+ assistant = ev.message
608
+
609
+ if assistant is None:
610
+ assistant = Message(
611
+ role="assistant",
612
+ content="Provider produced no assistant message",
613
+ metadata={"stop_reason": "error"},
614
+ )
615
+ yield MessageStart(assistant)
616
+ yield MessageEnd(assistant)
617
+
618
+ messages.append(assistant)
619
+
620
+ # 取消响应、异常阻断与断头自愈
621
+ if assistant.metadata and assistant.metadata.get("stop_reason") in (
622
+ "cancelled",
623
+ "error",
624
+ ):
625
+ stop_reason = assistant.metadata.get("stop_reason", "cancelled")
626
+ calls = assistant.metadata.get("tool_calls", [])
627
+ synth_tools = _synthesize_interrupted_tool_calls(calls)
628
+ for s in synth_tools:
629
+ messages.append(s)
630
+ yield MessageStart(s)
631
+ yield MessageEnd(s)
632
+ yield TurnEnd(message=assistant, tool_results=synth_tools)
633
+ yield AgentEnd(
634
+ messages=list(messages),
635
+ final_text=assistant.content if stop_reason == "error" else None,
636
+ iterations=iteration,
637
+ stop_reason=stop_reason,
638
+ )
639
+ return
640
+
641
+ # 委托工具车间
642
+ tool_results: list[Message] = []
643
+ calls = (assistant.metadata or {}).get("tool_calls")
644
+ is_truncated = (assistant.metadata or {}).get("stop_reason") == "length"
645
+ if calls:
646
+ tool_stream = (
647
+ _fail_tool_calls_from_truncated_message(calls)
648
+ if is_truncated
649
+ else _execute_tools_turn(
650
+ tool_calls=calls,
651
+ registry=registry,
652
+ before_tool_call=before_tool_call,
653
+ after_tool_call=after_tool_call,
654
+ signal=signal,
655
+ )
656
+ )
657
+ async for ev in tool_stream:
658
+ yield ev
659
+ if isinstance(ev, MessageEnd) and ev.message.role == "tool":
660
+ tool_results.append(ev.message)
661
+ messages.append(ev.message)
662
+
663
+ # 阶段 7: 批量优雅熔断判定(any 语义)
664
+ terminating_obs = [
665
+ m.content
666
+ for m in tool_results
667
+ if (m.metadata or {}).get("terminate")
668
+ ]
669
+ if terminating_obs:
670
+ has_more_tools = False
671
+ final_text = assistant.content or terminating_obs[-1]
672
+ else:
673
+ has_more_tools = True
674
+ else:
675
+ has_more_tools = False
676
+ final_text = assistant.content
677
+
678
+ # 严密闭合当前轮次
679
+ yield TurnEnd(message=assistant, tool_results=tool_results)
680
+
681
+ if signal is not None and signal.is_cancelled():
682
+ yield AgentEnd(
683
+ messages=list(messages),
684
+ final_text=final_text,
685
+ iterations=iteration,
686
+ stop_reason="cancelled",
687
+ )
688
+ return
689
+
690
+ # 收割即时转向
691
+ if get_steering_messages is not None:
692
+ steer_msgs = get_steering_messages()
693
+ if steer_msgs:
694
+ pending_messages = _as_messages(steer_msgs)
695
+
696
+ # 收割宏观追问任务
697
+ if get_follow_up_messages is not None:
698
+ followups = get_follow_up_messages()
699
+ if followups:
700
+ pending_messages = _as_messages(followups)
701
+ continue
702
+ break
703
+
704
+ yield AgentEnd(
705
+ messages=list(messages),
706
+ final_text=final_text,
707
+ iterations=iteration,
708
+ stop_reason="end_turn",
709
+ )