agent: extract reasoning struct (swappable agent state)
Move agent-specific fields into a nested 'reasoning' struct: - history, runtime, cfg (per-turn params) - agentName, agentsDir - baseLayers, promptEnvExtra - newDispatcher, newBackend - currentAction, warnedContext, resultCache The agent struct is now the session host (owns sess + reasoning). On /agent swap, a new reasoning can be built from the new config while the session (identity, state, bus, env) remains stable.
This commit is contained in:
parent
23de2e8267
commit
da4d047aa1
|
|
@ -58,30 +58,30 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
|
|
||||||
"/backend": func(args []string) {
|
"/backend": func(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
a.emit(infoEvent(a.runtime.Backend.Name()))
|
a.emit(infoEvent(a.r.runtime.Backend.Name()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if a.IsRunning() {
|
if a.IsRunning() {
|
||||||
a.emit(infoEvent("error: cannot switch backend while agent is running"))
|
a.emit(infoEvent("error: cannot switch backend while agent is running"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
be, err := a.newBackend(args[0])
|
be, err := a.r.newBackend(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.emit(infoEvent(fmt.Sprintf("error: failed to switch backend: %v", err)))
|
a.emit(infoEvent(fmt.Sprintf("error: failed to switch backend: %v", err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.runtime.Backend = be
|
a.r.runtime.Backend = be
|
||||||
a.emit(infoEvent(fmt.Sprintf("switched backend to: %s (model: %s)", be.Name(), be.Model())))
|
a.emit(infoEvent(fmt.Sprintf("switched backend to: %s (model: %s)", be.Name(), be.Model())))
|
||||||
},
|
},
|
||||||
|
|
||||||
"/models": func(args []string) {
|
"/models": func(args []string) {
|
||||||
models := a.runtime.Backend.Models(ctx)
|
models := a.r.runtime.Backend.Models(ctx)
|
||||||
if len(models) == 0 {
|
if len(models) == 0 {
|
||||||
a.emit(infoEvent("no models available"))
|
a.emit(infoEvent("no models available"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
slices.Sort(models)
|
slices.Sort(models)
|
||||||
current := a.runtime.Backend.Model()
|
current := a.r.runtime.Backend.Model()
|
||||||
for _, m := range models {
|
for _, m := range models {
|
||||||
marker := " "
|
marker := " "
|
||||||
if m == current {
|
if m == current {
|
||||||
|
|
@ -93,19 +93,19 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
|
|
||||||
"/model": func(args []string) {
|
"/model": func(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
a.emit(infoEvent(a.runtime.Backend.Model()))
|
a.emit(infoEvent(a.r.runtime.Backend.Model()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.runtime.Backend.SetModel(args[0])
|
a.r.runtime.Backend.SetModel(args[0])
|
||||||
a.emit(infoEvent("switched model to: " + args[0]))
|
a.emit(infoEvent("switched model to: " + args[0]))
|
||||||
},
|
},
|
||||||
|
|
||||||
"/maxsteps": func(args []string) {
|
"/maxsteps": func(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
if a.runtime.MaxSteps == 0 {
|
if a.r.runtime.MaxSteps == 0 {
|
||||||
a.emit(infoEvent("maxsteps: unlimited"))
|
a.emit(infoEvent("maxsteps: unlimited"))
|
||||||
} else {
|
} else {
|
||||||
a.emit(infoEvent(fmt.Sprintf("maxsteps: %d", a.runtime.MaxSteps)))
|
a.emit(infoEvent(fmt.Sprintf("maxsteps: %d", a.r.runtime.MaxSteps)))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +114,7 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
a.emit(infoEvent("error: maxsteps requires a non-negative integer (0 = unlimited)"))
|
a.emit(infoEvent("error: maxsteps requires a non-negative integer (0 = unlimited)"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.runtime.MaxSteps = n
|
a.r.runtime.MaxSteps = n
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
a.emit(infoEvent("maxsteps: unlimited"))
|
a.emit(infoEvent("maxsteps: unlimited"))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -140,7 +140,7 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
}
|
}
|
||||||
seen[name] = true
|
seen[name] = true
|
||||||
marker := " "
|
marker := " "
|
||||||
if name == a.agentName {
|
if name == a.r.agentName {
|
||||||
marker = "* "
|
marker = "* "
|
||||||
}
|
}
|
||||||
a.emit(infoEvent(marker + name))
|
a.emit(infoEvent(marker + name))
|
||||||
|
|
@ -154,7 +154,7 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
|
|
||||||
"/agent": func(args []string) {
|
"/agent": func(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
a.emit(infoEvent("active agent: " + a.agentName))
|
a.emit(infoEvent("active agent: " + a.r.agentName))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if a.IsRunning() {
|
if a.IsRunning() {
|
||||||
|
|
@ -162,7 +162,7 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
name := args[0]
|
name := args[0]
|
||||||
cfgPath := AgentConfigPath(a.agentsDir, name)
|
cfgPath := AgentConfigPath(a.r.agentsDir, name)
|
||||||
f, err := os.Open(cfgPath)
|
f, err := os.Open(cfgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.emit(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err)))
|
a.emit(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err)))
|
||||||
|
|
@ -174,12 +174,12 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
a.emit(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err)))
|
a.emit(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d := a.newDispatcher()
|
d := a.r.newDispatcher()
|
||||||
env := []string{"OLLIE_SESSION_ID=" + a.sess.ID(), "OLLIE_UNAME=" + a.sess.Uname()}
|
env := []string{"OLLIE_SESSION_ID=" + a.sess.ID(), "OLLIE_UNAME=" + a.sess.Uname()}
|
||||||
env = append(env, a.promptEnvExtra...)
|
env = append(env, a.r.promptEnvExtra...)
|
||||||
rt := BuildRuntime(cfg, d, a.sess.CWD(), env, a.baseLayers...)
|
rt := BuildRuntime(cfg, d, a.sess.CWD(), env, a.r.baseLayers...)
|
||||||
if rt.CfgBackend != "" {
|
if rt.CfgBackend != "" {
|
||||||
newBe, err := a.newBackend(rt.CfgBackend)
|
newBe, err := a.r.newBackend(rt.CfgBackend)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.emit(infoEvent(fmt.Sprintf("error: backend %q: %v", rt.CfgBackend, err)))
|
a.emit(infoEvent(fmt.Sprintf("error: backend %q: %v", rt.CfgBackend, err)))
|
||||||
return
|
return
|
||||||
|
|
@ -189,14 +189,14 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
}
|
}
|
||||||
rt.Backend = newBe
|
rt.Backend = newBe
|
||||||
} else {
|
} else {
|
||||||
rt.Backend = a.runtime.Backend
|
rt.Backend = a.r.runtime.Backend
|
||||||
if rt.CfgModel != "" {
|
if rt.CfgModel != "" {
|
||||||
rt.Backend.SetModel(rt.CfgModel)
|
rt.Backend.SetModel(rt.CfgModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.runtime = rt
|
a.r.runtime = rt
|
||||||
a.agentName = name
|
a.r.agentName = name
|
||||||
a.history = nil
|
a.r.history = nil
|
||||||
a.pushSessionEnv()
|
a.pushSessionEnv()
|
||||||
a.notifyChange()
|
a.notifyChange()
|
||||||
for _, msg := range rt.Messages {
|
for _, msg := range rt.Messages {
|
||||||
|
|
@ -210,11 +210,11 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
a.emit(infoEvent("error: cannot compact while agent is running"))
|
a.emit(infoEvent("error: cannot compact while agent is running"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
a.emit(infoEvent("nothing to compact"))
|
a.emit(infoEvent("nothing to compact"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
snapshot := a.history.PreCompactionSnapshot()
|
snapshot := a.r.history.PreCompactionSnapshot()
|
||||||
a.setState("compacting")
|
a.setState("compacting")
|
||||||
n, err := a.runCompact(ctx, "manual")
|
n, err := a.runCompact(ctx, "manual")
|
||||||
a.setState("idle")
|
a.setState("idle")
|
||||||
|
|
@ -243,56 +243,56 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
},
|
},
|
||||||
|
|
||||||
"/context": func(args []string) {
|
"/context": func(args []string) {
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
a.emit(infoEvent("no active session"))
|
a.emit(infoEvent("no active session"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctxLen := a.runtime.Backend.ContextLength(ctx)
|
ctxLen := a.r.runtime.Backend.ContextLength(ctx)
|
||||||
if ctxLen <= 0 {
|
if ctxLen <= 0 {
|
||||||
ctxLen = defaultContextLength
|
ctxLen = defaultContextLength
|
||||||
}
|
}
|
||||||
estimated := a.history.estimateTokens()
|
estimated := a.r.history.estimateTokens()
|
||||||
pct := estimated * 100 / ctxLen
|
pct := estimated * 100 / ctxLen
|
||||||
a.emit(infoEvent(fmt.Sprintf("~%d / %d tokens (%d%%)", estimated, ctxLen, pct)))
|
a.emit(infoEvent(fmt.Sprintf("~%d / %d tokens (%d%%)", estimated, ctxLen, pct)))
|
||||||
a.emit(infoEvent(strings.TrimRight(a.history.contextDebug(), "\n")))
|
a.emit(infoEvent(strings.TrimRight(a.r.history.contextDebug(), "\n")))
|
||||||
},
|
},
|
||||||
|
|
||||||
"/cost": func(args []string) {
|
"/cost": func(args []string) {
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
a.emit(infoEvent("no active session"))
|
a.emit(infoEvent("no active session"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.emit(infoEvent(fmt.Sprintf("last=$%.4f session=$%.4f",
|
a.emit(infoEvent(fmt.Sprintf("last=$%.4f session=$%.4f",
|
||||||
a.history.LastTurnCostUSD, a.history.SessionCostUSD)))
|
a.r.history.LastTurnCostUSD, a.r.history.SessionCostUSD)))
|
||||||
},
|
},
|
||||||
|
|
||||||
"/usage": func(args []string) {
|
"/usage": func(args []string) {
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
a.emit(infoEvent("no active session"))
|
a.emit(infoEvent("no active session"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctxLen := a.runtime.Backend.ContextLength(ctx)
|
ctxLen := a.r.runtime.Backend.ContextLength(ctx)
|
||||||
if ctxLen <= 0 {
|
if ctxLen <= 0 {
|
||||||
ctxLen = defaultContextLength
|
ctxLen = defaultContextLength
|
||||||
}
|
}
|
||||||
estimated := a.history.estimateTokens()
|
estimated := a.r.history.estimateTokens()
|
||||||
pct := estimated * 100 / ctxLen
|
pct := estimated * 100 / ctxLen
|
||||||
usageStr := fmt.Sprintf("~%d / %d tokens (%d%%) | %d in, %d out, %d requests",
|
usageStr := fmt.Sprintf("~%d / %d tokens (%d%%) | %d in, %d out, %d requests",
|
||||||
estimated, ctxLen, pct,
|
estimated, ctxLen, pct,
|
||||||
a.history.TotalInputTokens, a.history.TotalOutputTokens,
|
a.r.history.TotalInputTokens, a.r.history.TotalOutputTokens,
|
||||||
a.history.TotalRequests)
|
a.r.history.TotalRequests)
|
||||||
if a.history.Estimated {
|
if a.r.history.Estimated {
|
||||||
usageStr += " [estimated]"
|
usageStr += " [estimated]"
|
||||||
}
|
}
|
||||||
a.emit(infoEvent(usageStr))
|
a.emit(infoEvent(usageStr))
|
||||||
},
|
},
|
||||||
|
|
||||||
"/history": func(args []string) {
|
"/history": func(args []string) {
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
a.emit(infoEvent("no active session"))
|
a.emit(infoEvent("no active session"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, msg := range a.history.history() {
|
for _, msg := range a.r.history.history() {
|
||||||
preview := msg.Content
|
preview := msg.Content
|
||||||
if len(preview) > 200 {
|
if len(preview) > 200 {
|
||||||
preview = preview[:200] + "..."
|
preview = preview[:200] + "..."
|
||||||
|
|
@ -306,7 +306,7 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
a.emit(infoEvent("error: cannot clear while agent is running"))
|
a.emit(infoEvent("error: cannot clear while agent is running"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.history = nil
|
a.r.history = nil
|
||||||
a.emit(infoEvent("cleared"))
|
a.emit(infoEvent("cleared"))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -373,7 +373,7 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
},
|
},
|
||||||
|
|
||||||
"/save": func(args []string) {
|
"/save": func(args []string) {
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
a.emit(infoEvent("error: no active session"))
|
a.emit(infoEvent("error: no active session"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -383,7 +383,7 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
}
|
}
|
||||||
name := args[0]
|
name := args[0]
|
||||||
path := a.sessionsDir + "/" + name + ".json"
|
path := a.sessionsDir + "/" + name + ".json"
|
||||||
if err := a.history.saveTo(path, name, a.agentName, a.CWD()); err != nil {
|
if err := a.r.history.saveTo(path, name, a.r.agentName, a.CWD()); err != nil {
|
||||||
a.emit(infoEvent("error: " + err.Error()))
|
a.emit(infoEvent("error: " + err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -411,7 +411,7 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
a.emit(infoEvent(fmt.Sprintf("error: %v", err)))
|
a.emit(infoEvent(fmt.Sprintf("error: %v", err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.history = RestoreHistory(&ps)
|
a.r.history = RestoreHistory(&ps)
|
||||||
a.emit(infoEvent(fmt.Sprintf("resumed session %s (%d messages)", name, len(ps.Messages))))
|
a.emit(infoEvent(fmt.Sprintf("resumed session %s (%d messages)", name, len(ps.Messages))))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -432,7 +432,7 @@ func (a *agent) handleCommand(ctx context.Context, input string) bool {
|
||||||
"/tools": func(args []string) { listFromHandler("tools") },
|
"/tools": func(args []string) { listFromHandler("tools") },
|
||||||
|
|
||||||
"/sp": func(args []string) {
|
"/sp": func(args []string) {
|
||||||
a.emit(infoEvent(a.runtime.Preamble))
|
a.emit(infoEvent(a.r.runtime.Preamble))
|
||||||
},
|
},
|
||||||
|
|
||||||
"/help": func(args []string) {
|
"/help": func(args []string) {
|
||||||
|
|
|
||||||
320
agent/core.go
320
agent/core.go
|
|
@ -311,30 +311,20 @@ type AgentCoreConfig struct {
|
||||||
// but has no knowledge of how output is rendered.
|
// but has no knowledge of how output is rendered.
|
||||||
type agent struct {
|
type agent struct {
|
||||||
sess *session.Session
|
sess *session.Session
|
||||||
history *History
|
r *reasoning
|
||||||
runtime *Runtime
|
|
||||||
cfg agentConfig // per-turn config built from runtime; set in executeTurn
|
|
||||||
log *olog.Logger
|
log *olog.Logger
|
||||||
agentName string
|
|
||||||
agentsDir string
|
|
||||||
sessionsDir string
|
sessionsDir string
|
||||||
newDispatcher func() tools.Dispatcher
|
|
||||||
newBackend func(string) (backend.Backend, error)
|
|
||||||
readPlanStep func() string
|
readPlanStep func() string
|
||||||
listHandlers map[string]func() []string
|
listHandlers map[string]func() []string
|
||||||
turnError func(ctx context.Context, errType, errMsg string) HookResult // overridable for tests
|
turnError func(ctx context.Context, errType, errMsg string) HookResult // overridable for tests
|
||||||
remote string // SSH target for remote execution
|
remote string // SSH target for remote execution
|
||||||
promptEnvExtra []string // PRIME_* vars for prompt resolution
|
|
||||||
baseLayers []string // system prompt, operational model, env block — for /agent reloads
|
|
||||||
startupMessages []string
|
startupMessages []string
|
||||||
currentAction atomic.Pointer[actionHandle]
|
|
||||||
toolCallCount atomic.Int64
|
toolCallCount atomic.Int64
|
||||||
pendingInject atomic.Pointer[string]
|
pendingInject atomic.Pointer[string]
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
changeMu sync.Mutex
|
changeMu sync.Mutex
|
||||||
changeCond *sync.Cond
|
changeCond *sync.Cond
|
||||||
submitMu sync.Mutex // serializes Submit calls (commands + turns)
|
submitMu sync.Mutex // serializes Submit calls (commands + turns)
|
||||||
warnedContext bool // true after a context-usage warning; cleared on compaction
|
|
||||||
auditLog *olog.Logger
|
auditLog *olog.Logger
|
||||||
|
|
||||||
// Debounced session persistence
|
// Debounced session persistence
|
||||||
|
|
@ -342,8 +332,6 @@ type agent struct {
|
||||||
saveDirty bool
|
saveDirty bool
|
||||||
saveTimer *time.Timer
|
saveTimer *time.Timer
|
||||||
|
|
||||||
// Cross-turn tool result cache (read-safe tools only)
|
|
||||||
resultCache sync.Map
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolCallCount returns the total number of tool calls executed in this
|
// ToolCallCount returns the total number of tool calls executed in this
|
||||||
|
|
@ -356,10 +344,10 @@ func (a *agent) ToolCallCount() int64 {
|
||||||
// SetEnv stores a session-scoped variable and propagates it to the execute server.
|
// SetEnv stores a session-scoped variable and propagates it to the execute server.
|
||||||
func (a *agent) SetEnv(key, value string) {
|
func (a *agent) SetEnv(key, value string) {
|
||||||
a.sess.SetEnv(key, value)
|
a.sess.SetEnv(key, value)
|
||||||
if a.runtime == nil || a.runtime.Dispatcher == nil {
|
if a.r.runtime == nil || a.r.runtime.Dispatcher == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if srv, ok := a.runtime.Dispatcher.GetServer("execute"); ok {
|
if srv, ok := a.r.runtime.Dispatcher.GetServer("execute"); ok {
|
||||||
if es, ok := srv.(tools.EnvSetter); ok {
|
if es, ok := srv.(tools.EnvSetter); ok {
|
||||||
es.SetEnv(key, value)
|
es.SetEnv(key, value)
|
||||||
}
|
}
|
||||||
|
|
@ -368,10 +356,10 @@ func (a *agent) SetEnv(key, value string) {
|
||||||
|
|
||||||
// pushSessionEnv injects OLLIE_SESSION_ID into the execute server subprocess env.
|
// pushSessionEnv injects OLLIE_SESSION_ID into the execute server subprocess env.
|
||||||
func (a *agent) pushSessionEnv() {
|
func (a *agent) pushSessionEnv() {
|
||||||
if a.runtime == nil || a.runtime.Dispatcher == nil || a.sess.ID() == "" {
|
if a.r.runtime == nil || a.r.runtime.Dispatcher == nil || a.sess.ID() == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if srv, ok := a.runtime.Dispatcher.GetServer("execute"); ok {
|
if srv, ok := a.r.runtime.Dispatcher.GetServer("execute"); ok {
|
||||||
if es, ok := srv.(tools.EnvSetter); ok {
|
if es, ok := srv.(tools.EnvSetter); ok {
|
||||||
es.SetEnv("OLLIE_SESSION_ID", a.sess.ID())
|
es.SetEnv("OLLIE_SESSION_ID", a.sess.ID())
|
||||||
if a.sess.Uname() != "" {
|
if a.sess.Uname() != "" {
|
||||||
|
|
@ -383,7 +371,7 @@ func (a *agent) pushSessionEnv() {
|
||||||
|
|
||||||
// pushLockDir sets the flock directory on the execute server to the session tmpdir.
|
// pushLockDir sets the flock directory on the execute server to the session tmpdir.
|
||||||
func (a *agent) pushLockDir() {
|
func (a *agent) pushLockDir() {
|
||||||
if a.runtime == nil || a.runtime.Dispatcher == nil || a.sess.ID() == "" {
|
if a.r.runtime == nil || a.r.runtime.Dispatcher == nil || a.sess.ID() == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -471,19 +459,21 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
|
||||||
Uname: cfg.Uname,
|
Uname: cfg.Uname,
|
||||||
CWD: paths.ExpandHome(cfg.CWD),
|
CWD: paths.ExpandHome(cfg.CWD),
|
||||||
}),
|
}),
|
||||||
history: cfg.History,
|
r: &reasoning{
|
||||||
runtime: rt,
|
history: cfg.History,
|
||||||
|
runtime: rt,
|
||||||
|
agentName: cfg.AgentName,
|
||||||
|
agentsDir: cfg.AgentsDir,
|
||||||
|
promptEnvExtra: cfg.PromptEnvExtra,
|
||||||
|
baseLayers: cfg.BaseLayers,
|
||||||
|
newDispatcher: cfg.NewDispatcher,
|
||||||
|
newBackend: cfg.NewBackend,
|
||||||
|
},
|
||||||
log: log,
|
log: log,
|
||||||
auditLog: log.Sub("audit"),
|
auditLog: log.Sub("audit"),
|
||||||
agentName: cfg.AgentName,
|
|
||||||
agentsDir: cfg.AgentsDir,
|
|
||||||
sessionsDir: cfg.SessionsDir,
|
sessionsDir: cfg.SessionsDir,
|
||||||
remote: cfg.Remote,
|
remote: cfg.Remote,
|
||||||
promptEnvExtra: cfg.PromptEnvExtra,
|
|
||||||
baseLayers: cfg.BaseLayers,
|
|
||||||
startupMessages: rt.Messages,
|
startupMessages: rt.Messages,
|
||||||
newDispatcher: cfg.NewDispatcher,
|
|
||||||
newBackend: cfg.NewBackend,
|
|
||||||
readPlanStep: readPlanStep,
|
readPlanStep: readPlanStep,
|
||||||
listHandlers: cfg.ListHandlers,
|
listHandlers: cfg.ListHandlers,
|
||||||
}
|
}
|
||||||
|
|
@ -491,10 +481,10 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
|
||||||
a.turnError = func(_ context.Context, errType, errMsg string) HookResult {
|
a.turnError = func(_ context.Context, errType, errMsg string) HookResult {
|
||||||
hookCtx, cancel := context.WithTimeout(context.Background(), time.Duration(hookTimeout)*time.Second)
|
hookCtx, cancel := context.WithTimeout(context.Background(), time.Duration(hookTimeout)*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return a.runtime.Hooks.Run(hookCtx, HookTurnError, map[string]string{
|
return a.r.runtime.Hooks.Run(hookCtx, HookTurnError, map[string]string{
|
||||||
"session_id": a.sess.ID(),
|
"session_id": a.sess.ID(),
|
||||||
"cwd": a.CWD(),
|
"cwd": a.CWD(),
|
||||||
"model": a.runtime.Backend.Model(),
|
"model": a.r.runtime.Backend.Model(),
|
||||||
"error_type": errType,
|
"error_type": errType,
|
||||||
"error": errMsg,
|
"error": errMsg,
|
||||||
}, a.log)
|
}, a.log)
|
||||||
|
|
@ -508,8 +498,8 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
|
||||||
func (a *agent) Close() {
|
func (a *agent) Close() {
|
||||||
a.log.Debug("Close() session=%q", a.sess.ID())
|
a.log.Debug("Close() session=%q", a.sess.ID())
|
||||||
a.flushSave()
|
a.flushSave()
|
||||||
if a.runtime != nil && a.runtime.Dispatcher != nil {
|
if a.r.runtime != nil && a.r.runtime.Dispatcher != nil {
|
||||||
if srv, ok := a.runtime.Dispatcher.GetServer("execute"); ok {
|
if srv, ok := a.r.runtime.Dispatcher.GetServer("execute"); ok {
|
||||||
if c, ok := srv.(interface{ Close() }); ok {
|
if c, ok := srv.(interface{ Close() }); ok {
|
||||||
a.log.Debug("Close() calling execute.Close()")
|
a.log.Debug("Close() calling execute.Close()")
|
||||||
c.Close()
|
c.Close()
|
||||||
|
|
@ -523,10 +513,10 @@ func (a *agent) Close() {
|
||||||
|
|
||||||
// execServer returns the execute server if available, or nil.
|
// execServer returns the execute server if available, or nil.
|
||||||
func (a *agent) execServer() interface{} {
|
func (a *agent) execServer() interface{} {
|
||||||
if a.runtime == nil || a.runtime.Dispatcher == nil {
|
if a.r.runtime == nil || a.r.runtime.Dispatcher == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
srv, _ := a.runtime.Dispatcher.GetServer("execute")
|
srv, _ := a.r.runtime.Dispatcher.GetServer("execute")
|
||||||
return srv
|
return srv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -634,10 +624,10 @@ func classifyReaction(emoji string) (category, description string, positive bool
|
||||||
|
|
||||||
func (a *agent) Reactions() map[string]string {
|
func (a *agent) Reactions() map[string]string {
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
for _, reaction := range a.history.Reactions {
|
for _, reaction := range a.r.history.Reactions {
|
||||||
result[reaction.ResponseID] = reaction.Emoji
|
result[reaction.ResponseID] = reaction.Emoji
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
@ -648,7 +638,7 @@ func (a *agent) React(emoji string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) ReactTo(responseID, emoji string) error {
|
func (a *agent) ReactTo(responseID, emoji string) error {
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
return fmt.Errorf("no active session")
|
return fmt.Errorf("no active session")
|
||||||
}
|
}
|
||||||
category, _, _ := classifyReaction(emoji)
|
category, _, _ := classifyReaction(emoji)
|
||||||
|
|
@ -656,9 +646,9 @@ func (a *agent) ReactTo(responseID, emoji string) error {
|
||||||
return fmt.Errorf("unsupported reaction: %s", emoji)
|
return fmt.Errorf("unsupported reaction: %s", emoji)
|
||||||
}
|
}
|
||||||
if responseID == "" {
|
if responseID == "" {
|
||||||
for i := len(a.history.messages) - 1; i >= 0; i-- {
|
for i := len(a.r.history.messages) - 1; i >= 0; i-- {
|
||||||
if a.history.messages[i].Role == "assistant" {
|
if a.r.history.messages[i].Role == "assistant" {
|
||||||
responseID = a.history.messages[i].ID
|
responseID = a.r.history.messages[i].ID
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -667,8 +657,8 @@ func (a *agent) ReactTo(responseID, emoji string) error {
|
||||||
return fmt.Errorf("no assistant response to react to")
|
return fmt.Errorf("no assistant response to react to")
|
||||||
}
|
}
|
||||||
found := false
|
found := false
|
||||||
for i := range a.history.messages {
|
for i := range a.r.history.messages {
|
||||||
if a.history.messages[i].Role == "assistant" && a.history.messages[i].ID == responseID {
|
if a.r.history.messages[i].Role == "assistant" && a.r.history.messages[i].ID == responseID {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -679,36 +669,36 @@ func (a *agent) ReactTo(responseID, emoji string) error {
|
||||||
|
|
||||||
reaction := Reaction{ID: NewReactionID(), ResponseID: responseID, Emoji: emoji, Category: category, CreatedAt: time.Now()}
|
reaction := Reaction{ID: NewReactionID(), ResponseID: responseID, Emoji: emoji, Category: category, CreatedAt: time.Now()}
|
||||||
replaced := false
|
replaced := false
|
||||||
for i := range a.history.Reactions {
|
for i := range a.r.history.Reactions {
|
||||||
if a.history.Reactions[i].ResponseID == responseID {
|
if a.r.history.Reactions[i].ResponseID == responseID {
|
||||||
if a.history.Reactions[i].Emoji == emoji {
|
if a.r.history.Reactions[i].Emoji == emoji {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
a.history.Reactions[i] = reaction
|
a.r.history.Reactions[i] = reaction
|
||||||
replaced = true
|
replaced = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !replaced {
|
if !replaced {
|
||||||
a.history.Reactions = append(a.history.Reactions, reaction)
|
a.r.history.Reactions = append(a.r.history.Reactions, reaction)
|
||||||
}
|
}
|
||||||
a.history.recomputeReactionCounts()
|
a.r.history.recomputeReactionCounts()
|
||||||
a.saveSession()
|
a.saveSession()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) AgentName() string {
|
func (a *agent) AgentName() string {
|
||||||
v := a.agentName
|
v := a.r.agentName
|
||||||
a.log.Debug("AgentName() = %q", v)
|
a.log.Debug("AgentName() = %q", v)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
func (a *agent) BackendName() string {
|
func (a *agent) BackendName() string {
|
||||||
v := a.runtime.Backend.Name()
|
v := a.r.runtime.Backend.Name()
|
||||||
a.log.Debug("BackendName() = %q", v)
|
a.log.Debug("BackendName() = %q", v)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
func (a *agent) ModelName() string {
|
func (a *agent) ModelName() string {
|
||||||
v := a.runtime.Backend.Model()
|
v := a.r.runtime.Backend.Model()
|
||||||
a.log.Debug("ModelName() = %q", v)
|
a.log.Debug("ModelName() = %q", v)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
@ -800,11 +790,11 @@ func (a *agent) SetCWD(dir string) error {
|
||||||
a.sess.SetCWD(dir)
|
a.sess.SetCWD(dir)
|
||||||
// Update cwd references in the system prompt.
|
// Update cwd references in the system prompt.
|
||||||
if oldCwd != "" && dir != "" && oldCwd != dir {
|
if oldCwd != "" && dir != "" && oldCwd != dir {
|
||||||
a.runtime.Preamble = strings.ReplaceAll(a.runtime.Preamble, oldCwd, dir)
|
a.r.runtime.Preamble = strings.ReplaceAll(a.r.runtime.Preamble, oldCwd, dir)
|
||||||
}
|
}
|
||||||
// Propagate to any tool server that knows how to handle it (e.g. execute).
|
// Propagate to any tool server that knows how to handle it (e.g. execute).
|
||||||
if a.runtime != nil && a.runtime.Dispatcher != nil {
|
if a.r.runtime != nil && a.r.runtime.Dispatcher != nil {
|
||||||
if srv, ok := a.runtime.Dispatcher.GetServer("execute"); ok {
|
if srv, ok := a.r.runtime.Dispatcher.GetServer("execute"); ok {
|
||||||
if ws, ok := srv.(tools.CWDSetter); ok {
|
if ws, ok := srv.(tools.CWDSetter); ok {
|
||||||
ws.SetCWD(dir)
|
ws.SetCWD(dir)
|
||||||
}
|
}
|
||||||
|
|
@ -835,7 +825,7 @@ func (a *agent) SetSessionID(newID string) error {
|
||||||
}
|
}
|
||||||
a.sess.SetID(newID)
|
a.sess.SetID(newID)
|
||||||
// Update session ID references in the system prompt.
|
// Update session ID references in the system prompt.
|
||||||
a.runtime.Preamble = strings.ReplaceAll(a.runtime.Preamble, oldID, newID)
|
a.r.runtime.Preamble = strings.ReplaceAll(a.r.runtime.Preamble, oldID, newID)
|
||||||
// Rename tmpdir so isread markers remain valid after rename.
|
// Rename tmpdir so isread markers remain valid after rename.
|
||||||
oldTemp := filepath.Join(ollieTmpDir(), oldID)
|
oldTemp := filepath.Join(ollieTmpDir(), oldID)
|
||||||
newTemp := filepath.Join(ollieTmpDir(), newID)
|
newTemp := filepath.Join(ollieTmpDir(), newID)
|
||||||
|
|
@ -854,7 +844,7 @@ const defaultToolResultMaxBytes = 131072
|
||||||
|
|
||||||
// autoCompactLimit returns the token threshold for auto-compaction (75%).
|
// autoCompactLimit returns the token threshold for auto-compaction (75%).
|
||||||
func (a *agent) autoCompactLimit(ctx context.Context) int {
|
func (a *agent) autoCompactLimit(ctx context.Context) int {
|
||||||
ctxLen := a.runtime.Backend.ContextLength(ctx)
|
ctxLen := a.r.runtime.Backend.ContextLength(ctx)
|
||||||
if ctxLen <= 0 {
|
if ctxLen <= 0 {
|
||||||
ctxLen = defaultContextLength
|
ctxLen = defaultContextLength
|
||||||
}
|
}
|
||||||
|
|
@ -863,7 +853,7 @@ func (a *agent) autoCompactLimit(ctx context.Context) int {
|
||||||
|
|
||||||
// autoWarnLimit returns the token threshold for a context-usage warning (60%).
|
// autoWarnLimit returns the token threshold for a context-usage warning (60%).
|
||||||
func (a *agent) autoWarnLimit(ctx context.Context) int {
|
func (a *agent) autoWarnLimit(ctx context.Context) int {
|
||||||
ctxLen := a.runtime.Backend.ContextLength(ctx)
|
ctxLen := a.r.runtime.Backend.ContextLength(ctx)
|
||||||
if ctxLen <= 0 {
|
if ctxLen <= 0 {
|
||||||
ctxLen = defaultContextLength
|
ctxLen = defaultContextLength
|
||||||
}
|
}
|
||||||
|
|
@ -874,11 +864,11 @@ func (a *agent) autoWarnLimit(ctx context.Context) int {
|
||||||
// point (session start, post-clear, post-compaction). It combines the
|
// point (session start, post-clear, post-compaction). It combines the
|
||||||
// agent-specific prompt with any agentSpawn hook output.
|
// agent-specific prompt with any agentSpawn hook output.
|
||||||
func (a *agent) spawnContext(ctx context.Context) string {
|
func (a *agent) spawnContext(ctx context.Context) string {
|
||||||
result := a.runtime.Hooks.Run(ctx, HookAgentSpawn, map[string]string{
|
result := a.r.runtime.Hooks.Run(ctx, HookAgentSpawn, map[string]string{
|
||||||
"session_id": a.sess.ID(),
|
"session_id": a.sess.ID(),
|
||||||
"agent": a.agentName,
|
"agent": a.r.agentName,
|
||||||
"cwd": a.CWD(),
|
"cwd": a.CWD(),
|
||||||
"model": a.runtime.Backend.Model(),
|
"model": a.r.runtime.Backend.Model(),
|
||||||
}, a.log)
|
}, a.log)
|
||||||
if result.Warning != "" {
|
if result.Warning != "" {
|
||||||
a.emit(infoEvent(result.Warning))
|
a.emit(infoEvent(result.Warning))
|
||||||
|
|
@ -898,7 +888,7 @@ func (a *agent) spawnContext(ctx context.Context) string {
|
||||||
// the pre-hook blocked or there was nothing to compact. Caller manages setState.
|
// the pre-hook blocked or there was nothing to compact. Caller manages setState.
|
||||||
func (a *agent) runCompact(ctx context.Context, trigger string) (int, error) {
|
func (a *agent) runCompact(ctx context.Context, trigger string) (int, error) {
|
||||||
payload := map[string]string{"session_id": a.sess.ID(), "trigger": trigger, "cwd": a.CWD()}
|
payload := map[string]string{"session_id": a.sess.ID(), "trigger": trigger, "cwd": a.CWD()}
|
||||||
pre := a.runtime.Hooks.Run(ctx, HookPreCompact, payload, a.log)
|
pre := a.r.runtime.Hooks.Run(ctx, HookPreCompact, payload, a.log)
|
||||||
if pre.Warning != "" {
|
if pre.Warning != "" {
|
||||||
a.emit(infoEvent(pre.Warning))
|
a.emit(infoEvent(pre.Warning))
|
||||||
}
|
}
|
||||||
|
|
@ -910,27 +900,27 @@ func (a *agent) runCompact(ctx context.Context, trigger string) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
if pre.Context != "" {
|
if pre.Context != "" {
|
||||||
a.history.appendUserMessage(pre.Context)
|
a.r.history.appendUserMessage(pre.Context)
|
||||||
}
|
}
|
||||||
// Use a cheaper model for compaction if configured.
|
// Use a cheaper model for compaction if configured.
|
||||||
compactModel := resolveCompactionModel(a.runtime.CompactionModel, a.runtime.Backend)
|
compactModel := resolveCompactionModel(a.r.runtime.CompactionModel, a.r.runtime.Backend)
|
||||||
origModel := a.runtime.Backend.Model()
|
origModel := a.r.runtime.Backend.Model()
|
||||||
if compactModel != "" && compactModel != origModel {
|
if compactModel != "" && compactModel != origModel {
|
||||||
a.runtime.Backend.SetModel(compactModel)
|
a.r.runtime.Backend.SetModel(compactModel)
|
||||||
defer a.runtime.Backend.SetModel(origModel)
|
defer a.r.runtime.Backend.SetModel(origModel)
|
||||||
}
|
}
|
||||||
n, _, err := a.history.compact(ctx, a.runtime.Backend)
|
n, _, err := a.r.history.compact(ctx, a.r.runtime.Backend)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
a.auditLog.Debug("compact: removed %d messages trigger=%s session=%s", n, trigger, a.sess.ID())
|
a.auditLog.Debug("compact: removed %d messages trigger=%s session=%s", n, trigger, a.sess.ID())
|
||||||
a.warnedContext = false
|
a.r.warnedContext = false
|
||||||
if sc := a.spawnContext(ctx); sc != "" {
|
if sc := a.spawnContext(ctx); sc != "" {
|
||||||
a.history.appendUserMessage(sc)
|
a.r.history.appendUserMessage(sc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
post := a.runtime.Hooks.Run(ctx, HookPostCompact, payload, a.log)
|
post := a.r.runtime.Hooks.Run(ctx, HookPostCompact, payload, a.log)
|
||||||
if post.Warning != "" {
|
if post.Warning != "" {
|
||||||
a.emit(infoEvent(post.Warning))
|
a.emit(infoEvent(post.Warning))
|
||||||
}
|
}
|
||||||
|
|
@ -938,7 +928,7 @@ func (a *agent) runCompact(ctx context.Context, trigger string) (int, error) {
|
||||||
a.emit(infoEvent("postCompact: " + sum))
|
a.emit(infoEvent("postCompact: " + sum))
|
||||||
}
|
}
|
||||||
if post.Context != "" {
|
if post.Context != "" {
|
||||||
a.history.appendUserMessage(post.Context)
|
a.r.history.appendUserMessage(post.Context)
|
||||||
}
|
}
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
@ -969,7 +959,7 @@ func (a *agent) flushSave() {
|
||||||
if !dirty {
|
if !dirty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if a.history == nil || a.sess.ID() == "" || a.sessionsDir == "" {
|
if a.r.history == nil || a.sess.ID() == "" || a.sessionsDir == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
path := a.activeSessionPath(a.sess.ID(), ".json")
|
path := a.activeSessionPath(a.sess.ID(), ".json")
|
||||||
|
|
@ -977,8 +967,8 @@ func (a *agent) flushSave() {
|
||||||
a.log.Error("session save: %v", err)
|
a.log.Error("session save: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := a.history.saveToFull(path, a.sess.ID(), a.agentName,
|
if err := a.r.history.saveToFull(path, a.sess.ID(), a.r.agentName,
|
||||||
a.runtime.Backend.Name(), a.runtime.Backend.Model(), a.CWD(), a.remote); err != nil {
|
a.r.runtime.Backend.Name(), a.r.runtime.Backend.Model(), a.CWD(), a.remote); err != nil {
|
||||||
a.log.Error("session save: %v", err)
|
a.log.Error("session save: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -988,15 +978,15 @@ func (a *agent) flushSave() {
|
||||||
func (a *agent) SaveSession(path string) error {
|
func (a *agent) SaveSession(path string) error {
|
||||||
a.mu.RLock()
|
a.mu.RLock()
|
||||||
defer a.mu.RUnlock()
|
defer a.mu.RUnlock()
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
return fmt.Errorf("no active session")
|
return fmt.Errorf("no active session")
|
||||||
}
|
}
|
||||||
return a.history.saveToFull(path, a.sess.ID(), a.agentName,
|
return a.r.history.saveToFull(path, a.sess.ID(), a.r.agentName,
|
||||||
a.runtime.Backend.Name(), a.runtime.Backend.Model(), a.CWD(), a.remote)
|
a.r.runtime.Backend.Name(), a.r.runtime.Backend.Model(), a.CWD(), a.remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) getActionCancel() context.CancelCauseFunc {
|
func (a *agent) getActionCancel() context.CancelCauseFunc {
|
||||||
if a := a.currentAction.Load(); a != nil {
|
if a := a.r.currentAction.Load(); a != nil {
|
||||||
return a.cancel
|
return a.cancel
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -1054,21 +1044,21 @@ func (a *agent) PopQueue() (string, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) IsRunning() bool {
|
func (a *agent) IsRunning() bool {
|
||||||
v := a.currentAction.Load() != nil
|
v := a.r.currentAction.Load() != nil
|
||||||
a.log.Debug("IsRunning() = %v", v)
|
a.log.Debug("IsRunning() = %v", v)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) CtxSz() string {
|
func (a *agent) CtxSz() string {
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
a.log.Debug("CtxSz() no session")
|
a.log.Debug("CtxSz() no session")
|
||||||
return "no active session"
|
return "no active session"
|
||||||
}
|
}
|
||||||
ctxLen := a.runtime.Backend.ContextLength(context.Background())
|
ctxLen := a.r.runtime.Backend.ContextLength(context.Background())
|
||||||
if ctxLen <= 0 {
|
if ctxLen <= 0 {
|
||||||
ctxLen = defaultContextLength
|
ctxLen = defaultContextLength
|
||||||
}
|
}
|
||||||
estimated := a.history.estimateTokens()
|
estimated := a.r.history.estimateTokens()
|
||||||
pct := estimated * 100 / ctxLen
|
pct := estimated * 100 / ctxLen
|
||||||
v := fmt.Sprintf("%d / %d (%d%%)", estimated, ctxLen, pct)
|
v := fmt.Sprintf("%d / %d (%d%%)", estimated, ctxLen, pct)
|
||||||
a.log.Debug("CtxSz() = %q", v)
|
a.log.Debug("CtxSz() = %q", v)
|
||||||
|
|
@ -1076,25 +1066,25 @@ func (a *agent) CtxSz() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) Cost() string {
|
func (a *agent) Cost() string {
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
return "no active session"
|
return "no active session"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("costLast=$%.4f\ncostSession=$%.4f\n",
|
return fmt.Sprintf("costLast=$%.4f\ncostSession=$%.4f\n",
|
||||||
a.history.LastTurnCostUSD, a.history.SessionCostUSD)
|
a.r.history.LastTurnCostUSD, a.r.history.SessionCostUSD)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) Usage() string {
|
func (a *agent) Usage() string {
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
a.log.Debug("Usage() no session")
|
a.log.Debug("Usage() no session")
|
||||||
return "no active session"
|
return "no active session"
|
||||||
}
|
}
|
||||||
str := fmt.Sprintf("%d in, %d out, %d requests",
|
str := fmt.Sprintf("%d in, %d out, %d requests",
|
||||||
a.history.TotalInputTokens, a.history.TotalOutputTokens,
|
a.r.history.TotalInputTokens, a.r.history.TotalOutputTokens,
|
||||||
a.history.TotalRequests)
|
a.r.history.TotalRequests)
|
||||||
if a.history.TotalCachedInputTokens > 0 {
|
if a.r.history.TotalCachedInputTokens > 0 {
|
||||||
str += fmt.Sprintf(", %d cached", a.history.TotalCachedInputTokens)
|
str += fmt.Sprintf(", %d cached", a.r.history.TotalCachedInputTokens)
|
||||||
}
|
}
|
||||||
if a.history.Estimated {
|
if a.r.history.Estimated {
|
||||||
str += " [estimated]"
|
str += " [estimated]"
|
||||||
}
|
}
|
||||||
a.log.Debug("Usage() = %q", str)
|
a.log.Debug("Usage() = %q", str)
|
||||||
|
|
@ -1104,37 +1094,37 @@ func (a *agent) Usage() string {
|
||||||
func (a *agent) Context() []backend.Message {
|
func (a *agent) Context() []backend.Message {
|
||||||
a.mu.RLock()
|
a.mu.RLock()
|
||||||
var msgs []backend.Message
|
var msgs []backend.Message
|
||||||
if a.history != nil {
|
if a.r.history != nil {
|
||||||
msgs = slices.Clone(a.history.history())
|
msgs = slices.Clone(a.r.history.history())
|
||||||
}
|
}
|
||||||
a.mu.RUnlock()
|
a.mu.RUnlock()
|
||||||
if a.runtime.Preamble != "" {
|
if a.r.runtime.Preamble != "" {
|
||||||
msgs = append([]backend.Message{{Role: "system", Content: a.runtime.Preamble}}, msgs...)
|
msgs = append([]backend.Message{{Role: "system", Content: a.r.runtime.Preamble}}, msgs...)
|
||||||
}
|
}
|
||||||
return msgs
|
return msgs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) SystemPrompt() string {
|
func (a *agent) SystemPrompt() string {
|
||||||
a.log.Debug("SystemPrompt() len=%d", len(a.runtime.Preamble))
|
a.log.Debug("SystemPrompt() len=%d", len(a.r.runtime.Preamble))
|
||||||
return a.runtime.Preamble
|
return a.r.runtime.Preamble
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) GenerationParams() backend.GenerationParams {
|
func (a *agent) GenerationParams() backend.GenerationParams {
|
||||||
a.mu.RLock()
|
a.mu.RLock()
|
||||||
defer a.mu.RUnlock()
|
defer a.mu.RUnlock()
|
||||||
return a.runtime.GenParams
|
return a.r.runtime.GenParams
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) CompactionModel() string {
|
func (a *agent) CompactionModel() string {
|
||||||
a.mu.RLock()
|
a.mu.RLock()
|
||||||
defer a.mu.RUnlock()
|
defer a.mu.RUnlock()
|
||||||
return a.runtime.CompactionModel
|
return a.r.runtime.CompactionModel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) SetCompactionModel(model string) {
|
func (a *agent) SetCompactionModel(model string) {
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer a.mu.Unlock()
|
||||||
a.runtime.CompactionModel = model
|
a.r.runtime.CompactionModel = model
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) SetGenerationParams(params backend.GenerationParams) error {
|
func (a *agent) SetGenerationParams(params backend.GenerationParams) error {
|
||||||
|
|
@ -1142,14 +1132,14 @@ func (a *agent) SetGenerationParams(params backend.GenerationParams) error {
|
||||||
return fmt.Errorf("cannot change params while agent is running")
|
return fmt.Errorf("cannot change params while agent is running")
|
||||||
}
|
}
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
a.runtime.GenParams = params
|
a.r.runtime.GenParams = params
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) ListModels() string {
|
func (a *agent) ListModels() string {
|
||||||
a.log.Debug("ListModels()")
|
a.log.Debug("ListModels()")
|
||||||
models := a.runtime.Backend.Models(context.Background())
|
models := a.r.runtime.Backend.Models(context.Background())
|
||||||
slices.Sort(models)
|
slices.Sort(models)
|
||||||
return strings.Join(models, "\n")
|
return strings.Join(models, "\n")
|
||||||
}
|
}
|
||||||
|
|
@ -1180,7 +1170,7 @@ func (a *agent) Submit(ctx context.Context, input string) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
a.log.Error("panic: %v\n%s", r, debug.Stack())
|
a.log.Error("panic: %v\n%s", r, debug.Stack())
|
||||||
if a := a.currentAction.Swap(nil); a != nil {
|
if a := a.r.currentAction.Swap(nil); a != nil {
|
||||||
a.cancel(fmt.Errorf("%v", r))
|
a.cancel(fmt.Errorf("%v", r))
|
||||||
}
|
}
|
||||||
a.setState("idle")
|
a.setState("idle")
|
||||||
|
|
@ -1226,7 +1216,7 @@ func (a *agent) Submit(ctx context.Context, input string) {
|
||||||
func (a *agent) executeTurn(ctx context.Context, input string) string {
|
func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
a.emit(Event{Role: "user", Content: input})
|
a.emit(Event{Role: "user", Content: input})
|
||||||
|
|
||||||
hookResult := a.runtime.Hooks.Run(ctx, HookPreTurn, map[string]string{
|
hookResult := a.r.runtime.Hooks.Run(ctx, HookPreTurn, map[string]string{
|
||||||
"session_id": a.sess.ID(),
|
"session_id": a.sess.ID(),
|
||||||
"cwd": a.CWD(),
|
"cwd": a.CWD(),
|
||||||
"prompt": input,
|
"prompt": input,
|
||||||
|
|
@ -1247,50 +1237,50 @@ func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
|
|
||||||
// Snapshot session state before this turn modifies it. Restored on failure
|
// Snapshot session state before this turn modifies it. Restored on failure
|
||||||
// so the session is clean for the next attempt.
|
// so the session is clean for the next attempt.
|
||||||
snapSession := a.history
|
snapSession := a.r.history
|
||||||
var snapMessages []backend.Message
|
var snapMessages []backend.Message
|
||||||
if a.history != nil {
|
if a.r.history != nil {
|
||||||
snapMessages = cloneMessages(a.history.messages)
|
snapMessages = cloneMessages(a.r.history.messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.history == nil {
|
if a.r.history == nil {
|
||||||
for _, msg := range a.startupMessages {
|
for _, msg := range a.startupMessages {
|
||||||
a.log.Debug("startup: %s", msg)
|
a.log.Debug("startup: %s", msg)
|
||||||
a.emit(infoEvent(msg))
|
a.emit(infoEvent(msg))
|
||||||
}
|
}
|
||||||
a.startupMessages = nil
|
a.startupMessages = nil
|
||||||
a.history = newHistory(input)
|
a.r.history = newHistory(input)
|
||||||
if sc := a.spawnContext(ctx); sc != "" {
|
if sc := a.spawnContext(ctx); sc != "" {
|
||||||
a.history.appendUserMessage(sc)
|
a.r.history.appendUserMessage(sc)
|
||||||
}
|
}
|
||||||
a.history.appendUserMessage(input)
|
a.r.history.appendUserMessage(input)
|
||||||
} else {
|
} else {
|
||||||
a.history.appendUserMessage(input)
|
a.r.history.appendUserMessage(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
actCtx, actCancel := context.WithCancelCause(ctx)
|
actCtx, actCancel := context.WithCancelCause(ctx)
|
||||||
handle := &actionHandle{cancel: actCancel}
|
handle := &actionHandle{cancel: actCancel}
|
||||||
a.currentAction.Store(handle)
|
a.r.currentAction.Store(handle)
|
||||||
a.setState("thinking")
|
a.setState("thinking")
|
||||||
|
|
||||||
a.auditLog.Debug("turn: start input=%s session=%s", auditTruncate(input), a.sess.ID())
|
a.auditLog.Debug("turn: start input=%s session=%s", auditTruncate(input), a.sess.ID())
|
||||||
|
|
||||||
// Build per-turn agentConfig from the current runtime.
|
// Build per-turn agentConfig from the current runtime.
|
||||||
a.cfg = agentConfig{
|
a.r.cfg = agentConfig{
|
||||||
Backend: a.runtime.Backend,
|
Backend: a.r.runtime.Backend,
|
||||||
preamble: a.runtime.Preamble,
|
preamble: a.r.runtime.Preamble,
|
||||||
Tools: a.runtime.Tools,
|
Tools: a.r.runtime.Tools,
|
||||||
Exec: a.runtime.Exec,
|
Exec: a.r.runtime.Exec,
|
||||||
ClassifyTool: a.runtime.ClassifyTool,
|
ClassifyTool: a.r.runtime.ClassifyTool,
|
||||||
ClassifyTier: a.runtime.ClassifyTier,
|
ClassifyTier: a.r.runtime.ClassifyTier,
|
||||||
GenerationParams: a.runtime.GenParams,
|
GenerationParams: a.r.runtime.GenParams,
|
||||||
MaxSteps: a.runtime.MaxSteps,
|
MaxSteps: a.r.runtime.MaxSteps,
|
||||||
ReadPlanStep: a.readPlanStep,
|
ReadPlanStep: a.readPlanStep,
|
||||||
TurnError: a.turnError,
|
TurnError: a.turnError,
|
||||||
}
|
}
|
||||||
|
|
||||||
var replyBuf strings.Builder
|
var replyBuf strings.Builder
|
||||||
a.cfg.Output = func(ev Event) {
|
a.r.cfg.Output = func(ev Event) {
|
||||||
switch ev.Role {
|
switch ev.Role {
|
||||||
case "assistant":
|
case "assistant":
|
||||||
replyBuf.WriteString(ev.Content)
|
replyBuf.WriteString(ev.Content)
|
||||||
|
|
@ -1306,11 +1296,11 @@ func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
case "error":
|
case "error":
|
||||||
a.auditLog.Debug("error: %s", ev.Content)
|
a.auditLog.Debug("error: %s", ev.Content)
|
||||||
}
|
}
|
||||||
if ev.Role == "usage" && a.history != nil {
|
if ev.Role == "usage" && a.r.history != nil {
|
||||||
var in, out, est, cached, creation int
|
var in, out, est, cached, creation int
|
||||||
var costUSD float64
|
var costUSD float64
|
||||||
fmt.Sscanf(ev.Content, "%d %d %d %g %d %d", &in, &out, &est, &costUSD, &cached, &creation)
|
fmt.Sscanf(ev.Content, "%d %d %d %g %d %d", &in, &out, &est, &costUSD, &cached, &creation)
|
||||||
a.history.addUsage(backend.Usage{
|
a.r.history.addUsage(backend.Usage{
|
||||||
InputTokens: in,
|
InputTokens: in,
|
||||||
CachedInputTokens: cached,
|
CachedInputTokens: cached,
|
||||||
CacheCreationTokens: creation,
|
CacheCreationTokens: creation,
|
||||||
|
|
@ -1319,7 +1309,7 @@ func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
}, est != 0)
|
}, est != 0)
|
||||||
a.notifyChange()
|
a.notifyChange()
|
||||||
if maxCostStr := os.Getenv("OLLIE_MAX_SESSION_COST"); maxCostStr != "" {
|
if maxCostStr := os.Getenv("OLLIE_MAX_SESSION_COST"); maxCostStr != "" {
|
||||||
if limit, ferr := strconv.ParseFloat(maxCostStr, 64); ferr == nil && limit > 0 && a.history.SessionCostUSD >= limit {
|
if limit, ferr := strconv.ParseFloat(maxCostStr, 64); ferr == nil && limit > 0 && a.r.history.SessionCostUSD >= limit {
|
||||||
a.emit(infoEvent(fmt.Sprintf("spending cap $%.2f reached — stopping", limit)))
|
a.emit(infoEvent(fmt.Sprintf("spending cap $%.2f reached — stopping", limit)))
|
||||||
a.Interrupt(ErrInterrupted)
|
a.Interrupt(ErrInterrupted)
|
||||||
}
|
}
|
||||||
|
|
@ -1327,22 +1317,22 @@ func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
}
|
}
|
||||||
a.emit(ev)
|
a.emit(ev)
|
||||||
}
|
}
|
||||||
a.cfg.PopInject = func() string {
|
a.r.cfg.PopInject = func() string {
|
||||||
if p := a.pendingInject.Swap(nil); p != nil {
|
if p := a.pendingInject.Swap(nil); p != nil {
|
||||||
return *p
|
return *p
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
a.cfg.PreTool = func(ctx context.Context, name string, args json.RawMessage) HookResult {
|
a.r.cfg.PreTool = func(ctx context.Context, name string, args json.RawMessage) HookResult {
|
||||||
return a.runtime.Hooks.Run(ctx, HookPreTool, map[string]string{
|
return a.r.runtime.Hooks.Run(ctx, HookPreTool, map[string]string{
|
||||||
"session_id": a.sess.ID(),
|
"session_id": a.sess.ID(),
|
||||||
"cwd": a.CWD(),
|
"cwd": a.CWD(),
|
||||||
"tool": name,
|
"tool": name,
|
||||||
"args": string(args),
|
"args": string(args),
|
||||||
}, a.log)
|
}, a.log)
|
||||||
}
|
}
|
||||||
a.cfg.PostTool = func(ctx context.Context, name string, args json.RawMessage, result string) HookResult {
|
a.r.cfg.PostTool = func(ctx context.Context, name string, args json.RawMessage, result string) HookResult {
|
||||||
return a.runtime.Hooks.Run(ctx, HookPostTool, map[string]string{
|
return a.r.runtime.Hooks.Run(ctx, HookPostTool, map[string]string{
|
||||||
"session_id": a.sess.ID(),
|
"session_id": a.sess.ID(),
|
||||||
"cwd": a.CWD(),
|
"cwd": a.CWD(),
|
||||||
"tool": name,
|
"tool": name,
|
||||||
|
|
@ -1350,17 +1340,17 @@ func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
"result": result,
|
"result": result,
|
||||||
}, a.log)
|
}, a.log)
|
||||||
}
|
}
|
||||||
a.cfg.IncrToolCallCount = func() int64 {
|
a.r.cfg.IncrToolCallCount = func() int64 {
|
||||||
return a.toolCallCount.Add(1)
|
return a.toolCallCount.Add(1)
|
||||||
}
|
}
|
||||||
a.cfg.SaveSession = func() { a.saveSession() }
|
a.r.cfg.SaveSession = func() { a.saveSession() }
|
||||||
a.cfg.ResultCache = &a.resultCache
|
a.r.cfg.ResultCache = &a.r.resultCache
|
||||||
a.cfg.AutoCompact = func(ctx context.Context) {
|
a.r.cfg.AutoCompact = func(ctx context.Context) {
|
||||||
if ctx.Err() != nil || a.history == nil {
|
if ctx.Err() != nil || a.r.history == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
limit := a.autoCompactLimit(ctx)
|
limit := a.autoCompactLimit(ctx)
|
||||||
if limit <= 0 || a.history.estimateTokens() < limit {
|
if limit <= 0 || a.r.history.estimateTokens() < limit {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.emit(Event{Role: "info", Content: "auto-compacting context...\n"})
|
a.emit(Event{Role: "info", Content: "auto-compacting context...\n"})
|
||||||
|
|
@ -1372,8 +1362,8 @@ func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn once when context usage crosses 60%; compact at 75%.
|
// Warn once when context usage crosses 60%; compact at 75%.
|
||||||
if a.history != nil {
|
if a.r.history != nil {
|
||||||
tokens := a.history.estimateTokens()
|
tokens := a.r.history.estimateTokens()
|
||||||
if compactLimit := a.autoCompactLimit(ctx); compactLimit > 0 && tokens >= compactLimit {
|
if compactLimit := a.autoCompactLimit(ctx); compactLimit > 0 && tokens >= compactLimit {
|
||||||
a.emit(Event{Role: "info", Content: "auto-compacting context...\n"})
|
a.emit(Event{Role: "info", Content: "auto-compacting context...\n"})
|
||||||
a.setState("compacting")
|
a.setState("compacting")
|
||||||
|
|
@ -1381,37 +1371,37 @@ func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
panic(fmt.Sprintf("auto-compact: %v", err))
|
panic(fmt.Sprintf("auto-compact: %v", err))
|
||||||
}
|
}
|
||||||
a.setState("thinking")
|
a.setState("thinking")
|
||||||
} else if warnLimit := a.autoWarnLimit(ctx); warnLimit > 0 && tokens >= warnLimit && !a.warnedContext {
|
} else if warnLimit := a.autoWarnLimit(ctx); warnLimit > 0 && tokens >= warnLimit && !a.r.warnedContext {
|
||||||
ctxLen := a.cfg.Backend.ContextLength(ctx)
|
ctxLen := a.r.cfg.Backend.ContextLength(ctx)
|
||||||
if ctxLen <= 0 {
|
if ctxLen <= 0 {
|
||||||
ctxLen = defaultContextLength
|
ctxLen = defaultContextLength
|
||||||
}
|
}
|
||||||
pct := tokens * 100 / ctxLen
|
pct := tokens * 100 / ctxLen
|
||||||
a.emit(Event{Role: "info", Content: fmt.Sprintf("context at %d%% — will auto-compact at 75%%\n", pct)})
|
a.emit(Event{Role: "info", Content: fmt.Sprintf("context at %d%% — will auto-compact at 75%%\n", pct)})
|
||||||
a.warnedContext = true
|
a.r.warnedContext = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spending cap: reject before spending more tokens.
|
// Spending cap: reject before spending more tokens.
|
||||||
if maxCostStr := os.Getenv("OLLIE_MAX_SESSION_COST"); maxCostStr != "" && a.history != nil {
|
if maxCostStr := os.Getenv("OLLIE_MAX_SESSION_COST"); maxCostStr != "" && a.r.history != nil {
|
||||||
if limit, err := strconv.ParseFloat(maxCostStr, 64); err == nil && limit > 0 {
|
if limit, err := strconv.ParseFloat(maxCostStr, 64); err == nil && limit > 0 {
|
||||||
if a.history.SessionCostUSD >= limit {
|
if a.r.history.SessionCostUSD >= limit {
|
||||||
a.emit(Event{Role: "error", Content: fmt.Sprintf("spending cap $%.2f reached (session total $%.4f)", limit, a.history.SessionCostUSD)})
|
a.emit(Event{Role: "error", Content: fmt.Sprintf("spending cap $%.2f reached (session total $%.4f)", limit, a.r.history.SessionCostUSD)})
|
||||||
a.setState("idle")
|
a.setState("idle")
|
||||||
actCancel(nil)
|
actCancel(nil)
|
||||||
a.currentAction.CompareAndSwap(handle, nil)
|
a.r.currentAction.CompareAndSwap(handle, nil)
|
||||||
if snapSession == nil {
|
if snapSession == nil {
|
||||||
a.history = nil
|
a.r.history = nil
|
||||||
} else {
|
} else {
|
||||||
a.history.messages = snapMessages
|
a.r.history.messages = snapMessages
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.history != nil {
|
if a.r.history != nil {
|
||||||
a.history.resetTurnAccumulators()
|
a.r.history.resetTurnAccumulators()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the turn, retrying once after compaction on context overflow.
|
// Run the turn, retrying once after compaction on context overflow.
|
||||||
|
|
@ -1420,9 +1410,9 @@ func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
for {
|
for {
|
||||||
err = run(actCtx, a.cfg, a.history)
|
err = run(actCtx, a.r.cfg, a.r.history)
|
||||||
actCancel(nil)
|
actCancel(nil)
|
||||||
a.currentAction.CompareAndSwap(handle, nil)
|
a.r.currentAction.CompareAndSwap(handle, nil)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
|
|
@ -1431,21 +1421,21 @@ func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
var ctxErr *backend.ContextOverflowError
|
var ctxErr *backend.ContextOverflowError
|
||||||
if !overflowRetried && errors.As(err, &ctxErr) && a.history != nil {
|
if !overflowRetried && errors.As(err, &ctxErr) && a.r.history != nil {
|
||||||
overflowRetried = true
|
overflowRetried = true
|
||||||
a.history.messages = snapMessages
|
a.r.history.messages = snapMessages
|
||||||
a.emit(Event{Role: "info", Content: "context overflow — compacting and retrying...\n"})
|
a.emit(Event{Role: "info", Content: "context overflow — compacting and retrying...\n"})
|
||||||
a.setState("compacting")
|
a.setState("compacting")
|
||||||
if _, cerr := a.runCompact(ctx, "overflow"); cerr != nil {
|
if _, cerr := a.runCompact(ctx, "overflow"); cerr != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
a.history.appendUserMessage(input)
|
a.r.history.appendUserMessage(input)
|
||||||
a.setState("thinking")
|
a.setState("thinking")
|
||||||
a.history.resetTurnAccumulators()
|
a.r.history.resetTurnAccumulators()
|
||||||
replyBuf.Reset()
|
replyBuf.Reset()
|
||||||
actCtx, actCancel = context.WithCancelCause(ctx)
|
actCtx, actCancel = context.WithCancelCause(ctx)
|
||||||
handle = &actionHandle{cancel: actCancel}
|
handle = &actionHandle{cancel: actCancel}
|
||||||
a.currentAction.Store(handle)
|
a.r.currentAction.Store(handle)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
@ -1461,8 +1451,8 @@ func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Keep completed work — only remove cancelled tool results.
|
// Keep completed work — only remove cancelled tool results.
|
||||||
// Error results are valuable feedback for the agent.
|
// Error results are valuable feedback for the agent.
|
||||||
if a.history != nil {
|
if a.r.history != nil {
|
||||||
a.history.removeCancelledToolResults()
|
a.r.history.removeCancelledToolResults()
|
||||||
}
|
}
|
||||||
if errors.Is(err, context.Canceled) || errors.Is(err, ErrInterrupted) {
|
if errors.Is(err, context.Canceled) || errors.Is(err, ErrInterrupted) {
|
||||||
a.auditLog.Debug("turn: interrupted session=%s", a.sess.ID())
|
a.auditLog.Debug("turn: interrupted session=%s", a.sess.ID())
|
||||||
|
|
@ -1477,7 +1467,7 @@ func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
stopResult := a.runtime.Hooks.Run(ctx, HookPostTurn, map[string]string{
|
stopResult := a.r.runtime.Hooks.Run(ctx, HookPostTurn, map[string]string{
|
||||||
"session_id": a.sess.ID(),
|
"session_id": a.sess.ID(),
|
||||||
"cwd": a.CWD(),
|
"cwd": a.CWD(),
|
||||||
}, a.log)
|
}, a.log)
|
||||||
|
|
@ -1487,18 +1477,18 @@ func (a *agent) executeTurn(ctx context.Context, input string) string {
|
||||||
if sum := stopResult.Summary(); sum != "" {
|
if sum := stopResult.Summary(); sum != "" {
|
||||||
a.emit(infoEvent("postTurn: " + sum))
|
a.emit(infoEvent("postTurn: " + sum))
|
||||||
}
|
}
|
||||||
if !stopResult.Blocked && stopResult.Context != "" && a.history != nil {
|
if !stopResult.Blocked && stopResult.Context != "" && a.r.history != nil {
|
||||||
a.history.appendUserMessage(stopResult.Context)
|
a.r.history.appendUserMessage(stopResult.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.history != nil {
|
if a.r.history != nil {
|
||||||
a.history.recordTurnCost(a.cfg.Backend.Model())
|
a.r.history.recordTurnCost(a.r.cfg.Backend.Model())
|
||||||
appendUsageLog(a.sess.ID(), a.cfg.Backend.Name(), a.cfg.Backend.Model(), a.history)
|
appendUsageLog(a.sess.ID(), a.r.cfg.Backend.Name(), a.r.cfg.Backend.Model(), a.r.history)
|
||||||
if a.history.LastTurnCostUSD > 0 {
|
if a.r.history.LastTurnCostUSD > 0 {
|
||||||
a.emit(Event{Role: "info", Content: fmt.Sprintf("costLast=$%.4f\n", a.history.LastTurnCostUSD)})
|
a.emit(Event{Role: "info", Content: fmt.Sprintf("costLast=$%.4f\n", a.r.history.LastTurnCostUSD)})
|
||||||
}
|
}
|
||||||
a.auditLog.Debug("turn: end reply=%s cost=$%.4f session_total=$%.4f session=%s",
|
a.auditLog.Debug("turn: end reply=%s cost=$%.4f session_total=$%.4f session=%s",
|
||||||
auditTruncate(a.sess.Reply()), a.history.LastTurnCostUSD, a.history.SessionCostUSD, a.sess.ID())
|
auditTruncate(a.sess.Reply()), a.r.history.LastTurnCostUSD, a.r.history.SessionCostUSD, a.sess.ID())
|
||||||
a.notifyChange()
|
a.notifyChange()
|
||||||
}
|
}
|
||||||
a.saveSession()
|
a.saveSession()
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ func TestSubmit_ToolCallStateTransitions(t *testing.T) {
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
|
|
||||||
var stateAtExec string
|
var stateAtExec string
|
||||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
stateAtExec = c.State()
|
stateAtExec = c.State()
|
||||||
return `{}`, nil, nil
|
return `{}`, nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -460,21 +460,21 @@ func TestManualCompact(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed a session with more than hotTailSize+warmIndexSize messages so compact() doesn't short-circuit.
|
// Seed a session with more than hotTailSize+warmIndexSize messages so compact() doesn't short-circuit.
|
||||||
c.history = newHistory("goal")
|
c.r.history = newHistory("goal")
|
||||||
for i := range 15 {
|
for i := range 15 {
|
||||||
c.history.messages = append(c.history.messages,
|
c.r.history.messages = append(c.r.history.messages,
|
||||||
backend.Message{Role: "assistant", Content: fmt.Sprintf("response %d with enough text to count", i)},
|
backend.Message{Role: "assistant", Content: fmt.Sprintf("response %d with enough text to count", i)},
|
||||||
backend.Message{Role: "user", Content: fmt.Sprintf("follow up %d", i)},
|
backend.Message{Role: "user", Content: fmt.Sprintf("follow up %d", i)},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
before := len(c.history.messages)
|
before := len(c.r.history.messages)
|
||||||
|
|
||||||
evs := collectEvents(context.Background(), c, "/compact")
|
evs := collectEvents(context.Background(), c, "/compact")
|
||||||
|
|
||||||
if c.history == nil {
|
if c.r.history == nil {
|
||||||
t.Fatal("session nil after compact")
|
t.Fatal("session nil after compact")
|
||||||
}
|
}
|
||||||
if after := len(c.history.messages); after >= before {
|
if after := len(c.r.history.messages); after >= before {
|
||||||
t.Errorf("messages: before=%d after=%d; want fewer after compact", before, after)
|
t.Errorf("messages: before=%d after=%d; want fewer after compact", before, after)
|
||||||
}
|
}
|
||||||
if callCount == 0 {
|
if callCount == 0 {
|
||||||
|
|
@ -514,9 +514,9 @@ func TestAutoCompact(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.history = newHistory("goal")
|
c.r.history = newHistory("goal")
|
||||||
for range 15 {
|
for range 15 {
|
||||||
c.history.messages = append(c.history.messages,
|
c.r.history.messages = append(c.r.history.messages,
|
||||||
backend.Message{Role: "assistant", Content: "a long response that exceeds seven tokens of content"},
|
backend.Message{Role: "assistant", Content: "a long response that exceeds seven tokens of content"},
|
||||||
backend.Message{Role: "user", Content: "follow up question with enough text to push over the limit"},
|
backend.Message{Role: "user", Content: "follow up question with enough text to push over the limit"},
|
||||||
)
|
)
|
||||||
|
|
@ -600,12 +600,12 @@ func TestCommand_Help(t *testing.T) {
|
||||||
func TestCommand_Clear(t *testing.T) {
|
func TestCommand_Clear(t *testing.T) {
|
||||||
c := newCore(t, nil, nil)
|
c := newCore(t, nil, nil)
|
||||||
collectEvents(context.Background(), c, "first turn")
|
collectEvents(context.Background(), c, "first turn")
|
||||||
if c.history == nil {
|
if c.r.history == nil {
|
||||||
t.Fatal("session nil after first turn")
|
t.Fatal("session nil after first turn")
|
||||||
}
|
}
|
||||||
oldID := c.sess.ID()
|
oldID := c.sess.ID()
|
||||||
collectEvents(context.Background(), c, "/clear")
|
collectEvents(context.Background(), c, "/clear")
|
||||||
if c.history != nil {
|
if c.r.history != nil {
|
||||||
t.Error("session not nil after /clear")
|
t.Error("session not nil after /clear")
|
||||||
}
|
}
|
||||||
if c.sess.ID() != oldID {
|
if c.sess.ID() != oldID {
|
||||||
|
|
@ -785,9 +785,9 @@ func TestCommand_Compact_NilSession(t *testing.T) {
|
||||||
|
|
||||||
func TestCommand_Compact_PreHookBlocks(t *testing.T) {
|
func TestCommand_Compact_PreHookBlocks(t *testing.T) {
|
||||||
c := newCore(t, nil, Hooks{HookPreCompact: []string{"exit 2"}})
|
c := newCore(t, nil, Hooks{HookPreCompact: []string{"exit 2"}})
|
||||||
c.history = newHistory("goal")
|
c.r.history = newHistory("goal")
|
||||||
for i := range 5 {
|
for i := range 5 {
|
||||||
c.history.messages = append(c.history.messages,
|
c.r.history.messages = append(c.r.history.messages,
|
||||||
backend.Message{Role: "assistant", Content: fmt.Sprintf("response %d", i)},
|
backend.Message{Role: "assistant", Content: fmt.Sprintf("response %d", i)},
|
||||||
backend.Message{Role: "user", Content: fmt.Sprintf("follow up %d", i)},
|
backend.Message{Role: "user", Content: fmt.Sprintf("follow up %d", i)},
|
||||||
)
|
)
|
||||||
|
|
@ -985,7 +985,7 @@ func TestRun_NoExec(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.Exec = nil // no executor
|
c.r.runtime.Exec = nil // no executor
|
||||||
collectEvents(context.Background(), c, "run tool")
|
collectEvents(context.Background(), c, "run tool")
|
||||||
if callCount != 2 {
|
if callCount != 2 {
|
||||||
t.Errorf("backend called %d times; want 2", callCount)
|
t.Errorf("backend called %d times; want 2", callCount)
|
||||||
|
|
@ -1225,7 +1225,7 @@ func TestCommand_Agent_NotFound(t *testing.T) {
|
||||||
|
|
||||||
func TestCommand_Agents_Empty(t *testing.T) {
|
func TestCommand_Agents_Empty(t *testing.T) {
|
||||||
c := newCore(t, nil, nil) // agentsDir is a fresh temp dir
|
c := newCore(t, nil, nil) // agentsDir is a fresh temp dir
|
||||||
t.Setenv("OLLIE_AGENTS_PATH", c.agentsDir)
|
t.Setenv("OLLIE_AGENTS_PATH", c.r.agentsDir)
|
||||||
evs := collectEvents(context.Background(), c, "/agents")
|
evs := collectEvents(context.Background(), c, "/agents")
|
||||||
found := false
|
found := false
|
||||||
for _, s := range byRole(evs, "info") {
|
for _, s := range byRole(evs, "info") {
|
||||||
|
|
@ -1435,8 +1435,8 @@ func TestCommand_SP(t *testing.T) {
|
||||||
|
|
||||||
func TestCommand_Agents_List(t *testing.T) {
|
func TestCommand_Agents_List(t *testing.T) {
|
||||||
c := newCore(t, nil, nil)
|
c := newCore(t, nil, nil)
|
||||||
t.Setenv("OLLIE_AGENTS_PATH", c.agentsDir)
|
t.Setenv("OLLIE_AGENTS_PATH", c.r.agentsDir)
|
||||||
if err := os.WriteFile(c.agentsDir+"/myagent.json", []byte(`{}`), 0600); err != nil {
|
if err := os.WriteFile(c.r.agentsDir+"/myagent.json", []byte(`{}`), 0600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
evs := collectEvents(context.Background(), c, "/agents")
|
evs := collectEvents(context.Background(), c, "/agents")
|
||||||
|
|
@ -1594,7 +1594,7 @@ func TestRun_ExecCancelledWithInject(t *testing.T) {
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
inject := "user interrupt"
|
inject := "user interrupt"
|
||||||
c.pendingInject.Store(&inject)
|
c.pendingInject.Store(&inject)
|
||||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
cancel() // ctx cancelled during exec
|
cancel() // ctx cancelled during exec
|
||||||
return "", nil, fmt.Errorf("exec cancelled")
|
return "", nil, fmt.Errorf("exec cancelled")
|
||||||
}
|
}
|
||||||
|
|
@ -1625,9 +1625,9 @@ func TestAutoCompact_WithHookContext(t *testing.T) {
|
||||||
return textStream("answer"), nil
|
return textStream("answer"), nil
|
||||||
|
|
||||||
}
|
}
|
||||||
c.history = newHistory("goal")
|
c.r.history = newHistory("goal")
|
||||||
for range 15 {
|
for range 15 {
|
||||||
c.history.messages = append(c.history.messages,
|
c.r.history.messages = append(c.r.history.messages,
|
||||||
backend.Message{Role: "assistant", Content: "a long response that exceeds seven tokens of content"},
|
backend.Message{Role: "assistant", Content: "a long response that exceeds seven tokens of content"},
|
||||||
backend.Message{Role: "user", Content: "follow up question with enough text to push over the limit"},
|
backend.Message{Role: "user", Content: "follow up question with enough text to push over the limit"},
|
||||||
)
|
)
|
||||||
|
|
@ -1844,7 +1844,7 @@ func TestAgentSpawn_WithContext(t *testing.T) {
|
||||||
collectEvents(context.Background(), c, "first")
|
collectEvents(context.Background(), c, "first")
|
||||||
// spawn context goes into session history, not preamble
|
// spawn context goes into session history, not preamble
|
||||||
found := false
|
found := false
|
||||||
for _, m := range c.history.messages {
|
for _, m := range c.r.history.messages {
|
||||||
if strings.Contains(m.Content, "spawn context") {
|
if strings.Contains(m.Content, "spawn context") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
|
|
@ -1887,9 +1887,9 @@ func TestManualCompact_BackendError(t *testing.T) {
|
||||||
callCount++
|
callCount++
|
||||||
return nil, fmt.Errorf("compact backend error")
|
return nil, fmt.Errorf("compact backend error")
|
||||||
}
|
}
|
||||||
c.history = newHistory("goal")
|
c.r.history = newHistory("goal")
|
||||||
for i := range 15 {
|
for i := range 15 {
|
||||||
c.history.messages = append(c.history.messages,
|
c.r.history.messages = append(c.r.history.messages,
|
||||||
backend.Message{Role: "assistant", Content: fmt.Sprintf("response %d", i)},
|
backend.Message{Role: "assistant", Content: fmt.Sprintf("response %d", i)},
|
||||||
backend.Message{Role: "user", Content: fmt.Sprintf("follow up %d", i)},
|
backend.Message{Role: "user", Content: fmt.Sprintf("follow up %d", i)},
|
||||||
)
|
)
|
||||||
|
|
@ -1921,9 +1921,9 @@ func TestManualCompact_EmptySummary(t *testing.T) {
|
||||||
return ch, nil
|
return ch, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
c.history = newHistory("goal")
|
c.r.history = newHistory("goal")
|
||||||
for i := range 15 {
|
for i := range 15 {
|
||||||
c.history.messages = append(c.history.messages,
|
c.r.history.messages = append(c.r.history.messages,
|
||||||
backend.Message{Role: "assistant", Content: fmt.Sprintf("response %d", i)},
|
backend.Message{Role: "assistant", Content: fmt.Sprintf("response %d", i)},
|
||||||
backend.Message{Role: "user", Content: fmt.Sprintf("follow up %d", i)},
|
backend.Message{Role: "user", Content: fmt.Sprintf("follow up %d", i)},
|
||||||
)
|
)
|
||||||
|
|
@ -1949,15 +1949,15 @@ func TestManualCompact_WithToolMessages(t *testing.T) {
|
||||||
return textStream("summary"), nil
|
return textStream("summary"), nil
|
||||||
|
|
||||||
}
|
}
|
||||||
c.history = newHistory("goal")
|
c.r.history = newHistory("goal")
|
||||||
// Seed enough messages to pass the compaction threshold.
|
// Seed enough messages to pass the compaction threshold.
|
||||||
for i := range 10 {
|
for i := range 10 {
|
||||||
c.history.messages = append(c.history.messages,
|
c.r.history.messages = append(c.r.history.messages,
|
||||||
backend.Message{Role: "assistant", Content: fmt.Sprintf("response %d", i)},
|
backend.Message{Role: "assistant", Content: fmt.Sprintf("response %d", i)},
|
||||||
backend.Message{Role: "user", Content: fmt.Sprintf("follow up %d", i)},
|
backend.Message{Role: "user", Content: fmt.Sprintf("follow up %d", i)},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
c.history.messages = append(c.history.messages,
|
c.r.history.messages = append(c.r.history.messages,
|
||||||
backend.Message{
|
backend.Message{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: "calling tool",
|
Content: "calling tool",
|
||||||
|
|
@ -2018,7 +2018,7 @@ func TestHookTimeout_Branch(t *testing.T) {
|
||||||
func TestCommand_Backend_Switch(t *testing.T) {
|
func TestCommand_Backend_Switch(t *testing.T) {
|
||||||
c := newCore(t, nil, nil)
|
c := newCore(t, nil, nil)
|
||||||
newBE := &mockBackend{name: "injected", model: "new-model"}
|
newBE := &mockBackend{name: "injected", model: "new-model"}
|
||||||
c.newBackend = func(string) (backend.Backend, error) { return newBE, nil }
|
c.r.newBackend = func(string) (backend.Backend, error) { return newBE, nil }
|
||||||
|
|
||||||
evs := collectEvents(context.Background(), c, "/backend other")
|
evs := collectEvents(context.Background(), c, "/backend other")
|
||||||
infos := byRole(evs, "info")
|
infos := byRole(evs, "info")
|
||||||
|
|
@ -2031,14 +2031,14 @@ func TestCommand_Backend_Switch(t *testing.T) {
|
||||||
if !found {
|
if !found {
|
||||||
t.Errorf("/backend switch: expected 'injected' in info events; got %v", infos)
|
t.Errorf("/backend switch: expected 'injected' in info events; got %v", infos)
|
||||||
}
|
}
|
||||||
if c.runtime.Backend != newBE {
|
if c.r.runtime.Backend != newBE {
|
||||||
t.Errorf("/backend switch: backend not updated")
|
t.Errorf("/backend switch: backend not updated")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCommand_Backend_Error(t *testing.T) {
|
func TestCommand_Backend_Error(t *testing.T) {
|
||||||
c := newCore(t, nil, nil)
|
c := newCore(t, nil, nil)
|
||||||
c.newBackend = func(string) (backend.Backend, error) { return nil, fmt.Errorf("no such backend") }
|
c.r.newBackend = func(string) (backend.Backend, error) { return nil, fmt.Errorf("no such backend") }
|
||||||
|
|
||||||
evs := collectEvents(context.Background(), c, "/backend bad")
|
evs := collectEvents(context.Background(), c, "/backend bad")
|
||||||
infos := byRole(evs, "info")
|
infos := byRole(evs, "info")
|
||||||
|
|
@ -2187,21 +2187,21 @@ func TestRestoreSession_RoundTrip(t *testing.T) {
|
||||||
|
|
||||||
func TestReactionTargetsResponseAndReplaces(t *testing.T) {
|
func TestReactionTargetsResponseAndReplaces(t *testing.T) {
|
||||||
c := newCore(t, nil, nil)
|
c := newCore(t, nil, nil)
|
||||||
c.history = &History{messages: []backend.Message{
|
c.r.history = &History{messages: []backend.Message{
|
||||||
{Role: "assistant", ID: "r1", Content: "first"},
|
{Role: "assistant", ID: "r1", Content: "first"},
|
||||||
{Role: "assistant", ID: "r2", Content: "second"},
|
{Role: "assistant", ID: "r2", Content: "second"},
|
||||||
}}
|
}}
|
||||||
if err := c.ReactTo("r1", "👍"); err != nil {
|
if err := c.ReactTo("r1", "👍"); err != nil {
|
||||||
t.Fatalf("ReactTo: %v", err)
|
t.Fatalf("ReactTo: %v", err)
|
||||||
}
|
}
|
||||||
if len(c.history.Reactions) != 1 || c.history.Reactions[0].ResponseID != "r1" || c.history.PositiveReactions != 1 {
|
if len(c.r.history.Reactions) != 1 || c.r.history.Reactions[0].ResponseID != "r1" || c.r.history.PositiveReactions != 1 {
|
||||||
t.Fatalf("reaction state = %+v positive=%d", c.history.Reactions, c.history.PositiveReactions)
|
t.Fatalf("reaction state = %+v positive=%d", c.r.history.Reactions, c.r.history.PositiveReactions)
|
||||||
}
|
}
|
||||||
if err := c.ReactTo("r1", "👎"); err != nil {
|
if err := c.ReactTo("r1", "👎"); err != nil {
|
||||||
t.Fatalf("replace ReactTo: %v", err)
|
t.Fatalf("replace ReactTo: %v", err)
|
||||||
}
|
}
|
||||||
if len(c.history.Reactions) != 1 || c.history.PositiveReactions != 0 || c.history.NegativeReactions != 1 {
|
if len(c.r.history.Reactions) != 1 || c.r.history.PositiveReactions != 0 || c.r.history.NegativeReactions != 1 {
|
||||||
t.Fatalf("replaced reaction state = %+v positive=%d negative=%d", c.history.Reactions, c.history.PositiveReactions, c.history.NegativeReactions)
|
t.Fatalf("replaced reaction state = %+v positive=%d negative=%d", c.r.history.Reactions, c.r.history.PositiveReactions, c.r.history.NegativeReactions)
|
||||||
}
|
}
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
if got := ctx[len(ctx)-1].Content; !strings.Contains(got, "response r1: negative") {
|
if got := ctx[len(ctx)-1].Content; !strings.Contains(got, "response r1: negative") {
|
||||||
|
|
@ -2323,7 +2323,7 @@ func TestSetEnv_StoredInCore(t *testing.T) {
|
||||||
|
|
||||||
func TestSetEnv_NilDispatcher_NoPanic(t *testing.T) {
|
func TestSetEnv_NilDispatcher_NoPanic(t *testing.T) {
|
||||||
c := newCore(t, nil, nil)
|
c := newCore(t, nil, nil)
|
||||||
c.runtime.Dispatcher = nil
|
c.r.runtime.Dispatcher = nil
|
||||||
// Must not panic.
|
// Must not panic.
|
||||||
c.SetEnv("K", "V")
|
c.SetEnv("K", "V")
|
||||||
env := c.sess.Env()
|
env := c.sess.Env()
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ func TestResultCache_HitSkipsExec(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.ClassifyTool = func(string) bool { return true }
|
c.r.runtime.ClassifyTool = func(string) bool { return true }
|
||||||
c.runtime.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
atomic.AddInt32(&execCount, 1)
|
atomic.AddInt32(&execCount, 1)
|
||||||
return "contents of a.txt", nil, nil
|
return "contents of a.txt", nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -53,8 +53,8 @@ func TestResultCache_DifferentArgsMiss(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.ClassifyTool = func(string) bool { return true }
|
c.r.runtime.ClassifyTool = func(string) bool { return true }
|
||||||
c.runtime.Exec = func(_ context.Context, name string, args json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, name string, args json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
atomic.AddInt32(&execCount, 1)
|
atomic.AddInt32(&execCount, 1)
|
||||||
return "result for " + string(args), nil, nil
|
return "result for " + string(args), nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -77,8 +77,8 @@ func TestResultCache_SerialToolNotCached(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.ClassifyTool = func(name string) bool { return false } // all serial
|
c.r.runtime.ClassifyTool = func(name string) bool { return false } // all serial
|
||||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
atomic.AddInt32(&execCount, 1)
|
atomic.AddInt32(&execCount, 1)
|
||||||
return "ok", nil, nil
|
return "ok", nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -122,8 +122,8 @@ func TestResultCache_ErrorNotCached(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.ClassifyTool = func(string) bool { return true }
|
c.r.runtime.ClassifyTool = func(string) bool { return true }
|
||||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
n := atomic.AddInt32(&execCount, 1)
|
n := atomic.AddInt32(&execCount, 1)
|
||||||
if n == 1 {
|
if n == 1 {
|
||||||
return "", nil, &mockErr{"transient failure"}
|
return "", nil, &mockErr{"transient failure"}
|
||||||
|
|
@ -157,8 +157,8 @@ func TestResultCache_IdenticalParallelReads(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.ClassifyTool = func(string) bool { return true } // both parallel-safe → same batch
|
c.r.runtime.ClassifyTool = func(string) bool { return true } // both parallel-safe → same batch
|
||||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
atomic.AddInt32(&execCount, 1)
|
atomic.AddInt32(&execCount, 1)
|
||||||
return "file contents", nil, nil
|
return "file contents", nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ func TestConsecutiveErrors_HardLimit(t *testing.T) {
|
||||||
be.respond = alwaysFailStream()
|
be.respond = alwaysFailStream()
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
return "", nil, fmt.Errorf("always fails")
|
return "", nil, fmt.Errorf("always fails")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,7 +92,7 @@ func TestConsecutiveErrors_SoftLimitNudge(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
return "", nil, fmt.Errorf("always fails")
|
return "", nil, fmt.Errorf("always fails")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,7 +148,7 @@ func TestConsecutiveErrors_ResetOnSuccess(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
if name == "good_tool" {
|
if name == "good_tool" {
|
||||||
return "ok", nil, nil
|
return "ok", nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ func TestParallel_ConcurrentExecution(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.ClassifyTool = func(string) bool { return true }
|
c.r.runtime.ClassifyTool = func(string) bool { return true }
|
||||||
c.runtime.Exec = func(ctx context.Context, name string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(ctx context.Context, name string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
started <- struct{}{}
|
started <- struct{}{}
|
||||||
select {
|
select {
|
||||||
case <-gate:
|
case <-gate:
|
||||||
|
|
@ -92,10 +92,10 @@ func TestParallel_SerialToolBreaksBatch(t *testing.T) {
|
||||||
var order []string
|
var order []string
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.ClassifyTool = func(name string) bool {
|
c.r.runtime.ClassifyTool = func(name string) bool {
|
||||||
return name == "read_a" || name == "read_c"
|
return name == "read_a" || name == "read_c"
|
||||||
}
|
}
|
||||||
c.runtime.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
order = append(order, name)
|
order = append(order, name)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
@ -133,10 +133,10 @@ func TestParallel_CancellationFillsRemaining(t *testing.T) {
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
// tool_a and tool_b are parallel-safe; tool_c is serial.
|
// tool_a and tool_b are parallel-safe; tool_c is serial.
|
||||||
c.runtime.ClassifyTool = func(name string) bool {
|
c.r.runtime.ClassifyTool = func(name string) bool {
|
||||||
return name == "tool_a" || name == "tool_b"
|
return name == "tool_a" || name == "tool_b"
|
||||||
}
|
}
|
||||||
c.runtime.Exec = func(execCtx context.Context, name string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(execCtx context.Context, name string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
cancel() // cancel on first execution; propagates to all
|
cancel() // cancel on first execution; propagates to all
|
||||||
<-execCtx.Done()
|
<-execCtx.Done()
|
||||||
return "", nil, execCtx.Err()
|
return "", nil, execCtx.Err()
|
||||||
|
|
@ -176,8 +176,8 @@ func TestParallel_NilClassifyToolIsSerial(t *testing.T) {
|
||||||
var order []string
|
var order []string
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.ClassifyTool = nil // no classifier → all serial
|
c.r.runtime.ClassifyTool = nil // no classifier → all serial
|
||||||
c.runtime.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, name string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
order = append(order, name)
|
order = append(order, name)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ func TestTruncation_LargeResultTruncated(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
return large, nil, nil
|
return large, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ func TestTruncation_SmallResultNotTruncated(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
return "short result", nil, nil
|
return "short result", nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ func TestTruncation_ErrorTruncated(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
return "", nil, &mockErr{longErr}
|
return "", nil, &mockErr{longErr}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,8 +107,8 @@ func TestTruncation_CachedResultAlreadyTruncated(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
c := newCore(t, be, nil)
|
c := newCore(t, be, nil)
|
||||||
c.runtime.ClassifyTool = func(string) bool { return true } // read-safe → cacheable
|
c.r.runtime.ClassifyTool = func(string) bool { return true } // read-safe → cacheable
|
||||||
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
c.r.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
|
||||||
return large, nil, nil
|
return large, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"ollie/backend"
|
||||||
|
"ollie/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
// reasoning holds the state of the current reasoning entity (the "agent"
|
||||||
|
// in the traditional sense). It is swappable: when the user runs /agent,
|
||||||
|
// a new reasoning is built from the new agent config while the session
|
||||||
|
// host remains stable.
|
||||||
|
type reasoning struct {
|
||||||
|
history *History
|
||||||
|
runtime *Runtime
|
||||||
|
cfg agentConfig // per-turn config built from runtime
|
||||||
|
agentName string
|
||||||
|
agentsDir string
|
||||||
|
baseLayers []string // system prompt layers for /agent reloads
|
||||||
|
promptEnvExtra []string // PRIME_* vars for prompt resolution
|
||||||
|
newDispatcher func() tools.Dispatcher
|
||||||
|
newBackend func(string) (backend.Backend, error)
|
||||||
|
currentAction atomic.Pointer[actionHandle]
|
||||||
|
warnedContext bool
|
||||||
|
resultCache sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend returns the active backend from the runtime.
|
||||||
|
func (r *reasoning) Backend() backend.Backend {
|
||||||
|
if r.runtime == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.runtime.Backend
|
||||||
|
}
|
||||||
Reference in New Issue