remove unused feed file and observer agent support

The feed file was documented but never used by any frontend
or script. The observer agent pattern was never adopted.

- Remove feed.go, FeedWrite, ConsumeFeed
- Remove WatchFeed constant
- Remove feed file from 9P namespace
- Remove ConsumeFeed goroutine spawns from session
- Update docs (architecture-9p, architecture-ide, architecture, usage)

The event stream now covers real-time observation patterns better.
This commit is contained in:
Levi Neely 2026-10-06 12:55:39 +02:00
parent 88062d0ed0
commit a90ca6b57c
9 changed files with 16 additions and 238 deletions

View File

@ -3,7 +3,6 @@ package agent
import (
"context"
"fmt"
"io"
"os"
"runtime"
"slices"
@ -12,8 +11,6 @@ import (
"sync/atomic"
"time"
"9fans.net/go/plan9"
p9client "9fans.net/go/plan9/client"
"ollie/cmd/olliesrv/internal/backend"
toolclient "ollie/cmd/olliesrv/internal/toolclient"
olog "ollie/log"
@ -49,11 +46,10 @@ type Agent struct {
parentID string // immutable ID of the agent that spawned this agent
depth int // sub-agent depth (0=top-level, 1=sub-agent, 2=sub-sub-agent)
activeChildren atomic.Int32 // number of currently-running child sub-agents
peers map[string]struct{} // peer agent names (same session)
peerMu sync.RWMutex
fifo Fifo // prompt queue
Feed Feed // streaming input gate (change-detecting)
toolCallCount atomic.Int64
peers map[string]struct{} // peer agent names (same session)
peerMu sync.RWMutex
fifo Fifo // prompt queue
toolCallCount atomic.Int64
pendingInject atomic.Pointer[string]
submitMu sync.Mutex // serializes Submit calls (commands + turns)
stateMu sync.RWMutex
@ -525,69 +521,6 @@ func (ag *Agent) PopQueue() (string, bool) {
return ag.fifo.Pop()
}
// FeedWrite stores data in the feed and signals waiters.
func (ag *Agent) FeedWrite(data []byte) {
ag.Feed.Set(data)
ag.notifyChange()
}
// ConsumeFeed reads from the agent's feed file in a loop via 9P.
// Blocks until feed changes, submits the new data as a prompt.
// Exits when ctx is cancelled or on read error.
func ConsumeFeed(ctx context.Context, ag *Agent) {
path := fmt.Sprintf("session/%s/agent/%s/feed", ag.sessionID, ag.id)
ns := os.Getenv("NAMESPACE")
if ns == "" {
ns = p9client.Namespace()
}
user := os.Getenv("USER")
if user == "" {
user = "none"
}
aname := os.Getenv("OLLIE_UNAME")
if aname == "" {
aname = user
}
conn, err := p9client.Dial("unix", ns+"/ollie")
if err != nil {
ag.log.Error("feed consumer: dial: %v", err)
return
}
fsys, err := conn.Attach(nil, user, aname)
if err != nil {
conn.Close()
ag.log.Error("feed consumer: attach: %v", err)
return
}
feedDone := make(chan struct{})
go func() {
select {
case <-ctx.Done():
fsys.Close()
case <-feedDone:
}
}()
defer close(feedDone)
defer fsys.Close()
defer conn.Close()
for ctx.Err() == nil {
fid, err := fsys.Open(path, plan9.OREAD)
if err != nil {
return
}
data, err := io.ReadAll(fid)
fid.Close()
if err != nil {
return
}
if len(data) == 0 {
continue
}
ag.Submit(ctx, string(data))
ag.EnsureTrailingNewline()
}
}
// AgentParams holds the runtime dependencies for constructing a new Agent.
// Not to be confused with AgentConfig, which is the on-disk JSON schema.
type AgentParams struct {

View File

@ -1,46 +0,0 @@
// feed.go — Feed value storage with dedup.
//
// Feed holds the current feed value for BlockOnce-style reads. Writes
// store data and compute a short hash. Readers compare hashes to dedup;
// only changed values are returned.
package agent
import (
"crypto/sha256"
"encoding/hex"
"sync"
)
// Feed holds the current feed value. Writes store data and signal.
// Dedup is handled on the read side via base comparison (BlockOnce pattern).
type Feed struct {
mu sync.Mutex
data []byte
hash string // short hash of current data
}
// Set stores new data and returns true (always accepts).
func (f *Feed) Set(data []byte) {
f.mu.Lock()
f.data = append(f.data[:0], data...)
h := sha256.Sum256(data)
f.hash = hex.EncodeToString(h[:8])
f.mu.Unlock()
}
// Get returns the current feed data.
func (f *Feed) Get() []byte {
f.mu.Lock()
defer f.mu.Unlock()
out := make([]byte, len(f.data))
copy(out, f.data)
return out
}
// Hash returns a short hash of the current data (used as base for dedup).
func (f *Feed) Hash() string {
f.mu.Lock()
defer f.mu.Unlock()
return f.hash
}

View File

@ -15,10 +15,7 @@ import (
)
// WatchField names supported by Agent.WaitChange.
const (
WatchState = "state"
WatchFeed = "feed"
)
const WatchState = "state"
// State returns the agent's current execution state.
func (ag *Agent) State() string {
@ -121,11 +118,6 @@ func (ag *Agent) WaitChange(ctx context.Context, field, current string) (string,
switch field {
case WatchState:
val = ag.State()
case WatchFeed:
val = ag.Feed.Hash()
if val == "" {
val = current // no data yet — block
}
default:
return "", false
}

View File

@ -804,23 +804,6 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
return []byte(item), nil
}),
),
virtfs.FileNode("feed", 0666,
virtfs.Write(func(data []byte) error {
if len(data) == 0 {
return nil
}
for _, b := range data {
if b != 0 {
a.FeedWrite(data)
return nil
}
}
return nil
}),
virtfs.BlockOnce(func() ([]byte, string, error) {
return a.Feed.Get(), a.Feed.Hash(), nil
}, a.SignalCh),
),
virtfs.FileNode("chat", 0444,
virtfs.StatOverride(chatStat),
virtfs.Read(func() ([]byte, error) {

View File

@ -361,9 +361,6 @@ func (s *Session) AddAgent(ag *agent.Agent) error {
}
}
s.agents = append(s.agents, ag)
if s.Ctx != nil {
go agent.ConsumeFeed(s.Ctx, ag)
}
return nil
}
@ -546,10 +543,6 @@ func (s *Session) Resume() error {
}
}
// Start feed consumers for restored agents (context now available).
for _, ag := range s.agents {
go agent.ConsumeFeed(ctx, ag)
}
go s.saveSession()
PublishEvent("session."+s.ID+".resume", "")
s.log.Debug("Resume: complete")

View File

@ -87,7 +87,6 @@ echo "name=coding cwd=$PWD" | ollie-9p write session/myproj/agent/new
| `agent/idx` | r | Agent index: `session-id\tagent-id\tagent-name\tparent-id\tdepth\tstate`. |
| `agent/{aname}/prompt` | w | Queue a user turn. |
| `agent/{aname}/fifo` | r/w | Prompt queue. |
| `agent/{aname}/feed` | r/w | Change-detecting input stream. |
| `agent/{aname}/chat` | r | Filtered streaming chat output. |
| `agent/{aname}/chat.raw` | r | Full streaming output with markers. |
| `agent/{aname}/state` | r | Current agent state (idle, calling, thinking, paused). |
@ -162,9 +161,8 @@ Control files use request-response semantics: write one command, then read the r
## Plan 9 interaction patterns
- **Control files:** writes express operations such as stop, kill, rename, compact, and reload.
- **Blocking reads:** `event`, `feed`, and streaming chat replace event subscriptions with ordinary reads. The `event` file supports filtered subscriptions via the streaming rdwr pattern.
- **Blocking reads:** `event` and streaming chat replace event subscriptions with ordinary reads. The `event` file supports filtered subscriptions via the streaming rdwr pattern.
- **Stateless request/response:** `generate`, `ctl`, and `rdwr` files accept a request and return a result.
- **Change detection:** `feed` does not wake readers for identical consecutive data.
- **Shared namespace:** multiple clients can inspect and modify the same sessions concurrently.
- **Stable aliases:** mutable names are listed normally; immutable IDs can resolve the same nodes through invisible aliases.

View File

@ -55,26 +55,15 @@ The editor does not need to display agent output or reload files itself. Use the
| Inline completion | Send a bounded prefix/suffix request to root `generate`. |
| Agent control | Write a command to `ctl`. |
## Companion and observer agents
## Companion agents
A companion agent receives direct prompts while the coder works. An observer agent receives diffs or editor state through `feed`.
`feed` is change-detecting. A write stores the value and signals waiters. A blocking read returns only when the value differs from the reader’s previous value. The agent feed consumer submits new values as prompts.
```sh
o project/observer ctl agent observer
while :; do
git diff HEAD -- | o project/observer write feed
sleep 5
done
```
A companion agent receives direct prompts while the coder works.
Automation level is a client policy:
- Manual queries minimize automation and token use.
- Selection actions provide human-triggered focused operations.
- Companion agents provide interactive assistance.
- Observers trigger review on state changes.
- Cursor completion uses bounded one-shot generation.
- External scripts or task systems can trigger agents and collect results.
@ -86,7 +75,7 @@ Acme and Kate are two clients that implement the pattern above. The examples bel
### Acme
The Acme scripts use Acme’s own 9P namespace to obtain the window filename and selection, then use the `o` wrapper to write a prompt to the project agent. They are ordinary shell scripts.
The Acme scripts use Acme's own 9P namespace to obtain the window filename and selection, then use the `o` wrapper to write a prompt to the project agent. They are ordinary shell scripts.
Setup:
@ -128,7 +117,7 @@ win o acme/6c460de7 prompt
### Kate
The Kate plugin implements the same pattern natively. It obtains the active document and selection through KTextEditor, ensures a project agent, and writes directly to the agent’s `prompt` file through `libollie9p`.
The Kate plugin implements the same pattern natively. It obtains the active document and selection through KTextEditor, ensures a project agent, and writes directly to the agent's `prompt` file through `libollie9p`.
Its context-menu actions are concrete examples:
@ -141,12 +130,12 @@ Its context-menu actions are concrete examples:
| **Add tests for this** | Selection and source location. |
| **Document this** | Selection and source location. |
| **Send verbatim** | Selection in a language-tagged code fence. |
| **Review Diff** | Git++’s current diff, or the active diff document. |
| **Review Diff** | Git++'s current diff, or the active diff document. |
| **Start Session** | Ensures the agent and updates its project working directory. |
Kate also provides an optional one-shot completion path. It sends a bounded prefix and suffix around the cursor to the root `generate` operation and displays the returned code as ghost text. This is separate from the project agent’s prompt stream.
Kate also provides an optional one-shot completion path. It sends a bounded prefix and suffix around the cursor to the root `generate` operation and displays the returned code as ghost text. This is separate from the project agent's prompt stream.
The Kate implementation is a native plugin, but the integration boundary is unchanged: obtain editor context and write a 9P prompt. The plugin does not provide an integrated chat or prompt window; select the corresponding Kate session in the KDE GUI and use its chat stream to interact with the agent. If preferred, run `o kate/{session} tui` in Kate’s integrated terminal to use the TUI.
The Kate implementation is a native plugin, but the integration boundary is unchanged: obtain editor context and write a 9P prompt. The plugin does not provide an integrated chat or prompt window; select the corresponding Kate session in the KDE GUI and use its chat stream to interact with the agent. If preferred, run `o kate/{session} tui` in Kate's integrated terminal to use the TUI.
## Summary
@ -158,4 +147,4 @@ editor context → agent/.../prompt → Ollie agent → files/chat/state
Read context from the editor, select an agent for the project, construct a prompt, and write it to `prompt`. The Kate plugin does not provide an integrated chat or prompt window; use the KDE GUI or TUI as the agent interaction surface.
A shell command is sufficient when the editor exposes the needed context and command execution. Otherwise, use its extension or plugin mechanism. Vim, Emacs, Acme, Kate, and other editors can use the same pattern through whatever integration mechanism they provide. The editor need only provide context and a way to issue a 9P request.
A shell command is sufficient when the editor exposes the needed context and command execution. Otherwise, use its extension or plugin mechanism. Vim, Emacs, Acme, Kate, and other editors can use the same pattern through whatever integration mechanism they provide. The editor need only provide context and a way to issue a 9P request.

View File

@ -43,7 +43,7 @@ Ollie deliberately does not build the following into the agent runtime:
- **Native MCP client support.** Use executable or metadata-only tools. An external bridge can invoke an MCP client when required.
- **Embedded tool frameworks.** Tools live outside the agent loop; toolsrv owns discovery, loading, execution, sandboxing, and process state. See [`architecture-tools.md`](architecture-tools.md) and [`architecture-toolsrv.md`](architecture-toolsrv.md).
- **Plan-and-execute workflow engines.** Ollie does not own planners, task graphs, schedulers, retries, compensation, or durable workflow state. A system such as [Beads](https://github.com/steveyegge/beads) can expose those capabilities through a tool.
- **External coordination protocols.** Ollie does not expose a workflow engine, actor framework, master coordinator, or public message bus. It does have an internal session event bus for observers. Inter-agent communication uses peer links (`peer/` directory) for topology-controlled messaging within a session. External coordination uses sessions, agents, `prompt`, `chat`, `event`, `feed`, and `ctl`.
- **External coordination protocols.** Ollie does not expose a workflow engine, actor framework, master coordinator, or public message bus. It does have an internal session event bus for observers. Inter-agent communication uses peer links (`peer/` directory) for topology-controlled messaging within a session. External coordination uses sessions, agents, `prompt`, `chat`, `event`, and `ctl`.
- **Separate frontend control planes.** UIs, editor integrations, shell clients, and automation are 9P clients. They do not maintain a parallel session store or frontend-specific API. See [`architecture-9p.md`](architecture-9p.md).
- **Distributed agent state.** Remote execution moves toolsrv and tool execution, not the agent loop, prompts, history, or model calls. See [`architecture-remote.md`](architecture-remote.md).
- **A competing memory store.** [OptMem](https://github.com/VictorTaelin/OptMem) owns persistent memory; Ollie exposes it through tools.
@ -72,7 +72,7 @@ Installed runtime data includes backend configuration, agent definitions, prompt
## Multi-agent operation
`subagent_spawn` creates child agents with independent runtime state. Cascade-style fan-out and observer/feed agents are orchestration patterns built on the same session and 9P primitives, not a second agent runtime. Parent and child sessions exchange prompts and results through files.
`subagent_spawn` creates child agents with independent runtime state. Cascade-style fan-out is an orchestration pattern built on the same session and 9P primitives, not a second agent runtime. Parent and child sessions exchange prompts and results through files.
## Design constraints

View File

@ -230,70 +230,6 @@ context purely from its positional arguments.
---
## Pair programming — observer agents
An observer agent watches your coding session and makes real-time observations: bugs, missed error handling, security issues. It uses the `feed` file — a change-detecting input present on every agent. The same mechanism works whether a human or another agent is coding.
### Setup
```sh
# Create the observer (use the observer profile for read-only tools):
o myproj/obs new /path/to/repo
o myproj/obs ctl agent observer
```
### Wire a human coding session
Poll git diffs and pipe into the observer's feed. The `feed` file deduplicates — same diff written twice is ignored:
```sh
while :; do git diff HEAD --; sleep 5; done | o myproj/obs write feed
```
Read observations in another terminal:
```sh
o myproj/obs read chat
```
### Wire an agent coding session
Use the event stream as the trigger — subscribe to state changes and pipe in the git diff when the coder's turn completes:
```sh
echo "session.myproj.agent.coder.state" | ollie-9p rdwrs event | while read -r topic state; do
git diff HEAD --
done | o myproj/obs write feed
```
### How it works
The `feed` file on each agent is a `BlockOnce` file:
- **Write**: stores the data + signals change.
- **Read** (blocking): blocks until the content changes from what it was when you opened. Returns new data once, then EOF.
- **Dedup**: built into the read side. Same content written repeatedly never wakes the reader.
Internally, each agent has a `ConsumeFeed` goroutine that reads from its own feed file (via 9P) and submits new data as prompts. The observer processes each diff it receives, optionally reads surrounding code for context, and produces terse observations.
### Multiple observers
Nothing stops you from running N observers on the same coder:
```sh
# Security-focused observer:
o myproj/security new /path/to/repo
o myproj/security ctl agent observer
# Performance-focused observer (custom prompt):
o myproj/perf new /path/to/repo
# ... configure with a different prompt
# Same feed wiring for both:
while :; do git diff HEAD --; sleep 5; done | tee >(o myproj/security write feed) | o myproj/perf write feed
```
---
## Other front-ends
- **Session clients** — shell, KDE, and other 9P clients