diff --git a/.plan.md b/.plan.md
index 65c0594..4fd7204 100644
--- a/.plan.md
+++ b/.plan.md
@@ -1,10 +1,39 @@
-# Plan: Fix tool_load to return schema + docs
+# Cleanup Checklist
-The problem: tool_load returns bare "loaded: {name}" with no metadata.
-The agent can't see the schema or usage docs without reading source files.
+## Phase 1: Delete ✅
+- [x] `agent/hooks.go` — deleted (237 lines)
+- [x] `agent/agent_config.go` — removed HookCmds + Hooks field
+- [x] `agent/runtime.go` — removed Hooks field
+- [x] `agent/build_runtime.go` — removed hooks construction
+- [x] `agent/turn.go` — removed 8 hook call sites + defaultTurnError
+- [x] `agent/loop.go` — removed PreTool/PostTool/TurnError + classifyError
+- [x] `agent/agent.go` + `new.go` — removed turnError field + assignment
+- [x] `backend/noop.go` — deleted
+- [x] `backend/oneshot.go:Route()` — removed Route/RouteRequest/RouteResult
+- [x] `cmd/ollie-9p/mount/` — deleted entire directory
+- [x] `fs/spec.go` + `fs/handlers.go` — removed /route file + handler
+- [x] `data/tools/route.meta` — deleted
+- [x] `justfile` — removed route install line
+- [x] `cmd/ollie-9p/main.go` — removed mount references
+- [x] `agent/commands.go:CompactionSnapshot()` — removed
+- [x] `agent/history.go:PreCompactionSnapshot()` — removed
+- [x] `toolsrv/discover.go:ToolPrompt()` + `ReadTool()` — removed
+- [x] `toolsrv/schema.go:ParseToolInfo()` — deleted entire file
-Fix: make ToolLoad return the full ToolInfo (name, description, input_schema, prompt)
-so the agent sees how to use the tool immediately after loading.
+## Phase 2: Strip down
+- [ ] `detach/process.go` — strip to ring buffer + pid
+- [ ] `env/env.go` — fix 27:1 scope mismatch, remove `All()`, fix `Format()`
+- [ ] `agent/compaction.go` — replace hardcoded map with YAML config file
-1. Modify ToolLoad in tools/tool.go to return structured JSON with schema + prompt
-2. Commit + push
+## Phase 3: Optimize
+- [ ] `agent/complete.go` — extract into tool script, use `Generate()`
+- [ ] `lib9p/` + `cmd/ollie-9p` — ollie-9p imports and uses lib9p
+
+## Phase 4: Quick fixes
+- [ ] `fs/spec.go` — move `GID("agent")` to root node
+- [ ] Namespace width — audit 23 agent-level files
+
+## Phase 5: Config/sidecar cleanup
+- [ ] Remove `freeloader` refs from agent JSON `hooks.turnError`
+- [ ] Remove beads shell one-liner from `default.json`
+- [ ] Remove `hooks` blocks from all 8 agent JSON files
\ No newline at end of file
diff --git a/agent/agent.go b/agent/agent.go
index a5c0731..7af6c45 100644
--- a/agent/agent.go
+++ b/agent/agent.go
@@ -54,10 +54,9 @@ type Agent struct {
sessionID string // the owning session's ID
startupMessages []string
readPlanStep func() string
- save func() // trigger debounced persistence
- flush func() // immediately flush persistence
- turnError func(ctx context.Context, errType, errMsg string) HookResult // overridable for tests
- onStateChange func(agentID, state string) // optional callback for state changes
+ save func() // trigger debounced persistence
+ flush func() // immediately flush persistence
+ onStateChange func(agentID, state string) // optional callback for state changes
}
// Backend returns the active backend from the runtime.
diff --git a/agent/agent_config.go b/agent/agent_config.go
index 00809ed..0ff84a9 100644
--- a/agent/agent_config.go
+++ b/agent/agent_config.go
@@ -5,24 +5,6 @@ import (
"io"
)
-// HookCmds holds one or more shell commands for a hook. It unmarshals from
-// either a JSON string ("cmd") or array (["cmd1","cmd2"]).
-type HookCmds []string
-
-func (h *HookCmds) UnmarshalJSON(data []byte) error {
- var s string
- if err := json.Unmarshal(data, &s); err == nil {
- *h = HookCmds{s}
- return nil
- }
- var ss []string
- if err := json.Unmarshal(data, &ss); err != nil {
- return err
- }
- *h = HookCmds(ss)
- return nil
-}
-
// Prompt holds the agent prompt. It unmarshals from either a JSON string
// (treated as literal text, with the existing resolvePrompt semantics) or
// an array of strings (each element is a shell command whose stdout is
@@ -59,7 +41,6 @@ func (p Prompt) MarshalJSON() ([]byte, error) {
}
type AgentConfig struct {
- Hooks map[string]HookCmds `json:"hooks,omitempty"`
Prompt Prompt `json:"prompt,omitempty"`
Backend string `json:"backend,omitempty"`
Model string `json:"model,omitempty"`
diff --git a/agent/build_runtime.go b/agent/build_runtime.go
index 23c87ff..3bf4454 100644
--- a/agent/build_runtime.go
+++ b/agent/build_runtime.go
@@ -33,14 +33,10 @@ func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string
allTools = toolInfosToBackend(allToolInfos)
}
- hooks := Hooks{}
var preamble string
var genParams backend.GenerationParams
var maxSteps int
if cfg != nil {
- for k, v := range cfg.Hooks {
- hooks[k] = []string(v)
- }
if resolved, err := resolvePrompt(cfg.Prompt, cwd, env); err != nil {
fmt.Fprintf(os.Stderr, "resolve prompt: %v\n", err)
} else {
@@ -179,7 +175,6 @@ func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string
ClassifyTool: classify,
OutputFormat: outputFormat,
ClassifyTier: tierFn,
- Hooks: hooks,
Preamble: preamble,
GenParams: genParams,
MaxSteps: maxSteps,
diff --git a/agent/commands.go b/agent/commands.go
index 07a2727..6182d23 100644
--- a/agent/commands.go
+++ b/agent/commands.go
@@ -2,7 +2,6 @@ package agent
import (
"context"
- "encoding/json"
"fmt"
"os"
"slices"
@@ -349,17 +348,3 @@ func (ag *Agent) InjectRewrite(prompt string) {
ag.emit(Event{Role: "info", Content: "\n"})
ag.emit(Event{Role: "user", Content: prompt})
}
-
-// CompactionSnapshot returns the pre-compaction snapshot for external persistence.
-// Returns nil if no history exists.
-func (ag *Agent) CompactionSnapshot() json.RawMessage {
- if ag.history == nil {
- return nil
- }
- snap := ag.history.PreCompactionSnapshot()
- data, err := json.Marshal(snap)
- if err != nil {
- return nil
- }
- return data
-}
diff --git a/agent/history.go b/agent/history.go
index 3b5b61f..5a3a1bf 100644
--- a/agent/history.go
+++ b/agent/history.go
@@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"os"
- "slices"
"strings"
"ollie/backend"
@@ -396,12 +395,6 @@ func isCancelledToolResult(content string) bool {
strings.Contains(content, "tool execution interrupted by user")
}
-// PreCompactionSnapshot returns a copy of the current messages for persistence
-// before compaction. Call this before compact().
-func (s *History) PreCompactionSnapshot() []backend.Message {
- return slices.Clone(s.messages)
-}
-
// Compact summarizes the conversation via an LLM call, replacing the history
// with system messages + preserved user messages + a structured summary.
// Returns (n compacted, summary text, error); n==0 means nothing to compact.
diff --git a/agent/hooks.go b/agent/hooks.go
deleted file mode 100644
index 26e374a..0000000
--- a/agent/hooks.go
+++ /dev/null
@@ -1,237 +0,0 @@
-package agent
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "os"
- "os/exec"
- "strings"
- "syscall"
- "time"
-
- olog "ollie/log"
- "ollie/paths"
-)
-
-// Hook name constants for well-known agent lifecycle events.
-const (
- HookAgentSpawn = "agentSpawn"
- HookPreTurn = "preTurn"
- HookPostTurn = "postTurn"
- HookPreTool = "preTool"
- HookPostTool = "postTool"
- HookPreCompact = "preCompact"
- HookPostCompact = "postCompact"
- HookTurnError = "turnError"
-)
-
-const defaultHookTimeout = 60
-
-// hookTimeout is the hook execution timeout in seconds. Overridable in tests.
-var hookTimeout = defaultHookTimeout
-
-// Hooks maps hook names to one or more shell commands.
-type Hooks map[string][]string
-
-// HookResult holds the outcome of running a hook.
-type HookResult struct {
- // Ran is true when a hook command was configured and executed.
- Ran bool
- // Handled is true when all hook commands exited 0 (no warnings, not blocked).
- // Use this to check whether a hook fully handled an event (e.g. turnError).
- Handled bool
- // Blocked is true when the hook wants to prevent the action (exit 2).
- // For Stop hooks, Blocked means "don't stop, continue".
- Blocked bool
- // Context is stdout from the hook, injected into the conversation.
- Context string
- // Warning, if non-empty, is a message about hook execution problems
- // (e.g. timeout, start failure) that should be surfaced to the user.
- Warning string
- // Total is the number of hook commands configured for this event.
- Total int
- // Succeeded is the number of hook commands that exited 0.
- Succeeded int
- // Failed is the number of hook commands that did not succeed (non-zero, timeout, start failure).
- Failed int
- // FailedCmds identifies which commands failed (truncated to 40 chars each).
- FailedCmds []string
-}
-
-// Run executes all commands for the named hook in order, sending payload as
-// JSON on stdin for each. Returns a combined HookResult.
-//
-// Exit codes per command:
-// - 0: success. Stdout is appended to combined context.
-// - 2: block. Stops execution immediately and returns blocked.
-// - other: non-blocking warning (stderr logged, execution continues).
-func (h Hooks) Run(ctx context.Context, name string, payload any, log *olog.Logger) HookResult {
- cmds := h[name]
- if len(cmds) == 0 {
- return HookResult{}
- }
-
- payloadJSON, _ := json.Marshal(payload)
- var cwd string
- if m, ok := payload.(map[string]string); ok {
- cwd = m["cwd"]
- }
-
- total := len(cmds)
- var contextParts []string
- var warnings []string
- var failedCmds []string
- succeeded := 0
- failed := 0
- allHandled := true
- for _, cmdStr := range cmds {
- log.Debug("hook %s: cmd=%q", name, cmdStr)
- result := runHookCmd(ctx, name, cmdStr, payloadJSON, cwd, log)
- if result.Warning != "" {
- warnings = append(warnings, result.Warning)
- allHandled = false
- failed++
- failedCmds = append(failedCmds, truncateCmd(cmdStr))
- } else if !result.Ran {
- allHandled = false
- failed++
- failedCmds = append(failedCmds, truncateCmd(cmdStr))
- } else if result.Blocked {
- // Blocked counts as "ran" for accounting but stops iteration.
- succeeded++
- return HookResult{
- Ran: true, Blocked: true, Context: result.Context,
- Warning: strings.Join(warnings, "; "),
- Total: total,
- Succeeded: succeeded,
- Failed: failed,
- FailedCmds: failedCmds,
- }
- } else {
- succeeded++
- if result.Context != "" {
- contextParts = append(contextParts, result.Context)
- }
- }
- }
- return HookResult{
- Ran: true, Handled: allHandled,
- Context: strings.Join(contextParts, "\n"),
- Warning: strings.Join(warnings, "; "),
- Total: total,
- Succeeded: succeeded,
- Failed: failed,
- FailedCmds: failedCmds,
- }
-}
-
-func runHookCmd(ctx context.Context, name, cmdStr string, payloadJSON []byte, cwd string, log *olog.Logger) HookResult {
- cmd := exec.CommandContext(ctx, "sh", "-c", cmdStr)
- cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
- cmd.Stdin = bytes.NewReader(payloadJSON)
- if cwd != "" {
- expanded := paths.ExpandHome(cwd)
- // Only set Dir if the path exists locally (remote sessions
- // have a CWD that only exists on the remote host).
- if _, err := os.Stat(expanded); err == nil {
- cmd.Dir = expanded
- }
- }
-
- // Inject payload map keys as OLLIE_* environment variables so hook
- // commands can use $OLLIE_SESSION_ID, $OLLIE_MODEL, etc. without
- // parsing the JSON payload on stdin.
- var payloadMap map[string]string
- if err := json.Unmarshal(payloadJSON, &payloadMap); err == nil {
- env := cmd.Environ()
- for k, v := range payloadMap {
- env = append(env, "OLLIE_"+strings.ToUpper(k)+"="+v)
- }
- cmd.Env = env
- }
-
- var stdout, stderr bytes.Buffer
- cmd.Stdout = &stdout
- cmd.Stderr = &stderr
-
- done := make(chan error, 1)
- if err := cmd.Start(); err != nil {
- log.Debug("hook %s: start error: %v", name, err)
- return HookResult{Ran: true, Warning: fmt.Sprintf("hook %s: failed to start: %v", name, err)}
- }
- go func() { done <- cmd.Wait() }()
-
- timeout := time.After(time.Duration(hookTimeout) * time.Second)
- select {
- case err := <-done:
- exitCode := 0
- if err != nil {
- var exitErr *exec.ExitError
- if errors.As(err, &exitErr) {
- exitCode = exitErr.ExitCode()
- } else {
- log.Debug("hook %s: wait error: %v", name, err)
- return HookResult{}
- }
- }
- switch exitCode {
- case 0:
- out := strings.TrimSpace(stdout.String())
- log.Debug("hook %s: exit=0 context_len=%d", name, len(out))
- return HookResult{Ran: true, Context: out}
- case 2:
- msg := strings.TrimSpace(stderr.String())
- log.Debug("hook %s: exit=2 (blocked) msg=%q", name, msg)
- return HookResult{Ran: true, Blocked: true, Context: msg}
- default:
- log.Debug("hook %s: exit=%d (non-blocking error) stderr=%q", name, exitCode, stderr.String())
- return HookResult{Ran: false, Warning: fmt.Sprintf("hook %s: exit %d", name, exitCode)}
- }
- case <-ctx.Done():
- syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) //nolint:errcheck
- <-done
- log.Debug("hook %s: cancelled (context done)", name)
- return HookResult{}
- case <-timeout:
- syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) //nolint:errcheck
- <-done
- log.Debug("hook %s: timed out after %ds", name, hookTimeout)
- return HookResult{Ran: true, Warning: fmt.Sprintf("hook %s: timed out after %ds", name, hookTimeout)}
- }
-}
-
-// Summary returns a human-readable summary of hook execution, e.g.
-// "(2 of 3 hooks run) (1 of 3 failed: my-script.sh)"
-func (r HookResult) Summary() string {
- if !r.Ran || r.Total == 0 {
- return ""
- }
- ran := r.Succeeded + r.Failed // commands that were attempted
- s := fmt.Sprintf("(%d of %d hooks run)", ran, r.Total)
- if r.Failed > 0 {
- detail := strings.Join(r.FailedCmds, ", ")
- s += fmt.Sprintf(" (%d of %d failed: %s)", r.Failed, r.Total, detail)
- }
- return s
-}
-
-// truncateCmd returns a short identifier for a hook command string.
-func truncateCmd(cmd string) string {
- cmd = strings.TrimSpace(cmd)
- if len(cmd) <= 40 {
- return cmd
- }
- return cmd[:37] + "..."
-}
-
-// hooksRan returns a display string for N hooks having run, e.g. "1 hook run".
-// Deprecated: prefer HookResult.Summary() for richer output.
-func hooksRan(n int) string {
- if n == 1 {
- return "1 hook run"
- }
- return fmt.Sprintf("%d hooks run", n)
-}
diff --git a/agent/loop.go b/agent/loop.go
index e089c3c..dedb27f 100644
--- a/agent/loop.go
+++ b/agent/loop.go
@@ -60,12 +60,9 @@ type agentConfig struct {
Output EventHandler
preamble string // compiled system+agent prompt sent as the system role
GenerationParams backend.GenerationParams
- PopInject func() string // returns and clears pending inject, or ""
- AutoCompact func(ctx context.Context) // called after each tool round; may compact in-place
- Save func() // called after each state.update(); persists mid-turn progress
- PreTool func(ctx context.Context, name string, args json.RawMessage) HookResult // called before each tool; exit 2 blocks execution
- PostTool func(ctx context.Context, name string, args json.RawMessage, result string) HookResult // called after each tool; exit 0 appends, exit 2 replaces result
- TurnError func(ctx context.Context, errType, errMsg string) HookResult // called on first backend error; if ran, skips retries
+ PopInject func() string // returns and clears pending inject, or ""
+ AutoCompact func(ctx context.Context) // called after each tool round; may compact in-place
+ Save func() // called after each state.update(); persists mid-turn progress
// MaxSteps is the maximum number of tool-call rounds per turn.
// When reached, a soft nudge is injected and the loop exits cleanly.
// 0 means unlimited.
@@ -135,15 +132,6 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
if ctx.Err() != nil {
return ctx.Err()
}
- // On the first error, fire the turnError hook. If it handles
- // the error (exit 0), return immediately — the hook is
- // responsible for recovery (e.g. switching model and resubmitting).
- if attempt == 0 && cfg.TurnError != nil {
- errType := classifyError(err)
- if r := cfg.TurnError(ctx, errType, err.Error()); r.Handled {
- return fmt.Errorf("step %d: %w", step, err)
- }
- }
wait, retryable := transientWait(err, attempt)
if !retryable || attempt >= maxTransientRetries {
return fmt.Errorf("step %d: %w", step, err)
@@ -287,20 +275,6 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
return cr, true
}
emit(cfg, Event{Role: "call", Name: tc.Name, Content: string(tc.Arguments)})
- if cfg.PreTool != nil {
- hr := cfg.PreTool(ctx, tc.Name, tc.Arguments)
- if hr.Failed > 0 {
- emit(cfg, Event{Role: "info", Content: "preTool: " + hr.Summary()})
- }
- if hr.Blocked {
- blocked := hr.Context
- if blocked == "" {
- blocked = fmt.Sprintf("tool %q blocked by hook", tc.Name)
- }
- emit(cfg, Event{Role: "tool", Name: tc.Name, Content: blocked, OutputFormat: toolOutputFormat(cfg, tc.Name)})
- return toolResult{ToolCallID: tc.ID, Name: tc.Name, Content: blocked, IsError: true}, false
- }
- }
readSafe := cfg.ClassifyTool != nil && cfg.ClassifyTool(tc.Name)
if readSafe {
key := tc.Name + "\x00" + string(tc.Arguments)
@@ -359,20 +333,9 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
result = "error: no tool executor configured"
isErr = true
}
- // Accumulate suffix text (PostTool context, user-interruptions,
- // truncation hints) that must be emitted after streaming completes.
+ // Accumulate suffix text (user-interruptions, truncation hints)
+ // that must be emitted after streaming completes.
var suffix string
- if cfg.PostTool != nil {
- hr := cfg.PostTool(ctx, tc.Name, tc.Arguments, result)
- if hr.Failed > 0 {
- emit(cfg, Event{Role: "info", Content: "postTool: " + hr.Summary()})
- }
- if hr.Blocked {
- result = hr.Context
- } else if hr.Context != "" {
- suffix += "\n" + hr.Context
- }
- }
if cfg.PopInject != nil {
if injected := cfg.PopInject(); injected != "" {
suffix += "\n\n\n" + injected + "\n"
@@ -1007,28 +970,6 @@ func relaxJSON(s string) string {
return out.String()
}
-// classifyError returns a short string identifying the error type for the
-// turnError hook payload.
-func classifyError(err error) string {
- var rlErr *backend.RateLimitError
- if errors.As(err, &rlErr) {
- return "rate_limit"
- }
- var tuErr *backend.ToolUnsupportedError
- if errors.As(err, &tuErr) {
- return "tool_unsupported"
- }
- var coErr *backend.ContextOverflowError
- if errors.As(err, &coErr) {
- return "context_overflow"
- }
- var tErr *backend.TransientError
- if errors.As(err, &tErr) {
- return "transient"
- }
- return "unknown"
-}
-
// transientWait returns the retry wait for a retryable error and whether it is
// retryable. Rate limits use longer waits; transient/network errors use shorter.
func transientWait(err error, attempt int) (time.Duration, bool) {
diff --git a/agent/new.go b/agent/new.go
index 1096fc7..b82b8c2 100644
--- a/agent/new.go
+++ b/agent/new.go
@@ -61,7 +61,6 @@ func NewAgent(cfg AgentCfg) *Agent {
state: "idle",
}
ag.changeCond = sync.NewCond(&ag.changeMu)
- ag.turnError = ag.defaultTurnError
ag.wireToolsChanged()
return ag
}
diff --git a/agent/runtime.go b/agent/runtime.go
index 05e470b..aec2f63 100644
--- a/agent/runtime.go
+++ b/agent/runtime.go
@@ -14,7 +14,6 @@ import (
type Runtime struct {
Backend backend.Backend
ToolServer toolsrv.Runner // the execute server (tool runtime, env, cwd)
- Hooks Hooks
Preamble string // compiled system prompt
Tools []backend.Tool
Exec toolExecutor
diff --git a/agent/turn.go b/agent/turn.go
index da027fb..d96c998 100644
--- a/agent/turn.go
+++ b/agent/turn.go
@@ -2,14 +2,12 @@ package agent
import (
"context"
- "encoding/json"
"errors"
"fmt"
"os"
"runtime/debug"
"strconv"
"strings"
- "time"
"ollie/backend"
)
@@ -72,25 +70,6 @@ func (ag *Agent) Submit(ctx context.Context, input string) {
func (ag *Agent) executeTurn(ctx context.Context, input string) string {
ag.emit(Event{Role: "user", Content: input})
- hookResult := ag.runtime.Hooks.Run(ctx, HookPreTurn, map[string]string{
- "session_id": ag.sessionID,
- "cwd": ag.effectiveCwd(),
- "prompt": input,
- }, ag.log)
- if hookResult.Blocked {
- ag.emit(infoEvent("hook blocked prompt"))
- return ""
- }
- if hookResult.Warning != "" {
- ag.emit(infoEvent(hookResult.Warning))
- }
- if sum := hookResult.Summary(); sum != "" {
- ag.emit(infoEvent("preTurn: " + sum))
- }
- if hookResult.Context != "" {
- input += "\n" + hookResult.Context
- }
-
// Snapshot session state before this turn modifies it. Restored on failure
// so the session is clean for the next attempt.
snapSession := ag.history
@@ -153,11 +132,10 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
ClassifyTool: ag.runtime.ClassifyTool,
OutputFormat: outputFormatFn,
ClassifyTier: ag.runtime.ClassifyTier,
- GenerationParams: ag.runtime.GenParams,
- MaxSteps: ag.runtime.MaxSteps,
- ReadPlanStep: ag.readPlanStep,
- TurnError: ag.turnError,
- }
+ GenerationParams: ag.runtime.GenParams,
+ MaxSteps: ag.runtime.MaxSteps,
+ ReadPlanStep: ag.readPlanStep,
+ }
var replyBuf strings.Builder
ag.cfg.Output = func(ev Event) {
@@ -203,23 +181,6 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
}
return ""
}
- ag.cfg.PreTool = func(ctx context.Context, name string, args json.RawMessage) HookResult {
- return ag.runtime.Hooks.Run(ctx, HookPreTool, map[string]string{
- "session_id": ag.sessionID,
- "cwd": ag.effectiveCwd(),
- "tool": name,
- "args": string(args),
- }, ag.log)
- }
- ag.cfg.PostTool = func(ctx context.Context, name string, args json.RawMessage, result string) HookResult {
- return ag.runtime.Hooks.Run(ctx, HookPostTool, map[string]string{
- "session_id": ag.sessionID,
- "cwd": ag.effectiveCwd(),
- "tool": name,
- "args": string(args),
- "result": result,
- }, ag.log)
- }
ag.cfg.IncrToolCallCount = func() int64 {
return ag.toolCallCount.Add(1)
}
@@ -333,31 +294,17 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
}
if errors.Is(err, context.Canceled) || errors.Is(err, ErrInterrupted) {
ag.auditLog.Debug("turn: interrupted session=%s", ag.sessionID)
- ag.save()
+ ag.save()
return ""
}
ag.emit(Event{Role: "error", Content: err.Error()})
- // Drain one FIFO item — the turnError hook may have queued a recovery prompt.
+ // Drain one FIFO item.
if next, ok := ag.fifo.Pop(); ok {
return next
}
return ""
}
- stopResult := ag.runtime.Hooks.Run(ctx, HookPostTurn, map[string]string{
- "session_id": ag.sessionID,
- "cwd": ag.effectiveCwd(),
- }, ag.log)
- if stopResult.Warning != "" {
- ag.emit(infoEvent(stopResult.Warning))
- }
- if sum := stopResult.Summary(); sum != "" {
- ag.emit(infoEvent("postTurn: " + sum))
- }
- if !stopResult.Blocked && stopResult.Context != "" && ag.history != nil {
- ag.history.appendUserMessage(stopResult.Context)
- }
-
if ag.history != nil {
ag.history.recordTurnCost(ag.cfg.Backend.Model())
appendUsageLog(ag.sessionID, ag.cfg.Backend.Name(), ag.cfg.Backend.Model(), ag.history)
@@ -368,16 +315,11 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
auditTruncate(ag.Reply()), ag.history.LastTurnCostUSD, ag.history.SessionCostUSD, ag.sessionID)
ag.notifyChange()
}
- ag.save()
+ ag.save()
- // Post-turn hook said "continue" — its context becomes the next prompt.
- if stopResult.Blocked && stopResult.Context != "" {
- return stopResult.Context
- }
-
- // Inject that was pending but never consumed (text-only response with no
+ // Inject that was pending but never consumed (text-only response with no
// tool calls) — treat it as the next user message.
- if p := ag.pendingInject.Swap(nil); p != nil {
+ if p := ag.pendingInject.Swap(nil); p != nil {
return *p
}
@@ -389,21 +331,6 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
return ""
}
-// turnError is called on the first backend error of a turn. If the hook handles
-// the error (exit 0), the turn exits immediately.
-// This is the default implementation; it can be overridden via the turnError field.
-func (ag *Agent) defaultTurnError(ctx context.Context, errType, errMsg string) HookResult {
- hookCtx, cancel := context.WithTimeout(context.Background(), time.Duration(hookTimeout)*time.Second)
- defer cancel()
- return ag.runtime.Hooks.Run(hookCtx, HookTurnError, map[string]string{
- "session_id": ag.sessionID,
- "cwd": ag.effectiveCwd(),
- "model": ag.runtime.Backend.Model(),
- "error_type": errType,
- "error": errMsg,
- }, ag.log)
-}
-
// autoCompactLimit returns the token threshold for auto-compaction (75%).
func (ag *Agent) autoCompactLimit(ctx context.Context) int {
ctxLen := ag.runtime.Backend.ContextLength(ctx)
@@ -423,27 +350,9 @@ func (ag *Agent) autoWarnLimit(ctx context.Context) int {
}
// spawnContext assembles the agent context injected at each session refresh
-// point (session start, post-clear, post-compaction). It combines the
-// agent-specific prompt with any agentSpawn hook output.
+// point (session start, post-clear, post-compaction).
func (ag *Agent) spawnContext(ctx context.Context) string {
- result := ag.runtime.Hooks.Run(ctx, HookAgentSpawn, map[string]string{
- "session_id": ag.sessionID,
- "agent_id": ag.ID(),
- "profile": ag.Profile(),
- "agent": ag.Name(),
- "cwd": ag.effectiveCwd(),
- "model": ag.runtime.Backend.Model(),
- }, ag.log)
- if result.Warning != "" {
- ag.emit(infoEvent(result.Warning))
- }
- if sum := result.Summary(); sum != "" {
- ag.emit(infoEvent("agentSpawn: " + sum))
- }
var parts []string
- if result.Context != "" {
- parts = append(parts, result.Context)
- }
// Inject AGENTS.md from the working directory if it exists.
if cwd := ag.effectiveCwd(); cwd != "" {
if data, err := os.ReadFile(cwd + "/AGENTS.md"); err == nil && len(data) > 0 {
@@ -453,25 +362,10 @@ func (ag *Agent) spawnContext(ctx context.Context) string {
return strings.Join(parts, "\n\n---\n\n")
}
-// runCompact executes a full compaction cycle: pre-hook, compact, spawn-context
-// re-injection, post-hook. Returns (n compacted, error). Returns (0, nil) if
-// the pre-hook blocked or there was nothing to compact. Caller manages setState.
+// runCompact executes a full compaction cycle: compact, spawn-context
+// re-injection. Returns (n compacted, error). Returns (0, nil) if
+// there was nothing to compact. Caller manages setState.
func (ag *Agent) runCompact(ctx context.Context, trigger string) (int, error) {
- payload := map[string]string{"session_id": ag.sessionID, "trigger": trigger, "cwd": ag.effectiveCwd()}
- pre := ag.runtime.Hooks.Run(ctx, HookPreCompact, payload, ag.log)
- if pre.Warning != "" {
- ag.emit(infoEvent(pre.Warning))
- }
- if sum := pre.Summary(); sum != "" {
- ag.emit(infoEvent("preCompact: " + sum))
- }
- if pre.Blocked {
- ag.emit(infoEvent("compact cancelled by hook"))
- return 0, nil
- }
- if pre.Context != "" {
- ag.history.appendUserMessage(pre.Context)
- }
// Use a cheaper model for compaction if configured.
compactModel := resolveCompactionModel(ag.runtime.CompactionModel, ag.runtime.Backend)
origModel := ag.runtime.Backend.Model()
@@ -483,27 +377,17 @@ func (ag *Agent) runCompact(ctx context.Context, trigger string) (int, error) {
if err != nil {
return 0, err
}
- if n > 0 {
- ag.auditLog.Debug("compact: removed %d messages trigger=%s session=%s", n, trigger, ag.sessionID)
- ag.warnedContext = false
- if sc := ag.spawnContext(ctx); sc != "" {
- ag.history.appendUserMessage(sc)
+if n > 0 {
+ ag.auditLog.Debug("compact: removed %d messages trigger=%s session=%s", n, trigger, ag.sessionID)
+ ag.warnedContext = false
+ if sc := ag.spawnContext(ctx); sc != "" {
+ ag.history.appendUserMessage(sc)
+ }
}
+ return n, nil
}
- post := ag.runtime.Hooks.Run(ctx, HookPostCompact, payload, ag.log)
- if post.Warning != "" {
- ag.emit(infoEvent(post.Warning))
- }
- if sum := post.Summary(); sum != "" {
- ag.emit(infoEvent("postCompact: " + sum))
- }
- if post.Context != "" {
- ag.history.appendUserMessage(post.Context)
- }
- return n, nil
-}
-// ErrInterrupted is returned when the user cancels an agent turn (Ctrl-C).
+ // ErrInterrupted is returned when the user cancels an agent turn (Ctrl-C).
var ErrInterrupted = errors.New("interrupted")
// defaultContextLength is used when the backend cannot report the model's
diff --git a/backend/noop.go b/backend/noop.go
deleted file mode 100644
index 330fdc1..0000000
--- a/backend/noop.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package backend
-
-import "context"
-
-// Noop is a programmable test backend that satisfies the Backend interface
-// without making any network calls. By default it returns an empty response
-// immediately. Set ChatStreamFunc to override behavior.
-type Noop struct {
- name string
- model string
-
- // ChatStreamFunc, if non-nil, replaces the default ChatStream behavior.
- // Use this to simulate blocking, errors, or custom responses.
- ChatStreamFunc func(ctx context.Context, messages []Message, tools []Tool, params GenerationParams) (<-chan StreamEvent, error)
-}
-
-// NewNoop creates a Noop backend with the given name and model.
-func NewNoop(name, model string) *Noop {
- if name == "" {
- name = "noop"
- }
- if model == "" {
- model = "noop"
- }
- return &Noop{name: name, model: model}
-}
-
-func (n *Noop) Name() string { return n.name }
-func (n *Noop) DefaultModel() string { return n.model }
-func (n *Noop) Model() string { return n.model }
-func (n *Noop) SetModel(model string) { n.model = model }
-
-func (n *Noop) ContextLength(_ context.Context) int { return 128000 }
-
-func (n *Noop) Models(_ context.Context) []string {
- return []string{n.model}
-}
-
-func (n *Noop) ChatStream(ctx context.Context, messages []Message, tools []Tool, params GenerationParams) (<-chan StreamEvent, error) {
- if n.ChatStreamFunc != nil {
- return n.ChatStreamFunc(ctx, messages, tools, params)
- }
- ch := make(chan StreamEvent, 1)
- ch <- StreamEvent{Done: true, StopReason: "end_turn"}
- close(ch)
- return ch, nil
-}
diff --git a/backend/oneshot.go b/backend/oneshot.go
index 8a1c16c..3bc913f 100644
--- a/backend/oneshot.go
+++ b/backend/oneshot.go
@@ -5,7 +5,6 @@ package backend
import (
"context"
"fmt"
- "os"
"strings"
)
@@ -45,97 +44,3 @@ func Generate(ctx context.Context, req GenerateRequest) (string, error) {
}
return sb.String(), nil
}
-
-// RouteRequest is the input for model routing.
-type RouteRequest struct {
- Task string `json:"task"`
- Backend string `json:"backend"` // optional: only consider models from this backend
-}
-
-// RouteResult is the output of model routing.
-type RouteResult struct {
- Backend string
- Model string
-}
-
-// Route classifies a task description and selects the most appropriate model
-// from the actually available models. Fetches the model list from configured
-// backends, presents them to a cheap classifier, and returns the selection.
-func Route(ctx context.Context, req RouteRequest) (RouteResult, error) {
- if req.Task == "" {
- return RouteResult{}, fmt.Errorf("provide a task description")
- }
-
- // Classifier backend/model
- classifyBackend := os.Getenv("OLLIE_ROUTE_BACKEND")
- if classifyBackend == "" {
- classifyBackend = "ollama"
- }
- classifyModel := os.Getenv("OLLIE_ROUTE_MODEL")
- if classifyModel == "" {
- classifyModel = "qwen3:8b"
- }
- cbe, err := NewWithName(classifyBackend)
- if err != nil {
- return RouteResult{}, fmt.Errorf("route backend: %w", err)
- }
- cbe.SetModel(classifyModel)
-
- // Fetch available models
- type modelEntry struct {
- backend string
- model string
- }
- var entries []modelEntry
- var modelList strings.Builder
-
- backends := Backends()
- if req.Backend != "" {
- backends = []string{req.Backend}
- }
- for _, name := range backends {
- b, err := NewWithName(name)
- if err != nil {
- continue
- }
- for _, m := range b.Models(ctx) {
- entries = append(entries, modelEntry{backend: name, model: m})
- fmt.Fprintf(&modelList, "%d. %s/%s\n", len(entries), name, m)
- }
- }
- if len(entries) == 0 {
- return RouteResult{}, fmt.Errorf("no models available")
- }
-
- classifyPrompt := fmt.Sprintf(`You are a task router. Given a task and a numbered list of available models, select the single best model for the task.
-
-Available models:
-%s
-Reply with ONLY the number of the best model. Nothing else.
-
-Task: %s`, modelList.String(), req.Task)
-
- msgs := []Message{
- {Role: "user", Content: classifyPrompt},
- }
- ch, err := cbe.ChatStream(ctx, msgs, nil, GenerationParams{})
- if err != nil {
- return RouteResult{}, fmt.Errorf("route classification failed: %w", err)
- }
- var sb strings.Builder
- for ev := range ch {
- sb.WriteString(ev.Content)
- }
- result := strings.TrimSpace(sb.String())
-
- // Parse the number from the response
- var choice int
- fmt.Sscanf(result, "%d", &choice)
- if choice < 1 || choice > len(entries) {
- // Default to first model if parsing fails
- choice = 1
- }
- picked := entries[choice-1]
-
- return RouteResult{Backend: picked.backend, Model: picked.model}, nil
-}
diff --git a/cmd/ollie-9p/main.go b/cmd/ollie-9p/main.go
index d0d3fba..ba9e2eb 100644
--- a/cmd/ollie-9p/main.go
+++ b/cmd/ollie-9p/main.go
@@ -23,14 +23,12 @@ import (
"fmt"
"io"
"os"
- "path/filepath"
"strings"
"text/tabwriter"
"time"
"9fans.net/go/plan9"
"9fans.net/go/plan9/client"
- "ollie/cmd/ollie-9p/mount"
)
var (
@@ -39,7 +37,7 @@ var (
func usage() {
fmt.Fprintf(os.Stderr, "usage: ollie-9p [-a addr] cmd args...\n")
- fmt.Fprintf(os.Stderr, "commands: read write rdwr stat ls create rm mkdir mv mount\n")
+ fmt.Fprintf(os.Stderr, "commands: read write rdwr stat ls create rm mkdir mv\n")
os.Exit(1)
}
@@ -106,9 +104,7 @@ func main() {
fatalf("usage: ollie-9p mv old new")
}
cmdMv(args[0], args[1])
- case "mount":
- cmdMount(args)
- default:
+ default:
fatalf("unknown command: %s", cmd)
}
}
@@ -420,33 +416,3 @@ func splitPath(path string) (string, string) {
}
return path[:i], path[i+1:]
}
-
-func cmdMount(args []string) {
- if len(args) < 1 {
- fatalf("usage: ollie-9p mount
[mountpoint]")
- }
- address := args[0]
- mnt := ""
- if len(args) > 1 {
- mnt = args[1]
- }
- if mnt == "" {
- home, _ := os.UserHomeDir()
- mnt = filepath.Join(home, "mnt", address)
- }
- if err := os.MkdirAll(mnt, 0755); err != nil {
- fatalf("cannot create mount dir: %v", err)
- }
- network := "unix"
- if strings.Contains(address, ":") {
- network = "tcp"
- }
- fmt.Printf("mounting %s at %s\n", address, mnt)
- if err := mount.Run(mount.Config{
- Network: network,
- Address: address,
- Mountpoint: mnt,
- }); err != nil {
- fatalf("mount: %v", err)
- }
-}
diff --git a/cmd/ollie-9p/mount/mount.go b/cmd/ollie-9p/mount/mount.go
deleted file mode 100644
index adedc89..0000000
--- a/cmd/ollie-9p/mount/mount.go
+++ /dev/null
@@ -1,65 +0,0 @@
-// Package mount provides FUSE-based 9P mount functionality.
-// Used by `ollie-9p mount` for network-transparent access to remote servers.
-package mount
-
-import (
- "fmt"
- "os"
- "os/signal"
- "syscall"
-
- "9fans.net/go/plan9/client"
- "github.com/hanwen/go-fuse/v2/fs"
- "github.com/hanwen/go-fuse/v2/fuse"
-)
-
-// Config configures a FUSE mount.
-type Config struct {
- Network string // "unix" or "tcp"
- Address string // server address
- Mountpoint string // local mountpoint
- Uname string // uname for Tattach (default: $USER)
-}
-
-// Run mounts the remote 9P server and blocks until unmounted or signaled.
-func Run(cfg Config) error {
- if cfg.Uname == "" {
- cfg.Uname = os.Getenv("USER")
- }
- if cfg.Uname == "" {
- cfg.Uname = "none"
- }
-
- conn, err := client.Dial(cfg.Network, cfg.Address)
- if err != nil {
- return fmt.Errorf("dial %s!%s: %w", cfg.Network, cfg.Address, err)
- }
- fsys, err := conn.Attach(nil, cfg.Uname, "")
- if err != nil {
- conn.Close()
- return fmt.Errorf("attach: %w", err)
- }
-
- root := &p9Dir{fsys: fsys, path: "/"}
- server, err := fs.Mount(cfg.Mountpoint, root, &fs.Options{
- MountOptions: fuse.MountOptions{
- FsName: "ollie",
- Name: "ollie9p",
- DisableXAttrs: true,
- MaxBackground: 32,
- },
- })
- if err != nil {
- return fmt.Errorf("mount %s: %w", cfg.Mountpoint, err)
- }
-
- sigCh := make(chan os.Signal, 1)
- signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
- go func() {
- <-sigCh
- server.Unmount()
- }()
-
- server.Wait()
- return nil
-}
diff --git a/cmd/ollie-9p/mount/node.go b/cmd/ollie-9p/mount/node.go
deleted file mode 100644
index 33a0ebf..0000000
--- a/cmd/ollie-9p/mount/node.go
+++ /dev/null
@@ -1,271 +0,0 @@
-package mount
-
-import (
- "context"
- "os/user"
- "strconv"
- "sync"
- "syscall"
-
- "9fans.net/go/plan9"
- "9fans.net/go/plan9/client"
- "github.com/hanwen/go-fuse/v2/fs"
- "github.com/hanwen/go-fuse/v2/fuse"
-)
-
-// p9Dir is a FUSE directory node backed by a 9P path.
-type p9Dir struct {
- fs.Inode
- fsys *client.Fsys
- path string // 9P path (empty string = root)
-}
-
-// p9File is a FUSE file node backed by a 9P path.
-type p9File struct {
- fs.Inode
- fsys *client.Fsys
- path string
- mu sync.Mutex
-}
-
-var _ = (fs.NodeGetattrer)((*p9Dir)(nil))
-var _ = (fs.NodeReaddirer)((*p9Dir)(nil))
-var _ = (fs.NodeLookuper)((*p9Dir)(nil))
-var _ = (fs.NodeCreater)((*p9Dir)(nil))
-var _ = (fs.NodeUnlinker)((*p9Dir)(nil))
-var _ = (fs.NodeMkdirer)((*p9Dir)(nil))
-var _ = (fs.NodeRmdirer)((*p9Dir)(nil))
-var _ = (fs.NodeRenamer)((*p9Dir)(nil))
-
-var _ = (fs.NodeGetattrer)((*p9File)(nil))
-var _ = (fs.NodeOpener)((*p9File)(nil))
-var _ = (fs.NodeReader)((*p9File)(nil))
-var _ = (fs.NodeWriter)((*p9File)(nil))
-var _ = (fs.NodeSetattrer)((*p9File)(nil))
-
-func p9path(base, name string) string {
- if base == "" {
- return name
- }
- return base + "/" + name
-}
-
-// --- p9Dir ---
-
-func (d *p9Dir) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
- path := d.path
- if path == "" {
- path = "/"
- }
- dir, err := d.fsys.Stat(path)
- if err != nil {
- return syscall.ENOENT
- }
- out.Mode = uint32(dir.Mode)&0777 | syscall.S_IFDIR
- out.Uid = lookupUid(dir.Uid)
- out.Gid = lookupGid(dir.Gid)
- return 0
-}
-
-func (d *p9Dir) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
- path := d.path
- if path == "" {
- path = "/"
- }
- fid, err := d.fsys.Open(path, plan9.OREAD)
- if err != nil {
- return nil, syscall.EIO
- }
- defer fid.Close()
- dirs, err := fid.Dirreadall()
- if err != nil {
- return nil, syscall.EIO
- }
- entries := make([]fuse.DirEntry, len(dirs))
- for i, dir := range dirs {
- m := uint32(dir.Mode) & 0777
- if dir.Mode&plan9.DMDIR != 0 {
- m |= syscall.S_IFDIR
- }
- entries[i] = fuse.DirEntry{Name: dir.Name, Mode: m}
- }
- return fs.NewListDirStream(entries), 0
-}
-
-func (d *p9Dir) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
- child := p9path(d.path, name)
- dir, err := d.fsys.Stat(child)
- if err != nil {
- return nil, syscall.ENOENT
- }
- out.Uid = lookupUid(dir.Uid)
- out.Gid = lookupGid(dir.Gid)
- if dir.Mode&plan9.DMDIR != 0 {
- out.Mode = uint32(dir.Mode)&0777 | syscall.S_IFDIR
- node := &p9Dir{fsys: d.fsys, path: child}
- return d.NewInode(ctx, node, fs.StableAttr{Mode: syscall.S_IFDIR}), 0
- }
- out.Mode = uint32(dir.Mode) & 0777
- out.Size = dir.Length
- node := &p9File{fsys: d.fsys, path: child}
- return d.NewInode(ctx, node, fs.StableAttr{}), 0
-}
-
-func (d *p9Dir) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (*fs.Inode, fs.FileHandle, uint32, syscall.Errno) {
- child := p9path(d.path, name)
- fid, err := d.fsys.Create(child, plan9.OWRITE, plan9.Perm(mode&0777))
- if err != nil {
- return nil, nil, 0, syscall.EIO
- }
- fid.Close()
- out.Mode = mode & 0777
- node := &p9File{fsys: d.fsys, path: child}
- inode := d.NewInode(ctx, node, fs.StableAttr{})
- return inode, nil, fuse.FOPEN_DIRECT_IO, 0
-}
-
-func (d *p9Dir) Unlink(ctx context.Context, name string) syscall.Errno {
- child := p9path(d.path, name)
- if err := d.fsys.Remove(child); err != nil {
- return syscall.EIO
- }
- return 0
-}
-
-func (d *p9Dir) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
- child := p9path(d.path, name)
- fid, err := d.fsys.Create(child, plan9.OREAD, plan9.DMDIR|plan9.Perm(mode&0777))
- if err != nil {
- return nil, syscall.EIO
- }
- fid.Close()
- out.Mode = mode&0777 | syscall.S_IFDIR
- node := &p9Dir{fsys: d.fsys, path: child}
- return d.NewInode(ctx, node, fs.StableAttr{Mode: syscall.S_IFDIR}), 0
-}
-
-func (d *p9Dir) Rmdir(ctx context.Context, name string) syscall.Errno {
- child := p9path(d.path, name)
- if err := d.fsys.Remove(child); err != nil {
- return syscall.EIO
- }
- return 0
-}
-
-func (d *p9Dir) Rename(ctx context.Context, name string, newParent fs.InodeEmbedder, newName string, flags uint32) syscall.Errno {
- child := p9path(d.path, name)
- var dir plan9.Dir
- dir.Null()
- dir.Name = newName
- if err := d.fsys.Wstat(child, &dir); err != nil {
- return syscall.EIO
- }
- return 0
-}
-
-// --- p9File ---
-
-func (f *p9File) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
- dir, err := f.fsys.Stat(f.path)
- if err != nil {
- return syscall.ENOENT
- }
- out.Mode = uint32(dir.Mode) & 0777
- out.Size = dir.Length
- out.Uid = lookupUid(dir.Uid)
- out.Gid = lookupGid(dir.Gid)
- return 0
-}
-
-func (f *p9File) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno {
- // Accept truncate silently — 9P files are append/overwrite on write.
- dir, err := f.fsys.Stat(f.path)
- if err != nil {
- return syscall.ENOENT
- }
- out.Mode = uint32(dir.Mode) & 0777
- return 0
-}
-
-func (f *p9File) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
- return nil, fuse.FOPEN_DIRECT_IO, 0
-}
-
-func (f *p9File) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
- f.mu.Lock()
- defer f.mu.Unlock()
- fid, err := f.fsys.Open(f.path, plan9.OREAD)
- if err != nil {
- return nil, syscall.EIO
- }
- defer fid.Close()
- if off > 0 {
- buf := make([]byte, min(off, 8192))
- remaining := off
- for remaining > 0 {
- n := int64(len(buf))
- if n > remaining {
- n = remaining
- }
- nr, err := fid.Read(buf[:n])
- if err != nil || nr == 0 {
- break
- }
- remaining -= int64(nr)
- }
- }
- n, err := fid.Read(dest)
- if n == 0 {
- return fuse.ReadResultData(nil), 0
- }
- return fuse.ReadResultData(dest[:n]), 0
-}
-
-func (f *p9File) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) {
- f.mu.Lock()
- defer f.mu.Unlock()
- fid, err := f.fsys.Open(f.path, plan9.OWRITE)
- if err != nil {
- return 0, syscall.EACCES
- }
- n, err := fid.Write(data)
- if err != nil {
- fid.Close()
- return 0, syscall.EIO
- }
- if err := fid.Close(); err != nil {
- return 0, syscall.EACCES
- }
- return uint32(n), 0
-}
-
-func min(a, b int64) int64 {
- if a < b {
- return a
- }
- return b
-}
-
-func lookupUid(name string) uint32 {
- if id, err := strconv.ParseUint(name, 10, 32); err == nil {
- return uint32(id)
- }
- if u, err := user.Lookup(name); err == nil {
- if id, err := strconv.ParseUint(u.Uid, 10, 32); err == nil {
- return uint32(id)
- }
- }
- return 0
-}
-
-func lookupGid(name string) uint32 {
- if id, err := strconv.ParseUint(name, 10, 32); err == nil {
- return uint32(id)
- }
- if g, err := user.LookupGroup(name); err == nil {
- if id, err := strconv.ParseUint(g.Gid, 10, 32); err == nil {
- return uint32(id)
- }
- }
- return 0
-}
diff --git a/data/tools/route.meta b/data/tools/route.meta
deleted file mode 100644
index 59efae9..0000000
--- a/data/tools/route.meta
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "description": "Route a subtask to an appropriate model tier. Spawns a subagent that reports back.",
- "prompt": "## route\n\nRoute a subtask to an appropriate model tier. Spawns a subagent that reports back.\n\n```\nroute(instruction=\"implement the login page\")\nroute(instruction=\"refactor auth module\", tier=\"expert\")\n```",
- "args": {
- "type": "object",
- "required": [
- "instruction"
- ],
- "properties": {
- "instruction": {
- "type": "string",
- "description": "Task description to route"
- },
- "tier": {
- "type": "string",
- "description": "Model tier: worker (default) or expert"
- },
- "backend": {
- "type": "string",
- "description": "Backend override"
- }
- }
- },
- "readOnly": true
-}
diff --git a/detach/process.go b/detach/process.go
index a98eda2..6e91ebe 100644
--- a/detach/process.go
+++ b/detach/process.go
@@ -1,10 +1,7 @@
package detach
import (
- "fmt"
- "os"
"sync"
- "syscall"
"time"
)
@@ -20,7 +17,6 @@ type Process struct {
ExitCode int
Ring *RingBuffer
- Cmd *os.Process
Done chan struct{}
Mu sync.Mutex
}
@@ -53,20 +49,6 @@ func (p *Process) Output() string {
return p.Ring.String()
}
-// Signal sends a signal to the detached process.
-func (p *Process) Signal(sig syscall.Signal) error {
- p.Mu.Lock()
- defer p.Mu.Unlock()
- if p.Exited {
- return fmt.Errorf("process %d already exited", p.PID)
- }
- if p.Cmd == nil {
- return fmt.Errorf("process %d has no handle", p.PID)
- }
- // Send to process group
- return syscall.Kill(-p.PID, sig)
-}
-
// ringBuffer is a fixed-size circular byte buffer.
type RingBuffer struct {
buf []byte
diff --git a/fs/handlers.go b/fs/handlers.go
index db6f356..0c66396 100644
--- a/fs/handlers.go
+++ b/fs/handlers.go
@@ -147,18 +147,6 @@ func requestGenerate(_ HandlerCtx, data []byte) ([]byte, error) {
return []byte(result + "\n"), nil
}
-func requestRoute(_ HandlerCtx, data []byte) ([]byte, error) {
- var req backend.RouteRequest
- if err := json.Unmarshal(data, &req); err != nil {
- req.Task = strings.TrimSpace(string(data))
- }
- result, err := backend.Route(context.Background(), req)
- if err != nil {
- return nil, err
- }
- return []byte(fmt.Sprintf("backend=%s model=%s\n", result.Backend, result.Model)), nil
-}
-
func readAliases(_ HandlerCtx) ([]byte, error) {
var sb strings.Builder
for name, sess := range session.Sessions() {
diff --git a/fs/spec.go b/fs/spec.go
index dff957c..cfde804 100644
--- a/fs/spec.go
+++ b/fs/spec.go
@@ -134,20 +134,14 @@ var treeSpec = DirNode("/",
GID("agent"),
),
- FileNode("generate", 0666,
- Doc("One-shot LLM generation. Write JSON {prompt,...} or plain text. Read returns result."),
- Request(requestGenerate),
- GID("agent"),
- ),
+FileNode("generate", 0666,
+ Doc("One-shot LLM generation. Write JSON {prompt,...} or plain text. Read returns result."),
+ Request(requestGenerate),
+ GID("agent"),
+ ),
- FileNode("route", 0666,
- Doc("Model routing. Write task description. Read returns 'backend=X model=Y' recommendation."),
- Request(requestRoute),
- GID("agent"),
- ),
-
- FileNode("aliases", 0444,
- Doc("Alias table for ID-based lookups. Format: idpath per line."),
+ FileNode("aliases", 0444,
+ Doc("Alias table for ID-based lookups. Format: idpath per line."),
Read(readAliases),
GID("agent"),
),
diff --git a/justfile b/justfile
index b3e8934..6bece7a 100644
--- a/justfile
+++ b/justfile
@@ -159,7 +159,6 @@ install-data:
install -m755 data/tools/process_list {{cfg}}/tools/process_list
install -m755 data/tools/process_output {{cfg}}/tools/process_output
install -m755 data/tools/process_signal {{cfg}}/tools/process_signal
- install -m755 data/tools/route {{cfg}}/tools/route
install -m755 data/tools/subagent_generate {{cfg}}/tools/subagent_generate
install -m755 data/tools/subagent_spawn {{cfg}}/tools/subagent_spawn
install -m755 data/tools/logseq {{cfg}}/tools/logseq
diff --git a/toolsrv/discover.go b/toolsrv/discover.go
index 7b793ef..abe23a1 100644
--- a/toolsrv/discover.go
+++ b/toolsrv/discover.go
@@ -43,30 +43,4 @@ func DiscoverTools() []ToolInfo {
infos = append(infos, info)
}
return infos
-}
-
-// ToolPrompt returns the full prompt content for a named tool.
-// Returns "" if the tool has no .meta file or no prompt.
-func ToolPrompt(name string) string {
- if strings.Contains(name, "/") || strings.Contains(name, "..") {
- return ""
- }
- m, err := LoadMetaFile(name)
- if err != nil || m == nil {
- return ""
- }
- return m.Prompt
-}
-
-// ReadTool reads a named tool script from the tools directory.
-func ReadTool(name string) (string, error) {
- if strings.Contains(name, "/") || strings.Contains(name, "..") {
- return "", os.ErrNotExist
- }
- path := filepath.Join(ToolsPath(), name)
- data, err := os.ReadFile(path)
- if err != nil {
- return "", err
- }
- return string(data), nil
-}
+}
\ No newline at end of file
diff --git a/toolsrv/schema.go b/toolsrv/schema.go
deleted file mode 100644
index 129fd2d..0000000
--- a/toolsrv/schema.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package toolsrv
-
-import (
- "encoding/json"
-)
-
-// ParseToolInfo builds a ToolInfo from the .meta sidecar file for the named tool.
-// Falls back to a minimal ToolInfo if no .meta exists.
-func ParseToolInfo(name, script string) ToolInfo {
- m, err := LoadMetaFile(name)
- if err == nil && m != nil {
- return ToolInfoFromMeta(name, m)
- }
- return ToolInfo{
- Name: name,
- Description: name,
- InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
- Tier: "hot",
- }
-}
diff --git a/toolsrv/server.go b/toolsrv/server.go
index 72f1ecd..f3b355b 100644
--- a/toolsrv/server.go
+++ b/toolsrv/server.go
@@ -346,7 +346,12 @@ func (e *Server) SignalDetached(pid int, sig syscall.Signal) error {
defer e.detachMu.Unlock()
for _, p := range e.detached {
if p.PID == pid {
- return p.Signal(sig)
+ p.Mu.Lock()
+ defer p.Mu.Unlock()
+ if p.Exited {
+ return fmt.Errorf("process %d already exited", pid)
+ }
+ return syscall.Kill(-pid, sig)
}
}
return fmt.Errorf("no detached process with pid %d", pid)
@@ -379,11 +384,9 @@ func (e *Server) DismissDetached(pid int) bool {
// cleanupDetached sends SIGTERM to all running detached processes.
func (e *Server) cleanupDetached() {
- e.detachMu.Lock()
- defer e.detachMu.Unlock()
for _, p := range e.detached {
p.Mu.Lock()
- if !p.Exited && p.Cmd != nil {
+ if !p.Exited {
syscall.Kill(-p.PID, syscall.SIGTERM)
}
p.Mu.Unlock()
diff --git a/toolsrv/shell.go b/toolsrv/shell.go
index 1ced844..79dabed 100644
--- a/toolsrv/shell.go
+++ b/toolsrv/shell.go
@@ -244,12 +244,12 @@ func (e *Server) executeElevatedOpts(ctx context.Context, cmd, dir string, timeo
ring := detach.NewRingBuffer(detach.RingBufSize)
pid := int(time.Now().UnixNano() & 0x7FFFFFFF) // synthetic PID
- proc := &detach.Process{
+ proc := &detach.Process{
PID: pid,
- Command: cmdStr,
- Started: time.Now(),
- Ring: ring,
- Done: make(chan struct{}),
+ Command: cmdStr,
+ Started: time.Now(),
+ Ring: ring,
+ Done: make(chan struct{}),
}
e.detachMu.Lock()
e.detached = append(e.detached, proc)
@@ -614,13 +614,12 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
lw.stream = nil
lw.mu.Unlock()
- proc := &detach.Process{
- PID: cmd.Process.Pid,
- Command: cmdStr,
- Started: time.Now(),
- Ring: ring,
- Cmd: cmd.Process,
- Done: make(chan struct{}),
+ proc := &detach.Process{
+ PID: cmd.Process.Pid,
+ Command: cmdStr,
+ Started: time.Now(),
+ Ring: ring,
+ Done: make(chan struct{}),
}
e.detachMu.Lock()
e.detached = append(e.detached, proc)