package runtime import ( "encoding/json" "fmt" "strings" agentllm "agent-native/runtime/llm" core "agent-system-core" "agent-system-backend/internal/ctxasm" ) // LLMRunner 主对话执行器:复用 agent-native 的 OpenAI 兼容流式客户端(不自研接入层)。 // 多轮循环:流式 → 工具调用(画像策略)→ 回灌结果 → 直至无工具调用或预算耗尽。 // 内置三个编排型工具(不经注册表):delegate_task(子代理)、update_plan、update_todos。 type LLMRunner struct{} func (l *LLMRunner) Name() string { return "llm" } const ( toolDelegate = "delegate_task" toolPlan = "update_plan" toolTodos = "update_todos" ) func (l *LLMRunner) Execute(rc RunCtx) error { eff := rc.Settings() chat := eff.GetString("models.chat.model", "") base := eff.GetString("models.chat.baseUrl", "") key := eff.GetString("models.chat.apiKey", "") if chat == "" || base == "" { return fmt.Errorf("llm: 主模型未配置(settings: models.chat.baseUrl / models.chat.model;演示模式可开 runtime.mock)") } client := &agentllm.Client{BaseURL: base, APIKey: key} prof := rc.Profile() // 知识注入:当前消息包含 @knowledge 引用时检索注入(最小实现:显式引用)。 var knowledge []core.RetrievalHit if strings.Contains(currentUserText(rc), "@knowledge") { q := strings.TrimSpace(strings.ReplaceAll(currentUserText(rc), "@knowledge", "")) if hits, err := rc.Retrieve(core.RetrievalQuery{Text: q}); err == nil { knowledge = hits } } // 组装初始上下文。 hist := rc.History() var currentUser core.Message for i := len(hist) - 1; i >= 0; i-- { if hist[i].Role == core.RoleUser { currentUser = hist[i] break } } msgs, _ := ctxasm.AssembleInput(prof, eff, hist, currentUser, rc.Attachments(), knowledge) // 声明工具:编排型内置 + 注册表中画像允许的工具。 decls := []agentllm.ToolDecl{ {Name: toolDelegate, Description: "派生一个子代理执行独立子任务(如检索、撰写),返回其结果摘要", Parameters: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string","description":"子代理角色名"},"task":{"type":"string","description":"子任务描述"},"hint":{"type":"string","description":"执行提示"}},"required":["name","task"]}`)}, {Name: toolPlan, Description: "创建或更新当前会话的计划(步骤列表)", Parameters: json.RawMessage(`{"type":"object","properties":{"title":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"},"status":{"type":"string","enum":["pending","in_progress","done","skipped"]}},"required":["id","text","status"]}}},"required":["title","steps"]}`)}, {Name: toolTodos, Description: "更新当前会话的待办清单", Parameters: json.RawMessage(`{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"},"status":{"type":"string","enum":["pending","in_progress","done"]}},"required":["id","text","status"]}}},"required":["items"]}`)}, } var toolDescs []core.ToolDesc for _, t := range toolList(rc) { if prof.Tools.ToolAllowed(t.Name) { toolDescs = append(toolDescs, t) decls = append(decls, agentllm.ToolDecl{Name: t.Name, Description: t.Description, Parameters: t.Parameters}) } } var finalParts []core.Part usageTotal := core.Usage{} maxTurns := 16 if prof.Budget.MaxTurns != nil { maxTurns = *prof.Budget.MaxTurns } maxTools := 40 if prof.Budget.MaxToolCalls != nil { maxTools = *prof.Budget.MaxToolCalls } toolCallsMade := 0 thinkingIdx, textIdx := 0, 0 for turn := 0; turn < maxTurns; turn++ { select { case <-rc.Context().Done(): return rc.Context().Err() default: } var turnThinking, turnText strings.Builder var calls []agentllm.ToolCall var usage agentllm.Usage ev := client.Stream(rc.Context(), chat, msgs, decls, agentllm.StreamOptions{}) phaseEmitted := false for e := range ev { switch e.Kind { case agentllm.KindTextDelta: if !phaseEmitted { rc.Emit(core.EvRunPhase, core.RunPhasePayload{Phase: core.PhaseResponding}) phaseEmitted = true } turnText.WriteString(e.Text) rc.Emit(core.EvMessageDelta, core.MessageDeltaPayload{Index: textIdx, Text: e.Text}) case agentllm.KindReasoningDelta: turnThinking.WriteString(e.ReasoningDelta) rc.Emit(core.EvThinkingDelta, core.ThinkingDeltaPayload{Index: thinkingIdx, Text: e.ReasoningDelta}) case agentllm.KindToolCallDelta: // 累积由 message_end 的 ToolCalls 提供;此处忽略增量。 case agentllm.KindMessageEnd: calls = e.Message.ToolCalls usage = e.Usage case agentllm.KindError: return fmt.Errorf("llm stream: %s (retryable=%v)", e.ErrorMessage, e.Retryable) } } usageTotal.PromptTokens += usage.PromptTokens usageTotal.CompletionTokens += usage.CompletionTokens usageTotal.TotalTokens += usage.TotalTokens // 记录本轮部件。 if turnThinking.Len() > 0 { finalParts = append(finalParts, core.Part{Type: core.PartThinking, Text: turnThinking.String(), Process: true}) thinkingIdx++ } if turnText.Len() > 0 { finalParts = append(finalParts, core.Part{Type: core.PartText, Text: turnText.String()}) textIdx++ } if len(calls) == 0 { // 终轮:无工具调用。 if len(finalParts) == 0 { finalParts = append(finalParts, core.Part{Type: core.PartText, Text: "(模型未返回内容)"}) } rc.SetAssistantParts(finalParts) return nil } // 工具轮。 rc.Emit(core.EvRunPhase, core.RunPhasePayload{Phase: core.PhaseActing}) msgs = append(msgs, agentllm.Message{Role: agentllm.RoleAssistant, Content: turnText.String(), ToolCalls: calls, StopReason: agentllm.StopToolCalls}) for _, call := range calls { toolCallsMade++ if toolCallsMade > maxTools { return fmt.Errorf("budget: 工具调用超过上限 %d(profile.budget.maxToolCalls)", maxTools) } rc.Emit(core.EvToolCall, core.ToolCallPayload{CallID: call.ID, Name: call.Name, Arguments: call.Arguments}) var out string var err error var artifactRef *core.ArtifactRef switch call.Name { case toolDelegate: var a struct{ Name, Task, Hint string } json.Unmarshal([]byte(call.Arguments), &a) res, serr := rc.SpawnSubagent(orDefault(a.Name, "子代理"), a.Task, a.Hint) if serr != nil { out = "子代理失败:" + serr.Error() } else { out = orDefault(res.Summary, "子代理完成") } case toolPlan: var p core.PlanSnapshot if err := json.Unmarshal([]byte(call.Arguments), &p); err == nil { rc.UpdatePlan(p) out = "计划已更新" } else { out = "计划参数错误: " + err.Error() } case toolTodos: var t core.TodoList if err := json.Unmarshal([]byte(call.Arguments), &t); err == nil { rc.UpdateTodos(t) out = "待办已更新" } else { out = "待办参数错误: " + err.Error() } default: res := rc.ExecuteTool(core.ToolCallRequest{CallID: call.ID, Name: call.Name, Arguments: json.RawMessage(call.Arguments)}) if res.Status != "ok" { err = fmt.Errorf("%s", res.Output) } else { out = res.Output } if res.Artifact != nil { artifactRef = &core.ArtifactRef{ArtifactID: res.Artifact.ID, Name: res.Artifact.Name, Kind: res.Artifact.Kind} } rc.Emit(core.EvToolResult, core.ToolResultPayload{CallID: call.ID, Status: res.Status, Summary: summarize(out, err), Truncated: res.Truncated}) } if call.Name == toolDelegate || call.Name == toolPlan || call.Name == toolTodos { rc.Emit(core.EvToolResult, core.ToolResultPayload{CallID: call.ID, Status: "ok", Summary: summarize(out, err)}) } summary := "" if err != nil { summary = "error: " + err.Error() out = summary } finalParts = append(finalParts, core.Part{Type: core.PartToolCall, Process: true, ToolCall: &core.ToolCallPart{ID: call.ID, Name: call.Name, Arguments: call.Arguments}}, core.Part{Type: core.PartToolResult, Process: true, ToolResult: &core.ToolResultPart{CallID: call.ID, Status: statusOf(err), Summary: summary}}, ) if artifactRef != nil { finalParts = append(finalParts, core.Part{Type: core.PartArtifactRef, Artifact: artifactRef}) } // 回灌工具结果(截断保护)。 if len(out) > 16000 { out = out[:16000] + "…(超长截断)" } msgs = append(msgs, agentllm.Message{Role: agentllm.RoleTool, ToolCallID: call.ID, Name: call.Name, Content: out, IsError: err != nil}) } } return fmt.Errorf("budget: 轮次超过上限 %d(profile.budget.maxTurns)", maxTurns) } // SubLLMRunner 子代理执行器:轻量模型单轮完成任务并返回摘要。 type SubLLMRunner struct{} func (l *SubLLMRunner) Name() string { return "llm-subagent" } func (l *SubLLMRunner) Execute(rc RunCtx) error { eff := rc.Settings() model := eff.GetString("models.lightweight.model", "") base := eff.GetString("models.lightweight.baseUrl", "") key := eff.GetString("models.lightweight.apiKey", "") if model == "" || base == "" { model = eff.GetString("models.chat.model", "") base = eff.GetString("models.chat.baseUrl", "") key = eff.GetString("models.chat.apiKey", "") } if model == "" || base == "" { return fmt.Errorf("llm-subagent: 模型未配置") } rc.Emit(core.EvAgentProgress, core.AgentProgressPayload{AgentRun: rc.Run().ID, Note: "执行子任务……", Phase: core.PhaseActing}) client := &agentllm.Client{BaseURL: base, APIKey: key} msgs := []agentllm.Message{ {Role: agentllm.RoleSystem, Content: "你是被派生的子代理。独立完成给定子任务,输出简明结果摘要(不超过 300 字)。"}, {Role: agentllm.RoleUser, Content: rc.Subtask()}, } var sb strings.Builder for e := range client.Stream(rc.Context(), model, msgs, nil, agentllm.StreamOptions{}) { if e.Kind == agentllm.KindTextDelta { sb.WriteString(e.Text) rc.Emit(core.EvAgentProgress, core.AgentProgressPayload{AgentRun: rc.Run().ID, Note: truncateRun(sb.String(), 80)}) } if e.Kind == agentllm.KindError { return fmt.Errorf("subagent stream: %s", e.ErrorMessage) } } rc.SetAssistantParts([]core.Part{{Type: core.PartText, Text: sb.String()}}) return nil } func currentUserText(rc RunCtx) string { hist := rc.History() for i := len(hist) - 1; i >= 0; i-- { if hist[i].Role == core.RoleUser { for _, p := range hist[i].Parts { if p.Type == core.PartText { return p.Text } } } } return "" } func toolList(rc RunCtx) []core.ToolDesc { return rc.Tools() } func orDefault(s, def string) string { if strings.TrimSpace(s) == "" { return def } return s } func summarize(out string, err error) string { if err != nil { return truncateRun(err.Error(), 160) } return truncateRun(out, 160) } func statusOf(err error) string { if err != nil { return "error" } return "ok" } func truncateRun(s string, n int) string { r := []rune(s) if len(r) <= n { return s } return string(r[:n]) + "…" }