Files
Agent-System-Backend/internal/runtime/mock.go
T
weichen c7736a1917 Integrate the Agent-Squad engine
Details (original message):
Integrate the Agent-Squad engine: squadadapter maps RoleRegistry/RulePlanner/BackendResolver/ProgressSink/HITL onto the unified Run and event model (step subagents, plan updates, deliverable artifacts, confirmation cards); step-wise engine driving loop; model routing respects profile routes with layered resolver + integration tests; context assembly observation flows to run.completed; mock outputs and e2e coverage.
2026-09-29 18:27:49 +00:00

182 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]) + "…"
}