This commit is contained in:
leokun
2026-06-30 10:38:52 +08:00
commit c083be5ec2
312 changed files with 146628 additions and 0 deletions
+237
View File
@@ -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
}
}
+2
View File
@@ -0,0 +1,2 @@
// Package runtimecore 定义 runtime/loop、checkpoint、session 之间共享的状态与事件模型。
package runtimecore
+110
View File
@@ -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
}
+370
View File
@@ -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)
}
+399
View File
@@ -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
}
}