初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,303 @@
|
||||
"""Pi agent-core 风格的有状态 Agent 门面。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass, field, replace
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from app.ai_provider.types.chat import ChatChunk, TokenUsage
|
||||
from app.ai_provider.types.messages import BaseMessage, HumanMessage
|
||||
from app.application.agent.confirmation import ConfirmationRegistry
|
||||
from app.application.agent.extensions import (
|
||||
AgentEvent,
|
||||
AgentRunContext,
|
||||
CancellationToken,
|
||||
ExtensionRegistry,
|
||||
)
|
||||
from app.application.agent.runtime import AgentTurnResult, run_agent_turn
|
||||
from app.modules.tool_runtime.definition import ToolRuntimeLike
|
||||
|
||||
|
||||
class AgentStatus(StrEnum):
|
||||
idle = "idle"
|
||||
running = "running"
|
||||
completed = "completed"
|
||||
failed = "failed"
|
||||
cancelled = "cancelled"
|
||||
|
||||
|
||||
class QueueMode(StrEnum):
|
||||
all = "all"
|
||||
one_at_a_time = "one-at-a-time"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentState:
|
||||
"""有状态 Agent 的进程内快照。"""
|
||||
|
||||
messages: list[BaseMessage] = field(default_factory=list)
|
||||
status: AgentStatus = AgentStatus.idle
|
||||
streaming_message: str = ""
|
||||
pending_tool_calls: set[str] = field(default_factory=set)
|
||||
error_message: str | None = None
|
||||
usage: TokenUsage = field(default_factory=TokenUsage)
|
||||
|
||||
|
||||
class PendingMessageQueue:
|
||||
"""steering/follow-up 共用的小型 FIFO。"""
|
||||
|
||||
def __init__(self, mode: QueueMode = QueueMode.one_at_a_time) -> None:
|
||||
self.mode = mode
|
||||
self._messages: list[BaseMessage] = []
|
||||
|
||||
def put(self, message: BaseMessage) -> None:
|
||||
self._messages.append(message)
|
||||
|
||||
def drain(self) -> list[BaseMessage]:
|
||||
if not self._messages:
|
||||
return []
|
||||
if self.mode is QueueMode.all:
|
||||
messages, self._messages = self._messages, []
|
||||
return messages
|
||||
return [self._messages.pop(0)]
|
||||
|
||||
def clear(self) -> None:
|
||||
self._messages.clear()
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._messages)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentConfig:
|
||||
"""Stateful Agent 的稳定运行配置。"""
|
||||
|
||||
tools: tuple[dict[str, Any], ...]
|
||||
tool_runtime: ToolRuntimeLike | None
|
||||
model_hint: str | None = None
|
||||
thinking_enabled: bool | None = None
|
||||
reasoning_effort: str | None = None
|
||||
max_iterations: int = 8
|
||||
confirmation_registry: ConfirmationRegistry | None = None
|
||||
confirmation_timeout: float = 300.0
|
||||
on_turn: Callable[[], Awaitable[None]] | None = None
|
||||
on_chunk: Callable[[ChatChunk], Awaitable[None]] | None = None
|
||||
on_tool_event: Callable[[str, dict[str, Any]], Awaitable[None]] | None = None
|
||||
on_confirm_timeout: Callable[[str], Awaitable[None]] | None = None
|
||||
|
||||
|
||||
class Agent:
|
||||
"""管理状态、消息队列、取消与扩展 Hook;低层循环仍由 run_agent_turn 负责。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: AgentConfig,
|
||||
*,
|
||||
messages: list[BaseMessage] | None = None,
|
||||
run_context: AgentRunContext | None = None,
|
||||
extensions: ExtensionRegistry | None = None,
|
||||
runner: Callable[..., Awaitable[AgentTurnResult]] = run_agent_turn,
|
||||
) -> None:
|
||||
self.config = config
|
||||
self.state = AgentState(messages=list(messages or []))
|
||||
self.run_context = run_context or AgentRunContext()
|
||||
self.extensions = extensions or ExtensionRegistry()
|
||||
self._runner = runner
|
||||
self.steering = PendingMessageQueue()
|
||||
self.follow_ups = PendingMessageQueue()
|
||||
self._active_task: asyncio.Task[AgentTurnResult] | None = None
|
||||
self._accepting_messages = False
|
||||
self._idle = asyncio.Event()
|
||||
self._idle.set()
|
||||
|
||||
def steer(self, message: str | BaseMessage) -> bool:
|
||||
if not self._accepting_messages:
|
||||
return False
|
||||
self.steering.put(self._as_message(message))
|
||||
return True
|
||||
|
||||
def follow_up(self, message: str | BaseMessage) -> bool:
|
||||
if not self._accepting_messages:
|
||||
return False
|
||||
self.follow_ups.put(self._as_message(message))
|
||||
return True
|
||||
|
||||
async def prompt(self, message: str | BaseMessage) -> AgentTurnResult:
|
||||
if self.state.status is AgentStatus.running:
|
||||
raise RuntimeError("Agent 正在运行,请使用 steer 或 follow_up")
|
||||
self.state.messages.append(self._as_message(message))
|
||||
return await self._run()
|
||||
|
||||
async def continue_run(self) -> AgentTurnResult:
|
||||
if self.state.status is AgentStatus.running:
|
||||
raise RuntimeError("Agent 正在运行")
|
||||
return await self._run()
|
||||
|
||||
async def abort(self) -> bool:
|
||||
task = self._active_task
|
||||
if task is None or task.done():
|
||||
return False
|
||||
self.run_context.cancellation.cancel()
|
||||
task.cancel()
|
||||
return True
|
||||
|
||||
async def wait_for_idle(self) -> None:
|
||||
await self._idle.wait()
|
||||
|
||||
async def reset(self) -> None:
|
||||
await self.abort()
|
||||
await self.wait_for_idle()
|
||||
self.state = AgentState()
|
||||
self.steering.clear()
|
||||
self.follow_ups.clear()
|
||||
|
||||
async def _run(self) -> AgentTurnResult:
|
||||
if self.run_context.cancellation.cancelled:
|
||||
self.run_context = replace(self.run_context, cancellation=CancellationToken())
|
||||
self.state.status = AgentStatus.running
|
||||
self.state.streaming_message = ""
|
||||
self.state.pending_tool_calls.clear()
|
||||
self.state.error_message = None
|
||||
self._accepting_messages = True
|
||||
self._idle.clear()
|
||||
try:
|
||||
await self.extensions.publish(AgentEvent("agent.started"), self.run_context)
|
||||
task = asyncio.create_task(self._run_loop())
|
||||
self._active_task = task
|
||||
result = await task
|
||||
self._accepting_messages = False
|
||||
self.state.status = AgentStatus.completed
|
||||
self.state.streaming_message = result.content
|
||||
self.state.usage = result.usage
|
||||
await self.extensions.publish(
|
||||
AgentEvent("agent.ended", {"iterations": result.iterations}), self.run_context
|
||||
)
|
||||
return result
|
||||
except asyncio.CancelledError:
|
||||
self._accepting_messages = False
|
||||
self.state.status = AgentStatus.cancelled
|
||||
await self.extensions.publish(AgentEvent("agent.cancelled"), self.run_context)
|
||||
raise
|
||||
except Exception as exc:
|
||||
self._accepting_messages = False
|
||||
self.state.status = AgentStatus.failed
|
||||
self.state.error_message = str(exc)
|
||||
with suppress(Exception):
|
||||
await self.extensions.publish(
|
||||
AgentEvent("agent.failed", {"error": str(exc)}), self.run_context
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
self._accepting_messages = False
|
||||
with suppress(Exception):
|
||||
await self.extensions.publish(
|
||||
AgentEvent("agent.settled", {"status": self.state.status.value}),
|
||||
self.run_context,
|
||||
)
|
||||
self._active_task = None
|
||||
self._idle.set()
|
||||
|
||||
async def _run_loop(self) -> AgentTurnResult:
|
||||
remaining = self.config.max_iterations
|
||||
total_iterations = 0
|
||||
total_usage = TokenUsage()
|
||||
total_tool_history: list[dict[str, Any]] = []
|
||||
final = AgentTurnResult(content="", reasoning="")
|
||||
while remaining > 0:
|
||||
working_messages = list(self.state.messages)
|
||||
final = await self._runner(
|
||||
messages=working_messages,
|
||||
tools=list(self.config.tools),
|
||||
tool_runtime=self.config.tool_runtime,
|
||||
model_hint=self.config.model_hint,
|
||||
thinking_enabled=self.config.thinking_enabled,
|
||||
reasoning_effort=self.config.reasoning_effort,
|
||||
max_iterations=remaining,
|
||||
run_id=self.run_context.run_id,
|
||||
character_id=self.run_context.character_id,
|
||||
trace_id=self.run_context.trace_id,
|
||||
on_turn=self.config.on_turn,
|
||||
on_chunk=self._on_chunk,
|
||||
on_tool_event=self._on_tool_event,
|
||||
confirmation_registry=self.config.confirmation_registry,
|
||||
confirmation_timeout=self.config.confirmation_timeout,
|
||||
on_confirm_timeout=self.config.on_confirm_timeout,
|
||||
extensions=self.extensions,
|
||||
run_context=self.run_context,
|
||||
get_steering_messages=self.steering.drain,
|
||||
)
|
||||
self.state.messages = working_messages
|
||||
total_iterations += final.iterations
|
||||
total_usage += final.usage
|
||||
total_tool_history.extend(final.tool_history)
|
||||
remaining -= final.iterations
|
||||
if final.truncated:
|
||||
return AgentTurnResult(
|
||||
content=final.content,
|
||||
reasoning=final.reasoning,
|
||||
tool_history=total_tool_history,
|
||||
iterations=total_iterations,
|
||||
truncated=True,
|
||||
usage=total_usage,
|
||||
)
|
||||
if remaining <= 0:
|
||||
return AgentTurnResult(
|
||||
content=final.content,
|
||||
reasoning=final.reasoning,
|
||||
tool_history=total_tool_history,
|
||||
iterations=total_iterations,
|
||||
truncated=bool(self.follow_ups),
|
||||
usage=total_usage,
|
||||
)
|
||||
follow_ups = self.follow_ups.drain()
|
||||
if not follow_ups:
|
||||
return AgentTurnResult(
|
||||
content=final.content,
|
||||
reasoning=final.reasoning,
|
||||
tool_history=total_tool_history,
|
||||
iterations=total_iterations,
|
||||
truncated=final.truncated,
|
||||
usage=total_usage,
|
||||
)
|
||||
self.state.messages.extend(follow_ups)
|
||||
return final
|
||||
|
||||
async def _on_chunk(self, chunk: ChatChunk) -> None:
|
||||
if chunk.delta_content:
|
||||
self.state.streaming_message += chunk.delta_content
|
||||
if self.config.on_chunk is not None:
|
||||
await self.config.on_chunk(chunk)
|
||||
|
||||
async def _on_tool_event(self, event_type: str, payload: dict[str, Any]) -> None:
|
||||
call_id = str(payload.get("tool_call_id", ""))
|
||||
if event_type == "tool.started" and call_id:
|
||||
self.state.pending_tool_calls.add(call_id)
|
||||
elif event_type in ("tool.completed", "tool.failed") and call_id:
|
||||
self.state.pending_tool_calls.discard(call_id)
|
||||
# 先转发 SSE(用户可见流优先),再把同一事件发布进扩展事件流,
|
||||
# 让 AgentEventListener 也能观测工具级生命周期。
|
||||
# 扩展 listener 异常被吞掉,避免观测副菜阻断工具事件主流程。
|
||||
if self.config.on_tool_event is not None:
|
||||
await self.config.on_tool_event(event_type, payload)
|
||||
with suppress(Exception):
|
||||
await self.extensions.publish(
|
||||
AgentEvent(event_type, payload), self.run_context
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _as_message(message: str | BaseMessage) -> BaseMessage:
|
||||
return HumanMessage(content=message) if isinstance(message, str) else message
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Agent",
|
||||
"AgentConfig",
|
||||
"AgentState",
|
||||
"AgentStatus",
|
||||
"PendingMessageQueue",
|
||||
"QueueMode",
|
||||
]
|
||||
Reference in new issue
Block a user