• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

smallnest / goclaw / 21978860214

13 Feb 2026 07:45AM UTC coverage: 5.772% (+0.008%) from 5.764%
21978860214

push

github

chaoyuepan
improve web fetch

4 of 24 new or added lines in 10 files covered. (16.67%)

221 existing lines in 4 files now uncovered.

1517 of 26284 relevant lines covered (5.77%)

0.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/cli/commands/tui.go
1
package commands
2

3
import (
4
        "context"
5
        "fmt"
6
        "io"
7
        "os"
8
        "sort"
9
        "strconv"
10
        "strings"
11
        "time"
12

13
        "github.com/ergochat/readline"
14
        "github.com/smallnest/goclaw/agent"
15
        "github.com/smallnest/goclaw/agent/tools"
16
        "github.com/smallnest/goclaw/bus"
17
        "github.com/smallnest/goclaw/cli/input"
18
        "github.com/smallnest/goclaw/config"
19
        "github.com/smallnest/goclaw/internal"
20
        "github.com/smallnest/goclaw/internal/logger"
21
        "github.com/smallnest/goclaw/providers"
22
        "github.com/smallnest/goclaw/session"
23
        "github.com/spf13/cobra"
24
        "go.uber.org/zap"
25
)
26

27
// TUIAgent wraps Agent for TUI with additional functionality
28
type TUIAgent struct {
29
        *agent.Agent
30
        sessionMgr    *session.Manager
31
        sessionKey    string
32
        skillsLoader  *agent.SkillsLoader
33
        maxIterations int
34
        cmdRegistry   *CommandRegistry
35
}
36

37
// NewTUIAgent creates a new TUI agent
38
func NewTUIAgent(
39
        messageBus *bus.MessageBus,
40
        sessionMgr *session.Manager,
41
        provider providers.Provider,
42
        contextBuilder *agent.ContextBuilder,
43
        workspace string,
44
        maxIterations int,
45
        skillsLoader *agent.SkillsLoader,
46
) (*TUIAgent, error) {
×
47
        toolRegistry := agent.NewToolRegistry()
×
48

×
49
        // Register file system tool
×
50
        fsTool := tools.NewFileSystemTool([]string{}, []string{}, workspace)
×
51
        for _, tool := range fsTool.GetTools() {
×
52
                _ = toolRegistry.RegisterExisting(tool)
×
53
        }
×
54

55
        // Register use_skill tool
56
        _ = toolRegistry.RegisterExisting(tools.NewUseSkillTool())
×
57

×
58
        // Register shell tool
×
59
        shellTool := tools.NewShellTool(
×
60
                true,                   // enabled
×
61
                []string{},             // allowedCmds
×
62
                []string{},             // deniedCmds
×
63
                120,                    // timeout
×
64
                workspace,              // workingDir
×
65
                config.SandboxConfig{}, // sandbox
×
66
        )
×
67
        for _, tool := range shellTool.GetTools() {
×
68
                _ = toolRegistry.RegisterExisting(tool)
×
69
        }
×
70

71
        // Register web tool
72
        webTool := tools.NewWebTool("", "", 30)
×
73
        for _, tool := range webTool.GetTools() {
×
74
                _ = toolRegistry.RegisterExisting(tool)
×
75
        }
×
76

77
        // Register smart search
78
        _ = toolRegistry.RegisterExisting(tools.NewSmartSearch(webTool, true, 30).GetTool())
×
79

×
80
        // Register browser tool
×
81
        browserTool := tools.NewBrowserTool(true, 30)
×
82
        for _, tool := range browserTool.GetTools() {
×
83
                _ = toolRegistry.RegisterExisting(tool)
×
84
        }
×
85

86
        // Create Agent
87
        newAgent, err := agent.NewAgent(&agent.NewAgentConfig{
×
88
                Bus:          messageBus,
×
89
                Provider:     provider,
×
90
                SessionMgr:   sessionMgr,
×
91
                Tools:        toolRegistry,
×
92
                Context:      contextBuilder,
×
93
                Workspace:    workspace,
×
94
                MaxIteration: maxIterations,
×
95
                SkillsLoader: skillsLoader,
×
96
        })
×
97
        if err != nil {
×
98
                return nil, err
×
99
        }
×
100

101
        return &TUIAgent{
×
102
                Agent:         newAgent,
×
103
                sessionMgr:    sessionMgr,
×
104
                sessionKey:    "",
×
105
                skillsLoader:  skillsLoader,
×
106
                maxIterations: maxIterations,
×
107
                cmdRegistry:   &CommandRegistry{},
×
108
        }, nil
×
109
}
110

111
var (
112
        tuiURL          string
113
        tuiToken        string
114
        tuiPassword     string
115
        tuiSession      string
116
        tuiDeliver      bool
117
        tuiThinking     bool
118
        tuiMessage      string
119
        tuiTimeoutMs    int
120
        tuiHistoryLimit int
121
)
122

123
// TUICommand returns the tui command
124
func TUICommand() *cobra.Command {
×
125
        cmd := &cobra.Command{
×
126
                Use:   "tui",
×
127
                Short: "Open Terminal UI for goclaw",
×
128
                Long:  `Open an interactive terminal UI for interacting with goclaw agent.`,
×
129
                Run:   runTUI,
×
130
        }
×
131

×
132
        cmd.Flags().StringVar(&tuiURL, "url", "", "Gateway URL (default: ws://localhost:28789)")
×
133
        cmd.Flags().StringVar(&tuiToken, "token", "", "Authentication token")
×
134
        cmd.Flags().StringVar(&tuiPassword, "password", "", "Password for authentication")
×
135
        cmd.Flags().StringVar(&tuiSession, "session", "", "Session ID to resume")
×
136
        cmd.Flags().BoolVar(&tuiDeliver, "deliver", false, "Enable message delivery notifications")
×
137
        cmd.Flags().BoolVar(&tuiThinking, "thinking", false, "Show thinking indicator")
×
138
        cmd.Flags().StringVar(&tuiMessage, "message", "", "Send message on start")
×
139
        cmd.Flags().IntVar(&tuiTimeoutMs, "timeout-ms", 600000, "Timeout in milliseconds")
×
140
        cmd.Flags().IntVar(&tuiHistoryLimit, "history-limit", 50, "History limit")
×
141

×
142
        return cmd
×
143
}
×
144

145
// runTUI runs the terminal UI
146
func runTUI(cmd *cobra.Command, args []string) {
×
147
        // 确保内置技能被复制到用户目录
×
148
        if err := internal.EnsureBuiltinSkills(); err != nil {
×
149
                fmt.Fprintf(os.Stderr, "Warning: Failed to ensure builtin skills: %v\n", err)
×
150
        }
×
151

152
        // Load configuration
153
        cfg, err := config.Load("")
×
154
        if err != nil {
×
155
                fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
×
156
                os.Exit(1)
×
157
        }
×
158

159
        // Initialize logger
160
        logLevel := "info"
×
161
        if tuiThinking {
×
162
                logLevel = "debug"
×
163
        }
×
164
        if err := logger.Init(logLevel, false); err != nil {
×
165
                fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err)
×
166
                os.Exit(1)
×
167
        }
×
168
        defer logger.Sync() // nolint:errcheck
×
169

×
170
        fmt.Println("🐾 goclaw Terminal UI")
×
171
        fmt.Println()
×
172

×
173
        // Create workspace
×
174
        workspace := os.Getenv("HOME") + "/.goclaw/workspace"
×
175

×
176
        // Create message bus
×
177
        messageBus := bus.NewMessageBus(100)
×
178
        defer messageBus.Close()
×
179

×
180
        // Create session manager
×
181
        sessionDir := os.Getenv("HOME") + "/.goclaw/sessions"
×
182
        sessionMgr, err := session.NewManager(sessionDir)
×
183
        if err != nil {
×
184
                fmt.Fprintf(os.Stderr, "Failed to create session manager: %v\n", err)
×
185
                os.Exit(1)
×
186
        }
×
187

188
        // Create memory store
189
        memoryStore := agent.NewMemoryStore(workspace)
×
190
        _ = memoryStore.EnsureBootstrapFiles()
×
191

×
192
        // Create context builder
×
193
        contextBuilder := agent.NewContextBuilder(memoryStore, workspace)
×
194

×
195
        // Create LLM provider
×
196
        provider, err := providers.NewProvider(cfg)
×
197
        if err != nil {
×
198
                fmt.Fprintf(os.Stderr, "Failed to create LLM provider: %v\n", err)
×
199
                os.Exit(1)
×
200
        }
×
201
        defer provider.Close()
×
202

×
203
        // Create skills loader
×
204
        goclawDir := os.Getenv("HOME") + "/.goclaw"
×
205
        skillsDir := goclawDir + "/skills"
×
206
        skillsLoader := agent.NewSkillsLoader(goclawDir, []string{skillsDir})
×
207
        if err := skillsLoader.Discover(); err != nil {
×
208
                logger.Warn("Failed to discover skills", zap.Error(err))
×
209
        } else {
×
210
                skills := skillsLoader.List()
×
211
                if len(skills) > 0 {
×
212
                        logger.Info("Skills loaded", zap.Int("count", len(skills)))
×
213
                }
×
214
        }
215

216
        // Create TUI agent
217
        maxIterations := cfg.Agents.Defaults.MaxIterations
×
218
        if maxIterations == 0 {
×
219
                maxIterations = 15
×
220
        }
×
221

222
        tuiAgent, err := NewTUIAgent(messageBus, sessionMgr, provider, contextBuilder, workspace, maxIterations, skillsLoader)
×
223
        if err != nil {
×
224
                fmt.Fprintf(os.Stderr, "Failed to create TUI agent: %v\n", err)
×
225
                os.Exit(1)
×
226
        }
×
227

228
        // Start agent (starts event dispatcher)
229
        agentCtx, agentCancel := context.WithCancel(context.Background())
×
230
        if err := tuiAgent.Start(agentCtx); err != nil {
×
231
                logger.Error("Failed to start agent", zap.Error(err))
×
232
        }
×
233
        defer func() {
×
234
                agentCancel()
×
NEW
235
                _ = tuiAgent.Stop()
×
236
        }()
×
237

238
        // Always create a new session (unless explicitly specified)
239
        sessionKey := tuiSession
×
240
        if sessionKey == "" {
×
241
                // Always create a fresh session with timestamp
×
242
                sessionKey = "tui:" + strconv.FormatInt(time.Now().Unix(), 10)
×
243
        }
×
244
        tuiAgent.sessionKey = sessionKey
×
245

×
246
        sess, err := sessionMgr.GetOrCreate(sessionKey)
×
247
        if err != nil {
×
248
                fmt.Fprintf(os.Stderr, "Failed to create session: %v\n", err)
×
249
                os.Exit(1)
×
250
        }
×
251

252
        fmt.Printf("New Session: %s\n", sessionKey)
×
253
        fmt.Printf("History limit: %d\n", tuiHistoryLimit)
×
254
        fmt.Printf("Timeout: %d ms\n", tuiTimeoutMs)
×
255
        fmt.Println()
×
256

×
257
        // Create context
×
258
        ctx, cancel := context.WithCancel(context.Background())
×
259
        defer cancel()
×
260

×
261
        // Create command registry for slash commands
×
262
        cmdRegistry := NewCommandRegistry()
×
263
        cmdRegistry.SetSessionManager(sessionMgr)
×
264
        cmdRegistry.SetTUIAgent(tuiAgent)
×
265

×
266
        tuiAgent.cmdRegistry = cmdRegistry
×
267

×
268
        // Get orchestrator for running messages
×
269
        orchestrator := tuiAgent.GetOrchestrator()
×
270

×
271
        // Handle message flag
×
272
        if tuiMessage != "" {
×
273
                fmt.Printf("Sending message: %s\n", tuiMessage)
×
274
                timeout := time.Duration(tuiTimeoutMs) * time.Millisecond
×
275
                msgCtx, msgCancel := context.WithTimeout(ctx, timeout)
×
276
                defer msgCancel()
×
277

×
278
                response := processTUIDialogue(msgCtx, sess, orchestrator, tuiHistoryLimit)
×
279
                if response != "" {
×
280
                        fmt.Println("\n" + response + "\n")
×
281
                        _ = sessionMgr.Save(sess)
×
282
                }
×
283

284
                if !tuiDeliver {
×
285
                        return
×
286
                }
×
287
        }
288

289
        // Start interactive mode
290
        fmt.Println("Starting interactive TUI mode...")
×
291
        fmt.Println("Press Ctrl+C to exit")
×
292
        fmt.Println()
×
293
        fmt.Println("Arrow keys: ↑/↓ for history, ←/→ for edit")
×
294
        fmt.Println()
×
295

×
296
        // Create persistent readline instance for history navigation
×
297
        rl, err := input.NewReadline("➤ ")
×
298
        if err != nil {
×
299
                fmt.Fprintf(os.Stderr, "Failed to create readline: %v\n", err)
×
300
                os.Exit(1)
×
301
        }
×
302
        defer rl.Close()
×
303

×
304
        // Initialize history from session
×
305
        input.InitReadlineHistory(rl, getUserInputHistory(sess))
×
306

×
307
        // Input loop with persistent readline
×
308
        fmt.Println("Enter your message (or /help for commands):")
×
309
        for {
×
310
                line, err := rl.ReadLine()
×
311
                if err != nil {
×
312
                        // ergochat/readline returns io.EOF for Ctrl+C
×
313
                        if err == readline.ErrInterrupt || err == io.EOF {
×
314
                                fmt.Println("\nGoodbye!")
×
315
                                break
×
316
                        }
317
                        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
×
318
                        continue
×
319
                }
320

321
                // Save non-empty input to history
322
                if line != "" {
×
323
                        _ = rl.SaveToHistory(line)
×
324
                }
×
325

326
                if line == "" {
×
327
                        continue
×
328
                }
329

330
                // Check for commands
331
                result, isCommand, shouldExit := cmdRegistry.Execute(line)
×
332
                if isCommand {
×
333
                        if shouldExit {
×
334
                                fmt.Println("Goodbye!")
×
335
                                break
×
336
                        }
337
                        if result != "" {
×
338
                                fmt.Println(result)
×
339
                        }
×
340
                        continue
×
341
                }
342

343
                // Add user message
344
                sess.AddMessage(session.Message{
×
345
                        Role:    "user",
×
346
                        Content: line,
×
347
                })
×
348

×
349
                // Run agent with orchestrator
×
350
                timeout := time.Duration(tuiTimeoutMs) * time.Millisecond
×
351
                msgCtx, msgCancel := context.WithTimeout(ctx, timeout)
×
352
                defer msgCancel()
×
353

×
354
                response := processTUIDialogue(msgCtx, sess, orchestrator, tuiHistoryLimit)
×
355

×
356
                if response != "" {
×
357
                        fmt.Println("\n" + response + "\n")
×
358
                        _ = sessionMgr.Save(sess)
×
359
                }
×
360

361
                // Force readline to refresh terminal state
362
                rl.Refresh()
×
363
        }
364
}
365

366
// processTUIDialogue 处理 TUI 对话(使用 Orchestrator)
367
func processTUIDialogue(
368
        ctx context.Context,
369
        sess *session.Session,
370
        orchestrator *agent.Orchestrator,
371
        historyLimit int,
372
) string {
×
373
        // Load history messages
×
374
        history := sess.GetHistory(historyLimit)
×
375
        if historyLimit < 0 || historyLimit > 1000 {
×
376
                history = sess.GetHistory(-1) // unlimited
×
377
        }
×
378

379
        // Convert session messages to agent messages
380
        agentMsgs := sessionMessagesToAgentMessages(history)
×
381

×
382
        // Run orchestrator
×
383
        finalMessages, err := orchestrator.Run(ctx, agentMsgs)
×
384
        if err != nil {
×
385
                return fmt.Sprintf("Error: %v", err)
×
386
        }
×
387

388
        // Update session with new messages
389
        // Only save new messages (not the history)
390
        historyLen := len(history)
×
391
        if len(finalMessages) > historyLen {
×
392
                newMessages := finalMessages[historyLen:]
×
393
                for _, msg := range newMessages {
×
394
                        sessMsg := session.Message{
×
395
                                Role:      string(msg.Role),
×
396
                                Content:   extractAgentMessageText(msg),
×
397
                                Timestamp: time.Unix(msg.Timestamp/1000, 0),
×
398
                        }
×
399

×
400
                        // Handle tool calls in assistant messages
×
401
                        if msg.Role == "assistant" {
×
402
                                for _, block := range msg.Content {
×
403
                                        if tc, ok := block.(agent.ToolCallContent); ok {
×
404
                                                sessMsg.ToolCalls = append(sessMsg.ToolCalls, session.ToolCall{
×
405
                                                        ID:     tc.ID,
×
406
                                                        Name:   tc.Name,
×
407
                                                        Params: convertToMapParams(tc.Arguments),
×
UNCOV
408
                                                })
×
UNCOV
409
                                        }
×
410
                                }
411
                        }
412

413
                        // Handle tool result messages
414
                        if msg.Role == "tool" || msg.Role == agent.RoleToolResult {
×
415
                                if id, ok := msg.Metadata["tool_call_id"].(string); ok {
×
416
                                        sessMsg.ToolCallID = id
×
UNCOV
417
                                        sessMsg.Role = "tool"
×
UNCOV
418
                                }
×
419
                                // Preserve tool_name in metadata for validation
UNCOV
420
                                if toolName, ok := msg.Metadata["tool_name"].(string); ok {
×
UNCOV
421
                                        if sessMsg.Metadata == nil {
×
UNCOV
422
                                                sessMsg.Metadata = make(map[string]interface{})
×
UNCOV
423
                                        }
×
424
                                        sessMsg.Metadata["tool_name"] = toolName
×
425
                                }
426
                        }
427

428
                        sess.AddMessage(sessMsg)
×
429
                }
430
        }
431

432
        // Extract final assistant response
UNCOV
433
        if len(finalMessages) > 0 {
×
UNCOV
434
                lastMsg := finalMessages[len(finalMessages)-1]
×
UNCOV
435
                if lastMsg.Role == "assistant" {
×
UNCOV
436
                        return extractAgentMessageText(lastMsg)
×
UNCOV
437
                }
×
438
        }
439

UNCOV
440
        return ""
×
441
}
442

443
// runAgentIteration runs a single agent iteration (copied from chat.go)
444
//
445
//nolint:unused
446
func runAgentIteration(
447
        ctx context.Context,
448
        sess *session.Session,
449
        provider providers.Provider,
450
        contextBuilder *agent.ContextBuilder,
451
        toolRegistry *tools.Registry,
452
        skillsLoader *agent.SkillsLoader,
453
        maxIterations int,
454
        cmdRegistry *CommandRegistry,
UNCOV
455
) (string, error) {
×
456
        iteration := 0
×
457
        var lastResponse string
×
458

×
459
        // 重置停止标志
×
460
        if cmdRegistry != nil {
×
461
                cmdRegistry.ResetStop()
×
462
        }
×
463

464
        // 创建失败追踪器
465
        failureTracker := NewFailureTracker()
×
466

×
467
        // 获取可用的工具名称列表(用于错误提示)
×
468
        availableTools := getAvailableToolNames(toolRegistry)
×
469

×
470
        // Get loaded skills
×
471
        loadedSkills := getLoadedSkills(sess)
×
472

×
473
        for iteration < maxIterations {
×
474
                iteration++
×
UNCOV
475
                logger.Debug("Agent iteration",
×
UNCOV
476
                        zap.Int("iteration", iteration),
×
477
                        zap.Int("max_iterations", maxIterations))
×
478

×
479
                // 检查停止标志
×
480
                if cmdRegistry != nil && cmdRegistry.IsStopped() {
×
UNCOV
481
                        logger.Info("Agent run stopped by /stop command")
×
UNCOV
482
                        return lastResponse, nil
×
483
                }
×
484

485
                // Get available skills
486
                var skills []*agent.Skill
×
487
                if skillsLoader != nil {
×
488
                        skills = skillsLoader.List()
×
489
                }
×
490

491
                // Build messages
492
                history := sess.GetHistory(tuiHistoryLimit)
×
493

×
494
                // 检查是否需要添加错误处理指导
×
495
                var errorGuidance string
×
496
                if shouldUseErrorGuidance(history) {
×
497
                        failedTools := failureTracker.GetFailedToolNames()
×
498
                        errorGuidance = "\n\n## 重要提示\n\n"
×
UNCOV
499
                        errorGuidance += "检测到工具调用连续失败。请仔细分析错误原因,并尝试以下策略:\n"
×
UNCOV
500
                        errorGuidance += "1. 检查失败的工具是否使用了正确的参数\n"
×
UNCOV
501
                        errorGuidance += "2. 尝试使用其他可用的工具完成任务(参考上面的工具列表)\n"
×
502
                        errorGuidance += "3. 如果所有工具都无法完成任务,向用户说明情况\n"
×
503
                        if len(failedTools) > 0 {
×
504
                                errorGuidance += fmt.Sprintf("\n**失败的工具**: %s\n", strings.Join(failedTools, ", "))
×
505
                        }
×
506
                        logger.Info("Added error guidance due to consecutive failures",
×
507
                                zap.Strings("failed_tools", failedTools))
×
508
                }
509

510
                // 如果有错误指导,追加到最后一条用户消息中
UNCOV
511
                if errorGuidance != "" && len(history) > 0 {
×
512
                        // 找到最后一条用户消息并追加错误指导
×
513
                        for i := len(history) - 1; i >= 0; i-- {
×
514
                                if history[i].Role == "user" {
×
515
                                        history[i].Content += errorGuidance
×
516
                                        break
×
517
                                }
518
                        }
519
                }
520

521
                messages := contextBuilder.BuildMessages(history, "", skills, loadedSkills)
×
522
                providerMessages := make([]providers.Message, len(messages))
×
523
                for i, msg := range messages {
×
524
                        var tcs []providers.ToolCall
×
525
                        for _, tc := range msg.ToolCalls {
×
526
                                tcs = append(tcs, providers.ToolCall{
×
527
                                        ID:     tc.ID,
×
528
                                        Name:   tc.Name,
×
UNCOV
529
                                        Params: tc.Params,
×
UNCOV
530
                                })
×
UNCOV
531
                        }
×
532
                        providerMessages[i] = providers.Message{
×
533
                                Role:       msg.Role,
×
534
                                Content:    msg.Content,
×
535
                                ToolCallID: msg.ToolCallID,
×
536
                                ToolCalls:  tcs,
×
537
                        }
×
538
                }
539

540
                // Prepare tool definitions
541
                var toolDefs []providers.ToolDefinition
×
UNCOV
542
                if toolRegistry != nil {
×
UNCOV
543
                        toolList := toolRegistry.List()
×
UNCOV
544
                        for _, t := range toolList {
×
545
                                toolDefs = append(toolDefs, providers.ToolDefinition{
×
546
                                        Name:        t.Name(),
×
547
                                        Description: t.Description(),
×
548
                                        Parameters:  t.Parameters(),
×
UNCOV
549
                                })
×
UNCOV
550
                        }
×
551
                }
552

553
                // Call LLM
554
                response, err := provider.Chat(ctx, providerMessages, toolDefs)
×
555
                if err != nil {
×
556
                        return "", fmt.Errorf("LLM call failed: %w", err)
×
557
                }
×
558

559
                // Check for tool calls
560
                if len(response.ToolCalls) > 0 {
×
561
                        logger.Debug("LLM returned tool calls",
×
562
                                zap.Int("count", len(response.ToolCalls)),
×
563
                                zap.Int("iteration", iteration))
×
564

×
565
                        var assistantToolCalls []session.ToolCall
×
566
                        for _, tc := range response.ToolCalls {
×
567
                                assistantToolCalls = append(assistantToolCalls, session.ToolCall{
×
568
                                        ID:     tc.ID,
×
569
                                        Name:   tc.Name,
×
570
                                        Params: tc.Params,
×
571
                                })
×
572
                        }
×
573
                        sess.AddMessage(session.Message{
×
574
                                Role:      "assistant",
×
575
                                Content:   response.Content,
×
576
                                ToolCalls: assistantToolCalls,
×
577
                        })
×
578

×
579
                        // Execute tool calls
×
580
                        hasNewSkill := false
×
581
                        for _, tc := range response.ToolCalls {
×
582
                                logger.Debug("Executing tool",
×
583
                                        zap.String("tool", tc.Name),
×
584
                                        zap.Int("iteration", iteration))
×
585

×
586
                                fmt.Fprint(os.Stderr, ".")
×
587
                                result, err := toolRegistry.Execute(ctx, tc.Name, tc.Params)
×
588
                                fmt.Fprint(os.Stderr, "")
×
589

×
590
                                if err != nil {
×
UNCOV
591
                                        logger.Error("Tool execution failed",
×
UNCOV
592
                                                zap.String("tool", tc.Name),
×
593
                                                zap.Error(err))
×
594
                                        failureTracker.RecordFailure(tc.Name)
×
595
                                        // 使用增强的错误格式化
×
596
                                        result = formatToolError(tc.Name, tc.Params, err, availableTools)
×
597
                                } else {
×
598
                                        failureTracker.RecordSuccess(tc.Name)
×
UNCOV
599
                                }
×
600

601
                                // Check for use_skill
602
                                if tc.Name == "use_skill" {
×
603
                                        hasNewSkill = true
×
604
                                        if skillName, ok := tc.Params["skill_name"].(string); ok {
×
605
                                                loadedSkills = append(loadedSkills, skillName)
×
606
                                                setLoadedSkills(sess, loadedSkills)
×
607
                                        }
×
608
                                }
609

UNCOV
610
                                sess.AddMessage(session.Message{
×
611
                                        Role:       "tool",
×
612
                                        Content:    result,
×
UNCOV
613
                                        ToolCallID: tc.ID,
×
614
                                        Metadata: map[string]interface{}{
×
UNCOV
615
                                                "tool_name": tc.Name,
×
UNCOV
616
                                        },
×
UNCOV
617
                                })
×
618
                        }
619

UNCOV
620
                        if hasNewSkill {
×
UNCOV
621
                                continue
×
622
                        }
623
                        continue
×
624
                }
625

626
                // No tool calls, return response
627
                lastResponse = response.Content
×
UNCOV
628
                break
×
629
        }
630

631
        if iteration >= maxIterations {
×
632
                logger.Warn("Agent reached max iterations",
×
633
                        zap.Int("max", maxIterations))
×
634
        }
×
635

636
        return lastResponse, nil
×
637
}
638

639
// getLoadedSkills from session
640
//
641
//nolint:unused
UNCOV
642
func getLoadedSkills(sess *session.Session) []string {
×
UNCOV
643
        if sess.Metadata == nil {
×
644
                return []string{}
×
645
        }
×
646
        if v, ok := sess.Metadata["loaded_skills"].([]string); ok {
×
647
                return v
×
648
        }
×
UNCOV
649
        return []string{}
×
650
}
651

652
// setLoadedSkills in session
653
//
654
//nolint:unused
655
func setLoadedSkills(sess *session.Session, skills []string) {
×
656
        if sess.Metadata == nil {
×
657
                sess.Metadata = make(map[string]interface{})
×
658
        }
×
659
        sess.Metadata["loaded_skills"] = skills
×
660
}
661

662
// getUserInputHistory extracts user message history for readline
UNCOV
663
func getUserInputHistory(sess *session.Session) []string {
×
UNCOV
664
        history := sess.GetHistory(100)
×
665
        userInputs := make([]string, 0, len(history))
×
UNCOV
666

×
UNCOV
667
        // Extract only user messages (in reverse order - most recent first)
×
UNCOV
668
        for i := len(history) - 1; i >= 0; i-- {
×
669
                if history[i].Role == "user" {
×
670
                        userInputs = append(userInputs, history[i].Content)
×
671
                }
×
672
        }
673

UNCOV
674
        return userInputs
×
675
}
676

677
// findMostRecentTUISession finds the most recently updated tui session
678
//
679
//nolint:unused
680
func findMostRecentTUISession(mgr *session.Manager) string {
×
681
        keys, err := mgr.List()
×
682
        if err != nil {
×
683
                return ""
×
684
        }
×
685

686
        // Filter and collect tui sessions with their update time
687
        type sessionInfo struct {
×
UNCOV
688
                key       string
×
UNCOV
689
                updatedAt time.Time
×
UNCOV
690
        }
×
691

×
692
        var tuiSessions []sessionInfo
×
693
        for _, key := range keys {
×
UNCOV
694
                // Only consider sessions starting with "tui:" or "tui_"
×
UNCOV
695
                if !strings.HasPrefix(key, "tui:") && !strings.HasPrefix(key, "tui_") {
×
696
                        continue
×
697
                }
698

699
                // Load the session to get its update time
UNCOV
700
                sess, err := mgr.GetOrCreate(key)
×
UNCOV
701
                if err != nil {
×
UNCOV
702
                        continue
×
703
                }
704

705
                tuiSessions = append(tuiSessions, sessionInfo{
×
UNCOV
706
                        key:       key,
×
UNCOV
707
                        updatedAt: sess.UpdatedAt,
×
708
                })
×
709
        }
710

711
        // If no tui sessions found, return empty
712
        if len(tuiSessions) == 0 {
×
UNCOV
713
                return ""
×
UNCOV
714
        }
×
715

716
        // Sort by updated time (most recent first)
UNCOV
717
        sort.Slice(tuiSessions, func(i, j int) bool {
×
UNCOV
718
                return tuiSessions[i].updatedAt.After(tuiSessions[j].updatedAt)
×
UNCOV
719
        })
×
720

UNCOV
721
        return tuiSessions[0].key
×
722
}
723

724
// FailureTracker 追踪工具调用失败
725
type FailureTracker struct {
726
        toolFailures map[string]int // tool_name -> failure count
727
        totalCount   int
728
}
729

730
// NewFailureTracker 创建失败追踪器
731
func NewFailureTracker() *FailureTracker {
×
732
        return &FailureTracker{
×
733
                toolFailures: make(map[string]int),
×
734
                totalCount:   0,
×
735
        }
×
736
}
×
737

738
// RecordFailure 记录工具失败
UNCOV
739
func (ft *FailureTracker) RecordFailure(toolName string) {
×
740
        ft.toolFailures[toolName]++
×
741
        ft.totalCount++
×
742
        logger.Debug("Tool failure recorded",
×
743
                zap.String("tool", toolName),
×
744
                zap.Int("count", ft.toolFailures[toolName]),
×
UNCOV
745
                zap.Int("total", ft.totalCount))
×
UNCOV
746
}
×
747

748
// RecordSuccess 记录工具成功
749
func (ft *FailureTracker) RecordSuccess(toolName string) {
×
750
        // 同一工具成功后,可以重置其失败计数
×
UNCOV
751
        if count, ok := ft.toolFailures[toolName]; ok && count > 0 {
×
UNCOV
752
                ft.toolFailures[toolName] = 0
×
753
        }
×
754
}
755

756
// HasConsecutiveFailures 检查是否有连续失败
757
func (ft *FailureTracker) HasConsecutiveFailures(threshold int) bool {
×
758
        return ft.totalCount >= threshold
×
UNCOV
759
}
×
760

761
// GetFailedToolNames 获取失败的工具名称列表
UNCOV
762
func (ft *FailureTracker) GetFailedToolNames() []string {
×
UNCOV
763
        var names []string
×
764
        for name, count := range ft.toolFailures {
×
765
                if count > 0 {
×
766
                        names = append(names, name)
×
767
                }
×
768
        }
769
        return names
×
770
}
771

772
// formatToolError 格式化工具错误,提供替代建议
773
//
774
//nolint:unused
775
func formatToolError(toolName string, params map[string]interface{}, err error, availableTools []string) string {
×
776
        errorMsg := err.Error()
×
777

×
778
        var sb strings.Builder
×
779
        sb.WriteString(fmt.Sprintf("## 工具执行失败: `%s`\n\n", toolName))
×
780
        sb.WriteString(fmt.Sprintf("**错误**: %s\n\n", errorMsg))
×
781

×
782
        // 提供降级建议
×
783
        var suggestions []string
×
784
        switch toolName {
×
785
        case "write_file":
×
786
                suggestions = []string{
×
787
                        "1. **输出到控制台**: 直接将内容显示给用户",
×
788
                        "2. **使用相对路径**: 尝试使用 `./filename`",
×
789
                        "3. **使用完整路径**: 尝试使用绝对路径",
×
790
                        "4. **检查权限**: 确认当前目录有写入权限",
×
791
                }
×
792
        case "read_file":
×
793
                suggestions = []string{
×
794
                        "1. **检查路径**: 确认文件路径是否正确",
×
795
                        "2. **列出目录**: 使用 `list_dir` 工具查看目录内容",
×
796
                        "3. **使用相对路径**: 尝试使用 `./filename`",
×
797
                }
×
798
        case "smart_search", "web_search":
×
799
                suggestions = []string{
×
800
                        "1. **简化查询**: 使用更简单的关键词",
×
801
                        "2. **稍后重试**: 网络暂时不可用",
×
802
                        "3. **告知用户**: 让用户自己搜索并提供结果",
×
803
                }
×
804
        case "browser":
×
UNCOV
805
                suggestions = []string{
×
UNCOV
806
                        "1. **检查URL**: 确认URL格式正确",
×
807
                        "2. **使用web_reader**: 尝试使用 web_reader 工具替代",
×
808
                }
×
809
        default:
×
810
                suggestions = []string{
×
811
                        "1. **检查参数**: 确认工具参数是否正确",
×
UNCOV
812
                        "2. **尝试替代方案**: 使用其他工具或方法",
×
UNCOV
813
                }
×
814
        }
815

816
        if len(suggestions) > 0 {
×
817
                sb.WriteString("**建议的替代方案**:\n\n")
×
818
                for _, s := range suggestions {
×
819
                        sb.WriteString(fmt.Sprintf("%s\n", s))
×
820
                }
×
821
        }
822

823
        // 显示可用的替代工具
824
        if len(availableTools) > 0 {
×
UNCOV
825
                sb.WriteString("\n**可用的工具列表**:\n\n")
×
UNCOV
826
                for _, tool := range availableTools {
×
UNCOV
827
                        if tool != toolName {
×
828
                                sb.WriteString(fmt.Sprintf("- %s\n", tool))
×
829
                        }
×
830
                }
831
        }
832

UNCOV
833
        return sb.String()
×
834
}
835

836
// shouldUseErrorGuidance 判断是否需要添加错误处理指导
837
//
838
//nolint:unused
839
func shouldUseErrorGuidance(history []session.Message) bool {
×
840
        // 检查最近的消息中是否有工具失败
×
841
        if len(history) == 0 {
×
842
                return false
×
843
        }
×
844

UNCOV
845
        consecutiveFailures := 0
×
UNCOV
846
        for i := len(history) - 1; i >= 0 && i >= len(history)-6; i-- {
×
UNCOV
847
                msg := history[i]
×
UNCOV
848
                if msg.Role == "tool" {
×
849
                        if strings.Contains(msg.Content, "## 工具执行失败") ||
×
UNCOV
850
                                strings.Contains(msg.Content, "Error:") {
×
UNCOV
851
                                consecutiveFailures++
×
UNCOV
852
                        } else {
×
853
                                break // 遇到成功的工具调用就停止
×
854
                        }
855
                }
856
        }
857

858
        return consecutiveFailures >= 2
×
859
}
860

861
// getAvailableToolNames 获取可用的工具名称列表
862
//
863
//nolint:unused
864
func getAvailableToolNames(toolRegistry *tools.Registry) []string {
×
865
        if toolRegistry == nil {
×
UNCOV
866
                return []string{}
×
UNCOV
867
        }
×
868

869
        tools := toolRegistry.List()
×
870
        names := make([]string, 0, len(tools))
×
871
        for _, t := range tools {
×
872
                names = append(names, t.Name())
×
873
        }
×
874
        return names
×
875
}
876

877
// sessionMessagesToAgentMessages converts session messages to agent messages
878
func sessionMessagesToAgentMessages(history []session.Message) []agent.AgentMessage {
×
879
        result := make([]agent.AgentMessage, 0, len(history))
×
880
        for _, sessMsg := range history {
×
881
                agentMsg := agent.AgentMessage{
×
882
                        Role:      agent.MessageRole(sessMsg.Role),
×
883
                        Content:   []agent.ContentBlock{agent.TextContent{Text: sessMsg.Content}},
×
884
                        Timestamp: sessMsg.Timestamp.UnixMilli(),
×
885
                }
×
886

×
887
                // Handle tool calls in assistant messages
×
UNCOV
888
                if sessMsg.Role == "assistant" && len(sessMsg.ToolCalls) > 0 {
×
UNCOV
889
                        agentMsg.Content = []agent.ContentBlock{}
×
UNCOV
890
                        for _, tc := range sessMsg.ToolCalls {
×
891
                                agentMsg.Content = append(agentMsg.Content, agent.ToolCallContent{
×
892
                                        ID:        tc.ID,
×
893
                                        Name:      tc.Name,
×
894
                                        Arguments: map[string]any(tc.Params),
×
895
                                })
×
896
                        }
×
897
                }
898

899
                // Handle tool result messages
UNCOV
900
                if sessMsg.Role == "tool" {
×
901
                        agentMsg.Role = "tool"
×
UNCOV
902
                        if agentMsg.Metadata == nil {
×
UNCOV
903
                                agentMsg.Metadata = make(map[string]any)
×
UNCOV
904
                        }
×
905
                        agentMsg.Metadata["tool_call_id"] = sessMsg.ToolCallID
×
906
                }
907

908
                result = append(result, agentMsg)
×
909
        }
UNCOV
910
        return result
×
911
}
912

913
// extractAgentMessageText extracts text content from an agent message
UNCOV
914
func extractAgentMessageText(msg agent.AgentMessage) string {
×
915
        for _, block := range msg.Content {
×
916
                if text, ok := block.(agent.TextContent); ok {
×
917
                        return text.Text
×
918
                }
×
919
        }
920
        return ""
×
921
}
922

923
// convertToMapParams converts map[string]any to session ToolCall Params type
UNCOV
924
func convertToMapParams(params map[string]any) map[string]interface{} {
×
UNCOV
925
        result := make(map[string]interface{})
×
UNCOV
926
        for k, v := range params {
×
UNCOV
927
                result[k] = v
×
UNCOV
928
        }
×
UNCOV
929
        return result
×
930
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc