Files
Agent-System-Backend/internal/runtime/mock.go
T
weichen 70afe40b00
ci / vet-and-test (pull_request) Successful in 47s
Retire the legacy workflow executor and switch squad steps to agent-native (Q3 WP2)
Why: Q3 WP2 retires the SpawnSubagent-compression execution path: all
WorkflowDef execution now goes through the wfcompile compiled graph, and
squad steps run on the agent-native kernel backend instead of the
agent-system step backend.

Evidence: runtime-level P4 parity tests (workflow_compiled_test.go):
registered branch lookup/errors, planned branch confirmation gate +
dynamic 8-step cap fail-closed before any HITL, model script switch
default-off, missing-slot HITL collection with compile-time rendering,
judge stream routed to the configured lightweight model (nil = fail-closed
verifier-unavailable). squadadapter TestOrchestrateRunsOnKernelStub:
four-phase skeleton runs end-to-end on the kernel stub (write_file
materialization, seeded skills, exec allowlist). server TestSquadOrchestration
reworked: kernel steps run in-process (no subagent child runs). Full
backend suite green; wfcompile/squadadapter/runmgr green with -race
(pre-existing llm_test fake race on main recorded as debt, CI does not
gate -race).

Impact: DELETED runtime/workflow.go + workflow_script.go (legacy step loop,
critic, script host) and their tests; squadadapter stepBackend
'agent-system' + profileForRole deleted; BuildBackends exported and
registers only agent-native; Orchestrate DefaultBackend -> agent-native;
builtin roles declare agent-native with write_file added to allowlists and
writer run_shell -> run_command (kernel tool name); runmgr default workflow
runner -> CompiledWorkflowRunner; mock #workflow demo runs compiled
(host-only steps). Model script branch keeps the single script-level
confirmation (no extra plan confirmation), matching legacy.

Refs: Q3 plan WP2 (2026-10-07-agent-stack-q2q3-plans.md); task card
.task/feature-agent-stack-q3.md
2026-10-08 03:24:30 +00:00

298 lines
16 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"
"agent-system-backend/internal/docgen"
"agent-system-backend/internal/squadadapter"
"agent-system-backend/internal/wfcompile"
"agent-system-backend/internal/workflow"
core "agent-system-core"
)
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, "#upload"):
// 收件箱卡片流演示(提案 T2):摄取卡生命周期 + 冲突对比卡(待处理)。
addText("📎 已上传 临床前动物试验方案-草案.pdf(演示)——上传即构建记忆:先给相关性结论,解析进度在右栏卡片。")
card, _ := rc.CreateCard(core.ActivityCard{Kind: core.CardKindProgress, Title: "临床前动物试验方案-草案.pdf", Status: core.CardActive, Tone: "info",
Body: []core.UINode{{Type: core.UIProgress, Progress: &core.UIProgressSpec{Label: "解析中", Value: 0.25}}}})
rc.Emit(core.EvThinkingDelta, core.ThinkingDeltaPayload{Index: 0, Text: "记忆构建管道:相关性 ✓(命中:试验样本量、随访周期)→ 拆解 → 打标签 → 切片入库……"})
parts = append(parts, core.Part{Type: core.PartThinking, Text: "记忆构建管道:相关性 ✓ → 拆解 → 打标签 → 切片入库", Process: true})
card.Body = []core.UINode{
{Type: core.UIProgress, Progress: &core.UIProgressSpec{Label: "切片 24/24", Value: 1, Tone: "ok"}},
{Type: core.UIKV, KV: [][2]string{{"快通道", "✓ 38s"}, {"慢通道", "✓ 3m12s"}, {"追溯候选边", "6 条"}}},
{Type: core.UIList, Title: "标签", Items: []string{"验证确认(跨阶段引用)", "technical ×1", "regulatory ×1"}},
}
card.Status = core.CardDone
card.Tone = "ok"
_ = rc.SetCardStatus(card.ID, core.CardDone, "ok", "")
_ = rc.Emit(core.EvCardUpdated, core.CardPayload{Card: card})
cmp, _ := rc.CreateCard(core.ActivityCard{Kind: core.CardKindCompare, Title: "项目执行周期不一致 · 目标已基线", Status: core.CardActionRequired, Tone: "danger",
Body: []core.UINode{{Type: core.UICompare, Compare: &core.UICompareSpec{
Left: core.UICompareSide{Title: "基线 V1.2 · 已评审", Tone: "danger", Nodes: []core.UINode{{Type: core.UIMarkdown, Markdown: "12 个月(立项到注册)"}}},
Right: core.UICompareSide{Title: "新资料 · 待确认", Tone: "ok", Nodes: []core.UINode{{Type: core.UIMarkdown, Markdown: "18 个月(含临床试验)"}}},
}}},
Actions: []core.UIAction{{ID: "escalate", Label: "发起变更流程", Style: "primary"}, {ID: "keep", Label: "维持原样", Style: "danger"}},
})
parts = append(parts, core.Part{Type: core.PartCard, Card: &core.CardPart{Kind: "cardlink", Data: []core.UINode{{Type: core.UIMarkdown, Markdown: fmt.Sprintf("↗ 解析完成见卡片;冲突处理卡 `card:%s` 已进右栏收件箱(待处理)", cmp.ID)}}}})
case strings.Contains(user, "#cardflow"):
// 卡流机制演示(CF1/D39):产物卡显式路由——中栏结论摘要 + 引用锚点,全文进收件箱。
rc.Emit(core.EvThinkingDelta, core.ThinkingDeltaPayload{Index: 0, Text: "生成会议纪要……"})
parts = append(parts, core.Part{Type: core.PartThinking, Text: "生成会议纪要……", Process: true})
body := "# 会议纪要(演示)\n\n## 决议\n\n1. 注册路径走 FDA 510(k)\n2. 样机 B 版冻结\n3. 供应商切换延后一轮\n\n## 待办\n\n- 法规组输出对比表\n- 结构组更新 BOM\n"
card, _ := rc.SubmitCard(core.ActivityCard{
Kind: core.CardKindResult, Title: "会议纪要 · 第 12 次周会", Status: core.CardDone, Tone: "ok",
Body: []core.UINode{
{Type: core.UIMarkdown, Markdown: body},
{Type: core.UIKV, KV: [][2]string{{"摘要", "三项决议、两项待办;注册路径定为 510(k)"}}},
},
})
addText("已完成会议纪要:三项决议、两项待办,注册路径定为 510(k)。全文见右侧卡片,可在收件箱置顶或收纳。")
parts = append(parts, core.Part{Type: core.PartCard, Card: &core.CardPart{Kind: string(core.CardKindResult), CardID: card.ID}})
case strings.Contains(user, "#cardstream"):
// 流式产卡演示(CF-S/D44-D45):card_begin → append×N → card_commit,
// 期间收件箱可见 active 卡随 card.updated 增量刷新。
rc.Emit(core.EvThinkingDelta, core.ThinkingDeltaPayload{Index: 0, Text: "流式生成周报……"})
parts = append(parts, core.Part{Type: core.PartThinking, Text: "流式生成周报……", Process: true})
if _, err := rc.BeginStreamCard(core.ActivityCard{
Kind: core.CardKindResult, Title: "项目周报 · 流式", Tone: "ok",
Body: []core.UINode{{Type: core.UIKV, KV: [][2]string{{"摘要", "三段式流式产卡演示:进展 / 风险 / 下周"}}}},
}); err != nil {
addText("收件箱未启用或流式卡开始失败(" + err.Error() + "),内容直接写在对话中。")
} else {
for _, seg := range []string{
"## 进展\n\n- 卡流三段式工具上线\n- 组合验证全绿\n",
"## 风险\n\n- Gitea 主机偶发中断(重试可恢复)\n",
"## 下周\n\n- artifact 统一(CF-A)\n- 跨会话收件箱(CF3)\n",
} {
if aerr := rc.AppendStreamCard(seg); aerr != nil {
break
}
}
if done, cerr := rc.CommitStreamCard(); cerr == nil {
addText("周报已流式生成完毕,全文见右侧卡片。")
parts = append(parts, core.Part{Type: core.PartCard, Card: &core.CardPart{Kind: string(core.CardKindResult), CardID: done.ID}})
} else {
addText("周报流式收尾失败:" + cerr.Error())
}
}
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})
// 真 docx 演示(mock 专用确定性生成器——真实链路由 Agent 经 run_python +
// register_artifact 自产自登记):mock 模式即可看到「带排版 Word + 交付物
// 卡片」全链路——标题层级/表格/列表都在文档字节里。
md := "# 演示会议纪要\n\n这是 mock runner 生成的示例 Word 工件。\n\n## 基本信息\n\n| 项目 | 内容 |\n|---|---|\n| 指令 | #doc |\n| 生成器 | docgen |\n\n## 要点\n\n1. 由 #doc 指令触发\n2. 带 Word 排版(标题/表格/编号列表)\n3. 对话内以交付物卡片呈现,点击右栏预览\n"
buf, derr := docgen.Build("", md)
if derr != nil {
addText("演示文档生成失败:" + derr.Error())
break
}
addText("Word 文档已生成——点击下方交付卡片右栏预览,或 ⌘1 工件面板查看。")
art, err := rc.WriteArtifact("demo-会议纪要.docx", "document",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", buf)
if err == nil {
// 交付物钉尾(内容部件,与 llm.go 契约一致):卡片在最终正文之后。
parts = append(parts, core.Part{Type: core.PartArtifactRef, Artifact: &core.ArtifactRef{ArtifactID: art.ID, Name: art.Name, Kind: art.Kind}})
// 工件门面卡演示(CF-A/D47):registry 工具大结果的统一呈现——
// 摘要 + 来源 kv + doc-ref(正文权威在工件);收件箱关闭时 SubmitCard
// 返回空 ID,跳过卡部件。
if fc, ferr := rc.SubmitCard(artifactFacadeCard("run_python", art, "演示 Word 工件已生成(#doc,带排版)")); ferr == nil && fc.ID != "" {
parts = append(parts, core.Part{Type: core.PartCard, Card: &core.CardPart{Kind: string(core.CardKindResult), CardID: fc.ID}})
}
}
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, "#workflow"):
// Q3 WP2:演示工作流同走编译图(纯宿主步,不依赖模型后端)。
res, err := (CompiledWorkflowRunner{}).runLaunch(rc, workflow.Launch{Def: mockWorkflow()}, wfcompile.RunOptions{})
if err != nil {
if res.Summary != "" {
addText(res.Summary)
}
addText("演示工作流没有跑完:" + err.Error())
break
}
parts = append(parts, res.Parts...)
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 / #workflow / #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 mockWorkflow() core.WorkflowDef {
return core.WorkflowDef{
ID: "mock.workflow", Title: "演示工作流",
Steps: []core.WorkflowStep{
{ID: "length", Title: "篇幅", Kind: "hitl", Hitl: &core.HITLCardTemplate{
Question: "要多长?", Kind: core.HITLKindChoice, AllowFreeInput: false,
Choices: []core.HITLChoice{
{ID: "short", Label: "短", Description: "一段", Impact: "只写一段"},
{ID: "long", Label: "长", Description: "几段", Impact: "会写得更长"},
},
}},
{ID: "save", Title: "成稿", Kind: "artifact", Artifact: "mock-brief.md", Prompt: "篇幅:{{steps.length.output}}\n{{input}}"},
{ID: "note", Title: "收件", Kind: "card", Card: &core.ActivityCardTemplate{Kind: core.CardKindResult, Title: "演示工作流完成"}},
},
}
}
func truncateForJSON(s string, n int) string {
r := []rune(s)
if len(r) <= n {
return s
}
return string(r[:n]) + "…"
}