execute, agent: thread workdir through Decl and AgentCoreConfig

execute.Decl is now a function (func(workdir string) func() tools.Server)
rather than a var, making workdir an explicit dependency rather than
an implicit os.Getwd() call. execute.Server stores the workdir and
sets cmd.Dir accordingly; falls back to os.Getwd() if empty.

AgentCoreConfig gains WorkDir string, threaded into BuildAgentEnv and
the system prompt. The /agent switch uses s.workdir so the correct
directory is preserved across agent switches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-10 09:04:19 +02:00
parent 5afe380437
commit 23913d0d76
3 changed files with 29 additions and 14 deletions

View File

@ -119,14 +119,16 @@ func buildFirstPrompt(input string) string {
}
// systemPrompt builds the full system prompt for a given tool set.
func systemPrompt(allTools []backend.Tool) string {
cwd, _ := os.Getwd()
func systemPrompt(allTools []backend.Tool, workdir string) string {
if workdir == "" {
workdir, _ = os.Getwd()
}
now := time.Now().Format("2006-01-02 15:04:05 MST")
names := make([]string, len(allTools))
for i, t := range allTools {
names[i] = t.Name
}
return systemPromptBase + "\n\nWorking directory: " + cwd +
return systemPromptBase + "\n\nWorking directory: " + workdir +
"\nCurrent time: " + now +
"\nAvailable tools: " + strings.Join(names, ", ")
}
@ -144,9 +146,10 @@ type AgentEnv struct {
}
// BuildAgentEnv constructs an AgentEnv from a pre-configured Dispatcher and
// optional agent config. The caller is responsible for registering all servers
// on d before calling this.
func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher) AgentEnv {
// optional agent config. workdir sets the working directory reported in the
// system prompt; if empty, the process working directory is used.
// The caller is responsible for registering all servers on d before calling this.
func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher, workdir string) AgentEnv {
var messages []string
if cfg != nil {
@ -193,9 +196,9 @@ func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher) AgentEnv {
}
}
sp := systemPrompt(allTools)
sp := systemPrompt(allTools, workdir)
if agentPrompt != "" {
sp = systemPrompt(allTools) + "\n\n" + agentPrompt
sp = systemPrompt(allTools, workdir) + "\n\n" + agentPrompt
}
overhead := len(sp)
@ -281,6 +284,7 @@ type AgentCoreConfig struct {
AgentsDir string
SessionsDir string
SessionID string
WorkDir string // working directory for tool execution and system prompt
Session *Session
Env AgentEnv
NewDispatcher func() tools.Dispatcher
@ -298,6 +302,7 @@ type agentCore struct {
sessionID string
dispatcher tools.Dispatcher
newDispatcher func() tools.Dispatcher
workdir string
ctxOverhead int
currentAction atomic.Pointer[actionHandle]
}
@ -325,6 +330,7 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
agentsDir: cfg.AgentsDir,
sessionsDir: cfg.SessionsDir,
sessionID: cfg.SessionID,
workdir: cfg.WorkDir,
dispatcher: cfg.Env.dispatcher,
newDispatcher: cfg.NewDispatcher,
ctxOverhead: cfg.Env.CtxOverhead,
@ -500,7 +506,7 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve
s.dispatcher.Close()
}
d := s.newDispatcher()
env := BuildAgentEnv(cfg, d)
env := BuildAgentEnv(cfg, d, s.workdir)
s.dispatcher = env.dispatcher
s.hooks = env.Hooks
s.loopcfg.systemPrompt = env.systemPrompt

View File

@ -24,6 +24,10 @@ type Server struct {
// If it returns false, the operation is denied.
Confirm func(string) bool
// workdir is the working directory for sandboxed commands. If empty,
// the process working directory is used.
workdir string
// rate limiting state (per-Server)
rateLimitMu sync.Mutex
validationFailures int
@ -31,8 +35,8 @@ type Server struct {
blockedUntil time.Time
}
// New creates a new Server.
func New() *Server { return &Server{} }
// New creates a new Server with the given working directory.
func New(workdir string) *Server { return &Server{workdir: workdir} }
var dangerousPatterns = []*regexp.Regexp{
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s*/(home|var|usr|etc|boot|root|bin|sbin|lib|opt|srv)?`),
@ -158,7 +162,10 @@ func (e *Server) Execute(ctx context.Context, code, language string, timeout int
return "", err
}
workDir, _ := os.Getwd()
workDir := e.workdir
if workDir == "" {
workDir, _ = os.Getwd()
}
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
defer cancel()

View File

@ -7,8 +7,10 @@ import (
"ollie/pkg/tools"
)
// Decl is the default factory for an execute Server. Reassign to override.
var Decl func() tools.Server = func() tools.Server { return New() }
// Decl returns a factory for an execute Server with the given working directory.
func Decl(workdir string) func() tools.Server {
return func() tools.Server { return New(workdir) }
}
// ListTools implements tools.Server.
func (e *Server) ListTools() ([]tools.ToolInfo, error) {