mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 19:47:10 +08:00
- Added support for checkpointing phases and blob management in the forwarder. - Introduced new types and methods for handling checkpoint blobs, including queuing and publishing checkpoints. - Enhanced the projector to build checkpoint projections with content-addressed blobs. - Implemented tests to ensure proper checkpoint blob synchronization and handling of cancellation scenarios.
629 lines
19 KiB
Go
629 lines
19 KiB
Go
// events.go 负责构造对外兼容的 legacy RunSSE 消息。
|
|
package forwarder
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
|
|
"cursor/gen/agentv1"
|
|
execbridge "cursor/internal/backend/agent/bridge/exec"
|
|
runtimecore "cursor/internal/backend/agent/core"
|
|
)
|
|
|
|
// buildHeartbeatMessage 构造一个服务端心跳消息。
|
|
func buildHeartbeatMessage() *agentv1.AgentServerMessage {
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_Heartbeat{
|
|
Heartbeat: &agentv1.HeartbeatUpdate{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildTextDeltaMessage 构造文本增量消息。
|
|
func buildTextDeltaMessage(text string) *agentv1.AgentServerMessage {
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_TextDelta{
|
|
TextDelta: &agentv1.TextDeltaUpdate{Text: text},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildThinkingDeltaMessage 构造思考文本增量消息。
|
|
func buildThinkingDeltaMessage(text string, style agentv1.ThinkingStyle) *agentv1.AgentServerMessage {
|
|
styleCopy := style
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_ThinkingDelta{
|
|
ThinkingDelta: &agentv1.ThinkingDeltaUpdate{
|
|
Text: text,
|
|
ThinkingStyle: &styleCopy,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildThinkingCompletedMessage 构造思考阶段结束消息。
|
|
func buildThinkingCompletedMessage(durationMS int32) *agentv1.AgentServerMessage {
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_ThinkingCompleted{
|
|
ThinkingCompleted: &agentv1.ThinkingCompletedUpdate{
|
|
ThinkingDurationMs: durationMS,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildSummaryStartedMessage 构造摘要开始消息。
|
|
func buildSummaryStartedMessage() *agentv1.AgentServerMessage {
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_SummaryStarted{
|
|
SummaryStarted: &agentv1.SummaryStartedUpdate{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildSummaryMessage 构造摘要增量/完成内容消息。
|
|
func buildSummaryMessage(summary string) *agentv1.AgentServerMessage {
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_Summary{
|
|
Summary: &agentv1.SummaryUpdate{Summary: strings.TrimSpace(summary)},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildSummaryCompletedMessage 构造摘要完成消息。
|
|
// field 11 在 legacy aiserver 协议里会被客户端按 GetThoughtAnnotationRequest 解码,
|
|
// 所以这里需要把 request_id 写入 field 1,驱动客户端继续查询 "Chat context summarized" 注解。
|
|
func buildSummaryCompletedMessage(requestID string) *agentv1.AgentServerMessage {
|
|
var message *string
|
|
if strings.TrimSpace(requestID) != "" {
|
|
value := strings.TrimSpace(requestID)
|
|
message = &value
|
|
}
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_SummaryCompleted{
|
|
SummaryCompleted: &agentv1.SummaryCompletedUpdate{
|
|
HookMessage: message,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildToolCallStartedMessage 构造工具调用开始消息。
|
|
func buildToolCallStartedMessage(callID string, modelCallID string, toolCall *agentv1.ToolCall) *agentv1.AgentServerMessage {
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_ToolCallStarted{
|
|
ToolCallStarted: &agentv1.ToolCallStartedUpdate{
|
|
CallId: callID,
|
|
ToolCall: toolCall,
|
|
ModelCallId: modelCallID,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildPartialToolCallMessage 构造工具调用参数流式生成中的兼容消息。
|
|
func buildPartialToolCallMessage(callID string, modelCallID string, toolCall *agentv1.ToolCall, argsTextDelta string) *agentv1.AgentServerMessage {
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_PartialToolCall{
|
|
PartialToolCall: &agentv1.PartialToolCallUpdate{
|
|
CallId: callID,
|
|
ToolCall: toolCall,
|
|
ArgsTextDelta: argsTextDelta,
|
|
ModelCallId: modelCallID,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildToolCallDeltaMessage 构造工具调用流式增量消息。
|
|
func buildToolCallDeltaMessage(callID string, modelCallID string, delta *agentv1.ToolCallDelta) *agentv1.AgentServerMessage {
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_ToolCallDelta{
|
|
ToolCallDelta: &agentv1.ToolCallDeltaUpdate{
|
|
CallId: callID,
|
|
ToolCallDelta: delta,
|
|
ModelCallId: modelCallID,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildToolCallCompletedMessage 构造工具调用完成消息。
|
|
func buildToolCallCompletedMessage(callID string, modelCallID string, toolCall *agentv1.ToolCall) *agentv1.AgentServerMessage {
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_ToolCallCompleted{
|
|
ToolCallCompleted: &agentv1.ToolCallCompletedUpdate{
|
|
CallId: callID,
|
|
ToolCall: toolCall,
|
|
ModelCallId: modelCallID,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildShellOutputDeltaMessage 把 shell 流输出包装成兼容消息。
|
|
func buildShellOutputDeltaMessage(delta *agentv1.ShellOutputDeltaUpdate) *agentv1.AgentServerMessage {
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_ShellOutputDelta{
|
|
ShellOutputDelta: delta,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildTurnEndedMessage 构造 turn 结束消息,并携带标准化后的 token 统计。
|
|
func buildTurnEndedMessage(inputTokens int64, outputTokens int64, cacheReadTokens int64, cacheWriteTokens int64) *agentv1.AgentServerMessage {
|
|
inputTokensValue := inputTokens
|
|
outputTokensValue := outputTokens
|
|
cacheReadTokensValue := cacheReadTokens
|
|
cacheWriteTokensValue := cacheWriteTokens
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_InteractionUpdate{
|
|
InteractionUpdate: &agentv1.InteractionUpdate{
|
|
Message: &agentv1.InteractionUpdate_TurnEnded{
|
|
TurnEnded: &agentv1.TurnEndedUpdate{
|
|
InputTokens: &inputTokensValue,
|
|
OutputTokens: &outputTokensValue,
|
|
CacheReadTokens: &cacheReadTokensValue,
|
|
CacheWriteTokens: &cacheWriteTokensValue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildCheckpointMessage 根据投影出的状态生成 legacy checkpoint 消息。
|
|
func buildCheckpointMessage(state *agentv1.ConversationStateStructure) *agentv1.AgentServerMessage {
|
|
cloned := &agentv1.ConversationStateStructure{}
|
|
if state != nil {
|
|
if next, ok := proto.Clone(state).(*agentv1.ConversationStateStructure); ok && next != nil {
|
|
cloned = next
|
|
}
|
|
}
|
|
if cloned.TokenDetails == nil {
|
|
cloned.TokenDetails = &agentv1.ConversationTokenDetails{}
|
|
}
|
|
if cloned.TokenDetails.MaxTokens == 0 {
|
|
cloned.TokenDetails.MaxTokens = projectedConversationMaxTokens
|
|
}
|
|
cloned.TurnTimings = nil
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_ConversationCheckpointUpdate{
|
|
ConversationCheckpointUpdate: cloned,
|
|
},
|
|
}
|
|
}
|
|
|
|
func buildSetCheckpointBlobMessage(id uint32, blob CheckpointBlob) *agentv1.AgentServerMessage {
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_KvServerMessage{
|
|
KvServerMessage: &agentv1.KvServerMessage{
|
|
Id: id,
|
|
Message: &agentv1.KvServerMessage_SetBlobArgs{
|
|
SetBlobArgs: &agentv1.SetBlobArgs{
|
|
BlobId: append([]byte(nil), blob.ID...),
|
|
BlobData: append([]byte(nil), blob.Data...),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildExecAbortMessage 构造对客户端执行桥的 abort 控制消息。
|
|
func buildExecAbortMessage(pending runtimecore.PendingExec) *agentv1.AgentServerMessage {
|
|
return &agentv1.AgentServerMessage{
|
|
Message: &agentv1.AgentServerMessage_ExecServerControlMessage{
|
|
ExecServerControlMessage: &agentv1.ExecServerControlMessage{
|
|
Message: &agentv1.ExecServerControlMessage_Abort{
|
|
Abort: &agentv1.ExecServerAbort{
|
|
Id: pending.MessageID,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildStartedToolCall 把工具意图映射为可发送给客户端的 started ToolCall 结构。
|
|
func buildStartedToolCall(invocation runtimecore.ToolInvocation) *agentv1.ToolCall {
|
|
switch strings.TrimSpace(invocation.ToolName) {
|
|
case "Glob":
|
|
args, err := execbridge.DecodeGlobToolArgs(invocation.ArgsJSON)
|
|
if err != nil {
|
|
args = &agentv1.GlobToolArgs{}
|
|
}
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_GlobToolCall{
|
|
GlobToolCall: &agentv1.GlobToolCall{Args: args},
|
|
},
|
|
}
|
|
case "Grep":
|
|
args, err := execbridge.DecodeGrepToolArgs(invocation.ArgsJSON, invocation.CallID)
|
|
if err != nil && args == nil {
|
|
args = &agentv1.GrepArgs{ToolCallId: invocation.CallID}
|
|
}
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_GrepToolCall{
|
|
GrepToolCall: &agentv1.GrepToolCall{Args: args},
|
|
},
|
|
}
|
|
case "Read":
|
|
args, err := execbridge.DecodeReadToolArgs(invocation.ArgsJSON)
|
|
if err != nil && args == nil {
|
|
args = &agentv1.ReadToolArgs{}
|
|
}
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_ReadToolCall{
|
|
ReadToolCall: &agentv1.ReadToolCall{
|
|
Args: args,
|
|
},
|
|
},
|
|
}
|
|
case "TodoWrite":
|
|
args, _ := decodeUpdateTodosArgsJSON(invocation.ArgsJSON)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_UpdateTodosToolCall{
|
|
UpdateTodosToolCall: &agentv1.UpdateTodosToolCall{Args: args},
|
|
},
|
|
}
|
|
case "ReadTodos":
|
|
args, _ := decodeReadTodosArgsJSON(invocation.ArgsJSON)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_ReadTodosToolCall{
|
|
ReadTodosToolCall: &agentv1.ReadTodosToolCall{Args: args},
|
|
},
|
|
}
|
|
case "Delete":
|
|
var input struct {
|
|
Path string `json:"path"`
|
|
}
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &input)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_DeleteToolCall{
|
|
DeleteToolCall: &agentv1.DeleteToolCall{
|
|
Args: &agentv1.DeleteArgs{Path: strings.TrimSpace(input.Path), ToolCallId: invocation.CallID},
|
|
},
|
|
},
|
|
}
|
|
case "Shell":
|
|
var input struct {
|
|
Command string `json:"command"`
|
|
Description string `json:"description,omitempty"`
|
|
WorkingDirectory string `json:"working_directory,omitempty"`
|
|
NotifyOnOutput *struct {
|
|
Pattern string `json:"pattern"`
|
|
Reason string `json:"reason"`
|
|
DebounceMS *float64 `json:"debounce_ms,omitempty"`
|
|
NotificationLimit *int32 `json:"notification_limit,omitempty"`
|
|
} `json:"notify_on_output,omitempty"`
|
|
}
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &input)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_ShellToolCall{
|
|
ShellToolCall: &agentv1.ShellToolCall{
|
|
Args: &agentv1.ShellArgs{
|
|
Command: strings.TrimSpace(input.Command),
|
|
WorkingDirectory: strings.TrimSpace(input.WorkingDirectory),
|
|
ToolCallId: invocation.CallID,
|
|
Description: stringPtr(strings.TrimSpace(input.Description)),
|
|
OutputNotification: buildShellOutputNotificationConfig(input.NotifyOnOutput),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
case "AwaitShell":
|
|
args, err := decodeAwaitShellArgs(invocation.ArgsJSON)
|
|
if err != nil {
|
|
args = awaitShellArgs{}
|
|
}
|
|
return buildAwaitShellToolCall(buildAwaitArgsFromAwaitShellArgs(args), nil)
|
|
case "WriteShellStdin":
|
|
var input struct {
|
|
ShellID uint32 `json:"shell_id"`
|
|
Chars string `json:"chars"`
|
|
}
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &input)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_WriteShellStdinToolCall{
|
|
WriteShellStdinToolCall: &agentv1.WriteShellStdinToolCall{
|
|
Args: &agentv1.WriteShellStdinArgs{
|
|
ShellId: input.ShellID,
|
|
Chars: input.Chars,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
case "Task":
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_TaskToolCall{
|
|
TaskToolCall: &agentv1.TaskToolCall{
|
|
Args: buildTaskArgsFromJSON(invocation.ArgsJSON),
|
|
},
|
|
},
|
|
}
|
|
case "Ls":
|
|
var input struct {
|
|
Path string `json:"path"`
|
|
Ignore []string `json:"ignore,omitempty"`
|
|
}
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &input)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_LsToolCall{
|
|
LsToolCall: &agentv1.LsToolCall{
|
|
Args: &agentv1.LsArgs{
|
|
Path: strings.TrimSpace(input.Path),
|
|
Ignore: append([]string(nil), input.Ignore...),
|
|
ToolCallId: invocation.CallID,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
case "Write":
|
|
var input struct {
|
|
Path string `json:"path"`
|
|
Contents string `json:"contents"`
|
|
}
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &input)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_EditToolCall{
|
|
EditToolCall: &agentv1.EditToolCall{
|
|
Args: &agentv1.EditArgs{
|
|
Path: strings.TrimSpace(input.Path),
|
|
StreamContent: literalStringPtr(input.Contents),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
case "PatchEdit":
|
|
var input struct {
|
|
FilePath string `json:"file_path"`
|
|
Path string `json:"path,omitempty"`
|
|
}
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &input)
|
|
path := strings.TrimSpace(input.FilePath)
|
|
if path == "" {
|
|
path = strings.TrimSpace(input.Path)
|
|
}
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_EditToolCall{
|
|
EditToolCall: &agentv1.EditToolCall{
|
|
Args: &agentv1.EditArgs{
|
|
Path: path,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
case "CallMcpTool":
|
|
var input struct {
|
|
Server string `json:"server"`
|
|
ToolName string `json:"toolName"`
|
|
Arguments map[string]any `json:"arguments,omitempty"`
|
|
}
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &input)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_McpToolCall{
|
|
McpToolCall: &agentv1.McpToolCall{
|
|
Args: &agentv1.McpArgs{
|
|
Name: forwarderCanonicalMCPToolLookupName(input.Server, input.ToolName),
|
|
Args: forwarderStructValueMap(input.Arguments),
|
|
ToolCallId: invocation.CallID,
|
|
ProviderIdentifier: strings.TrimSpace(input.Server),
|
|
ToolName: strings.TrimSpace(input.ToolName),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
case "ListMcpResources":
|
|
var input struct {
|
|
Server string `json:"server,omitempty"`
|
|
}
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &input)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_ListMcpResourcesToolCall{
|
|
ListMcpResourcesToolCall: &agentv1.ListMcpResourcesToolCall{
|
|
Args: &agentv1.ListMcpResourcesExecArgs{
|
|
Server: stringPtr(strings.TrimSpace(input.Server)),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
case "AskQuestion":
|
|
var args agentv1.AskQuestionArgs
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &args)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_AskQuestionToolCall{
|
|
AskQuestionToolCall: &agentv1.AskQuestionToolCall{Args: &args},
|
|
},
|
|
}
|
|
case "WebSearch":
|
|
var args agentv1.WebSearchArgs
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &args)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_WebSearchToolCall{
|
|
WebSearchToolCall: &agentv1.WebSearchToolCall{Args: &args},
|
|
},
|
|
}
|
|
case "WebFetch":
|
|
var args agentv1.WebFetchArgs
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &args)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_WebFetchToolCall{
|
|
WebFetchToolCall: &agentv1.WebFetchToolCall{Args: &args},
|
|
},
|
|
}
|
|
case "CreatePlan":
|
|
args, err := runtimecore.DecodeCreatePlanArgsJSON(invocation.ArgsJSON)
|
|
if err != nil {
|
|
args = &agentv1.CreatePlanArgs{}
|
|
}
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_CreatePlanToolCall{
|
|
CreatePlanToolCall: &agentv1.CreatePlanToolCall{Args: args},
|
|
},
|
|
}
|
|
case "GenerateImage":
|
|
carrier, _ := decodeGenerateImageToolCarrier(invocation.ArgsJSON)
|
|
args := buildGenerateImageArgsFromCarrier(carrier)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_GenerateImageToolCall{
|
|
GenerateImageToolCall: &agentv1.GenerateImageToolCall{Args: args},
|
|
},
|
|
}
|
|
case "SwitchMode":
|
|
var args agentv1.SwitchModeArgs
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &args)
|
|
args.ToolCallId = invocation.CallID
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_SwitchModeToolCall{
|
|
SwitchModeToolCall: &agentv1.SwitchModeToolCall{Args: &args},
|
|
},
|
|
}
|
|
case "FetchMcpResource":
|
|
var input struct {
|
|
Server string `json:"server"`
|
|
URI string `json:"uri"`
|
|
DownloadPath string `json:"downloadPath,omitempty"`
|
|
}
|
|
_ = json.Unmarshal(invocation.ArgsJSON, &input)
|
|
return &agentv1.ToolCall{
|
|
Tool: &agentv1.ToolCall_ReadMcpResourceToolCall{
|
|
ReadMcpResourceToolCall: &agentv1.ReadMcpResourceToolCall{
|
|
Args: &agentv1.ReadMcpResourceExecArgs{
|
|
Server: strings.TrimSpace(input.Server),
|
|
Uri: strings.TrimSpace(input.URI),
|
|
DownloadPath: stringPtr(strings.TrimSpace(input.DownloadPath)),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func forwarderCanonicalMCPToolLookupName(server string, toolName string) string {
|
|
trimmedServer := strings.TrimSpace(server)
|
|
trimmedToolName := strings.TrimSpace(toolName)
|
|
if trimmedToolName == "" {
|
|
return ""
|
|
}
|
|
if trimmedServer == "" {
|
|
return trimmedToolName
|
|
}
|
|
return trimmedServer + "-" + trimmedToolName
|
|
}
|
|
|
|
func forwarderStructValueMap(items map[string]any) map[string]*structpb.Value {
|
|
if len(items) == 0 {
|
|
return map[string]*structpb.Value{}
|
|
}
|
|
result := make(map[string]*structpb.Value, len(items))
|
|
for key, value := range items {
|
|
item, err := structpb.NewValue(value)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
result[key] = item
|
|
}
|
|
return result
|
|
}
|
|
|
|
// stringPtr 在字符串非空时返回指针,避免把空串写入 optional 字段。
|
|
func buildShellOutputNotificationConfig(input *struct {
|
|
Pattern string `json:"pattern"`
|
|
Reason string `json:"reason"`
|
|
DebounceMS *float64 `json:"debounce_ms,omitempty"`
|
|
NotificationLimit *int32 `json:"notification_limit,omitempty"`
|
|
}) *agentv1.ShellOutputNotificationConfig {
|
|
if input == nil {
|
|
return nil
|
|
}
|
|
pattern := strings.TrimSpace(input.Pattern)
|
|
reason := strings.TrimSpace(input.Reason)
|
|
if pattern == "" || reason == "" {
|
|
return nil
|
|
}
|
|
var debounce *float64
|
|
if input.DebounceMS != nil {
|
|
value := *input.DebounceMS / 1000
|
|
if value < 5 {
|
|
value = 5
|
|
}
|
|
debounce = &value
|
|
}
|
|
return &agentv1.ShellOutputNotificationConfig{
|
|
Pattern: pattern,
|
|
Reason: reason,
|
|
Debounce: debounce,
|
|
NotificationLimit: input.NotificationLimit,
|
|
}
|
|
}
|
|
|
|
func stringPtr(value string) *string {
|
|
if strings.TrimSpace(value) == "" {
|
|
return nil
|
|
}
|
|
next := strings.TrimSpace(value)
|
|
return &next
|
|
}
|
|
|
|
func literalStringPtr(value string) *string {
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
next := value
|
|
return &next
|
|
}
|