Files
Agent-Native/runtime/session_agent.go
T
weichenandCursor 6fd1d441a9
ci / vet-and-test (pull_request) Successful in 45s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Keep tool pairs and fold reminders into the last result
Why: Old tool results were filling the window, compaction could split a tool group or commit a stale summary, and reminders arrived as extra user messages.
Evidence: Compaction retries a conflict and opens the circuit after three failures; clearing keeps the last five tool results; a stale file write is rejected; RPC reports started, queued, or handled.
Impact: Requests over the context window replace older tool results with a placeholder. Rewriting an existing file now requires a read in the same run. Subagent text over 32000 characters is capped.
Refs: feature/context-engineering
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 16:19:36 +00:00

208 lines
4.4 KiB
Go

package runtime
import (
"context"
"sync"
"agent-native/runtime/llm"
"agent-native/runtime/session"
)
// Delivery selects how a queued steer or follow-up is handed to the loop.
type Delivery string
const (
DeliverOne Delivery = "one"
DeliverAll Delivery = "all"
)
// AgentSession is the headless session object: queues, abort, and
// mid-run model changes around Run.
type AgentSession struct {
cfg Config
mu sync.Mutex
steer []llm.Message
follow []llm.Message
steerMode Delivery
followMode Delivery
model string
thinking string
cancel context.CancelFunc
runID int
}
// NewAgentSession copies cfg. Queue modes default to one-at-a-time.
func NewAgentSession(cfg Config) *AgentSession {
if cfg.Model == "" {
cfg.Model = "default"
}
return &AgentSession{
cfg: cfg,
steerMode: DeliverOne,
followMode: DeliverOne,
model: cfg.Model,
thinking: cfg.ThinkingLevel,
}
}
// SetQueueMode sets the delivery mode for steer and follow-up.
func (a *AgentSession) SetQueueMode(steer, follow Delivery) {
a.mu.Lock()
defer a.mu.Unlock()
if steer != "" {
a.steerMode = steer
}
if follow != "" {
a.followMode = follow
}
}
// Steer queues a message to be delivered after the current tool batch.
func (a *AgentSession) Steer(msg llm.Message) {
a.mu.Lock()
a.steer = append(a.steer, msg)
a.mu.Unlock()
a.emitQueue("steer")
}
// FollowUp queues a message for after the assistant stops.
func (a *AgentSession) FollowUp(msg llm.Message) {
a.mu.Lock()
a.follow = append(a.follow, msg)
a.mu.Unlock()
a.emitQueue("follow_up")
}
// ClearQueue drops both queues.
func (a *AgentSession) ClearQueue() {
a.mu.Lock()
a.steer = nil
a.follow = nil
a.mu.Unlock()
a.emitQueue("clear")
}
// Busy 报告是否有尚未结束的 Prompt。
func (a *AgentSession) Busy() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.cancel != nil
}
// Abort cancels the in-flight Prompt.
func (a *AgentSession) Abort() {
a.mu.Lock()
cancel := a.cancel
a.mu.Unlock()
if cancel != nil {
cancel()
}
}
// SetModel changes the model and thinking level used on the next turn.
// A session store, when present, records the change.
func (a *AgentSession) SetModel(model, thinking string) error {
if thinking != "" {
norm, err := llm.NormalizeThinking(thinking)
if err != nil {
return err
}
thinking = norm
}
a.mu.Lock()
if model != "" {
a.model = model
}
if thinking != "" {
a.thinking = thinking
}
store := a.cfg.Session
a.mu.Unlock()
if store == nil {
return nil
}
if model != "" {
if _, err := store.AppendEntry(session.Entry{Type: session.TypeModelChange, Model: model}); err != nil {
return err
}
}
if thinking != "" {
if _, err := store.AppendEntry(session.Entry{Type: session.TypeThinkingLevelChange, ThinkingLevel: thinking}); err != nil {
return err
}
}
return nil
}
// Prompt runs one user message until the agent settles.
func (a *AgentSession) Prompt(ctx context.Context, user llm.Message) (Result, error) {
ctx, cancel := context.WithCancel(ctx)
a.mu.Lock()
a.runID++
id := a.runID
a.cancel = cancel
cfg := a.cfg
a.mu.Unlock()
defer func() {
cancel()
a.mu.Lock()
if a.runID == id {
a.cancel = nil
}
a.mu.Unlock()
}()
cfg.GetSteering = func() []llm.Message { return a.take("steer") }
cfg.GetFollowUp = func() []llm.Message { return a.take("follow") }
cfg.Resolve = func() (string, string) {
a.mu.Lock()
defer a.mu.Unlock()
return a.model, a.thinking
}
return Run(ctx, cfg, user)
}
// Compact appends a compaction entry and emits compaction events.
func (a *AgentSession) Compact(keep int, summarize func([]llm.Message) (string, error)) (bool, error) {
if a.cfg.Session == nil {
return false, nil
}
emit(a.cfg, Event{Type: "compaction_start"})
done, err := a.cfg.Session.Compact(keep, summarize)
text := "noop"
if done {
text = "compacted"
}
if err != nil {
text = err.Error()
}
emit(a.cfg, Event{Type: "compaction_end", Text: text})
return done, err
}
func (a *AgentSession) take(kind string) []llm.Message {
a.mu.Lock()
defer a.mu.Unlock()
q := &a.steer
mode := a.steerMode
if kind == "follow" {
q = &a.follow
mode = a.followMode
}
if len(*q) == 0 {
return nil
}
if mode == DeliverAll {
out := append([]llm.Message(nil), (*q)...)
*q = nil
return out
}
out := []llm.Message{(*q)[0]}
*q = (*q)[1:]
return out
}
func (a *AgentSession) emitQueue(text string) {
emit(a.cfg, Event{Type: "queue_update", Text: text})
}