From 4be45bc628ad35d330f4915b13a5e6db4d04898c Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Sun, 12 Apr 2026 22:35:45 +0200 Subject: [PATCH] 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 --- README.md | 39 +++++++++--------- toys/ollie.sh | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 21 deletions(-) create mode 100755 toys/ollie.sh diff --git a/README.md b/README.md index b33be49..65bb971 100644 --- a/README.md +++ b/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: diff --git a/toys/ollie.sh b/toys/ollie.sh new file mode 100755 index 0000000..d51a4cb --- /dev/null +++ b/toys/ollie.sh @@ -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