From fc0c368e91b6d042c0b3bd006729b11d06a76966 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Wed, 5 Aug 2026 21:51:55 +0200 Subject: [PATCH] Good idea fairy cleanup: ~960 lines removed Phase 1: - agent/hooks.go (entire hooks engine, 237 lines) - backend/noop.go (unused test backend) - backend/oneshot.go: Route/RouteRequest/RouteResult + /route 9P file - cmd/ollie-9p/mount/ (FUSE mount, 336 lines) - toolsrv/schema.go (ParseToolInfo, zero callers) - toolsrv/discover.go: ToolPrompt/ReadTool (zero callers) - agent/commands.go: CompactionSnapshot + history.go: PreCompactionSnapshot - data/tools/route.meta + justfile route install Phase 2: - detach/process.go: stripped Signal(), Cmd field, unused imports - SignalDetached reimplemented with syscall.Kill directly 24 files changed, 19 insertions, ~980 deletions --- .plan.md | 43 +++++- agent/agent.go | 7 +- agent/agent_config.go | 19 --- agent/build_runtime.go | 5 - agent/commands.go | 15 -- agent/history.go | 7 - agent/hooks.go | 237 ------------------------------- agent/loop.go | 69 +-------- agent/new.go | 1 - agent/runtime.go | 1 - agent/turn.go | 158 +++------------------ backend/noop.go | 47 ------- backend/oneshot.go | 95 ------------- cmd/ollie-9p/main.go | 38 +---- cmd/ollie-9p/mount/mount.go | 65 --------- cmd/ollie-9p/mount/node.go | 271 ------------------------------------ data/tools/route.meta | 25 ---- detach/process.go | 18 --- fs/handlers.go | 12 -- fs/spec.go | 20 +-- justfile | 1 - toolsrv/discover.go | 28 +--- toolsrv/schema.go | 20 --- toolsrv/server.go | 11 +- toolsrv/shell.go | 23 ++- 25 files changed, 93 insertions(+), 1143 deletions(-) delete mode 100644 agent/hooks.go delete mode 100644 backend/noop.go delete mode 100644 cmd/ollie-9p/mount/mount.go delete mode 100644 cmd/ollie-9p/mount/node.go delete mode 100644 data/tools/route.meta delete mode 100644 toolsrv/schema.go 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)