修改了nanobot,往Hermes agent的风格走,进度1/3

This commit is contained in:
2026-04-20 18:11:14 +08:00
parent cdfc222c9f
commit 36882a7d7b
261 changed files with 12659 additions and 604 deletions

View File

@ -0,0 +1,84 @@
"""结构化过程事件辅助工具。
这个模块的作用是把“运行中的中间状态”从底层执行逻辑安全地带到上层 UI
1. 用 `ContextVar` 记录当前异步上下文是否挂了事件 sink
2. 用单独的 run_id 上下文把父子流程串起来;
3. 让委派、MCP、A2A 等模块只管发事件,不需要知道 WebSocket/SSE 细节。
"""
from __future__ import annotations
import uuid
from contextlib import contextmanager
from contextvars import ContextVar
from datetime import datetime, timezone
from typing import Any, Awaitable, Callable
ProcessEvent = dict[str, Any]
ProcessEventSink = Callable[[ProcessEvent], Awaitable[None]]
# `_sink_var` 保存“当前异步上下文的事件接收器”。
# 这样可以避免把回调一层层显式往下传,同时又不会污染并发请求之间的上下文。
_sink_var: ContextVar[ProcessEventSink | None] = ContextVar("process_event_sink", default=None)
# `_run_id_var` 保存“当前流程的父 run_id”。
# 子流程发事件时可以把它挂到 `parent_run_id`,供前端拼接树状执行视图。
_run_id_var: ContextVar[str | None] = ContextVar("process_current_run_id", default=None)
def new_run_id(prefix: str = "run") -> str:
"""生成一个短且可读的运行 ID。"""
# 只截取 8 位十六进制是为了兼顾:
# 1. 日志 / WebSocket 里更短、更容易肉眼追踪;
# 2. 同一进程内短期冲突概率仍足够低。
return f"{prefix}-{uuid.uuid4().hex[:8]}"
def utc_now_iso() -> str:
"""返回带 `Z` 后缀的 UTC ISO8601 时间戳。"""
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
@contextmanager
def process_event_sink(sink: ProcessEventSink | None):
"""为当前异步上下文临时绑定一个事件 sink。"""
# `ContextVar.set()` 会返回 token退出时要 reset避免泄漏到后续请求。
token = _sink_var.set(sink)
try:
yield
finally:
_sink_var.reset(token)
@contextmanager
def process_run_context(run_id: str | None):
"""为当前异步上下文绑定一个逻辑父 run_id。"""
token = _run_id_var.set(run_id)
try:
yield
finally:
_run_id_var.reset(token)
def current_process_run_id() -> str | None:
"""读取当前上下文里绑定的 run_id。"""
return _run_id_var.get()
def has_process_event_sink() -> bool:
"""判断当前上下文是否具备过程事件接收能力。"""
return _sink_var.get() is not None
async def emit_process_event(event_type: str, **payload: Any) -> None:
"""在存在 sink 时发出一个结构化过程事件。"""
sink = _sink_var.get()
# 没有 sink 说明当前调用链不关心中间态,例如纯 CLI 单轮场景,直接静默跳过。
if sink is None:
return
# `created_at` 允许调用方覆盖;未传时统一补 UTC 时间,方便前端排序。
event: ProcessEvent = {
"type": event_type,
"created_at": payload.pop("created_at", utc_now_iso()),
**payload,
}
await sink(event)