Files
Agent-Native/runtime/session_agent_test.go
T
weichenandCursor 35f65a3c49 Add optional stop, reminder, and continuation hooks to the turn loop
Why: the direct chat path needs one shared loop for deliverable checks, tool pairing, reminders, and truncated replies, while squad callers keep today's behavior when the new fields stay unset.
Evidence: go test ./... in agent-native passed, including Stop cap, BeforeTool pairing, reminders absent from the session, repeat-guard cap, length continuation, and zero-value loop tests.
Impact: runtime.Config and llm.Event grow optional fields; AgentSession.Prompt copies them through. Squad sessions that leave them zero are unchanged.
Refs: feature/agent-harness-p1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 13:23:07 +00:00

113 lines
3.4 KiB
Go

package runtime
import (
"context"
"sync"
"testing"
"agent-native/runtime/llm"
)
func TestAgentSessionPromptForwardsStop(t *testing.T) {
called := false
sess := NewAgentSession(Config{
Model: "m",
Stream: func(ctx context.Context, model string, messages []llm.Message, td []llm.ToolDecl, opts llm.StreamOptions) <-chan llm.Event {
ch := make(chan llm.Event, 1)
ch <- llm.Event{Kind: llm.KindMessageEnd, Message: llm.Message{Role: llm.RoleAssistant, Content: "hi", StopReason: llm.StopEnd}}
close(ch)
return ch
},
Stop: func(last llm.Message, info StopInfo) StopDecision {
called = true
return StopDecision{}
},
})
res, err := sess.Prompt(context.Background(), llm.Message{Role: llm.RoleUser, Content: "go"})
if err != nil {
t.Fatal(err)
}
if !called || res.Text != "hi" {
t.Fatalf("called=%v text=%q", called, res.Text)
}
}
func TestAgentSessionSteerFollowAndAbort(t *testing.T) {
var mu sync.Mutex
step := 0
stream := llm.StreamFunc(func(ctx context.Context, model string, messages []llm.Message, td []llm.ToolDecl, opts llm.StreamOptions) <-chan llm.Event {
ch := make(chan llm.Event, 2)
go func() {
defer close(ch)
mu.Lock()
n := step
step++
mu.Unlock()
if opts.ThinkingLevel != "low" && n == 0 {
t.Errorf("thinking level not forwarded: %s", opts.ThinkingLevel)
}
if n == 0 {
ch <- llm.Event{Kind: llm.KindMessageEnd, Message: llm.Message{
Role: llm.RoleAssistant, StopReason: llm.StopToolCalls,
ToolCalls: []llm.ToolCall{{ID: "1", Name: "missing", Arguments: "{}"}},
}}
return
}
ch <- llm.Event{Kind: llm.KindMessageEnd, Message: llm.Message{
Role: llm.RoleAssistant, Content: "done", StopReason: llm.StopEnd,
}}
}()
return ch
})
var types []string
sess := NewAgentSession(Config{
Model: "m",
ThinkingLevel: "low",
Stream: stream,
MaxTurns: 6,
OnEvent: func(ev Event) {
types = append(types, ev.Type)
},
})
sess.SetQueueMode(DeliverOne, DeliverAll)
sess.Steer(llm.Message{Role: llm.RoleUser, Content: "steer-me"})
sess.FollowUp(llm.Message{Role: llm.RoleUser, Content: "later"})
res, err := sess.Prompt(context.Background(), llm.Message{Role: llm.RoleUser, Content: "go"})
if err != nil {
t.Fatal(err)
}
if res.StopReason != llm.StopEnd {
t.Fatalf("stop %s err %v", res.StopReason, res.Err)
}
found := map[string]bool{}
for _, typ := range types {
found[typ] = true
}
for _, want := range []string{"message_start", "queue_update", "tool_execution_update", "agent_settled"} {
if !found[want] {
t.Fatalf("missing %s in %v", want, types)
}
}
}
func TestClearQueueAndSetModel(t *testing.T) {
sess := NewAgentSession(Config{Model: "old", Stream: func(ctx context.Context, model string, messages []llm.Message, td []llm.ToolDecl, opts llm.StreamOptions) <-chan llm.Event {
ch := make(chan llm.Event, 1)
ch <- llm.Event{Kind: llm.KindMessageEnd, Message: llm.Message{Role: llm.RoleAssistant, Content: model, StopReason: llm.StopEnd}}
close(ch)
return ch
}})
sess.Steer(llm.Message{Role: llm.RoleUser, Content: "drop"})
sess.ClearQueue()
if err := sess.SetModel("new-model", "high"); err != nil {
t.Fatal(err)
}
res, err := sess.Prompt(context.Background(), llm.Message{Role: llm.RoleUser, Content: "hi"})
if err != nil {
t.Fatal(err)
}
if res.Text != "new-model" {
t.Fatalf("model switch did not apply: %q", res.Text)
}
}