mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
v4 architecture: pipeline stages, SDK, event bus, LLM-driven memory consolidation
- SDK PluginAPI (internal/plugin/sdk/): RegisterTool/RegisterStage/Subscribe/Publish - EventBus (internal/events/): system-level pub/sub with wildcard support - StageHost (internal/agent/core/stages.go): 7-stage message pipeline - Agent core: on_input/pre_action/post_action/before_toolcall/after_toolcall/before_output/after_output - Plugin Registry: SDK plugin registration and tool routing - GraphDB.MergeEntities: entity consolidation with relation redirection - memory_merge tool: allows LLM to merge similar entities - Consolidation task: heartbeat detects conflicts, enqueues via IO for LLM decision - _consolidation_ internal channel for system-level memory maintenance - Comprehensive documentation: ARCHITECTURE.md, PLAN.md, DESIGN.md, README.md - 54 tests across all packages, all passing
This commit is contained in:
@ -11,10 +11,12 @@ import (
|
||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
agentPkg "gitcode.com/JianFeeeee/HomeAgent/internal/agent"
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/plugin/sdk"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/skill"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||||
@ -59,6 +61,10 @@ type Agent struct {
|
||||
|
||||
// 当前请求的输出通道(mutex 保护,process() 内独占)
|
||||
currentOutputChannel string
|
||||
|
||||
// 阶段管道:插件消息流编辑
|
||||
stageHost *StageHost
|
||||
eventBus *events.Bus
|
||||
}
|
||||
|
||||
type AgentConfig struct {
|
||||
@ -78,7 +84,10 @@ type AgentConfig struct {
|
||||
PluginReg *plugin.Registry
|
||||
PluginDir string
|
||||
DistillInterval time.Duration
|
||||
MaxContextSize int // 活跃上下文最大条数,超出按相关性裁剪
|
||||
MaxContextSize int // 活跃上下文最大条数,超出按相关性裁剪
|
||||
ContextSavePath string // 上下文持久化路径,空则不持久化
|
||||
StageHost *StageHost
|
||||
EventBus *events.Bus
|
||||
}
|
||||
|
||||
func New(cfg AgentConfig) *Agent {
|
||||
@ -100,7 +109,7 @@ func New(cfg AgentConfig) *Agent {
|
||||
indexer: cfg.Indexer,
|
||||
skills: cfg.Skills,
|
||||
tracker: cfg.Tracker,
|
||||
context: NewRelevanceContext(),
|
||||
context: NewRelevanceContext(cfg.ContextSavePath),
|
||||
systemPrompt: cfg.SystemPrompt,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
@ -112,6 +121,8 @@ func New(cfg AgentConfig) *Agent {
|
||||
pluginDir: cfg.PluginDir,
|
||||
distillInterval: cfg.DistillInterval,
|
||||
maxContextSize: cfg.MaxContextSize,
|
||||
stageHost: cfg.StageHost,
|
||||
eventBus: cfg.EventBus,
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,13 +179,31 @@ func (a *Agent) processTextInput(evt *agentIO.InputEvent, input string) {
|
||||
a.currentOutputChannel = evt.Source
|
||||
}
|
||||
|
||||
// 记忆整理任务:不路由到外部输出通道
|
||||
if evt.OutputChannel == "_consolidation_" {
|
||||
a.processConsolidation(input)
|
||||
return
|
||||
}
|
||||
|
||||
// === Stage: on_input — 消息到达,插件可拦截 ===
|
||||
stageCtx := a.stageCtxFromInput(input, evt.Source, "")
|
||||
a.publishEvent(events.EventRawInput, map[string]interface{}{
|
||||
"content": input,
|
||||
"source": evt.Source,
|
||||
})
|
||||
if a.runStage(sdk.StageOnInput, stageCtx) {
|
||||
a.emitResponse(evt, *stageCtx.Response)
|
||||
return
|
||||
}
|
||||
input = stageCtx.RawMessage
|
||||
|
||||
a.context.Append(ContextEvent{
|
||||
Timestamp: start,
|
||||
Source: evt.Source,
|
||||
Input: input,
|
||||
})
|
||||
|
||||
response, toolsUsed, err := a.process(input)
|
||||
response, toolsUsed, err := a.process(input, stageCtx)
|
||||
if err != nil {
|
||||
log.Printf("[agent] process error: %v", err)
|
||||
resp := fmt.Sprintf("处理错误: %v", err)
|
||||
@ -206,6 +235,14 @@ func (a *Agent) processTextInput(evt *agentIO.InputEvent, input string) {
|
||||
}
|
||||
|
||||
func (a *Agent) emitResponse(evt *agentIO.InputEvent, response string) {
|
||||
// === Stage: before_output — 最终文本就绪,插件可改写 ===
|
||||
stageCtx := &sdk.StageContext{
|
||||
FinalText: response,
|
||||
Phase: sdk.StageBeforeOutput,
|
||||
}
|
||||
a.runStage(sdk.StageBeforeOutput, stageCtx)
|
||||
response = stageCtx.FinalText
|
||||
|
||||
// 读取当前输出通道(可能已被 AI 通过 output_set_channel 切换)
|
||||
ch := a.currentOutputChannel
|
||||
if ch == "" {
|
||||
@ -230,10 +267,19 @@ func (a *Agent) emitResponse(evt *agentIO.InputEvent, response string) {
|
||||
OutputChannel: ch,
|
||||
}
|
||||
}
|
||||
|
||||
// === Stage: after_output — 输出完成,插件只读 ===
|
||||
a.publishEvent(events.EventAgentOutput, map[string]interface{}{
|
||||
"content": response,
|
||||
"channel": ch,
|
||||
"source": evt.Source,
|
||||
})
|
||||
stageCtx.Phase = sdk.StageAfterOutput
|
||||
a.runStage(sdk.StageAfterOutput, stageCtx)
|
||||
}
|
||||
|
||||
// process — 内部处理,带工具循环
|
||||
func (a *Agent) process(input string) (response string, toolsUsed []string, err error) {
|
||||
// process — 内部处理,带工具循环和阶段管道
|
||||
func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response string, toolsUsed []string, err error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
@ -248,6 +294,20 @@ func (a *Agent) process(input string) (response string, toolsUsed []string, err
|
||||
a.personality != nil && a.personality.Content != "",
|
||||
a.docStoreSize())
|
||||
|
||||
// === Stage: pre_action — 上下文就绪,即将调用 LLM ===
|
||||
if a.runStage(sdk.StagePreAction, stageCtx) {
|
||||
return *stageCtx.Response, toolsUsed, 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 < a.maxTurns; turn++ {
|
||||
req := &agentAPI.CompletionRequest{
|
||||
Messages: msgs,
|
||||
@ -264,6 +324,15 @@ func (a *Agent) process(input string) (response string, toolsUsed []string, err
|
||||
return "", toolsUsed, fmt.Errorf("provider: %w", err)
|
||||
}
|
||||
|
||||
// === Stage: post_action — LLM 返回,插件可审查/修改 ===
|
||||
stageCtx.LLMText = resp.Content
|
||||
stageCtx.ToolCalls = convertToolCalls(resp.ToolCalls)
|
||||
if a.runStage(sdk.StagePostAction, stageCtx) {
|
||||
return *stageCtx.Response, toolsUsed, nil
|
||||
}
|
||||
resp.Content = stageCtx.LLMText
|
||||
resp.ToolCalls = convertBackToolCalls(stageCtx.ToolCalls)
|
||||
|
||||
if len(resp.ToolCalls) == 0 {
|
||||
return resp.Content, toolsUsed, nil
|
||||
}
|
||||
@ -271,16 +340,74 @@ func (a *Agent) process(input string) (response string, toolsUsed []string, err
|
||||
for _, tc := range resp.ToolCalls {
|
||||
toolsUsed = append(toolsUsed, tc.Name)
|
||||
log.Printf("[agent] executing tool: %s (id=%s)", tc.Name, tc.ID)
|
||||
|
||||
// === Stage: before_toolcall — 插件可拒绝/改参 ===
|
||||
sdkTC := sdk.ToolCall{ID: tc.ID, Name: tc.Name, 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", Content: resp.Content, 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,
|
||||
"args": tc.Arguments,
|
||||
"result": result,
|
||||
"status": "denied",
|
||||
})
|
||||
continue
|
||||
}
|
||||
tc.Arguments = stageCtx.ToolCalls[0].Arguments
|
||||
|
||||
result := a.executeToolCall(tc)
|
||||
log.Printf("[agent] tool %s result: %s", tc.Name, truncateStr(result, 100))
|
||||
|
||||
// === Stage: after_toolcall — 插件可改结果 ===
|
||||
stageCtx.ToolResults = []sdk.ToolResult{{CallID: tc.ID, Name: tc.Name, 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
|
||||
}
|
||||
}
|
||||
|
||||
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, 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,
|
||||
"args": tc.Arguments,
|
||||
"result": result,
|
||||
"status": "ok",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return "", toolsUsed, fmt.Errorf("tool execution exceeded %d turns", a.maxTurns)
|
||||
}
|
||||
|
||||
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
|
||||
@ -322,6 +449,15 @@ func (a *Agent) executeToolCall(tc agentAPI.ToolCall) string {
|
||||
return a.executeOutputListChannels()
|
||||
case tc.Name == "plgreload":
|
||||
return a.executePluginReload()
|
||||
case tc.Name == "spawn_child":
|
||||
return a.executeSpawnChild(tc)
|
||||
}
|
||||
|
||||
// 插件工具(通过 SDK RegisterTool 注册)
|
||||
if a.stageHost != nil {
|
||||
if result, err := a.stageHost.ExecuteTool(tc.Name, tc.Arguments); err == nil {
|
||||
return fmt.Sprintf("%v", result)
|
||||
}
|
||||
}
|
||||
|
||||
if a.tracker != nil {
|
||||
@ -414,7 +550,19 @@ func (a *Agent) executeMemoryTool(tc agentAPI.ToolCall) string {
|
||||
return fmt.Sprintf("记忆统计: %v", stats)
|
||||
|
||||
case "memory_document_query":
|
||||
return a.executeDocTool(tc)
|
||||
return a.executeDocTool(tc)
|
||||
|
||||
case "memory_merge":
|
||||
source, _ := tc.Arguments["source"].(string)
|
||||
target, _ := tc.Arguments["target"].(string)
|
||||
if source == "" || target == "" {
|
||||
return "source 和 target 不能为空"
|
||||
}
|
||||
count, err := a.memory.MergeEntities(source, target)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("合并失败: %v", err)
|
||||
}
|
||||
return fmt.Sprintf("已将「%s」合并到「%s」,%d 条关系已重定向", source, target, count)
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("未知的记忆工具: %s", tc.Name)
|
||||
@ -600,12 +748,45 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
// 插件注册的工具(通过 SDK RegisterTool)
|
||||
if a.stageHost != nil {
|
||||
for _, td := range a.stageHost.GetToolDefs() {
|
||||
tools = append(tools, map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": td.Name,
|
||||
"description": td.Description,
|
||||
"parameters": td.Parameters,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if a.indexer != nil {
|
||||
for _, td := range a.indexer.GetToolDefinitions() {
|
||||
tools = append(tools, td)
|
||||
}
|
||||
}
|
||||
|
||||
// 实体合并工具(心跳检测到冲突时 LLM 使用)
|
||||
if a.memory != nil {
|
||||
tools = append(tools, map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": "memory_merge",
|
||||
"description": "合并两个同义实体:将所有关系从 source 重定向到 target,source 标记为 merged。仅在有明确证据时使用。",
|
||||
"parameters": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"source": map[string]interface{}{"type": "string", "description": "被合并的实体名(合并后消失)"},
|
||||
"target": map[string]interface{}{"type": "string", "description": "保留的实体名"},
|
||||
},
|
||||
"required": []string{"source", "target"},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 知识库工具
|
||||
if a.knowledge != nil {
|
||||
tools = append(tools, map[string]interface{}{
|
||||
@ -709,6 +890,25 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
})
|
||||
}
|
||||
|
||||
// 子任务工具
|
||||
tools = append(tools, map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": "spawn_child",
|
||||
"description": "创建一个子 Agent 执行独立任务。子 Agent 使用传统上下文(无持久记忆),任务完成即销毁。适用于需要多步推理但不需要写入长期记忆的场景,例如:计算、分析、生成报告草稿等。",
|
||||
"parameters": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"task": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "要子 Agent 完成的任务描述。请描述清晰、完整,包含所有必要背景。",
|
||||
},
|
||||
},
|
||||
"required": []string{"task"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 输出通道工具
|
||||
tools = append(tools, map[string]interface{}{
|
||||
"type": "function",
|
||||
@ -764,6 +964,20 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
return tools
|
||||
}
|
||||
|
||||
// ConsolidationTask 心跳检测到的记忆整理任务,通过 IO 发送给 Agent 让 LLM 决策
|
||||
type ConsolidationTask struct {
|
||||
Type string `json:"type"` // "entity_merge", "relation_conflict", "doc_archival"
|
||||
Reason string `json:"reason"` // 人类可读的描述
|
||||
Data interface{} `json:"data"` // 任务相关数据
|
||||
}
|
||||
|
||||
// enqueueConsolidationTask 将记忆整理任务注入到 Agent 输入队列
|
||||
func (a *Agent) enqueueConsolidationTask(task ConsolidationTask) {
|
||||
msg := fmt.Sprintf("【记忆整理任务】\n类型: %s\n说明: %s", task.Type, task.Reason)
|
||||
a.io.InjectTextTo("system", "_consolidation_", msg)
|
||||
log.Printf("[agent] enqueued consolidation task: %s", task.Reason)
|
||||
}
|
||||
|
||||
// distillLoop — 定期心跳:上下文→文档 + 图→文档 + 图重整
|
||||
func (a *Agent) distillLoop() {
|
||||
if a.docStore == nil && a.memory == nil {
|
||||
@ -896,34 +1110,85 @@ func (a *Agent) reorgGraph() {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 实体向量同义合并
|
||||
// 4. 实体同义冲突检测 → 交由 LLM 决策
|
||||
result, err := a.memory.Recall(nil, nil, 1, "")
|
||||
if err != nil || result == nil || len(result.Entities) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
merged := 0
|
||||
candidates := 0
|
||||
for i := 0; i < len(result.Entities); i++ {
|
||||
for j := i + 1; j < len(result.Entities); j++ {
|
||||
if isSimilarName(result.Entities[i].Name, result.Entities[j].Name) {
|
||||
if result.Entities[i].MentionCount >= result.Entities[j].MentionCount {
|
||||
log.Printf("[agent] reorg: merging '%s' → '%s'", result.Entities[j].Name, result.Entities[i].Name)
|
||||
} else {
|
||||
log.Printf("[agent] reorg: merging '%s' → '%s'", result.Entities[i].Name, result.Entities[j].Name)
|
||||
}
|
||||
merged++
|
||||
sim := entitySimilarity(result.Entities[i].Name, result.Entities[j].Name)
|
||||
if sim > 0.5 {
|
||||
candidates++
|
||||
a.enqueueConsolidationTask(ConsolidationTask{
|
||||
Type: "entity_merge",
|
||||
Reason: fmt.Sprintf(
|
||||
"实体「%s」(类型:%s, 提及%d次) 与「%s」(类型:%s, 提及%d次) 相似度 %.0f%%,可能指代同一事物,请判断是否需要合并",
|
||||
result.Entities[i].Name, result.Entities[i].Type, result.Entities[i].MentionCount,
|
||||
result.Entities[j].Name, result.Entities[j].Type, result.Entities[j].MentionCount,
|
||||
sim*100,
|
||||
),
|
||||
Data: map[string]interface{}{
|
||||
"entity_a": result.Entities[i].Name,
|
||||
"entity_a_type": result.Entities[i].Type,
|
||||
"entity_a_mentions": result.Entities[i].MentionCount,
|
||||
"entity_b": result.Entities[j].Name,
|
||||
"entity_b_type": result.Entities[j].Type,
|
||||
"entity_b_mentions": result.Entities[j].MentionCount,
|
||||
"similarity": sim,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if merged > 0 {
|
||||
log.Printf("[agent] graph reorg: merged %d similar entities", merged)
|
||||
if candidates > 0 {
|
||||
log.Printf("[agent] graph reorg: %d merge candidates sent for LLM decision", candidates)
|
||||
} else {
|
||||
log.Printf("[agent] graph reorg: no merges needed")
|
||||
log.Printf("[agent] graph reorg: no similar entities found")
|
||||
}
|
||||
}
|
||||
|
||||
// isSimilarName — 使用字符 bigram Jaccard 相似度判断实体名是否同义
|
||||
// entitySimilarity 计算两个实体名的相似度(字符 bigram Jaccard)
|
||||
func entitySimilarity(a, b string) float64 {
|
||||
if a == "" || b == "" {
|
||||
return 0
|
||||
}
|
||||
if a == b {
|
||||
return 1.0
|
||||
}
|
||||
runesA, runesB := []rune(a), []rune(b)
|
||||
if len(runesA) < 2 || len(runesB) < 2 {
|
||||
if len(runesA) == len(runesB) && len(runesA) == 1 {
|
||||
if runesA[0] == runesB[0] {
|
||||
return 1.0
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
setA := make(map[string]bool)
|
||||
for i := 0; i < len(runesA)-1; i++ {
|
||||
setA[string(runesA[i:i+2])] = true
|
||||
}
|
||||
|
||||
intersect := 0
|
||||
for i := 0; i < len(runesB)-1; i++ {
|
||||
if setA[string(runesB[i:i+2])] {
|
||||
intersect++
|
||||
}
|
||||
}
|
||||
|
||||
union := len(setA) + len(runesB) - 1 - intersect
|
||||
if union <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return float64(intersect) / float64(union)
|
||||
}
|
||||
|
||||
// docToTriples 将文档转为图记忆三元组
|
||||
func docToTriples(doc *document.Doc) []memory.Triple {
|
||||
var triples []memory.Triple
|
||||
@ -964,36 +1229,6 @@ func docToTriples(doc *document.Doc) []memory.Triple {
|
||||
return triples
|
||||
}
|
||||
|
||||
func isSimilarName(a, b string) bool {
|
||||
if a == b {
|
||||
return false // 自带跳过
|
||||
}
|
||||
runesA, runesB := []rune(a), []rune(b)
|
||||
if len(runesA) < 2 || len(runesB) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
setA := make(map[string]bool)
|
||||
for i := 0; i < len(runesA)-1; i++ {
|
||||
setA[string(runesA[i:i+2])] = true
|
||||
}
|
||||
|
||||
intersect := 0
|
||||
for i := 0; i < len(runesB)-1; i++ {
|
||||
if setA[string(runesB[i:i+2])] {
|
||||
intersect++
|
||||
}
|
||||
}
|
||||
|
||||
union := len(setA) + len(runesB) - 1 - intersect
|
||||
if union <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
jaccard := float64(intersect) / float64(union)
|
||||
return jaccard > 0.5
|
||||
}
|
||||
|
||||
func (a *Agent) emitMemoryCandidate(source, input, response string, toolsUsed []string) {
|
||||
a.io.EmitOutput("memory", "memory_candidate", map[string]interface{}{
|
||||
"source": source,
|
||||
@ -1007,6 +1242,33 @@ func (a *Agent) emitMemoryCandidate(source, input, response string, toolsUsed []
|
||||
|
||||
// executeOutputChannelTool — AI 切换当前请求的输出通道
|
||||
// 在 process() 内调用,mutex 保护,只有一个请求在执行
|
||||
// processConsolidation 处理后台记忆整理任务(不发外部输出)
|
||||
func (a *Agent) processConsolidation(input string) {
|
||||
start := time.Now()
|
||||
a.currentOutputChannel = "_consolidation_"
|
||||
a.context.Append(ContextEvent{
|
||||
Timestamp: start,
|
||||
Source: "system",
|
||||
Input: input,
|
||||
})
|
||||
response, toolsUsed, err := a.process(input, &sdk.StageContext{RawMessage: input})
|
||||
if err != nil {
|
||||
log.Printf("[agent] consolidation error: %v", err)
|
||||
return
|
||||
}
|
||||
a.context.Append(ContextEvent{
|
||||
Timestamp: time.Now(),
|
||||
Source: "agent",
|
||||
Input: input,
|
||||
Response: response,
|
||||
ToolsUsed: toolsUsed,
|
||||
})
|
||||
_ = a.context.Prune(response, a.maxContextSize, a.docStore)
|
||||
// 只写入记忆,不发外部输出
|
||||
a.emitMemoryCandidate("system", input, response, toolsUsed)
|
||||
log.Printf("[agent] consolidation done (%dms, tools=%v)", time.Since(start).Milliseconds(), toolsUsed)
|
||||
}
|
||||
|
||||
func (a *Agent) executeOutputChannelTool(tc agentAPI.ToolCall) string {
|
||||
channel, _ := tc.Arguments["channel"].(string)
|
||||
if channel == "" {
|
||||
@ -1077,6 +1339,118 @@ func (a *Agent) executePluginReload() string {
|
||||
return msg
|
||||
}
|
||||
|
||||
// executeSpawnChild 创建子 Agent 执行独立任务
|
||||
// 子 Agent 使用传统上下文(单轮对话),无持久记忆,任务完即销毁
|
||||
func (a *Agent) executeSpawnChild(tc agentAPI.ToolCall) string {
|
||||
task, _ := tc.Arguments["task"].(string)
|
||||
if task == "" {
|
||||
return "请提供 task 参数"
|
||||
}
|
||||
|
||||
sysPrompt := fmt.Sprintf(`你是 HomeAgent 的子任务助手。
|
||||
请完成以下任务。完成即可,无需保留记忆或查询历史。
|
||||
任务: %s`, task)
|
||||
|
||||
msgs := []agentAPI.Message{
|
||||
{Role: "system", Content: sysPrompt},
|
||||
{Role: "user", Content: task},
|
||||
}
|
||||
|
||||
// 子 Agent 无特殊工具,只保留基础 tool 定义(无记忆/知识/文档工具)
|
||||
childTools := []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": "output_send",
|
||||
"description": "通过指定输出通道发送消息",
|
||||
"parameters": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"channel": map[string]interface{}{"type": "string", "description": "输出通道"},
|
||||
"content": map[string]interface{}{"type": "string", "description": "消息内容"},
|
||||
},
|
||||
"required": []string{"channel", "content"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for turn := 0; turn < 5; turn++ {
|
||||
req := &agentAPI.CompletionRequest{
|
||||
Messages: msgs,
|
||||
MaxTokens: 4096,
|
||||
Tools: childTools,
|
||||
ToolChoice: "auto",
|
||||
ExtraBody: map[string]interface{}{
|
||||
"thinking": map[string]interface{}{"type": "disabled"},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := a.provider.Chat(a.ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("子 Agent 执行失败: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.ToolCalls) == 0 {
|
||||
return resp.Content
|
||||
}
|
||||
|
||||
for _, ct := range resp.ToolCalls {
|
||||
var result string
|
||||
if ct.Name == "output_send" {
|
||||
channel, _ := ct.Arguments["channel"].(string)
|
||||
content, _ := ct.Arguments["content"].(string)
|
||||
if channel != "" && content != "" {
|
||||
a.io.EmitTextTo("child_agent", channel, content)
|
||||
result = fmt.Sprintf("已通过 [%s] 通道发送", channel)
|
||||
} else {
|
||||
result = "channel 和 content 不能为空"
|
||||
}
|
||||
} else {
|
||||
result = fmt.Sprintf("子 Agent 无法调用工具 %s", ct.Name)
|
||||
}
|
||||
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: []agentAPI.ToolCall{ct}})
|
||||
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: ct.ID, Content: result})
|
||||
}
|
||||
}
|
||||
|
||||
return "子 Agent 执行超时(超过 5 轮)"
|
||||
}
|
||||
|
||||
// runStage — 运行阶段管道,若插件 Response 被设置则返回 true(短路)
|
||||
func (a *Agent) runStage(stage sdk.Stage, ctx *sdk.StageContext) bool {
|
||||
if a.stageHost == nil {
|
||||
return false
|
||||
}
|
||||
ctx.Phase = stage
|
||||
a.stageHost.RunStage(stage, ctx)
|
||||
return ctx.Response != nil
|
||||
}
|
||||
|
||||
// publishEvent — 发布系统事件
|
||||
func (a *Agent) publishEvent(evtType events.EventType, payload map[string]interface{}) {
|
||||
if a.eventBus == nil {
|
||||
return
|
||||
}
|
||||
a.eventBus.Publish(&events.Event{
|
||||
Type: evtType,
|
||||
Source: string(a.id),
|
||||
Payload: payload,
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
// stageCtxFromInput — 根据输入构建阶段上下文
|
||||
func (a *Agent) stageCtxFromInput(input, userID, groupID string) *sdk.StageContext {
|
||||
return &sdk.StageContext{
|
||||
RawMessage: input,
|
||||
UserID: userID,
|
||||
GroupID: groupID,
|
||||
Phase: sdk.StageOnInput,
|
||||
Extra: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
func getFloat(m map[string]interface{}, key string) float64 {
|
||||
if v, ok := m[key]; ok {
|
||||
switch n := v.(type) {
|
||||
|
||||
Reference in New Issue
Block a user