Skip to content

agent, tools: 支持 OpenAI Responses API、Agent HITL 多轮交互,修复 async generator span 泄漏 - #245

Open
pcerypeng wants to merge 1 commit into
trpc-group:mainfrom
pcerypeng:main
Open

agent, tools: 支持 OpenAI Responses API、Agent HITL 多轮交互,修复 async generator span 泄漏#245
pcerypeng wants to merge 1 commit into
trpc-group:mainfrom
pcerypeng:main

Conversation

@pcerypeng

Copy link
Copy Markdown

agent, tools: 支持 OpenAI Responses API、Agent HITL 多轮交互,修复 async generator span 泄漏

本次 PR 包含三项改进:

  1. OpenAI Responses API 适配 — 新增对 OpenAI Responses API 的流式和非流式
    支持,涵盖 reasoning、tool calls、logprobs 等特性。

  2. Agent 节点 HITL(人机多轮交互)机制 — 通过 interrupt bridge 桥接机制,
    允许 Agent 节点在运行中暂停等待人工输入,支持多轮审批或修正流程后继续执行。

  3. 修复 async generator 中 OpenTelemetry span 泄漏 — 将 start_as_current_span
    替换为手动 start_span + attach/detach + try/finally 模式,确保 async
    generator 被取消时 span 仍能正确结束。新增防御性 ValueError 捕获,处理
    跨 task 清理场景。

此外,本次 PR 还新增了框架级工具错误检测能力:

  • 引入 ToolArgumentErrorResponse 标记类,用于参数校验失败的场景。
  • 新增标准化工具错误码(tool_not_found、tool_argument_error、
    tool_execution_error),并提供 is_tool_execution_error() 公共辅助函数。

RELEASE NOTES: 新增 OpenAI Responses API 支持、Agent 节点 HITL 多轮交互能力,
修复 async generator 取消时 OpenTelemetry span 泄漏问题。

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.60972% with 68 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@81c798a). Learn more about missing BASE report.

Files with missing lines Patch % Lines
trpc_agent_sdk/models/_openai_model.py 93.24759% 21 Missing ⚠️
trpc_agent_sdk/server/ag_ui/_core/_agui_agent.py 61.11111% 21 Missing ⚠️
trpc_agent_sdk/dsl/graph/_graph_agent.py 68.96552% 9 Missing ⚠️
trpc_agent_sdk/agents/_base_agent.py 64.28571% 5 Missing ⚠️
trpc_agent_sdk/agents/core/_tools_processor.py 66.66667% 4 Missing ⚠️
trpc_agent_sdk/agents/_llm_agent.py 88.00000% 3 Missing ⚠️
trpc_agent_sdk/dsl/graph/_node_action/_agent.py 96.51163% 3 Missing ⚠️
...c_agent_sdk/server/ag_ui/_core/_session_manager.py 77.77778% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main        #245   +/-   ##
==========================================
  Coverage        ?   87.92373%           
==========================================
  Files           ?         483           
  Lines           ?       45577           
  Branches        ?           0           
==========================================
  Hits            ?       40073           
  Misses          ?        5504           
  Partials        ?           0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

确认 — 已删除且未迁移。现在我来检查 test_openai_responses_model.py 是否存在严重问题 — 特别是 _model 辅助函数默认使用 api_key="test_key",以及是否有测试实际发起了网络请求。测试全部 patch 了 _create_async_client,因此不会产生网络请求。很好。

现在我来检查一下 test_langfuse 的 detached span 修复中可能存在的问题 — span_token 变量仅在 if pattern == "detached" 分支内定义。如果 callback() 抛出异常,finally 会运行 detach(span_token)span_token 已定义。没问题。

现在我来验证 _agui_agent.py 中的一个真正问题:_is_graph_checkpoint_resume 检查 session.state 中的 _trpc_graph_checkpoint* 键以及 _trpc_graph_pending_interrupt is True。但检查点状态键可能并不总是以这种方式持久化。注释承认它依赖于 pending_interrupt 键。但有一个微妙之处:当 auto_persist=True 时,检查点键确实存在。当 auto_persist=False 时,则依赖于 pending_interrupt。这是为 graph resume 设计的。风险在于:对于提交带有 _trpc_graph_pending_interrupt=True 状态的 tool results 的非 graph agents,状态同步会被跳过。但这正是预期的行为。我觉得这没问题。

我来检查一个担忧:_is_graph_checkpoint_resume 使用传入的 session。但 session 来自 _ensure_session_exists,它返回持久化/缓存的 session。对于 SqlSessionService,它从数据库加载状态。很好。

现在我来检查 GraphAgent 上下文中的 LongRunningEvent,以及 _create_interrupt_events 是否正确映射了 HITL。当 AgentNodeAction 调用 interrupt(self._interrupt_payload(current_round)) 时,LangGraph 会捕获它,GraphAgent 的 _iter_interrupts 会检测到中断,然后 _create_interrupt_events 会调用 _build_interrupt_function,该方法检查 raw_args.get("_trpc_agent_node_hitl") is True。很好 — 有效载荷包含该标志。

我还要验证恢复时的 _build_interrupt_function:当 GraphAgent 恢复时,Command(resume={interrupt_id: function_response.response})function_response.response 是用户的答案。这成为 LangGraph 中的 interrupt() 返回值 → human_response。然后 _resume_content 使用该响应构建内容。很好。

现在我来看一下 test_agent_node_multiround_hitl_resume_with_correct_order 测试 — 它有一个条件性的 if stale_pending is not None: ... else:。这有点脆弱,但反映了一个已记录的不确定性。不是一个阻塞性问题。

我来检查最后一个潜在的实际问题:在 _pending_round 中,child_statedict(child_ctx.session.state)... 等等,不对,是 dict(child_state),其中 child_statefinal_state。我来重读一下。

current_round = self._pending_round(event, child_ctx, final_state),且 _pending_round 执行 "child_state": dict(child_state),其中 child_statefinal_state。所以它捕获了到目前为止累积的最终状态。恢复时,saved_child_state = current_round.get("child_state"); child_state = dict(saved_child_state)。这会在恢复时恢复子图状态。很好。

但有一个细微的问题:completed_rounds 排除了 child_state 键:{key: value for key, value in previous_current.items() if key != "child_state"}。因此,完成的轮次不存储 child_state(为了减小持久化负载的大小)。当重放完成的轮次时,interrupt(self._interrupt_payload(completed)) 只是重新触发中断以推进 LangGraph 状态 — interrupt 的返回值被忽略了。很好。

我现在已经完全理解了。我来写审查报告。我也要再次检查一下 pyproject.toml 中 openai 的版本提升不会破坏 Chat Completions 的任何功能 — 1.66 版本仍然支持 Chat Completions。没有问题。

我来验证一下是否存在导入问题:_node_action/_agent.py 在模块顶部导入了 from langgraph.errors import GraphInterrupt。我来确认一下这是否可用,以及 except GraphInterrupt: raise 是否能正确传播。LangGraph 在内部使用 GraphInterrupt 进行暂停;重新引发它允许图的执行循环捕获它。这是正确的模式。但是 — 在 execute() 中捕获并重新引发 GraphInterrupt 意味着父图的 astream 会检测到中断。很好。

现在,interrupt(...)async for 循环内部被调用。如果 interrupt() 引发了 GraphInterruptawait agent_stream.aclose() 就不会执行(异常会先传播)。我来检查一下顺序:

await agent_stream.aclose()
interrupt(self._interrupt_payload(current_round))
raise RuntimeError(...)

aclose()interrupt() 之前。所以 agent_stream 首先被关闭,然后 interrupt() 引发 GraphInterruptGraphInterrupt 会传播到 try/except GraphInterrupt: raise 之外。很好 — 子生成器已正确关闭。RuntimeError 永远不会执行(interrupt 总是会引发 GraphInterrupt)。这是作为后备保护措施使用的。没问题。

我来做最后总结。我还要仔细检查一个真正值得关注的问题:_agui_agent.py 第 1118 行的变更现在跳过了 graph-resume 轮次的 update_session_state。但注释说“非 graph agents 有意提交带有 tool result 的状态补丁”。对于 graph agents,状态同步被跳过。这意味着对于 graph HITL 恢复,来自浏览器 input.state 的任何合法状态更新都会被丢弃。这是有意为之的(检查点是权威的)。但值得注意其局限性。这不是一个 bug,而是设计使然。

现在我来写审查报告。

发现的问题

🚨 Critical

无。

⚠️ Warning

  • tests/tools/test_long_running_tool.py:1-16:删除了 LongRunningFunctionTool 的全部既有测试覆盖

    • 该文件原有 TestLongRunningFunctionToolInitTestLongRunningFunctionToolGetDeclaration 等用例(覆盖 is_long_running/name/filters 校验/声明描述拼接),被整文件替换为仅 3 个 is_tool_execution_error 单测,且未迁移到其他文件(已 grep 确认)。这使 LongRunningFunctionTool 的初始化与 _get_declaration 行为(含“不要重复调用”提示注入)回归到无测试保护状态。建议保留原有 init/declaration 用例,再叠加新增的错误分类测试。
  • trpc_agent_sdk/agents/_llm_agent.py:561-566:长时工具与 parallel_tool_calls=True 混用时仅打印 warning 后照常执行

    • 当同一批 tool_calls 中同时存在长时工具和普通工具且开启并行调用时,代码只记录警告,仍进入正常执行/挂起流程。由于长时工具会 return 终止本轮 agent 执行,同批次其他工具的 function_response 虽被写入 session 但不会被 LLM 总结,下一轮恢复后模型上下文可能与实际工具状态不一致。建议要么在此组合下强制 parallel_tool_calls=False,要么在文档/类型层面禁止该组合并显式报错,而非静默继续。

💡 Suggestion

  • trpc_agent_sdk/models/_openai_model.py:1386-1397_generate_responses_streamresponse.function_call_arguments.delta 分支):当 item_id 未出现过时直接 function_order.append(item_id) 并新建条目,但随后 name 可能为空字符串;只有 name in streaming_tool_names 才会流式吐出 delta。若上游先发 delta 再发 output_item.added,初始 delta 会因 name 为空被丢弃(与 fallback 路径行为一致,但可读性上易误判)。可在注释中说明该顺序依赖,便于后续维护。

总结

整体风险较低,无 Critical 阻塞问题。核心新增逻辑(AgentNode HITL 桥接、OpenAI Responses API 适配、长时工具错误码归类、AG-UI graph 检查点恢复保护)正确性与异常路径处理总体得当;主要需关注的是既有 LongRunningFunctionTool 测试覆盖被删除导致的回归保护缺失,以及长时工具与并行工具调用混用时的静默行为。

测试建议

  • 补回 LongRunningFunctionTool 的 init / _get_declaration 用例(含无 docstring 与 filters_name 校验失败两条路径),避免既有能力回归无保护。
  • 建议补充一条用例:同一批 tool_calls 同时包含 LongRunningFunctionTool 与普通工具、且 parallel_tool_calls=True 时,验证 agent 行为(是否如 warning 所述、恢复后模型上下文是否一致),明确该组合的预期契约。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

现在让我检查一下 _is_graph_checkpoint_resume 的关键行为——当工具结果恢复图形时,_ensure_session_exists 返回的会话状态是否仍然包含 True_trpc_graph_pending_interrupt?它会在恢复运行之前被清除吗?恢复操作是在另一个流程中通过 _handle_tool_result_submission 处理的。让我验证一下顺序:run_async_ensure_session_exists_is_graph_checkpoint_resume 跳过状态更新。会话状态中仍然保留着来自上次中断的 pending_interrupt=True。所以检测是有效的,并且跳过状态更新可以防止浏览器状态覆盖检查点(checkpoint)。很好——这是该 PR 的核心修复。

我已经掌握了所需的所有上下文。让我来撰写审查意见。

发现的问题

🚨 Critical

  • trpc_agent_sdk/server/ag_ui/_core/_agui_agent.py:1127_is_graph_checkpoint_resume 在 tool-result 提交时跳过了 update_session_state,但检测信号依赖 session.state 中残留的 _trpc_graph_checkpoint* / _trpc_graph_pending_interrupt 标记。当使用 InMemorySessionService 且前端在 tool-result 提交时显式带上了 input.state(前端覆盖式同步),由于 update_session_state 被跳过是正确的;但若 session 在该次请求前因超时/清理被重建(_ensure_session_exists 会用 initial_state=input.state 重新建会话),旧的 checkpoint 标记会丢失,_is_graph_checkpoint_resume 返回 False,导致前端 state 覆盖 LangGraph checkpoint,Command(resume=...) 从 START 重新执行。
    • 影响:HITL resume 在 session 被清理重建的场景下静默失败,checkpoint 丢失。
    • 修复方向:resume 路径不应依赖 session 缓存中的内部标记判定;建议改为依据 input 中是否为 tool-result 提交 + GraphAgent 类型/上一轮产出 LongRunningEvent 来判定,或在 _ensure_session_exists 重建会话时显式保留 checkpoint 类 state keys。

⚠️ Warning

  • pyproject.toml:113openai>=1.3.0 提升到 openai>=1.66.0 是对下游使用者的破坏性依赖变更,任何 pin 在旧版 openai 的环境安装新版本 SDK 会直接失败。Responses API 路径需要该版本,但默认 use_responses_api=False 时旧版本本可工作。

    • 修复方向:确认这是预期 breaking change 并在 CHANGELOG/release notes 显式标注;或考虑将 Responses 支持做成可选 extra,仅在启用时要求高版本。
  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:211STATE_KEY_PENDING_AGENT_NODE_HITLcurrent 轮里保存了完整的 child_state_pending_round"child_state": dict(child_state)),而该 key 已加入 UNSAFE_STATE_KEYS(test 也验证了),不会被输出到 completion state_delta。但它会随 interrupt bridge event 的 state_delta 持久化到 session.state(测试 test_agent_node_hitl_survives_service_restart 依赖此行为)。child_state 可能包含较大或敏感的子代理中间状态,长期累积于 session 且多轮 completed 列表只剔除了 child_state、保留其余字段,存在状态膨胀风险。

    • 修复方向:评估是否需要在完成轮次后对 completed 列表裁剪,或对 child_state 体积设限。
  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:216-218await agent_stream.aclose() 后立即 interrupt(...)raise RuntimeError(...)interrupt() 会抛 GraphInterrupt,因此 raise RuntimeError 实为不可达防御代码;但若 interrupt 因实现变更未抛异常,RuntimeError 会被外层 except Exception 捕获并清空 STATE_KEY_PENDING_AGENT_NODE_HITL(305-306 行),反而清掉刚写入的 pending 状态。

    • 修复方向:将 interrupt(...) 后的兜底改为更明确的断言/日志,或在 except Exception 中避免在 GraphInterrupt 已设置 pending 后清空状态。
  • trpc_agent_sdk/models/_openai_model.py:4197async for event in response 迭代 Responses 流,但 response 对象未在 finally 之外显式关闭。当客户端在迭代中途取消(aclose/CancelledError)时,底层流可能未被显式 close,仅依赖 _http_client_provider.close_http_client(client) 关闭 httpx client;若 provider 的 close 不保证取消底层 SSE 流,可能残留连接。

    • 修复方向:在 finally 中对 response 调用 aclose(若为 async stream)或确保 provider close 涵盖流取消。

💡 Suggestion

  • trpc_agent_sdk/dsl/graph/_events/_builder.py:3350node_description.strip() if node_description and node_description.strip() else node_id 调用了两次 .strip(),可简化为 display_name = (node_description.strip() or node_id) if node_description else node_id,行为等价且更清晰。

总结

整体实现质量较高,HITL 多轮恢复、Responses API 支持、span 上下文修复等核心逻辑均有对应测试覆盖。存在一个 Critical 风险:graph resume 检测依赖 session 缓存中的内部标记,在 session 被重建时会失效导致 checkpoint 被前端 state 覆盖;其余为依赖版本破坏性变更与若干边界/资源清理建议。建议优先修复 Critical 项的检测逻辑。

测试建议

  • 补充 session 在 tool-result 提交前被清理重建(_ensure_session_exists 走新建分支)后仍能正确保留 LangGraph checkpoint 并 resume 的集成测试。
  • 补充 Responses 流式过程中客户端中途取消(aclose)时底层连接/流被正确释放的测试。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

确认。现在我来写审查意见。

发现的问题

🚨 Critical

  • trpc_agent_sdk/models/_openai_model.py:2023-2026:Responses 流式生成器 finally 块引用未初始化的 response,会掩盖原始异常并泄漏 HTTP 客户端
    • response 只在 try 内部(_openai_model.py:1885)赋值。若 client.responses.create(...)_prepare_responses_api_params(...) 抛错,finallyif response is not None 会触发 UnboundLocalError,导致真正的 API 错误被覆盖,且后续 close_http_client(client) 永不执行(httpx 客户端泄漏)。流式路径每次请求都会经过这里,属于高频错误路径。
    • 修复:在 try 之前加 response: Any = None,并将 close_http_client 放到独立的 try/except 或先判断后关闭。
          response: Any = None
          try:
              response = await client.responses.create(...)
              ...
          finally:
              if response is not None and hasattr(response, "aclose"):
                  await response.aclose()
              await self._http_client_provider.close_http_client(client)

⚠️ Warning

  • trpc_agent_sdk/server/ag_ui/_core/_agui_agent.py:1345-1353_is_graph_checkpoint_resume 的 LongRunningEvent 回退判定过宽,会误伤非 Graph Agent 的 HITL 恢复

    • 回退逻辑只要在 session 事件历史中看到任意 LongRunningEvent 即返回 True。但 LongRunningEvent 在产生后会被 append_event 持久化到事件历史,且 graph 正常完成后也不会清除。因此任何曾发生过 LongRunningEvent 的会话(包括普通 LlmAgent + LongRunningFunctionTool 的 HITL 场景,以及 graph 已完成的后续轮次),在后续所有 tool-result 提交时都会跳过 update_session_state,导致前端 state patch 被静默丢弃。
    • 修复方向:限定回退条件——仅当存在「未恢复」的 LongRunningEvent(例如结合 _trpc_graph_pending_interrupt/pending_tool_calls,或检查该 LongRunningEvent 之后无对应的 final/completion 事件)才跳过;不要仅凭历史中存在过 LongRunningEvent 就跳过同步。
  • trpc_agent_sdk/models/_openai_model.py:1284-1300:每次请求都执行 inspect.signature(client.responses.create),存在不必要的反射开销

    • _prepare_responses_api_params 在流式与非流式 Responses 路径的每次请求都会被调用,内部对 SDK 方法做签名反射;对于高频请求这是可避免的 per-request 成本。
    • 修复方向:在客户端创建时(或按 client 实例)缓存签名探测结果,或在构造时探测一次 openai 版本能力。
  • pyproject.toml:113:将 openai 下限从 1.3.0 提升到 1.66.0 是破坏性依赖变更

    • Responses API 为 opt-in(use_responses_api 默认 False),但依赖下限被强制抬高,会使所有现有使用旧版 openai 的部署在升级本 SDK 时被迫升级 openai。若 Responses 路径并非默认启用,建议将 >=1.66.0 仅作为使用 Responses 时的运行时校验/可选 extra,保持基础下限不变;或在变更日志中明确标注该破坏性升级。

💡 Suggestion

  • trpc_agent_sdk/agents/_base_agent.py:3028-3048span.end()detach 放在同一 finally 中,若 set_span_in_context/attach 在赋值 span_token 之前抛错,已创建的 span 不会被 end()。可把 span = tracer.start_span(...) 与后续 attach 分开处理,或将 span.end() 放入独立 try 以保证 span 始终关闭。

总结

存在 1 个必须修复的问题:Responses 流式路径 finally 块引用未初始化的 response,会在 API 报错时掩盖异常并泄漏 httpx 客户端。另有 3 项 Warning(LongRunningEvent 回退判定过宽可能误禁用非 graph 场景的前端状态同步、每次请求的签名反射开销、openai 依赖下限破坏性升级)建议处理。

测试建议

  • 补充 _generate_responses_streamclient.responses.create 直接抛错时 finally 仍能正常关闭客户端且不抛 UnboundLocalError 的用例。
  • 补充非 Graph 的 LlmAgent + LongRunningFunctionTool 场景:在产生 LongRunningEvent 后提交 tool-result 且携带 state patch,断言 update_session_state 仍被调用(验证 _is_graph_checkpoint_resume 回退不误伤)。

"type": "summary_text",
"text": accumulated_reasoning
}],
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Responses 流式 finally 引用未初始化的 response

response 仅在 try 内部赋值,若 client.responses.create_prepare_responses_api_params 抛错,finally 中 if response is not None 会触发 UnboundLocalError,掩盖原始 API 异常并导致 close_http_client 永不执行(httpx 客户端泄漏)。建议在 try 前初始化 response: Any = None,并将 close_http_client 放入独立 try/except。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经充分理解了,可以开始编写审查意见了。多轮恢复中断(interrupt)重放是一个复杂点,但 HITL 测试(test_agent_node_multiround_hitl_resume_with_correct_order 和重启测试)断言其可以正常工作,因此恢复值(resume-value)的匹配显然有效(LangGraph 将 Command(resume={interrupt_id: value}) 映射到待处理的中断索引;已完成的轮次会重放它们的中断,这些中断会返回 之前 存储的恢复值)。等等——实际上,已完成的中断在重放时会再次调用 interrupt(payload),这会引发中断,除非提供了恢复值。恢复命令只为最新的 interrupt_id 提供了一个值。让我仔细检查一下,以确认这确实是一个 Bug,还是说 LangGraph 会重放之前的值。

实际上,在 LangGraph 中,当使用 Command(resume={...}) 恢复时,图会重放节点;重放期间对 interrupt() 的调用会返回 原始 的恢复值(如果有的话),或者如果该中断没有提交恢复值,则会再次引发中断。由于 Command(resume={interrupt_id: value}) 只为一个 interrupt_id 提供了值,因此重放之前 已完成 轮次的中断(它们有不同的 interrupt_id)在重放时将没有恢复值 → 它们会再次引发中断,从而导致流程停滞。这很令人担忧,但重启测试断言第二轮恢复工作正常。测试显示了 pending2["completed"] 的长度为 1 且当前轮次为 child-question-2,然后最终完成。因此,在实践中,已完成的轮次重放不会导致停滞——这意味着 LangGraph 确实从检查点(checkpoint)重放了它们之前的恢复值。该设计依赖于检查点状态。我会将其记录为一个微小的潜在风险,而不是一个已确认的 Bug,因为测试涵盖了它。让我来编写审查意见。

发现的问题

🚨 Critical

  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:137-143:HITL 恢复时重放 completed 轮次的 interrupt 可能丢回复合值
    • 恢复路径先对 completed 中每个历史轮次调用 interrupt(self._interrupt_payload(completed)),再对 current 调用一次。但 _graph_agent._extract_resume_command 只构造 Command(resume={interrupt_id: function_response.response})_graph_agent.py:300),其中 interrupt_id 仅来自最新 pending interrupt id。completed 轮次的 interrupt 在重放时没有对应的 resume 值,依赖 LangGraph 从 checkpoint 自动重放其历史 resume 值;若 checkpoint 未持久化(如 auto_persist=False 且首次中断时未写入 checkpoint key,见 _agui_agent.py 注释),重放会再次抛出 interrupt,导致多轮 HITL 恢复卡住。建议显式为 completed 轮次在 Command(resume=...) 中提供其历史响应,或在重放时跳过 interrupt() 而直接复用已存响应。

⚠️ Warning

  • trpc_agent_sdk/server/ag_ui/_core/_agui_agent.py:1339-1364:graph checkpoint resume 的 fallback 依据 session.events 未解析 LongRunningEvent 判定,对普通 LlmAgent HITL 也会跳过 state 同步

    • _is_graph_checkpoint_resume 的 fallback 分支遍历 session.events 查找“未被 function_response 解析”的 LongRunningEvent,命中即跳过 update_session_state。但该信号对非 Graph 的 LlmAgent HITL 同样成立(LlmAgent 也发 LongRunningEvent),会让普通 LlmAgent 在 HITL 续轮时不再同步前端 state,破坏文档所述“backend 始终用 frontend state 更新”的既有行为。建议该 fallback 仅在确实存在 graph 状态标记(或 agent 确为 GraphAgent)时启用,避免误伤普通 HITL。
  • trpc_agent_sdk/server/ag_ui/_core/_agui_agent.py:1118-1131:检测所用的 session 来自内存缓存,state/events 可能落后

    • session = await self._ensure_session_exists(...) 返回的是 get_or_create_session 的对象,其 state/events 取自 session service 当时快照。在异步并发或刚写入 interrupt 标记尚未落库时,_is_graph_checkpoint_resume 可能读不到 _trpc_graph_* 标记而误判为非 resume,进而错误地 update_session_state 覆盖 checkpoint。建议检测前显式重读 session state,或对 graph agent 的 tool result 提交一律保守跳过 state 同步。
  • trpc_agent_sdk/models/_openai_model.py:1275-1280store=False 时合并 include 未校验类型

    • include = list(responses_params.get("include") or []) 在用户通过 responses_api_params 传入 include 为字符串时,会被拆成字符列表(如 "reasoning.encrypted_content" → 逐字符),再追加 reasoning.encrypted_content,生成无效 include 值导致请求 400。建议显式断言/规范化 include 为列表,非列表时按整体项处理。
  • trpc_agent_sdk/agents/_llm_agent.py:561-567:long-running + parallel_tool_calls 在工具已执行后才抛错

    • 该检查在收集 long_running_tool_ids 之后立即 raise,但此时 tools 尚未执行,属可接受;不过错误以 RuntimeError 抛出会冒泡为未捕获异常而非走 agent 错误事件路径,调用方难以区分。建议改为产出结构化 error event(或在前置校验阶段更早拒绝),保持与其他配置错误的错误处理一致。

💡 Suggestion

  • trpc_agent_sdk/dsl/graph/_events/_builder.py:188,231,271:三处 display_name 计算逻辑完全重复,可抽取为 _display_name(node_description, node_id) 辅助方法以避免后续维护漂移。

总结

整体新增 Responses API 适配与 Agent 节点 HITL 提升逻辑改动较大,关键风险集中在多轮 HITL 恢复时 interrupt resume 值的匹配(Critical)以及 AG-UI graph resume 检测对普通 LlmAgent HITL 的误伤与并发读状态竞态(Warning)。建议合并前优先验证多轮 HITL 在 auto_persist=False、无 checkpoint key 场景下的恢复正确性。

测试建议

  • 补充多轮 HITL 恢复在 auto_persist=False(session.state 无 _trpc_graph_checkpoint*、仅 pending interrupt 标记)场景下的端到端用例,断言 completed 轮次重放后能正确推进到下一轮而非卡死。
  • 补充非 Graph 的 LlmAgent HITL 续轮请求经过 _is_graph_checkpoint_resume 后仍触发 update_session_state 的回归用例,避免 state 同步被误跳过。

resume_content: Optional[Content] = None
if pending_hitl is not None:
completed_rounds = pending_hitl.get("completed", [])
for completed in completed_rounds if isinstance(completed_rounds, list) else []:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HITL 恢复重放 completed 轮次 interrupt 可能丢失复合 resume 值

恢复路径对 completed 中每个历史轮次调用 interrupt() 再对 current 调用一次,但 Command(resume=...) 仅含最新 pending interrupt id 的响应。completed 轮次依赖 LangGraph 从 checkpoint 重放历史 resume 值;若未持久化 checkpoint(auto_persist=False),重放会再次抛出 interrupt 导致多轮 HITL 卡死。建议显式为 completed 轮次提供历史响应或重放时跳过 interrupt()。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

现在让我来撰写评审。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/dsl/graph/_graph_agent.py:388:HITL 响应字典 key 由 "desicion" 改为 "decision",是破坏性变更

    • 该 key 写入 FunctionResponse.response,是对外暴露给工具/客户端的中断契约。已发布版本中下游若按旧拼写 "desicion" 读取中断结果,升级后会取不到值。建议在发布说明中明确标注,或保留旧 key 兼容一段时间(同时写入两个 key)。
  • pyproject.toml:28openai 最低版本由 >=1.3.0 提升到 >=1.66.0,是破坏性依赖收紧

    • 这是为支持 Responses API 所需,但跨度很大,会与锁定了旧版 openai 的下游环境产生依赖冲突。Responses API 在更早版本已可用,且 logprobs 兼容性已通过 inspect.signature 动态探测处理,建议确认 1.66 是否为真实下限,或补充说明升级要求。
  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:219-222:子会话事件去重改变了事件累积语义

    • 新逻辑用 event.id 去重后只追加不重复事件,原先会无条件追加。对于合法产生相同 id 的非重复事件(或 id 为空但语义不同的事件),现在可能被错误丢弃。当前对 not event.id 的事件仍会追加,但同一 id 的二次事件被静默丢弃,建议确认这是恢复重放场景的预期行为而非通用语义变更。

💡 Suggestion

  • trpc_agent_sdk/agents/_llm_agent.py:594is_tool_execution_error 分支仅由 _long_running_tool 的单测覆盖,缺少通过 LlmAgent 实际执行路径验证“长运行工具执行报错时不挂起图、错误事件正常 yield 给模型”的集成测试。建议补一个长运行工具抛异常的端到端用例,锁定该行为不被回归。

总结

整体改动质量较高,Responses API 适配、Agent 节点 HITL 桥接、span 泄漏修复等核心逻辑均有相应测试覆盖,未见明确安全或核心功能阻塞问题。主要风险集中在两处破坏性兼容性变更("desicion""decision"openai>=1.66.0),建议在合并前确认或同步发布说明。

测试建议

  • 补充长运行工具在 LlmAgent 执行路径中抛异常的端到端测试,验证不会被误判为需挂起的 LongRunningEvent。
  • 补充一个下游按 FunctionResponse.response["desicion"] 读取旧中断结果的兼容性回归用例,确认 key 变更的影响范围。

@pcerypeng

Copy link
Copy Markdown
Author

AI Code Review

现在让我来撰写评审。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/dsl/graph/_graph_agent.py:388:HITL 响应字典 key 由 "desicion" 改为 "decision",是破坏性变更

    • 该 key 写入 FunctionResponse.response,是对外暴露给工具/客户端的中断契约。已发布版本中下游若按旧拼写 "desicion" 读取中断结果,升级后会取不到值。建议在发布说明中明确标注,或保留旧 key 兼容一段时间(同时写入两个 key)。
  • pyproject.toml:28openai 最低版本由 >=1.3.0 提升到 >=1.66.0,是破坏性依赖收紧

    • 这是为支持 Responses API 所需,但跨度很大,会与锁定了旧版 openai 的下游环境产生依赖冲突。Responses API 在更早版本已可用,且 logprobs 兼容性已通过 inspect.signature 动态探测处理,建议确认 1.66 是否为真实下限,或补充说明升级要求。
  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:219-222:子会话事件去重改变了事件累积语义

    • 新逻辑用 event.id 去重后只追加不重复事件,原先会无条件追加。对于合法产生相同 id 的非重复事件(或 id 为空但语义不同的事件),现在可能被错误丢弃。当前对 not event.id 的事件仍会追加,但同一 id 的二次事件被静默丢弃,建议确认这是恢复重放场景的预期行为而非通用语义变更。

💡 Suggestion

  • trpc_agent_sdk/agents/_llm_agent.py:594is_tool_execution_error 分支仅由 _long_running_tool 的单测覆盖,缺少通过 LlmAgent 实际执行路径验证“长运行工具执行报错时不挂起图、错误事件正常 yield 给模型”的集成测试。建议补一个长运行工具抛异常的端到端用例,锁定该行为不被回归。

总结

整体改动质量较高,Responses API 适配、Agent 节点 HITL 桥接、span 泄漏修复等核心逻辑均有相应测试覆盖,未见明确安全或核心功能阻塞问题。主要风险集中在两处破坏性兼容性变更("desicion""decision"openai>=1.66.0),建议在合并前确认或同步发布说明。

测试建议

  • 补充长运行工具在 LlmAgent 执行路径中抛异常的端到端测试,验证不会被误判为需挂起的 LongRunningEvent。
  • 补充一个下游按 FunctionResponse.response["desicion"] 读取旧中断结果的兼容性回归用例,确认 key 变更的影响范围。

1、这是拼写错误修正("desicion" 是 "decision" 的笔误)。该 key 仅在 interrupt.value 非 dict 时触发,实际使用场景极窄。
2、Responses API 需要 client.responses.create,该接口在 1.66.0 才稳定可用。虽然 use_responses_api 默认 False(opt-in),但统一提升下限可避免运行时版本检测的维护负担。
3、不是真实问题。event.id 由 uuid.uuid4() 自动生成(Event.model_post_init),碰撞概率极低,不存在"合法产生相同 id 的非重复事件"的情况。去重逻辑在当前 UUID 机制下不会错误丢弃任何事件。event.id 为空时(not event.id 为 True)直接 append,也不会丢弃无 id 事件。该去重是 HITL 重放场景的防御性保护。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

确认:received_call_ids 是类级别的可变默认值,由 4 个测试实例共享。重启/多轮测试断言 resumed_child.received_call_ids == ["child-question-1", "child-question-2"] 是脆弱的——如果 test_agent_node_long_running_interrupts_parent_and_resumes_multiple_rounds 先运行,该列表会累积,导致重启测试失败;或者如果重启测试先运行,它会虚假地通过。这是一个测试有效性 Bug。

现在我已经有足够的内容来撰写审查报告了。让我来汇总一下。

发现的问题

🚨 Critical

  • tests/trpc_agent_dsl/graph/test_agent_node_hitl.py:2598(及 :2646):received_call_ids 为类级可变默认值
    • TwoRoundClarifyingAgentTeamHitlLeader 均声明 received_call_ids: list[str] = [] 作为类属性,被 4 个测试用例的多个实例共享。跨用例累积后,断言 resumed_child.received_call_ids == ["child-question-1", "child-question-2"] 会在不同测试执行顺序下假性通过或失败,无法真正验证“重启后仍按序收到两轮响应”。建议改为实例属性(在 __init__model_post_init 中初始化 self.received_call_ids = []),或用 field(default_factory=list)

⚠️ Warning

  • trpc_agent_sdk/dsl/graph/_graph_agent.py:388:修复 desicion→decision 拼写属于破坏性变更

    • {"decision": interrupt.value} 改变了对外暴露的 FunctionResponse.response 键名。任何已读取 ["desicion"] 的客户端/前端在升级后会取不到值。仓库内已无其他引用,但该字段是面向 AG-UI 客户端的契约。建议确认无下游依赖,或保留旧键做兼容(同时写 desiciondecision)一段时间。
  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:424-427_interrupt_payload**response 覆盖 **arguments

    • 注释声称“保留模型提供的 call arguments”,但实现以 response 在后覆盖 arguments。当工具返回的 response 含与入参同名的键(如 statusround)时,入参被静默改写,前端拿到的 arguments 不再是模型真实调用参数。建议改为 **response, **arguments 或显式合并策略以匹配注释意图,并补一条参数冲突场景的测试。
  • trpc_agent_sdk/server/ag_ui/_core/_agui_agent.py:1398-1416:fallback 检测为 O(n²) 且 getattr 链脆弱

    • events 上对每个 LongRunningEvent 都用嵌套 any(... for later in events[i+1:]) 扫描后续事件,长会话(事件多)下开销显著。同时多层 getattr(getattr(...))content/partsNone 时靠 and 短路,可读性差且易漏判。建议先构建 resolved_ids 集合再做 O(n) 判定,并显式处理 None
  • pyproject.toml:28openai>=1.3.0>=1.66.0 为破坏性依赖变更

    • 该版本跨度大,下游若 pin 旧版 openai 会在升级 trpc_agent_sdk 时产生依赖冲突。Responses API 确需新版,但建议在 changelog/升级文档中显式标注,避免静默阻塞下游安装。
  • trpc_agent_sdk/models/_openai_model.py:1170:缺失 call_id 时回退为字面量 "unknown"

    • 多个 tool 输出若都缺 tool_call_id 会共用同一 call_id="unknown",Responses API 侧可能匹配错乱且无告警。建议缺失时生成唯一 id 或至少 logger.warning,与同文件对 tool_call id 缺失的处理保持一致。

💡 Suggestion

  • trpc_agent_sdk/models/_openai_model.py:1740_prepare_responses_api_params 每次请求都 inspect.signature(client.responses.create)

    • 客户端类型在实例生命周期内不变,可在构造或首次调用时缓存签名结果,减少每次请求的反射开销。
  • trpc_agent_sdk/agents/_llm_agent.py:561getattr(self, "parallel_tool_calls", False) 冗余

    • parallel_tool_calls 已是 LlmAgent 的 Pydantic 字段(默认 False,见 :99),直接 self.parallel_tool_calls 即可,getattr 默认值在此无意义,易让人误以为属性可能不存在。

总结

整体改动方向清晰,AgentNode 多轮 HITL、Responses API 传输与会话保活逻辑较完整且配有测试。存在 1 个 Critical(测试类级可变状态导致断言不可靠)、若干 Warning(对外字段重命名、参数覆盖语义与注释不符、fallback 性能与依赖升级),建议合并前至少修复 Critical 与 _interrupt_payload 合并顺序问题。

测试建议

  • 补一条 _interrupt_payload 在工具 response 含与入参同名键(如 round)时的断言,验证 arguments 是否被错误覆盖。
  • 补 Responses API 流式场景:response.function_call_arguments.delta 早于 response.output_item.added 到达时,验证最终 response.completed 仍能还原完整 function_call(当前注释已承认该顺序依赖未防御)。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经彻底理解了。现在我来整理一下审查意见。

发现的问题

🚨 Critical

  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:208-227:子 agent 抛出 LongRunningEvent 后写入 parent_ctx.state[STATE_KEY_PENDING_AGENT_NODE_HITL]await agent_stream.aclose(),随后调用 interrupt(...)。但 interrupt() 会抛出 GraphInterrupt 向上穿透该节点函数,而写入 pending HITL 状态使用的是 parent_ctx.state(delta 感知 State,写入 _delta),该 delta 只有在节点函数正常返回节点输出字典后才会被 LangGraph 提交到 graph state。GraphInterrupt 不会让节点返回,因此 _delta 中的 pending HITL 标记不会被 LangGraph 落盘;它只在 TRPC 层 event_actions.state_delta 经由中断桥事件写出时才可能持久化。结果是 auto_persist=False 且桥事件未携带该 key 时,重启后 _get_pending_hitl 读不到标记,多轮 HITL 重放失败、Command(resume=...) 从 START 重跑。建议显式将该 marker 通过中断桥事件的 state_delta(与 _trpc_graph_pending_interrupt 同一路径)持久化,或在 _extract_resume_command 之外保证其落盘,而非依赖 delta 提交时序。

⚠️ Warning

  • trpc_agent_sdk/models/_openai_model.py:1244-1278:Responses API 路径只映射了 temperature/top_p/tool_choice/parallel_tool_calls/prompt_cache_*/logprobs/max_output_tokens/tools/response_format,而 Chat Completions 路径设置的 stopn(candidate_count)、frequency_penaltypresence_penaltyseed 全部被静默丢弃。用户在 Responses 模式下配置这些参数会被忽略而无任何告警,行为与 Chat Completions 不一致。建议至少对未映射参数输出 warning,或将可映射项(如 frequency_penalty/presence_penalty/seed 在 Responses 中同样支持)纳入 parameter_map

  • trpc_agent_sdk/server/ag_ui/_core/_agui_agent.py:1379-1417_is_graph_checkpoint_resume 的 fallback 分支用 getattr(session,"events") 检查未解决的 LongRunningEvent,但传入的 session 可能是 _SessionStateView(其 events 委托给缓存快照 session),而快照 events 同样可能落后于真实持久化的事件;同时 _ensure_session_exists 返回的 session 不保证 .events 已填充最新内容。该 fallback 在事件滞后时会漏判(误同步状态覆盖 checkpoint)或误判(把已解决的 LongRunningEvent 当作未解决)。建议以 get_session_state 同等可信的途径获取 events,或明确该 fallback 的不可靠性并补充测试覆盖事件滞后期。

  • trpc_agent_sdk/agents/_llm_agent.py:561-572parallel_tool_calls=True 且存在 long-running 工具时直接 yield 错误事件并 return,但此时尚未执行任何工具,错误事件没有 content/function_response。对于已发起的请求这会让本轮以纯错误结束(无 tool response 回填),下游模型历史可能处于 pending tool_calls 无对应 response 的状态——正是该 guard 想避免的不一致。此外该路径无测试覆盖(test_llm_agent_ext.py 仅测了正常 long-running 与 error 两条路径)。建议至少补充 parallel+long-running 的测试,并确认错误事件是否需要回填 dummy tool response。

  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:143:resume 时对每个 completed 轮次调用 interrupt(self._interrupt_payload(completed)) 以重放历史中断,但这些已完成轮次的 interrupt() 返回值被丢弃(未用作 resume 值),仅靠 LangGraph checkpoint 匹配 id 来“消化”它们。若 checkpoint 已丢失(auto_persist=False 重启场景),这些重放的 interrupt 会以新的 interrupt id 注册并再次中断而非被消化,可能导致图重新挂起在已回答的轮次上。_extract_resume_commandresume_valuessetdefault 补了 completed 的 response,但前提是 completed 的 fc_id 仍带 STATE_KEY_LONG_RUNNING_PREFIX——若中断 id 复用前缀则可覆盖,否则该重放会残留。建议验证重放 interrupt 在 checkpoint 缺失时不会造成二次中断,或补充对应重启重放测试。

  • trpc_agent_sdk/tools/_function_tool.py:182trpc_agent_sdk/agents/core/_tools_processor.py:421-423FunctionTool 缺参时返回 ToolArgumentErrorResponse(error=...),而正常错误路径(异常/未找到)通过 _create_error_event 生成带 function_response 内容的事件;但缺参路径生成的是正常成功事件(带 function_response response={"error": ...})仅附加 error_code。这意味着缺参事件的 content 里 function_response 的 response 仍是 {"error": error_str},模型会把它当作工具结果。与 is_tool_execution_error 配合的逻辑成立,但缺参事件没有 execution_time 以外的区分,且该路径无单测覆盖 error_message 字段。建议补充缺参场景的 tools_processor 测试,确认 error_code/error_message 正确设置且模型可见。

💡 Suggestion

  • trpc_agent_sdk/models/_openai_model.py:1159-1164_model_dumpvalue 既非 dict 又无 model_dump 时会抛 AttributeError;流式与非流式路径都依赖它。可加一个 hasattr 兜底返回 {} 或抛更明确错误,避免上游返回非预期类型时栈底报错难以定位。

总结

整体改动(Responses API、AgentNode 多轮 HITL、graph checkpoint resume 检测、工具错误码统一)方向清晰且配有较完整测试,但存在一处必须修复的 Critical:AgentNode HITL pending 状态依赖 delta 提交时序,在 GraphInterrupt 路径下可能不落盘导致重启后多轮 HITL 失败。其余为参数静默丢弃、resume 检测事件滞后、parallel+long-running guard 行为与测试缺口等 Warning 级问题。

测试建议

  • 补充 auto_persist=False + 进程重启后 AgentNode 多轮 HITL 重放测试,验证 STATE_KEY_PENDING_AGENT_NODE_HITL 确实可被新 Runner 读到并能正确 resume(当前 test_agent_node_hitl_survives_service_restartisolated_messages=True,建议再覆盖 pending 状态写入中断桥事件 state_delta 的路径)。
  • 补充 parallel_tool_calls=True + LongRunningFunctionTool 触发 invalid_tool_configuration 错误事件的测试,并断言下游状态一致性。
  • 补充 Responses API 下设置 stop/frequency_penalty/presence_penalty/seed/candidate_count 时的行为测试(确认是否被丢弃或告警)。

transfer_requested = False
child_ctx.agent = current_agent

async for event in current_agent.run_async(child_ctx):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AgentNode HITL pending 状态依赖 delta 提交时序,GraphInterrupt 路径下可能不落盘

子 agent 抛出 LongRunningEvent 后写入 parent_ctx.state 的 pending HITL marker(落进 _delta),随后 interrupt() 抛 GraphInterrupt 穿透节点函数,节点不会正常返回,LangGraph 因此不会提交 _delta,marker 不落盘。auto_persist=False 且中断桥事件未携带该 key 时,重启后 _get_pending_hitl 读不到标记,多轮 HITL 重放失败、Command(resume=...) 从 START 重跑。建议显式将该 marker 通过中断桥事件 state_delta 持久化,而非依赖 delta 提交时序。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经足够了解了。让我来整理一下审查结论。

基于我对 diff 的分析,以下是我的发现:

发现的问题

🚨 Critical

  • trpc_agent_sdk/agents/core/_tools_processor.py:421-423parallel_tool_calls=True 时 long-running 工具的 argument error 事件会丢失错误码,导致降级失效
    • 该路径(_execute_tool)生成的 event 在 resultToolArgumentErrorResponse 时写入 error_code=tool_argument_error。但 execute_tools_asyncparallel_tool_calls=True 时会把多个 event 通过 _merge_parallel_function_response_events:677)合并,合并逻辑只搬移 content.parts/actions/timestamp不搬移 error_code/error_message,合并后 error_code 变回 None。此时 _llm_agent.py:594is_tool_execution_error(tool_event) 会判错,把一个参数错误的 long-running 调用误判为正常返回并发出 LongRunningEvent 挂起整图。建议在 merge 时聚合 error_code(任一子事件有错即置错),或在合并路径里保留首个错误码。

⚠️ Warning

  • trpc_agent_sdk/models/_openai_model.py:1244-1292:Responses API 转换丢弃了 stopfrequency_penaltypresence_penaltyseedn 等参数

    • _convert_api_params_to_responsesparameter_map 只映射 temperature/top_p/tool_choice/parallel_tool_calls/prompt_cache_*;_generate_async_impl 仍会把 stop_sequencesfrequency_penaltypresence_penaltyseed 写入 api_params:1776 起),但 Responses 转换后这些键被静默丢弃。Responses API 支持 stop/frequency_penalty/presence_penalty/seed,用户显式配置后不生效且无任何告警。建议补全映射或在丢弃时 logger.warning
  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:448-456_resume_content 用空字符串作为 FunctionResponse.id 的兜底,可能破坏 HITL 恢复匹配

    • 当 pending round 的 function_call.id 缺失时,id=str(function_call.get("id") or "") 会生成 id 为空的 FunctionResponse;后续 _graph_agent._handle_resumefunction_response_id.startswith(STATE_KEY_LONG_RUNNING_PREFIX) 判断(:294),空串会直接返回 None,导致这一轮 HITL 响应无法被映射回原 interrupt,恢复静默失败。建议在 id 缺失时直接抛错而非兜底空串。
  • trpc_agent_sdk/server/ag_ui/_core/_agui_agent.py:1163-1172merged_state.update(fresh_state) 合并顺序会用缓存值覆盖最新值

    • 注释声称「任一来源带 graph marker 即视为 resume」,但 merged_state = dict(cached_state); merged_state.update(fresh_state) 实际是 fresh 覆盖 cached,与「cached 也参与判断」无冲突;然而 get_session_state 出错时返回 None_session_manager.py:265 broad-except),此时 check_session 回退到可能过期的 session,若该快照尚未写入 _trpc_graph_pending_interrupt(auto_persist=False 下首个 interrupt 的典型情况),会漏判 resume 并调用 update_session_state 覆盖 checkpoint。建议在 fresh_state is None 时显式 logger.warning 并保守按 resume 处理,而不是静默回退。
  • trpc_agent_sdk/agents/_llm_agent.py:594-602:long-running 工具的 framework 错误在批量并行场景下仍会被部分降级

    • 虽然 PR 新增了 parallel_tool_calls + long_running 的前置拦截(:561),但该拦截只针对 parallel_tool_calls=True。当 parallel_tool_calls=False 且同一批次里同时有 long-running 工具和普通工具时,long-running 工具若返回 tool_argument_errorcontinue 后该批次的普通工具仍会执行,行为正常;但若 long-running 工具返回 tool_execution_error(例如异步执行异常),同样走 continue,错误事件已通过 yield tool_event 之前的逻辑……实际上该分支 continue 发生在 yield 之前,错误事件不会被 yield,模型看不到该错误,与注释「模型可见错误并决定重试」不符。建议在 continue 前先 yield tool_event 以让模型感知错误。

💡 Suggestion

  • trpc_agent_sdk/agents/_base_agent.py:267-350:手动 attach/detach span 的实现里 except Exception 过宽
    • attach 几乎只会抛 ValueError(跨 Context detach)或 OpenTelemetry 内部异常,当前 except Exception 会吞掉所有错误并 span.end(); raise,语义正确但可读性差;可考虑只捕获 (AttributeError, RuntimeError) 或保留现样并补注释说明为何不收窄。非阻塞,仅维护性。

总结

整体改动方向(Responses API、Agent 节点多轮 HITL、history_scope、filter 错误传播)实现完整且测试覆盖较好;存在 1 个 Critical:parallel_tool_calls=True 合并路径丢失 long-running 工具的错误码,可能让参数错误的 long-running 调用误触发图挂起,建议修复后再合入。

测试建议

  • 补充 _merge_parallel_function_response_events 在含 error_code 子事件下的合并测试,断言合并后 error_code 不丢失。
  • 补充 history_scope="branch" + pending HITL 恢复(含多轮 completed round)在 _handle_resume 中的端到端测试,覆盖 function_call.id 正常与缺失两种情况。

@@ -414,6 +418,9 @@

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

并行合并路径丢失 long-running 工具的错误码导致降级失效

parallel_tool_calls=True_merge_parallel_function_response_events 合并 event 仅搬移 content/actions/timestamp,不搬移 error_code/error_message,合并后错误码变回 None,使 is_tool_execution_error 判错并把参数错误的 long-running 调用误判为正常返回、发出 LongRunningEvent 挂起整图。建议合并时聚合 error_code(任一子事件有错即置错)或保留首个错误码。

功能新增:
- OpenAI Responses API 适配(opt-in,use_responses_api 默认 False)
- Agent Node HITL 多轮交互恢复机制
- LongRunningFunctionTool 执行错误检测

修复:
- async generator 中 span 泄漏:start_as_current_span 改为手动 attach/detach + try/finally
- OpenAI stream response 资源泄漏:finally 中 response.aclose() + UnboundLocalError 防御
- 多轮 HITL 恢复时 completed 轮次 interrupt 提供历史 resume 值
- LongRunningEvent fallback 仅对 graph 场景启用,避免误伤普通 LlmAgent HITL
- tool result 提交前重读 session state 避免竞态误判
- _openai_model include 类型校验(字符串不再被拆成字符列表)
- _llm_agent long-running + parallel_tool_calls 改为结构化 error event
- _events/_builder 抽取 _display_name 辅助方法消除重复
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经充分理解了,现在开始撰写审查结论。

发现的问题

🚨 Critical

  • tests/server/ag_ui/_core/test_agui_agent.py:1690-1772test_preserves_graph_state_on_pending_interrupt_without_checkpoint 方法体内混入了被删除的 test_handles_lro_events 的全部残留代码
    • diff 在把 test_handles_lro_events 改名为新测试时,只替换了方法签名和开头部分,新测试的 assert_not_awaited()(约 1718 行)之后仍保留了原 LRO 测试的逻辑:重新构造 lro_event、再次调用 _run_trpc_in_background 并断言 len(events) >= 2。这使一个测试方法里执行了两段无关场景,第二段是“死代码”但会被真实运行,既掩盖了第一段的断言意图,也可能在第二段失败时给出误导性报错。应删除 1718 行 assert_not_awaited() 之后到下一个 def test_ 之前的全部 LRO 残留代码,恢复为独立的 LRO 测试或彻底移除。
          agui_agent._session_manager.update_session_state.assert_not_awaited()
          from trpc_agent_sdk.events import LongRunningEvent   # ← 此行及之后均为残留,应删除
          from trpc_agent_sdk import types
          ...
          assert len(events) >= 2

⚠️ Warning

  • trpc_agent_sdk/server/ag_ui/_core/_agui_agent.py:1162-1188get_session_state 返回 None 时通过置 check_session = None 来“跳过同步”,但 check_session is not None and not self._is_graph_checkpoint_resume(...) 的逻辑实际效果是:当 check_session is None 时整个 if 为假,既不调用 update_session_state 也不调用 resume 判断,即跳过同步——符合注释意图;但注释说“forces _is_graph_checkpoint_resume to skip sync”表述与实现不符,且该分支把“读状态失败”一律按图恢复处理,对非图 Agent 的工具结果也会静默丢弃前端 state 同步。建议显式 return/continue 或在跳过同步前至少校验存在 _trpc_graph* 标记,避免普通 LlmAgent HITL 在读状态偶发失败时丢失前端状态。

  • trpc_agent_sdk/models/_openai_model.py:1830-1845:Responses 路径下 reasoning 通过 reasoning.setdefault("summary"/"effort", ...) 合并,但 _convert_api_params_to_responses 末尾执行 responses_params.update(self.responses_api_params)(约 1284 行),用户传入的 responses_api_params={"reasoning": {...}}整体覆盖而非合并此前根据 thinking_config 设置的 reasoning 字典,导致 thinking_budget→effort 映射在用户同时配置 reasoning 时被丢弃。建议对 reasoning 做 merge 而非 replace,或在文档中明确互斥关系。

  • trpc_agent_sdk/agents/_llm_agent.py:561-573:新增的 long-running tool + parallel_tool_calls=True 冲突检测仅在收集到 long-running tool 调用时报错并 return,但同一批次中已存在的普通 tool 调用也会被一并丢弃,且只产生一个 invalid_tool_configuration 错误事件、无对应测试覆盖(tests/agents/test_llm_agent_ext.py 未覆盖该分支)。建议补充测试并确认该中断语义(整批放弃 vs 仅禁用并行)符合预期。

  • trpc_agent_sdk/filter/_run_filter.py:50-58run_stream_filters 行为变更为“event.error 非 None 即 raisersp 为 None 即跳过、is_continue=Falsereturn”。这是一处兼容性变更:现有自定义流式 filter 若依赖“FilterResult(rsp=None, is_continue=False) 仍 yield None”或“error 以 None 形式下发到下游”的旧语义会行为改变(前者不再产出 None,后者改为抛异常)。新增测试覆盖了主路径,但未覆盖“同一 filter 链中先 yield 正常 rsp 再 yield error”的混合序列;建议补充该场景测试以确认 raise 不会吞掉已 yield 的部分流。

💡 Suggestion

  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:220-230_pending_round 把整个 child_statedict(child_state))存入 STATE_KEY_PENDING_AGENT_NODE_HITL["current"]["child_state"],该结构随后会经 state_delta 持久化到 Session.state。child_state 可能包含任意业务数据,虽该 key 已加入 UNSAFE_STATE_KEYS 不会暴露给前端 completion 事件,但仍会落入 SessionService 持久层;建议在注释中明确该字段会进入 checkpoint 存储,或仅保存恢复所需的最小子集以减小持久化体积与潜在敏感信息落盘。

总结

本 PR 主体(OpenAI Responses API、Agent 节点多轮 HITL、graph checkpoint resume 检测、错误码统一)设计合理且配套测试较完整;存在一个必须修复的测试代码合并残留(Critical),以及若干兼容性/边界隐患(Warning),建议优先处理测试残留与 reasoning 覆盖问题。

测试建议

  • 补充 run_stream_filters 在“正常 rsp 已 yield 后再 yield 带 error 的 FilterResult”混合序列下的行为测试。
  • 补充 LlmAgent 在 parallel_tool_calls=True 且批次含 long-running tool 时冲突检测路径的测试。


tc = _make_tool_call(tc_id="tc-1", name="search")
inp = _make_input(messages=[
_make_assistant_message(tool_calls=[tc]),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

测试方法中混入被删除 LRO 测试的残留代码

diff 把 test_handles_lro_events 改名为新测试时只替换了签名和开头,assert_not_awaited() 之后仍保留原 LRO 测试逻辑(重新构造 lro_event、再次运行并断言 len(events) >= 2),导致一个方法执行两段无关场景。应删除 1718 行 assert_not_awaited() 之后到下一个 def test_ 之前的全部残留代码。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants