feat(agent): 云端 Agent - 加固运行时并新增导演工具

This commit is contained in:
ddcat666
2026-09-27 08:17:03 +08:00
parent 1080b22196
commit 1a4598e496
16 changed files with 815 additions and 195 deletions

View File

@@ -123,6 +123,7 @@ type cloudAgentRuntime struct {
StepSnapshotHash string `json:"stepSnapshotHash,omitempty"`
StoryboardTaskID string `json:"storyboardTaskId,omitempty"`
Plan []cloudAgentPlanItem `json:"plan,omitempty"`
ConfirmationRounds int `json:"confirmationRounds,omitempty"`
PendingInterjections []cloudAgentInterjection `json:"pendingInterjections,omitempty"`
TransientReferences map[string]cloudAgentTransientReference `json:"transientReferences,omitempty"`
InterjectionIDs []string `json:"interjectionIds,omitempty"`
@@ -218,7 +219,7 @@ func (s *Service) ensureCloudAgentExecution(task *model.Task, initial cloudAgent
if err != nil {
return err
}
state := cloudAgentRuntime{Request: initial.Request, Policy: initial.Policy, ParentID: initial.ParentID, Fingerprint: initial.Fingerprint, CreativeAnchor: initial.CreativeAnchor, TextHistory: input.TextHistory, Skills: initial.Skills, Profile: initial.Profile, Canonical: canonical, ActiveTaskID: task.ID, TaskIDs: []string{task.ID}, Step: 1, Decisions: map[string]string{}, Plan: initial.Plan, Events: []CloudAgentEvent{}, StepLimits: limits}
state := cloudAgentRuntime{Request: initial.Request, Policy: initial.Policy, ParentID: initial.ParentID, Fingerprint: initial.Fingerprint, CreativeAnchor: initial.CreativeAnchor, TextHistory: input.TextHistory, Skills: initial.Skills, Profile: initial.Profile, Canonical: canonical, ActiveTaskID: task.ID, TaskIDs: []string{task.ID}, Step: 1, Decisions: map[string]string{}, Plan: initial.Plan, ConfirmationRounds: initial.ConfirmationRounds, Events: []CloudAgentEvent{}, StepLimits: limits}
if len(initial.Skills) > 0 {
// skillIds makes the enablement auditable: usage telemetry can attribute a
// run to the skills it actually loaded instead of only counting the total.
@@ -383,6 +384,9 @@ func validateCloudAgentRuntime(run *model.CloudAgentExecution, state *cloudAgent
if state.Step < 0 || state.Generations < 0 || state.VideoSeconds < 0 {
return errors.New("Agent runtime budget or step is invalid")
}
if state.ConfirmationRounds < 0 || state.ConfirmationRounds > cloudAgentMaxConfirmationRounds {
return errors.New("Agent runtime confirmation round is invalid")
}
if state.ImageInspectCalls < 0 {
return errors.New("Agent runtime image inspection budget is invalid")
}
@@ -1846,10 +1850,16 @@ func (s *Service) advanceCloudAgentTool(run *model.CloudAgentExecution, state *c
}
if call.Function.Name == "ask_user" && toolErr == nil {
payload, _ := result.(map[string]any)
state.event(run.ID, "user_question", payload)
if payload["phase"] == "question" {
state.event(run.ID, "user_question", payload)
cloudAgentRecordToolResult(current, state, call, result, nil)
skipRemainingCloudAgentCalls(run.ID, state)
current.Status = "completed"
return cloudAgentSave(current, state)
}
// The server-side round limit turns further questions into a normal
// tool result so the model must continue with safe defaults.
cloudAgentRecordToolResult(current, state, call, result, nil)
skipRemainingCloudAgentCalls(run.ID, state)
current.Status = "completed"
return cloudAgentSave(current, state)
}
cloudAgentRecordToolResult(current, state, call, result, toolErr)
@@ -2086,6 +2096,10 @@ func (s *Service) cloudAgentMediaError(run *model.CloudAgentExecution, state *cl
state.event(run.ID, "run_failed", map[string]any{"text": "Agent 媒体调用状态无效,本轮已停止"})
return cloudAgentSave(current, state)
}
if phase == "admission" && !submitted {
err = cloudAgentWrapMediaAdmissionError(err)
}
toolName := state.Calls[state.CallIndex].Function.Name
cloudAgentRecordToolResult(current, state, state.Calls[state.CallIndex], map[string]any{"phase": phase, "taskSubmitted": submitted}, err)
// Any admission failure advances the call into the repair path. Do not let
// a prepared quote from the failed attempt leak into the corrected call.
@@ -2094,9 +2108,30 @@ func (s *Service) cloudAgentMediaError(run *model.CloudAgentExecution, state *cl
if submitted {
state.MediaTaskID = ""
}
if terminal {
// Only explicitly typed argument errors and a stale canvas snapshot may
// continue into another model turn. Every other pre-submission media
// failure is a real admission boundary failure; continuing would invite
// the model to submit unverified variants of a billed write.
continueAfterAdmissionError := false
if phase == "admission" && !submitted {
var argumentErr *cloudAgentArgumentError
continueAfterAdmissionError = errors.As(err, &argumentErr)
var admissionErr *cloudAgentMediaAdmissionError
if errors.As(err, &admissionErr) && admissionErr.Reason == "snapshot_conflict" {
continueAfterAdmissionError = true
}
}
if terminal || (phase == "admission" && !submitted && !continueAfterAdmissionError) {
current.Status = "failed"
state.event(run.ID, "run_failed", map[string]any{"text": "媒体任务已提交,但结果处理失败;任务不会自动重试"})
message := "媒体生成准入失败,本轮已停止;请检查模型、能力和预算后由用户明确重试"
reason := "tool_admission_failed"
if terminal {
message = "媒体任务已提交,但结果处理失败;任务不会自动重试"
reason = "media_task_failed"
}
current.FailureMessage = truncateRunes(message, 1000)
cloudAgentDropInterjections(run.ID, "本轮已结束:"+truncateRunes(message, 120), state)
state.event(run.ID, "run_failed", map[string]any{"text": message, "reason": reason, "toolName": toolName})
}
return cloudAgentSave(current, state)
})