Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions internal/backend/agent/bridge/exec/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ func (bridge *Bridge) OpenExec(openContext OpenExecContext, toolCall runtimecore
return bridge.openListMcpResources(toolCall)
case "FetchMcpResource":
return bridge.openReadMcpResource(toolCall)
case "create-agent":
return bridge.openCreateAgent(toolCall)
case "send-message-to-agent":
return bridge.openSendMessageToAgent(openContext, toolCall)
case "AWAIT":
return bridge.openSubagentAwait(toolCall)
default:
return nil, runtimecore.PendingExec{}, fmt.Errorf("unsupported exec tool: %s", toolCall.ToolName)
}
Expand Down Expand Up @@ -219,6 +225,22 @@ func (bridge *Bridge) ApplyExecClientMessage(msg *agentv1.ExecClientMessage, pen
result.ToolResultPayload = summarizeForceBackgroundShellResult(forceResult)
result.IsTerminal = true
return result, nil
case "force_background_subagent":
subagentResult := msg.GetForceBackgroundSubagentResult()
if subagentResult == nil {
return ExecApplyResult{}, fmt.Errorf("force background subagent result is required")
}
result.ToolResultPayload = summarizeForceBackgroundSubagentResult(subagentResult)
result.IsTerminal = true
return result, nil
case "subagent_await":
awaitResult := msg.GetSubagentAwaitResult()
if awaitResult == nil {
return ExecApplyResult{}, fmt.Errorf("subagent await result is required")
}
result.ToolResultPayload = summarizeSubagentAwaitResult(awaitResult)
result.IsTerminal = true
return result, nil
case "execute_hook_pre_compact":
hookResult := msg.GetExecuteHookResult()
if hookResult == nil {
Expand Down Expand Up @@ -821,6 +843,142 @@ func (bridge *Bridge) openTask(openContext OpenExecContext, toolCall runtimecore
}, nil
}

// openCreateAgent 构造 create-agent 对应的执行桥请求。
func (bridge *Bridge) openCreateAgent(toolCall runtimecore.ToolInvocation) (*agentv1.AgentServerMessage, runtimecore.PendingExec, error) {
if _, err := decodeArgsMap(toolCall.ArgsJSON); err != nil {
return nil, runtimecore.PendingExec{}, fmt.Errorf("decode create-agent args failed: %w", err)
}
messageID := bridge.nextID()
execID := fmt.Sprintf("exec-force-background-subagent-%d", time.Now().UnixNano())
serverMessage := &agentv1.AgentServerMessage{
Message: &agentv1.AgentServerMessage_ExecServerMessage{
ExecServerMessage: &agentv1.ExecServerMessage{
Id: messageID,
ExecId: execID,
Message: &agentv1.ExecServerMessage_ForceBackgroundSubagentArgs{
ForceBackgroundSubagentArgs: &agentv1.ForceBackgroundSubagentArgs{
ToolCallId: toolCall.CallID,
},
},
},
},
}
return serverMessage, runtimecore.PendingExec{
MessageID: messageID,
ExecID: execID,
ArgsJSON: append([]byte(nil), toolCall.ArgsJSON...),
ToolCallID: toolCall.CallID,
ExecKind: "force_background_subagent",
StreamState: "opened",
OpenedAt: time.Now().UTC(),
}, nil
}

// openSendMessageToAgent 构造 send-message-to-agent 对应的执行桥请求。
func (bridge *Bridge) openSendMessageToAgent(openContext OpenExecContext, toolCall runtimecore.ToolInvocation) (*agentv1.AgentServerMessage, runtimecore.PendingExec, error) {
args, err := decodeArgsMap(toolCall.ArgsJSON)
if err != nil {
return nil, runtimecore.PendingExec{}, fmt.Errorf("decode send-message-to-agent args failed: %w", err)
}
agentID := strings.TrimSpace(readStringArg(args, "agent_id", "agentId"))
prompt := strings.TrimSpace(readStringArg(args, "prompt"))
subagentType := strings.TrimSpace(readStringArg(args, "subagent_type", "subagentType"))
readonly := readBoolArg(args, "readonly", "readOnly")
parentConversationID := strings.TrimSpace(openContext.ConversationID)
requestedModelID := strings.TrimSpace(readStringArg(args, "model", "model_id", "modelId"))
modelID := requestedModelID
if subagentType != "" {
if override, _, ok := runtimecore.LookupSubagentModelOverride(openContext.SubagentModelOverrides, subagentType); ok {
switch strings.TrimSpace(override.Selection) {
case "disabled":
return nil, runtimecore.PendingExec{}, fmt.Errorf("subagent type %q is disabled by model override", subagentType)
case "model":
modelID = strings.TrimSpace(override.ModelID)
case "inherit":
modelID = strings.TrimSpace(openContext.ModelID)
}
}
}
if modelID == "" {
modelID = strings.TrimSpace(openContext.ModelID)
}

messageID := bridge.nextID()
now := time.Now().UTC()
execID := fmt.Sprintf("exec-subagent-%d", now.UnixNano())
serverMessage := &agentv1.AgentServerMessage{
Message: &agentv1.AgentServerMessage_ExecServerMessage{
ExecServerMessage: &agentv1.ExecServerMessage{
Id: messageID,
ExecId: execID,
Message: &agentv1.ExecServerMessage_SubagentArgs{
SubagentArgs: &agentv1.SubagentArgs{
ToolCallId: toolCall.CallID,
SubagentType: subagentType,
ModelId: modelID,
Prompt: prompt,
Readonly: readonly,
ResumeAgentId: stringPtrIfNonEmpty(agentID),
ParentConversationId: stringPtrIfNonEmpty(parentConversationID),
Mode: taskModeFromReadonly(readonly),
},
},
},
},
}
return serverMessage, runtimecore.PendingExec{
MessageID: messageID,
ExecID: execID,
ArgsJSON: append([]byte(nil), toolCall.ArgsJSON...),
ToolCallID: toolCall.CallID,
ExecKind: "subagent",
StreamState: "opened",
OpenedAt: now,
}, nil
}

// openSubagentAwait 构造 AWAIT 对应的执行桥请求。
func (bridge *Bridge) openSubagentAwait(toolCall runtimecore.ToolInvocation) (*agentv1.AgentServerMessage, runtimecore.PendingExec, error) {
args, err := decodeArgsMap(toolCall.ArgsJSON)
if err != nil {
return nil, runtimecore.PendingExec{}, fmt.Errorf("decode AWAIT args failed: %w", err)
}
agentID := strings.TrimSpace(readStringArg(args, "task_id", "taskId", "agent_id", "agentId"))
var timeoutMs uint32
if val, found, err := runtimecore.ReadUint32Arg(args, "block_until_ms", "blockUntilMs", "timeout_ms", "timeoutMs"); err == nil && found {
timeoutMs = val
} else if fval, ffound, err := runtimecore.ReadFloat64Arg(args, "block_until_ms", "blockUntilMs", "timeout_ms", "timeoutMs"); err == nil && ffound && fval > 0 {
timeoutMs = uint32(fval)
}

messageID := bridge.nextID()
now := time.Now().UTC()
execID := fmt.Sprintf("exec-subagent-await-%d", now.UnixNano())
serverMessage := &agentv1.AgentServerMessage{
Message: &agentv1.AgentServerMessage_ExecServerMessage{
ExecServerMessage: &agentv1.ExecServerMessage{
Id: messageID,
ExecId: execID,
Message: &agentv1.ExecServerMessage_SubagentAwaitArgs{
SubagentAwaitArgs: &agentv1.SubagentAwaitArgs{
AgentId: agentID,
TimeoutMs: timeoutMs,
},
},
},
},
}
return serverMessage, runtimecore.PendingExec{
MessageID: messageID,
ExecID: execID,
ArgsJSON: append([]byte(nil), toolCall.ArgsJSON...),
ToolCallID: toolCall.CallID,
ExecKind: "subagent_await",
StreamState: "opened",
OpenedAt: now,
}, nil
}

// openGrep 构造 Grep 对应的执行桥请求。
func (bridge *Bridge) openGrep(toolCall runtimecore.ToolInvocation) (*agentv1.AgentServerMessage, runtimecore.PendingExec, error) {
input, err := DecodeGrepToolArgs(toolCall.ArgsJSON, toolCall.CallID)
Expand Down Expand Up @@ -2166,6 +2324,56 @@ func summarizeForceBackgroundShellResult(result *agentv1.ForceBackgroundShellRes
}
}

func summarizeForceBackgroundSubagentResult(result *agentv1.ForceBackgroundSubagentResult) string {
if result == nil {
return ""
}
switch result.GetStatus() {
case agentv1.ForceBackgroundSubagentStatus_FORCE_BACKGROUND_SUBAGENT_STATUS_ACCEPTED:
return "force background subagent accepted"
case agentv1.ForceBackgroundSubagentStatus_FORCE_BACKGROUND_SUBAGENT_STATUS_NOT_FOUND:
return "force background subagent not found"
default:
return "force background subagent completed"
}
}

func summarizeSubagentAwaitResult(result *agentv1.SubagentAwaitResult) string {
if result == nil {
return "subagent await result missing"
}
switch item := result.GetResult().(type) {
case *agentv1.SubagentAwaitResult_Complete:
if item.Complete == nil {
return "subagent await complete missing"
}
finalMessage := strings.TrimSpace(item.Complete.GetFinalMessage())
transcriptPath := strings.TrimSpace(item.Complete.GetTranscriptPath())
if finalMessage != "" && transcriptPath != "" {
return fmt.Sprintf("%s (transcript: %s)", finalMessage, transcriptPath)
} else if finalMessage != "" {
return finalMessage
} else if transcriptPath != "" {
return fmt.Sprintf("transcript: %s", transcriptPath)
}
return "subagent await completed"
case *agentv1.SubagentAwaitResult_StillRunning:
return "subagent still running"
case *agentv1.SubagentAwaitResult_NotFound:
if item.NotFound != nil && strings.TrimSpace(item.NotFound.GetAgentId()) != "" {
return fmt.Sprintf("subagent not found: %s", strings.TrimSpace(item.NotFound.GetAgentId()))
}
return "subagent not found"
case *agentv1.SubagentAwaitResult_Error:
if item.Error != nil && strings.TrimSpace(item.Error.GetError()) != "" {
return fmt.Sprintf("subagent error: %s", strings.TrimSpace(item.Error.GetError()))
}
return "subagent error"
default:
return "unknown subagent await result"
}
}

// buildGrepCompletedToolCall 构造 Grep 对应的完成态 ToolCall。
func buildGrepCompletedToolCall(toolCallID string, argsJSON []byte, result *agentv1.GrepResult) *agentv1.ToolCall {
args, err := DecodeGrepToolArgs(argsJSON, toolCallID)
Expand Down
2 changes: 2 additions & 0 deletions internal/backend/agent/prompt/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,8 @@ func mapPromptMode(mode agentv1.AgentMode) (promptassets.Mode, error) {
return promptassets.ModeDebug, nil
case agentv1.AgentMode_AGENT_MODE_MULTITASK:
return promptassets.ModeMultitask, nil
case agentv1.AgentMode_AGENT_MODE_PROJECT:
return promptassets.ModeProjects, nil
default:
return "", fmt.Errorf("unsupported prompt compile mode: %s", mode.String())
}
Expand Down
24 changes: 24 additions & 0 deletions internal/backend/forwarder/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,30 @@ func buildStartedToolCall(invocation runtimecore.ToolInvocation) *agentv1.ToolCa
},
},
}
case "create-agent":
// create-agent 需要在客户端先注册 task_tool_call,
// 后续 force_background_subagent_args 才能按 tool_call_id 找到它。
payload, _ := decodeJSONObject(invocation.ArgsJSON)
title := strings.TrimSpace(stringValue(valueByAlias(payload, "title", "description")))
taskArgs := buildTaskArgsFromMap(payload)
if taskArgs.Description == "" {
taskArgs.Description = title
}
return &agentv1.ToolCall{
Tool: &agentv1.ToolCall_TaskToolCall{
TaskToolCall: &agentv1.TaskToolCall{
Args: taskArgs,
},
},
}
case "send-message-to-agent":
return &agentv1.ToolCall{
Tool: &agentv1.ToolCall_TaskToolCall{
TaskToolCall: &agentv1.TaskToolCall{
Args: buildTaskArgsFromJSON(invocation.ArgsJSON),
},
},
}
case "Ls":
var input struct {
Path string `json:"path"`
Expand Down
6 changes: 5 additions & 1 deletion internal/backend/forwarder/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3422,6 +3422,10 @@ func deriveToolNameFromPendingExec(pending runtimecore.PendingExec) string {
return "ForceBackgroundShell"
case "subagent":
return "Task"
case "subagent_await":
return "AWAIT"
case "force_background_subagent":
return "create-agent"
default:
return ""
}
Expand Down Expand Up @@ -3466,7 +3470,7 @@ func execKindFromToolName(name string) (string, bool) {

func isExecTool(name string) bool {
switch strings.TrimSpace(name) {
case "Read", "Write", "PatchEdit", "Delete", "Shell", "WriteShellStdin", "ForceBackgroundShell", "Grep", "Glob", "Ls", "ReadLints", "CallMcpTool", "FetchMcpResource", "Task":
case "Read", "Write", "PatchEdit", "Delete", "Shell", "WriteShellStdin", "ForceBackgroundShell", "Grep", "Glob", "Ls", "ReadLints", "CallMcpTool", "FetchMcpResource", "Task", "create-agent", "send-message-to-agent", "AWAIT":
return true
default:
return false
Expand Down
Loading