mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-18 03:57:06 +08:00
v0.3.8
This commit is contained in:
@@ -0,0 +1,871 @@
|
||||
package forwarder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"cursor/gen/agentv1"
|
||||
modeladapter "cursor/internal/backend/agent/model"
|
||||
)
|
||||
|
||||
const todoSectionReminderMessage = "<system_reminder>\nYou are currently under the todo section, be sure to track tasks and do not forget to update.\n</system_reminder>"
|
||||
|
||||
const (
|
||||
promptContextSourceStructuredCurrentPlan = "structured_state/current_plan"
|
||||
promptContextSourceStructuredTodoList = "structured_state/todo_list"
|
||||
promptContextSourceStructuredTodoReminder = "structured_state/todo_reminder"
|
||||
)
|
||||
|
||||
const todoUnsafeReplaceBaseError = "todo update rejected: merge=false may only omit completed or cancelled todos; use merge=true for incremental updates or include every active todo"
|
||||
|
||||
type structuredConversationState struct {
|
||||
PlanText string
|
||||
HasPlan bool
|
||||
Plans map[string]*agentv1.PlanRegistryEntry
|
||||
Todos []*agentv1.TodoItem
|
||||
HasTodos bool
|
||||
}
|
||||
|
||||
func projectConversationStructuredState(conversation *ConversationFile) (structuredConversationState, error) {
|
||||
state := structuredConversationState{}
|
||||
if conversation == nil {
|
||||
return state, nil
|
||||
}
|
||||
for _, entry := range checkpointProjectionEntries(conversation.Entries) {
|
||||
switch strings.TrimSpace(entry.Kind) {
|
||||
case "runtime_state":
|
||||
var payload runtimeStateEntryPayload
|
||||
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
||||
return structuredConversationState{}, fmt.Errorf("decode runtime_state entry: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(payload.PlanText) != "" {
|
||||
state.PlanText = strings.TrimSpace(payload.PlanText)
|
||||
state.HasPlan = true
|
||||
}
|
||||
if len(payload.Plans) > 0 {
|
||||
state.Plans = clonePlanRegistryEntries(payload.Plans)
|
||||
}
|
||||
if len(payload.Todos) > 0 {
|
||||
state.Todos = cloneTodoItems(payload.Todos)
|
||||
state.HasTodos = true
|
||||
}
|
||||
continue
|
||||
case "tool_result":
|
||||
default:
|
||||
continue
|
||||
}
|
||||
var payload toolResultEntryPayload
|
||||
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
|
||||
return structuredConversationState{}, fmt.Errorf("decode structured state tool_result: %w", err)
|
||||
}
|
||||
if len(payload.ToolCall) == 0 {
|
||||
continue
|
||||
}
|
||||
toolCall := &agentv1.ToolCall{}
|
||||
if err := protojson.Unmarshal(payload.ToolCall, toolCall); err != nil {
|
||||
return structuredConversationState{}, fmt.Errorf("decode structured state tool_call: %w", err)
|
||||
}
|
||||
entryTime := entry.CreatedAt
|
||||
switch item := toolCall.GetTool().(type) {
|
||||
case *agentv1.ToolCall_CreatePlanToolCall:
|
||||
createPlanToolCall := item.CreatePlanToolCall
|
||||
if createPlanToolCall == nil || createPlanToolCall.GetResult().GetSuccess() == nil {
|
||||
continue
|
||||
}
|
||||
planURI := strings.TrimSpace(createPlanToolCall.GetResult().GetPlanUri())
|
||||
if planURI == "" {
|
||||
continue
|
||||
}
|
||||
state.PlanText = strings.TrimSpace(createPlanToolCall.GetArgs().GetPlan())
|
||||
state.HasPlan = true
|
||||
state.Plans = upsertCurrentPlanRegistryEntry(state.Plans, planURI)
|
||||
todos, err := normalizeTodoItems(flattenCreatePlanTodos(createPlanToolCall.GetArgs()), entryTime, false)
|
||||
if err != nil {
|
||||
return structuredConversationState{}, err
|
||||
}
|
||||
state.Todos = todos
|
||||
state.HasTodos = true
|
||||
case *agentv1.ToolCall_UpdateTodosToolCall:
|
||||
updateTodosToolCall := item.UpdateTodosToolCall
|
||||
if updateTodosToolCall == nil || updateTodosToolCall.GetResult().GetSuccess() == nil {
|
||||
continue
|
||||
}
|
||||
success := updateTodosToolCall.GetResult().GetSuccess()
|
||||
todos, err := normalizeTodoItems(success.GetTodos(), entryTime, false)
|
||||
if err != nil {
|
||||
return structuredConversationState{}, err
|
||||
}
|
||||
if !success.GetWasMerge() && len(missingActiveTodoReplacementIDs(state.Todos, todos)) > 0 {
|
||||
todos, err = mergeTodoItems(state.Todos, todos, entryTime)
|
||||
if err != nil {
|
||||
return structuredConversationState{}, err
|
||||
}
|
||||
}
|
||||
state.Todos = todos
|
||||
state.HasTodos = true
|
||||
}
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func refreshConversationRuntimeState(conversation *ConversationFile) error {
|
||||
if conversation == nil {
|
||||
return nil
|
||||
}
|
||||
state, err := projectConversationStructuredState(conversation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conversation.CurrentPlanText = ""
|
||||
conversation.CurrentPlans = nil
|
||||
conversation.CurrentTodos = nil
|
||||
if state.HasPlan && strings.TrimSpace(state.PlanText) != "" {
|
||||
conversation.CurrentPlanText = strings.TrimSpace(state.PlanText)
|
||||
}
|
||||
if len(state.Plans) > 0 {
|
||||
conversation.CurrentPlans = clonePlanRegistryEntries(state.Plans)
|
||||
}
|
||||
if state.HasTodos && len(state.Todos) > 0 {
|
||||
conversation.CurrentTodos = cloneTodoItems(state.Todos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildStructuredStatePromptContexts(conversation *ConversationFile) ([]PromptContextMessage, []modeladapter.Message, error) {
|
||||
state, err := projectConversationStructuredState(conversation)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
contexts := make([]PromptContextMessage, 0, 2)
|
||||
tailMessages := make([]modeladapter.Message, 0, 1)
|
||||
if state.HasPlan && strings.TrimSpace(state.PlanText) != "" {
|
||||
contexts = append(contexts, newPromptContextMessage(
|
||||
promptContextSourceStructuredCurrentPlan,
|
||||
modeladapter.Message{
|
||||
Role: "user",
|
||||
Content: "<current_plan>\n" + strings.TrimSpace(state.PlanText) + "\n</current_plan>",
|
||||
},
|
||||
false,
|
||||
))
|
||||
}
|
||||
if state.HasTodos {
|
||||
if todoText := renderTodoList(state.Todos); todoText != "" {
|
||||
contexts = append(contexts, newPromptContextMessage(
|
||||
promptContextSourceStructuredTodoList,
|
||||
modeladapter.Message{
|
||||
Role: "user",
|
||||
Content: "<todo_list>\n" + todoText + "\n</todo_list>",
|
||||
},
|
||||
false,
|
||||
))
|
||||
tailMessages = append(tailMessages, modeladapter.Message{
|
||||
Role: "user",
|
||||
Content: todoSectionReminderMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
return contexts, tailMessages, nil
|
||||
}
|
||||
|
||||
func upsertCurrentPlanRegistryEntry(plans map[string]*agentv1.PlanRegistryEntry, planURI string) map[string]*agentv1.PlanRegistryEntry {
|
||||
trimmedURI := strings.TrimSpace(planURI)
|
||||
if trimmedURI == "" {
|
||||
return clonePlanRegistryEntries(plans)
|
||||
}
|
||||
next := clonePlanRegistryEntries(plans)
|
||||
if next == nil {
|
||||
next = make(map[string]*agentv1.PlanRegistryEntry, 1)
|
||||
}
|
||||
key := "current"
|
||||
if _, ok := next[key]; !ok && len(next) == 1 {
|
||||
for existingKey := range next {
|
||||
key = existingKey
|
||||
}
|
||||
}
|
||||
next[key] = &agentv1.PlanRegistryEntry{
|
||||
Id: key,
|
||||
Path: trimmedURI,
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func clonePlanRegistryEntries(items map[string]*agentv1.PlanRegistryEntry) map[string]*agentv1.PlanRegistryEntry {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[string]*agentv1.PlanRegistryEntry, len(items))
|
||||
for key, item := range items {
|
||||
trimmedKey := strings.TrimSpace(key)
|
||||
if trimmedKey == "" || item == nil {
|
||||
continue
|
||||
}
|
||||
cloned[trimmedKey] = &agentv1.PlanRegistryEntry{
|
||||
Id: strings.TrimSpace(item.GetId()),
|
||||
Path: strings.TrimSpace(item.GetPath()),
|
||||
}
|
||||
if cloned[trimmedKey].GetId() == "" {
|
||||
cloned[trimmedKey].Id = trimmedKey
|
||||
}
|
||||
}
|
||||
if len(cloned) == 0 {
|
||||
return nil
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func encodeConversationPlanBytes(plan string) []byte {
|
||||
text := strings.TrimSpace(plan)
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
payload, err := proto.Marshal(&agentv1.ConversationPlan{Plan: text})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func encodeConversationTodoBytes(items []*agentv1.TodoItem) [][]byte {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
encoded := make([][]byte, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
payload, err := proto.Marshal(cloneTodoItem(item))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
encoded = append(encoded, payload)
|
||||
}
|
||||
if len(encoded) == 0 {
|
||||
return nil
|
||||
}
|
||||
return encoded
|
||||
}
|
||||
|
||||
func flattenCreatePlanTodos(args *agentv1.CreatePlanArgs) []*agentv1.TodoItem {
|
||||
if args == nil {
|
||||
return nil
|
||||
}
|
||||
if len(args.GetTodos()) > 0 {
|
||||
return cloneTodoItems(args.GetTodos())
|
||||
}
|
||||
items := make([]*agentv1.TodoItem, 0, len(args.GetPhases()))
|
||||
for _, phase := range args.GetPhases() {
|
||||
if phase == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, cloneTodoItems(phase.GetTodos())...)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
type decodedUpdateTodosArgs struct {
|
||||
Args *agentv1.UpdateTodosArgs
|
||||
MergeSet bool
|
||||
}
|
||||
|
||||
func decodeUpdateTodosArgsJSON(raw []byte) (*agentv1.UpdateTodosArgs, error) {
|
||||
decoded, err := decodeUpdateTodosArgsJSONWithPresence(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decoded.Args, nil
|
||||
}
|
||||
|
||||
func decodeUpdateTodosArgsJSONWithPresence(raw []byte) (decodedUpdateTodosArgs, error) {
|
||||
payload, err := decodeJSONObject(raw)
|
||||
if err != nil {
|
||||
return decodedUpdateTodosArgs{}, err
|
||||
}
|
||||
todos, err := decodeTodoItemsValue(valueByAlias(payload, "todos"), false)
|
||||
if err != nil {
|
||||
return decodedUpdateTodosArgs{}, err
|
||||
}
|
||||
mergeValue, mergeSet := valueByAliasWithPresence(payload, "merge")
|
||||
return decodedUpdateTodosArgs{
|
||||
Args: &agentv1.UpdateTodosArgs{
|
||||
Todos: todos,
|
||||
Merge: boolValue(mergeValue),
|
||||
},
|
||||
MergeSet: mergeSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeReadTodosArgsJSON(raw []byte) (*agentv1.ReadTodosArgs, error) {
|
||||
payload, err := decodeJSONObject(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statuses, err := decodeTodoStatusesValue(valueByAlias(payload, "status_filter", "statusFilter"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &agentv1.ReadTodosArgs{
|
||||
StatusFilter: statuses,
|
||||
IdFilter: stringSliceValue(valueByAlias(payload, "id_filter", "idFilter")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeJSONObject(raw []byte) (map[string]any, error) {
|
||||
if len(raw) == 0 {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if payload == nil {
|
||||
payload = map[string]any{}
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func decodeTodoItemsValue(value any, validate bool) ([]*agentv1.TodoItem, error) {
|
||||
items, ok := value.([]any)
|
||||
if !ok || len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
todos := make([]*agentv1.TodoItem, 0, len(items))
|
||||
for _, rawItem := range items {
|
||||
item, ok := rawItem.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("todo item must be an object")
|
||||
}
|
||||
status, err := todoStatusFromValue(valueByAlias(item, "status"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
todo := &agentv1.TodoItem{
|
||||
Id: strings.TrimSpace(stringValue(valueByAlias(item, "id"))),
|
||||
Content: strings.TrimSpace(stringValue(valueByAlias(item, "content"))),
|
||||
Status: status,
|
||||
CreatedAt: int64Value(valueByAlias(item, "created_at", "createdAt")),
|
||||
UpdatedAt: int64Value(valueByAlias(item, "updated_at", "updatedAt")),
|
||||
Dependencies: stringSliceValue(valueByAlias(item, "dependencies")),
|
||||
}
|
||||
if validate {
|
||||
if todo.GetId() == "" {
|
||||
return nil, fmt.Errorf("todo id is required")
|
||||
}
|
||||
if todo.GetContent() == "" {
|
||||
return nil, fmt.Errorf("todo content is required")
|
||||
}
|
||||
}
|
||||
todos = append(todos, todo)
|
||||
}
|
||||
return todos, nil
|
||||
}
|
||||
|
||||
func decodeTodoStatusesValue(value any) ([]agentv1.TodoStatus, error) {
|
||||
items, ok := value.([]any)
|
||||
if !ok || len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
statuses := make([]agentv1.TodoStatus, 0, len(items))
|
||||
for _, item := range items {
|
||||
status, err := todoStatusFromValue(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status == agentv1.TodoStatus_TODO_STATUS_UNSPECIFIED {
|
||||
continue
|
||||
}
|
||||
statuses = append(statuses, status)
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func valueByAlias(values map[string]any, aliases ...string) any {
|
||||
value, _ := valueByAliasWithPresence(values, aliases...)
|
||||
return value
|
||||
}
|
||||
|
||||
func valueByAliasWithPresence(values map[string]any, aliases ...string) (any, bool) {
|
||||
for _, alias := range aliases {
|
||||
if alias == "" {
|
||||
continue
|
||||
}
|
||||
if value, ok := values[alias]; ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func stringValue(value any) string {
|
||||
text, ok := value.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
func boolValue(value any) bool {
|
||||
switch item := value.(type) {
|
||||
case bool:
|
||||
return item
|
||||
case string:
|
||||
parsed, _ := strconv.ParseBool(strings.TrimSpace(item))
|
||||
return parsed
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func int64Value(value any) int64 {
|
||||
switch item := value.(type) {
|
||||
case float64:
|
||||
return int64(item)
|
||||
case float32:
|
||||
return int64(item)
|
||||
case int64:
|
||||
return item
|
||||
case int32:
|
||||
return int64(item)
|
||||
case int:
|
||||
return int64(item)
|
||||
case json.Number:
|
||||
parsed, _ := item.Int64()
|
||||
return parsed
|
||||
case string:
|
||||
parsed, _ := strconv.ParseInt(strings.TrimSpace(item), 10, 64)
|
||||
return parsed
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func stringSliceValue(value any) []string {
|
||||
items, ok := value.([]any)
|
||||
if !ok || len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
text := stringValue(item)
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, text)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func todoStatusFromValue(value any) (agentv1.TodoStatus, error) {
|
||||
switch item := value.(type) {
|
||||
case nil:
|
||||
return agentv1.TodoStatus_TODO_STATUS_UNSPECIFIED, nil
|
||||
case float64:
|
||||
return agentv1.TodoStatus(int32(item)), nil
|
||||
case float32:
|
||||
return agentv1.TodoStatus(int32(item)), nil
|
||||
case int:
|
||||
return agentv1.TodoStatus(item), nil
|
||||
case int32:
|
||||
return agentv1.TodoStatus(item), nil
|
||||
case int64:
|
||||
return agentv1.TodoStatus(item), nil
|
||||
case string:
|
||||
return todoStatusFromString(item)
|
||||
default:
|
||||
return agentv1.TodoStatus_TODO_STATUS_UNSPECIFIED, fmt.Errorf("unsupported todo status type %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
func todoStatusFromString(raw string) (agentv1.TodoStatus, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case "", "unspecified", "todo_status_unspecified":
|
||||
return agentv1.TodoStatus_TODO_STATUS_UNSPECIFIED, nil
|
||||
case "pending", "todo_status_pending":
|
||||
return agentv1.TodoStatus_TODO_STATUS_PENDING, nil
|
||||
case "in_progress", "in-progress", "inprogress", "todo_status_in_progress":
|
||||
return agentv1.TodoStatus_TODO_STATUS_IN_PROGRESS, nil
|
||||
case "completed", "complete", "todo_status_completed":
|
||||
return agentv1.TodoStatus_TODO_STATUS_COMPLETED, nil
|
||||
case "cancelled", "canceled", "todo_status_cancelled":
|
||||
return agentv1.TodoStatus_TODO_STATUS_CANCELLED, nil
|
||||
default:
|
||||
return agentv1.TodoStatus_TODO_STATUS_UNSPECIFIED, fmt.Errorf("unsupported todo status %q", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func todoStatusLabel(status agentv1.TodoStatus) string {
|
||||
switch status {
|
||||
case agentv1.TodoStatus_TODO_STATUS_PENDING:
|
||||
return "pending"
|
||||
case agentv1.TodoStatus_TODO_STATUS_IN_PROGRESS:
|
||||
return "in_progress"
|
||||
case agentv1.TodoStatus_TODO_STATUS_COMPLETED:
|
||||
return "completed"
|
||||
case agentv1.TodoStatus_TODO_STATUS_CANCELLED:
|
||||
return "cancelled"
|
||||
default:
|
||||
return "unspecified"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeTodoItems(items []*agentv1.TodoItem, baseTime time.Time, validate bool) ([]*agentv1.TodoItem, error) {
|
||||
if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
baseMillis := timestampMillis(baseTime)
|
||||
normalized := make([]*agentv1.TodoItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
next := cloneTodoItem(item)
|
||||
next.Id = strings.TrimSpace(next.GetId())
|
||||
next.Content = strings.TrimSpace(next.GetContent())
|
||||
if validate {
|
||||
if next.GetId() == "" {
|
||||
return nil, fmt.Errorf("todo id is required")
|
||||
}
|
||||
if next.GetContent() == "" {
|
||||
return nil, fmt.Errorf("todo content is required")
|
||||
}
|
||||
}
|
||||
if next.GetStatus() == agentv1.TodoStatus_TODO_STATUS_UNSPECIFIED {
|
||||
next.Status = agentv1.TodoStatus_TODO_STATUS_PENDING
|
||||
}
|
||||
if next.GetCreatedAt() <= 0 {
|
||||
next.CreatedAt = baseMillis
|
||||
}
|
||||
if next.GetUpdatedAt() <= 0 {
|
||||
next.UpdatedAt = next.GetCreatedAt()
|
||||
}
|
||||
normalized = append(normalized, next)
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func mergeTodoItems(existing []*agentv1.TodoItem, updates []*agentv1.TodoItem, baseTime time.Time) ([]*agentv1.TodoItem, error) {
|
||||
result, err := normalizeTodoItems(existing, baseTime, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
indexByID := make(map[string]int, len(result))
|
||||
for index, item := range result {
|
||||
if item == nil || strings.TrimSpace(item.GetId()) == "" {
|
||||
continue
|
||||
}
|
||||
indexByID[strings.TrimSpace(item.GetId())] = index
|
||||
}
|
||||
nowMillis := timestampMillis(baseTime)
|
||||
for _, update := range updates {
|
||||
if update == nil {
|
||||
continue
|
||||
}
|
||||
id := strings.TrimSpace(update.GetId())
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("todo id is required")
|
||||
}
|
||||
content := strings.TrimSpace(update.GetContent())
|
||||
if index, ok := indexByID[id]; ok {
|
||||
current := result[index]
|
||||
next := cloneTodoItem(current)
|
||||
next.Id = id
|
||||
if content != "" {
|
||||
next.Content = content
|
||||
}
|
||||
if update.GetStatus() != agentv1.TodoStatus_TODO_STATUS_UNSPECIFIED {
|
||||
next.Status = update.GetStatus()
|
||||
}
|
||||
if len(update.GetDependencies()) > 0 {
|
||||
next.Dependencies = append([]string(nil), update.GetDependencies()...)
|
||||
}
|
||||
if next.GetContent() == "" {
|
||||
return nil, fmt.Errorf("todo content is required")
|
||||
}
|
||||
if next.GetStatus() == agentv1.TodoStatus_TODO_STATUS_UNSPECIFIED {
|
||||
next.Status = agentv1.TodoStatus_TODO_STATUS_PENDING
|
||||
}
|
||||
next.CreatedAt = current.GetCreatedAt()
|
||||
if next.GetCreatedAt() <= 0 {
|
||||
next.CreatedAt = nowMillis
|
||||
}
|
||||
next.UpdatedAt = nowMillis
|
||||
result[index] = next
|
||||
continue
|
||||
}
|
||||
next := cloneTodoItem(update)
|
||||
next.Id = id
|
||||
next.Content = content
|
||||
if next.GetContent() == "" {
|
||||
return nil, fmt.Errorf("todo content is required for new todo")
|
||||
}
|
||||
if next.GetStatus() == agentv1.TodoStatus_TODO_STATUS_UNSPECIFIED {
|
||||
next.Status = agentv1.TodoStatus_TODO_STATUS_PENDING
|
||||
}
|
||||
if next.GetCreatedAt() <= 0 {
|
||||
next.CreatedAt = nowMillis
|
||||
}
|
||||
if next.GetUpdatedAt() <= 0 {
|
||||
next.UpdatedAt = next.GetCreatedAt()
|
||||
}
|
||||
indexByID[id] = len(result)
|
||||
result = append(result, next)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func missingActiveTodoReplacementIDs(existing []*agentv1.TodoItem, replacement []*agentv1.TodoItem) []string {
|
||||
existingIDs := make(map[string]struct{}, len(existing))
|
||||
for _, item := range existing {
|
||||
if item == nil || isTerminalTodoStatus(item.GetStatus()) {
|
||||
continue
|
||||
}
|
||||
id := strings.TrimSpace(item.GetId())
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
existingIDs[id] = struct{}{}
|
||||
}
|
||||
if len(existingIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
replacementIDs := make(map[string]struct{}, len(replacement))
|
||||
for _, item := range replacement {
|
||||
id := strings.TrimSpace(item.GetId())
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
replacementIDs[id] = struct{}{}
|
||||
}
|
||||
missing := make([]string, 0)
|
||||
for id := range existingIDs {
|
||||
if _, ok := replacementIDs[id]; !ok {
|
||||
missing = append(missing, id)
|
||||
}
|
||||
}
|
||||
sort.Strings(missing)
|
||||
return missing
|
||||
}
|
||||
|
||||
func isTerminalTodoStatus(status agentv1.TodoStatus) bool {
|
||||
switch status {
|
||||
case agentv1.TodoStatus_TODO_STATUS_COMPLETED,
|
||||
agentv1.TodoStatus_TODO_STATUS_CANCELLED:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func unsafeTodoReplaceError(missingIDs []string) string {
|
||||
if len(missingIDs) == 0 {
|
||||
return todoUnsafeReplaceBaseError
|
||||
}
|
||||
return fmt.Sprintf("%s; missing active todo ids: %s", todoUnsafeReplaceBaseError, strings.Join(missingIDs, ", "))
|
||||
}
|
||||
|
||||
func filterTodoItems(items []*agentv1.TodoItem, statuses []agentv1.TodoStatus, ids []string) []*agentv1.TodoItem {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
statusFilter := make(map[agentv1.TodoStatus]struct{}, len(statuses))
|
||||
for _, status := range statuses {
|
||||
statusFilter[status] = struct{}{}
|
||||
}
|
||||
idFilter := make(map[string]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
trimmed := strings.TrimSpace(id)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
idFilter[trimmed] = struct{}{}
|
||||
}
|
||||
filtered := make([]*agentv1.TodoItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
if len(statusFilter) > 0 {
|
||||
if _, ok := statusFilter[item.GetStatus()]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(idFilter) > 0 {
|
||||
if _, ok := idFilter[strings.TrimSpace(item.GetId())]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
filtered = append(filtered, cloneTodoItem(item))
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func renderTodoList(items []*agentv1.TodoItem) string {
|
||||
if len(items) == 0 {
|
||||
return ""
|
||||
}
|
||||
lines := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("- [%s] %s: %s", todoStatusLabel(item.GetStatus()), strings.TrimSpace(item.GetId()), strings.TrimSpace(item.GetContent())))
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func buildUpdateTodosToolCall(args *agentv1.UpdateTodosArgs, result *agentv1.UpdateTodosResult) *agentv1.ToolCall {
|
||||
return &agentv1.ToolCall{
|
||||
Tool: &agentv1.ToolCall_UpdateTodosToolCall{
|
||||
UpdateTodosToolCall: &agentv1.UpdateTodosToolCall{
|
||||
Args: args,
|
||||
Result: result,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildUpdateTodosErrorResult(args *agentv1.UpdateTodosArgs, message string) *agentv1.UpdateTodosResult {
|
||||
return &agentv1.UpdateTodosResult{
|
||||
Result: &agentv1.UpdateTodosResult_Error{
|
||||
Error: &agentv1.UpdateTodosError{Error: strings.TrimSpace(message)},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildReadTodosToolCall(args *agentv1.ReadTodosArgs, result *agentv1.ReadTodosResult) *agentv1.ToolCall {
|
||||
return &agentv1.ToolCall{
|
||||
Tool: &agentv1.ToolCall_ReadTodosToolCall{
|
||||
ReadTodosToolCall: &agentv1.ReadTodosToolCall{
|
||||
Args: args,
|
||||
Result: result,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func summarizeUpdateTodosResult(result *agentv1.UpdateTodosResult) string {
|
||||
if result == nil {
|
||||
return "todo update missing"
|
||||
}
|
||||
switch item := result.GetResult().(type) {
|
||||
case *agentv1.UpdateTodosResult_Success:
|
||||
return fmt.Sprintf("todo update success count=%d merge=%t", item.Success.GetTotalCount(), item.Success.GetWasMerge())
|
||||
case *agentv1.UpdateTodosResult_Error:
|
||||
return strings.TrimSpace(item.Error.GetError())
|
||||
default:
|
||||
return "todo update finished"
|
||||
}
|
||||
}
|
||||
|
||||
func summarizeReadTodosResult(result *agentv1.ReadTodosResult) string {
|
||||
if result == nil {
|
||||
return "todo read missing"
|
||||
}
|
||||
switch item := result.GetResult().(type) {
|
||||
case *agentv1.ReadTodosResult_Success:
|
||||
return fmt.Sprintf("todo read success count=%d", item.Success.GetTotalCount())
|
||||
case *agentv1.ReadTodosResult_Error:
|
||||
return strings.TrimSpace(item.Error.GetError())
|
||||
default:
|
||||
return "todo read finished"
|
||||
}
|
||||
}
|
||||
|
||||
func buildTaskArgsFromJSON(raw []byte) *agentv1.TaskArgs {
|
||||
payload, err := decodeJSONObject(raw)
|
||||
if err != nil {
|
||||
return &agentv1.TaskArgs{}
|
||||
}
|
||||
return buildTaskArgsFromMap(payload)
|
||||
}
|
||||
|
||||
func buildTaskArgsFromMap(payload map[string]any) *agentv1.TaskArgs {
|
||||
args := &agentv1.TaskArgs{
|
||||
Description: strings.TrimSpace(stringValue(valueByAlias(payload, "description"))),
|
||||
Prompt: strings.TrimSpace(stringValue(valueByAlias(payload, "prompt"))),
|
||||
SubagentType: subagentTypeFromString(
|
||||
strings.TrimSpace(stringValue(valueByAlias(payload, "subagent_type", "subagentType"))),
|
||||
),
|
||||
Attachments: stringSliceValue(valueByAlias(payload, "attachments")),
|
||||
Mode: taskModeFromReadonly(boolValue(valueByAlias(payload, "readonly", "readOnly"))),
|
||||
}
|
||||
if model := strings.TrimSpace(stringValue(valueByAlias(payload, "model"))); model != "" {
|
||||
args.Model = &model
|
||||
}
|
||||
if resume := strings.TrimSpace(stringValue(valueByAlias(payload, "resume"))); resume != "" {
|
||||
args.Resume = &resume
|
||||
}
|
||||
if agentID := strings.TrimSpace(stringValue(valueByAlias(payload, "agent_id", "agentId"))); agentID != "" {
|
||||
args.AgentId = &agentID
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func subagentTypeFromString(raw string) *agentv1.SubagentType {
|
||||
switch strings.TrimSpace(raw) {
|
||||
case "explore":
|
||||
return &agentv1.SubagentType{Type: &agentv1.SubagentType_Explore{Explore: &agentv1.SubagentTypeExplore{}}}
|
||||
case "browser-use", "browserUse":
|
||||
return &agentv1.SubagentType{Type: &agentv1.SubagentType_BrowserUse{BrowserUse: &agentv1.SubagentTypeBrowserUse{}}}
|
||||
case "shell":
|
||||
return &agentv1.SubagentType{Type: &agentv1.SubagentType_Shell{Shell: &agentv1.SubagentTypeShell{}}}
|
||||
case "":
|
||||
return &agentv1.SubagentType{Type: &agentv1.SubagentType_Unspecified{Unspecified: &agentv1.SubagentTypeUnspecified{}}}
|
||||
default:
|
||||
return &agentv1.SubagentType{
|
||||
Type: &agentv1.SubagentType_Custom{
|
||||
Custom: &agentv1.SubagentTypeCustom{Name: strings.TrimSpace(raw)},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func taskModeFromReadonly(readonly bool) agentv1.TaskMode {
|
||||
if readonly {
|
||||
return agentv1.TaskMode_TASK_MODE_PLAN
|
||||
}
|
||||
return agentv1.TaskMode_TASK_MODE_AGENT
|
||||
}
|
||||
|
||||
func cloneTodoItems(items []*agentv1.TodoItem) []*agentv1.TodoItem {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
cloned := make([]*agentv1.TodoItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
cloned = append(cloned, cloneTodoItem(item))
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneTodoItem(item *agentv1.TodoItem) *agentv1.TodoItem {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
return &agentv1.TodoItem{
|
||||
Id: item.GetId(),
|
||||
Content: item.GetContent(),
|
||||
Status: item.GetStatus(),
|
||||
CreatedAt: item.GetCreatedAt(),
|
||||
UpdatedAt: item.GetUpdatedAt(),
|
||||
Dependencies: append([]string(nil), item.GetDependencies()...),
|
||||
}
|
||||
}
|
||||
|
||||
func timestampMillis(baseTime time.Time) int64 {
|
||||
now := baseTime
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
return now.UTC().UnixMilli()
|
||||
}
|
||||
Reference in New Issue
Block a user