From 10186ab916e3eb0f306a831102766ab6d7bcba98 Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 1 Oct 2026 16:21:50 +0000 Subject: [PATCH] Add a mock workflow script and load the example plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: Mock mode needs a path that drives the real step runner, and the example plugin should fail the host test if its workflow or slot question is wrong. Evidence: go test ./internal/plugins/ -run TestRepoExamplePluginLoads passed. The brief intent asks 主题是什么? and keeps the topic slot. Impact: #workflow is only the mock runner. The example plugin itself is in the Dev repo. Refs: feature/agent-harness-p4 Co-authored-by: Cursor --- internal/plugins/host_test.go | 44 +++++++++++++++++++++++++++++++++++ internal/runtime/mock.go | 31 +++++++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/internal/plugins/host_test.go b/internal/plugins/host_test.go index beecccd..aed4787 100644 --- a/internal/plugins/host_test.go +++ b/internal/plugins/host_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "os" "path/filepath" + "strings" "testing" "agent-system-backend/internal/intentgate" @@ -208,3 +209,46 @@ func TestWorkflowScriptCannotEscape(t *testing.T) { t.Fatal("script path outside the plugin dir should be rejected") } } + +func TestRepoExamplePluginLoads(t *testing.T) { + root := filepath.Join("..", "..", "..", "plugins") + if _, err := os.Stat(filepath.Join(root, "example", "workflows", "demo-brief.json")); err != nil { + t.Skip("example plugin is not beside this module") + } + dir := t.TempDir() + st, err := store.OpenSQLite(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { st.Close() }) + regTools := tools.NewRegistry() + regTools.RegisterBuiltin() + host := New(st, regTools, settings.NewService(st), root) + gate := intentgate.New() + wf := workflow.NewRegistry() + host.OnIntents = func(overlay []json.RawMessage) error { + return gate.Rebuild(context.Background(), overlay) + } + host.OnWorkflows = func(defs []core.WorkflowDef, scripts map[string]string) error { + wf.Set(defs, scripts) + return nil + } + if err := host.LoadDir()["example"]; err != nil { + t.Fatal(err) + } + if _, ok := wf.ByIntent("demo.brief"); !ok { + t.Fatal("demo.brief is not bound") + } + body, ok := wf.Script("demo.script", "workflows/scripts/demo.py") + if !ok || !strings.Contains(body, `wf.tool("time_now"`) { + t.Fatalf("script %q", body) + } + got := gate.Recognize(context.Background(), "写一份简报") + if got.ID != "demo.brief" || len(got.Missing) != 1 || got.Missing[0].Question != "主题是什么?" { + t.Fatalf("%+v", got) + } + filled := gate.Recognize(context.Background(), "主题:核能 写一份简报") + if filled.Slots["topic"] != "核能" || len(filled.Missing) != 0 { + t.Fatalf("%+v", filled) + } +} diff --git a/internal/runtime/mock.go b/internal/runtime/mock.go index 40c2613..9e4f066 100644 --- a/internal/runtime/mock.go +++ b/internal/runtime/mock.go @@ -7,6 +7,7 @@ import ( "agent-system-backend/internal/docgen" "agent-system-backend/internal/squadadapter" + "agent-system-backend/internal/workflow" core "agent-system-core" ) @@ -212,6 +213,17 @@ func (m *MockRunner) Execute(rc RunCtx) error { addText("Agent-Squad 编排完成:" + summary + "。步骤与子代理见右栏「计划」「代理」。") } + case strings.Contains(user, "#workflow"): + res, err := (WorkflowRunner{}).run(rc, workflow.Launch{Def: mockWorkflow()}) + 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 指令)") @@ -229,7 +241,7 @@ func (m *MockRunner) Execute(rc RunCtx) error { 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) + 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)}) @@ -257,6 +269,23 @@ func (m *MockSubagent) Execute(rc RunCtx) error { 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 {