feat(cloud-agent): 单步输出上限与秒级墙钟,超时/空输出可自恢复

画布 Agent 一轮要跑很多步模型调用,此前只有"按任务类型的分钟级超时":某一步卡住只能等
整轮判死;更根本的是每一步请求根本不带输出上限,不写 max_tokens 时上游按剩余上下文放行,
思考模型可以把单步拖到几分钟,且偶发把整个输出预算花在推理上、正文与工具调用皆空。

- 新增运行时策略 task.agentStepTimeoutSeconds 与 task.agentStepMaxOutputTokens:秒级墙钟
  与单步输出上限,都支持环境变量给部署级默认值、管理端热更新;旧配置 JSON 缺字段按默认值
  回填,无需数据迁移。
- 单步墙钟到点中止这一步时不再把整轮判死:以可识别标记记为该步失败,运行期关掉上游思考
  重试同一步一次(model_failure_recovered / step_timeout_retried),重试仍超时才以可读原因
  (model_step_timeout)结束本轮。
- 空输出催办用尽后新增一级升级:关思考 + 放大输出预算重试同一步
  (model_failure_recovered / empty_output_escalated),而不是直接判死。放大档在原值上翻倍
  并以 131072 封顶,策略值为 0 时用 32768 兜底,让重试仍然有界。
- 输出上限真正下发到上游:textOptions.maxOutputTokens 成为任务信封字段,Agent 的两条请求
  路径(内置协议与声明式协议)组装请求体时写入 max_tokens / max_output_tokens,与既有的
  input.MaxOutputTokens 取较小值,避免策略上限超过模型声明的物理上限。
- task_worker:执行时限改用带画布 Agent 分支的 taskExecutionTimeout;执行时限到点本身不再
  被当成"租约丢失",否则任务会停在 running 等租约过期后被反复重跑(实测一次调用被重跑成
  三次上游请求)。续租仍用上游已合入的"不继承父 context 取消"的独立 context。
This commit is contained in:
AetherNo2332
2026-09-22 01:20:36 +08:00
parent 6f45240599
commit 63c9e32259
11 changed files with 609 additions and 11 deletions

View File

@@ -0,0 +1,57 @@
package app
import (
"strings"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
)
// cloudAgentStepTimeoutError 是一次画布 Agent 模型调用被单步墙钟掐断时写进任务错误的标记。
// 秒级超时(策略 AgentStepTimeoutSeconds)与文本任务超时都会走到这里,运行期按这个标记
// 把它当成"可恢复失败"处理,而不是整轮判死。
const cloudAgentStepTimeoutError = "画布 Agent 单步模型调用超时"
// cloudAgentMaxStepTimeoutEscalations 是单步超时的重试次数:超时几乎总是思考阶段在长时间
// 无输出,因此重试的手段是关掉思考(更省时的路径),只给一次机会。
const cloudAgentMaxStepTimeoutEscalations = 1
// cloudAgentModelOperation 判断任务是不是"画布 Agent 的一次模型调用":根任务(第一步)与
// 后续每一步都属于同一类,都要按单步口径计时。
func cloudAgentModelOperation(task *model.Task) bool {
if task == nil {
return false
}
switch task.Operation {
case cloudAgentOperation, cloudAgentStepOperation:
return true
default:
return false
}
}
// cloudAgentStepTimedOut 判断这一步是不是被单步墙钟中止的。
func cloudAgentStepTimedOut(task *model.Task) bool {
if task == nil || task.Status == model.TaskStatusSucceeded {
return false
}
return strings.Contains(task.Error, cloudAgentStepTimeoutError)
}
// correctCloudAgentStepTimeout 是单步超时的兜底:关掉上游思考并重发同一步。
// 超时不改输出预算——墙钟是这一档的约束,放大预算只会让重试更容易再次撞上墙钟。
func (s *Service) correctCloudAgentStepTimeout(run *model.CloudAgentExecution, state *cloudAgentRuntime) error {
return s.repo.MutateCloudAgent(run.UserID, run.ID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
state.StepTimeoutEscalated++
state.ForceThinkingOff = true
state.BoostStepOutputBudget = false
state.ActiveTaskID = ""
state.Calls = nil
state.CallIndex = 0
state.event(run.ID, "model_failure_recovered", map[string]any{
"reason": "step_timeout_retried",
"text": "上一步模型调用超时;已关闭思考重试同一步",
})
return cloudAgentSave(current, state)
})
}