Why: Subagents were a single lightweight call that dropped the hint and wrote their messages into the parent history. Profiles now decide tools, model, and turn cap, and the parent only keeps the summary plus confirmed artifacts. Evidence: go test ./... passed. New tests cover the six builtin specs, tool execution and whitelist, explore versus writer models, maxTurns, artifact parts, hint text, SpawnAgent pinning, depth-1 refusal, summary artifact lines, and history isolation. Impact: Squad steps share the general profile (up to 12 lightweight turns, no exec tools). Child runs do not attach the final-turn verifier. Profile fields stay in builtin files, so core.PromptModule is unchanged. The agent-native pin stays a34689c. Refs: feature/agent-harness-p3 Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
2.8 KiB
Go
107 lines
2.8 KiB
Go
package runmgr
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"agent-system-backend/internal/bus"
|
|
"agent-system-backend/internal/profiles"
|
|
"agent-system-backend/internal/runtime"
|
|
"agent-system-backend/internal/settings"
|
|
"agent-system-backend/internal/store"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
type captureRunner struct {
|
|
id string
|
|
parts []core.Part
|
|
}
|
|
|
|
func (c *captureRunner) Name() string { return "capture" }
|
|
|
|
func (c *captureRunner) Execute(rc runtime.RunCtx) error {
|
|
if src, ok := rc.(interface{ SubagentID() string }); ok {
|
|
c.id = src.SubagentID()
|
|
}
|
|
return rc.SetAssistantParts(c.parts)
|
|
}
|
|
|
|
func newSpawnEnv(t *testing.T, runner runtime.Runner) (*runCtx, *store.Store) {
|
|
t.Helper()
|
|
c, st := newExecEnv(t)
|
|
c.mgr.profiles = profiles.New(st)
|
|
c.mgr.settings = settings.NewService(st)
|
|
c.mgr.subRunner = runner
|
|
c.mgr.active = map[string]*runHandle{}
|
|
if c.mgr.events == nil {
|
|
c.mgr.events = newEventBuffer(st, bus.New())
|
|
}
|
|
c.run.Kind = core.RunConversation
|
|
c.run.ID = "run_parent"
|
|
c.h = &runHandle{hitlCh: make(chan hitlAnswer, 1)}
|
|
return c, st
|
|
}
|
|
|
|
func TestSubagentCannotSpawnAgain(t *testing.T) {
|
|
c, st := newSpawnEnv(t, &captureRunner{})
|
|
c.run.Kind = core.RunSubagent
|
|
_, err := c.SpawnAgent("agent.sub.explore", "再派一个", "继续", "")
|
|
if err == nil || !strings.Contains(err.Error(), "子代理不能再派发子代理") {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
ids, qerr := st.SubagentRunIDs(c.run.ConversationID)
|
|
if qerr != nil {
|
|
t.Fatal(qerr)
|
|
}
|
|
if len(ids) != 0 {
|
|
t.Fatalf("created runs = %v", ids)
|
|
}
|
|
}
|
|
|
|
func TestSubagentSummaryAndHistory(t *testing.T) {
|
|
runner := &captureRunner{parts: []core.Part{
|
|
{Type: core.PartText, Text: "正文很长,超过摘要窗口也要保留工件编号。"},
|
|
{Type: core.PartArtifactRef, Artifact: &core.ArtifactRef{ArtifactID: "art_0123456789ab", Name: "说明.md"}},
|
|
}}
|
|
c, st := newSpawnEnv(t, runner)
|
|
if err := st.AppendMessage(&core.Message{
|
|
ConversationID: c.run.ConversationID,
|
|
Role: core.RoleUser,
|
|
Parts: []core.Part{{Type: core.PartText, Text: "请写说明"}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res, err := c.SpawnSubagent("通用", "写说明", "简短")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if runner.id != "agent.sub.general" {
|
|
t.Fatalf("agent = %q", runner.id)
|
|
}
|
|
if !strings.Contains(res.Summary, "工件:art_0123456789ab 说明.md") {
|
|
t.Fatalf("summary = %q", res.Summary)
|
|
}
|
|
if !strings.Contains(res.Text, "正文很长") {
|
|
t.Fatalf("text = %q", res.Text)
|
|
}
|
|
hist := c.History()
|
|
for _, m := range hist {
|
|
if m.RunID != "" {
|
|
t.Fatalf("history kept sub-run message %+v", m)
|
|
}
|
|
if m.Role != core.RoleUser {
|
|
t.Fatalf("history = %+v", hist)
|
|
}
|
|
}
|
|
if len(hist) != 1 {
|
|
t.Fatalf("history len = %d", len(hist))
|
|
}
|
|
all, err := st.ListMessages(c.run.ConversationID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(all) != 2 {
|
|
t.Fatalf("list len = %d", len(all))
|
|
}
|
|
}
|