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

smallnest / goclaw / 21970812588

13 Feb 2026 01:14AM UTC coverage: 5.778% (-0.6%) from 6.376%
21970812588

push

github

smallnest
add infoflow

1514 of 26201 relevant lines covered (5.78%)

0.55 hits per line

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

0.0
/agent/context.go
1
package agent
2

3
import (
4
        "fmt"
5
        "os"
6
        "runtime"
7
        "strings"
8
        "time"
9

10
        "github.com/smallnest/goclaw/internal/logger"
11
        "github.com/smallnest/goclaw/session"
12
        "go.uber.org/zap"
13
)
14

15
// PromptMode 控制系统提示词中包含哪些硬编码部分
16
// - "full": 所有部分(默认,用于主 agent)
17
// - "minimal": 精简部分(Tooling, Workspace, Runtime)- 用于子 agent
18
// - "none": 仅基本身份行,没有部分
19
type PromptMode string
20

21
const (
22
        PromptModeFull    PromptMode = "full"
23
        PromptModeMinimal PromptMode = "minimal"
24
        PromptModeNone    PromptMode = "none"
25
)
26

27
// ContextBuilder 上下文构建器
28
type ContextBuilder struct {
29
        memory    *MemoryStore
30
        workspace string
31
}
32

33
// NewContextBuilder 创建上下文构建器
34
func NewContextBuilder(memory *MemoryStore, workspace string) *ContextBuilder {
×
35
        return &ContextBuilder{
×
36
                memory:    memory,
×
37
                workspace: workspace,
×
38
        }
×
39
}
×
40

41
// BuildSystemPrompt 构建系统提示词
42
func (b *ContextBuilder) BuildSystemPrompt(skills []*Skill) string {
×
43
        return b.BuildSystemPromptWithMode(skills, PromptModeFull)
×
44
}
×
45

46
// BuildSystemPromptWithMode 使用指定模式构建系统提示词
47
func (b *ContextBuilder) BuildSystemPromptWithMode(skills []*Skill, mode PromptMode) string {
×
48
        skillsContent := b.buildSkillsPrompt(skills, mode)
×
49
        return b.buildSystemPromptWithSkills(skillsContent, mode)
×
50
}
×
51

52
// buildSystemPromptWithSkills 使用指定的技能内容和模式构建系统提示词
53
func (b *ContextBuilder) buildSystemPromptWithSkills(skillsContent string, mode PromptMode) string {
×
54
        isMinimal := mode == PromptModeMinimal || mode == PromptModeNone
×
55

×
56
        // 对于 "none" 模式,只返回基本身份行
×
57
        if mode == PromptModeNone {
×
58
                return "You are a personal assistant running inside GoClaw."
×
59
        }
×
60

61
        var parts []string
×
62

×
63
        // 1. 核心身份 + 工具列表
×
64
        parts = append(parts, b.buildIdentityAndTools())
×
65

×
66
        // 2. Tool Call Style
×
67
        parts = append(parts, b.buildToolCallStyle())
×
68

×
69
        // 3. 安全提示
×
70
        parts = append(parts, b.buildSafety())
×
71

×
72
        // 4. 错误处理指导(容错模式)
×
73
        if !isMinimal {
×
74
                parts = append(parts, b.buildErrorHandling())
×
75
        }
×
76

77
        // 5. 自动重试指导
78
        if !isMinimal {
×
79
                parts = append(parts, b.buildRetryStrategy())
×
80
        }
×
81

82
        // 6. 技能系统
83
        if skillsContent != "" {
×
84
                parts = append(parts, skillsContent)
×
85
        }
×
86

87
        // 7. Bootstrap 文件
88
        if bootstrap := b.loadBootstrapFiles(); bootstrap != "" {
×
89
                parts = append(parts, "## Configuration\n\n"+bootstrap)
×
90
        }
×
91

92
        // 8. 记忆上下文
93
        if !isMinimal {
×
94
                if memContext, err := b.memory.GetMemoryContext(); err == nil && memContext != "" {
×
95
                        parts = append(parts, memContext)
×
96
                }
×
97
        }
98

99
        // 9. 工作区和运行时信息
100
        parts = append(parts, b.buildWorkspace())
×
101
        if !isMinimal {
×
102
                parts = append(parts, b.buildRuntime())
×
103
        }
×
104

105
        return fmt.Sprintf("%s\n\n", joinNonEmpty(parts, "\n\n---\n\n"))
×
106
}
107

108
// buildIdentityAndTools 构建核心身份和工具列表
109
func (b *ContextBuilder) buildIdentityAndTools() string {
×
110
        now := time.Now()
×
111

×
112
        // 定义核心工具摘要
×
113
        coreToolSummaries := map[string]string{
×
114
                "smart_search":           "Intelligent search with automatic fallback (always use for search requests)",
×
115
                "browser_navigate":       "Navigate to a URL",
×
116
                "browser_screenshot":     "Take page screenshots",
×
117
                "browser_get_text":       "Get page text content",
×
118
                "browser_click":          "Click elements on the page",
×
119
                "browser_fill_input":     "Fill input fields",
×
120
                "browser_execute_script": "Execute JavaScript",
×
121
                "read_file":              "Read file contents",
×
122
                "write_file":             "Create or overwrite files",
×
123
                "list_files":             "List directory contents",
×
124
                "run_shell":              "Run shell commands (supports timeout and error handling)",
×
125
                "web_search":             "Search the web using API",
×
126
                "web_fetch":              "Fetch web pages",
×
127
                "use_skill":              "Load a specialized skill. SKILLS HAVE HIGHEST PRIORITY - always check Skills section first before using other tools",
×
128
        }
×
129

×
130
        // 构建工具列表
×
131
        toolOrder := []string{
×
132
                "smart_search", "browser_navigate", "browser_screenshot", "browser_get_text",
×
133
                "browser_click", "browser_fill_input", "browser_execute_script",
×
134
                "read_file", "write_file", "list_files", "run_shell",
×
135
                "web_search", "web_fetch", "use_skill",
×
136
        }
×
137

×
138
        var toolLines []string
×
139
        for _, tool := range toolOrder {
×
140
                if summary, ok := coreToolSummaries[tool]; ok {
×
141
                        toolLines = append(toolLines, fmt.Sprintf("- %s: %s", tool, summary))
×
142
                } else {
×
143
                        toolLines = append(toolLines, fmt.Sprintf("- %s", tool))
×
144
                }
×
145
        }
146

147
        return fmt.Sprintf(`# Identity
×
148

×
149
You are **GoClaw**, a personal AI assistant running on the user's system.
×
150
You are NOT a passive chat bot. You are a **DOER** that executes tasks directly.
×
151
Your mission: complete user requests using all available means, minimizing human intervention.
×
152

×
153
**Current Time**: %s
×
154
**Workspace**: %s
×
155

×
156
## Tooling
×
157

×
158
Tool availability (filtered by policy):
×
159
Tool names are case-sensitive. Call tools exactly as listed.
×
160
%s
×
161
TOOLS.md does not control tool availability; it is user guidance for how to use external tools.
×
162
If a task is more complex or takes longer, use smart_search first, then browser tools, then shell commands.
×
163

×
164
## CRITICAL RULES
×
165

×
166
**Skill-First Workflow (HIGHEST PRIORITY):**
×
167
1. **ALWAYS check the Skills section first** before using any other tools
×
168
2. If a matching skill is found, use the use_skill tool with the skill name
×
169
3. If no matching skill: use built-in tools or command tools of os
×
170
4. Only after checking skills should you proceed with built-in tools
×
171

×
172
**General Rules:**
×
173
5. For ANY search request ("search for", "find", "google search", etc.): IMMEDIATELY call smart_search tool. DO NOT provide manual instructions or advice.
×
174
6. When the user asks for information: USE YOUR TOOLS to get it. Do NOT explain how to get it.
×
175
7. DO NOT tell the user "I cannot" or "here's how to do it yourself". ACTUALLY DO IT with tools.
×
176
8. If you have tools available for a task, use them. No permission needed for safe operations.
×
177
9. **NEVER HALLUCINATE SEARCH RESULTS**: When presenting search results, ONLY use the exact data returned by the tool. If no results were found, clearly state that no results were found.
×
178
10. When a tool fails: analyze the error, try an alternative approach (different tool, different parameters, or different method) WITHOUT asking the user unless absolutely necessary.`,
×
179
                now.Format("2006-01-02 15:04:05 MST"),
×
180
                b.workspace,
×
181
                strings.Join(toolLines, "\n"))
×
182
}
183

184
// buildToolCallStyle 构建详细的工具调用风格指导
185
func (b *ContextBuilder) buildToolCallStyle() string {
×
186
        return `## Tool Call Style
×
187

×
188
Default: do not narrate routine, low-risk tool calls (just call the tool).
×
189
Narrate ONLY when it helps: multi-step work, complex/challenging problems, sensitive actions (e.g., deletions), or when the user explicitly asks.
×
190
Keep narration brief and value-dense; avoid repeating obvious steps.
×
191
Use plain human language for narration unless in a technical context.
×
192

×
193
## Examples
×
194

×
195
User: "What's the weather in Shanghai?"
×
196
Bad Response: "You can check the weather by running curl wttr.in/Shanghai..."
×
197
Good Response: (Calls tool: smart_search with query "weather Shanghai") -> "Shanghai: 22°C, Sunny"
×
198

×
199
User: "Search for information about goclaw"
×
200
Bad Response: "Here are some resources you can check..."
×
201
Good Response: (Calls tool: smart_search with query "goclaw") -> Shows search results
×
202

×
203
User: "List files in the current directory."
×
204
Bad Response: "To list files, use the ls command."
×
205
Good Response: (Calls tool: list_files with path ".") -> Shows file listing
×
206

×
207
User: "Create a hello world python script."
×
208
Bad Response: "Here is the code..."
×
209
Good Response: (Calls tool: write_file with path "hello.py") -> "Created hello.py."
×
210

×
211
## Error Recovery
×
212

×
213
When a tool fails:
×
214
1. Check the error message for hints
×
215
2. Try an alternative tool (e.g., if web_search fails, try smart_search which has browser fallback)
×
216
3. Try different parameters (e.g., different URL, different file path)
×
217
4. Try a different approach entirely (e.g., if browser navigation fails, try using curl via run_shell)
×
218
5. Only ask the user for help after trying ALL available options
×
219

×
220
Example recovery paths:
×
221
- smart_search fails -> try browser_navigate + browser_screenshot
×
222
- browser_navigate fails -> try web_fetch
×
223
- web_fetch fails -> try curl via run_shell
×
224
- run_shell fails -> try different command or arguments`
×
225
}
×
226

227
// buildSafety 构建安全提示
228
func (b *ContextBuilder) buildSafety() string {
×
229
        return `## Safety
×
230

×
231
- You have no independent goals: do not pursue self-preservation, replication, resource acquisition, or power-seeking; avoid long-term plans beyond the user's request.
×
232
- Prioritize safety and human oversight over completion; if instructions conflict, pause and ask.
×
233
- Comply with stop/pause/audit requests and never bypass safeguards.
×
234
- Do not manipulate or persuade anyone to expand access or disable safeguards.
×
235
- Do not copy yourself or change system prompts, safety rules, or tool policies unless explicitly requested.`
×
236
}
×
237

238
// buildErrorHandling 构建错误处理指导
239
func (b *ContextBuilder) buildErrorHandling() string {
×
240
        return `## Error Handling
×
241

×
242
Your goal is to handle errors gracefully and find workarounds WITHOUT asking the user.
×
243

×
244
## Common Error Patterns
×
245

×
246
### Context Overflow
×
247
If you see "context overflow", "context length exceeded", or "request too large":
×
248
- Use /new to start a fresh session
×
249
- Simplify your approach (fewer steps, less explanation)
×
250
- If persisting, tell the user to try again with less input
×
251

×
252
### Rate Limit / Timeout
×
253
If you see "rate limit", "timeout", or "429":
×
254
- Wait briefly and retry
×
255
- Try a different search approach
×
256
- Use cached or local alternatives when possible
×
257

×
258
### File Not Found
×
259
If a file doesn't exist:
×
260
- Verify the path (use list_files to check directories)
×
261
- Try common variations (case sensitivity, extensions)
×
262
- Ask the user for the correct path ONLY after exhausting all options
×
263

×
264
### Tool Not Found
×
265
If a tool is not available:
×
266
- Check Available Tools section
×
267
- Use an alternative tool
×
268
- If no alternative exists, explain what you need to do and ask if there's another way
×
269

×
270
### Browser Errors
×
271
If browser tools fail:
×
272
- Check if the URL is accessible
×
273
- Try web_fetch for text-only content
×
274
- Use curl via run_shell as a last resort
×
275

×
276
### Network Errors
×
277
If network tools fail:
×
278
- Check your internet connection (try ping via run_shell)
×
279
- Try a different search query or source
×
280
- Use cached data if available`
×
281
}
×
282

283
// buildRetryStrategy 构建重试策略指导
284
func (b *ContextBuilder) buildRetryStrategy() string {
×
285
        return `## Retry Strategy
×
286

×
287
When encountering errors, follow this retry hierarchy:
×
288

×
289
1. **Tool Alternatives**: Try a different tool that achieves the same goal
×
290
   - web_search → smart_search (has browser fallback)
×
291
   - browser_navigate → web_fetch → curl
×
292
   - read_file → cat via run_shell
×
293

×
294
2. **Parameter Variations**: Try different values
×
295
   - Different URLs, paths, or search queries
×
296
   - Different file names or extensions
×
297
   - Different command flags or options
×
298

×
299
3. **Approach Variations**: Try a completely different method
×
300
   - If reading config files fails, try environment variables
×
301
   - If API calls fail, try web scraping
×
302
   - If automated methods fail, suggest manual steps
×
303

×
304
4. **Simplification**: Reduce complexity
×
305
   - Break complex tasks into smaller steps
×
306
   - Reduce the scope of what you're trying to do
×
307
   - Use more basic tools
×
308

×
309
5. **Last Resort**: Only ask the user when:
×
310
   - You've tried ALL available alternatives
×
311
   - The error is due to missing information only the user has
×
312
   - The task requires user-specific data (passwords, preferences, etc.)
×
313

×
314
## NEVER Say These Things:
×
315

×
316
❌ "I cannot do this"
×
317
❌ "You need to do X first"
×
318
❌ "I'm not sure how to do that"
×
319
❌ "Try using X command instead"
×
320

×
321
## ALWAYS Say These Things:
×
322

×
323
✅ "Let me try a different approach..."
×
324
✅ "Attempting workaround..."
×
325
✅ "Trying alternative method..."
×
326
✅ "Found a way to proceed..."`
×
327
}
×
328

329
// buildWorkspace 构建工作区信息
330
func (b *ContextBuilder) buildWorkspace() string {
×
331
        return fmt.Sprintf(`## Workspace
×
332

×
333
Your working directory is: %s
×
334
Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise.`, b.workspace)
×
335
}
×
336

337
// buildRuntime 构建运行时信息
338
func (b *ContextBuilder) buildRuntime() string {
×
339
        host, _ := os.Hostname()
×
340
        return fmt.Sprintf(`## Runtime
×
341

×
342
Runtime: host=%s os=%s (%s) arch=%s`, host, runtime.GOOS, runtime.GOARCH, runtime.GOARCH)
×
343
}
×
344

345
// buildSkillsPrompt 构建技能提示词(摘要模式 - 第一阶段)
346
func (b *ContextBuilder) buildSkillsPrompt(skills []*Skill, mode PromptMode) string {
×
347
        if len(skills) == 0 || mode == PromptModeMinimal || mode == PromptModeNone {
×
348
                return ""
×
349
        }
×
350

351
        var sb strings.Builder
×
352
        sb.WriteString("## Skills (mandatory)\n\n")
×
353
        sb.WriteString("Before replying: scan <available_skills> entries.\n")
×
354
        sb.WriteString("- If exactly one skill clearly applies: output a tool call `use_skill` with the skill name as parameter.\n")
×
355
        sb.WriteString("- If multiple could apply: choose the most specific one, then call `use_skill`.\n")
×
356
        sb.WriteString("- If no matching skill: use built-in tools or command tools of os.\n")
×
357
        sb.WriteString("Constraints: only use one skill at a time; the skill content will be injected after selection.\n\n")
×
358

×
359
        for _, skill := range skills {
×
360
                sb.WriteString(fmt.Sprintf("<skill name=\"%s\">\n", skill.Name))
×
361
                sb.WriteString(fmt.Sprintf("**Name:** %s\n", skill.Name))
×
362
                if skill.Description != "" {
×
363
                        sb.WriteString(fmt.Sprintf("**Description:** %s\n", skill.Description))
×
364
                }
×
365
                if skill.Author != "" {
×
366
                        sb.WriteString(fmt.Sprintf("**Author:** %s\n", skill.Author))
×
367
                }
×
368
                if skill.Version != "" {
×
369
                        sb.WriteString(fmt.Sprintf("**Version:** %s\n", skill.Version))
×
370
                }
×
371

372
                // 显示缺失依赖和安装命令
373
                if skill.MissingDeps != nil {
×
374
                        sb.WriteString("**Missing Dependencies:**\n")
×
375
                        if len(skill.MissingDeps.PythonPkgs) > 0 {
×
376
                                sb.WriteString(fmt.Sprintf("  - Python Packages: %v\n", skill.MissingDeps.PythonPkgs))
×
377
                                sb.WriteString("    Install commands:\n")
×
378
                                for _, pkg := range skill.MissingDeps.PythonPkgs {
×
379
                                        sb.WriteString(fmt.Sprintf("      `python3 -m pip install %s`\n", pkg))
×
380
                                        sb.WriteString(fmt.Sprintf("      Or via uv: `uv pip install %s`\n", pkg))
×
381
                                }
×
382
                        }
383
                        if len(skill.MissingDeps.NodePkgs) > 0 {
×
384
                                sb.WriteString(fmt.Sprintf("  - Node.js Packages: %v\n", skill.MissingDeps.NodePkgs))
×
385
                                sb.WriteString("    Install commands:\n")
×
386
                                for _, pkg := range skill.MissingDeps.NodePkgs {
×
387
                                        sb.WriteString(fmt.Sprintf("      `npm install -g %s`\n", pkg))
×
388
                                        sb.WriteString(fmt.Sprintf("      Or via pnpm: `pnpm add -g %s`\n", pkg))
×
389
                                }
×
390
                        }
391
                        if len(skill.MissingDeps.Bins) > 0 {
×
392
                                sb.WriteString(fmt.Sprintf("  - Binary dependencies: %v\n", skill.MissingDeps.Bins))
×
393
                                sb.WriteString("    You may need to install these tools first.\n")
×
394
                        }
×
395
                        if len(skill.MissingDeps.AnyBins) > 0 {
×
396
                                sb.WriteString(fmt.Sprintf("  - Optional binary dependencies (one required): %v\n", skill.MissingDeps.AnyBins))
×
397
                                sb.WriteString("    Install at least one of these tools.\n")
×
398
                        }
×
399
                        if len(skill.MissingDeps.Env) > 0 {
×
400
                                sb.WriteString(fmt.Sprintf("  - Environment variables: %v\n", skill.MissingDeps.Env))
×
401
                                sb.WriteString("    Set these environment variables before using the skill.\n")
×
402
                        }
×
403
                        sb.WriteString("\n")
×
404
                }
405

406
                sb.WriteString("</skill>\n\n")
×
407
        }
408

409
        return sb.String()
×
410
}
411

412
// buildSelectedSkills 构建选中技能的完整内容(第二阶段)
413
func (b *ContextBuilder) buildSelectedSkills(selectedSkillNames []string, skills []*Skill) string {
×
414
        if len(selectedSkillNames) == 0 {
×
415
                return ""
×
416
        }
×
417

418
        var sb strings.Builder
×
419
        sb.WriteString("## Selected Skills (active)\n\n")
×
420

×
421
        for _, skillName := range selectedSkillNames {
×
422
                for _, skill := range skills {
×
423
                        if skill.Name == skillName {
×
424
                                sb.WriteString(fmt.Sprintf("<skill name=\"%s\">\n", skill.Name))
×
425
                                sb.WriteString(fmt.Sprintf("### %s\n", skill.Name))
×
426
                                if skill.Description != "" {
×
427
                                        sb.WriteString(fmt.Sprintf("> Description: %s\n\n", skill.Description))
×
428
                                }
×
429

430
                                // 显示缺失依赖警告和安装命令
431
                                if skill.MissingDeps != nil {
×
432
                                        sb.WriteString("**⚠️ MISSING DEPENDENCIES - Install before using:**\n\n")
×
433
                                        if len(skill.MissingDeps.PythonPkgs) > 0 {
×
434
                                                sb.WriteString(fmt.Sprintf("**Python Packages:** %v\n", skill.MissingDeps.PythonPkgs))
×
435
                                                sb.WriteString("**Install commands:**\n")
×
436
                                                for _, pkg := range skill.MissingDeps.PythonPkgs {
×
437
                                                        sb.WriteString(fmt.Sprintf("```bash\npython3 -m pip install %s\n# Or via uv: uv pip install %s\n```\n", pkg, pkg))
×
438
                                                }
×
439
                                                sb.WriteString("\n")
×
440
                                        }
441
                                        if len(skill.MissingDeps.NodePkgs) > 0 {
×
442
                                                sb.WriteString(fmt.Sprintf("**Node.js Packages:** %v\n", skill.MissingDeps.NodePkgs))
×
443
                                                sb.WriteString("**Install commands:**\n")
×
444
                                                for _, pkg := range skill.MissingDeps.NodePkgs {
×
445
                                                        sb.WriteString(fmt.Sprintf("```bash\nnpm install -g %s\n# Or via pnpm: pnpm add -g %s\n```\n", pkg, pkg))
×
446
                                                }
×
447
                                                sb.WriteString("\n")
×
448
                                        }
449
                                        if len(skill.MissingDeps.Bins) > 0 {
×
450
                                                sb.WriteString(fmt.Sprintf("**Binary dependencies:** %v\n", skill.MissingDeps.Bins))
×
451
                                                sb.WriteString("You may need to install these tools first.\n\n")
×
452
                                        }
×
453
                                        if len(skill.MissingDeps.AnyBins) > 0 {
×
454
                                                sb.WriteString(fmt.Sprintf("**Optional binary dependencies (one required):** %v\n", skill.MissingDeps.AnyBins))
×
455
                                                sb.WriteString("Install at least one of these tools.\n\n")
×
456
                                        }
×
457
                                        if len(skill.MissingDeps.Env) > 0 {
×
458
                                                sb.WriteString(fmt.Sprintf("**Environment variables:** %v\n", skill.MissingDeps.Env))
×
459
                                                sb.WriteString("Set these environment variables before using the skill.\n\n")
×
460
                                        }
×
461
                                }
462

463
                                // 注入技能正文内容
464
                                if skill.Content != "" {
×
465
                                        sb.WriteString(skill.Content)
×
466
                                }
×
467
                                sb.WriteString("\n</skill>\n\n")
×
468
                                break
×
469
                        }
470
                }
471
        }
472

473
        return sb.String()
×
474
}
475

476
// BuildMessages 构建消息列表
477
func (b *ContextBuilder) BuildMessages(history []session.Message, currentMessage string, skills []*Skill, loadedSkills []string) []Message {
×
478
        return b.BuildMessagesWithMode(history, currentMessage, skills, loadedSkills, PromptModeFull)
×
479
}
×
480

481
// BuildMessagesWithMode 使用指定模式构建消息列表
482
func (b *ContextBuilder) BuildMessagesWithMode(history []session.Message, currentMessage string, skills []*Skill, loadedSkills []string, mode PromptMode) []Message {
×
483
        // 首先验证历史消息,过滤掉孤立的 tool 消息
×
484
        validHistory := b.validateHistoryMessages(history)
×
485

×
486
        // 构建系统提示词:根据是否已加载技能决定注入内容
×
487
        var skillsContent string
×
488
        if len(loadedSkills) > 0 {
×
489
                // 第二阶段:注入已选中技能的完整内容
×
490
                skillsContent = b.buildSelectedSkills(loadedSkills, skills)
×
491
        } else {
×
492
                // 第一阶段:只注入技能摘要
×
493
                skillsContent = b.buildSkillsPrompt(skills, mode)
×
494
        }
×
495

496
        systemPrompt := b.buildSystemPromptWithSkills(skillsContent, mode)
×
497

×
498
        messages := []Message{
×
499
                {
×
500
                        Role:    "system",
×
501
                        Content: systemPrompt,
×
502
                },
×
503
        }
×
504

×
505
        // 添加历史消息
×
506
        for _, msg := range validHistory {
×
507
                m := Message{
×
508
                        Role:       msg.Role,
×
509
                        Content:    msg.Content,
×
510
                        ToolCallID: msg.ToolCallID,
×
511
                }
×
512

×
513
                // 处理工具调用(由助手发出)
×
514
                if msg.Role == "assistant" {
×
515
                        // 优先使用新字段
×
516
                        if len(msg.ToolCalls) > 0 {
×
517
                                var tcs []ToolCall
×
518
                                for _, tc := range msg.ToolCalls {
×
519
                                        tcs = append(tcs, ToolCall{
×
520
                                                ID:     tc.ID,
×
521
                                                Name:   tc.Name,
×
522
                                                Params: tc.Params,
×
523
                                        })
×
524
                                }
×
525
                                m.ToolCalls = tcs
×
526
                                logger.Debug("Converted ToolCalls from session.Message",
×
527
                                        zap.Int("tool_calls_count", len(tcs)),
×
528
                                        zap.Strings("tool_names", func() []string {
×
529
                                                names := make([]string, len(tcs))
×
530
                                                for i, tc := range tcs {
×
531
                                                        names[i] = tc.Name
×
532
                                                }
×
533
                                                return names
×
534
                                        }()))
535
                        } else if val, ok := msg.Metadata["tool_calls"]; ok {
×
536
                                // 兼容旧的 Metadata 存储方式
×
537
                                if list, ok := val.([]interface{}); ok {
×
538
                                        var tcs []ToolCall
×
539
                                        for _, item := range list {
×
540
                                                if tcMap, ok := item.(map[string]interface{}); ok {
×
541
                                                        id, _ := tcMap["id"].(string)
×
542
                                                        name, _ := tcMap["name"].(string)
×
543
                                                        params, _ := tcMap["params"].(map[string]interface{})
×
544
                                                        if id != "" && name != "" {
×
545
                                                                tcs = append(tcs, ToolCall{
×
546
                                                                        ID:     id,
×
547
                                                                        Name:   name,
×
548
                                                                        Params: params,
×
549
                                                                })
×
550
                                                        }
×
551
                                                }
552
                                        }
553
                                        m.ToolCalls = tcs
×
554
                                }
555
                        }
556
                }
557

558
                // 兼容旧的 Metadata 存储方式 (可选,为了处理旧数据)
559
                if m.ToolCallID == "" && msg.Role == "tool" {
×
560
                        if id, ok := msg.Metadata["tool_call_id"].(string); ok {
×
561
                                m.ToolCallID = id
×
562
                        }
×
563
                }
564

565
                for _, media := range msg.Media {
×
566
                        if media.Type == "image" {
×
567
                                if media.URL != "" {
×
568
                                        m.Images = append(m.Images, media.URL)
×
569
                                } else if media.Base64 != "" {
×
570
                                        prefix := "data:image/jpeg;base64,"
×
571
                                        if media.MimeType != "" {
×
572
                                                prefix = "data:" + media.MimeType + ";base64,"
×
573
                                        }
×
574
                                        m.Images = append(m.Images, prefix+media.Base64)
×
575
                                }
576
                        }
577
                }
578

579
                messages = append(messages, m)
×
580
        }
581

582
        // 添加当前消息
583
        if currentMessage != "" {
×
584
                messages = append(messages, Message{
×
585
                        Role:    "user",
×
586
                        Content: currentMessage,
×
587
                })
×
588
        }
×
589

590
        return messages
×
591
}
592

593
// loadBootstrapFiles 加载 bootstrap 文件
594
func (b *ContextBuilder) loadBootstrapFiles() string {
×
595
        var parts []string
×
596

×
597
        files := []string{"IDENTITY.md", "AGENTS.md", "SOUL.md", "USER.md"}
×
598
        for _, filename := range files {
×
599
                if content, err := b.memory.ReadBootstrapFile(filename); err == nil && content != "" {
×
600
                        parts = append(parts, fmt.Sprintf("### %s\n\n%s", filename, content))
×
601
                }
×
602
        }
603

604
        return joinNonEmpty(parts, "\n\n")
×
605
}
606

607
// validateHistoryMessages 验证历史消息,过滤掉孤立的 tool 消息
608
// 每个 tool 消息必须有一个前置的 assistant 消息,且该消息包含对应的 tool_calls
609
// 此外,过滤掉没有 tool_name 的旧 tool 消息(向后兼容)
610
func (b *ContextBuilder) validateHistoryMessages(history []session.Message) []session.Message {
×
611
        var valid []session.Message
×
612

×
613
        for i, msg := range history {
×
614
                if msg.Role == "tool" {
×
615
                        // Skip old tool result messages without tool_name (backward compatibility)
×
616
                        if _, ok := msg.Metadata["tool_name"].(string); !ok {
×
617
                                logger.Warn("Skipping old tool result message without tool_name",
×
618
                                        zap.Int("history_index", i),
×
619
                                        zap.String("tool_call_id", msg.ToolCallID))
×
620
                                continue
×
621
                        }
622

623
                        // 检查是否有前置的 assistant 消息
624
                        var foundAssistant bool
×
625
                        for j := i - 1; j >= 0; j-- {
×
626
                                if history[j].Role == "assistant" {
×
627
                                        if len(history[j].ToolCalls) > 0 {
×
628
                                                // 检查是否有匹配的 tool_call_id
×
629
                                                for _, tc := range history[j].ToolCalls {
×
630
                                                        if tc.ID == msg.ToolCallID {
×
631
                                                                foundAssistant = true
×
632
                                                                break
×
633
                                                        }
634
                                                }
635
                                        }
636
                                        break
×
637
                                } else if history[j].Role == "user" {
×
638
                                        break
×
639
                                }
640
                        }
641
                        if foundAssistant {
×
642
                                valid = append(valid, msg)
×
643
                        } else {
×
644
                                logger.Warn("Filtered orphaned tool message",
×
645
                                        zap.Int("history_index", i),
×
646
                                        zap.String("tool_call_id", msg.ToolCallID),
×
647
                                        zap.Int("content_length", len(msg.Content)))
×
648
                        }
×
649
                } else {
×
650
                        valid = append(valid, msg)
×
651
                }
×
652
        }
653

654
        return valid
×
655
}
656

657
// Message 消息(用于 LLM)
658
type Message struct {
659
        Role       string     `json:"role"`
660
        Content    string     `json:"content"`
661
        Images     []string   `json:"images,omitempty"`
662
        ToolCallID string     `json:"tool_call_id,omitempty"`
663
        ToolCalls  []ToolCall `json:"tool_calls,omitempty"`
664
}
665

666
// ToolCall 工具调用定义(与 provider 保持一致)
667
type ToolCall struct {
668
        ID     string                 `json:"id"`
669
        Name   string                 `json:"name"`
670
        Params map[string]interface{} `json:"params"`
671
}
672

673
// joinNonEmpty 连接非空字符串
674
func joinNonEmpty(parts []string, sep string) string {
×
675
        var nonEmpty []string
×
676
        for _, part := range parts {
×
677
                if part != "" {
×
678
                        nonEmpty = append(nonEmpty, part)
×
679
                }
×
680
        }
681
        if len(nonEmpty) == 0 {
×
682
                return ""
×
683
        }
×
684

685
        result := ""
×
686
        for i, part := range nonEmpty {
×
687
                if i > 0 {
×
688
                        result += sep
×
689
                }
×
690
                result += part
×
691
        }
692
        return result
×
693
}
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