package runtime import ( "fmt" "strings" "time" core "agent-system-core" "agent-system-backend/internal/squadadapter" ) func firstNonEmpty0(vals ...string) string { for _, v := range vals { if strings.TrimSpace(v) != "" { return v } } return "" } // MockRunner 按脚本回放事件流(P1 骨架闭环与验收基线;不调用任何模型)。 // 用户消息中的指令词触发不同演示路径,其余走默认脚本(思考→工具→回复)。 type MockRunner struct{} func (m *MockRunner) Name() string { return "mock" } func (m *MockRunner) Execute(rc RunCtx) error { // 取最后一条用户消息文本 user := "" if hist := rc.History(); len(hist) > 0 { last := hist[len(hist)-1] if last.Role == core.RoleUser { for _, p := range last.Parts { if p.Type == core.PartText { user = p.Text } } } } var parts []core.Part addText := func(t string) { parts = append(parts, core.Part{Type: core.PartText, Text: t}) } // mock 路径同样产出上下文观测(演示可解释性入口;真实路径由 ctxasm 产出)。 rc.SetContextObservation(core.ContextObservation{ Items: []string{"system(画像)", "history(mock)", "attachments(0)"}, EstChars: len(user) + 128, }) switch { case strings.Contains(user, "#hitl"): resp, err := rc.AskHITL(core.HITLCard{ Question: "演示:采用哪种输出格式?", Kind: core.HITLKindChoice, Choices: []core.HITLChoice{ {ID: "md", Label: "Markdown 文档", Description: "可版本管理的轻量文档", Impact: "写入 artifacts,可在右栏查看与继续编辑", Recommended: true}, {ID: "docx", Label: "Word 文档", Description: "正式交付格式", Impact: "生成 docx 工件,后续修订走版本对比"}, {ID: "both", Label: "两种都要", Description: "同时产出", Impact: "生成两个工件"}, }, AllowFreeInput: true, FreeInputHint: "也可以直接输入你的要求(输入 / 开头则转新命令)", }) if err != nil { return err } answer := resp.FreeText if resp.ChoiceID != "" { answer = "选项 " + resp.ChoiceID } parts = append(parts, core.Part{Type: core.PartHITL, Hitl: &core.HITLSummary{Question: "演示:采用哪种输出格式?", Resolved: true, Answer: answer}}) addText(fmt.Sprintf("收到你的回答:%s。已按该选择继续执行。", answer)) case strings.Contains(user, "#agents"): rc.UpdatePlan(core.PlanSnapshot{Title: "多代理演示计划", Steps: []core.PlanStep{ {ID: "s1", Text: "检索资料", Status: "in_progress"}, {ID: "s2", Text: "撰写摘要", Status: "pending"}, {ID: "s3", Text: "汇总输出", Status: "pending"}, }}) rc.UpdateTodos(core.TodoList{Items: []core.TodoItem{ {ID: "t1", Text: "派生两个子代理", Status: "in_progress"}, {ID: "t2", Text: "等待结果", Status: "pending"}, }}) rc.Emit(core.EvThinkingDelta, core.ThinkingDeltaPayload{Index: 0, Text: "任务需要并行检索,派生子代理……"}) parts = append(parts, core.Part{Type: core.PartThinking, Text: "任务需要并行检索,派生子代理……", Process: true}) r1, _ := rc.SpawnSubagent("资料检索员", "检索项目知识与历史会话", "知识检索") r2, _ := rc.SpawnSubagent("撰稿员", "根据检索结果撰写摘要", "写作") rc.UpdateTodos(core.TodoList{Items: []core.TodoItem{ {ID: "t1", Text: "派生两个子代理", Status: "done"}, {ID: "t2", Text: "等待结果", Status: "done"}, }}) rc.UpdatePlan(core.PlanSnapshot{Title: "多代理演示计划", Steps: []core.PlanStep{ {ID: "s1", Text: "检索资料", Status: "done"}, {ID: "s2", Text: "撰写摘要", Status: "done"}, {ID: "s3", Text: "汇总输出", Status: "in_progress"}, }}) addText(fmt.Sprintf("两个子代理已完成:%s(%s);%s(%s)。可在右栏 Agents 标签查看任务树与中间过程。", r1.Summary, r1.Status, r2.Summary, r2.Status)) rc.UpdatePlan(core.PlanSnapshot{Title: "多代理演示计划", Steps: []core.PlanStep{ {ID: "s1", Text: "检索资料", Status: "done"}, {ID: "s2", Text: "撰写摘要", Status: "done"}, {ID: "s3", Text: "汇总输出", Status: "done"}, }}) case strings.Contains(user, "#doc"): rc.Emit(core.EvThinkingDelta, core.ThinkingDeltaPayload{Index: 0, Text: "准备生成文档……"}) parts = append(parts, core.Part{Type: core.PartThinking, Text: "准备生成文档……", Process: true}) content := "# 演示文档\n\n这是 mock runner 生成的示例工件。\n\n## 要点\n\n- 由 #doc 指令触发\n- 写入 artifacts 并可在右栏查看\n" art, err := rc.WriteArtifact("demo-报告.md", "document", "text/markdown", []byte(content)) if err == nil { parts = append(parts, core.Part{Type: core.PartArtifactRef, Artifact: &core.ArtifactRef{ArtifactID: art.ID, Name: art.Name, Kind: art.Kind}}) } addText("文档已生成,见右侧 Artifacts 标签(也可点击消息中的工件引用)。") case strings.Contains(user, "#squad"): rc.Emit(core.EvThinkingDelta, core.ThinkingDeltaPayload{Index: 0, Text: "调用 Agent-Squad 引擎编排……"}) parts = append(parts, core.Part{Type: core.PartThinking, Text: "调用 Agent-Squad 引擎编排……", Process: true}) goal := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(user, "#squad", ""), "#hitl", "")) if eff := rc.Settings(); !eff.GetBool("runtime.squad", true) { addText("小队编排已被设置禁用(runtime.squad)。") rc.SetAssistantParts(parts) return nil } dataDir := rc.Settings().GetString("storage.dataDir", "./data") summary, err := squadadapter.Orchestrate(rc, dataDir, firstNonEmpty0(goal, "生成一份关于演示目标的简要报告")) if err != nil { addText("小队编排结束(" + err.Error() + ")。摘要:" + summary) } else { addText("Agent-Squad 编排完成:" + summary + "。步骤与子代理见右栏「计划」「代理」。") } case strings.Contains(user, "#error"): return fmt.Errorf("mock: 演示失败路径(#error 指令)") default: // 默认脚本:思考 → 工具调用(知识检索)→ 回复(流式逐字)。 rc.Emit(core.EvThinkingDelta, core.ThinkingDeltaPayload{Index: 0, Text: "理解问题,先检索项目知识……"}) parts = append(parts, core.Part{Type: core.PartThinking, Text: "理解问题,先检索项目知识……", Process: true}) rc.Emit(core.EvRunPhase, core.RunPhasePayload{Phase: core.PhaseActing}) callID := "call_mock_1" rc.Emit(core.EvToolCall, core.ToolCallPayload{CallID: callID, Name: "knowledge_search", Arguments: `{"query":"` + truncateForJSON(user, 40) + `"}`}) res := rc.ExecuteTool(core.ToolCallRequest{CallID: callID, Name: "knowledge_search", Arguments: []byte(`{"query":"演示"}`)}) rc.Emit(core.EvToolResult, core.ToolResultPayload{CallID: callID, Status: res.Status, Summary: truncateForJSON(res.Output, 120), Truncated: res.Truncated}) parts = append(parts, core.Part{Type: core.PartToolCall, Process: true, ToolCall: &core.ToolCallPart{ID: callID, Name: "knowledge_search", Arguments: `{"query":"演示"}`}}, core.Part{Type: core.PartToolResult, Process: true, ToolResult: &core.ToolResultPart{CallID: callID, Status: res.Status, Summary: truncateForJSON(res.Output, 120)}}, ) rc.Emit(core.EvRunPhase, core.RunPhasePayload{Phase: core.PhaseResponding}) reply := fmt.Sprintf("(mock 回复)你说:%s\n\n这是 mock runner 的脚本化回复——未调用真实模型。可发送 #hitl / #agents / #doc / #error 触发对应演示路径。", user) idx := 0 for _, r := range reply { rc.Emit(core.EvMessageDelta, core.MessageDeltaPayload{Index: idx, Text: string(r)}) time.Sleep(8 * time.Millisecond) } addText(reply) } rc.SetAssistantParts(parts) return nil } // MockSubagent 子代理脚本回放:进度事件 + 摘要结果。 type MockSubagent struct{} func (m *MockSubagent) Name() string { return "mock-subagent" } func (m *MockSubagent) Execute(rc RunCtx) error { rc.Emit(core.EvAgentProgress, core.AgentProgressPayload{AgentRun: rc.Run().ID, Note: "开始子任务……", Phase: core.PhaseThinking}) time.Sleep(150 * time.Millisecond) rc.Emit(core.EvAgentProgress, core.AgentProgressPayload{AgentRun: rc.Run().ID, Note: "处理中……", Phase: core.PhaseActing}) time.Sleep(150 * time.Millisecond) task := rc.Subtask() rc.SetAssistantParts([]core.Part{{Type: core.PartText, Text: fmt.Sprintf("# 子代理产出(mock)\n\n任务:%s\n\n- 已按子代理角色完成处理\n- 本文本将作为小队步骤的交付物写入工作区\n- 任务树与中间过程可在右栏与弹窗查看\n", firstNonEmpty0(task, "(未提供任务描述)"))}}) return nil } func truncateForJSON(s string, n int) string { r := []rune(s) if len(r) <= n { return s } return string(r[:n]) + "…" }