diff --git a/cmd/olliesrv/internal/prompts/system_prompt.md b/cmd/olliesrv/internal/prompts/system_prompt.md index fe21810..40a6fdb 100644 --- a/cmd/olliesrv/internal/prompts/system_prompt.md +++ b/cmd/olliesrv/internal/prompts/system_prompt.md @@ -26,9 +26,16 @@ Proactively load what you need — don't wait to be told. ## Tools -Tools are loaded at startup. Additional tools can be loaded at runtime via the `tool_load` tool. Once loaded, a tool is a first-class function — call it directly by name. Do not invoke tools through `shell`. +Tools are loaded at startup. Additional tools can be loaded at runtime via the `tool_load` tool. Once loaded, a tool is a first-class function — call it directly by name. -To see all available tools (loaded and unloaded): `ollie-9p read tools` +**Important**: Use native tool calls, not shell. For example: +- To read a file: call `file_read` directly, NOT `cat` or `shell` with any command +- To search: call `file_grep` directly, NOT `grep` via shell +- To edit: call `file_edit` directly, NOT `sed` via shell + +The tool scripts in `~/.config/ollie/tools/` are already exposed as callable functions. Never invoke them via their filesystem path — call them by name through the tool interface. + +Shell is for system commands that have no native tool equivalent: builds, git, package managers, interpreters, etc. ## Parallel Calls diff --git a/docs/context-cancellation-flow.md b/docs/context-cancellation-flow.md new file mode 100644 index 0000000..1f3a250 --- /dev/null +++ b/docs/context-cancellation-flow.md @@ -0,0 +1,161 @@ +# Context Cancellation Flow + +This document traces context cancellation from user interrupt to tool process termination. + +## Overview + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ OLLIESRV │ +│ │ +│ User sends "stop" to agent ctl │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ Agent.Interrupt │ Loads currentAction (actionHandle) │ +│ │ (cause) │ Calls h.cancel(cause) │ +│ └────────┬────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ actCtx (context.WithCancelCause) │ Created in Agent.Run() │ +│ │ │ at turn start (turn.go:95) │ +│ └────────┬────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ TurnCtx (embeds │ Passed to loop.go, execOne(), etc. │ +│ │ context.Context)│ │ +│ └────────┬────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ execOne() checks ctx.Err() │ loop.go:540 │ +│ │ If cancelled → returns immediately │ │ +│ │ with status:"cancelled" │ │ +│ └────────┬────────────────────────────────┘ │ +│ │ │ +│ │ If not cancelled, calls rt.Exec(ctx, name, args) │ +│ │ which wraps srv.CallTool(ctx, ...) (runtime.go:181) │ +│ ▼ │ +└───────────┼──────────────────────────────────────────────────────────────────┘ + │ + │ UNIX SOCKET (9P PROTOCOL) + │ +┌───────────┼──────────────────────────────────────────────────────────────────┐ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ toolsrv/client9p.go: CallTool() │ │ +│ │ │ │ +│ │ 1. Opens "proc/new" file (9P fid) │ │ +│ │ │ │ +│ │ 2. Starts goroutine watching ctx.Done(): │ │ +│ │ go func() { │ │ +│ │ select { │ │ +│ │ case <-ctx.Done(): │ │ +│ │ fid.Close() // ← Force close to unblock read │ │ +│ │ case <-done: │ │ +│ │ } │ │ +│ │ }() │ │ +│ │ │ │ +│ │ 3. Writes payload to fid (Twrite) │ │ +│ │ 4. Reads result from fid (Tread) — BLOCKS │ │ +│ │ 5. If read fails AND ctx.Err() != nil → return ctx.Err() │ │ +│ └────────┬────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ fid.Close() sends Tclunk to toolsrv │ +│ ▼ │ +└───────────┼──────────────────────────────────────────────────────────────────┘ + │ +┌───────────┼──────────────────────────────────────────────────────────────────┐ +│ │ TOOLSRV │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ p9.go: handleClunk() receives Tclunk │ │ +│ │ │ │ +│ │ 1. Removes fid from map │ │ +│ │ 2. Calls f.entry.Close() (p9.go:550) │ │ +│ └────────┬────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ virtfs/builder.go: closeFn() for Request mode │ │ +│ │ │ │ +│ │ closeFn := func() error { │ │ +│ │ if reqCancel != nil { │ │ +│ │ reqCancel() // ← Cancels the request context │ │ +│ │ } │ │ +│ │ return nil │ │ +│ │ } │ │ +│ │ │ │ +│ │ This cancels reqCtx created in writeFn (builder.go:274) │ │ +│ └────────┬────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ server/proc.go: HandleProcNew() receives cancelled context │ │ +│ │ │ │ +│ │ The Request handler was called with reqCtx: │ │ +│ │ virtfs.Request(func(ctx context.Context, data []byte) ...) │ │ +│ │ │ │ +│ │ When reqCancel() is called, ctx.Done() becomes readable │ │ +│ └────────┬────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ exec/exec.go: ExecuteTool() sees context cancellation │ │ +│ │ │ │ +│ │ The exec code should check ctx.Done() and: │ │ +│ │ 1. Send SIGKILL to the child process │ │ +│ │ 2. Return error to caller │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +## Key Files + +| Location | Responsibility | +|----------|----------------| +| `cmd/olliesrv/internal/agent/agent.go:308` | `Interrupt()` calls `h.cancel(cause)` | +| `cmd/olliesrv/internal/agent/turn.go:95` | Creates `actCtx` with `WithCancelCause` | +| `cmd/olliesrv/internal/agent/loop.go:540` | `execOne()` checks `ctx.Err()` early | +| `cmd/olliesrv/internal/agent/runtime.go:181` | `rt.Exec` calls `srv.CallTool(ctx, ...)` | +| `toolsrv/client9p.go:140-155` | `CallTool()` goroutine closes fid on ctx.Done() | +| `cmd/toolsrv/p9.go:549-551` | `handleClunk()` calls `f.entry.Close()` | +| `virtfs/builder.go:282-286` | `closeFn` cancels `reqCtx` | +| `cmd/toolsrv/internal/exec/exec.go` | Should kill process on context cancellation | + +## Sequence Diagram + +``` +User Agent CallTool 9P Server virtfs exec + │ │ │ │ │ │ + │──"stop"────▶│ │ │ │ │ + │ │ │ │ │ │ + │ │──cancel()─────▶│ │ │ │ + │ │ │ │ │ │ + │ │ │──fid.Close()──▶│ │ │ + │ │ │ (Tclunk) │ │ │ + │ │ │ │ │ │ + │ │ │ │──Close()─────▶│ │ + │ │ │ │ │ │ + │ │ │ │ │──reqCancel()─▶│ + │ │ │ │ │ │ + │ │ │ │ │ │──SIGKILL──▶[process] + │ │ │ │ │ │ + │ │◀───ctx.Err()───│ │ │ │ + │ │ │ │ │ │ + │◀──cancelled─│ │ │ │ │ +``` + +## Potential Issues + +1. **Race condition**: The Tclunk might arrive after the tool has already completed + - This is benign: cancelling a completed context is a no-op + +2. **Long cleanup**: If the tool doesn't respond to SIGKILL quickly, the cancellation + may appear to hang briefly + +3. **Background processes**: Background tools (via `proc/new.bg`) are NOT cancelled + by this mechanism - they run until completion or explicit kill