refactor agent core: extract runtime, simplify commands
This commit is contained in:
parent
82ac183357
commit
9d4818af8d
|
|
@ -94,7 +94,7 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
|
|||
|
||||
"/backend": func(args []string) {
|
||||
if len(args) == 0 {
|
||||
s.emit(infoEvent(s.cfg.Backend.Name()))
|
||||
s.emit(infoEvent(s.runtime.Backend.Name()))
|
||||
return
|
||||
}
|
||||
if s.IsRunning() {
|
||||
|
|
@ -106,18 +106,18 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
|
|||
s.emit(infoEvent(fmt.Sprintf("error: failed to switch backend: %v", err)))
|
||||
return
|
||||
}
|
||||
s.cfg.Backend = be
|
||||
s.runtime.Backend = be
|
||||
s.emit(infoEvent(fmt.Sprintf("switched backend to: %s (model: %s)", be.Name(), be.Model())))
|
||||
},
|
||||
|
||||
"/models": func(args []string) {
|
||||
models := s.cfg.Backend.Models(ctx)
|
||||
models := s.runtime.Backend.Models(ctx)
|
||||
if len(models) == 0 {
|
||||
s.emit(infoEvent("no models available"))
|
||||
return
|
||||
}
|
||||
slices.Sort(models)
|
||||
current := s.cfg.Backend.Model()
|
||||
current := s.runtime.Backend.Model()
|
||||
for _, m := range models {
|
||||
marker := " "
|
||||
if m == current {
|
||||
|
|
@ -129,19 +129,19 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
|
|||
|
||||
"/model": func(args []string) {
|
||||
if len(args) == 0 {
|
||||
s.emit(infoEvent(s.cfg.Backend.Model()))
|
||||
s.emit(infoEvent(s.runtime.Backend.Model()))
|
||||
return
|
||||
}
|
||||
s.cfg.Backend.SetModel(args[0])
|
||||
s.runtime.Backend.SetModel(args[0])
|
||||
s.emit(infoEvent("switched model to: " + args[0]))
|
||||
},
|
||||
|
||||
"/maxsteps": func(args []string) {
|
||||
if len(args) == 0 {
|
||||
if s.cfg.MaxSteps == 0 {
|
||||
if s.runtime.MaxSteps == 0 {
|
||||
s.emit(infoEvent("maxsteps: unlimited"))
|
||||
} else {
|
||||
s.emit(infoEvent(fmt.Sprintf("maxsteps: %d", s.cfg.MaxSteps)))
|
||||
s.emit(infoEvent(fmt.Sprintf("maxsteps: %d", s.runtime.MaxSteps)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -150,7 +150,7 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
|
|||
s.emit(infoEvent("error: maxsteps requires a non-negative integer (0 = unlimited)"))
|
||||
return
|
||||
}
|
||||
s.cfg.MaxSteps = n
|
||||
s.runtime.MaxSteps = n
|
||||
if n == 0 {
|
||||
s.emit(infoEvent("maxsteps: unlimited"))
|
||||
} else {
|
||||
|
|
@ -211,31 +211,28 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
|
|||
return
|
||||
}
|
||||
d := s.newDispatcher()
|
||||
env := BuildAgentEnv(cfg, d, s.cwd, []string{"OLLIE_SESSION_ID=" + s.sessionID, "OLLIE=" + execute.MountPath(s.uname)})
|
||||
if env.Backend != "" {
|
||||
newBe, err := s.newBackend(env.Backend)
|
||||
rt := BuildRuntime(cfg, d, s.cwd, []string{"OLLIE_SESSION_ID=" + s.sessionID, "OLLIE=" + execute.MountPath(s.uname)})
|
||||
if rt.CfgBackend != "" {
|
||||
newBe, err := s.newBackend(rt.CfgBackend)
|
||||
if err != nil {
|
||||
s.emit(infoEvent(fmt.Sprintf("error: backend %q: %v", env.Backend, err)))
|
||||
s.emit(infoEvent(fmt.Sprintf("error: backend %q: %v", rt.CfgBackend, err)))
|
||||
return
|
||||
}
|
||||
if env.Model != "" {
|
||||
newBe.SetModel(env.Model)
|
||||
if rt.CfgModel != "" {
|
||||
newBe.SetModel(rt.CfgModel)
|
||||
}
|
||||
rt.Backend = newBe
|
||||
} else {
|
||||
rt.Backend = s.runtime.Backend
|
||||
if rt.CfgModel != "" {
|
||||
rt.Backend.SetModel(rt.CfgModel)
|
||||
}
|
||||
s.cfg.Backend = newBe
|
||||
} else if env.Model != "" {
|
||||
s.cfg.Backend.SetModel(env.Model)
|
||||
}
|
||||
s.dispatcher = env.dispatcher
|
||||
s.hooks = env.Hooks
|
||||
s.cfg.preamble = env.preamble
|
||||
s.cfg.Tools = env.tools
|
||||
s.cfg.Exec = env.exec
|
||||
s.cfg.GenerationParams = env.genParams
|
||||
s.cfg.MaxSteps = env.maxSteps
|
||||
s.runtime = rt
|
||||
s.agentName = name
|
||||
s.session = nil
|
||||
s.pushSessionEnv()
|
||||
for _, msg := range env.Messages {
|
||||
for _, msg := range rt.Messages {
|
||||
s.emit(infoEvent(msg))
|
||||
}
|
||||
s.emit(infoEvent("agent: " + name))
|
||||
|
|
@ -281,7 +278,7 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
|
|||
s.emit(infoEvent("no active session"))
|
||||
return
|
||||
}
|
||||
ctxLen := s.cfg.Backend.ContextLength(ctx)
|
||||
ctxLen := s.runtime.Backend.ContextLength(ctx)
|
||||
if ctxLen <= 0 {
|
||||
ctxLen = defaultContextLength
|
||||
}
|
||||
|
|
@ -305,7 +302,7 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
|
|||
s.emit(infoEvent("no active session"))
|
||||
return
|
||||
}
|
||||
ctxLen := s.cfg.Backend.ContextLength(ctx)
|
||||
ctxLen := s.runtime.Backend.ContextLength(ctx)
|
||||
if ctxLen <= 0 {
|
||||
ctxLen = defaultContextLength
|
||||
}
|
||||
|
|
@ -403,7 +400,7 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
|
|||
"/tools": func(args []string) { listMountDir("t") },
|
||||
|
||||
"/sp": func(args []string) {
|
||||
s.emit(infoEvent(s.cfg.preamble))
|
||||
s.emit(infoEvent(s.runtime.Preamble))
|
||||
},
|
||||
|
||||
"/help": func(args []string) {
|
||||
|
|
|
|||
|
|
@ -29,29 +29,15 @@ import (
|
|||
// with other read-class tools. nil means treat all tools as serial.
|
||||
type toolClassifier func(name string) bool
|
||||
|
||||
// AgentEnv holds the runtime state derived from an agent config file.
|
||||
type AgentEnv struct {
|
||||
dispatcher tools.Dispatcher
|
||||
tools []backend.Tool
|
||||
exec toolExecutor
|
||||
classifyTool toolClassifier
|
||||
classifyTier func(name string) ResultTier
|
||||
Hooks Hooks
|
||||
preamble string
|
||||
genParams backend.GenerationParams
|
||||
maxSteps int
|
||||
Backend string // from config; empty means no override
|
||||
Model string // from config; empty means no override
|
||||
Messages []string
|
||||
}
|
||||
|
||||
// BuildAgentEnv constructs an AgentEnv from a pre-configured Dispatcher and
|
||||
|
||||
// BuildRuntime constructs a Runtime from a pre-configured Dispatcher and
|
||||
// optional agent config. cwd sets the working directory reported in the
|
||||
// system prompt; if empty, the process working directory is used.
|
||||
// env provides additional environment variables injected into prompt resolution
|
||||
// subprocesses (e.g. OLLIE_SESSION_ID=xxx).
|
||||
// The caller is responsible for registering all servers on d before calling this.
|
||||
func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher, cwd string, env []string) AgentEnv {
|
||||
func BuildRuntime(cfg *config.Config, d tools.Dispatcher, cwd string, env []string) *Runtime {
|
||||
var messages []string
|
||||
|
||||
var allToolInfos []tools.ToolInfo
|
||||
|
|
@ -157,18 +143,18 @@ func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher, cwd string, env []str
|
|||
modelName = cfg.Model
|
||||
}
|
||||
|
||||
return AgentEnv{
|
||||
dispatcher: d,
|
||||
tools: allTools,
|
||||
exec: exec,
|
||||
classifyTool: classify,
|
||||
classifyTier: tierFn,
|
||||
return &Runtime{
|
||||
Dispatcher: d,
|
||||
Tools: allTools,
|
||||
Exec: exec,
|
||||
ClassifyTool: classify,
|
||||
ClassifyTier: tierFn,
|
||||
Hooks: hooks,
|
||||
preamble: preamble,
|
||||
genParams: genParams,
|
||||
maxSteps: maxSteps,
|
||||
Backend: backendName,
|
||||
Model: modelName,
|
||||
Preamble: preamble,
|
||||
GenParams: genParams,
|
||||
MaxSteps: maxSteps,
|
||||
CfgBackend: backendName,
|
||||
CfgModel: modelName,
|
||||
Messages: messages,
|
||||
}
|
||||
}
|
||||
|
|
@ -243,7 +229,7 @@ type AgentCoreConfig struct {
|
|||
Uname string // immutable user principal for 9P identity
|
||||
CWD string // working directory for tool execution and system prompt
|
||||
Session *Session
|
||||
Env AgentEnv
|
||||
Runtime *Runtime
|
||||
NewDispatcher func() tools.Dispatcher
|
||||
NewBackend func(string) (backend.Backend, error) // if nil, defaults to backend.NewWithName
|
||||
Log *olog.Logger // if nil, logging is disabled
|
||||
|
|
@ -256,17 +242,18 @@ type AgentCoreConfig struct {
|
|||
// but has no knowledge of how output is rendered.
|
||||
type agent struct {
|
||||
session *Session
|
||||
cfg agentConfig
|
||||
hooks Hooks
|
||||
runtime *Runtime
|
||||
cfg agentConfig // per-turn config built from runtime; set in executeTurn
|
||||
log *olog.Logger
|
||||
agentName string
|
||||
agentsDir string
|
||||
sessionsDir string
|
||||
sessionID string
|
||||
uname string
|
||||
dispatcher tools.Dispatcher
|
||||
newDispatcher func() tools.Dispatcher
|
||||
newBackend func(string) (backend.Backend, error)
|
||||
readPlanStep func() string
|
||||
turnError func(ctx context.Context, errType, errMsg string) HookResult // overridable for tests
|
||||
cwd string
|
||||
startupMessages []string
|
||||
currentAction atomic.Pointer[actionHandle]
|
||||
|
|
@ -301,10 +288,10 @@ func (s *agent) SetEnv(key, value string) {
|
|||
}
|
||||
s.env[key] = value
|
||||
s.envMu.Unlock()
|
||||
if s.dispatcher == nil {
|
||||
if s.runtime == nil || s.runtime.Dispatcher == nil {
|
||||
return
|
||||
}
|
||||
if srv, ok := s.dispatcher.GetServer("execute"); ok {
|
||||
if srv, ok := s.runtime.Dispatcher.GetServer("execute"); ok {
|
||||
if es, ok := srv.(tools.EnvSetter); ok {
|
||||
es.SetEnv(key, value)
|
||||
}
|
||||
|
|
@ -313,10 +300,10 @@ func (s *agent) SetEnv(key, value string) {
|
|||
|
||||
// pushSessionEnv injects OLLIE_SESSION_ID into the execute server subprocess env.
|
||||
func (s *agent) pushSessionEnv() {
|
||||
if s.dispatcher == nil || s.sessionID == "" {
|
||||
if s.runtime == nil || s.runtime.Dispatcher == nil || s.sessionID == "" {
|
||||
return
|
||||
}
|
||||
if srv, ok := s.dispatcher.GetServer("execute"); ok {
|
||||
if srv, ok := s.runtime.Dispatcher.GetServer("execute"); ok {
|
||||
if es, ok := srv.(tools.EnvSetter); ok {
|
||||
es.SetEnv("OLLIE_SESSION_ID", s.sessionID)
|
||||
if s.uname != "" {
|
||||
|
|
@ -328,10 +315,10 @@ func (s *agent) pushSessionEnv() {
|
|||
|
||||
// pushLockDir sets the flock directory on the execute server to the session tmpdir.
|
||||
func (s *agent) pushLockDir() {
|
||||
if s.dispatcher == nil || s.sessionID == "" {
|
||||
if s.runtime == nil || s.runtime.Dispatcher == nil || s.sessionID == "" {
|
||||
return
|
||||
}
|
||||
if srv, ok := s.dispatcher.GetServer("execute"); ok {
|
||||
if srv, ok := s.runtime.Dispatcher.GetServer("execute"); ok {
|
||||
if ls, ok := srv.(tools.LockDirSetter); ok {
|
||||
ls.SetLockDir(filepath.Join(ollieTmpDir(), s.sessionID))
|
||||
}
|
||||
|
|
@ -385,28 +372,23 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
|
|||
if cfg.NewBackend == nil {
|
||||
cfg.NewBackend = backend.NewWithName
|
||||
}
|
||||
run := agentConfig{
|
||||
Backend: cfg.Backend,
|
||||
preamble: cfg.Env.preamble,
|
||||
Tools: cfg.Env.tools,
|
||||
Exec: cfg.Env.exec,
|
||||
ClassifyTool: cfg.Env.classifyTool,
|
||||
ClassifyTier: cfg.Env.classifyTier,
|
||||
ToolResultMaxBytes: defaultToolResultMaxBytes,
|
||||
GenerationParams: cfg.Env.genParams,
|
||||
MaxSteps: cfg.Env.maxSteps,
|
||||
rt := cfg.Runtime
|
||||
if rt == nil {
|
||||
rt = &Runtime{}
|
||||
}
|
||||
// Store the backend on the runtime so it's the single source of truth.
|
||||
rt.Backend = cfg.Backend
|
||||
// A non-zero MaxSteps in AgentCoreConfig takes precedence over the
|
||||
// value loaded from the agent JSON, allowing callers to impose a
|
||||
// per-session limit at construction time (e.g. via a CLI flag).
|
||||
// value loaded from the agent JSON.
|
||||
if cfg.MaxSteps > 0 {
|
||||
run.MaxSteps = cfg.MaxSteps
|
||||
rt.MaxSteps = cfg.MaxSteps
|
||||
}
|
||||
var readPlanStep func() string
|
||||
if cfg.SessionID != "" {
|
||||
os.MkdirAll(filepath.Join(ollieTmpDir(), cfg.SessionID), 0700) //nolint:errcheck
|
||||
if mount := os.Getenv("OLLIE"); mount != "" {
|
||||
planPath := filepath.Join(mount, "s", cfg.SessionID, "plan")
|
||||
run.ReadPlanStep = func() string {
|
||||
readPlanStep = func() string {
|
||||
return readNextUnchecked(planPath)
|
||||
}
|
||||
}
|
||||
|
|
@ -419,8 +401,7 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
|
|||
|
||||
a := &agent{
|
||||
session: cfg.Session,
|
||||
cfg: run,
|
||||
hooks: cfg.Env.Hooks,
|
||||
runtime: rt,
|
||||
log: log,
|
||||
auditLog: log.Sub("audit"),
|
||||
agentName: cfg.AgentName,
|
||||
|
|
@ -429,37 +410,35 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
|
|||
sessionID: cfg.SessionID,
|
||||
uname: cfg.Uname,
|
||||
cwd: paths.ExpandHome(cfg.CWD),
|
||||
startupMessages: cfg.Env.Messages,
|
||||
dispatcher: cfg.Env.dispatcher,
|
||||
startupMessages: rt.Messages,
|
||||
newDispatcher: cfg.NewDispatcher,
|
||||
newBackend: cfg.NewBackend,
|
||||
readPlanStep: readPlanStep,
|
||||
state: "idle",
|
||||
bus: pubsub.NewBus(),
|
||||
}
|
||||
a.changeCond = sync.NewCond(&a.changeMu)
|
||||
a.pushSessionEnv()
|
||||
a.pushLockDir()
|
||||
a.cfg.TurnError = func(_ context.Context, errType, errMsg string) HookResult {
|
||||
// Use a detached context so the hook can safely write "stop" to ctl
|
||||
// without cancelling its own execution via the action context.
|
||||
a.turnError = func(_ context.Context, errType, errMsg string) HookResult {
|
||||
hookCtx, cancel := context.WithTimeout(context.Background(), time.Duration(hookTimeout)*time.Second)
|
||||
defer cancel()
|
||||
return a.hooks.Run(hookCtx, HookTurnError, map[string]string{
|
||||
return a.runtime.Hooks.Run(hookCtx, HookTurnError, map[string]string{
|
||||
"session_id": a.sessionID,
|
||||
"cwd": a.CWD(),
|
||||
"model": a.cfg.Backend.Model(),
|
||||
"model": a.runtime.Backend.Model(),
|
||||
"error_type": errType,
|
||||
"error": errMsg,
|
||||
}, a.log)
|
||||
}
|
||||
a.pushSessionEnv()
|
||||
a.pushLockDir()
|
||||
return a
|
||||
}
|
||||
|
||||
// Close releases resources for this session, including its tmpdir.
|
||||
func (s *agent) Close() {
|
||||
s.log.Debug("Close() session=%q", s.sessionID)
|
||||
if s.dispatcher != nil {
|
||||
if srv, ok := s.dispatcher.GetServer("execute"); ok {
|
||||
if s.runtime != nil && s.runtime.Dispatcher != nil {
|
||||
if srv, ok := s.runtime.Dispatcher.GetServer("execute"); ok {
|
||||
if c, ok := srv.(interface{ Close() }); ok {
|
||||
s.log.Debug("Close() calling execute.Close()")
|
||||
c.Close()
|
||||
|
|
@ -477,12 +456,12 @@ func (s *agent) AgentName() string {
|
|||
return v
|
||||
}
|
||||
func (s *agent) BackendName() string {
|
||||
v := s.cfg.Backend.Name()
|
||||
v := s.runtime.Backend.Name()
|
||||
s.log.Debug("BackendName() = %q", v)
|
||||
return v
|
||||
}
|
||||
func (s *agent) ModelName() string {
|
||||
v := s.cfg.Backend.Model()
|
||||
v := s.runtime.Backend.Model()
|
||||
s.log.Debug("ModelName() = %q", v)
|
||||
return v
|
||||
}
|
||||
|
|
@ -574,8 +553,8 @@ func (s *agent) SetCWD(dir string) error {
|
|||
}
|
||||
s.cwd = dir
|
||||
// Propagate to any tool server that knows how to handle it (e.g. execute).
|
||||
if s.dispatcher != nil {
|
||||
if srv, ok := s.dispatcher.GetServer("execute"); ok {
|
||||
if s.runtime != nil && s.runtime.Dispatcher != nil {
|
||||
if srv, ok := s.runtime.Dispatcher.GetServer("execute"); ok {
|
||||
if ws, ok := srv.(tools.CWDSetter); ok {
|
||||
ws.SetCWD(dir)
|
||||
}
|
||||
|
|
@ -606,7 +585,7 @@ func (s *agent) SetSessionID(newID string) error {
|
|||
}
|
||||
s.sessionID = newID
|
||||
// Update session ID references in the system prompt.
|
||||
s.cfg.preamble = strings.ReplaceAll(s.cfg.preamble, oldID, newID)
|
||||
s.runtime.Preamble = strings.ReplaceAll(s.runtime.Preamble, oldID, newID)
|
||||
// Rename tmpdir so isread markers remain valid after rename.
|
||||
oldTemp := filepath.Join(ollieTmpDir(), oldID)
|
||||
newTemp := filepath.Join(ollieTmpDir(), newID)
|
||||
|
|
@ -625,7 +604,7 @@ const defaultToolResultMaxBytes = 131072
|
|||
|
||||
// autoCompactLimit returns the token threshold for auto-compaction (75%).
|
||||
func (s *agent) autoCompactLimit(ctx context.Context) int {
|
||||
ctxLen := s.cfg.Backend.ContextLength(ctx)
|
||||
ctxLen := s.runtime.Backend.ContextLength(ctx)
|
||||
if ctxLen <= 0 {
|
||||
ctxLen = defaultContextLength
|
||||
}
|
||||
|
|
@ -634,7 +613,7 @@ func (s *agent) autoCompactLimit(ctx context.Context) int {
|
|||
|
||||
// autoWarnLimit returns the token threshold for a context-usage warning (60%).
|
||||
func (s *agent) autoWarnLimit(ctx context.Context) int {
|
||||
ctxLen := s.cfg.Backend.ContextLength(ctx)
|
||||
ctxLen := s.runtime.Backend.ContextLength(ctx)
|
||||
if ctxLen <= 0 {
|
||||
ctxLen = defaultContextLength
|
||||
}
|
||||
|
|
@ -645,11 +624,11 @@ func (s *agent) autoWarnLimit(ctx context.Context) int {
|
|||
// point (session start, post-clear, post-compaction). It combines the
|
||||
// agent-specific prompt with any agentSpawn hook output.
|
||||
func (s *agent) spawnContext(ctx context.Context) string {
|
||||
result := s.hooks.Run(ctx, HookAgentSpawn, map[string]string{
|
||||
result := s.runtime.Hooks.Run(ctx, HookAgentSpawn, map[string]string{
|
||||
"session_id": s.sessionID,
|
||||
"agent": s.agentName,
|
||||
"cwd": s.CWD(),
|
||||
"model": s.cfg.Backend.Model(),
|
||||
"model": s.runtime.Backend.Model(),
|
||||
}, s.log)
|
||||
if result.Warning != "" {
|
||||
s.emit(infoEvent(result.Warning))
|
||||
|
|
@ -666,7 +645,7 @@ func (s *agent) spawnContext(ctx context.Context) string {
|
|||
// the pre-hook blocked or there was nothing to compact. Caller manages setState.
|
||||
func (s *agent) runCompact(ctx context.Context, trigger string) (int, error) {
|
||||
payload := map[string]string{"session_id": s.sessionID, "trigger": trigger, "cwd": s.CWD()}
|
||||
pre := s.hooks.Run(ctx, HookPreCompact, payload, s.log)
|
||||
pre := s.runtime.Hooks.Run(ctx, HookPreCompact, payload, s.log)
|
||||
if pre.Warning != "" {
|
||||
s.emit(infoEvent(pre.Warning))
|
||||
}
|
||||
|
|
@ -677,7 +656,7 @@ func (s *agent) runCompact(ctx context.Context, trigger string) (int, error) {
|
|||
if pre.Context != "" {
|
||||
s.session.appendUserMessage(pre.Context)
|
||||
}
|
||||
n, _, err := s.session.compact(ctx, s.cfg.Backend)
|
||||
n, _, err := s.session.compact(ctx, s.runtime.Backend)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
@ -688,7 +667,7 @@ func (s *agent) runCompact(ctx context.Context, trigger string) (int, error) {
|
|||
s.session.appendUserMessage(sc)
|
||||
}
|
||||
}
|
||||
post := s.hooks.Run(ctx, HookPostCompact, payload, s.log)
|
||||
post := s.runtime.Hooks.Run(ctx, HookPostCompact, payload, s.log)
|
||||
if post.Warning != "" {
|
||||
s.emit(infoEvent(post.Warning))
|
||||
}
|
||||
|
|
@ -783,7 +762,7 @@ func (s *agent) CtxSz() string {
|
|||
s.log.Debug("CtxSz() no session")
|
||||
return "no active session"
|
||||
}
|
||||
ctxLen := s.cfg.Backend.ContextLength(context.Background())
|
||||
ctxLen := s.runtime.Backend.ContextLength(context.Background())
|
||||
if ctxLen <= 0 {
|
||||
ctxLen = defaultContextLength
|
||||
}
|
||||
|
|
@ -827,21 +806,21 @@ func (s *agent) Context() []backend.Message {
|
|||
msgs = slices.Clone(s.session.history())
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
if s.cfg.preamble != "" {
|
||||
msgs = append([]backend.Message{{Role: "system", Content: s.cfg.preamble}}, msgs...)
|
||||
if s.runtime.Preamble != "" {
|
||||
msgs = append([]backend.Message{{Role: "system", Content: s.runtime.Preamble}}, msgs...)
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
func (s *agent) SystemPrompt() string {
|
||||
s.log.Debug("SystemPrompt() len=%d", len(s.cfg.preamble))
|
||||
return s.cfg.preamble
|
||||
s.log.Debug("SystemPrompt() len=%d", len(s.runtime.Preamble))
|
||||
return s.runtime.Preamble
|
||||
}
|
||||
|
||||
func (s *agent) GenerationParams() backend.GenerationParams {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.cfg.GenerationParams
|
||||
return s.runtime.GenParams
|
||||
}
|
||||
|
||||
func (s *agent) SetGenerationParams(params backend.GenerationParams) error {
|
||||
|
|
@ -849,14 +828,14 @@ func (s *agent) SetGenerationParams(params backend.GenerationParams) error {
|
|||
return fmt.Errorf("cannot change params while agent is running")
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.cfg.GenerationParams = params
|
||||
s.runtime.GenParams = params
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *agent) ListModels() string {
|
||||
s.log.Debug("ListModels()")
|
||||
models := s.cfg.Backend.Models(context.Background())
|
||||
models := s.runtime.Backend.Models(context.Background())
|
||||
slices.Sort(models)
|
||||
return strings.Join(models, "\n")
|
||||
}
|
||||
|
|
@ -933,7 +912,7 @@ func (s *agent) Submit(ctx context.Context, input string) {
|
|||
func (s *agent) executeTurn(ctx context.Context, input string) string {
|
||||
s.emit(Event{Role: "user", Content: input})
|
||||
|
||||
hookResult := s.hooks.Run(ctx, HookPreTurn, map[string]string{
|
||||
hookResult := s.runtime.Hooks.Run(ctx, HookPreTurn, map[string]string{
|
||||
"session_id": s.sessionID,
|
||||
"cwd": s.CWD(),
|
||||
"prompt": input,
|
||||
|
|
@ -979,6 +958,21 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
|
|||
|
||||
s.auditLog.Debug("turn: start input=%s session=%s", auditTruncate(input), s.sessionID)
|
||||
|
||||
// Build per-turn agentConfig from the current runtime.
|
||||
s.cfg = agentConfig{
|
||||
Backend: s.runtime.Backend,
|
||||
preamble: s.runtime.Preamble,
|
||||
Tools: s.runtime.Tools,
|
||||
Exec: s.runtime.Exec,
|
||||
ClassifyTool: s.runtime.ClassifyTool,
|
||||
ClassifyTier: s.runtime.ClassifyTier,
|
||||
ToolResultMaxBytes: s.runtime.toolResultMaxBytes(),
|
||||
GenerationParams: s.runtime.GenParams,
|
||||
MaxSteps: s.runtime.MaxSteps,
|
||||
ReadPlanStep: s.readPlanStep,
|
||||
TurnError: s.turnError,
|
||||
}
|
||||
|
||||
var replyBuf strings.Builder
|
||||
s.cfg.Output = func(ev Event) {
|
||||
switch ev.Role {
|
||||
|
|
@ -1023,7 +1017,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
|
|||
return ""
|
||||
}
|
||||
s.cfg.PreTool = func(ctx context.Context, name string, args json.RawMessage) HookResult {
|
||||
return s.hooks.Run(ctx, HookPreTool, map[string]string{
|
||||
return s.runtime.Hooks.Run(ctx, HookPreTool, map[string]string{
|
||||
"session_id": s.sessionID,
|
||||
"cwd": s.CWD(),
|
||||
"tool": name,
|
||||
|
|
@ -1031,7 +1025,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
|
|||
}, s.log)
|
||||
}
|
||||
s.cfg.PostTool = func(ctx context.Context, name string, args json.RawMessage, result string) HookResult {
|
||||
return s.hooks.Run(ctx, HookPostTool, map[string]string{
|
||||
return s.runtime.Hooks.Run(ctx, HookPostTool, map[string]string{
|
||||
"session_id": s.sessionID,
|
||||
"cwd": s.CWD(),
|
||||
"tool": name,
|
||||
|
|
@ -1164,7 +1158,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
stopResult := s.hooks.Run(ctx, HookPostTurn, map[string]string{
|
||||
stopResult := s.runtime.Hooks.Run(ctx, HookPostTurn, map[string]string{
|
||||
"session_id": s.sessionID,
|
||||
"cwd": s.CWD(),
|
||||
}, s.log)
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ func defaultBE() *mockBackend {
|
|||
}
|
||||
|
||||
// newCore builds a minimal *agent for tests, bypassing loadSystemPrompt
|
||||
// by directly setting preamble on AgentEnv.
|
||||
// by directly setting Preamble on Runtime.
|
||||
func newCore(t *testing.T, be backend.Backend, hooks Hooks) *agent {
|
||||
t.Helper()
|
||||
t.Setenv("OLLIE", "")
|
||||
|
|
@ -93,9 +93,9 @@ func newCore(t *testing.T, be backend.Backend, hooks Hooks) *agent {
|
|||
if hooks == nil {
|
||||
hooks = Hooks{}
|
||||
}
|
||||
env := AgentEnv{
|
||||
env := &Runtime{
|
||||
Hooks: hooks,
|
||||
preamble: "test system prompt",
|
||||
Preamble: "test system prompt",
|
||||
}
|
||||
c := NewAgentCore(AgentCoreConfig{
|
||||
Backend: be,
|
||||
|
|
@ -104,7 +104,7 @@ func newCore(t *testing.T, be backend.Backend, hooks Hooks) *agent {
|
|||
SessionsDir: t.TempDir(),
|
||||
SessionID: NewSessionID(),
|
||||
CWD: t.TempDir(),
|
||||
Env: env,
|
||||
Runtime: env,
|
||||
NewDispatcher: tools.NewDispatcher,
|
||||
})
|
||||
t.Cleanup(c.Close)
|
||||
|
|
@ -229,7 +229,7 @@ func TestSubmit_ToolCallStateTransitions(t *testing.T) {
|
|||
c := newCore(t, be, nil)
|
||||
|
||||
var stateAtExec string
|
||||
c.cfg.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
stateAtExec = c.State()
|
||||
return `{}`, nil
|
||||
}
|
||||
|
|
@ -993,7 +993,7 @@ func TestRun_NoExec(t *testing.T) {
|
|||
return textStream("done"), nil
|
||||
}
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.Exec = nil // no executor
|
||||
c.runtime.Exec = nil // no executor
|
||||
collectEvents(context.Background(), c, "run tool")
|
||||
if callCount != 2 {
|
||||
t.Errorf("backend called %d times; want 2", callCount)
|
||||
|
|
@ -1563,7 +1563,7 @@ func TestRun_ExecCancelledWithInject(t *testing.T) {
|
|||
c := newCore(t, be, nil)
|
||||
inject := "user interrupt"
|
||||
c.pendingInject.Store(&inject)
|
||||
c.cfg.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
cancel() // ctx cancelled during exec
|
||||
return "", fmt.Errorf("exec cancelled")
|
||||
}
|
||||
|
|
@ -1742,8 +1742,8 @@ func TestSetSessionID_Rename(t *testing.T) {
|
|||
|
||||
func TestSetSessionID_UpdatesPreamble(t *testing.T) {
|
||||
oldID := NewSessionID()
|
||||
env := AgentEnv{
|
||||
preamble: "session is " + oldID + " end",
|
||||
env := &Runtime{
|
||||
Preamble: "session is " + oldID + " end",
|
||||
}
|
||||
c := NewAgentCore(AgentCoreConfig{
|
||||
Backend: defaultBE(),
|
||||
|
|
@ -1752,7 +1752,7 @@ func TestSetSessionID_UpdatesPreamble(t *testing.T) {
|
|||
SessionsDir: t.TempDir(),
|
||||
SessionID: oldID,
|
||||
CWD: t.TempDir(),
|
||||
Env: env,
|
||||
Runtime: env,
|
||||
NewDispatcher: tools.NewDispatcher,
|
||||
}).(*agent)
|
||||
t.Cleanup(c.Close)
|
||||
|
|
@ -1995,7 +1995,7 @@ func TestCommand_Backend_Switch(t *testing.T) {
|
|||
if !found {
|
||||
t.Errorf("/backend switch: expected 'injected' in info events; got %v", infos)
|
||||
}
|
||||
if c.cfg.Backend != newBE {
|
||||
if c.runtime.Backend != newBE {
|
||||
t.Errorf("/backend switch: backend not updated")
|
||||
}
|
||||
}
|
||||
|
|
@ -2174,10 +2174,10 @@ func newCoreWithExecServer(t *testing.T, srv *mockEnvServer) *agent {
|
|||
t.Setenv("OLLIE", "")
|
||||
d := tools.NewDispatcher()
|
||||
d.AddServer("execute", srv)
|
||||
env := AgentEnv{
|
||||
env := &Runtime{
|
||||
Hooks: Hooks{},
|
||||
preamble: "test system prompt",
|
||||
dispatcher: d,
|
||||
Preamble: "test system prompt",
|
||||
Dispatcher: d,
|
||||
}
|
||||
c := NewAgentCore(AgentCoreConfig{
|
||||
Backend: defaultBE(),
|
||||
|
|
@ -2186,7 +2186,7 @@ func newCoreWithExecServer(t *testing.T, srv *mockEnvServer) *agent {
|
|||
SessionsDir: t.TempDir(),
|
||||
SessionID: NewSessionID(),
|
||||
CWD: t.TempDir(),
|
||||
Env: env,
|
||||
Runtime: env,
|
||||
NewDispatcher: tools.NewDispatcher,
|
||||
})
|
||||
t.Cleanup(c.Close)
|
||||
|
|
@ -2242,7 +2242,7 @@ func TestSetEnv_ShellCommandSeesEnv(t *testing.T) {
|
|||
|
||||
func TestSetEnv_NilDispatcher_NoPanic(t *testing.T) {
|
||||
c := newCore(t, nil, nil)
|
||||
c.dispatcher = nil
|
||||
c.runtime.Dispatcher = nil
|
||||
// Must not panic.
|
||||
c.SetEnv("K", "V")
|
||||
if c.env["K"] != "V" {
|
||||
|
|
@ -2278,7 +2278,7 @@ func TestFirstSentence_ShortNoSentenceEnd(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- BuildAgentEnv (nil config path) ---
|
||||
// --- BuildRuntime (nil config path) ---
|
||||
|
||||
func setupCfgDir(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
|
@ -2287,37 +2287,37 @@ func setupCfgDir(t *testing.T) string {
|
|||
return dir
|
||||
}
|
||||
|
||||
func TestBuildAgentEnv_NilConfig(t *testing.T) {
|
||||
func TestBuildRuntime_NilConfig(t *testing.T) {
|
||||
setupCfgDir(t)
|
||||
d := tools.NewDispatcher()
|
||||
env := BuildAgentEnv(nil, d, t.TempDir(), nil)
|
||||
env := BuildRuntime(nil, d, t.TempDir(), nil)
|
||||
|
||||
if env.preamble != "" {
|
||||
t.Errorf("preamble = %q; want empty", env.preamble)
|
||||
if env.Preamble != "" {
|
||||
t.Errorf("preamble = %q; want empty", env.Preamble)
|
||||
}
|
||||
if len(env.Hooks) != 0 {
|
||||
t.Errorf("expected no hooks; got %v", env.Hooks)
|
||||
}
|
||||
if len(env.tools) != 0 {
|
||||
t.Errorf("expected no tools; got %v", env.tools)
|
||||
if len(env.Tools) != 0 {
|
||||
t.Errorf("expected no tools; got %v", env.Tools)
|
||||
}
|
||||
if len(env.Messages) != 0 {
|
||||
t.Errorf("expected no startup messages; got %v", env.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentEnv_PromptBecomesPreamble(t *testing.T) {
|
||||
func TestBuildRuntime_PromptBecomesPreamble(t *testing.T) {
|
||||
setupCfgDir(t)
|
||||
d := tools.NewDispatcher()
|
||||
cfg := &config.Config{Prompt: config.Prompt{Value: []string{"the prompt"}}}
|
||||
env := BuildAgentEnv(cfg, d, t.TempDir(), nil)
|
||||
env := BuildRuntime(cfg, d, t.TempDir(), nil)
|
||||
|
||||
if env.preamble != "the prompt" {
|
||||
t.Errorf("preamble = %q; want %q", env.preamble, "the prompt")
|
||||
if env.Preamble != "the prompt" {
|
||||
t.Errorf("preamble = %q; want %q", env.Preamble, "the prompt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentEnv_HooksAndParams(t *testing.T) {
|
||||
func TestBuildRuntime_HooksAndParams(t *testing.T) {
|
||||
setupCfgDir(t)
|
||||
d := tools.NewDispatcher()
|
||||
temp := 0.7
|
||||
|
|
@ -2326,31 +2326,31 @@ func TestBuildAgentEnv_HooksAndParams(t *testing.T) {
|
|||
MaxTokens: 512,
|
||||
Temperature: &temp,
|
||||
}
|
||||
env := BuildAgentEnv(cfg, d, t.TempDir(), nil)
|
||||
env := BuildRuntime(cfg, d, t.TempDir(), nil)
|
||||
|
||||
if cmds := env.Hooks[HookPreTurn]; len(cmds) != 1 || cmds[0] != "echo hi" {
|
||||
t.Errorf("Hooks[preTurn] = %v", cmds)
|
||||
}
|
||||
if env.genParams.MaxTokens != 512 {
|
||||
t.Errorf("MaxTokens = %d; want 512", env.genParams.MaxTokens)
|
||||
if env.GenParams.MaxTokens != 512 {
|
||||
t.Errorf("MaxTokens = %d; want 512", env.GenParams.MaxTokens)
|
||||
}
|
||||
if env.genParams.Temperature == nil || *env.genParams.Temperature != 0.7 {
|
||||
t.Errorf("Temperature = %v; want 0.7", env.genParams.Temperature)
|
||||
if env.GenParams.Temperature == nil || *env.GenParams.Temperature != 0.7 {
|
||||
t.Errorf("Temperature = %v; want 0.7", env.GenParams.Temperature)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentEnv_ExecUnknownTool(t *testing.T) {
|
||||
func TestBuildRuntime_ExecUnknownTool(t *testing.T) {
|
||||
setupCfgDir(t)
|
||||
d := tools.NewDispatcher()
|
||||
env := BuildAgentEnv(nil, d, t.TempDir(), nil)
|
||||
env := BuildRuntime(nil, d, t.TempDir(), nil)
|
||||
|
||||
_, err := env.exec(context.Background(), "no_such_tool", json.RawMessage(`{}`))
|
||||
_, err := env.Exec(context.Background(), "no_such_tool", json.RawMessage(`{}`))
|
||||
if err == nil || !strings.Contains(err.Error(), "unknown tool") {
|
||||
t.Errorf("expected unknown tool error; got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- mock dispatcher for BuildAgentEnv tests ---
|
||||
// --- mock dispatcher for BuildRuntime tests ---
|
||||
|
||||
type mockDispatcher struct {
|
||||
tools []tools.ToolInfo
|
||||
|
|
@ -2379,64 +2379,64 @@ func (m *mockDispatcher) Dispatch(ctx context.Context, server, tool string, args
|
|||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func TestBuildAgentEnv_ListToolsError(t *testing.T) {
|
||||
func TestBuildRuntime_ListToolsError(t *testing.T) {
|
||||
setupCfgDir(t)
|
||||
d := &mockDispatcher{listErr: fmt.Errorf("boom")}
|
||||
env := BuildAgentEnv(nil, d, t.TempDir(), nil)
|
||||
env := BuildRuntime(nil, d, t.TempDir(), nil)
|
||||
if len(env.Messages) != 1 || !strings.Contains(env.Messages[0], "boom") {
|
||||
t.Errorf("Messages = %v", env.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentEnv_ToolsPopulated(t *testing.T) {
|
||||
func TestBuildRuntime_ToolsPopulated(t *testing.T) {
|
||||
setupCfgDir(t)
|
||||
d := &mockDispatcher{
|
||||
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool", Description: "desc", InputSchema: json.RawMessage(`{}`)}},
|
||||
}
|
||||
env := BuildAgentEnv(nil, d, t.TempDir(), nil)
|
||||
if len(env.tools) != 1 || env.tools[0].Name != "mytool" {
|
||||
t.Errorf("tools = %+v", env.tools)
|
||||
env := BuildRuntime(nil, d, t.TempDir(), nil)
|
||||
if len(env.Tools) != 1 || env.Tools[0].Name != "mytool" {
|
||||
t.Errorf("tools = %+v", env.Tools)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentEnv_ToolsDisabled(t *testing.T) {
|
||||
func TestBuildRuntime_ToolsDisabled(t *testing.T) {
|
||||
setupCfgDir(t)
|
||||
d := &mockDispatcher{
|
||||
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool", Description: "desc", InputSchema: json.RawMessage(`{}`)}},
|
||||
}
|
||||
f := false
|
||||
cfg := &config.Config{Tools: &f}
|
||||
env := BuildAgentEnv(cfg, d, t.TempDir(), nil)
|
||||
if len(env.tools) != 0 {
|
||||
t.Errorf("expected no tools when disabled; got %+v", env.tools)
|
||||
env := BuildRuntime(cfg, d, t.TempDir(), nil)
|
||||
if len(env.Tools) != 0 {
|
||||
t.Errorf("expected no tools when disabled; got %+v", env.Tools)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentEnv_PromptOnly(t *testing.T) {
|
||||
func TestBuildRuntime_PromptOnly(t *testing.T) {
|
||||
setupCfgDir(t)
|
||||
d := tools.NewDispatcher()
|
||||
cfg := &config.Config{Prompt: config.Prompt{Value: []string{"only agent"}}}
|
||||
env := BuildAgentEnv(cfg, d, t.TempDir(), nil)
|
||||
if env.preamble != "only agent" {
|
||||
t.Errorf("preamble = %q; want %q", env.preamble, "only agent")
|
||||
env := BuildRuntime(cfg, d, t.TempDir(), nil)
|
||||
if env.Preamble != "only agent" {
|
||||
t.Errorf("preamble = %q; want %q", env.Preamble, "only agent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentEnv_ExecPrompt(t *testing.T) {
|
||||
func TestBuildRuntime_ExecPrompt(t *testing.T) {
|
||||
setupCfgDir(t)
|
||||
d := tools.NewDispatcher()
|
||||
cfg := &config.Config{Prompt: config.Prompt{
|
||||
Value: []string{"echo hello", "echo 'You are a security auditor.'", "echo world"},
|
||||
IsExec: true,
|
||||
}}
|
||||
env := BuildAgentEnv(cfg, d, t.TempDir(), nil)
|
||||
env := BuildRuntime(cfg, d, t.TempDir(), nil)
|
||||
want := "hello\nYou are a security auditor.\nworld"
|
||||
if env.preamble != want {
|
||||
t.Errorf("preamble = %q; want %q", env.preamble, want)
|
||||
if env.Preamble != want {
|
||||
t.Errorf("preamble = %q; want %q", env.Preamble, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentEnv_ExecDispatchSuccess(t *testing.T) {
|
||||
func TestBuildRuntime_ExecDispatchSuccess(t *testing.T) {
|
||||
setupCfgDir(t)
|
||||
d := &mockDispatcher{
|
||||
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool"}},
|
||||
|
|
@ -2444,8 +2444,8 @@ func TestBuildAgentEnv_ExecDispatchSuccess(t *testing.T) {
|
|||
return json.RawMessage(`{"content":[{"type":"text","text":"ok"}]}`), nil
|
||||
},
|
||||
}
|
||||
env := BuildAgentEnv(nil, d, t.TempDir(), nil)
|
||||
result, err := env.exec(context.Background(), "mytool", json.RawMessage(`{}`))
|
||||
env := BuildRuntime(nil, d, t.TempDir(), nil)
|
||||
result, err := env.Exec(context.Background(), "mytool", json.RawMessage(`{}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -2454,7 +2454,7 @@ func TestBuildAgentEnv_ExecDispatchSuccess(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentEnv_ExecDispatchError(t *testing.T) {
|
||||
func TestBuildRuntime_ExecDispatchError(t *testing.T) {
|
||||
setupCfgDir(t)
|
||||
d := &mockDispatcher{
|
||||
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool"}},
|
||||
|
|
@ -2462,14 +2462,14 @@ func TestBuildAgentEnv_ExecDispatchError(t *testing.T) {
|
|||
return nil, fmt.Errorf("dispatch failed")
|
||||
},
|
||||
}
|
||||
env := BuildAgentEnv(nil, d, t.TempDir(), nil)
|
||||
_, err := env.exec(context.Background(), "mytool", json.RawMessage(`{}`))
|
||||
env := BuildRuntime(nil, d, t.TempDir(), nil)
|
||||
_, err := env.Exec(context.Background(), "mytool", json.RawMessage(`{}`))
|
||||
if err == nil || !strings.Contains(err.Error(), "dispatch failed") {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentEnv_ExecToolResultIsError(t *testing.T) {
|
||||
func TestBuildRuntime_ExecToolResultIsError(t *testing.T) {
|
||||
setupCfgDir(t)
|
||||
d := &mockDispatcher{
|
||||
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool"}},
|
||||
|
|
@ -2477,8 +2477,8 @@ func TestBuildAgentEnv_ExecToolResultIsError(t *testing.T) {
|
|||
return json.RawMessage(`{"isError":true,"content":[{"type":"text","text":"bad thing"}]}`), nil
|
||||
},
|
||||
}
|
||||
env := BuildAgentEnv(nil, d, t.TempDir(), nil)
|
||||
_, err := env.exec(context.Background(), "mytool", json.RawMessage(`{}`))
|
||||
env := BuildRuntime(nil, d, t.TempDir(), nil)
|
||||
_, err := env.Exec(context.Background(), "mytool", json.RawMessage(`{}`))
|
||||
if err == nil || !strings.Contains(err.Error(), "bad thing") {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ func TestResultCache_HitSkipsExec(t *testing.T) {
|
|||
})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ClassifyTool = func(string) bool { return true }
|
||||
c.cfg.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.ClassifyTool = func(string) bool { return true }
|
||||
c.runtime.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, error) {
|
||||
atomic.AddInt32(&execCount, 1)
|
||||
return "contents of a.txt", nil
|
||||
}
|
||||
|
|
@ -53,8 +53,8 @@ func TestResultCache_DifferentArgsMiss(t *testing.T) {
|
|||
})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ClassifyTool = func(string) bool { return true }
|
||||
c.cfg.Exec = func(_ context.Context, name string, args json.RawMessage) (string, error) {
|
||||
c.runtime.ClassifyTool = func(string) bool { return true }
|
||||
c.runtime.Exec = func(_ context.Context, name string, args json.RawMessage) (string, error) {
|
||||
atomic.AddInt32(&execCount, 1)
|
||||
return "result for " + string(args), nil
|
||||
}
|
||||
|
|
@ -77,8 +77,8 @@ func TestResultCache_SerialToolNotCached(t *testing.T) {
|
|||
})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ClassifyTool = func(name string) bool { return false } // all serial
|
||||
c.cfg.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.ClassifyTool = func(name string) bool { return false } // all serial
|
||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
atomic.AddInt32(&execCount, 1)
|
||||
return "ok", nil
|
||||
}
|
||||
|
|
@ -120,8 +120,8 @@ func TestResultCache_ErrorNotCached(t *testing.T) {
|
|||
})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ClassifyTool = func(string) bool { return true }
|
||||
c.cfg.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.ClassifyTool = func(string) bool { return true }
|
||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
n := atomic.AddInt32(&execCount, 1)
|
||||
if n == 1 {
|
||||
return "", &mockErr{"transient failure"}
|
||||
|
|
@ -155,8 +155,8 @@ func TestResultCache_IdenticalParallelReads(t *testing.T) {
|
|||
})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ClassifyTool = func(string) bool { return true } // both parallel-safe → same batch
|
||||
c.cfg.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.ClassifyTool = func(string) bool { return true } // both parallel-safe → same batch
|
||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
atomic.AddInt32(&execCount, 1)
|
||||
return "file contents", nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func TestConsecutiveErrors_HardLimit(t *testing.T) {
|
|||
be.respond = alwaysFailStream()
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
return "", fmt.Errorf("always fails")
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ func TestConsecutiveErrors_SoftLimitNudge(t *testing.T) {
|
|||
}
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
return "", fmt.Errorf("always fails")
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ func TestConsecutiveErrors_ResetOnSuccess(t *testing.T) {
|
|||
}
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, error) {
|
||||
if name == "good_tool" {
|
||||
return "ok", nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ func TestParallel_ConcurrentExecution(t *testing.T) {
|
|||
})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ClassifyTool = func(string) bool { return true }
|
||||
c.cfg.Exec = func(ctx context.Context, name string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.ClassifyTool = func(string) bool { return true }
|
||||
c.runtime.Exec = func(ctx context.Context, name string, _ json.RawMessage) (string, error) {
|
||||
started <- struct{}{}
|
||||
select {
|
||||
case <-gate:
|
||||
|
|
@ -90,10 +90,10 @@ func TestParallel_SerialToolBreaksBatch(t *testing.T) {
|
|||
var order []string
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ClassifyTool = func(name string) bool {
|
||||
c.runtime.ClassifyTool = func(name string) bool {
|
||||
return name == "read_a" || name == "read_c"
|
||||
}
|
||||
c.cfg.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, error) {
|
||||
mu.Lock()
|
||||
order = append(order, name)
|
||||
mu.Unlock()
|
||||
|
|
@ -131,10 +131,10 @@ func TestParallel_CancellationFillsRemaining(t *testing.T) {
|
|||
|
||||
c := newCore(t, be, nil)
|
||||
// tool_a and tool_b are parallel-safe; tool_c is serial.
|
||||
c.cfg.ClassifyTool = func(name string) bool {
|
||||
c.runtime.ClassifyTool = func(name string) bool {
|
||||
return name == "tool_a" || name == "tool_b"
|
||||
}
|
||||
c.cfg.Exec = func(execCtx context.Context, name string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.Exec = func(execCtx context.Context, name string, _ json.RawMessage) (string, error) {
|
||||
cancel() // cancel on first execution; propagates to all
|
||||
<-execCtx.Done()
|
||||
return "", execCtx.Err()
|
||||
|
|
@ -174,8 +174,8 @@ func TestParallel_NilClassifyToolIsSerial(t *testing.T) {
|
|||
var order []string
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ClassifyTool = nil // no classifier → all serial
|
||||
c.cfg.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.ClassifyTool = nil // no classifier → all serial
|
||||
c.runtime.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, error) {
|
||||
mu.Lock()
|
||||
order = append(order, name)
|
||||
mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func TestTextToolCall_DetectedAsUnsupported(t *testing.T) {
|
|||
}
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.Tools = []backend.Tool{{Name: "execute_code", Description: "run code"}}
|
||||
c.runtime.Tools = []backend.Tool{{Name: "execute_code", Description: "run code"}}
|
||||
|
||||
evs := collectEvents(context.Background(), c, "run something")
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ func TestTextToolCall_NoFalsePositive(t *testing.T) {
|
|||
}
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.Tools = []backend.Tool{{Name: "execute_code", Description: "run code"}}
|
||||
c.runtime.Tools = []backend.Tool{{Name: "execute_code", Description: "run code"}}
|
||||
|
||||
evs := collectEvents(context.Background(), c, "explain tools")
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ func TestTruncation_LargeResultTruncated(t *testing.T) {
|
|||
})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ClassifyTool = nil
|
||||
c.cfg.ToolResultMaxBytes = maxBytes
|
||||
c.cfg.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.ClassifyTool = nil
|
||||
c.runtime.ToolResultMaxBytes = maxBytes
|
||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
return large, nil
|
||||
}
|
||||
|
||||
|
|
@ -54,8 +54,8 @@ func TestTruncation_SmallResultNotTruncated(t *testing.T) {
|
|||
})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ToolResultMaxBytes = 8192
|
||||
c.cfg.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.ToolResultMaxBytes = 8192
|
||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
return "short result", nil
|
||||
}
|
||||
|
||||
|
|
@ -81,8 +81,8 @@ func TestTruncation_ZeroDisabled(t *testing.T) {
|
|||
})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ToolResultMaxBytes = 0 // disabled
|
||||
c.cfg.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.ToolResultMaxBytes = 0 // disabled
|
||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
return large, nil
|
||||
}
|
||||
|
||||
|
|
@ -109,8 +109,8 @@ func TestTruncation_ErrorNotTruncated(t *testing.T) {
|
|||
})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ToolResultMaxBytes = maxBytes
|
||||
c.cfg.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.ToolResultMaxBytes = maxBytes
|
||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
return "", &mockErr{longErr}
|
||||
}
|
||||
|
||||
|
|
@ -138,9 +138,9 @@ func TestTruncation_CachedResultAlreadyTruncated(t *testing.T) {
|
|||
})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.ClassifyTool = func(string) bool { return true } // read-safe → cacheable
|
||||
c.cfg.ToolResultMaxBytes = maxBytes
|
||||
c.cfg.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
c.runtime.ClassifyTool = func(string) bool { return true } // read-safe → cacheable
|
||||
c.runtime.ToolResultMaxBytes = maxBytes
|
||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, error) {
|
||||
return large, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func TestTurnError_HookInterceptsRateLimit(t *testing.T) {
|
|||
be.respond = errStream(&backend.RateLimitError{Message: "quota exceeded"})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.TurnError = func(_ context.Context, errType, _ string) HookResult {
|
||||
c.turnError = func(_ context.Context, errType, _ string) HookResult {
|
||||
atomic.AddInt32(&hookCalls, 1)
|
||||
if errType != "rate_limit" {
|
||||
t.Errorf("errType = %q; want rate_limit", errType)
|
||||
|
|
@ -62,7 +62,7 @@ func TestTurnError_HookInterceptsToolUnsupported(t *testing.T) {
|
|||
be.respond = errStream(&backend.ToolUnsupportedError{Message: "model does not support tools"})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.TurnError = func(_ context.Context, errType, _ string) HookResult {
|
||||
c.turnError = func(_ context.Context, errType, _ string) HookResult {
|
||||
atomic.AddInt32(&hookCalls, 1)
|
||||
if errType != "tool_unsupported" {
|
||||
t.Errorf("errType = %q; want tool_unsupported", errType)
|
||||
|
|
@ -110,7 +110,7 @@ func TestTurnError_NonRetryableErrorNoHook(t *testing.T) {
|
|||
be.respond = errStream(&backend.ToolUnsupportedError{Message: "no tools"})
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.TurnError = func(_ context.Context, errType, _ string) HookResult {
|
||||
c.turnError = func(_ context.Context, errType, _ string) HookResult {
|
||||
atomic.AddInt32(&hookCalls, 1)
|
||||
return HookResult{Ran: true, Handled: true}
|
||||
}
|
||||
|
|
@ -130,7 +130,7 @@ func TestTurnError_HookNotRunOnSuccess(t *testing.T) {
|
|||
// Default respond returns textStream("ok") — no error.
|
||||
|
||||
c := newCore(t, be, nil)
|
||||
c.cfg.TurnError = func(_ context.Context, _, _ string) HookResult {
|
||||
c.turnError = func(_ context.Context, _, _ string) HookResult {
|
||||
atomic.AddInt32(&hookCalls, 1)
|
||||
return HookResult{Ran: true}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"ollie/pkg/backend"
|
||||
"ollie/pkg/tools"
|
||||
)
|
||||
|
||||
func (r *Runtime) toolResultMaxBytes() int {
|
||||
if r.ToolResultMaxBytes > 0 {
|
||||
return r.ToolResultMaxBytes
|
||||
}
|
||||
return defaultToolResultMaxBytes
|
||||
}
|
||||
|
||||
// Runtime holds the swappable per-agent configuration. It contains everything
|
||||
// that changes on an /agent switch but is stable across turns within the same
|
||||
// agent. The agent struct stores a pointer to the active Runtime; switching
|
||||
// agents replaces it atomically.
|
||||
type Runtime struct {
|
||||
Backend backend.Backend
|
||||
Dispatcher tools.Dispatcher
|
||||
Hooks Hooks
|
||||
Preamble string // compiled system prompt
|
||||
Tools []backend.Tool
|
||||
Exec toolExecutor
|
||||
ClassifyTool toolClassifier
|
||||
ClassifyTier func(string) ResultTier
|
||||
GenParams backend.GenerationParams
|
||||
MaxSteps int
|
||||
ToolResultMaxBytes int // 0 means use defaultToolResultMaxBytes
|
||||
// CfgBackend and CfgModel are the backend/model overrides from the agent
|
||||
// config JSON. Empty means no override. Applied by the caller after
|
||||
// BuildRuntime returns.
|
||||
CfgBackend string
|
||||
CfgModel string
|
||||
// Messages holds non-fatal warnings from runtime construction
|
||||
// (e.g. tool listing errors). Surfaced once at session start.
|
||||
Messages []string
|
||||
}
|
||||
Reference in New Issue