普通函数调用靠 call_id 与 FunctionCallOutput 配对。发送前规范化执行两个方向的检查:有 call 无 output 时在 call 后插入合成 aborted;有 output 无 call 时删除孤儿 output。修复顺序固定为“先补缺,再删孤儿”。
先建立所有 output ID 集合
规范化器先扫描现有 outputs,构造 function_output_ids;再遍历 calls,收集所有缺失 output 及其插入位置。最后按索引逆序插入,防止前一次插入改变后续位置。
def ensure_function_outputs(items):
output_ids = {x.call_id for x in items if is_function_output(x)}
pending = []
for index, item in enumerate(items):
if is_function_call(item) and item.call_id not in output_ids:
pending.append((index, FunctionOutput(
id=stable_synthetic_id("fco", item.id),
call_id=item.call_id,
output="aborted",
)))
for index, output in reversed(pending):
items.insert(index + 1, output)
aborted 不是工具真的返回了该文本,而是告诉模型这次调用已不可能继续,避免它等待一个永远不会到来的结果。
合成 ID 必须稳定
若源 call 有 item ID,合成 output ID 用固定 UUID namespace 和 prefix:source_id 生成 UUIDv5;同一原始历史在重试或恢复时得到同一个 ID。源 call 没 ID 时保持兼容,合成 ID 也为空。
def remove_orphan_function_outputs(items):
call_ids = {x.call_id for x in items if is_function_call(x)}
shell_ids = {x.call_id for x in items if is_local_shell_call(x)}
items[:] = [x for x in items
if not is_function_output(x)
or x.call_id in call_ids | shell_ids]
Function output 也可以对应 LocalShellCall,删除孤儿时要把两类 call ID 联合判断。重复 call_id 属于上游协议异常,规范化器不是通用事件数据库,不负责猜测应该匹配哪一次。
源码与测试锚点
codex-rs/core/src/context_manager/normalize.rs:补缺、稳定 ID 与删孤儿。codex-rs/core/src/context_manager/history.rs::for_prompt:调用边界。codex-rs/core/src/context_manager/history_tests.rs:missing/orphan function output。
评论
登录后即可评论