diff --git a/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go b/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go
index 38cb25a..87b61f7 100644
--- a/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go
+++ b/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go
@@ -6,6 +6,8 @@ import (
"strings"
"time"
+ "cs-agent/internal/pkg/toolx"
+
einotool "github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/adk"
@@ -56,10 +58,25 @@ func (h *RuntimeTraceHandler) WrapInvokableToolCall(_ context.Context, endpoint
item.ErrorMessage = err.Error()
}
h.collector.AddToolItem(item)
+ if metadata, ok := h.resolveToolMetadata(item.ToolName); ok && strings.TrimSpace(metadata.ToolCode) == toolx.BuiltinToolSearchToolCode {
+ h.collector.AddToolSearchItem(buildToolSearchTraceItem(argumentsInJSON, result, err))
+ }
return result, err
}, nil
}
+func (h *RuntimeTraceHandler) resolveToolMetadata(modelToolName string) (ToolMetadata, bool) {
+ if h == nil || h.toolMetadataBy == nil {
+ return ToolMetadata{}, false
+ }
+ modelToolName = strings.TrimSpace(modelToolName)
+ if modelToolName == "" {
+ return ToolMetadata{}, false
+ }
+ metadata, ok := h.toolMetadataBy[modelToolName]
+ return metadata, ok
+}
+
func parseToolArguments(argumentsInJSON string) map[string]any {
argumentsInJSON = strings.TrimSpace(argumentsInJSON)
if argumentsInJSON == "" {
@@ -83,3 +100,62 @@ func previewToolText(text string, limit int) string {
}
return string(runes[:limit]) + "..."
}
+
+func buildToolSearchTraceItem(argumentsInJSON string, result string, runErr error) ToolSearchTraceItem {
+ item := ToolSearchTraceItem{
+ Status: "ok",
+ }
+ args := parseToolArguments(argumentsInJSON)
+ item.Query = strings.TrimSpace(readToolSearchString(args, "query"))
+ item.TargetToolCode = strings.TrimSpace(readToolSearchString(args, "toolCode"))
+ if item.TargetToolCode != "" {
+ item.Action = "invoke"
+ item.TargetServerCode, item.TargetToolName = toolx.SplitMCPToolCode(item.TargetToolCode)
+ } else {
+ item.Action = "search"
+ }
+ if runErr != nil {
+ item.Status = "error"
+ item.ErrorMessage = runErr.Error()
+ return item
+ }
+ payload := make(map[string]any)
+ if err := json.Unmarshal([]byte(strings.TrimSpace(result)), &payload); err != nil {
+ return item
+ }
+ candidateItems, _ := payload["candidates"].([]any)
+ item.CandidateToolCodes = extractCandidateToolCodes(candidateItems)
+ return item
+}
+
+func readToolSearchString(data map[string]any, key string) string {
+ if len(data) == 0 {
+ return ""
+ }
+ value, ok := data[key]
+ if !ok {
+ return ""
+ }
+ text, _ := value.(string)
+ return text
+}
+
+func extractCandidateToolCodes(items []any) []string {
+ if len(items) == 0 {
+ return nil
+ }
+ ret := make([]string, 0, len(items))
+ for _, item := range items {
+ payload, ok := item.(map[string]any)
+ if !ok {
+ continue
+ }
+ toolCode, _ := payload["toolCode"].(string)
+ toolCode = strings.TrimSpace(toolCode)
+ if toolCode == "" {
+ continue
+ }
+ ret = append(ret, toolCode)
+ }
+ return ret
+}
diff --git a/internal/ai/runtime/internal/impl/callbacks/runlog_callback.go b/internal/ai/runtime/internal/impl/callbacks/runlog_callback.go
index e323b99..e67e71e 100644
--- a/internal/ai/runtime/internal/impl/callbacks/runlog_callback.go
+++ b/internal/ai/runtime/internal/impl/callbacks/runlog_callback.go
@@ -39,3 +39,13 @@ func (c *RuntimeTraceCollector) AddToolItem(item ToolTraceItem) {
c.Data.Tools.Count++
c.Data.Tools.Items = append(c.Data.Tools.Items, item)
}
+
+func (c *RuntimeTraceCollector) AddToolSearchItem(item ToolSearchTraceItem) {
+ if c == nil {
+ return
+ }
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ c.Data.ToolSearch.Count++
+ c.Data.ToolSearch.Items = append(c.Data.ToolSearch.Items, item)
+}
diff --git a/internal/ai/runtime/internal/impl/callbacks/trace_callback.go b/internal/ai/runtime/internal/impl/callbacks/trace_callback.go
index 05b5cc7..71eea7a 100644
--- a/internal/ai/runtime/internal/impl/callbacks/trace_callback.go
+++ b/internal/ai/runtime/internal/impl/callbacks/trace_callback.go
@@ -11,6 +11,17 @@ type ToolTraceItem struct {
ErrorMessage string `json:"errorMessage,omitempty"`
}
+type ToolSearchTraceItem struct {
+ Action string `json:"action,omitempty"`
+ Query string `json:"query,omitempty"`
+ TargetToolCode string `json:"targetToolCode,omitempty"`
+ TargetServerCode string `json:"targetServerCode,omitempty"`
+ TargetToolName string `json:"targetToolName,omitempty"`
+ CandidateToolCodes []string `json:"candidateToolCodes,omitempty"`
+ Status string `json:"status,omitempty"`
+ ErrorMessage string `json:"errorMessage,omitempty"`
+}
+
type RetrieverTraceItem struct {
Query string `json:"query,omitempty"`
KnowledgeBaseID int64 `json:"knowledgeBaseId,omitempty"`
@@ -47,6 +58,10 @@ type RuntimeTraceData struct {
Count int `json:"count,omitempty"`
Items []ToolTraceItem `json:"items,omitempty"`
} `json:"tools"`
+ ToolSearch struct {
+ Count int `json:"count,omitempty"`
+ Items []ToolSearchTraceItem `json:"items,omitempty"`
+ } `json:"toolSearch"`
Output struct {
ReplyText string `json:"replyText,omitempty"`
FinishReason string `json:"finishReason,omitempty"`
diff --git a/internal/ai/runtime/reply_service.go b/internal/ai/runtime/reply_service.go
index 316e94b..82c41ca 100644
--- a/internal/ai/runtime/reply_service.go
+++ b/internal/ai/runtime/reply_service.go
@@ -11,6 +11,7 @@ import (
"cs-agent/internal/models"
"cs-agent/internal/pkg/dto"
"cs-agent/internal/pkg/enums"
+ "cs-agent/internal/pkg/toolx"
"cs-agent/internal/repositories"
svc "cs-agent/internal/services"
@@ -331,6 +332,7 @@ func (s *aiReplyService) writeRunLog(startedAt time.Time, message models.Message
PlannedSkillCode: strings.TrimSpace(summaryPlannedSkillCode(summary)),
PlannedSkillName: strings.TrimSpace(summaryPlannedSkillName(summary)),
SkillRouteTrace: strings.TrimSpace(summarySkillRouteTrace(summary)),
+ ToolSearchTrace: extractToolSearchTrace(summary),
PlannedToolCode: plannedToolCode,
PlanReason: planReason,
InterruptType: firstInterruptType(summary),
@@ -378,10 +380,15 @@ func buildRunLogPlan(summary *Summary) (plannedAction, plannedToolCode, planReas
return "interrupt", "", "pending interrupt checkpoint expired"
}
if summary.Interrupted {
- return "tool", firstInvokedToolCode(summary), "agent interrupted and is waiting for user confirmation"
+ return "tool", summaryPrimaryToolCode(summary), "agent interrupted and is waiting for user confirmation"
}
if len(summary.InvokedToolCodes) > 0 {
- return "tool", strings.TrimSpace(summary.InvokedToolCodes[0]), "agent invoked MCP tool"
+ toolCode := summaryPrimaryToolCode(summary)
+ reason := "agent invoked MCP tool"
+ if toolCode != "" && toolCode != firstInvokedToolCode(summary) {
+ reason = "agent invoked dynamic tool via tool_search"
+ }
+ return "tool", toolCode, reason
}
if strings.TrimSpace(summary.ReplyText) != "" {
return "reply", "", "agent replied directly"
@@ -541,6 +548,68 @@ func firstInvokedToolCode(summary *Summary) string {
return ""
}
+func summaryPrimaryToolCode(summary *Summary) string {
+ if summary == nil {
+ return ""
+ }
+ toolCode := firstInvokedToolCode(summary)
+ if toolCode != toolx.BuiltinToolSearchToolCode {
+ return toolCode
+ }
+ if targetToolCode := firstToolSearchTargetToolCode(summary); targetToolCode != "" {
+ return targetToolCode
+ }
+ return toolCode
+}
+
+func extractToolSearchTrace(summary *Summary) string {
+ if summary == nil {
+ return ""
+ }
+ trace := parseRuntimeTraceData(summary.TraceData)
+ if len(trace.ToolSearch.Items) == 0 || len(trace.ToolSearch.Raw) == 0 {
+ return ""
+ }
+ return string(trace.ToolSearch.Raw)
+}
+
+func firstToolSearchTargetToolCode(summary *Summary) string {
+ trace := parseRuntimeTraceData(summary.TraceData)
+ for _, item := range trace.ToolSearch.Items {
+ toolCode := strings.TrimSpace(item.TargetToolCode)
+ if toolCode != "" {
+ return toolCode
+ }
+ }
+ return ""
+}
+
+type runtimeTraceProjection struct {
+ ToolSearch struct {
+ Raw json.RawMessage `json:"-"`
+ Items []struct {
+ TargetToolCode string `json:"targetToolCode"`
+ } `json:"items"`
+ } `json:"toolSearch"`
+}
+
+func parseRuntimeTraceData(raw string) runtimeTraceProjection {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return runtimeTraceProjection{}
+ }
+ var payload map[string]json.RawMessage
+ if err := json.Unmarshal([]byte(raw), &payload); err != nil {
+ return runtimeTraceProjection{}
+ }
+ var trace runtimeTraceProjection
+ if toolSearchRaw, ok := payload["toolSearch"]; ok && len(toolSearchRaw) > 0 {
+ trace.ToolSearch.Raw = append(json.RawMessage(nil), toolSearchRaw...)
+ _ = json.Unmarshal(toolSearchRaw, &trace.ToolSearch)
+ }
+ return trace
+}
+
func isCancellationReply(replyText string) bool {
replyText = strings.TrimSpace(replyText)
return strings.Contains(replyText, "已取消本次工单创建")
diff --git a/internal/builders/agent_run_log_builder.go b/internal/builders/agent_run_log_builder.go
index 94f40aa..6daa240 100644
--- a/internal/builders/agent_run_log_builder.go
+++ b/internal/builders/agent_run_log_builder.go
@@ -20,6 +20,7 @@ func BuildAgentRunLog(item *models.AgentRunLog) response.AgentRunLogResponse {
PlannedSkillCode: item.PlannedSkillCode,
PlannedSkillName: item.PlannedSkillName,
SkillRouteTrace: item.SkillRouteTrace,
+ ToolSearchTrace: item.ToolSearchTrace,
PlannedToolCode: item.PlannedToolCode,
PlanReason: item.PlanReason,
InterruptType: item.InterruptType,
diff --git a/internal/models/models.go b/internal/models/models.go
index 8491b9d..d59d053 100644
--- a/internal/models/models.go
+++ b/internal/models/models.go
@@ -952,6 +952,7 @@ type AgentRunLog struct {
PlannedSkillCode string `gorm:"type:varchar(100);not null;default:'';index"`
PlannedSkillName string `gorm:"type:varchar(100);not null;default:''"`
SkillRouteTrace string `gorm:"type:text"`
+ ToolSearchTrace string `gorm:"type:text"`
PlannedToolCode string `gorm:"type:varchar(200);not null;default:'';index"`
PlanReason string `gorm:"type:varchar(500);not null;default:''"`
InterruptType string `gorm:"type:varchar(50);not null;default:'';index"`
diff --git a/internal/pkg/dto/response/skill_response.go b/internal/pkg/dto/response/skill_response.go
index 1745ede..8c93f42 100644
--- a/internal/pkg/dto/response/skill_response.go
+++ b/internal/pkg/dto/response/skill_response.go
@@ -48,6 +48,7 @@ type AgentRunLogResponse struct {
PlannedSkillCode string `json:"plannedSkillCode"`
PlannedSkillName string `json:"plannedSkillName"`
SkillRouteTrace string `json:"skillRouteTrace"`
+ ToolSearchTrace string `json:"toolSearchTrace"`
PlannedToolCode string `json:"plannedToolCode"`
PlanReason string `json:"planReason"`
InterruptType string `json:"interruptType"`
diff --git a/web/app/(console)/agent-run-logs/page.tsx b/web/app/(console)/agent-run-logs/page.tsx
index 4725f15..c981428 100644
--- a/web/app/(console)/agent-run-logs/page.tsx
+++ b/web/app/(console)/agent-run-logs/page.tsx
@@ -91,6 +91,10 @@ export default function DashboardAgentRunLogsPage() {
() => safeParseJSON(activeLog?.traceData ?? ""),
[activeLog?.traceData]
)
+ const activeToolSearchTrace = useMemo(
+ () => safeParseJSON(activeLog?.toolSearchTrace ?? ""),
+ [activeLog?.toolSearchTrace]
+ )
const aiAgentOptions = useMemo(
() => [
@@ -372,6 +376,14 @@ export default function DashboardAgentRunLogsPage() {
]}
/>
+
}
title="用户问题"
diff --git a/web/lib/api/admin.ts b/web/lib/api/admin.ts
index 5190b95..e76f8b4 100644
--- a/web/lib/api/admin.ts
+++ b/web/lib/api/admin.ts
@@ -365,6 +365,7 @@ export type AgentRunLog = {
plannedSkillCode: string
plannedSkillName: string
skillRouteTrace: string
+ toolSearchTrace: string
plannedToolCode: string
planReason: string
interruptType: string