Files
HomeAgent/internal/agent/core/process.go
JianFeeeee f91b20ee16 v0.7.3: 重构 Provider 层 + 计算层隔离 + Cleaner/NoMemory 架构
- 删除 OpenAIProvider/OllamaProvider 死代码,LuaAdaptedProvider 独存
- DisableThinking 从 ExtraBody 移到 CompletionRequest 顶层字段
- ContextWindow 从 Provider 签名移到 BaseConfig/ModelContextWindow() 统管
- 确认 CleanText 仅做基本空白 trim,QQ 模板剥离归插件 Cleaner
- Cleaner/NoMemory 仅作用于向量计算和 jieba 分词层,原文不变
- context.ContextEvent/Doc.Content 始终保存原文
- 删除 nlp/download.go 死代码
- media.go: context.Background() -> a.ctx 级联
- clawhubadapter: HTTP 超时
- cut.go: 跨平台 mod cache 路径 (GOMODCACHE->GOPATH->HomeDir)
- bridge_e2e_test: 移除未用 runtime import
- lua 适配器: disable_thinking 传参
2026-07-28 11:42:29 +08:00

359 lines
11 KiB
Go

package core
import (
"context"
"errors"
"fmt"
"log"
"strings"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response string, toolsUsed []string, toolResults []ToolResultItem, err error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.provider == nil {
return "", nil, nil, fmt.Errorf("agent: no LLM provider configured")
}
budget := ComputeTokenBudget(a.provider, a.systemPrompt)
memContext := a.buildMemoryContext(input, budget.MemoryTokens)
sysPrompt := a.buildSystemPrompt(memContext, input)
tools := a.buildToolDefs()
msgs := a.buildMessages(sysPrompt, input, budget.ContextTokens)
if blocks, ok := stageCtx.Extra["media_blocks"].([]agentAPI.ContentBlock); ok && len(blocks) > 0 {
if len(msgs) > 0 {
msgs[len(msgs)-1].Blocks = blocks
}
}
log.Printf("[agent] tool call loop start, max_ctx=%d target=%d fixed=%d mem=%d ctx=%d %d tools, %d events, personality=%t, docs=%d",
budget.MaxContext, budget.TargetUsage, budget.FixedTokens, budget.MemoryTokens, budget.ContextTokens,
len(tools), a.context.Len(),
a.personality != nil && a.personality.Content != "",
a.docStoreSize())
if a.runStage(sdk.StagePreAction, stageCtx) {
return *stageCtx.Response, toolsUsed, toolResults, nil
}
if len(stageCtx.ContextMsgs) > 0 {
for _, m := range stageCtx.ContextMsgs {
role, _ := m["role"].(string)
content, _ := m["content"].(string)
if role != "" {
msgs = append(msgs, agentAPI.Message{Role: role, Content: content})
}
}
}
for turn := 0; ; turn++ {
for _, interrupt := range a.drainInterrupts() {
msgs = append(msgs, agentAPI.Message{
Role: "system",
Content: interrupt,
})
}
req := &agentAPI.CompletionRequest{
Messages: msgs,
MaxTokens: 4096,
Tools: tools,
ToolChoice: "auto",
DisableThinking: !a.thinkingEnabled,
}
var providers []agentAPI.Provider
if a.providerManager != nil {
allProviders := a.providerManager.OrderedProviders()
providers = make([]agentAPI.Provider, 0, len(allProviders))
for _, p := range allProviders {
if a.providerManager.IsAvailable(p.Name()) {
providers = append(providers, p)
}
}
}
if len(providers) == 0 {
providers = []agentAPI.Provider{a.provider}
}
var resp *agentAPI.CompletionResponse
var llmErr error
for pi, fbProvider := range providers {
if pi > 0 {
log.Printf("[agent] LLM fallback: trying provider %q (fallback #%d/%d)",
fbProvider.Name(), pi, len(providers)-1)
}
fCtx, fCancel := context.WithCancel(a.ctx)
a.llmMu.Lock()
a.cancelLLM = fCancel
a.llmMu.Unlock()
resp, llmErr = fbProvider.Chat(fCtx, req)
a.llmMu.Lock()
a.cancelLLM = nil
a.llmMu.Unlock()
fCancel()
if llmErr == nil {
a.providerManager.ResetAvailability(fbProvider.Name())
if fbProvider != a.provider {
a.provider = fbProvider
log.Printf("[agent] switched active provider to %q after fallback",
fbProvider.Name())
}
break
}
if errors.Is(llmErr, context.Canceled) {
break
}
var pe *agentAPI.ProviderError
if errors.As(llmErr, &pe) && (pe.StatusCode == 401 || pe.StatusCode == 403) {
a.providerManager.ReportStatus(fbProvider.Name(), pe.StatusCode)
log.Printf("[agent] provider %q marked unavailable (HTTP %d)", fbProvider.Name(), pe.StatusCode)
} else {
a.providerManager.MarkUnavailable(fbProvider.Name())
}
log.Printf("[agent] provider %q failed: %v", fbProvider.Name(), llmErr)
}
if llmErr != nil {
if errors.Is(llmErr, context.Canceled) && a.ctx.Err() == nil {
if a.currentOutputChannel == "_consolidation_" {
return "", toolsUsed, toolResults, fmt.Errorf("interrupted by user input")
}
continue
}
return "", toolsUsed, toolResults, fmt.Errorf("all %d providers failed, last error: %w",
len(providers), llmErr)
}
stageCtx.LLMText = resp.Content
stageCtx.ReasoningContent = resp.ReasoningContent
stageCtx.TokenUsage = map[string]int{
"prompt_tokens": resp.TokenUsage.Prompt,
"completion_tokens": resp.TokenUsage.Completion,
"total_tokens": resp.TokenUsage.Total,
}
stageCtx.ToolCalls = convertToolCalls(resp.ToolCalls)
for i := range stageCtx.ToolCalls {
if stageCtx.ToolCalls[i].Plugin == "" {
stageCtx.ToolCalls[i].Plugin = a.resolveToolPlugin(stageCtx.ToolCalls[i].Name)
}
}
if a.runStage(sdk.StagePostAction, stageCtx) {
return *stageCtx.Response, toolsUsed, toolResults, nil
}
resp.Content = stageCtx.LLMText
resp.ToolCalls = convertBackToolCalls(stageCtx.ToolCalls)
chainPayload := map[string]interface{}{
"content": resp.Content,
"reasoning": resp.ReasoningContent,
"tool_calls": resp.ToolCalls,
"phase": "intermediate",
"turn": turn,
}
if resp.TokenUsage.Total > 0 {
chainPayload["usage"] = map[string]int{
"prompt": resp.TokenUsage.Prompt,
"completion": resp.TokenUsage.Completion,
"total": resp.TokenUsage.Total,
}
}
a.publishEvent(events.EventAgentLLMChain, chainPayload)
if len(resp.ToolCalls) == 0 {
return resp.Content, toolsUsed, toolResults, nil
}
contentOnce := true
for _, tc := range resp.ToolCalls {
if len(a.interceptCh) > 0 {
for _, interrupt := range a.drainInterrupts() {
msgs = append(msgs, agentAPI.Message{Role: "system", Content: interrupt})
}
a.publishEvent(events.EventToolCall, map[string]interface{}{
"tool": tc.Name,
"plugin": a.resolveToolPlugin(tc.Name),
"args": tc.Arguments,
"status": "interrupted",
"reason": "user interrupt before execution",
})
break
}
toolsUsed = append(toolsUsed, tc.Name)
pluginName := a.resolveToolPlugin(tc.Name)
log.Printf("[agent] executing tool: %s (plugin=%s, id=%s)", tc.Name, pluginName, tc.ID)
sdkTC := sdk.ToolCall{ID: tc.ID, Name: tc.Name, Plugin: pluginName, Arguments: tc.Arguments}
stageCtx.ToolCalls = []sdk.ToolCall{sdkTC}
stageCtx.ToolResults = nil
if a.runStage(sdk.StageBeforeToolcall, stageCtx) {
result := fmt.Sprintf("工具 %s 已被插件拒绝", tc.Name)
msgs = append(msgs, agentAPI.Message{Role: "assistant", ToolCalls: []agentAPI.ToolCall{tc}})
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result})
a.publishEvent(events.EventToolCall, map[string]interface{}{
"tool": tc.Name,
"plugin": pluginName,
"args": tc.Arguments,
"result": result,
"status": "denied",
})
continue
}
tc.Arguments = stageCtx.ToolCalls[0].Arguments
if pluginName != "" && !a.pluginHealth.isHealthy(pluginName) {
result := fmt.Sprintf("插件 %s 处于崩溃状态,已跳过执行,等待自动恢复重载", pluginName)
log.Printf("[agent] skip tool %s: plugin %s unhealthy", tc.Name, pluginName)
msgs = append(msgs, agentAPI.Message{Role: "assistant", ToolCalls: []agentAPI.ToolCall{tc}})
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result})
continue
}
result := a.executeToolCall(tc)
toolResults = append(toolResults, ToolResultItem{Name: tc.Name, Output: result})
log.Printf("[agent] tool %s result: %s", tc.Name, truncateStr(result, 100))
stageCtx.ToolResults = []sdk.ToolResult{{CallID: tc.ID, Name: tc.Name, Plugin: pluginName, Success: true, Result: result}}
a.runStage(sdk.StageAfterToolcall, stageCtx)
if len(stageCtx.ToolResults) > 0 {
if r, ok := stageCtx.ToolResults[0].Result.(string); ok {
result = r
}
}
msgContent := ""
if contentOnce {
msgContent = resp.Content
contentOnce = false
}
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: msgContent, ToolCalls: []agentAPI.ToolCall{tc}})
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result})
a.publishEvent(events.EventToolCall, map[string]interface{}{
"tool": tc.Name,
"plugin": pluginName,
"args": tc.Arguments,
"result": result,
"status": "ok",
})
if len(a.interceptCh) > 0 {
for _, interrupt := range a.drainInterrupts() {
msgs = append(msgs, agentAPI.Message{Role: "system", Content: interrupt})
}
break
}
}
}
}
func convertToolCalls(tcs []agentAPI.ToolCall) []sdk.ToolCall {
if tcs == nil {
return nil
}
result := make([]sdk.ToolCall, len(tcs))
for i, tc := range tcs {
result[i] = sdk.ToolCall{ID: tc.ID, Name: tc.Name, Arguments: tc.Arguments}
}
return result
}
func convertBackToolCalls(tcs []sdk.ToolCall) []agentAPI.ToolCall {
if tcs == nil {
return nil
}
result := make([]agentAPI.ToolCall, len(tcs))
for i, tc := range tcs {
result[i] = agentAPI.ToolCall{ID: tc.ID, Name: tc.Name, Arguments: tc.Arguments}
}
return result
}
func (a *Agent) docStoreSize() int {
if a.docStore == nil {
return 0
}
s := a.docStore.Stats()
if n, ok := s["doc_count"]; ok {
if ni, ok := n.(int); ok {
return ni
}
}
return 0
}
func (a *Agent) formatMergedTimeline(maxTokens int) string {
a.context.mu.Lock()
events := make([]*ContextEvent, len(a.context.events))
copy(events, a.context.events)
a.context.mu.Unlock()
if len(events) == 0 {
return ""
}
// 第一轮:从最新到最旧,计算在预算内能放多少条
headerTokens := EstimateTokens("【对话时序】\n")
remaining := maxTokens - headerTokens
include := 0
for i := len(events) - 1; i >= 0; i-- {
e := events[i]
est := len(e.Source) + len(e.Input) + 40
if e.Response != "" {
est += 120
}
estTokens := est * 2
if remaining-estTokens < 0 && include > 0 {
break
}
remaining -= estTokens
include++
}
if include == 0 && len(events) > 0 {
include = 1
}
// 第二轮:按时间正序渲染
start := len(events) - include
if start < 0 {
start = 0
}
var sb strings.Builder
sb.WriteString("【对话时序】\n")
for _, e := range events[start:] {
sb.WriteString(fmt.Sprintf("[%s] %s: %s",
e.Timestamp.Format("15:04:05"), e.Source, e.Input))
if len(e.ToolsUsed) > 0 {
sb.WriteString(fmt.Sprintf(" → 调用工具: %s", strings.Join(e.ToolsUsed, ", ")))
}
if e.Response != "" {
sb.WriteString(fmt.Sprintf(" → %s", truncateStr(e.Response, 120)))
}
sb.WriteString("\n")
}
return sb.String()
}
func (a *Agent) buildMessages(sysPrompt, input string, ctxTokens int) []agentAPI.Message {
msgs := []agentAPI.Message{{Role: "system", Content: sysPrompt}}
if ctxTok := a.formatMergedTimeline(ctxTokens); ctxTok != "" {
msgs = append(msgs, agentAPI.Message{Role: "system", Content: ctxTok})
}
msgs = append(msgs, agentAPI.Message{Role: "user", Content: input})
return msgs
}