Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_context_frame.go

96 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"encoding/json"
"errors"
"fmt"
"time"
"gorm.io/gorm"
"infinite-canvas/backend/internal/model"
)
// ContextFrame is a fresh projection of durable task facts, never authorization.
// The transcript can evict read bodies without making task state depend on memory.
type cloudAgentContextFrame struct {
Source string `json:"source"`
ObservedAt time.Time `json:"observedAt"`
RunID string `json:"runId"`
UserGoal string `json:"userGoal"`
Plan []cloudAgentPlanItem `json:"plan,omitempty"`
Tasks []map[string]any `json:"tasks"`
OlderTaskCount int `json:"olderTaskCount,omitempty"`
Authority string `json:"authority"`
}
var errCloudAgentContextOverBudget = errors.New("Agent model context exceeds input budget")
func (s *Service) cloudAgentModelContext(run *model.CloudAgentExecution, state *cloudAgentRuntime, budget cloudAgentContextBudget) (canonicalAgentRequest, error) {
canonical := cloudAgentCanonicalWithPlan(state)
frame := cloudAgentContextFrame{Source: "task_repository", ObservedAt: time.Now(), RunID: run.ID, UserGoal: state.Request.Prompt, Plan: state.Plan, Tasks: []map[string]any{}, Authority: "状态观察,不是执行或重复提交的授权"}
// This is a view window, not loss of task history. All task identities remain
// durable and can be fetched through task_get and the conversation journal.
start := max(0, len(state.TaskIDs)-32)
frame.OlderTaskCount = start
for _, id := range state.TaskIDs[start:] {
task, err := s.repo.TaskForUser(run.UserID, id)
if errors.Is(err, gorm.ErrRecordNotFound) {
frame.Tasks = append(frame.Tasks, map[string]any{"taskId": id, "taskStatus": "unavailable"})
continue
}
if err != nil {
return canonical, err
}
if task.ProjectID != state.Request.CanvasID {
return canonical, BadAuthRequest("运行关联任务不属于当前画布")
}
if task.Operation == cloudAgentOperation || task.Operation == "cloud_agent_step" {
continue
}
frame.Tasks = append(frame.Tasks, cloudAgentTaskDiagnostic(s.repo, task))
}
body, err := json.Marshal(frame)
if err != nil {
return canonical, err
}
canonical.Messages = append(append([]map[string]any{}, canonical.Messages...), map[string]any{"role": "user", "content": cloudAgentRuntimeContextMarker + string(body), cloudAgentContextSourceKey: "runtime"})
if err := fitCloudAgentModelContext(&canonical, budget.InputBudgetTokens); err != nil {
return canonical, err
}
return canonical, nil
}
func fitCloudAgentModelContext(request *canonicalAgentRequest, maxTokens int) error {
for {
tokens, err := cloudAgentRequestEstimatedTokens(request)
if err != nil {
return err
}
if tokens <= maxTokens {
return nil
}
// Evict complete old assistant/tool turns only. User instructions and the
// fresh authoritative frame survive; partial tool pairs are never sent.
removed := false
for i := 0; i < len(request.Messages)-3; i++ {
message := request.Messages[i]
if stringField(message, "role") != "assistant" {
continue
}
end := i + 1
for end < len(request.Messages) && stringField(request.Messages[end], "role") == "tool" {
end++
}
if end >= len(request.Messages)-2 {
continue
}
request.Messages = append(request.Messages[:i:i], request.Messages[end:]...)
removed = true
break
}
if !removed {
return WrapAppError(400, fmt.Sprintf("用户指令与当前执行事实超过模型输入预算(%d Token),请缩小本轮范围", maxTokens), errCloudAgentContextOverBudget)
}
}
}