remove stale IDEAS_CONTROLLER.md (dead /home/lkn/mnt/ollie/session/ refs)
This commit is contained in:
parent
5021dedabe
commit
5ce4477316
|
|
@ -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:<tool>` |
|
||||
| `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:<tool>` → `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.
|
||||
Loading…
Reference in New Issue