mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-18 03:57:06 +08:00
Implemented the buildShellToolCallDeltaMessage function to map client shell output to the delta format for Cursor's terminal bubble. This includes handling both stdout and stderr events. Updated the service to publish these messages when processing execution results. Additionally, added a test for enabling terminal output UI streaming in the bootstrap statsig configuration.
664 lines
20 KiB
Go
664 lines
20 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,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildShellToolCallDeltaMessage maps client shell output to the delta consumed by Cursor's terminal bubble.
|
|
func buildShellToolCallDeltaMessage(callID string, modelCallID string, output *agentv1.ShellOutputDeltaUpdate) *agentv1.AgentServerMessage {
|
|
if output == nil {
|
|
return nil
|
|
}
|
|
var delta *agentv1.ShellToolCallDelta
|
|
switch event := output.GetEvent().(type) {
|
|
case *agentv1.ShellOutputDeltaUpdate_Stdout:
|
|
content := event.Stdout.GetData()
|
|
if content == "" {
|
|
return nil
|
|
}
|
|
delta = &agentv1.ShellToolCallDelta{
|
|
Delta: &agentv1.ShellToolCallDelta_Stdout{
|
|
Stdout: &agentv1.ShellToolCallStdoutDelta{Content: content},
|
|
},
|
|
}
|
|
case *agentv1.ShellOutputDeltaUpdate_Stderr:
|
|
content := event.Stderr.GetData()
|
|
if content == "" {
|
|
return nil
|
|
}
|
|
delta = &agentv1.ShellToolCallDelta{
|
|
Delta: &agentv1.ShellToolCallDelta_Stderr{
|
|
Stderr: &agentv1.ShellToolCallStderrDelta{Content: content},
|
|
},
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
return buildToolCallDeltaMessage(callID, modelCallID, &agentv1.ToolCallDelta{
|
|
Delta: &agentv1.ToolCallDelta_ShellToolCallDelta{ShellToolCallDelta: 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
|
|
}
|