From 5ce4477316e0449718378a68fb6ae6d42cff04ee Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Fri, 31 Jul 2026 08:37:26 +0200 Subject: [PATCH] remove stale IDEAS_CONTROLLER.md (dead /home/lkn/mnt/ollie/session/ refs) --- doc/IDEAS_CONTROLLER.md | 302 ---------------------------------------- 1 file changed, 302 deletions(-) delete mode 100644 doc/IDEAS_CONTROLLER.md diff --git a/doc/IDEAS_CONTROLLER.md b/doc/IDEAS_CONTROLLER.md deleted file mode 100644 index ae9fcfc..0000000 --- a/doc/IDEAS_CONTROLLER.md +++ /dev/null @@ -1,302 +0,0 @@ -# IDEAS: Fit-for-Purpose Session Controller - -Ollie's 9P session filesystem exposes enough observable state that an external -controller process can enforce production-grade loop discipline without touching -the agent loop, the model, or any prompt. The controller is a first-class citizen -of the design — it reads structured synthetic files and actuates via `ctl` writes. - -## Background - -Production agent systems avoid "smart shell script with amnesia" behavior by -applying explicit control policies: - -- A **planner/executor split** where one component sets the next bounded action - and another performs it. -- **Per-step budgets**: max tool calls before replanning, max context tokens - before compaction, max output bytes admitted back into context, max turns - without net state change. -- **Structured tool results**: the model sees a normalized observation record - (status, key findings, changed files, exit condition) instead of raw terminal - output. - -Ollie's system prompt enforces the planner/executor split at the prompt layer -(mandatory `PLAN:` checklist, persisted to `s/$SID/plan`). The per-step budgets -and output normalization are not enforced mechanically — they are the gap that -an external controller closes. - -## Observable Surfaces - -All paths are relative to `$OLLIE/session/$SID/agent/0/`. - -| Path | Mode | Signal class | Description | -|---|---|---|---| -| `state` | read | state | `idle` / `thinking` / `calling:` | -| `statewait` | blocking read | state | Blocks until state changes — the event pump | -| `ctxsz` | read | budget | Current tokens / window ceiling as `N/M` | -| `ctxszwait` | blocking read | budget | Blocks until context size changes — fires after every tool call | -| `usage` | read | budget | Token usage counters (input / output / total) | -| `plan` | r/w | stall | Markdown checklist; `[x]` count delta = net progress signal | -| `ctl` | write | actuator | `stop` / `kill` / `compact` / `clear` — controller's actuator | -| `prompt` | write | actuator | Inject a prompt into the session | -| `fifo.in` | write | actuator | Queue a prompt to execute after the current turn | -| `chat` | read | stall | Full chat history — tail from `offset` to measure response size | -| `offset` | read | stall | Byte offset of last user prompt | - -**Key principle**: use the `*wait` blocking-read variants, not polling. A -controller that polls `state` every second wastes cycles and can miss fast -transitions. Block on `statewait`; wake only when state actually changes. - -## Signal Taxonomy - -### State signals -Derived from `statewait`. Fires on every `idle → thinking → calling:tool → idle` -transition. Gives turn boundaries without polling. - -### Context budget -Derived from `ctxszwait` and `usage`. Fires after each tool call, not just at -idle. Enforce a ceiling as a fraction of the window before the model starts -silently degrading from context loss. - -### Output byte budget -Computed from `offset` + `chat`. After each turn transitions to idle, measure -`wc -c` on the chat file from the saved offset. A large delta with no -corresponding `plan` checkbox change is raw terminal sludge. - -### Stall detection -Computed from the `[x]` line count in `plan` across consecutive idle events. -Count idle turns where the completed-task count didn't change. That's the -no-progress counter. - -## Controller Types - -Each controller is a small independent process. They compose via process -supervision — no shared framework, no IPC, just the filesystem. - -### budget-ctl -Watches `ctxszwait`. When context crosses a threshold, writes `compact` to `ctl`. -When a step ceiling is crossed (counted as `calling:*` → `idle` transitions), -writes a queued replan prompt to `fifo.in`. - -- **Watches**: `ctxszwait`, `usage` -- **Actuates**: `ctl ← compact`, `fifo.in ← replan prompt` -- **Implementation**: ~30 lines of `sh` + `awk` - -### stall-ctl -Blocks on `statewait` to get idle events. Reads the plan file on each idle, -diffs the `[x]` count. After N idle turns with no delta, writes `stop` and logs -the stall. - -- **Watches**: `statewait`, `plan` -- **Actuates**: `ctl ← stop` -- **Implementation**: ~40 lines of `sh` - -### output-ctl -After each idle, computes the chat delta from `offset`. If the raw response -exceeds `MAX_BYTES`, injects a normalization prompt: - -> Compress your last tool output into a structured observation: status, key -> findings, changed files, next action. - -- **Watches**: `statewait`, `offset`, `chat` -- **Actuates**: `prompt ← normalize instruction` -- **Implementation**: ~50 lines of `sh` - -### turn-auditor -Accumulates tool-call events from state transitions (`calling:` → `idle`). -Counts parallel vs sequential calls, flags turns where the agent re-read a file -it already read, emits a structured turn trace for post-hoc review. Observes -only — does not actuate. - -- **Watches**: `statewait` (all states) -- **Actuates**: write to log file -- **Implementation**: Go or Python; passive observer - -## Controller Loop Design - -The controller state machine per session: - -``` -WATCHING → block on statewait - → wake on "idle" -IDLE_FIRED → read ctxsz, plan; compute chat delta -EVALUATING → check all policies: budget, stall, output size -ACTING → write to ctl, prompt, or fifo.in (at most one action per idle) -WATCHING → block on statewait again -``` - -For multi-signal controllers, fan out checks in parallel after each idle event, -then apply a priority ordering before actuating (budget > stall > output) to -avoid conflicting writes to `ctl`. - -## Reference Sketches - -### stall-ctl - -```sh -#!/bin/sh -# stall-ctl: kill session after N idle turns with no plan progress -# Usage: stall-ctl --sid SID [--no-progress-turns N] -set -e -SID="" MAX_STALL=3 -while [ $# -gt 0 ]; do - case $1 in - --sid) SID=$2; shift 2 ;; - --no-progress-turns) MAX_STALL=$2; shift 2 ;; - *) shift ;; - esac -done -[ -z "$SID" ] && { echo "stall-ctl: --sid required" >&2; exit 1; } - -CTL="$OLLIE/session/$SID" -stall_count=0 -prev_checked=-1 - -while true; do - # Block until state changes - new_state=$(cat "$CTL/statewait") - [ "$new_state" != "idle" ] && continue - - # Count completed tasks in the plan checklist - checked=$(grep -c '^- \[x\]' "$CTL/plan" 2>/dev/null || echo 0) - - if [ "$prev_checked" -ge 0 ] && [ "$checked" -eq "$prev_checked" ]; then - stall_count=$((stall_count + 1)) - if [ "$stall_count" -ge "$MAX_STALL" ]; then - echo "stall-ctl: $SID stalled for $stall_count turns, stopping" - echo "stop" > "$CTL/ctl" - exit 0 - fi - else - stall_count=0 - fi - prev_checked=$checked -done -``` - -### budget-ctl - -```sh -#!/bin/sh -# budget-ctl: compact when context exceeds pct of window; replan after steps-max -# Usage: budget-ctl --sid SID [--ctx-pct N] [--steps-max N] [--replan-prompt FILE] -set -e -SID="" CTX_PCT=75 STEPS_MAX=0 REPLAN_PROMPT="" -while [ $# -gt 0 ]; do - case $1 in - --sid) SID=$2; shift 2 ;; - --ctx-pct) CTX_PCT=$2; shift 2 ;; - --steps-max) STEPS_MAX=$2; shift 2 ;; - --replan-prompt) REPLAN_PROMPT=$2; shift 2 ;; - *) shift ;; - esac -done -[ -z "$SID" ] && { echo "budget-ctl: --sid required" >&2; exit 1; } - -CTL="$OLLIE/session/$SID" -step=0 - -while true; do - # Wake on any ctx change (fires after every tool call, not just idle) - raw=$(cat "$CTL/ctxszwait") # e.g. "12847/65536" - used=$(echo "$raw" | cut -d/ -f1) - total=$(echo "$raw" | cut -d/ -f2) - pct=$(awk "BEGIN{print int($used*100/$total)}") - - if [ "$pct" -ge "$CTX_PCT" ]; then - echo "budget-ctl: ctx $pct% >= threshold, compacting" - echo "compact" > "$CTL/ctl" - fi - - # Count steps: calling:* → idle counts as one step - state=$(cat "$CTL/state") - case $state in idle) step=$((step+1)) ;; esac - if [ $STEPS_MAX -gt 0 ] && [ $step -ge $STEPS_MAX ]; then - echo "budget-ctl: step limit reached, injecting replan" - cat "$REPLAN_PROMPT" > "$CTL/prompt" - step=0 - fi -done -``` - -### output-ctl - -```sh -#!/bin/sh -# output-ctl: inject summarize prompt when turn output exceeds MAX_BYTES -# Usage: output-ctl --sid SID [--max-bytes N] -set -e -SID="" MAX_BYTES=4096 -while [ $# -gt 0 ]; do - case $1 in - --sid) SID=$2; shift 2 ;; - --max-bytes) MAX_BYTES=$2; shift 2 ;; - *) shift ;; - esac -done -[ -z "$SID" ] && { echo "output-ctl: --sid required" >&2; exit 1; } - -CTL="$OLLIE/session/$SID" -prev_offset=0 - -while true; do - new_state=$(cat "$CTL/statewait") - [ "$new_state" != "idle" ] && continue - - cur_offset=$(cat "$CTL/offset") - chat_size=$(wc -c < "$CTL/chat") - delta=$((chat_size - prev_offset)) - - if [ "$delta" -gt "$MAX_BYTES" ]; then - echo "output-ctl: response ${delta}B > ${MAX_BYTES}B, injecting normalize" - printf 'Compress your last tool output into a structured observation: '\ -'status, key findings, changed files, next action.' > "$CTL/prompt" - fi - - prev_offset=$cur_offset -done -``` - -## Composition - -All three controllers watch the same session directory as independent processes. -They share no state. Compose them per task type in a `mkfile` recipe: - -```sh -# Start a coding session with full controller suite -SID=$(ollie session new cwd=$PWD agent=coding) -CTL="$OLLIE/session/$SID" - -budget-ctl --sid "$SID" --ctx-pct 75 --steps-max 12 \ - --replan-prompt "$OLLIE_CFG_PATH/prompts/replan.md" & -stall-ctl --sid "$SID" --no-progress-turns 3 & -output-ctl --sid "$SID" --max-bytes 4096 & - -echo "$TASK" > "$CTL/prompt" -wait -``` - -Each controller process exits when it writes `kill`/`stop` to `ctl` or when the -session dies. No daemon, no framework, no shared memory. - -## Gap Closure - -| Control gap | Closed by | Signal used | -|---|---|---| -| Max tool calls before replan | `budget-ctl` step counter | `statewait` → count `calling:*` → `idle` | -| Max context before compact | `budget-ctl` ctx ceiling | `ctxszwait` → threshold → `compact` | -| Max output bytes in context | `output-ctl` | `offset` + `chat` delta | -| Stall detection | `stall-ctl` | `plan` checkbox delta across idle events | -| Structured tool results | `output-ctl` normalize prompt | Reactive: normalizes after the fact | -| Hard mid-turn enforcement | Not closeable externally | Controller acts at turn boundaries only | - -## Limitations - -The controller acts at turn boundaries — it sees output after it is already in -context. If a tool produces 100KB of stdout, `output-ctl` can inject a -normalization prompt on the next turn, but the current context window already -contains the raw output. Fully preventive output filtering would require a proxy -at the tool execution layer, not the session filesystem layer. - -The planner/executor discipline (mandatory `PLAN:` before tool calls) remains -prompt-layer only. The controller can stop a session after a bad turn but cannot -prevent a bad action mid-turn.