Skip to content

feat: 支持 OpenAI Responses API、Agent Node HITL 多轮交互及工具错误检测 - #227

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

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

Conversation

@pcerypeng

Copy link
Copy Markdown
Contributor
  • OpenAIModel 新增 use_responses_api 参数,使用 client.responses.create 替代 Chat Completions
  • 支持 responses_api_params 透传 store、reasoning、truncation 等待有参数
  • AgentNodeAction 支持 LongRunningEvent 中断并传播到父 Graph
  • 支持多轮 HITL 交互及服务重启后正确恢复
  • 新增 is_tool_execution_error() 函数,区分 Provider 参数解析错误和 HITL 检查点
  • 更新 Graph 和 Model 中英文文档
  • 升级 openai 最低版本依赖至 1.66.0

@codecov

codecov Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.60043% with 25 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@86781c7). Learn more about missing BASE report.

Files with missing lines Patch % Lines
trpc_agent_sdk/models/_openai_model.py 94.33333% 17 Missing ⚠️
trpc_agent_sdk/dsl/graph/_node_action/_agent.py 96.20253% 3 Missing ⚠️
trpc_agent_sdk/agents/_llm_agent.py 92.30769% 2 Missing ⚠️
trpc_agent_sdk/agents/core/_tools_processor.py 80.00000% 1 Missing ⚠️
...c_agent_sdk/server/ag_ui/_core/_session_manager.py 66.66667% 1 Missing ⚠️
trpc_agent_sdk/tools/_long_running_tool.py 90.00000% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main        #227   +/-   ##
==========================================
  Coverage        ?   87.95475%           
==========================================
  Files           ?         482           
  Lines           ?       45437           
  Branches        ?           0           
==========================================
  Hits            ?       39964           
  Misses          ?        5473           
  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.

@github-actions

Copy link
Copy Markdown

CLA Assistant Lite bot:
Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

现在我已经掌握了所需的所有信息,让我来撰写审查意见。

发现的问题

🚨 Critical

  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:199-218:Agent 节点 HITL 在捕获 LongRunningEvent 后调用 interrupt(...) 抛出 GraphInterrupt 暂停图,但在此之前已通过 self.writer.write_event 之外的路径产生了事件——LongRunningEvent 本身(partial=True)不会被 Runner 持久化,且节点函数在 interrupt() 之后直接 raise RuntimeError。问题在于:interrupt() 在 LangGraph 中通过抛出 GraphInterrupt 异常向上冒泡来暂停,而此处用 try/except 捕获了 GraphInterrupt 并 raise(302-303 行),但 interrupt() 之后的 raise RuntimeError(...) 这一行在 interrupt() 真正抛出时永远不会执行;若 interrupt() 未抛出(异常被吞),则进入 RuntimeError,被 304 行的 except Exception 捕获后会清空 STATE_KEY_PENDING_AGENT_NODE_HITL 并抛错。建议确认 interrupt() 在该 langgraph 版本下一定抛 GraphInterrupt,否则已写入 parent_ctx.state 的 pending HITL 会被错误清空,导致无法恢复。

    实际更明确的问题是:pending HITL 状态写入的是 parent_ctx.state(即 ctx.callback_state → event_actions.state_delta),而该状态要持久化到 session 必须通过某个 partial=False 事件的 state_delta 被 Runner append_event 应用。但中断路径下 _run_async_impl 在 interrupt 处直接抛出,GraphAgent 的 finally 分支因 interrupted=True 直接 return(_graph_agent.py:251-253),不会 yield completion 事件,因此 ctx.actions.state_delta 中的 STATE_KEY_PENDING_AGENT_NODE_HITL 可能从未被写入 session.state,重启后 _get_pending_hitl 读不到。测试 test_team_agent_leader_hitl_survives_service_restart 用 SqlSessionService 通过了,说明实际有别的路径落盘(TeamAgent 自身 yield 了 state_update 事件),但纯 AgentNodeAction + InMemorySessionService 的多轮测试并未验证重启恢复。建议补充:AgentNode HITL 中断后 session.state 中确实持久化了 STATE_KEY_PENDING_AGENT_NODE_HITL,否则多轮恢复依赖内存态不可靠。

⚠️ Warning

  • trpc_agent_sdk/models/_openai_model.py:1847:非流式 Responses 调用将 http_options 通过 {**api_params, **http_options} 合并进顶层参数字典,再交给 _prepare_responses_api_params 处理。http_options 可能包含 extra_body、extra_headers、timeout,其中 extra_body/extra_headers 作为 dict 会被原样塞进 responses.create(...) 的关键字参数——extra_headers 可被 responses.create 接受,但 extra_body 对 responses.create 不是合法顶层参数(Responses API 无此参数),且 timeout 是客户端级别参数。更关键的是 _prepare_responses_api_params 会对整个 dict 做 inspect.signature(client.responses.create).parameters 判断 logprobs,但不会剔除 extra_body。结果是非流式路径下 extra_body 会被作为未知 kwargs 传入 responses.create,可能触发 SDK TypeError。建议像流式路径(1891-1893 行分别 spread)那样把 http_options 单独传给 client.responses.create,而不是合并进 api_params。

  • trpc_agent_sdk/agents/_llm_agent.py:576-620:长运行工具检测按 part.function_response.id in long_running_tool_ids 判断,但当 parallel_tool_calls=True 时多个工具结果会被合并进单个 merged event(_merge_parallel_function_response_events,_tools_processor.py:673)。merged event 的 content.parts 包含所有工具响应,循环中一旦匹配到长运行工具即 yield 后 return,会丢弃同一批次中其它已执行工具的结果(既不 yield 也不写入 history),且 merged event 已被 yield 一次。建议在并行模式下确保非长运行工具的 function_response 仍被持久化,或文档/校验长运行工具不与并行批混用。

  • trpc_agent_sdk/dsl/graph/_constants.py:172-189:新增的 STATE_KEY_PENDING_AGENT_NODE_HITL 未加入 UNSAFE_STATE_KEYS,而其值包含 child_state(dict)、function_call/function_response 的 model_dump(含完整工具参数与响应)。该 key 会经由 is_unsafe_state_key 过滤为“安全”,从而出现在 GraphAgent._filter_safe_state 产出的 final_state/state_delta 中,并随 completion 事件对外暴露(可能包含敏感工具参数)。其它 pending interrupt 系列 key 都被标记为 unsafe,此处遗漏不一致。建议将其加入 UNSAFE_STATE_KEYS,或确保其不进入对外 state_delta。

  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:136-138:恢复多轮 HITL 时,对 completed 历史轮次逐个调用 interrupt(self._interrupt_payload(completed))。LangGraph 的 interrupt 在 resume 时要求按原顺序重放每个 interrupt 并提供对应 resume 值;此处对已完成轮次调用 interrupt 但不使用其返回值(仅为了重放占位),依赖客户端按相同顺序提交多份 resume。若客户端只提交了 current 轮的 FunctionResponse 而未按序重放历史轮次,interrupt() 会抛错或取到错误值。该多轮重放契约在测试中以特定顺序驱动通过,但缺少对“顺序错乱/部分提交”场景的覆盖。建议补充测试并文档化该约束。

  • trpc_agent_sdk/agents/_llm_agent.py:554-555:连续两行完全相同的 logger.debug("Executing %s tool calls", ...)。(该重复在 PR 基线之前已存在,但仍在本次 diff 触及的函数体内,属于可合并的维护性瑕疵。)

💡 Suggestion

  • trpc_agent_sdk/tools/_long_running_tool.py:19-22:is_tool_execution_error 通过硬编码 error_code 集合判断框架错误,与 _tools_processor.py 中实际使用的 "tool_not_found"/"tool_argument_error"/"tool_execution_error" 字符串重复定义。建议将这些 error_code 提取为常量共享,避免后续新增错误码时两处不同步导致长运行判定失效。

总结

本次 PR 新增 Responses API 适配与 Agent 节点多轮 HITL 机制,整体测试覆盖较充分,但存在若干稳定性与兼容性隐患:非流式 Responses 路径对 http_options 的合并方式可能误传 extra_body、并行工具批下长运行工具会丢弃同批其它结果、新增 pending HITL state key 未归入 unsafe 集合可能外泄、以及多轮 HITL 的 interrupt 重放契约缺少异常顺序测试。其中 HITL 中断态的持久化路径建议重点验证。建议修复上述 Warning 后合入。

测试建议

  • 补充 AgentNode HITL 中断后、进程重启(新建 Runner + 从同一 session_service 恢复)场景下 STATE_KEY_PENDING_AGENT_NODE_HITL 仍可被 _get_pending_hitl 读到的测试(当前仅 TeamAgent 路径覆盖了重启)。
  • 补充 Responses API 非流式路径在 http_options 含 extra_body/extra_headers 时不报错且参数正确传递的测试。
  • 补充多轮 HITL 在客户端乱序或部分提交 FunctionResponse 时的行为测试,明确失败语义。

Comment thread trpc_agent_sdk/dsl/graph/_node_action/_agent.py
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经完全理解了。让我整理一下审查意见。

基于我对 diff 和仓库上下文的彻底分析,以下是我的发现:

发现的问题

🚨 Critical

  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:132-143:恢复时重放已完成轮次的 interrupt() 调用会阻塞图恢复
    • 在 pending_hitl 恢复路径中,代码对 completed 列表里的每个历史轮次都调用 interrupt(self._interrupt_payload(completed)),再对 current 调用一次。LangGraph 的 interrupt() 在恢复(resume)阶段会按调用顺序依次消费 Command(resume=...) 提供的值,这意味着已完成轮次的 interrupt() 也必须由客户端逐个提供 resume 值,否则恢复会卡住或错位。但 _extract_resume_command(_graph_agent.py:304)只根据“最新一个 user FunctionResponse”构造 Command(resume={interrupt_id: response}),单条 resume 命令无法填满多个 interrupt() 调用。测试 test_agent_node_long_running_interrupts_parent_and_resumes_multiple_rounds 实际上是在第二轮把 previous_current 加入 completed 后再 interrupt(current_round),使恢复时需要先为旧 current 再为新 current 各提供一次 resume——而客户端每次 _run 只发一个 FunctionResponse,与该多轮重放契约不匹配,存在恢复失败/错位风险。建议改为仅对 current 轮发起 interrupt,已完成轮次在重建 child state/事件时直接回放,不再触发新的 interrupt()。

⚠️ Warning

  • trpc_agent_sdk/models/_openai_model.py:1251-1261:Responses API 路径未映射 stop/stop_sequences 等参数

    • _convert_api_params_to_responses 的 parameter_map 未包含 ApiParamsKey.STOP(以及 seed、frequency_penalty、presence_penalty、n)。上游 _generate_async_impl 在 api_params[ApiParamsKey.STOP] = request.config.stop_sequences(1766-1767)等处仍会写入这些键,但转换后被静默丢弃,导致 Responses 模式下 stop_sequences 等配置失效,行为与 Chat Completions 路径不一致。Responses API 的 stop 概念不同(无 stop),但应至少显式记录或映射到等价能力,避免用户配置被无声忽略。
  • trpc_agent_sdk/models/_openai_model.py:1934-1946:Responses 流式 reasoning delta 未保留 thought_signature,导致 store=False 回放链断裂

    • 文档(docs/mkdocs/*/model.md)承诺 store=False 时通过 reasoning.encrypted_content 在下一轮回放 reasoning。非流式路径(_create_responses_response 1346/1354)会把 reasoning item 序列化进 thought_signature,并在 _format_messages 中还原为 responses_input_items。但流式路径的 reasoning delta 只累加文本并 yield 为 Part.from_text(thought=True),从未设置 thought_signature;只有当服务端发了 response.completed(且其 output 含 reasoning item)时才能通过 _create_responses_response 补上签名。当走 fallback(无 completed 事件,1992-2010)时,合成 reasoning item 不含 encrypted_content,下一轮无法回放加密推理,可能触发模型报错或推理丢失。建议在 fallback 合成 reasoning item 时尽量保留 encrypted_content,或文档明确该限制。
  • trpc_agent_sdk/models/_openai_model.py:1316-1319:top_logprobs 启用条件可能导致 logprobs 被静默丢弃

    • _prepare_responses_api_params 中,当客户端只支持 top_logprobs 形参时,仅当 top_logprobs 真值才写入;若用户设置了 logprobs=True 但未指定 top_logprobs(或设为 0/None),则既不写 logprobs 也不报错,直接返回不带 logprobs 的请求——用户开启了 logprobs 却拿不到任何 logprob 且无提示。建议在该分支下当 logprobs 被请求但无法满足时,与“客户端不支持”一致地抛出明确错误,而非静默丢弃。
  • trpc_agent_sdk/dsl/graph/_node_action/_agent.py:170-181:child_ctx = parent_ctx.model_copy(deep=False) 浅拷贝导致 child 修改泄漏回父上下文

    • HITL 改动 parent_ctx.state[...]、child_session.events.append(...) 等本意作用于子执行,但 deep=False 使得 event_actions、callback_state 等共享引用被 child 写入后影响父 InvocationContext(例如 child_session.events 来自深拷贝 session,但 child_ctx.event_actions 是新对象——而其他非显式覆盖字段仍与父共享对象引用)。这与非 HITL 路径一致的既有行为,但新增的 resume_content/状态重写放大了副作用面,存在跨节点状态污染风险。建议显式覆盖会被修改的可变字段,或对回调状态做隔离。
  • tests/trpc_agent_dsl/graph/test_agent_node_hitl.py:1452:测试通过 team.__pydantic_private__["_leader_agent"] = leader 强注入私有属性,脆弱且脱离真实路径

    • 该测试用 __pydantic_private__ 直接替换 TeamAgent 的私有 _leader_agent,绕过 _init_leader_agent 的全部装配逻辑(指令、工具、parent 绑定等),无法验证真实 TeamAgent Leader HITL 链路;一旦 TeamAgent 内部装配方式变化(如增加校验或重置),测试会静默失效或误判通过。建议通过 TeamAgent 公开构造参数或子类化注入 Leader,使测试反映真实运行路径。

💡 Suggestion

  • trpc_agent_sdk/tools/_long_running_tool.py:15:from trpc_agent_sdk.events import Event 使 tools 包在模块加载期依赖 events 包。虽然当前 events 不反向依赖 tools(无循环),但 is_tool_execution_error 只用到 Event.error_code 字段,可考虑以 Any 或协议类型避免工具层对事件具体类的硬依赖,降低后续重构耦合。

总结

整体风险集中在 Agent 节点 HITL 多轮重放的 interrupt() 调用语义上:恢复时对已完成轮次重复 interrupt() 与单条 resume 命令的模型不匹配,存在恢复失败/错位的高风险(Critical)。Responses API 适配存在若干参数丢弃与流式 reasoning 回放断裂的稳定性问题。建议优先修复 Critical 中的重放逻辑,再处理 Warning 中的参数映射与资源/状态隔离问题。

测试建议

  • 补充“同一 Agent 节点连续 3 轮及以上 HITL、且每轮 resume 只发单个 FunctionResponse”的端到端恢复测试,验证已完成轮次不会被再次 interrupt()、最终能正常推进到后继节点。
  • 补充 Responses API 流式 + store=False 且服务端不发送 response.completed(走 fallback)时,下一轮请求能正确回放 encrypted_content reasoning 的测试。


child_scope = self.event_scope or self.agent.name
child_branch = f"{parent_ctx.branch}.{child_scope}" if parent_ctx.branch else child_scope
child_user_input = child_state.get(STATE_KEY_USER_INPUT, "")

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.

恢复时重放已完成轮次的 interrupt() 会阻塞图恢复

在 pending_hitl 恢复路径中对 completed 列表里的每个历史轮次都调用 interrupt(),而 _extract_resume_command 每次只构造单条 Command(resume=...),无法填满多个 interrupt() 调用,导致恢复卡住或错位。建议仅对 current 轮发起 interrupt,已完成轮次直接回放 child state/事件。

新增功能:
- OpenAI Responses API 适配 (非流式/流式), 支持 reasoning、tool calls、logprobs
- Agent 节点多轮 HITL 机制, 通过 interrupt bridge 桥接子 agent LongRunningEvent
- AG-UI GraphAgent checkpoint 保护, 防止工具结果恢复时覆盖 LangGraph checkpoint

代码修复 (代码审查):
- _constants.py: 将 STATE_KEY_PENDING_AGENT_NODE_HITL 加入 UNSAFE_STATE_KEYS,
  防止含敏感工具参数的 child_state 通过 completion 事件对外暴露
- _openai_model.py: 非流式 Responses 路径分离 http_options 传参, 与流式路径
  保持一致, 修复 extra_body 被误传为 responses.create 顶层参数的 Bug
- _llm_agent.py: 删除重复的 logger.debug 行; 在长运行工具与并行工具批混用时
  发出警告, 提示同批其它工具结果不会被 LLM 进一步处理
- _long_running_tool.py: 提取 TOOL_ERROR_CODE_* 共享常量, 消除与
  _tools_processor.py 中的硬编码字符串重复
- _agui_agent.py: 优化 GraphAgent checkpoint 检测——复用 _ensure_session_exists
  返回的 session 消除额外 DB 查询, 用模块级常量替代硬编码前缀, 改为同步方法
- _session_manager.py: 新增 session_service 公共属性, 消除私有成员访问

新增测试:
- AgentNode HITL 中断后进程重启恢复场景 (SqlSessionService)
- STATE_KEY_PENDING_AGENT_NODE_HITL unsafe 归类验证
- 多轮 HITL 客户端使用过期 function_call.id 提交 resume 时不静默完成
- 非流式 Responses 路径 http_options 含 extra_body/extra_headers/timeout 时参数分离
- GraphAgent checkpoint 保护测试
- test_constants.py 更新覆盖新增 unsafe key
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