diff --git a/pkg/agent/core.go b/pkg/agent/core.go index 980a38f..9201573 100644 --- a/pkg/agent/core.go +++ b/pkg/agent/core.go @@ -52,6 +52,7 @@ type AgentEnv struct { exec toolExecutor Hooks Hooks systemPrompt string + agentPrompt string // agent-specific suffix appended after the base system prompt genParams backend.GenerationParams Messages []string } @@ -140,6 +141,7 @@ func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher, workdir string) Agent exec: exec, Hooks: hooks, systemPrompt: sp, + agentPrompt: agentPrompt, genParams: genParams, Messages: messages, } @@ -224,6 +226,7 @@ type agentCore struct { dispatcher tools.Dispatcher newDispatcher func() tools.Dispatcher workdir string + agentPrompt string // agent-specific prompt suffix; kept for system prompt rebuilds currentAction atomic.Pointer[actionHandle] fifo PromptFIFO pendingInject atomic.Pointer[string] @@ -253,6 +256,7 @@ func NewAgentCore(cfg AgentCoreConfig) Core { sessionsDir: cfg.SessionsDir, sessionID: cfg.SessionID, workdir: cfg.WorkDir, + agentPrompt: cfg.Env.agentPrompt, dispatcher: cfg.Env.dispatcher, newDispatcher: cfg.NewDispatcher, } @@ -265,6 +269,41 @@ func (s *agentCore) prompt() string { // Prompt returns the display prompt string for the current session state. func (s *agentCore) Prompt() string { return s.prompt() } +// WorkDir returns the current working directory for tool execution. +func (s *agentCore) WorkDir() string { + if s.workdir != "" { + return s.workdir + } + wd, _ := os.Getwd() + return wd +} + +// SetWorkDir changes the working directory for tool execution and updates the +// system prompt. Returns an error if the path does not exist. +func (s *agentCore) SetWorkDir(dir string) error { + if dir != "" { + if _, err := os.Stat(dir); err != nil { + return fmt.Errorf("workdir: %w", err) + } + } + s.workdir = dir + // Propagate to any tool server that knows how to handle it (e.g. execute). + if s.dispatcher != nil { + if srv, ok := s.dispatcher.GetServer("execute"); ok { + if ws, ok := srv.(tools.WorkDirSetter); ok { + ws.SetWorkDir(dir) + } + } + } + // Rebuild system prompt so it reflects the new workdir. + sp := systemPrompt(dir) + if s.agentPrompt != "" { + sp += "\n\n" + s.agentPrompt + } + s.loopcfg.systemPrompt = sp + return nil +} + // defaultContextLength is used when the backend cannot report the model's // actual context window (e.g. CodeWhisperer). 128k tokens is a safe default // for modern models. @@ -617,6 +656,7 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve s.loopcfg.Tools = env.tools s.loopcfg.Exec = env.exec s.loopcfg.GenerationParams = env.genParams + s.agentPrompt = env.agentPrompt s.agentName = name s.session = nil s.sessionID = NewSessionID() @@ -745,6 +785,19 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve } return true + case "/cwd": + if len(args) == 0 { + handler(infoEvent("workdir: " + s.WorkDir())) + return true + } + dir := strings.Join(args, " ") + if err := s.SetWorkDir(dir); err != nil { + handler(infoEvent("error: " + err.Error())) + return true + } + handler(infoEvent("workdir: " + dir)) + return true + case "/mcp": handler(infoEvent(s.ListServers())) return true @@ -759,6 +812,7 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve " /model - switch model", " /models - list available models", " /mcp - list registered tool servers and their tools", + " /cwd [path] - show or change working directory", " /queued [pop|clear] - manage queued prompts", " /compact - summarize conversation and compact context", " /context - show context window debug info", diff --git a/pkg/agent/types.go b/pkg/agent/types.go index 2b64876..cf2ddf1 100644 --- a/pkg/agent/types.go +++ b/pkg/agent/types.go @@ -60,6 +60,13 @@ type Core interface { // ListServers returns all registered tool servers and their tools, // grouped by server name. ListServers() string + + // WorkDir returns the current working directory used for tool execution. + WorkDir() string + + // SetWorkDir changes the working directory for tool execution and + // updates the system prompt. Returns an error if the path does not exist. + SetWorkDir(dir string) error } // PromptFIFO is a simple thread-safe FIFO for queued prompts. diff --git a/pkg/tools/execute/executor.go b/pkg/tools/execute/executor.go index 97ea93f..a8b445a 100644 --- a/pkg/tools/execute/executor.go +++ b/pkg/tools/execute/executor.go @@ -26,6 +26,7 @@ type Server struct { // workdir is the working directory for sandboxed commands. If empty, // the process working directory is used. + wdMu sync.RWMutex workdir string // rate limiting state (per-Server) @@ -38,6 +39,13 @@ type Server struct { // New creates a new Server with the given working directory. func New(workdir string) *Server { return &Server{workdir: workdir} } +// SetWorkDir updates the working directory used for subsequent command executions. +func (e *Server) SetWorkDir(dir string) { + e.wdMu.Lock() + e.workdir = dir + e.wdMu.Unlock() +} + 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)?`), regexp.MustCompile(`rm\s+(-[a-z]*f[a-z]*\s+)*-[a-z]*r[a-z]*\s*/(home|var|usr|etc|boot|root|bin|sbin|lib|opt|srv)?`), @@ -162,7 +170,9 @@ func (e *Server) Execute(ctx context.Context, code, language string, timeout int return "", err } + e.wdMu.RLock() workDir := e.workdir + e.wdMu.RUnlock() if workDir == "" { workDir, _ = os.Getwd() } diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go index b3badee..e99a370 100644 --- a/pkg/tools/tools.go +++ b/pkg/tools/tools.go @@ -119,6 +119,12 @@ type PlanBackendSetter interface { SetPlanBackend(PlanBackend) } +// WorkDirSetter is implemented by tool servers that accept a dynamic working +// directory. SetWorkDir updates the directory used for subsequent tool calls. +type WorkDirSetter interface { + SetWorkDir(string) +} + // NewServer wraps an mcp.Client as a Server. func NewServer(client *mcp.Client) Server { return &mcpServer{client: client}