fix(agent): 画布助手 - 工具参数超限改为纠偏回执,不再判死整个运行 (#701)

* fix(agent): 画布助手 - 工具参数超限改为纠偏回执,不再判死整个运行

* fix(agent): 画布助手 - 桥接优先回放参数回执,结构类调用问题整步拒绝

---------

Co-authored-by: Future <jiaboxuan1205@163.com>
This commit is contained in:
jiabxandFuture authored and GitHub committed 2026-10-08 12:15:22 +08:00
1 parent d98e9f78f1
commit e69bea15ef
5 files changed
+414 -14

No files matched your search

+93 -9
View File
@@ -331,21 +331,27 @@ func (s *Service) runCloudAgentModelStep(ctx context.Context, userID, runID stri
finishText, finishReasoning = "", ""
}
if !compactionSummary {
if err := validateCloudAgentCalls(result.ToolCalls); err != nil {
// The task itself succeeded, but its protocol result is not executable.
// Release the adopted task before returning the hard protocol error so a
// restarted run cannot remain stuck behind an invalid active task.
invalid, structural := cloudAgentBatchInvalidCalls(result.ToolCalls)
if len(invalid) > 0 && (structural || state.InvalidArgumentSteps >= cloudAgentInvalidArgumentStepLimit) {
// 结构类问题(ID 无效/重复、工具名无效)按 ID 配对纠偏会产生歧义,连续多批
// 参数无效说明模型无法自我修正:两者都保持既有整步拒绝语义,
// 释放任务避免重启后的 run 卡在无效 active task 上。
if releaseErr := s.finishCloudAgentPiModelStep(userID, runID, task.ID, "", ""); releaseErr != nil {
return nil, false, releaseErr
}
return nil, false, fmt.Errorf("invalid Pi tool calls: %w", err)
if structural {
return nil, false, fmt.Errorf("invalid Pi tool calls: %w", validateCloudAgentCalls(result.ToolCalls))
}
return nil, false, fmt.Errorf("invalid Pi tool calls: 模型连续 %d 批工具调用未通过参数校验", cloudAgentInvalidArgumentStepLimit)
}
}
if compactionSummary {
if err := s.finishCloudAgentPiModelStep(userID, runID, task.ID, "", ""); err != nil {
if len(invalid) > 0 {
if err := s.finishCloudAgentPiModelStepWithInvalidCalls(userID, runID, task.ID, finishText, finishReasoning, result.ToolCalls, invalid); err != nil {
return nil, false, err
}
} else if err := s.finishCloudAgentPiModelStep(userID, runID, task.ID, finishText, finishReasoning, result.ToolCalls); err != nil {
return nil, false, err
}
} else if err := s.finishCloudAgentPiModelStep(userID, runID, task.ID, finishText, finishReasoning, result.ToolCalls); err != nil {
} else if err := s.finishCloudAgentPiModelStep(userID, runID, task.ID, "", ""); err != nil {
return nil, false, err
}
delivered := map[string]any{"text": result.Text, "reasoning": result.Reasoning, "toolCalls": runtimeToolCalls(result.ToolCalls)}
@@ -517,6 +523,10 @@ func (s *Service) finishCloudAgentPiModelStep(userID, runID, taskID, text, reaso
}
fresh.ActiveTaskID = ""
fresh.ActiveTextDraft = ""
if len(callBatches) > 0 {
// 一批全部有效的调用意味着模型回到了正常输出轨道,参数纠偏计数清零。
fresh.InvalidArgumentSteps = 0
}
if reasoning != "" {
fresh.event(runID, "reasoning_message", map[string]any{"messageId": taskID + ":reasoning", "text": truncateRunes(reasoning, 8000)})
}
@@ -547,6 +557,80 @@ func (s *Service) finishCloudAgentPiModelStep(userID, runID, taskID, text, reaso
return repository.ErrCreationConflict
}
// finishCloudAgentPiModelStepWithInvalidCalls 提交一个含无效调用的模型步骤(参数纠偏轮)。
//
// 模型一步里可能混发有效与无效调用——线上 run ag3580aca 就是把整份剧本大纲塞进
// canvas_apply_ops 的参数超过字节上限,此前整批被当致命协议错误判死成「Agent 执行中断」。
// 参数超限是模型输出问题而非运行时故障:无效调用立即以结构化错误回执(invalid_model_output,
// 指引拆分重试),有效调用照常进入执行批次;assistant 消息保留全部调用,canonical 的
// 调用/结果配对保持完整。连续 cloudAgentInvalidArgumentStepLimit 批无效才回到硬失败。
func (s *Service) finishCloudAgentPiModelStepWithInvalidCalls(userID, runID, taskID, text, reasoning string, calls []cloudAgentCall, invalid map[int]string) error {
valid := make([]cloudAgentCall, 0, len(calls))
for index, call := range calls {
if _, rejected := invalid[index]; !rejected {
valid = append(valid, call)
}
}
for attempt := 0; attempt < 8; attempt++ {
run, err := s.repo.CloudAgent(userID, runID)
if err != nil {
return err
}
err = s.repo.MutateCloudAgent(userID, runID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
fresh, err := cloudAgentDecode(current)
if err != nil {
return err
}
if fresh.ActiveTaskID != taskID {
return nil
}
fresh.ActiveTaskID = ""
fresh.ActiveTextDraft = ""
fresh.InvalidArgumentSteps++
if reasoning != "" {
fresh.event(runID, "reasoning_message", map[string]any{"messageId": taskID + ":reasoning", "text": truncateRunes(reasoning, 8000)})
}
fresh.Canonical.ToolChoice = "auto"
fresh.Calls = valid
fresh.CallIndex = 0
fresh.StepSnapshotHash = cloudAgentCaptureStepSnapshotHash(valid)
if text != "" {
fresh.event(runID, "assistant_message", map[string]any{"messageId": taskID, "text": text})
}
fresh.Canonical.Messages = append(fresh.Canonical.Messages, map[string]any{
"role": "assistant", "content": text, "tool_calls": calls,
})
rejected := make([]string, 0, len(invalid))
for index, call := range calls {
reason, ok := invalid[index]
if !ok {
continue
}
receipt, _ := json.Marshal(map[string]any{
"error": "该工具调用未通过参数校验:" + reason + "。本次调用没有执行,其余同批调用不受影响。",
"errorClass": cloudAgentToolErrorInvalidModelOutput,
"errorClassLabel": cloudAgentToolErrorLabel(cloudAgentToolErrorInvalidModelOutput),
"requiredAction": "fix_arguments",
})
fresh.Canonical.Messages = append(fresh.Canonical.Messages, map[string]any{
"role": "tool", "tool_call_id": call.ID, "content": string(receipt),
})
rejected = append(rejected, call.Function.Name)
}
fresh.event(runID, "tool_arguments_rejected", map[string]any{
"toolNames": rejected, "attempt": fresh.InvalidArgumentSteps,
"maxAttempts": cloudAgentInvalidArgumentStepLimit,
"text": "模型工具调用参数未通过校验,已回执要求修正后重试",
})
return cloudAgentSave(current, &fresh)
})
if !errors.Is(err, repository.ErrCreationConflict) {
return err
}
}
return repository.ErrCreationConflict
}
func (s *Service) waitCloudAgentTask(ctx context.Context, taskID string) (*model.Task, error) {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()