persist active sessions under active directory

This commit is contained in:
ollie 2026-07-26 12:10:11 +02:00
parent 2202014397
commit 95557b35ab
3 changed files with 133 additions and 106 deletions

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
@ -226,8 +227,10 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
return
}
if s.sessionsDir != "" && s.sessionID != "" {
histPath := s.sessionsDir + "/" + s.sessionID + ".compaction.jsonl"
if data, err := json.Marshal(snapshot); err == nil {
histPath := s.activeSessionPath(s.sessionID, ".compaction.jsonl")
if err := os.MkdirAll(filepath.Dir(histPath), 0700); err != nil {
s.emit(infoEvent("compaction history save: " + err.Error()))
} else if data, err := json.Marshal(snapshot); err == nil {
f, err := os.OpenFile(histPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err == nil {
f.Write(append(data, '\n')) //nolint:errcheck
@ -309,20 +312,34 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
"/sessions": func(args []string) {
allFlag := len(args) > 0 && args[0] == "-a"
entries, err := os.ReadDir(s.sessionsDir)
if err != nil {
s.emit(infoEvent(fmt.Sprintf("sessions: %v", err)))
return
type sessionFile struct {
id string
path string
}
var files []sessionFile
appendSessionFiles := func(dir string) {
entries, err := os.ReadDir(dir)
if err != nil {
return
}
for i := len(entries) - 1; i >= 0; i-- {
e := entries[i]
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
continue
}
files = append(files, sessionFile{
id: strings.TrimSuffix(e.Name(), ".json"),
path: filepath.Join(dir, e.Name()),
})
}
}
appendSessionFiles(s.sessionsDir)
appendSessionFiles(filepath.Join(s.sessionsDir, "active"))
cwd := s.CWD()
found := false
for i := len(entries) - 1; i >= 0; i-- {
e := entries[i]
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
continue
}
id := strings.TrimSuffix(e.Name(), ".json")
data, readErr := os.ReadFile(s.sessionsDir + "/" + e.Name())
for _, file := range files {
data, readErr := os.ReadFile(file.path)
if readErr != nil {
continue
}
@ -334,7 +351,7 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
continue
}
marker := " "
if id == s.sessionID {
if file.id == s.sessionID {
marker = "* "
}
goal := ""
@ -347,7 +364,7 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
if len(goal) > 60 {
goal = goal[:60] + "..."
}
s.emit(infoEvent(marker + fmt.Sprintf("%-24s [%s] %q", id, ps.Agent, goal)))
s.emit(infoEvent(marker + fmt.Sprintf("%-24s [%s] %q", file.id, ps.Agent, goal)))
found = true
}
if !found {

View File

@ -31,8 +31,6 @@ import (
// with other read-class tools. nil means treat all tools as serial.
type toolClassifier func(name string) bool
// 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.
@ -105,21 +103,21 @@ func BuildRuntime(cfg *config.Config, d tools.Dispatcher, cwd string, env []stri
}
}
exec := func(ctx context.Context, name string, args json.RawMessage) (string, []backend.ContentBlock, error) {
server, ok := serverOf[name]
if !ok {
return "", nil, fmt.Errorf("unknown tool: %s", name)
}
raw, err := d.Dispatch(ctx, server, name, args)
if err != nil {
return "", nil, err
}
text, blocks, isErr := extractToolResult(raw)
if isErr {
return "", nil, fmt.Errorf("%s", text)
}
return text, blocks, nil
exec := func(ctx context.Context, name string, args json.RawMessage) (string, []backend.ContentBlock, error) {
server, ok := serverOf[name]
if !ok {
return "", nil, fmt.Errorf("unknown tool: %s", name)
}
raw, err := d.Dispatch(ctx, server, name, args)
if err != nil {
return "", nil, err
}
text, blocks, isErr := extractToolResult(raw)
if isErr {
return "", nil, fmt.Errorf("%s", text)
}
return text, blocks, nil
}
var classify toolClassifier
if srv, ok := d.GetServer("execute"); ok {
@ -196,7 +194,6 @@ func BuildRuntime(cfg *config.Config, d tools.Dispatcher, cwd string, env []stri
}
}
// DefaultPromptsDir returns the default directory for prompt templates.
func DefaultPromptsDir() string {
return paths.CfgDir() + "/prompts"
@ -218,7 +215,6 @@ func AgentsDirs() []string {
return []string{paths.CfgDir() + "/agents"}
}
// AgentConfigPath resolves the config file path for a named agent.
// It searches all directories in OLLIE_AGENTS_PATH, falling back to agentsDir.
func AgentConfigPath(agentsDir, name string) string {
@ -240,13 +236,11 @@ func NewSessionID() string {
return strconv.FormatInt(time.Now().UnixNano(), 10) + "-" + fmt.Sprintf("%06x", b)
}
// infoEvent wraps a plain-text message as an info Event.
func infoEvent(text string) Event {
return Event{Role: "info", Content: text + "\n"}
}
// actionHandle holds the cancel function for the current agent turn.
type actionHandle struct {
cancel context.CancelCauseFunc
@ -266,13 +260,13 @@ type AgentCoreConfig struct {
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
Log *olog.Logger // if nil, logging is disabled
// MaxSteps overrides the agent JSON maxSteps when non-zero.
// 0 means use the value from the agent config (or unlimited if absent).
MaxSteps int
MaxSteps int
// ReadPlanStep, if non-nil, is called to get the next unchecked plan step.
// When provided, it is used directly instead of reading from the filesystem.
ReadPlanStep func() string
ReadPlanStep func() string
// ListHandlers provides harness-specific list implementations for slash
// commands such as /skills, /tools, /agents. Keyed by command name without
// the leading slash. Commands with no entry emit nothing.
@ -294,7 +288,7 @@ type AgentCoreConfig struct {
type agent struct {
session *Session
runtime *Runtime
cfg agentConfig // per-turn config built from runtime; set in executeTurn
cfg agentConfig // per-turn config built from runtime; set in executeTurn
log *olog.Logger
agentName string
agentsDir string
@ -310,7 +304,7 @@ type agent struct {
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
fifo PromptFIFO
@ -328,9 +322,9 @@ type agent struct {
auditLog *olog.Logger
// Debounced session persistence
saveMu sync.Mutex
saveDirty bool
saveTimer *time.Timer
saveMu sync.Mutex
saveDirty bool
saveTimer *time.Timer
// Cross-turn tool result cache (read-safe tools only)
resultCache sync.Map
@ -549,11 +543,21 @@ func (s *agent) ListDetached() []DetachedInfo {
for _, r := range raw {
if m, ok := r.(map[string]any); ok {
di := DetachedInfo{}
if v, ok := m["pid"].(int); ok { di.PID = v }
if v, ok := m["command"].(string); ok { di.Command = v }
if v, ok := m["started"].(int64); ok { di.Started = v }
if v, ok := m["exited"].(bool); ok { di.Exited = v }
if v, ok := m["exit_code"].(int); ok { di.ExitCode = v }
if v, ok := m["pid"].(int); ok {
di.PID = v
}
if v, ok := m["command"].(string); ok {
di.Command = v
}
if v, ok := m["started"].(int64); ok {
di.Started = v
}
if v, ok := m["exited"].(bool); ok {
di.Exited = v
}
if v, ok := m["exit_code"].(int); ok {
di.ExitCode = v
}
out = append(out, di)
}
}
@ -730,12 +734,12 @@ func (s *agent) SetSessionID(newID string) error {
if oldID == newID {
return nil
}
// Rename persisted files on disk.
// Rename active persisted files on disk.
if s.sessionsDir != "" && oldID != "" {
for _, suffix := range []string{".json", ".compaction.jsonl"} {
oldPath := s.sessionsDir + "/" + oldID + suffix
oldPath := s.activeSessionPath(oldID, suffix)
if _, err := os.Stat(oldPath); err == nil {
if err := os.Rename(oldPath, s.sessionsDir+"/"+newID+suffix); err != nil {
if err := os.Rename(oldPath, s.activeSessionPath(newID, suffix)); err != nil {
return fmt.Errorf("rename %s: %w", suffix, err)
}
}
@ -851,6 +855,10 @@ func (s *agent) runCompact(ctx context.Context, trigger string) (int, error) {
return n, nil
}
func (s *agent) activeSessionPath(id, suffix string) string {
return filepath.Join(s.sessionsDir, "active", id+suffix)
}
func (s *agent) saveSession() {
s.saveMu.Lock()
s.saveDirty = true
@ -876,7 +884,11 @@ func (s *agent) flushSave() {
if s.session == nil || s.sessionID == "" || s.sessionsDir == "" {
return
}
path := s.sessionsDir + "/" + s.sessionID + ".json"
path := s.activeSessionPath(s.sessionID, ".json")
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
s.log.Error("session save: %v", err)
return
}
if err := s.session.saveTo(path, s.sessionID, s.agentName, s.CWD()); err != nil {
s.log.Error("session save: %v", err)
}
@ -1192,7 +1204,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
GenerationParams: s.runtime.GenParams,
MaxSteps: s.runtime.MaxSteps,
ReadPlanStep: s.readPlanStep,
TurnError: s.turnError,
TurnError: s.turnError,
}
var replyBuf strings.Builder
@ -1427,7 +1439,6 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
return ""
}
func toolInfosToBackend(infos []tools.ToolInfo) []backend.Tool {
out := make([]backend.Tool, len(infos))
for i, t := range infos {
@ -1444,10 +1455,10 @@ func extractToolResult(raw json.RawMessage) (text string, contentBlocks []backen
var result struct {
IsError bool `json:"isError"`
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
MediaType string `json:"media_type"`
Data string `json:"data"`
Type string `json:"type"`
Text string `json:"text"`
MediaType string `json:"media_type"`
Data string `json:"data"`
} `json:"content"`
}
if err := json.Unmarshal(raw, &result); err != nil {

View File

@ -94,7 +94,7 @@ func newCore(t *testing.T, be backend.Backend, hooks Hooks) *agent {
hooks = Hooks{}
}
env := &Runtime{
Hooks: hooks,
Hooks: hooks,
Preamble: "test system prompt",
}
c := NewAgentCore(AgentCoreConfig{
@ -184,7 +184,7 @@ func TestSubmit_StateTransitions(t *testing.T) {
unblock := make(chan struct{})
be := defaultBE()
be.respond = func(ctx context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
return blockedStream(ctx, unblock), nil
return blockedStream(ctx, unblock), nil
}
c := newCore(t, be, nil)
@ -222,10 +222,10 @@ func TestSubmit_ToolCallStateTransitions(t *testing.T) {
StopReason: "tool_calls",
}
close(ch)
return ch, nil
return ch, nil
}
return textStream("done"), nil
return textStream("done"), nil
}
@ -234,7 +234,7 @@ return textStream("done"), nil
var stateAtExec string
c.runtime.Exec = func(_ context.Context, _ string, _ json.RawMessage) (string, []backend.ContentBlock, error) {
stateAtExec = c.State()
return `{}`, nil, nil
return `{}`, nil, nil
}
collectEvents(context.Background(), c, "run tool")
@ -255,7 +255,7 @@ func TestSubmit_PreTurnHookBlocks(t *testing.T) {
be := defaultBE()
be.respond = func(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
callCount++
return textStream("should not be called"), nil
return textStream("should not be called"), nil
}
c := newCore(t, be, Hooks{HookPreTurn: []string{"exit 2"}})
@ -285,7 +285,7 @@ func TestSubmit_PreTurnHookContext(t *testing.T) {
lastUserMsg = m.Content
}
}
return textStream("ok"), nil
return textStream("ok"), nil
}
c := newCore(t, be, Hooks{HookPreTurn: []string{`echo "extra context"`}})
@ -308,7 +308,7 @@ func TestSubmit_PostTurnHookContinue(t *testing.T) {
be := defaultBE()
be.respond = func(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
callCount++
return textStream(fmt.Sprintf("response %d", callCount)), nil
return textStream(fmt.Sprintf("response %d", callCount)), nil
}
c := newCore(t, be, Hooks{HookPostTurn: []string{hookScript}})
@ -327,7 +327,7 @@ func TestSubmit_FIFODrain(t *testing.T) {
be := defaultBE()
be.respond = func(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
callCount.Add(1)
return textStream("ok"), nil
return textStream("ok"), nil
}
c := newCore(t, be, nil)
@ -355,7 +355,7 @@ func TestSubmit_PendingInjectAsNextTurn(t *testing.T) {
be := defaultBE()
be.respond = func(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
callCount++
return textStream("ok"), nil
return textStream("ok"), nil
}
c := newCore(t, be, nil)
@ -375,7 +375,7 @@ func TestInterrupt_Running(t *testing.T) {
unblock := make(chan struct{})
be := defaultBE()
be.respond = func(ctx context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
return blockedStream(ctx, unblock), nil
return blockedStream(ctx, unblock), nil
}
c := newCore(t, be, nil)
@ -418,7 +418,7 @@ func TestSubmit_WhileRunning(t *testing.T) {
unblock := make(chan struct{})
be := defaultBE()
be.respond = func(ctx context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
return blockedStream(ctx, unblock), nil
return blockedStream(ctx, unblock), nil
}
c := newCore(t, be, nil)
@ -456,7 +456,7 @@ func TestManualCompact(t *testing.T) {
be.respond = func(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
callCount++
stateAtCompact = c.State()
return textStream("summary or answer"), nil
return textStream("summary or answer"), nil
}
@ -508,10 +508,10 @@ func TestAutoCompact(t *testing.T) {
callCount++
if callCount == 1 {
stateAtCompact = c.State()
return textStream("summary text for compaction"), nil
return textStream("summary text for compaction"), nil
}
return textStream("answer"), nil
return textStream("answer"), nil
}
@ -674,7 +674,7 @@ func TestCommand_I_SetsInject(t *testing.T) {
done := make(chan struct{})
be.respond = func(_ context.Context, msgs []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
<-done
return textStream("ok"), nil
return textStream("ok"), nil
}
c := newCore(t, be, nil)
submitDone := make(chan struct{})
@ -698,7 +698,7 @@ func TestCommand_I_SubmitsWhenIdle(t *testing.T) {
be := defaultBE()
be.respond = func(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
callCount.Add(1)
return textStream("ok"), nil
return textStream("ok"), nil
}
c := newCore(t, be, nil)
c.Submit(context.Background(), "/i my prompt")
@ -742,7 +742,7 @@ func TestCommand_Compact_WhileRunning(t *testing.T) {
unblock := make(chan struct{})
be := defaultBE()
be.respond = func(ctx context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
return blockedStream(ctx, unblock), nil
return blockedStream(ctx, unblock), nil
}
c := newCore(t, be, nil)
@ -811,7 +811,7 @@ func TestCommand_Clear_WhileRunning(t *testing.T) {
unblock := make(chan struct{})
be := defaultBE()
be.respond = func(ctx context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
return blockedStream(ctx, unblock), nil
return blockedStream(ctx, unblock), nil
}
c := newCore(t, be, nil)
@ -893,7 +893,7 @@ func TestRun_StreamInterrupted(t *testing.T) {
be.respond = func(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
ch := make(chan backend.StreamEvent)
close(ch) // close without sending Done=true
return ch, nil
return ch, nil
}
c := newCore(t, be, nil)
@ -917,7 +917,7 @@ func TestRun_UnknownStopReason(t *testing.T) {
ch := make(chan backend.StreamEvent, 1)
ch <- backend.StreamEvent{Done: true, StopReason: "max_completion_tokens", Content: "partial"}
close(ch)
return ch, nil
return ch, nil
}
c := newCore(t, be, nil)
@ -948,10 +948,10 @@ func TestRun_ToolEmptyName(t *testing.T) {
StopReason: "tool_calls",
}
close(ch)
return ch, nil
return ch, nil
}
return textStream("done"), nil
return textStream("done"), nil
}
c := newCore(t, be, nil)
@ -979,10 +979,10 @@ func TestRun_NoExec(t *testing.T) {
StopReason: "tool_calls",
}
close(ch)
return ch, nil
return ch, nil
}
return textStream("done"), nil
return textStream("done"), nil
}
c := newCore(t, be, nil)
@ -1000,7 +1000,7 @@ func TestHook_NonZeroExitCode(t *testing.T) {
be := defaultBE()
be.respond = func(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
callCount++
return textStream("ok"), nil
return textStream("ok"), nil
}
// exit 1 is a non-blocking warning: turn should still proceed.
c := newCore(t, be, Hooks{HookPreTurn: []string{"exit 1"}})
@ -1040,7 +1040,7 @@ func TestCommand_Backend_WhileRunning(t *testing.T) {
unblock := make(chan struct{})
be := defaultBE()
be.respond = func(ctx context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
return blockedStream(ctx, unblock), nil
return blockedStream(ctx, unblock), nil
}
c := newCore(t, be, nil)
@ -1097,7 +1097,7 @@ func TestCommand_Model_WhileRunning(t *testing.T) {
unblock := make(chan struct{})
be := defaultBE()
be.respond = func(ctx context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
return blockedStream(ctx, unblock), nil
return blockedStream(ctx, unblock), nil
}
c := newCore(t, be, nil)
@ -1180,7 +1180,7 @@ func TestCommand_Agent_WhileRunning(t *testing.T) {
unblock := make(chan struct{})
be := defaultBE()
be.respond = func(ctx context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
return blockedStream(ctx, unblock), nil
return blockedStream(ctx, unblock), nil
}
c := newCore(t, be, nil)
@ -1507,7 +1507,7 @@ func TestRun_RateLimitRetry(t *testing.T) {
if callCount == 1 {
return nil, &backend.RateLimitError{RetryAfter: time.Millisecond}
}
return textStream("ok"), nil
return textStream("ok"), nil
}
c := newCore(t, be, nil)
var retryEvents []Event
@ -1543,7 +1543,7 @@ func TestRun_ToolCancelledBeforeExec(t *testing.T) {
StopReason: "tool_calls",
}
close(ch)
return ch, nil
return ch, nil
}
c := newCore(t, be, nil)
@ -1589,7 +1589,7 @@ func TestRun_ExecCancelledWithInject(t *testing.T) {
StopReason: "tool_calls",
}
close(ch)
return ch, nil
return ch, nil
}
c := newCore(t, be, nil)
@ -1620,10 +1620,10 @@ func TestAutoCompact_WithHookContext(t *testing.T) {
be.respond = func(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
callCount++
if callCount == 1 {
return textStream("summary text for compaction"), nil
return textStream("summary text for compaction"), nil
}
return textStream("answer"), nil
return textStream("answer"), nil
}
c.session = newSession("goal")
@ -1691,7 +1691,7 @@ func TestCore_SetGenerationParams_WhileRunning(t *testing.T) {
unblock := make(chan struct{})
be := defaultBE()
be.respond = func(ctx context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
return blockedStream(ctx, unblock), nil
return blockedStream(ctx, unblock), nil
}
c := newCore(t, be, nil)
@ -1767,11 +1767,11 @@ func TestSetSessionID_Rename(t *testing.T) {
if c.sessionID != newID {
t.Errorf("sessionID = %q; want %q", c.sessionID, newID)
}
if _, err := os.Stat(c.sessionsDir + "/" + oldID + ".json"); !os.IsNotExist(err) {
t.Errorf("old session file still exists after rename; err=%v", err)
if _, err := os.Stat(filepath.Join(c.sessionsDir, "active", oldID+".json")); !os.IsNotExist(err) {
t.Errorf("old active session file still exists after rename; err=%v", err)
}
if _, err := os.Stat(c.sessionsDir + "/" + newID + ".json"); err != nil {
t.Errorf("new session file not found after rename: %v", err)
if _, err := os.Stat(filepath.Join(c.sessionsDir, "active", newID+".json")); err != nil {
t.Errorf("new active session file not found after rename: %v", err)
}
}
@ -1919,7 +1919,7 @@ func TestManualCompact_EmptySummary(t *testing.T) {
ch := make(chan backend.StreamEvent, 1)
ch <- backend.StreamEvent{Done: true, StopReason: "stop", Content: " "}
close(ch)
return ch, nil
return ch, nil
}
c.session = newSession("goal")
@ -1947,7 +1947,7 @@ func TestManualCompact_WithToolMessages(t *testing.T) {
be := defaultBE()
c := newCore(t, be, nil)
be.respond = func(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
return textStream("summary"), nil
return textStream("summary"), nil
}
c.session = newSession("goal")
@ -2206,7 +2206,7 @@ type mockEnvServer struct {
env map[string]string
}
func (m *mockEnvServer) ListTools() ([]tools.ToolInfo, error) { return nil, nil }
func (m *mockEnvServer) ListTools() ([]tools.ToolInfo, error) { return nil, nil }
func (m *mockEnvServer) CallTool(_ context.Context, _ string, _ json.RawMessage) (json.RawMessage, error) {
return nil, fmt.Errorf("not implemented")
}
@ -2230,9 +2230,9 @@ func newCoreWithExecServer(t *testing.T, srv *mockEnvServer) *agent {
d := tools.NewDispatcher()
d.AddServer("execute", srv)
env := &Runtime{
Hooks: Hooks{},
Preamble: "test system prompt",
Dispatcher: d,
Hooks: Hooks{},
Preamble: "test system prompt",
Dispatcher: d,
}
c := NewAgentCore(AgentCoreConfig{
Backend: defaultBE(),
@ -2276,7 +2276,6 @@ func TestSetEnv_StoredInCore(t *testing.T) {
}
}
func TestSetEnv_NilDispatcher_NoPanic(t *testing.T) {
c := newCore(t, nil, nil)
c.runtime.Dispatcher = nil
@ -2502,7 +2501,7 @@ func TestBuildRuntime_ExecDispatchSuccess(t *testing.T) {
d := &mockDispatcher{
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool"}},
dispatch: func(ctx context.Context, server, tool string, args json.RawMessage) (json.RawMessage, error) {
return json.RawMessage(`{"content":[{"type":"text","text":"ok"}]}`), nil
return json.RawMessage(`{"content":[{"type":"text","text":"ok"}]}`), nil
},
}
env := BuildRuntime(nil, d, t.TempDir(), nil)
@ -2535,7 +2534,7 @@ func TestBuildRuntime_ExecToolResultIsError(t *testing.T) {
d := &mockDispatcher{
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool"}},
dispatch: func(ctx context.Context, server, tool string, args json.RawMessage) (json.RawMessage, error) {
return json.RawMessage(`{"isError":true,"content":[{"type":"text","text":"bad thing"}]}`), nil
return json.RawMessage(`{"isError":true,"content":[{"type":"text","text":"bad thing"}]}`), nil
},
}
env := BuildRuntime(nil, d, t.TempDir(), nil)