Files
Agent-System-Backend/internal/runtime/sub_loop_test.go
T
weichenandCursor cd74434b4f Stream tool progress and drop hard agent limits
Why: Tool calls stayed invisible until execution finished, and turn, tool, timeout, and output caps stopped long tasks early.
Evidence: go test ./internal/runtime/ ./internal/tools/ ./internal/prompts/ ./internal/squadadapter/ ./internal/runmgr/ ./internal/app/ ./internal/settings/
Impact: tool.call.delta and tool.output reach the client; default profile, subagents, and squad no longer cap turns or tool calls; exec timeout defaults to 600s with no hard ceiling. A profile or subagent maxTurns above 0 still applies.
Refs: feature/stream-display-overhaul
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-02 02:17:02 +00:00

252 lines
6.9 KiB
Go

package runtime
import (
"context"
"strings"
"testing"
agentllm "agent-native/runtime/llm"
"agent-system-backend/internal/prompts"
core "agent-system-core"
)
type subRC struct {
fakeRunCtx
id string
hint string
task string
tools []core.ToolDesc
}
func (s *subRC) SubagentID() string { return s.id }
func (s *subRC) Subhint() string { return s.hint }
func (s *subRC) Subtask() string { return s.task }
func (s *subRC) Tools() []core.ToolDesc { return s.tools }
func subProfile() core.AgentProfile {
return core.AgentProfile{Model: core.ModelRoute{Chat: "test-model", Lightweight: "lite-model"}}
}
func subTools() []core.ToolDesc {
names := []string{
"knowledge_search", "web_search", "delegate_task", "squad_orchestrate",
"run_python", "register_artifact", "write_artifact", "quality_review",
"fs_read_file", "fs_list_dir",
}
out := make([]core.ToolDesc, 0, len(names))
for _, name := range names {
out = append(out, core.ToolDesc{Name: name, Description: name, Parameters: []byte(`{"type":"object"}`)})
}
return out
}
func mustPrompts(t *testing.T) *prompts.Service {
t.Helper()
lib, err := prompts.DefaultLibrary()
if err != nil {
t.Fatal(err)
}
svc, err := prompts.NewService(nil, lib)
if err != nil {
t.Fatal(err)
}
return svc
}
func TestSubagentLoopRunsToolThenFinal(t *testing.T) {
turns := [][]agentllm.Event{
textDeltaTurn("先查一下。", toolCall("knowledge_search", `{"q":"x"}`)),
textDeltaTurn("查完了。"),
}
var decls []agentllm.ToolDecl
var user string
base := scriptStream(turns, nil)
rc := &subRC{
fakeRunCtx: fakeRunCtx{ctx: context.Background(), prof: subProfile()},
id: "agent.sub.explore",
hint: "只要路径",
task: "找出配置文件",
tools: subTools(),
}
r := &SubLLMRunner{
Prompts: mustPrompts(t),
Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
if model != "lite-model" {
t.Errorf("explore model = %q", model)
}
if decls == nil {
decls = tools
}
for _, m := range msgs {
if m.Role == agentllm.RoleUser {
user = m.Content
}
}
return base(ctx, model, msgs, tools, opts)
},
}
if err := r.Execute(rc); err != nil {
t.Fatal(err)
}
if rc.toolRuns != 1 {
t.Fatalf("tool runs = %d", rc.toolRuns)
}
if !strings.Contains(user, "找出配置文件") || !strings.Contains(user, "执行提示:只要路径") {
t.Fatalf("user message = %q", user)
}
names := map[string]bool{}
for _, d := range decls {
names[d.Name] = true
}
if !names["knowledge_search"] || names["delegate_task"] || names["squad_orchestrate"] || names["run_python"] {
t.Fatalf("decls = %v", names)
}
var sawFinal, sawProcess bool
for _, p := range rc.assistant {
if p.Type == core.PartText && p.Text == "查完了。" && !p.Process {
sawFinal = true
}
if p.Type == core.PartToolCall {
sawProcess = true
}
}
if !sawFinal || !sawProcess {
t.Fatalf("parts = %+v", rc.assistant)
}
}
func TestSubagentWriterUsesChatModel(t *testing.T) {
var model string
base := scriptStream([][]agentllm.Event{textDeltaTurn("写好了。")}, nil)
rc := &subRC{
fakeRunCtx: fakeRunCtx{ctx: context.Background(), prof: subProfile()},
id: "agent.sub.writer",
task: "写一段",
tools: subTools(),
}
r := &SubLLMRunner{
Prompts: mustPrompts(t),
Stream: func(ctx context.Context, m string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
model = m
return base(ctx, m, msgs, tools, opts)
},
}
if err := r.Execute(rc); err != nil {
t.Fatal(err)
}
if model != "test-model" {
t.Fatalf("writer model = %q", model)
}
}
func TestSubagentMaxTurnsUnbounded(t *testing.T) {
turns := make([][]agentllm.Event, 8)
for i := 0; i < 7; i++ {
turns[i] = textDeltaTurn("", toolCall("knowledge_search", `{}`))
}
turns[7] = textDeltaTurn("计划写完了。")
calls := 0
base := scriptStream(turns, nil)
rc := &subRC{
fakeRunCtx: fakeRunCtx{ctx: context.Background(), prof: subProfile()},
id: "agent.sub.plan",
task: "列步骤",
tools: subTools(),
}
r := &SubLLMRunner{
Prompts: mustPrompts(t),
Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
calls++
return base(ctx, model, msgs, tools, opts)
},
}
if err := r.Execute(rc); err != nil {
t.Fatal(err)
}
if calls != 8 {
t.Fatalf("stream calls = %d, want past the old cap of 6", calls)
}
}
func TestSubagentArtifactPart(t *testing.T) {
rc := &subRC{
fakeRunCtx: fakeRunCtx{
ctx: context.Background(),
prof: subProfile(),
execArtifact: &core.Artifact{ID: "art_0123456789ab", Name: "note.md", Kind: "document"},
},
id: "agent.sub.general",
task: "记一笔",
tools: subTools(),
}
r := &SubLLMRunner{
Prompts: mustPrompts(t),
Stream: scriptStream([][]agentllm.Event{
textDeltaTurn("", toolCall("write_artifact", `{}`)),
textDeltaTurn("记下了。"),
}, nil),
}
if err := r.Execute(rc); err != nil {
t.Fatal(err)
}
var saw bool
for _, p := range rc.assistant {
if p.Type == core.PartArtifactRef && p.Artifact != nil && p.Artifact.ArtifactID == "art_0123456789ab" && p.Artifact.Name == "note.md" {
saw = true
}
}
if !saw {
t.Fatalf("parts = %+v", rc.assistant)
}
}
type spawnRC struct {
fakeRunCtx
agent string
called int
}
func (s *spawnRC) SpawnAgent(agentID, name, task, hint string) (core.AgentResultPayload, error) {
s.called++
s.agent = agentID
return core.AgentResultPayload{Summary: "写好了\n工件:art_0123456789ab 说明.md;工件:art_ffffffffffff 没有"}, nil
}
func TestDelegatePinsExistingArtifact(t *testing.T) {
for _, tc := range []struct {
name string
args string
agent string
}{
{name: "writer", args: `{"agent":"writer","name":"写手","task":"写说明"}`, agent: "agent.sub.writer"},
{name: "default", args: `{"name":"助手","task":"看一下"}`, agent: "agent.sub.general"},
} {
t.Run(tc.name, func(t *testing.T) {
rc := &spawnRC{fakeRunCtx: fakeRunCtx{
ctx: context.Background(),
prof: subProfile(),
execArtifact: &core.Artifact{ID: "art_0123456789ab", Name: "说明.md", Kind: "document"},
}}
r := &LLMRunner{Stream: scriptStream([][]agentllm.Event{
textDeltaTurn("", agentllm.ToolCall{ID: "c-del", Name: "delegate_task", Arguments: tc.args}),
textDeltaTurn("完成。"),
}, nil)}
if err := r.Execute(rc); err != nil {
t.Fatal(err)
}
if rc.called != 1 || rc.agent != tc.agent {
t.Fatalf("spawn agent=%q called=%d", rc.agent, rc.called)
}
var pinned []string
for _, p := range rc.assistant {
if p.Type == core.PartArtifactRef && p.Artifact != nil {
pinned = append(pinned, p.Artifact.ArtifactID)
}
}
if len(pinned) != 1 || pinned[0] != "art_0123456789ab" {
t.Fatalf("pinned = %v", pinned)
}
})
}
}