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,237 @@
|
||||
package runtimecore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"cursor/gen/agentv1"
|
||||
)
|
||||
|
||||
// DecodeCreatePlanArgsJSON 解析 CreatePlan 参数,并兼容字符串形式的 todo status。
|
||||
func DecodeCreatePlanArgsJSON(raw []byte) (*agentv1.CreatePlanArgs, error) {
|
||||
if len(strings.TrimSpace(string(raw))) == 0 {
|
||||
return &agentv1.CreatePlanArgs{}, nil
|
||||
}
|
||||
|
||||
var direct agentv1.CreatePlanArgs
|
||||
if err := json.Unmarshal(raw, &direct); err == nil {
|
||||
return &direct, nil
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if payload == nil {
|
||||
return &agentv1.CreatePlanArgs{}, nil
|
||||
}
|
||||
|
||||
todos, err := decodeCreatePlanTodoItems(createPlanValueByAlias(payload, "todos"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode todos: %w", err)
|
||||
}
|
||||
phases, err := decodeCreatePlanPhases(createPlanValueByAlias(payload, "phases"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode phases: %w", err)
|
||||
}
|
||||
|
||||
return &agentv1.CreatePlanArgs{
|
||||
Plan: createPlanStringValue(createPlanValueByAlias(payload, "plan")),
|
||||
Overview: createPlanStringValue(createPlanValueByAlias(payload, "overview")),
|
||||
Name: strings.TrimSpace(createPlanStringValue(createPlanValueByAlias(payload, "name"))),
|
||||
IsProject: createPlanBoolValue(createPlanValueByAlias(payload, "is_project", "isProject")),
|
||||
Todos: todos,
|
||||
Phases: phases,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeCreatePlanPhases(value any) ([]*agentv1.Phase, error) {
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
items, ok := value.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("phases must be an array")
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
phases := make([]*agentv1.Phase, 0, len(items))
|
||||
for index, item := range items {
|
||||
object, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("phase %d must be an object", index)
|
||||
}
|
||||
todos, err := decodeCreatePlanTodoItems(createPlanValueByAlias(object, "todos"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("phase %d todos: %w", index, err)
|
||||
}
|
||||
phases = append(phases, &agentv1.Phase{
|
||||
Name: strings.TrimSpace(createPlanStringValue(createPlanValueByAlias(object, "name"))),
|
||||
Todos: todos,
|
||||
})
|
||||
}
|
||||
return phases, nil
|
||||
}
|
||||
|
||||
func decodeCreatePlanTodoItems(value any) ([]*agentv1.TodoItem, error) {
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
items, ok := value.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("todos must be an array")
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
todos := make([]*agentv1.TodoItem, 0, len(items))
|
||||
for index, item := range items {
|
||||
object, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("todo %d must be an object", index)
|
||||
}
|
||||
status, err := decodeCreatePlanTodoStatus(createPlanValueByAlias(object, "status"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("todo %d status: %w", index, err)
|
||||
}
|
||||
todos = append(todos, &agentv1.TodoItem{
|
||||
Id: strings.TrimSpace(createPlanStringValue(createPlanValueByAlias(object, "id"))),
|
||||
Content: strings.TrimSpace(createPlanStringValue(createPlanValueByAlias(object, "content"))),
|
||||
Status: status,
|
||||
CreatedAt: createPlanInt64Value(createPlanValueByAlias(object, "created_at", "createdAt")),
|
||||
UpdatedAt: createPlanInt64Value(createPlanValueByAlias(object, "updated_at", "updatedAt")),
|
||||
Dependencies: createPlanStringSliceValue(createPlanValueByAlias(object, "dependencies")),
|
||||
})
|
||||
}
|
||||
return todos, nil
|
||||
}
|
||||
|
||||
func decodeCreatePlanTodoStatus(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 decodeCreatePlanTodoStatusString(item)
|
||||
default:
|
||||
return agentv1.TodoStatus_TODO_STATUS_UNSPECIFIED, fmt.Errorf("unsupported todo status type %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeCreatePlanTodoStatusString(raw string) (agentv1.TodoStatus, error) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||
if normalized == "" || normalized == "unspecified" || normalized == "todo_status_unspecified" {
|
||||
return agentv1.TodoStatus_TODO_STATUS_UNSPECIFIED, nil
|
||||
}
|
||||
if numeric, err := strconv.ParseInt(normalized, 10, 32); err == nil {
|
||||
return agentv1.TodoStatus(numeric), nil
|
||||
}
|
||||
switch normalized {
|
||||
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 createPlanValueByAlias(payload map[string]any, aliases ...string) any {
|
||||
for _, alias := range aliases {
|
||||
if value, ok := payload[alias]; ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createPlanStringValue(value any) string {
|
||||
text, ok := value.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func createPlanBoolValue(value any) bool {
|
||||
switch item := value.(type) {
|
||||
case bool:
|
||||
return item
|
||||
case string:
|
||||
return strings.EqualFold(strings.TrimSpace(item), "true")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func createPlanInt64Value(value any) int64 {
|
||||
switch item := value.(type) {
|
||||
case float64:
|
||||
return int64(item)
|
||||
case float32:
|
||||
return int64(item)
|
||||
case int:
|
||||
return int64(item)
|
||||
case int32:
|
||||
return int64(item)
|
||||
case int64:
|
||||
return item
|
||||
case string:
|
||||
parsed, err := strconv.ParseInt(strings.TrimSpace(item), 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return parsed
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func createPlanStringSliceValue(value any) []string {
|
||||
switch item := value.(type) {
|
||||
case []string:
|
||||
if len(item) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0, len(item))
|
||||
for _, text := range item {
|
||||
trimmed := strings.TrimSpace(text)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
return result
|
||||
case []any:
|
||||
if len(item) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0, len(item))
|
||||
for _, entry := range item {
|
||||
text := strings.TrimSpace(createPlanStringValue(entry))
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, text)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package runtimecore 定义 runtime/loop、checkpoint、session 之间共享的状态与事件模型。
|
||||
package runtimecore
|
||||
@@ -0,0 +1,110 @@
|
||||
package runtimecore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MCPToolPayload 表示 CallMcpTool 的宽容解码结果。
|
||||
type MCPToolPayload struct {
|
||||
Server string
|
||||
ProviderIdentifier string
|
||||
ToolName string
|
||||
Name string
|
||||
Arguments map[string]any
|
||||
}
|
||||
|
||||
// DecodeMCPToolPayload 解析 CallMcpTool 参数,并兼容字符串化的 arguments 对象。
|
||||
func DecodeMCPToolPayload(raw []byte) (MCPToolPayload, error) {
|
||||
payload := MCPToolPayload{
|
||||
Arguments: make(map[string]any),
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil {
|
||||
return payload, err
|
||||
}
|
||||
if decoded == nil {
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
payload.Server = decodeJSONStringValue(decoded["server"])
|
||||
payload.ProviderIdentifier = decodeJSONStringValue(decoded["providerIdentifier"])
|
||||
payload.ToolName = decodeJSONStringValue(decoded["toolName"])
|
||||
payload.Name = decodeJSONStringValue(decoded["name"])
|
||||
payload.Arguments = decodeJSONObjectLike(decoded["arguments"])
|
||||
if len(payload.Arguments) == 0 {
|
||||
payload.Arguments = decodeJSONObjectLike(decoded["args"])
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// InferMCPServerIdentifier 从 canonical lookup name 中反推出 server identifier。
|
||||
func InferMCPServerIdentifier(name string) string {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
if index := strings.Index(trimmed, "-"); index > 0 {
|
||||
return strings.TrimSpace(trimmed[:index])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// InferMCPToolName 从 canonical lookup name 中反推出 tool name。
|
||||
func InferMCPToolName(serverIdentifier string, name string) string {
|
||||
trimmedName := strings.TrimSpace(name)
|
||||
if trimmedName == "" {
|
||||
return ""
|
||||
}
|
||||
trimmedServer := strings.TrimSpace(serverIdentifier)
|
||||
if trimmedServer != "" && strings.HasPrefix(trimmedName, trimmedServer+"-") {
|
||||
return strings.TrimSpace(strings.TrimPrefix(trimmedName, trimmedServer+"-"))
|
||||
}
|
||||
return trimmedName
|
||||
}
|
||||
|
||||
func decodeJSONStringValue(value any) string {
|
||||
text, ok := value.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
func decodeJSONObjectLike(value any) map[string]any {
|
||||
switch item := value.(type) {
|
||||
case map[string]any:
|
||||
if item == nil {
|
||||
return make(map[string]any)
|
||||
}
|
||||
return item
|
||||
case string:
|
||||
return decodeJSONObjectBytes([]byte(item))
|
||||
case []byte:
|
||||
return decodeJSONObjectBytes(item)
|
||||
case json.RawMessage:
|
||||
return decodeJSONObjectBytes([]byte(item))
|
||||
default:
|
||||
return make(map[string]any)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeJSONObjectBytes(raw []byte) map[string]any {
|
||||
trimmed := strings.TrimSpace(string(raw))
|
||||
if trimmed == "" {
|
||||
return make(map[string]any)
|
||||
}
|
||||
var decoded any
|
||||
if err := json.Unmarshal([]byte(trimmed), &decoded); err != nil {
|
||||
return make(map[string]any)
|
||||
}
|
||||
object, ok := decoded.(map[string]any)
|
||||
if !ok || object == nil {
|
||||
return make(map[string]any)
|
||||
}
|
||||
return object
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
package runtimecore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DecodeArgsMap decodes model-produced built-in tool arguments while preserving
|
||||
// JSON number spellings for lossless numeric coercion by the typed readers below.
|
||||
func DecodeArgsMap(raw []byte) (map[string]any, error) {
|
||||
if len(bytes.TrimSpace(raw)) == 0 {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
decoder.UseNumber()
|
||||
var result map[string]any
|
||||
if err := decoder.Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
if err == nil {
|
||||
return nil, fmt.Errorf("invalid JSON arguments: multiple top-level values")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ReadStringArg reads the first string value matching one of the provided keys.
|
||||
func ReadStringArg(args map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
value, ok := args[key]
|
||||
if !ok || value == nil {
|
||||
continue
|
||||
}
|
||||
if text, ok := value.(string); ok {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ReadBoolArg reads the first bool value matching one of the provided keys.
|
||||
func ReadBoolArg(args map[string]any, keys ...string) bool {
|
||||
for _, key := range keys {
|
||||
value, ok := args[key]
|
||||
if !ok || value == nil {
|
||||
continue
|
||||
}
|
||||
if item, ok := value.(bool); ok {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasArgKey reports whether any candidate key is present with a non-null value.
|
||||
func HasArgKey(args map[string]any, keys ...string) bool {
|
||||
for _, key := range keys {
|
||||
value, ok := args[key]
|
||||
if ok && value != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// BoolPtrIfPresent returns a bool pointer only when a matching key is present.
|
||||
func BoolPtrIfPresent(args map[string]any, keys ...string) *bool {
|
||||
if !HasArgKey(args, keys...) {
|
||||
return nil
|
||||
}
|
||||
value := ReadBoolArg(args, keys...)
|
||||
return &value
|
||||
}
|
||||
|
||||
// ReadStringSliceArg reads a string array value matching one of the provided keys.
|
||||
func ReadStringSliceArg(args map[string]any, keys ...string) []string {
|
||||
for _, key := range keys {
|
||||
value, ok := args[key]
|
||||
if !ok || value == nil {
|
||||
continue
|
||||
}
|
||||
if direct, ok := value.([]string); ok {
|
||||
return append([]string(nil), direct...)
|
||||
}
|
||||
items, ok := value.([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
result := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
text, ok := item.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
trimmed := strings.TrimSpace(text)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readArgValue(args map[string]any, keys ...string) (any, string, bool) {
|
||||
for _, key := range keys {
|
||||
value, ok := args[key]
|
||||
if !ok || value == nil {
|
||||
continue
|
||||
}
|
||||
return value, key, true
|
||||
}
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
// ReadIntArg reads an int field from a JSON number or lossless numeric string.
|
||||
func ReadIntArg(args map[string]any, keys ...string) (int, bool, error) {
|
||||
value, key, found := readArgValue(args, keys...)
|
||||
if !found {
|
||||
return 0, false, nil
|
||||
}
|
||||
parsed, err := parseIntegerValue(value, key, int64MinForBits(strconv.IntSize), int64MaxForBits(strconv.IntSize))
|
||||
if err != nil {
|
||||
return 0, true, err
|
||||
}
|
||||
return int(parsed), true, nil
|
||||
}
|
||||
|
||||
// ReadInt32Arg reads an int32 field from a JSON number or lossless numeric string.
|
||||
func ReadInt32Arg(args map[string]any, keys ...string) (int32, bool, error) {
|
||||
value, key, found := readArgValue(args, keys...)
|
||||
if !found {
|
||||
return 0, false, nil
|
||||
}
|
||||
parsed, err := parseIntegerValue(value, key, math.MinInt32, math.MaxInt32)
|
||||
if err != nil {
|
||||
return 0, true, err
|
||||
}
|
||||
return int32(parsed), true, nil
|
||||
}
|
||||
|
||||
// ReadInt64Arg reads an int64 field from a JSON number or lossless numeric string.
|
||||
func ReadInt64Arg(args map[string]any, keys ...string) (int64, bool, error) {
|
||||
value, key, found := readArgValue(args, keys...)
|
||||
if !found {
|
||||
return 0, false, nil
|
||||
}
|
||||
parsed, err := parseIntegerValue(value, key, math.MinInt64, math.MaxInt64)
|
||||
if err != nil {
|
||||
return 0, true, err
|
||||
}
|
||||
return parsed, true, nil
|
||||
}
|
||||
|
||||
// ReadUint32Arg reads a uint32 field from a JSON number or lossless numeric string.
|
||||
func ReadUint32Arg(args map[string]any, keys ...string) (uint32, bool, error) {
|
||||
value, key, found := readArgValue(args, keys...)
|
||||
if !found {
|
||||
return 0, false, nil
|
||||
}
|
||||
parsed, err := parseUnsignedIntegerValue(value, key, math.MaxUint32)
|
||||
if err != nil {
|
||||
return 0, true, err
|
||||
}
|
||||
return uint32(parsed), true, nil
|
||||
}
|
||||
|
||||
// ReadFloat64Arg reads a float64 field from a JSON number or numeric string.
|
||||
func ReadFloat64Arg(args map[string]any, keys ...string) (float64, bool, error) {
|
||||
value, key, found := readArgValue(args, keys...)
|
||||
if !found {
|
||||
return 0, false, nil
|
||||
}
|
||||
parsed, err := parseFloatValue(value, key)
|
||||
if err != nil {
|
||||
return 0, true, err
|
||||
}
|
||||
return parsed, true, nil
|
||||
}
|
||||
|
||||
func parseIntegerValue(value any, key string, minValue int64, maxValue int64) (int64, error) {
|
||||
switch item := value.(type) {
|
||||
case json.Number:
|
||||
return parseIntegerLiteral(item.String(), key, minValue, maxValue)
|
||||
case string:
|
||||
return parseIntegerLiteral(strings.TrimSpace(item), key, minValue, maxValue)
|
||||
case float64:
|
||||
return parseIntegerFloat(item, key, minValue, maxValue)
|
||||
case float32:
|
||||
return parseIntegerFloat(float64(item), key, minValue, maxValue)
|
||||
default:
|
||||
reflected := reflect.ValueOf(value)
|
||||
switch reflected.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
parsed := reflected.Int()
|
||||
if parsed < minValue || parsed > maxValue {
|
||||
return 0, fmt.Errorf("%s is outside supported integer range", key)
|
||||
}
|
||||
return parsed, nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
parsed := reflected.Uint()
|
||||
if parsed > uint64(maxValue) {
|
||||
return 0, fmt.Errorf("%s is outside supported integer range", key)
|
||||
}
|
||||
return int64(parsed), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("%s must be an integer", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseUnsignedIntegerValue(value any, key string, maxValue uint64) (uint64, error) {
|
||||
switch item := value.(type) {
|
||||
case json.Number:
|
||||
return parseUnsignedIntegerLiteral(item.String(), key, maxValue)
|
||||
case string:
|
||||
return parseUnsignedIntegerLiteral(strings.TrimSpace(item), key, maxValue)
|
||||
case float64:
|
||||
return parseUnsignedIntegerFloat(item, key, maxValue)
|
||||
case float32:
|
||||
return parseUnsignedIntegerFloat(float64(item), key, maxValue)
|
||||
default:
|
||||
reflected := reflect.ValueOf(value)
|
||||
switch reflected.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
parsed := reflected.Int()
|
||||
if parsed < 0 {
|
||||
return 0, fmt.Errorf("%s must be a non-negative integer", key)
|
||||
}
|
||||
if uint64(parsed) > maxValue {
|
||||
return 0, fmt.Errorf("%s is outside supported unsigned integer range", key)
|
||||
}
|
||||
return uint64(parsed), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
parsed := reflected.Uint()
|
||||
if parsed > maxValue {
|
||||
return 0, fmt.Errorf("%s is outside supported unsigned integer range", key)
|
||||
}
|
||||
return parsed, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("%s must be a non-negative integer", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseIntegerLiteral(raw string, key string, minValue int64, maxValue int64) (int64, error) {
|
||||
if raw == "" {
|
||||
return 0, fmt.Errorf("%s must be an integer", key)
|
||||
}
|
||||
parsed, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s must be an integer", key)
|
||||
}
|
||||
if parsed < minValue || parsed > maxValue {
|
||||
return 0, fmt.Errorf("%s is outside supported integer range", key)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func parseUnsignedIntegerLiteral(raw string, key string, maxValue uint64) (uint64, error) {
|
||||
if raw == "" {
|
||||
return 0, fmt.Errorf("%s must be a non-negative integer", key)
|
||||
}
|
||||
parsed, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil {
|
||||
if strings.HasPrefix(raw, "-") {
|
||||
return 0, fmt.Errorf("%s must be a non-negative integer", key)
|
||||
}
|
||||
return 0, fmt.Errorf("%s must be a non-negative integer", key)
|
||||
}
|
||||
if parsed > maxValue {
|
||||
return 0, fmt.Errorf("%s is outside supported unsigned integer range", key)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func parseIntegerFloat(value float64, key string, minValue int64, maxValue int64) (int64, error) {
|
||||
if !isFiniteFloat(value) || math.Trunc(value) != value {
|
||||
return 0, fmt.Errorf("%s must be an integer", key)
|
||||
}
|
||||
if value < float64(minValue) || value > float64(maxValue) {
|
||||
return 0, fmt.Errorf("%s is outside supported integer range", key)
|
||||
}
|
||||
return int64(value), nil
|
||||
}
|
||||
|
||||
func parseUnsignedIntegerFloat(value float64, key string, maxValue uint64) (uint64, error) {
|
||||
if !isFiniteFloat(value) || math.Trunc(value) != value {
|
||||
return 0, fmt.Errorf("%s must be a non-negative integer", key)
|
||||
}
|
||||
if value < 0 {
|
||||
return 0, fmt.Errorf("%s must be a non-negative integer", key)
|
||||
}
|
||||
if value > float64(maxValue) {
|
||||
return 0, fmt.Errorf("%s is outside supported unsigned integer range", key)
|
||||
}
|
||||
return uint64(value), nil
|
||||
}
|
||||
|
||||
func parseFloatValue(value any, key string) (float64, error) {
|
||||
switch item := value.(type) {
|
||||
case json.Number:
|
||||
parsed, err := item.Float64()
|
||||
if err != nil || !isFiniteFloat(parsed) {
|
||||
return 0, fmt.Errorf("%s must be a finite number", key)
|
||||
}
|
||||
return parsed, nil
|
||||
case string:
|
||||
trimmed := strings.TrimSpace(item)
|
||||
if trimmed == "" {
|
||||
return 0, fmt.Errorf("%s must be a finite number", key)
|
||||
}
|
||||
parsed, err := strconv.ParseFloat(trimmed, 64)
|
||||
if err != nil || !isFiniteFloat(parsed) {
|
||||
return 0, fmt.Errorf("%s must be a finite number", key)
|
||||
}
|
||||
return parsed, nil
|
||||
case float64:
|
||||
if !isFiniteFloat(item) {
|
||||
return 0, fmt.Errorf("%s must be a finite number", key)
|
||||
}
|
||||
return item, nil
|
||||
case float32:
|
||||
parsed := float64(item)
|
||||
if !isFiniteFloat(parsed) {
|
||||
return 0, fmt.Errorf("%s must be a finite number", key)
|
||||
}
|
||||
return parsed, nil
|
||||
default:
|
||||
reflected := reflect.ValueOf(value)
|
||||
switch reflected.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return float64(reflected.Int()), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return float64(reflected.Uint()), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("%s must be a finite number", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func int64MinForBits(bits int) int64 {
|
||||
if bits >= 64 {
|
||||
return math.MinInt64
|
||||
}
|
||||
return -(int64(1) << (bits - 1))
|
||||
}
|
||||
|
||||
func int64MaxForBits(bits int) int64 {
|
||||
if bits >= 64 {
|
||||
return math.MaxInt64
|
||||
}
|
||||
return (int64(1) << (bits - 1)) - 1
|
||||
}
|
||||
|
||||
func isFiniteFloat(value float64) bool {
|
||||
return !math.IsNaN(value) && !math.IsInf(value, 0)
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
// types.go 定义运行时、公用命令、事件、状态与 pending 结构。
|
||||
package runtimecore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"cursor/gen/agentv1"
|
||||
)
|
||||
|
||||
// SubagentModelOverrideSelection 表示父 run 对某类 subagent 的模型选择覆盖。
|
||||
type SubagentModelOverrideSelection struct {
|
||||
SubagentType string `json:"subagent_type"`
|
||||
Selection string `json:"selection"`
|
||||
ModelID string `json:"model_id,omitempty"`
|
||||
MaxMode bool `json:"max_mode,omitempty"`
|
||||
ParameterCount int `json:"parameter_count,omitempty"`
|
||||
BuiltInModel bool `json:"built_in_model,omitempty"`
|
||||
IsVariantStringRepresentation bool `json:"is_variant_string_representation,omitempty"`
|
||||
}
|
||||
|
||||
// LookupSubagentModelOverride 按 Task subagent_type 查找运行期模型覆盖。
|
||||
func LookupSubagentModelOverride(overrides map[string]SubagentModelOverrideSelection, subagentType string) (SubagentModelOverrideSelection, string, bool) {
|
||||
if len(overrides) == 0 {
|
||||
return SubagentModelOverrideSelection{}, "", false
|
||||
}
|
||||
for _, key := range subagentModelOverrideLookupKeys(subagentType) {
|
||||
if selection, ok := overrides[key]; ok {
|
||||
return selection, key, true
|
||||
}
|
||||
}
|
||||
return SubagentModelOverrideSelection{}, "", false
|
||||
}
|
||||
|
||||
func subagentModelOverrideLookupKeys(subagentType string) []string {
|
||||
trimmed := strings.TrimSpace(subagentType)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
keys := []string{trimmed}
|
||||
switch trimmed {
|
||||
case "generalPurpose":
|
||||
keys = append(keys, "explore")
|
||||
case "explore":
|
||||
keys = append(keys, "generalPurpose")
|
||||
case "browserUse":
|
||||
keys = append(keys, "browser-use")
|
||||
case "browser-use":
|
||||
keys = append(keys, "browserUse")
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
type RunState string
|
||||
|
||||
const (
|
||||
// RunStateIdle 表示空闲态,此时 session 已存在但没有活跃 run。
|
||||
RunStateIdle RunState = "IDLE"
|
||||
// RunStateRestoring 表示恢复态,此时正在装载会话状态与最小恢复信息。
|
||||
RunStateRestoring RunState = "RESTORING"
|
||||
// RunStatePreparingModelInput 表示模型输入准备态。
|
||||
RunStatePreparingModelInput RunState = "PREPARING_MODEL_INPUT"
|
||||
// RunStateStreamingModel 表示模型流消费态。
|
||||
RunStateStreamingModel RunState = "STREAMING_MODEL"
|
||||
// RunStateWaitingExec 表示执行桥等待态。
|
||||
RunStateWaitingExec RunState = "WAITING_EXEC"
|
||||
// RunStateWaitingInteraction 表示交互桥等待态。
|
||||
RunStateWaitingInteraction RunState = "WAITING_INTERACTION"
|
||||
// RunStateApplyingExternalResult 表示外部结果回写态。
|
||||
RunStateApplyingExternalResult RunState = "APPLYING_EXTERNAL_RESULT"
|
||||
// RunStateCheckpointing 表示检查点写入态。
|
||||
RunStateCheckpointing RunState = "CHECKPOINTING"
|
||||
// RunStateCompleted 表示正常完成态。
|
||||
RunStateCompleted RunState = "COMPLETED"
|
||||
// RunStateCanceled 表示取消终态。
|
||||
RunStateCanceled RunState = "CANCELED"
|
||||
// RunStateFailed 表示失败终态。
|
||||
RunStateFailed RunState = "FAILED"
|
||||
)
|
||||
|
||||
// CommandKind 表示运行时接收的上行命令类型。
|
||||
type CommandKind string
|
||||
|
||||
const (
|
||||
// CommandKindRunRequested 表示收到 `run_request`。
|
||||
CommandKindRunRequested CommandKind = "run_requested"
|
||||
// CommandKindPrewarmRequested 表示收到 `prewarm_request`。
|
||||
CommandKindPrewarmRequested CommandKind = "prewarm_requested"
|
||||
// CommandKindCancelRequested 表示收到 `conversation_action.cancel_action`。
|
||||
CommandKindCancelRequested CommandKind = "cancel_requested"
|
||||
// CommandKindConversationActionRecordOnly 表示收到非取消型的 `conversation_action`,当前阶段只记录不推进状态。
|
||||
CommandKindConversationActionRecordOnly CommandKind = "conversation_action_record_only"
|
||||
// CommandKindExecClientMessage 表示收到 `exec_client_message`。
|
||||
CommandKindExecClientMessage CommandKind = "exec_client_message"
|
||||
// CommandKindInteractionResponse 表示收到 `interaction_response`。
|
||||
CommandKindInteractionResponse CommandKind = "interaction_response"
|
||||
// CommandKindExecClientControlMessage 表示收到 `exec_client_control_message`,当前阶段只记录不推进状态。
|
||||
CommandKindExecClientControlMessage CommandKind = "exec_client_control_message"
|
||||
// CommandKindClientHeartbeat 表示收到客户端心跳,当前阶段只记录不推进状态。
|
||||
CommandKindClientHeartbeat CommandKind = "client_heartbeat"
|
||||
// CommandKindKVClientMessage 表示收到 `kv_client_message`,当前阶段只记录不推进状态。
|
||||
CommandKindKVClientMessage CommandKind = "kv_client_message"
|
||||
)
|
||||
|
||||
// Command 描述一次投递到运行时协调层的上行命令。
|
||||
type Command struct {
|
||||
// Kind 指定该命令的运行时语义。
|
||||
Kind CommandKind
|
||||
// IsResume 标记当前命令是否为恢复型启动。
|
||||
IsResume bool
|
||||
// ClientKind 保留协议层顶级消息种类,便于观测与调试。
|
||||
ClientKind string
|
||||
// HistoryEntry 保存协议摘要文本,供当前 MVP 的合成回复使用。
|
||||
HistoryEntry string
|
||||
// ClientMessage 保存解码后的完整上行协议消息。
|
||||
ClientMessage *agentv1.AgentClientMessage
|
||||
}
|
||||
|
||||
// EventKind 表示一次可回放下行事件的业务类型。
|
||||
type EventKind string
|
||||
|
||||
const (
|
||||
// EventKindRunStarted 表示新 run 已创建并开始进入恢复路径。
|
||||
EventKindRunStarted EventKind = "run_started"
|
||||
// EventKindStepStarted 表示步骤开始事件。
|
||||
EventKindStepStarted EventKind = "step_started"
|
||||
// EventKindTextDelta 表示文本增量事件。
|
||||
EventKindTextDelta EventKind = "text_delta"
|
||||
// EventKindStepCompleted 表示步骤完成事件。
|
||||
EventKindStepCompleted EventKind = "step_completed"
|
||||
// EventKindTurnEnded 表示回合结束事件。
|
||||
EventKindTurnEnded EventKind = "turn_ended"
|
||||
// EventKindCheckpoint 表示会话检查点事件。
|
||||
EventKindCheckpoint EventKind = "checkpoint"
|
||||
// EventKindCanceled 表示取消事件。
|
||||
EventKindCanceled EventKind = "canceled"
|
||||
// EventKindHeartbeat 表示服务端心跳事件。
|
||||
EventKindHeartbeat EventKind = "heartbeat"
|
||||
)
|
||||
|
||||
// Event 表示一条可广播、可回放的下行事件记录。
|
||||
type Event struct {
|
||||
// Seq 是请求维度内递增的事件序号。
|
||||
Seq int64
|
||||
// RequestID 是事件所属请求标识。
|
||||
RequestID string
|
||||
// RunID 是事件所属运行标识。
|
||||
RunID string
|
||||
// Kind 标识该事件的业务类型。
|
||||
Kind EventKind
|
||||
// Message 是要透传到 RunSSE 的协议消息体。
|
||||
Message *agentv1.AgentServerMessage
|
||||
// End 表示该事件会结束当前 SSE 读取。
|
||||
End bool
|
||||
// TerminalErrorCode 表示当前终态 SSE 需要返回的 connect error code,例如 canceled。
|
||||
TerminalErrorCode string
|
||||
// TerminalErrorMessage 表示当前终态 SSE 需要返回的错误消息。
|
||||
TerminalErrorMessage string
|
||||
// CreatedAt 是事件入库时间。
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// RunSnapshot 表示一次 run 的最小快照信息。
|
||||
type RunSnapshot struct {
|
||||
// RunID 是运行唯一标识。
|
||||
RunID string
|
||||
// RequestID 是当前 run 绑定的请求标识。
|
||||
RequestID string
|
||||
// ConversationID 是当前 run 绑定的会话标识。
|
||||
ConversationID string
|
||||
// ModelID 表示当前运行使用的模型标识。
|
||||
ModelID string
|
||||
// State 表示该 run 当前所处状态。
|
||||
State RunState
|
||||
// Mode 表示该 run 当前使用的会话模式。
|
||||
Mode agentv1.AgentMode
|
||||
// Version 是运行时版本号,便于后续扩展乐观更新。
|
||||
Version int64
|
||||
// StartedAt 记录 run 启动时间。
|
||||
StartedAt time.Time
|
||||
// UpdatedAt 记录 run 最近一次状态更新时间。
|
||||
UpdatedAt time.Time
|
||||
// CurrentUserMessageText 保存当前 turn 的用户输入文本,直到本 turn 提交进 `turns`。
|
||||
CurrentUserMessageText string
|
||||
// CustomSystemPrompt 保存当前 run 附带的自定义系统提示词。
|
||||
CustomSystemPrompt string
|
||||
// RequestContextPayload 保存当前 run 的 request_context proto 序列化结果。
|
||||
RequestContextPayload []byte
|
||||
// IsPrewarm 标记当前 run 是否由 `prewarm_request` 触发。
|
||||
IsPrewarm bool
|
||||
}
|
||||
|
||||
// PendingAssistantOutput 表示尚未收口的一条 assistant 输出记录。
|
||||
type PendingAssistantOutput struct {
|
||||
// RawMessage 保存原始序列化 assistant message。
|
||||
RawMessage string
|
||||
// Role 表示该记录的 role,当前常见值为 assistant。
|
||||
Role string
|
||||
// ContentKinds 记录内容块类型顺序,例如 text 或 tool-call。
|
||||
ContentKinds []string
|
||||
// ToolCallIDs 记录该输出中出现的全部 tool_call_id。
|
||||
ToolCallIDs []string
|
||||
// ToolNames 记录该输出中出现的全部工具名称。
|
||||
ToolNames []string
|
||||
// TextPreview 保存文本块的简要摘要。
|
||||
TextPreview string
|
||||
}
|
||||
|
||||
// PendingExec 表示一条尚未收口的执行桥记录。
|
||||
type PendingExec struct {
|
||||
// MessageID 是打开该执行桥时下发给客户端的桥消息编号。
|
||||
MessageID uint32
|
||||
// ExecID 是执行桥唯一标识。
|
||||
ExecID string
|
||||
// ProviderPass 表示创建该执行桥时所属的 provider pass。
|
||||
ProviderPass int
|
||||
// ModelCallID 是触发该执行桥的模型调用标识。
|
||||
ModelCallID string
|
||||
// ToolCallID 是与该执行桥关联的工具调用标识。
|
||||
ToolCallID string
|
||||
// ArgsJSON 保存打开该执行桥时的原始参数 JSON,便于恢复 completed ToolCall。
|
||||
ArgsJSON []byte
|
||||
// ReasoningContent 保存触发该工具调用时的 thinking 文本,供 checkpoint/replay 续跑复用。
|
||||
ReasoningContent string
|
||||
// ReasoningSignature 保存 provider 对当前 thinking 文本签发的签名。
|
||||
ReasoningSignature string
|
||||
// ReasoningSignatureSource 保存 reasoning signature 的 provider 语义来源。
|
||||
ReasoningSignatureSource string
|
||||
// ExecKind 描述执行桥类型,例如 read、write、shellStream。
|
||||
ExecKind string
|
||||
// StreamState 描述当前流式执行桥的阶段。
|
||||
StreamState string
|
||||
// OpenedAt 表示执行桥请求发出的时间。
|
||||
OpenedAt time.Time
|
||||
// FirstChunkAt 表示 shellStream 首个输出块时间。
|
||||
FirstChunkAt time.Time
|
||||
// ChunkCount 表示 shellStream 已接收的输出块数量。
|
||||
ChunkCount int64
|
||||
// LastShellActivityAt 记录最近一次 shell 相关上行事件时间,包括输出、start、heartbeat 和 close。
|
||||
LastShellActivityAt time.Time
|
||||
// LastShellHeartbeatAt 记录最近一次 shell heartbeat 到达时间。
|
||||
LastShellHeartbeatAt time.Time
|
||||
// ShellForegroundDeadline 表示前台 shell 预计最晚应收到终态的时间点。
|
||||
ShellForegroundDeadline time.Time
|
||||
// ShellRecoveryScheduled 标记是否已经为该 shell 安排了异常收口协程。
|
||||
ShellRecoveryScheduled bool
|
||||
// StdoutBuffer 保存当前 shell 已累计的 stdout 文本。
|
||||
StdoutBuffer string
|
||||
// StderrBuffer 保存当前 shell 已累计的 stderr 文本。
|
||||
StderrBuffer string
|
||||
// ArtifactPath 保存该 exec 对应的原始桥接工件路径。
|
||||
ArtifactPath string
|
||||
}
|
||||
|
||||
// PendingInteraction 表示一条尚未收口的交互桥记录。
|
||||
type PendingInteraction struct {
|
||||
// InteractionID 是交互桥唯一标识。
|
||||
InteractionID string
|
||||
// ProviderPass 表示创建该交互桥时所属的 provider pass。
|
||||
ProviderPass int
|
||||
// ModelCallID 是触发该交互桥的模型调用标识。
|
||||
ModelCallID string
|
||||
// ToolCallID 是与该交互桥关联的工具调用标识。
|
||||
ToolCallID string
|
||||
// ArgsJSON 保存打开该交互桥时的原始参数 JSON,便于结果回写时恢复结构化状态。
|
||||
ArgsJSON []byte
|
||||
// ReasoningContent 保存触发该工具调用时的 thinking 文本,供 checkpoint/replay 续跑复用。
|
||||
ReasoningContent string
|
||||
// ReasoningSignature 保存 provider 对当前 thinking 文本签发的签名。
|
||||
ReasoningSignature string
|
||||
// ReasoningSignatureSource 保存 reasoning signature 的 provider 语义来源。
|
||||
ReasoningSignatureSource string
|
||||
// InteractionKind 描述交互类型,例如 ask_question、create_plan。
|
||||
InteractionKind string
|
||||
// OpenedAt 表示交互请求发出的时间。
|
||||
OpenedAt time.Time
|
||||
// ArtifactPath 保存该 interaction 对应的原始桥接工件路径。
|
||||
ArtifactPath string
|
||||
}
|
||||
|
||||
// ActiveStep 表示当前正在推进、尚未收口的 step 元数据。
|
||||
type ActiveStep struct {
|
||||
// StepID 是当前 step 唯一标识。
|
||||
StepID uint64
|
||||
// ModelCallID 是当前 step 绑定的模型调用标识。
|
||||
ModelCallID string
|
||||
// StartedAt 是当前 step 的开始时间。
|
||||
StartedAt time.Time
|
||||
// InputTokens 保存当前 step 已知的输入 token 数。
|
||||
InputTokens int64
|
||||
// OutputTokens 保存当前 step 已知的输出 token 数。
|
||||
OutputTokens int64
|
||||
}
|
||||
|
||||
// ExternalResultSummary 表示 APPLYING_EXTERNAL_RESULT 后继续下一轮编译所需的最小上下文。
|
||||
type ExternalResultSummary struct {
|
||||
// Source 表示结果来源,例如 exec 或 interaction。
|
||||
Source string
|
||||
// ToolName 表示对应工具名或交互名。
|
||||
ToolName string
|
||||
// Payload 表示可直接注入 prompt 的结果摘要。
|
||||
Payload string
|
||||
}
|
||||
|
||||
// ToolInvocation 表示一次模型产出的工具调用意图。
|
||||
type ToolInvocation struct {
|
||||
// CallID 是模型层工具调用标识。
|
||||
CallID string
|
||||
// ToolName 表示工具名称,例如 Read、Write、AskQuestion。
|
||||
ToolName string
|
||||
// ArgsJSON 保存工具参数原始 JSON。
|
||||
ArgsJSON []byte
|
||||
// ReasoningContent 保存当前工具调用前伴随的 thinking 文本。
|
||||
ReasoningContent string
|
||||
// ReasoningSignature 保存 provider 对当前 thinking 文本签发的签名。
|
||||
ReasoningSignature string
|
||||
// ReasoningSignatureSource 保存 reasoning signature 的 provider 语义来源。
|
||||
ReasoningSignatureSource string
|
||||
// ReasoningProviderItemID 保存 provider 原始 reasoning output item id。
|
||||
ReasoningProviderItemID string
|
||||
// ReasoningProviderStatus 保存 provider 原始 reasoning output item status。
|
||||
ReasoningProviderStatus string
|
||||
// ReasoningProviderSummary 保存 provider 原始 reasoning output item summary。
|
||||
ReasoningProviderSummary json.RawMessage
|
||||
// ProviderItemID 保存 provider 原始 tool/function output item id。
|
||||
ProviderItemID string
|
||||
// ProviderCallID 保存 provider 原始 tool/function call id。
|
||||
ProviderCallID string
|
||||
// ProviderStatus 保存 provider 原始 tool/function output item status。
|
||||
ProviderStatus string
|
||||
// ModelCallID 表示本轮模型调用标识。
|
||||
ModelCallID string
|
||||
}
|
||||
|
||||
// NormalizeSupportedMode 规范化并校验当前支持的会话 mode。
|
||||
//
|
||||
// 当前默认口径:
|
||||
// 1. 未显式携带 mode 或值为 `AGENT_MODE_UNSPECIFIED` 时,按 `AGENT_MODE_AGENT` 处理;
|
||||
// 2. 仅允许 `AGENT_MODE_AGENT`、`AGENT_MODE_ASK`、`AGENT_MODE_PLAN`、`AGENT_MODE_DEBUG`、`AGENT_MODE_MULTITASK`;
|
||||
// 3. 其他 mode 一律报错,不允许静默回退。
|
||||
func NormalizeSupportedMode(mode agentv1.AgentMode) (agentv1.AgentMode, error) {
|
||||
switch mode {
|
||||
case agentv1.AgentMode_AGENT_MODE_UNSPECIFIED:
|
||||
return agentv1.AgentMode_AGENT_MODE_AGENT, nil
|
||||
case agentv1.AgentMode_AGENT_MODE_AGENT,
|
||||
agentv1.AgentMode_AGENT_MODE_ASK,
|
||||
agentv1.AgentMode_AGENT_MODE_PLAN,
|
||||
agentv1.AgentMode_AGENT_MODE_DEBUG,
|
||||
agentv1.AgentMode_AGENT_MODE_MULTITASK:
|
||||
return mode, nil
|
||||
default:
|
||||
return agentv1.AgentMode_AGENT_MODE_UNSPECIFIED, fmt.Errorf("unsupported mode: %s", mode.String())
|
||||
}
|
||||
}
|
||||
|
||||
// CloneToolCallMap 深拷贝 tool_call 结果映射,避免共享 proto 指针。
|
||||
func CloneToolCallMap(items map[string]*agentv1.ToolCall) map[string]*agentv1.ToolCall {
|
||||
if len(items) == 0 {
|
||||
return make(map[string]*agentv1.ToolCall)
|
||||
}
|
||||
|
||||
cloned := make(map[string]*agentv1.ToolCall, len(items))
|
||||
for key, value := range items {
|
||||
if value == nil {
|
||||
cloned[key] = nil
|
||||
continue
|
||||
}
|
||||
typed, ok := proto.Clone(value).(*agentv1.ToolCall)
|
||||
if !ok {
|
||||
cloned[key] = nil
|
||||
continue
|
||||
}
|
||||
cloned[key] = typed
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
// IsCurrentlySupportedTool 判断当前 Phase 5 稳定化版本是否真正支持该工具。
|
||||
//
|
||||
// 当前规则:
|
||||
// 1. 只返回 runtime/loop 当前已经具备完整推进链路的能力;
|
||||
// 2. 结果用于限制实际对模型暴露的工具集合,避免模型调用未实现能力后把整轮 run 直接打失败;
|
||||
// 3. 必须保持最小闭环优先,而不是优先暴露抓包里存在但服务端尚未支持的能力。
|
||||
func IsCurrentlySupportedTool(name string) bool {
|
||||
switch strings.TrimSpace(name) {
|
||||
case "Read", "Write", "PatchEdit", "Delete", "Shell", "AwaitShell", "WriteShellStdin", "ForceBackgroundShell",
|
||||
"Glob", "Grep", "ReadLints",
|
||||
"AskQuestion", "CreatePlan", "SwitchMode", "WebSearch", "WebFetch",
|
||||
"TodoWrite", "Task",
|
||||
"CallMcpTool", "FetchMcpResource":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user