Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements. Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate. Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable. Refs: Versioned dependencies U1/U3; feature/dependency-workspace
210 lines
4.6 KiB
Go
210 lines
4.6 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"git-ssh.u1680832.nyat.app/weichen/Agent-Native.git/runtime/hooks"
|
|
"git-ssh.u1680832.nyat.app/weichen/Agent-Native.git/runtime/llm"
|
|
"git-ssh.u1680832.nyat.app/weichen/Agent-Native.git/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
|
|
}
|
|
a.cfg.Hooks.Emit(hooks.Call{Event: hooks.PreCompact})
|
|
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})
|
|
}
|