toys: add ollie.sh, a minimal shell chat interface for ollie
~110-line bash script providing a fully functional chat interface by reading/writing the 9p-mounted session filesystem directly. Includes readline key bindings (read -e) and input history (set -o history). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9cfc437bc1
commit
4be45bc628
39
README.md
39
README.md
|
|
@ -1,11 +1,18 @@
|
|||
# Ollie Monorepo
|
||||
# ollie
|
||||
|
||||
This repository contains all Ollie projects as submodules:
|
||||
What happens if agent primitives are exposed as an ordinary filesystem? How much can be subtracted from AI agent implementations without reducing their usefulness? These are some of the motivating questions behind `ollie`.
|
||||
|
||||
- **core/** – The core Ollie agent library ([ollie-core](https://github.com/lneely/ollie-core))
|
||||
- **tui/** – Terminal UI frontend ([ollie-tui](https://github.com/lneely/ollie-tui))
|
||||
- **9p/** – 9p filesystem server ([ollie-9p](https://github.com/lneely/ollie-9p))
|
||||
- **el/** – Emacs Lisp interface ([ollie-el](https://github.com/lneely/ollie-el))
|
||||
- The [core system](https://github.com/lneely/ollie-core) provides a public library of AI agent primitives.
|
||||
- The [9p filesystem server (Go)](https://github.com/lneely/ollie-9p) creates a virtual filesystem exposing the important state and behaviors as ordinary files and operations.
|
||||
- A reference [TUI](https://github.com/lneely/ollie-tui) implementation proves that it is possible to build a functioning and user-friendly terminal front-end using only the Go standard library.
|
||||
- And, just because I like my Emacs friends, a reference [Emacs interface](https://github.com/lneely/ollie-el) front-end implementation.
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `core/`:
|
||||
- `tui/`:
|
||||
- `9p/`:
|
||||
- `el/`:
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
|
@ -37,7 +44,11 @@ mk tui # Build terminal UI
|
|||
mk 9p # Build 9p filesystem server
|
||||
```
|
||||
|
||||
For Emacs Lisp, load `el/ollie.el` in Emacs.
|
||||
For Emacs Lisp, copy `el/ollie.el` into your own Emacs configuration directory, and:
|
||||
|
||||
```lisp
|
||||
(require 'ollie)
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
|
|
@ -47,20 +58,6 @@ Update all submodules to latest:
|
|||
git submodule update --remote
|
||||
```
|
||||
|
||||
## Project Links
|
||||
|
||||
- **Monorepo**: [github.com/lneely/ollie](https://github.com/lneely/ollie)
|
||||
- **Core Library**: [github.com/lneely/ollie-core](https://github.com/lneely/ollie-core)
|
||||
- **Terminal UI**: [github.com/lneely/ollie-tui](https://github.com/lneely/ollie-tui)
|
||||
- **9p Server**: [github.com/lneely/ollie-9p](https://github.com/lneely/ollie-9p)
|
||||
- **Emacs Lisp**: [github.com/lneely/ollie-el](https://github.com/lneely/ollie-el)
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `core/`: Core agent library (Go)
|
||||
- `tui/`: Terminal UI (Go)
|
||||
- `9p/`: 9p filesystem server (Go)
|
||||
- `el/`: Emacs Lisp interface (Emacs Lisp)
|
||||
## Credits
|
||||
|
||||
Many sources of inspiration:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
#!/usr/bin/env bash
|
||||
# ollie.sh: a simplified, fully-functional chat interface for ollie AI agents.
|
||||
set -o history
|
||||
|
||||
MOUNT=${OLLIE_9MOUNT:-$HOME/mnt/ollie}
|
||||
LAST=$HOME/.config/ollie/last-session
|
||||
|
||||
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
||||
is_idle() { [ "$(tr -d '[:space:]' < "$S/state" 2>/dev/null)" = "idle" ]; }
|
||||
drain() {
|
||||
local n
|
||||
n=$(wc -c < "$S/chat" 2>/dev/null || echo 0)
|
||||
[ "$n" -gt "$chat_off" ] && tail -c "+$((chat_off+1))" "$S/chat" | head -c "$((n-chat_off))"
|
||||
chat_off=$n
|
||||
}
|
||||
|
||||
# Parse flags
|
||||
new=0; resume=0; id_flag=
|
||||
backend=; model=; agent=; workdir=
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
-new) new=1 ;;
|
||||
-resume) resume=1 ;;
|
||||
-session) id_flag=$2; shift ;;
|
||||
-backend) backend=$2; shift ;;
|
||||
-model) model=$2; shift ;;
|
||||
-agent) agent=$2; shift ;;
|
||||
-workdir) workdir=$2; shift ;;
|
||||
*) die "unknown flag: $1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Build "new" ctl command with opts
|
||||
new_cmd() {
|
||||
local cmd=new
|
||||
[ -n "$backend" ] && cmd="$cmd backend=$backend"
|
||||
[ -n "$model" ] && cmd="$cmd model=$model"
|
||||
[ -n "$agent" ] && cmd="$cmd agent=$agent"
|
||||
cmd="$cmd workdir=${workdir:-$(pwd)}"
|
||||
printf '%s\n' "$cmd"
|
||||
}
|
||||
|
||||
# Create a fresh session, sets $id
|
||||
create_session() {
|
||||
local before d deadline
|
||||
before=$(ls "$MOUNT/s" 2>/dev/null)
|
||||
new_cmd > "$MOUNT/ctl" || die "write ctl"
|
||||
deadline=$(($(date +%s) + 5))
|
||||
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||||
for d in $(ls "$MOUNT/s" 2>/dev/null); do
|
||||
printf '%s\n' "$before" | grep -qxF "$d" || { id=$d; return; }
|
||||
done
|
||||
sleep 0.05
|
||||
done
|
||||
die "timeout waiting for new session"
|
||||
}
|
||||
|
||||
# Resolve session
|
||||
if [ -n "$id_flag" ]; then
|
||||
[ -d "$MOUNT/s/$id_flag" ] || die "session not found: $id_flag"
|
||||
id=$id_flag; printf 'session: %s (resumed)\n' "$id" >&2
|
||||
elif [ "$resume" = 1 ]; then
|
||||
id=$(cat "$LAST" 2>/dev/null | tr -d '[:space:]')
|
||||
[ -n "$id" ] && [ -d "$MOUNT/s/$id" ] || die "no resumable session"
|
||||
printf 'session: %s (resumed)\n' "$id" >&2
|
||||
elif [ "$new" = 1 ]; then
|
||||
create_session
|
||||
printf '%s\n' "$id" > "$LAST"
|
||||
printf 'session: %s\n' "$id" >&2
|
||||
else
|
||||
id=$(cat "$LAST" 2>/dev/null | tr -d '[:space:]')
|
||||
if [ -n "$id" ] && [ -d "$MOUNT/s/$id" ]; then
|
||||
printf 'session: %s (resumed)\n' "$id" >&2
|
||||
else
|
||||
create_session
|
||||
printf '%s\n' "$id" > "$LAST"
|
||||
printf 'session: %s\n' "$id" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
S=$MOUNT/s/$id
|
||||
chat_off=$(wc -c < "$S/chat" 2>/dev/null || echo 0)
|
||||
|
||||
trap '
|
||||
[ "$(tr -d "[:space:]" < "$S/state" 2>/dev/null)" = "idle" ] && exit
|
||||
printf "\n" >&2; printf "stop\n" > "$S/ctl"
|
||||
' INT
|
||||
|
||||
while true; do
|
||||
printf '> '
|
||||
IFS= read -re input || break
|
||||
[ -z "$input" ] && continue
|
||||
history -s "$input"
|
||||
case "$input" in
|
||||
/kill) printf 'kill %s\n' "$id" > "$MOUNT/ctl"; break ;;
|
||||
"/q "*) printf '%s\n' "${input#/q }" > "$S/enqueue"
|
||||
printf 'queued\n' >&2; continue ;;
|
||||
/*) printf '%s\n' "$input" > "$S/ctl" ;;
|
||||
*) printf '%s' "$input" > "$S/prompt" || { printf 'submit failed\n' >&2; continue; } ;;
|
||||
esac
|
||||
sleep 0.15
|
||||
while true; do
|
||||
drain
|
||||
is_idle && { drain; printf '\n'; break; }
|
||||
sleep 0.05
|
||||
done
|
||||
done
|
||||
Loading…
Reference in New Issue