diff --git a/README.md b/README.md index cb156dd..9db80c8 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ Everything lives under `$XDG_CONFIG_HOME/ollie/` (default: `~/.config/ollie/`) | **One-shot LLM** | `o generate "explain monads"` or `echo "prompt" \| o generate` | | **Run an agent** | Create a session + agent via 9P — then connect with any frontend ([ellie](doc/ellie.md), KDE (GUI/KRunner/Kate), `o tui`) | | **Remote execution** | Set `remote=user@host` in session config | -| **Background processes** | Pass `"background": true` to any tool — output auto-injected into context | +| **Background processes** | Pass `"background": true` to any tool — runs with no timeout, output auto-injected, lifecycle tied to session | +| **Parallel execution** | Non-conflicting tool calls within a turn run in parallel (scope-based conflict scheduling) | | **Agents** | Write a JSON config in `data/agents/` with agent prompt in `data/prompts/` | | **Tools** | Drop an executable + `.meta` in `~/.config/ollie/tools/` — load at runtime via `/tool_load` | | **Domain skills** | Load markdown skill modules at runtime | diff --git a/cmd/olliesrv/internal/prompts/system_prompt.md b/cmd/olliesrv/internal/prompts/system_prompt.md index 43eddb9..cd47992 100644 --- a/cmd/olliesrv/internal/prompts/system_prompt.md +++ b/cmd/olliesrv/internal/prompts/system_prompt.md @@ -52,9 +52,11 @@ Run long-running commands in the background by passing `"background": true` to s The result is a `` tag containing the process ID. You do NOT need to poll for output — background process updates are automatically injected into your context as `` blocks alongside tool results. These include status (running/exited), exit code, and the last 20 lines of output. +Background processes have no timeout — they run until they exit, you stop them, or the session ends. + **Rules**: - Use background for long operations (builds, test suites, deployments, log tailing). Do NOT background short commands (< 5s). -- React to `` naturally. If a build fails, fix it. If output is irrelevant, kill the process. +- React to `` naturally. If a build fails, fix it. If output is irrelevant, stop the process. - Stop background processes when done: `echo term | ollie-9p write session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/proc//ctl` (use `kill` instead of `term` to force-kill) - Read full output: `ollie-9p read session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/proc//out` diff --git a/cmd/toolsrv/internal/exec/exec.go b/cmd/toolsrv/internal/exec/exec.go index ae67bae..403acb0 100644 --- a/cmd/toolsrv/internal/exec/exec.go +++ b/cmd/toolsrv/internal/exec/exec.go @@ -26,8 +26,8 @@ type Config struct { CWD string Env map[string]string Yolo bool - Timeout int - Output io.Writer // if set, stream stdout/stderr here in real-time + Timeout int // 0 = no timeout + Output io.Writer // if set, stream stdout/stderr here in real-time Started chan *os.Process // if set, receives the os.Process after Start() } @@ -56,11 +56,7 @@ func ExecuteTool(ctx context.Context, info toolsrv.ToolInfo, args json.RawMessag // Extract dispatch-level flags from args bypassed := false - timeout := cfg.Timeout - timeoutExplicit := timeout > 0 - if !timeoutExplicit { - timeout = 30 - } + timeout := cfg.Timeout // 0 = no timeout; caller sets default sandboxName := "default" var argMap map[string]interface{} @@ -75,7 +71,6 @@ func ExecuteTool(ctx context.Context, info toolsrv.ToolInfo, args json.RawMessag delete(argMap, "bypass") } if t, ok := argMap["timeout"]; ok { - timeoutExplicit = true switch v := t.(type) { case float64: timeout = int(v) diff --git a/cmd/toolsrv/internal/fs/proc.go b/cmd/toolsrv/internal/fs/proc.go index 6a20e42..5be6dfc 100644 --- a/cmd/toolsrv/internal/fs/proc.go +++ b/cmd/toolsrv/internal/fs/proc.go @@ -247,9 +247,11 @@ func (st *State) NewProc(ctx context.Context, payload string, background bool) ( // For foreground, use nil (local buffer in exec path). var outputWriter io.Writer var startedCh chan *os.Process + timeout := 30 // default for foreground if background { outputWriter = &procWriter{proc: proc} startedCh = make(chan *os.Process, 1) + timeout = 0 // background procs never timeout } // Execute in goroutine @@ -257,7 +259,7 @@ func (st *State) NewProc(ctx context.Context, payload string, background bool) ( defer close(proc.done) defer cancel() - out, exitCode := st.executeTool(procCtx, info, args, cwd, envCopy, yolo, outputWriter, startedCh) + out, exitCode := st.executeTool(procCtx, info, args, cwd, envCopy, yolo, timeout, outputWriter, startedCh) proc.mu.Lock() if !background { @@ -299,7 +301,8 @@ func (st *State) NewProc(ctx context.Context, payload string, background bool) ( } // executeTool runs a tool and returns output + exit code. -func (st *State) executeTool(ctx context.Context, info toolsrv.ToolInfo, args map[string]string, cwd string, envExtra map[string]string, yolo bool, output io.Writer, started chan *os.Process) (string, int) { +// timeout: 0 = no timeout, >0 = seconds. +func (st *State) executeTool(ctx context.Context, info toolsrv.ToolInfo, args map[string]string, cwd string, envExtra map[string]string, yolo bool, timeout int, output io.Writer, started chan *os.Process) (string, int) { // Convert args map to JSON for the execution path jsonArgs := argsToJSON(args) @@ -307,6 +310,7 @@ func (st *State) executeTool(ctx context.Context, info toolsrv.ToolInfo, args ma CWD: cwd, Env: envExtra, Yolo: yolo, + Timeout: timeout, Output: output, Started: started, } diff --git a/doc/architecture.md b/doc/architecture.md index ad69708..849e4ef 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -156,12 +156,14 @@ Tool calls within a single turn are dispatched in parallel using resource-based This means N file edits on different paths complete in one round-trip instead of N sequential calls. Shell is always a barrier (global scope) since its effects are opaque. #### Background Processes -Any tool call can include `"background": true` to execute asynchronously. The result is an immediate PID; the tool runs in toolsrv's `proc/new.bg`. Background process output is automatically injected into the model's context as `` blocks at safe points (alongside subsequent tool results). The model can react to build failures, log events, etc. without polling. +Any tool call can include `"background": true` to execute asynchronously. The result is an immediate process ID; the tool runs in toolsrv's `proc/new.bg` with no timeout. Output streams in real-time into `proc/{id}/out` and is automatically injected into the model's context as `` blocks alongside subsequent tool results. The model can react to build failures, log events, etc. without polling. + +Process lifecycle is connection-based: toolsrv owns all procs, and session death (toolsrv exit) kills everything via `KillAll()` + `Pdeathsig`. Exited procs remain in the tree for 10 minutes after last read, then are garbage collected. The model controls procs via `proc/{id}/ctl` (term, kill, dismiss). #### Dispatch Flags All tools automatically receive four optional parameters (injected into schemas at runtime): - `bypass` — run outside sandbox via bypass broker -- `timeout` — execution timeout in seconds +- `timeout` — execution timeout in seconds (0 = no timeout; background forces 0) - `sandbox` — sandbox profile name - `background` — run asynchronously, auto-inject output diff --git a/doc/resources/evolution.md b/doc/resources/evolution.md index 2ca7fb9..d4a5695 100644 --- a/doc/resources/evolution.md +++ b/doc/resources/evolution.md @@ -1188,20 +1188,28 @@ opt in to parallelism. ### Background Process Interrupts Any tool call can include `"background": true` to execute asynchronously -via `proc/new.bg`. The model receives a PID immediately and continues working. -Background process output is automatically injected into the model's context -as `` blocks alongside subsequent tool results: +via `proc/new.bg`. The model receives a process ID immediately and continues working. +Background processes have no timeout — they run until they exit naturally, +are stopped via `proc/{id}/ctl`, or die with the session. + +Output is streamed in real-time into `proc/{id}/out` and automatically injected +into the model's context as `` blocks alongside +subsequent tool results: ```xml - + --- FAIL: TestFoo (0.00s) foo_test.go:12: expected 3, got 2 FAIL ``` -The model sees these naturally — no polling, no explicit `process_output` calls. -It can react to failures or kill processes by PID when done. +The model sees these naturally — no polling needed. It can react to failures +or stop processes via the proc ctl file (`term`, `kill`, `dismiss`). + +**Process lifecycle**: toolsrv owns all procs. Session death (toolsrv exit) +kills all procs via `KillAll()` + `Pdeathsig`. Exited procs remain in the +tree for 10 minutes after last read, then are garbage collected. ### Universal Dispatch Flags