Merge remote-tracking branch 'origin/pr-587' into codex/pr-557-589-security-integration
# Conflicts: # backend/internal/app/cloud_agent_runtime.go
This commit is contained in:
@@ -94,6 +94,18 @@ type cloudAgentRuntime struct {
|
||||
// StepLimits 是本步实际生效的执行边界(管理员策略解析结果),只用于构造请求:
|
||||
// 不进状态 JSON——每次推进都按当时的策略重新解析,改配置无需重发本轮。
|
||||
StepLimits cloudAgentStepLimits `json:"-"`
|
||||
// ImageInspectCounts 记录本轮内每张图被查看的次数,用于"同一张图不要反复看"的护栏。
|
||||
ImageInspectCounts map[string]int `json:"imageInspectCounts,omitempty"`
|
||||
// PendingVisualNodeID 是刚看过、还没写观察的那张图;模型下一次输出正文时把观察记到锚点。
|
||||
PendingVisualNodeID string `json:"pendingVisualNodeId,omitempty"`
|
||||
// PendingImageInspections 暂存"本批还有工具结果没入历史"的看图结果,等整批 tool
|
||||
// 结果都入历史后合并成一条 user 图片消息(见 cloudAgentFlushPendingImages)。
|
||||
//
|
||||
// 它必须进检查点,不能标 `json:"-"`:一次 advanceCloudAgent 只执行一个工具调用
|
||||
// (advanceCloudAgentTool 执行完就 return,下一批调用走下一次转移),而每次转移都
|
||||
// 从 StateJSON 重新解码(cloudAgentDecode)。进程内字段在下一个调用到来时必然为空,
|
||||
// 缓冲就白缓冲了。载荷只有回执与签名链接,几十字节级。
|
||||
PendingImageInspections []cloudAgentImageInspection `json:"pendingImageInspections,omitempty"`
|
||||
}
|
||||
|
||||
type cloudAgentTransientReference struct {
|
||||
@@ -414,6 +426,14 @@ func cloudAgentSave(run *model.CloudAgentExecution, state *cloudAgentRuntime) er
|
||||
if run == nil || state == nil {
|
||||
return errors.New("Agent runtime state is missing")
|
||||
}
|
||||
// 轮内唯一裁剪 = 图片:超出保留轮次的看图结果换成文字回执(正文一律保留)。
|
||||
// 上游每一步都会重新读取历史里的图片并按视觉 token 计费,保留整段历史既贵又没有新信息。
|
||||
if changed, pruned := cloudAgentPruneInspectedImages(&state.Canonical, state.cloudAgentVisualNotes()); changed && run.ID != "" {
|
||||
state.event(run.ID, "context_images_pruned", map[string]any{
|
||||
"prunedImages": pruned, "retentionRounds": cloudAgentImageRetentionRounds,
|
||||
"text": "已把超出保留轮次的看图结果移出模型上下文(保留文字回执与 nodeId)",
|
||||
})
|
||||
}
|
||||
if run.ID != "" {
|
||||
if err := validateCloudAgentRuntime(run, state); err != nil {
|
||||
return fmt.Errorf("%w: %v", errCloudAgentCheckpoint, err)
|
||||
@@ -694,6 +714,9 @@ func (s *Service) advanceCloudAgent(run *model.CloudAgentExecution) (err error)
|
||||
}
|
||||
if result.Text != "" {
|
||||
state.event(run.ID, "assistant_message", map[string]any{"messageId": task.ID, "text": result.Text})
|
||||
// 刚看过图时,模型在正文里写下的那句话就是可复用的视觉证据:
|
||||
// 推理内容不回灌上下文,只有正文留得下来。
|
||||
state.recordCloudAgentVisualNote(result.Text)
|
||||
if len(calls) == 0 {
|
||||
state.Canonical.Messages = append(state.Canonical.Messages, map[string]any{"role": "assistant", "content": result.Text})
|
||||
}
|
||||
@@ -731,6 +754,11 @@ func (s *Service) advanceCloudAgent(run *model.CloudAgentExecution) (err error)
|
||||
if state.CallIndex < len(state.Calls) {
|
||||
return s.advanceCloudAgentTool(run, &state)
|
||||
}
|
||||
// 兜底 flush:本批调用都执行完了(无论最后一个调用是不是看图、有没有被中断),
|
||||
// 缓冲里的图片必须在这里合并成一条 user 消息落到全部 tool 结果之后。少了这一步,
|
||||
// "看图不是最后一个调用"的批次会把图片永久丢掉,而对应的 tool 回执已经在历史里。
|
||||
// 幂等:正常路径(最后一个调用就是看图)已经在 cloudAgentRecordToolResult 里 flush 过。
|
||||
cloudAgentFlushPendingImages(&state)
|
||||
contextBudget := s.cloudAgentContextBudgetForRequest(state.Request)
|
||||
if compactCloudAgentContext(&state.Canonical, contextBudget) {
|
||||
// Evicted read bodies must be obtainable again after compaction.
|
||||
@@ -741,6 +769,14 @@ func (s *Service) advanceCloudAgent(run *model.CloudAgentExecution) (err error)
|
||||
return s.failCloudAgent(run, &state, fmt.Sprintf("达到 %d 次模型调用上限,本轮已停止", stepLimit))
|
||||
}
|
||||
cloudAgentDrainInterjections(run.ID, &state)
|
||||
// 轮内唯一裁剪 = 图片:超出保留轮次的看图结果换成文字回执(正文一律保留)。
|
||||
// 它必须在压缩判定之前跑:图片是最贵的一类内容,先移出再评估 token 压力才有意义。
|
||||
if changed, pruned := cloudAgentPruneInspectedImages(&state.Canonical, state.cloudAgentVisualNotes()); changed {
|
||||
state.event(run.ID, "context_images_pruned", map[string]any{
|
||||
"prunedImages": pruned, "retentionRounds": cloudAgentImageRetentionRounds,
|
||||
"text": "图片裁剪:移出超出保留轮次的看图结果",
|
||||
})
|
||||
}
|
||||
canonical, contextErr := s.cloudAgentModelContext(run, &state, contextBudget)
|
||||
if contextErr != nil {
|
||||
var appErr *AppError
|
||||
@@ -1042,6 +1078,30 @@ func cloudAgentToolResult(runID string, state *cloudAgentRuntime, call cloudAgen
|
||||
} else {
|
||||
payload["text"] = "工具执行成功"
|
||||
}
|
||||
if inspection, ok := result.(cloudAgentImageInspection); ok && err == nil {
|
||||
receipt, _ := json.Marshal(inspection.Receipt)
|
||||
payload["result"] = inspection.Receipt
|
||||
state.event(runID, kind, payload)
|
||||
// tool 角色只接受字符串内容(四种上游图式都是纯文本),因此工具回执照常入历史,
|
||||
// 图片另起一条 user 消息携带,并显式标注为数据而非指令。
|
||||
state.Canonical.Messages = append(state.Canonical.Messages,
|
||||
map[string]any{"role": "tool", "tool_call_id": call.ID, "content": string(receipt)})
|
||||
// 重复查看时只回执文字(ImageURL 为空),不再附图。
|
||||
if strings.TrimSpace(inspection.ImageURL) != "" {
|
||||
cloudAgentStageImageInspection(state, inspection)
|
||||
}
|
||||
// 一批里可能有多个调用(模型一次发起 parallel tool calls),上游要求
|
||||
// assistant(tool_calls) 之后紧跟每一个 tool_call_id 的 tool 消息,所以图片
|
||||
// 不能插在 tool 结果之间。只有本批最后一个调用执行完,才把整批缓冲合并成
|
||||
// 一条 user 消息追加在全部 tool 结果之后。
|
||||
if state.CallIndex+1 >= len(state.Calls) {
|
||||
cloudAgentFlushPendingImages(state)
|
||||
}
|
||||
state.CallIndex++
|
||||
state.Approval = nil
|
||||
// 看图是只读成功路径,不占自动纠错名额(那名额只给写/生成类工具的预执行参数错误)。
|
||||
return false
|
||||
}
|
||||
exhausted := cloudAgentTrackToolRepair(runID, state, call, result, err, payload)
|
||||
raw, _ := json.Marshal(result)
|
||||
payload["result"] = result
|
||||
@@ -1215,6 +1275,12 @@ func (s *Service) advanceCloudAgentTool(run *model.CloudAgentExecution, state *c
|
||||
modelList, modelListErr = s.cloudAgentModelList(intent)
|
||||
}
|
||||
}
|
||||
// 看图需要读取资源并签发链接,放在事务外完成,避免把网络/文件 IO 塞进写事务。
|
||||
var inspectionResult any
|
||||
var inspectionErr error
|
||||
if allowed && call.Function.Name == "canvas_inspect_image" && state.Request.VisionEnabled {
|
||||
inspectionResult, inspectionErr = s.prepareCloudAgentImageInspection(run.UserID, state.Request.CanvasID, state, call)
|
||||
}
|
||||
// Skill reads use the domain repository and filesystem, not the checkpoint
|
||||
// transaction's connection. Read first to avoid nesting DB reads on SQLite.
|
||||
var skillResult any
|
||||
@@ -1245,6 +1311,13 @@ func (s *Service) advanceCloudAgentTool(run *model.CloudAgentExecution, state *c
|
||||
result, toolErr = applyCloudAgentBatchTableMutation(repo, run.UserID, state.Request.CanvasID, call, policy, cloudAgentCanvasEventRecorder(run.ID, state))
|
||||
case call.Function.Name == "model_list":
|
||||
result, toolErr = modelList, modelListErr
|
||||
case call.Function.Name == "canvas_inspect_image":
|
||||
result, toolErr = inspectionResult, inspectionErr
|
||||
if toolErr == nil && inspectionResult != nil {
|
||||
if inspection, ok := inspectionResult.(cloudAgentImageInspection); ok {
|
||||
state.markCanvasAssetInspected(stringValue(inspection.Receipt["nodeId"]))
|
||||
}
|
||||
}
|
||||
case call.Function.Name == "skill_read_file", call.Function.Name == "image_annotation_render":
|
||||
result, toolErr = skillResult, skillErr
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user