mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 03:51:58 +00:00
587bf7e27c
起因是一次探索运行:Copilot 改节点时把工具绑定整份抹掉,agent 没有工具只能写「假设调用」, 校验节点的自动修复编出 total_count=0 让 schema 通过,协作成员把工具调用标记当文字输出了四轮, 轮数用完后节点把原话当成功结果交出去——运行标成已完成,数据库一次都没查。 平台可信度(先写失败测试) - Copilot 修改节点改为合并:没写的字段保留,null 才删除;协作成员按名字合并。前后端同一套规则, 工具集合变小时自查报警、改图回执列出变化 - 新校验规则:提示词点名了某个工具而节点没有绑定它,判错误,Copilot 自查会交回去修 - 校验修复只许改格式:冒出原文没有的值就判修复无效;修复这次模型调用计入用量 - 识别模型把工具调用写成文字的情况(DSML、<tool_call> 等标记):纠正一次,仍然如此就判失败并说明原因 - 协作团队用完轮数不再按成功交付:默认失败,可选降档,点出一次都没派到的成员 - 审批恢复后,同一节点里已经执行过的工具调用复用结果,不再重复执行 后端 - 查询时限交给数据库执行(Oracle call_timeout、PG statement_timeout、MySQL max_execution_time、 SQLite progress handler),可按数据源配置;超时报错与实际耗时一致 - 等审批的运行可以真正取消;会话列表带最后一轮状态;测连接结果持久化;探查结构可只看不存; 自定义工具可草稿试跑、写坏的参数定义保存时拒绝;MCP 服务改动后作废旧工具缓存; 检索命中带贡献分解;重建索引在后台跑并报进度;报错统一收进 core/errors 前端 - 公共层:管理页共用件、失败说明(lib/explain)、健康状态进公共库;目录按表各自加载并有超时, 一张表卡住不再拖垮整站;上传显示真实字节进度;离线横幅只播报不变的句子 - 画布:卡片只说游标所在的那一次执行;等审批时可放弃运行;团队收尾判定与未完成说清楚; 回放用量取那一刻的值 - 编排页:Copilot 合并语义与工具变化回执;「用完轮数时」选项;正式运行期间锁住所有改图入口 - 回答流 / 问数据:新失败类型说人话并给下一步;画布与右栏互相定位;数据源范围、回收站、重新核对 - 记录页:「航迹」页签复用画布时间轴,可拖动回放、列出关键时刻;列表行带执行 / 等人 / 失败缩略条 - 外壳:统一的离开前确认(data router),导航、⌘K、快捷键、后退都会先问 验证(沙箱):tsc 干净,pytest 838 passed(两遍),14 个检查脚本中 13 个全绿; check-ui 余 1 项旧断言(按旧日期格式找运行行),3c 更新。
72 lines
3.1 KiB
Python
72 lines
3.1 KiB
Python
"""节点重放时,已经发生过的事不在轨迹里再发生一次。
|
||
|
||
LangGraph 恢复一个停在 interrupt 上的节点,是把节点函数从头再跑一遍:前面几次
|
||
interrupt() 按顺序直接返回已经给过的答复,@task 包住的调用直接取 checkpoint 里的
|
||
结果。执行层面不会重复——agent 节点的模型调用、工具调用都在 task 里,副作用只发生
|
||
一次。可写在 task 外面的 ctx.emit 每重放一次就再发一遍:运行 fac480ff 的轨迹里,
|
||
同一个 call_id 的 python_exec 有两对 tool.start / tool.end,审批请求也问了两回,
|
||
签批人还按最后一次恢复的人记。看轨迹的人只能照字面理解成「执行了两次」。
|
||
「接着跑」一个失败的节点是同一回事。
|
||
|
||
所以要落进轨迹、又写在 task 外面的事,经这里发:它本身就是一次 task,第一次真正
|
||
走到这里时发出,重放时结果取自 checkpoint、不再发。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from langgraph.config import get_config
|
||
from langgraph.func import task
|
||
from langgraph.types import interrupt
|
||
|
||
from app.core.events import EventType
|
||
from app.engine.context import NodeContext
|
||
|
||
#: 重放协议的版本,记在每个 checkpoint 的 metadata 里。
|
||
#:
|
||
#: LangGraph 认 task 的缓存靠「函数名 + 节点里第几次调用」。这一版多了 once / ask 的
|
||
#: 记录 task,agent 的 tool_step 也换了位置和返回值:升级前停下的断点照新代码重放,
|
||
#: 节点里已经执行过的工具、模型调用对不上旧的位置,会再执行一次(副作用翻倍),缓存
|
||
#: 下来的旧返回值还可能直接对不上形状。恢复前先看版本,对不上又有这种风险就拒绝
|
||
#: (见 runner 的 _stale_replay)。以后再改节点里 task 的调用顺序,把它加一
|
||
PROTOCOL = 2
|
||
PROTOCOL_KEY = "agentlab_replay"
|
||
|
||
|
||
def protocol_of(metadata: Any) -> int:
|
||
"""checkpoint 是按哪一版重放协议写的。加这个标记之前的都算 1。"""
|
||
value = (metadata or {}).get(PROTOCOL_KEY) if isinstance(metadata, dict) else None
|
||
return int(value) if isinstance(value, int) else 1
|
||
|
||
|
||
def _in_graph() -> bool:
|
||
try:
|
||
get_config()
|
||
except RuntimeError: # 不在图执行上下文里(单独调用执行器的测试)
|
||
return False
|
||
return True
|
||
|
||
|
||
async def once(ctx: NodeContext, event_type: EventType | str, **data: Any) -> None:
|
||
"""发一条事件,节点重放时不再发。"""
|
||
if not _in_graph():
|
||
ctx.emit(event_type, **data)
|
||
return
|
||
|
||
@task
|
||
async def record(kind: str) -> bool:
|
||
ctx.emit(kind, **data)
|
||
return True
|
||
|
||
await record(str(event_type))
|
||
|
||
|
||
async def ask(ctx: NodeContext, payload: dict[str, Any], **request: Any) -> Any:
|
||
"""请人审批:human.requested 只在第一次走到这里时发,然后停下等答复。
|
||
|
||
返回答复。答复读完之后由调用方用 once() 发 human.resolved——那一条也只该在
|
||
答复真正到达的那次执行里出现。
|
||
"""
|
||
await once(ctx, EventType.HUMAN_REQUESTED, **request)
|
||
return interrupt(payload)
|