remove httpgw and webui; update AGENTS.md to reflect current structure
- Remove cmd/ollie-httpgw/ directory and webui/ submodule - Update justfile: remove httpgw/webui build targets - Update README.md: remove httpgw/webui references - Update doc/ARCHITECTURE.md: remove httpgw/webui references - Update doc/EVOLUTION.md: remove httpgw/webui references - Update doc/USAGE.md: remove httpgw/webui sections - Update AGENTS.md: fix repo layout, build targets, testing, architecture paths, key files, adding tools/prompts, submodule workflow
This commit is contained in:
parent
dd5701b1bb
commit
ea3db9eb08
|
|
@ -1,6 +1,3 @@
|
|||
[submodule "webui"]
|
||||
path = webui
|
||||
url = ../ollie-webui.git
|
||||
[submodule "kde"]
|
||||
path = kde
|
||||
url = ../ollie-kde.git
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
# Plan: Remove httpgw + webui
|
||||
|
||||
1. Remove cmd/ollie-httpgw/ directory ✓ (git rm)
|
||||
2. Remove webui/ submodule ✓ (git rm)
|
||||
3. Update justfile ✓
|
||||
4. Update README.md ✓
|
||||
5. Update doc/ARCHITECTURE.md ✓
|
||||
6. Update doc/EVOLUTION.md ✓
|
||||
7. Update doc/USAGE.md ✓
|
||||
8. Update AGENTS.md — repo layout, build system, testing, architecture, key files, submodule workflow ← IN PROGRESS
|
||||
9. Commit + push
|
||||
167
AGENTS.md
167
AGENTS.md
|
|
@ -1,27 +1,21 @@
|
|||
# AGENTS.md
|
||||
|
||||
Project-level context for AI agents working in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Ollie is an AI agent runtime inspired by Plan 9: agent state and behaviors are exposed as files in a 9P namespace. Orchestration, scheduling, and UIs are external — shell scripts, editors, web apps. The core is minimal; capabilities come from composing scripts.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
Monorepo with Git submodules. Each submodule has its own Go module (except `el` which is Elisp and `kde` which is C++/Qt).
|
||||
|
||||
```
|
||||
ollie/ ← you are here
|
||||
├── core/ (Go) Core library: agent loop, backends, tools, config
|
||||
├── 9p/ (Go) 9P filesystem server (olliesrv)
|
||||
├── dbus/ (Go) D-Bus session manager daemon (ollied)
|
||||
├── acme/ (Go) Plan 9 acme editor frontend
|
||||
├── httpgw/ (Go) HTTP-to-9P gateway
|
||||
├── webui/ (TypeScript) Preact SPA browser frontend
|
||||
├── kde/ (C++/Qt6) KDE integration: plasmoid, standalone GUI, Kate plugin, KRunner, tray
|
||||
├── agent/ (Go) Agent loop, history, hooks, commands
|
||||
├── toolsrv/ (Go) Tool server, registry, sandboxed execution
|
||||
├── session/ (Go) Session lifecycle, config, persistence
|
||||
├── fs/session/ (Go) 9P filesystem tree
|
||||
├── dbus/ (Go) D-Bus adapter
|
||||
├── cmd/ (Go) Binaries (olliesrv, ollie-9p, ollie-remote)
|
||||
├── kde/ (C++/Qt6) KDE integration: plasmoid, GUI, Kate plugin, KRunner, tray
|
||||
├── el/ (Elisp) Emacs frontend (ellie.el)
|
||||
├── agents/ Agent config JSONs (loaded at runtime)
|
||||
├── prompts/ System prompt templates (markdown) ← NOT used; see note below
|
||||
├── prompts/ System prompt templates (markdown)
|
||||
├── tools/ Tool scripts (file_read, lsp_*, memory_*, etc.)
|
||||
├── sandbox/ Landlock sandbox config YAML
|
||||
├── scripts/ User-facing CLI scripts:
|
||||
|
|
@ -32,162 +26,119 @@ ollie/ ← you are here
|
|||
├── contrib/ Community scripts
|
||||
└── experiments/ Trial notes
|
||||
```
|
||||
|
||||
### Canonical source for prompts/tools/skills
|
||||
|
||||
Three locations have prompt and tool files. The **source of truth** depends on which component you're editing:
|
||||
|
||||
Prompt and tool files live in `contrib/`. The `just install-data` target copies them to `~/.config/ollie/`. **Do not edit `~/.config/ollie/` directly** — changes are overwritten on next install.
|
||||
| Location | Purpose | Deployed by |
|
||||
|----------|---------|-------------|
|
||||
| `9p/prompts/`, `9p/tools/` | Canonical source for the 9P server | `just install-data` |
|
||||
| `9p/skills/`, `kde/skills/` | Shared `agent-skills` submodule (same repo in both) | `just install-data` |
|
||||
| `kde/prompts/`, `kde/tools/` | KDE-specific copies (may diverge for KDE-only tools like `gui_*`) | `just install-kde` |
|
||||
| Root `prompts/`, `tools/` | Top-level copies installed by `just install-data` | `just install-data` |
|
||||
|
||||
The `just install-data` target copies `agents/`, `prompts/`, `tools/`, `sandbox/` (and `skills/` from submodules) into `~/.config/ollie/`. **Do not edit `~/.config/ollie/` directly** — changes are overwritten on next install.
|
||||
|
||||
When adding a new prompt/tool that should be available to all frontends, add it to both `9p/prompts/` (or `9p/tools/`) and the root `prompts/` (or `tools/`) directory.
|
||||
|
||||
| `contrib/prompts/`, `contrib/tools/` | Canonical source for all prompts, tools, and skills | `just install-data` |
|
||||
| `contrib/skills/` | Domain knowledge modules (markdown) | `just install-data` |
|
||||
| `kde/` | KDE-specific tool scripts (`gui_*`) | `just install-kde` |
|
||||
## Build System
|
||||
|
||||
```bash
|
||||
# From monorepo root (pick one):
|
||||
just # 9P path (recommended): core, 9p, acme, kde, httpgw, webui, emacs
|
||||
just dbus # D-Bus path (KDE-only): core, ollied, kde, httpgw, webui
|
||||
|
||||
# Individual build targets:
|
||||
just core # core library
|
||||
just ninep # 9P server
|
||||
# Build everything:
|
||||
just
|
||||
# Individual targets:
|
||||
just ninep # olliesrv + ollie-9p
|
||||
just acme # acme frontend
|
||||
just kde # KDE integration (cmake with ~/.local prefix)
|
||||
just httpgw # HTTP gateway
|
||||
just webui # web UI (needs npm)
|
||||
just dbus-bin # D-Bus daemon (ollied)
|
||||
|
||||
just ollie-remote # remote execution binary
|
||||
# Install targets (run automatically by the top-level paths):
|
||||
just install-data # agents, prompts, tools, skills → ~/.config/ollie/
|
||||
just install-scripts # CLI scripts → ~/.config/ollie/scripts/
|
||||
just install-contrib # contrib scripts → ~/bin/
|
||||
just install-kde # KDE plugins, desktop file, env → ~/.local/
|
||||
just install-el # ellie.el → ~/.config/emacs/ellie/
|
||||
just install-dbus-bin # ollied + autostart → ~/.local/bin/, ~/.config/autostart/
|
||||
|
||||
# Test:
|
||||
just test # run all tests (core + 9p)
|
||||
just test # run all tests
|
||||
just test-core # core tests only
|
||||
just test-9p # 9p tests only
|
||||
|
||||
# Lifecycle:
|
||||
just uninstall # remove all installed files
|
||||
just clean # remove build artifacts
|
||||
```
|
||||
|
||||
Requires [just](https://github.com/casey/just): `cargo install just`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Go tests (from any Go submodule):
|
||||
# All Go tests:
|
||||
go test ./...
|
||||
|
||||
# Core has the most tests:
|
||||
cd core && go test ./...
|
||||
|
||||
# 9P server tests:
|
||||
cd 9p && go test ./...
|
||||
|
||||
# KDE has integration test scripts:
|
||||
cd kde && ./test-e2e.sh
|
||||
```
|
||||
|
||||
## Language & Conventions
|
||||
|
||||
- **Go** (core, 9p, dbus, acme, httpgw): Go 1.25+, standard library preferred, minimal dependencies. Modules are `ollie` (core), `olliesrv` (9p), `ollie-dbus`, `ollie-acme`, `ollie-httpgw`.
|
||||
- **Go** (root module): Go 1.25+, standard library preferred, minimal dependencies.
|
||||
- **C++20/Qt6/KF6** (kde): CMake build, dual Qt5/Qt6 support where noted.
|
||||
- **TypeScript** (webui): Vite + Preact.
|
||||
- **Elisp** (el): single file `ellie.el`.
|
||||
- **Tool scripts**: Python 3 or Bash. Must be executable. Header comments declare metadata (`# ollie:parallel read`, `# ollie:tier cold`, `# description:`, `# args:`).
|
||||
|
||||
### Code style
|
||||
- Go: `gofmt`, short variable names, error returns (no panics), table-driven tests.
|
||||
- Tool scripts: emit structured output (`STATUS=ok`, `STATUS=error`). Image/LSP tools return JSON content blocks.
|
||||
- Prompts: markdown, concise, example-driven. Follow the pattern in existing `tools-*.md` files.
|
||||
|
||||
## Architecture (key concepts)
|
||||
|
||||
1. **Two integration surfaces**: 9P filesystem (sessions at `s/{name}/`) and D-Bus (`org.ollie.SessionManager`). Tools, skills, memory on physical filesystem via env vars.
|
||||
2. **Agent loop** (`core/pkg/agent/`): Streaming LLM call → parse tool calls → dispatch → loop until no more tool calls or max steps.
|
||||
3. **Tool dispatch** (`core/pkg/tools/`): `shell` is built-in. All others are external scripts resolved from `OLLIE_TOOLS_PATH`.
|
||||
4. **Sandbox** (`core/internal/sandbox/`): Landlock-based. Config in `sandbox/*.yaml` defines filesystem access per profile.
|
||||
5. **Backends** (`core/pkg/backend/`): Ollama, OpenAI-compatible, Anthropic, Copilot, Kiro. Selectable per-session.
|
||||
1. **Two integration surfaces**: 9P filesystem (sessions at `session/{id}/agent/{aid}/`) and D-Bus (`org.ollie.SessionManager`). Tools, skills, memory on physical filesystem via env vars.
|
||||
2. **Agent loop** (`agent/loop.go`): Streaming LLM call → parse tool calls → dispatch → loop until no more tool calls or max steps.
|
||||
3. **Tool dispatch** (`toolsrv/`): `shell` is built-in. All others are external scripts resolved from `OLLIE_TOOLS_PATH`.
|
||||
4. **Sandbox** (`sandbox/`): Landlock-based. Config in `sandbox/*.yaml` defines filesystem access per profile.
|
||||
5. **Backends** (`backend/`): Ollama, OpenAI-compatible, Anthropic, Copilot, Kiro, Gemini, CodeWhisperer. Selectable per-session.
|
||||
6. **Prompts assembled at runtime**: Agent JSON `prompt` array runs shell commands (via `x/prime`) that cat markdown files together. No static prompt file is used directly.
|
||||
7. **D-Bus** (kde/dbus only): `ollied` exposes `org.ollie.SessionManager` for KDE frontends. It's a separate daemon from `olliesrv`.
|
||||
|
||||
7. **D-Bus** embedded in `olliesrv`: `org.ollie.SessionManager` exposed by the same binary as the 9P server.
|
||||
## Key Files
|
||||
|
||||
| What | Where |
|
||||
|------|-------|
|
||||
| Agent loop | `core/pkg/agent/loop.go` |
|
||||
| Tool dispatcher | `core/pkg/tools/execute/` |
|
||||
| Remote execution client | `core/pkg/remote/remote.go` |
|
||||
| Remote execution server | `remote/main.go` |
|
||||
| Sandbox enforcement | `core/internal/sandbox/` |
|
||||
| 9P filesystem | `9p/fs/` |
|
||||
| Session management | `9p/session/` |
|
||||
| System prompt template | Embedded in binary (`system_prompt.md`) |
|
||||
| Agent loop | `agent/loop.go` |
|
||||
| Tool server | `toolsrv/server.go` |
|
||||
| Remote execution | `toolsrv/remote.go`, `cmd/ollie-remote/` |
|
||||
| Sandbox enforcement | `sandbox/` |
|
||||
| 9P filesystem | `fs/session/` |
|
||||
| Session management | `session/session.go` |
|
||||
| System prompt template | Embedded in binary |
|
||||
| Agent configs | `agents/*.json` |
|
||||
| D-Bus daemon | `dbus/main.go` |
|
||||
| D-Bus adapter | `dbus/` |
|
||||
| KDE D-Bus client | `kde/plasmoid/plugin/olliedbusclient.cpp` |
|
||||
| Standalone GUI | `kde/gui/` |
|
||||
| Kate plugin | `kde/kate/` |
|
||||
|
||||
## Environment
|
||||
|
||||
Config lives in `~/.config/ollie/env`. Key variables:
|
||||
- `OLLIE_BACKEND` — default backend (ollama, openai, anthropic, copilot, kiro)
|
||||
- `OLLIE_MODEL` — default model
|
||||
- `OLLIE_TOOLS_PATH` — where tool scripts live (default: `~/.config/ollie/tools`)
|
||||
- `OLLIE_MEMORY_PATH` — persistent memory directory
|
||||
- `OLLIE_ENABLED_BACKENDS` — backends available for routing
|
||||
|
||||
|
||||
## Adding a new tool
|
||||
|
||||
1. Create an executable script in `tools/` (and `9p/tools/`, `kde/tools/` if needed)
|
||||
1. Create an executable script in `contrib/tools/`
|
||||
2. Add header comments: `# description:`, `# args:`, optionally `# ollie:parallel read` or `# ollie:tier cold`
|
||||
3. Create a matching `prompts/tools-*.md` documentation file (and in `9p/prompts/`, `kde/prompts/`)
|
||||
4. Run `just install-data` to install
|
||||
|
||||
3. Run `just install-data` to install
|
||||
## Adding a new prompt
|
||||
|
||||
1. Write the markdown file in `9p/prompts/` and root `prompts/`
|
||||
1. Write the markdown file in `contrib/prompts/`
|
||||
2. If it should be loaded by default, add a `$OLLIE_CFG_PATH/scripts/x/prime <name>` entry to `agents/default.json`
|
||||
3. Run `just install-data` to install
|
||||
|
||||
## Submodule workflow
|
||||
|
||||
The only remaining submodule is `kde/`. For KDE:
|
||||
```bash
|
||||
# Update all submodules:
|
||||
git submodule update --remote --merge
|
||||
|
||||
# Work in a submodule (e.g., core):
|
||||
cd core
|
||||
# Update KDE submodule:
|
||||
git submodule update --remote --merge kde
|
||||
# Work in the KDE submodule:
|
||||
cd kde
|
||||
# ... make changes, commit ...
|
||||
git push
|
||||
cd ..
|
||||
git add core
|
||||
git commit -m "update core submodule"
|
||||
git add kde
|
||||
git commit -m "update kde submodule"
|
||||
```
|
||||
|
||||
Each submodule has its own remote at `ssh://lkn@lneely.de:44220/lkn/ollie-{name}.git`.
|
||||
|
||||
### Nested submodules
|
||||
|
||||
Both `9p/` and `kde/` contain a nested `skills/` submodule pointing to the same repo (`agent-skills.git`). There is no root-level `skills/` directory.
|
||||
|
||||
When cloning, use `--recurse-submodules` or run `git submodule update --init --recursive`.
|
||||
## Environment
|
||||
The only submodules are `kde/` and `webui/` (now removed). For KDE:
|
||||
```bash
|
||||
# Update KDE submodule:
|
||||
git submodule update --remote --merge kde
|
||||
# Work in the KDE submodule:
|
||||
cd kde
|
||||
# ... make changes, commit ...
|
||||
git push
|
||||
cd ..
|
||||
git add kde
|
||||
git commit -m "update kde submodule"
|
||||
```
|
||||
9p/skills/ → ssh://lkn@lneely.de:44220/lkn/agent-skills.git
|
||||
kde/skills/ → ssh://lkn@lneely.de:44220/lkn/agent-skills.git (same repo)
|
||||
```
|
||||
|
||||
When cloning, use `--recurse-submodules` or run `git submodule update --init --recursive` to get these.
|
||||
Each submodule has its own remote at `ssh://lkn@lneely.de:44220/lkn/ollie-{name}.git`.
|
||||
When cloning, use `--recurse-submodules` or run `git submodule update --init --recursive`.
|
||||
|
|
|
|||
|
|
@ -69,10 +69,8 @@ Single Go module with two Git submodules for frontends.
|
|||
| `sandbox/` | Go | Landlock sandbox config |
|
||||
| `cmd/olliesrv/` | Go | The main binary |
|
||||
| `cmd/ollie-9p/` | Go | 9P client |
|
||||
| `cmd/ollie-httpgw/` | Go | HTTP-to-9P gateway |
|
||||
| `cmd/ollie-remote/` | Go | Remote execution binary |
|
||||
| `kde/` | C++/Qt6 | KDE plasmoid, GUI, Kate plugin, KRunner, tray *(submodule)* |
|
||||
| `webui/` | TypeScript | Preact SPA browser frontend *(submodule)* |
|
||||
| `agents/` | JSON | Agent configs |
|
||||
| `prompts/` | Markdown | System prompt templates |
|
||||
| `tools/` | Python/Bash | Tool scripts (file_read, lsp_*, memory_*, etc.) |
|
||||
|
|
@ -88,7 +86,6 @@ graph TB
|
|||
ACME[Plan 9 acme]
|
||||
EMACS[Emacs<br>ellie.el]
|
||||
KDE[KDE<br>plasmoid / GUI / Kate<br>KRunner / tray]
|
||||
WEB[Web UI<br>Preact SPA]
|
||||
end
|
||||
|
||||
subgraph "olliesrv"
|
||||
|
|
@ -115,11 +112,8 @@ graph TB
|
|||
RTOOL[Remote Tools]
|
||||
end
|
||||
|
||||
HTTPGW[ollie-httpgw<br>HTTP→9P gateway]
|
||||
|
||||
TER & ACME & EMACS -->|9P| P9
|
||||
KDE -->|D-Bus| DBUS
|
||||
WEB --> HTTPGW --> P9
|
||||
|
||||
P9 --> SM
|
||||
DBUS --> SM
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
ollie-httpgw
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
# ollie-httpgw
|
||||
|
||||
HTTP gateway for ollie. Translates REST requests to D-Bus calls against `org.ollie.SessionManager`, allowing any HTTP client to manage sessions without a D-Bus library. Also serves as the backend for the web UI.
|
||||
|
||||
## Building
|
||||
|
||||
```sh
|
||||
mk
|
||||
```
|
||||
|
||||
Installs `ollie-httpgw` to `$HOME/bin`.
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
ollie-httpgw start # start daemon (backgrounds itself)
|
||||
ollie-httpgw fgstart # start in foreground
|
||||
ollie-httpgw stop # stop daemon
|
||||
ollie-httpgw status # check if running
|
||||
```
|
||||
|
||||
The gateway listens on `:8011` by default. Override with `-listen`:
|
||||
|
||||
```sh
|
||||
ollie-httpgw start -listen :9090
|
||||
```
|
||||
|
||||
Requires either `olliesrv` or `ollie-dbus` running (provides the D-Bus service).
|
||||
|
||||
## API
|
||||
|
||||
All responses are JSON. An OpenAPI spec is served at `GET /openapi.json`.
|
||||
|
||||
### Sessions
|
||||
|
||||
| Method | Path | Action |
|
||||
|--------|------|--------|
|
||||
| `GET` | `/sessions` | List all sessions |
|
||||
| `POST` | `/sessions` | Create a session (body: `{cwd, backend, model, agent}`) |
|
||||
| `GET` | `/sessions/{id}` | Get session info |
|
||||
| `DELETE` | `/sessions/{id}` | Kill a session |
|
||||
| `PATCH` | `/sessions/{id}` | Rename a session (body: `{name}`) |
|
||||
|
||||
### Interaction
|
||||
|
||||
| Method | Path | Action |
|
||||
|--------|------|--------|
|
||||
| `POST` | `/sessions/{id}/submit` | Submit a prompt (body: `{prompt}`) |
|
||||
| `POST` | `/sessions/{id}/interrupt` | Interrupt the current turn |
|
||||
|
||||
### State queries
|
||||
|
||||
| Method | Path | Action |
|
||||
|--------|------|--------|
|
||||
| `GET` | `/sessions/{id}/state` | Get session state |
|
||||
| `GET` | `/sessions/{id}/chat?offset=N` | Get chat text from offset |
|
||||
| `GET` | `/sessions/{id}/usage` | Get token usage |
|
||||
| `GET` | `/sessions/{id}/cost` | Get cost |
|
||||
| `GET` | `/sessions/{id}/config` | Get config |
|
||||
| `POST` | `/sessions/{id}/config` | Set config (body: `{key, value}`) |
|
||||
| `GET` | `/sessions/{id}/context` | Get message history (JSONL) |
|
||||
| `GET` | `/sessions/{id}/models` | List models for session's backend |
|
||||
|
||||
### Peers
|
||||
|
||||
| Method | Path | Action |
|
||||
|--------|------|--------|
|
||||
| `GET` | `/sessions/{id}/peers` | List peers |
|
||||
| `POST` | `/sessions/{id}/peers` | Add peer (body: `{peer_id}`) |
|
||||
| `DELETE` | `/sessions/{id}/peers/{peer_id}` | Remove peer |
|
||||
| `POST` | `/sessions/{id}/peers/{peer_id}/submit` | Submit to peer (body: `{prompt}`) |
|
||||
|
||||
### Enumeration
|
||||
|
||||
| Method | Path | Action |
|
||||
|--------|------|--------|
|
||||
| `GET` | `/backends` | List available backends |
|
||||
| `GET` | `/agents` | List available agents |
|
||||
|
||||
## Example
|
||||
|
||||
```sh
|
||||
# Create a session
|
||||
curl -X POST http://localhost:8011/sessions \
|
||||
-d '{"cwd": "/home/user/project", "agent": "default"}'
|
||||
|
||||
# Submit a prompt
|
||||
curl -X POST http://localhost:8011/sessions/<id>/submit \
|
||||
-d '{"prompt": "list files in cwd"}'
|
||||
|
||||
# Poll chat
|
||||
curl http://localhost:8011/sessions/<id>/chat?offset=0
|
||||
```
|
||||
|
|
@ -1,662 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
dbusService = "org.ollie.SessionManager"
|
||||
dbusPath = "/org/ollie/SessionManager"
|
||||
dbusIface = "org.ollie.SessionManager"
|
||||
)
|
||||
|
||||
func dbusConn() (*dbus.Conn, error) {
|
||||
return dbus.ConnectSessionBus()
|
||||
}
|
||||
|
||||
func dbusCall(method string, args ...interface{}) (*dbus.Call, error) {
|
||||
conn, err := dbusConn()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dbus connect: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
obj := conn.Object(dbusService, dbusPath)
|
||||
call := obj.Call(dbusIface+"."+method, 0, args...)
|
||||
if call.Err != nil {
|
||||
return nil, call.Err
|
||||
}
|
||||
return call, nil
|
||||
}
|
||||
|
||||
// dbusHandler routes HTTP requests to D-Bus method calls.
|
||||
//
|
||||
// API:
|
||||
//
|
||||
// GET /sessions - ListSessions
|
||||
// POST /sessions - CreateSession
|
||||
// DELETE /sessions/{id} - KillSession
|
||||
// PATCH /sessions/{id} - RenameSession
|
||||
//
|
||||
// POST /sessions/{id}/submit - Submit prompt
|
||||
// POST /sessions/{id}/interrupt - Interrupt
|
||||
//
|
||||
// GET /sessions/{id}/state - GetState
|
||||
// GET /sessions/{id}/chat - GetChat (query: offset)
|
||||
// GET /sessions/{id}/usage - GetUsage
|
||||
// GET /sessions/{id}/cost - GetCost
|
||||
// GET /sessions/{id}/config - GetConfig
|
||||
// POST /sessions/{id}/config - SetConfig
|
||||
// GET /sessions/{id}/context - GetContext
|
||||
//
|
||||
// GET /backends - ListBackends
|
||||
// GET /sessions/{id}/models - ListModels
|
||||
// GET /agents - ListAgents
|
||||
//
|
||||
// GET /sessions/{id}/peers - PeerList
|
||||
// POST /sessions/{id}/peers - PeerAdd
|
||||
// DELETE /sessions/{id}/peers/{peer_id} - PeerRemove
|
||||
// POST /sessions/{id}/peers/{peer_id}/submit - PeerSubmit
|
||||
func dbusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
p := strings.TrimPrefix(r.URL.Path, "/")
|
||||
p = strings.TrimSuffix(p, "/")
|
||||
|
||||
parts := strings.Split(p, "/")
|
||||
|
||||
switch {
|
||||
// GET/POST /sessions
|
||||
case len(parts) == 1 && parts[0] == "sessions":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
dbusListSessions(w)
|
||||
case http.MethodPost:
|
||||
dbusCreateSession(w, r)
|
||||
default:
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
}
|
||||
|
||||
// /sessions/{id}
|
||||
case len(parts) == 2 && parts[0] == "sessions":
|
||||
sessionID := parts[1]
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
dbusGetSession(w, sessionID)
|
||||
case http.MethodDelete:
|
||||
dbusKillSession(w, sessionID)
|
||||
case http.MethodPatch:
|
||||
dbusRenameSession(w, r, sessionID)
|
||||
default:
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
}
|
||||
|
||||
// /sessions/{id}/{action}
|
||||
case len(parts) == 3 && parts[0] == "sessions":
|
||||
sessionID := parts[1]
|
||||
action := parts[2]
|
||||
switch action {
|
||||
case "submit":
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "POST only"})
|
||||
return
|
||||
}
|
||||
dbusSubmit(w, r, sessionID)
|
||||
case "interrupt":
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "POST only"})
|
||||
return
|
||||
}
|
||||
dbusInterrupt(w, sessionID)
|
||||
case "state":
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"})
|
||||
return
|
||||
}
|
||||
dbusGetState(w, sessionID)
|
||||
case "chat":
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"})
|
||||
return
|
||||
}
|
||||
dbusGetChat(w, r, sessionID)
|
||||
case "usage":
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"})
|
||||
return
|
||||
}
|
||||
dbusGetUsage(w, sessionID)
|
||||
case "cost":
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"})
|
||||
return
|
||||
}
|
||||
dbusGetCost(w, sessionID)
|
||||
case "config":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
dbusGetConfig(w, sessionID)
|
||||
case http.MethodPost:
|
||||
dbusSetConfig(w, r, sessionID)
|
||||
default:
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET or POST"})
|
||||
}
|
||||
case "context":
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"})
|
||||
return
|
||||
}
|
||||
dbusGetContext(w, sessionID)
|
||||
case "models":
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"})
|
||||
return
|
||||
}
|
||||
dbusListModels(w, sessionID)
|
||||
case "peers":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
dbusPeerList(w, sessionID)
|
||||
case http.MethodPost:
|
||||
dbusPeerAdd(w, r, sessionID)
|
||||
default:
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET or POST"})
|
||||
}
|
||||
default:
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
|
||||
}
|
||||
|
||||
// /sessions/{id}/peers/{peer_id}
|
||||
case len(parts) == 4 && parts[0] == "sessions" && parts[2] == "peers":
|
||||
sessionID := parts[1]
|
||||
peerID := parts[3]
|
||||
switch r.Method {
|
||||
case http.MethodDelete:
|
||||
dbusPeerRemove(w, sessionID, peerID)
|
||||
default:
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "DELETE only"})
|
||||
}
|
||||
|
||||
// /sessions/{id}/peers/{peer_id}/submit
|
||||
case len(parts) == 5 && parts[0] == "sessions" && parts[2] == "peers" && parts[4] == "submit":
|
||||
sessionID := parts[1]
|
||||
peerID := parts[3]
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "POST only"})
|
||||
return
|
||||
}
|
||||
dbusPeerSubmit(w, r, sessionID, peerID)
|
||||
|
||||
// GET /backends
|
||||
case len(parts) == 1 && parts[0] == "backends":
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"})
|
||||
return
|
||||
}
|
||||
dbusListBackends(w)
|
||||
|
||||
// GET /agents
|
||||
case len(parts) == 1 && parts[0] == "agents":
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"})
|
||||
return
|
||||
}
|
||||
dbusListAgents(w)
|
||||
|
||||
default:
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sessions ---
|
||||
|
||||
func dbusListSessions(w http.ResponseWriter) {
|
||||
call, err := dbusCall("ListSessions")
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var sessions []string
|
||||
if err := call.Store(&sessions); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var result []map[string]string
|
||||
for _, entry := range sessions {
|
||||
parts := strings.SplitN(entry, "\t", 4)
|
||||
if len(parts) >= 4 {
|
||||
result = append(result, map[string]string{
|
||||
"id": parts[0],
|
||||
"state": parts[1],
|
||||
"model": parts[2],
|
||||
"agent": parts[3],
|
||||
})
|
||||
}
|
||||
}
|
||||
if result == nil {
|
||||
result = []map[string]string{}
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i]["id"] < result[j]["id"]
|
||||
})
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func dbusCreateSession(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
CWD string `json:"cwd"`
|
||||
Backend string `json:"backend"`
|
||||
Model string `json:"model"`
|
||||
Agent string `json:"agent"`
|
||||
Remote string `json:"remote"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && err != io.EOF {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
call, err := dbusCall("CreateSession", req.CWD, req.Backend, req.Model, req.Agent, "", req.Remote)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var sessionID string
|
||||
if err := call.Store(&sessionID); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if sessionID == "" {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "session creation failed"})
|
||||
return
|
||||
}
|
||||
|
||||
// Rename if a name was provided
|
||||
if req.Name != "" {
|
||||
if call, err := dbusCall("RenameSession", sessionID, req.Name); err == nil {
|
||||
var success bool
|
||||
if call.Store(&success) == nil && success {
|
||||
sessionID = req.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"id": sessionID})
|
||||
}
|
||||
|
||||
func dbusGetSession(w http.ResponseWriter, sessionID string) {
|
||||
// Return config as the session detail view
|
||||
dbusGetConfig(w, sessionID)
|
||||
}
|
||||
|
||||
func dbusKillSession(w http.ResponseWriter, sessionID string) {
|
||||
call, err := dbusCall("KillSession", sessionID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
if err := call.Store(&success); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !success {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "session not found"})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func dbusRenameSession(w http.ResponseWriter, r *http.Request, sessionID string) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
call, err := dbusCall("RenameSession", sessionID, req.Name)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
if err := call.Store(&success); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !success {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "rename failed"})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// --- Interaction ---
|
||||
|
||||
func dbusSubmit(w http.ResponseWriter, r *http.Request, sessionID string) {
|
||||
var req struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
call, err := dbusCall("Submit", sessionID, req.Prompt)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
if err := call.Store(&success); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !success {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "session not found"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"submitted": true})
|
||||
}
|
||||
|
||||
func dbusInterrupt(w http.ResponseWriter, sessionID string) {
|
||||
call, err := dbusCall("Interrupt", sessionID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
if err := call.Store(&success); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !success {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "session not found"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"interrupted": true})
|
||||
}
|
||||
|
||||
// --- State queries ---
|
||||
|
||||
func dbusGetState(w http.ResponseWriter, sessionID string) {
|
||||
call, err := dbusCall("GetState", sessionID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var state string
|
||||
if err := call.Store(&state); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"state": state})
|
||||
}
|
||||
|
||||
func dbusGetChat(w http.ResponseWriter, r *http.Request, sessionID string) {
|
||||
offsetStr := r.URL.Query().Get("offset")
|
||||
var offset int64
|
||||
if offsetStr != "" {
|
||||
offset, _ = strconv.ParseInt(offsetStr, 10, 64)
|
||||
}
|
||||
|
||||
conn, err := dbusConn()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
obj := conn.Object(dbusService, dbusPath)
|
||||
call := obj.Call(dbusIface+".GetChat", 0, sessionID, offset)
|
||||
if call.Err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": call.Err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var text string
|
||||
var newOffset int64
|
||||
if err := call.Store(&text, &newOffset); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("X-Chat-Offset", strconv.FormatInt(newOffset, 10))
|
||||
w.Write([]byte(text))
|
||||
}
|
||||
|
||||
func dbusGetUsage(w http.ResponseWriter, sessionID string) {
|
||||
call, err := dbusCall("GetUsage", sessionID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var usage string
|
||||
if err := call.Store(&usage); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"usage": usage})
|
||||
}
|
||||
|
||||
func dbusGetCost(w http.ResponseWriter, sessionID string) {
|
||||
call, err := dbusCall("GetCost", sessionID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var cost string
|
||||
if err := call.Store(&cost); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"cost": cost})
|
||||
}
|
||||
|
||||
// --- Config ---
|
||||
|
||||
func dbusGetConfig(w http.ResponseWriter, sessionID string) {
|
||||
call, err := dbusCall("GetConfig", sessionID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var config string
|
||||
if err := call.Store(&config); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// Parse key=value\n into JSON object
|
||||
m := make(map[string]string)
|
||||
for _, line := range strings.Split(strings.TrimRight(config, "\n"), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
k, v, _ := strings.Cut(line, "=")
|
||||
m[k] = v
|
||||
}
|
||||
writeJSON(w, http.StatusOK, m)
|
||||
}
|
||||
|
||||
func dbusSetConfig(w http.ResponseWriter, r *http.Request, sessionID string) {
|
||||
var req struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
call, err := dbusCall("SetConfig", sessionID, req.Key, req.Value)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
if err := call.Store(&success); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !success {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "set config failed"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
// --- Context ---
|
||||
|
||||
func dbusGetContext(w http.ResponseWriter, sessionID string) {
|
||||
call, err := dbusCall("GetContext", sessionID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var context string
|
||||
if err := call.Store(&context); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// Already JSONL — return as-is with appropriate content type
|
||||
w.Header().Set("Content-Type", "application/x-ndjson")
|
||||
w.Write([]byte(context))
|
||||
}
|
||||
|
||||
// --- Backends/Models/Agents ---
|
||||
|
||||
func dbusListBackends(w http.ResponseWriter) {
|
||||
call, err := dbusCall("ListBackends")
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var backends []string
|
||||
if err := call.Store(&backends); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, backends)
|
||||
}
|
||||
|
||||
func dbusListModels(w http.ResponseWriter, sessionID string) {
|
||||
call, err := dbusCall("ListModels", sessionID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var models []string
|
||||
if err := call.Store(&models); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, models)
|
||||
}
|
||||
|
||||
func dbusListAgents(w http.ResponseWriter) {
|
||||
call, err := dbusCall("ListAgents")
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var agents []string
|
||||
if err := call.Store(&agents); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, agents)
|
||||
}
|
||||
|
||||
// --- Peers ---
|
||||
|
||||
func dbusPeerList(w http.ResponseWriter, sessionID string) {
|
||||
call, err := dbusCall("PeerList", sessionID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var peers []string
|
||||
if err := call.Store(&peers); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if peers == nil {
|
||||
peers = []string{}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, peers)
|
||||
}
|
||||
|
||||
func dbusPeerAdd(w http.ResponseWriter, r *http.Request, sessionID string) {
|
||||
var req struct {
|
||||
PeerID string `json:"peer_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
call, err := dbusCall("PeerAdd", sessionID, req.PeerID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
if err := call.Store(&success); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !success {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "peer add failed"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, map[string]bool{"added": true})
|
||||
}
|
||||
|
||||
func dbusPeerRemove(w http.ResponseWriter, sessionID, peerID string) {
|
||||
call, err := dbusCall("PeerRemove", sessionID, peerID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
if err := call.Store(&success); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !success {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "peer not found"})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func dbusPeerSubmit(w http.ResponseWriter, r *http.Request, sessionID, peerID string) {
|
||||
var req struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
call, err := dbusCall("PeerSubmit", sessionID, peerID, req.Prompt)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
if err := call.Store(&success); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !success {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "peer submit failed"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"submitted": true})
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
// ollie-httpgw - HTTP gateway for ollie session management.
|
||||
//
|
||||
// Translates HTTP requests to D-Bus calls against org.ollie.SessionManager.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ollie-httpgw [start|fgstart|stop|status]
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
//go:embed openapi.json
|
||||
var openapiSpec []byte
|
||||
|
||||
var listen = flag.String("listen", ":8011", "HTTP listen address")
|
||||
|
||||
const pidFile = "ollie-httpgw.pid"
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
|
||||
if len(args) >= 1 {
|
||||
subcmd := args[0]
|
||||
switch subcmd {
|
||||
case "start", "fgstart", "stop", "status":
|
||||
flag.CommandLine.Parse(args[1:]) //nolint:errcheck
|
||||
ns := os.Getenv("NAMESPACE")
|
||||
if ns == "" {
|
||||
ns = fmt.Sprintf("/tmp/ns.%s.:0", os.Getenv("USER"))
|
||||
}
|
||||
pidPath := filepath.Join(ns, pidFile)
|
||||
switch subcmd {
|
||||
case "start":
|
||||
if gwRunning() {
|
||||
fmt.Println("ollie-httpgw already running")
|
||||
os.Exit(0)
|
||||
}
|
||||
daemonize()
|
||||
case "fgstart":
|
||||
if gwRunning() {
|
||||
fmt.Println("ollie-httpgw already running")
|
||||
os.Exit(0)
|
||||
}
|
||||
runGateway(pidPath)
|
||||
case "stop":
|
||||
stopGateway(pidPath)
|
||||
case "status":
|
||||
if gwRunning() {
|
||||
fmt.Println("ollie-httpgw running")
|
||||
} else {
|
||||
fmt.Println("ollie-httpgw not running")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// No subcommand — run in foreground directly (no pid file).
|
||||
flag.CommandLine.Parse(args) //nolint:errcheck
|
||||
serve()
|
||||
}
|
||||
|
||||
func gwRunning() bool {
|
||||
conn, err := net.Dial("tcp", *listen)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func daemonize() {
|
||||
exe, _ := os.Executable()
|
||||
args := []string{"fgstart", "-listen", *listen}
|
||||
cmd := exec.Command(exe, args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to start: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("ollie-httpgw started (pid %d)\n", cmd.Process.Pid)
|
||||
}
|
||||
|
||||
func stopGateway(pidPath string) {
|
||||
data, err := os.ReadFile(pidPath)
|
||||
if err != nil {
|
||||
fmt.Println("ollie-httpgw not running")
|
||||
return
|
||||
}
|
||||
var pid int
|
||||
fmt.Sscanf(string(data), "%d", &pid)
|
||||
if pid > 0 {
|
||||
syscall.Kill(pid, syscall.SIGTERM) //nolint:errcheck
|
||||
}
|
||||
os.Remove(pidPath) //nolint:errcheck
|
||||
fmt.Println("ollie-httpgw stopped")
|
||||
}
|
||||
|
||||
func runGateway(pidPath string) {
|
||||
os.WriteFile(pidPath, fmt.Appendf(nil, "%d", os.Getpid()), 0644) //nolint:errcheck
|
||||
serve()
|
||||
os.Remove(pidPath) //nolint:errcheck
|
||||
}
|
||||
|
||||
func serve() {
|
||||
http.HandleFunc("/openapi.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(openapiSpec)
|
||||
})
|
||||
|
||||
http.HandleFunc("/", dbusHandler)
|
||||
|
||||
log.Printf("ollie-httpgw listening on %s", *listen)
|
||||
log.Fatal(http.ListenAndServe(*listen, nil))
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
|
@ -1,366 +0,0 @@
|
|||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "ollie-httpgw",
|
||||
"description": "HTTP gateway for ollie session management. Supports D-Bus (default) and 9P backend modes.",
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{ "url": "http://localhost:8011", "description": "Default local gateway" }
|
||||
],
|
||||
"paths": {
|
||||
"/sessions": {
|
||||
"get": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "List sessions",
|
||||
"description": "Returns all active sessions with their id, state, model, and agent.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Session list",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/SessionSummary" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"502": { "$ref": "#/components/responses/Error" }
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Create a new session",
|
||||
"requestBody": {
|
||||
"required": false,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/CreateSessionRequest" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Session created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": { "id": { "type": "string" } },
|
||||
"required": ["id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": { "$ref": "#/components/responses/Error" },
|
||||
"502": { "$ref": "#/components/responses/Error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}": {
|
||||
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
|
||||
"get": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Get session config/detail",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Session config as key-value object",
|
||||
"content": { "application/json": { "schema": { "type": "object", "additionalProperties": { "type": "string" } } } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Kill session",
|
||||
"responses": {
|
||||
"204": { "description": "Session killed" },
|
||||
"404": { "$ref": "#/components/responses/Error" }
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Rename session",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": { "name": { "type": "string" } },
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"204": { "description": "Renamed" },
|
||||
"404": { "$ref": "#/components/responses/Error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/submit": {
|
||||
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
|
||||
"post": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Submit a prompt to the session",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": { "prompt": { "type": "string" } },
|
||||
"required": ["prompt"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": { "description": "Submitted", "content": { "application/json": { "schema": { "type": "object", "properties": { "submitted": { "type": "boolean" } } } } } },
|
||||
"404": { "$ref": "#/components/responses/Error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/interrupt": {
|
||||
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
|
||||
"post": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Interrupt the running session",
|
||||
"responses": {
|
||||
"200": { "description": "Interrupted", "content": { "application/json": { "schema": { "type": "object", "properties": { "interrupted": { "type": "boolean" } } } } } },
|
||||
"404": { "$ref": "#/components/responses/Error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/state": {
|
||||
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
|
||||
"get": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Get session state",
|
||||
"responses": {
|
||||
"200": { "description": "State", "content": { "application/json": { "schema": { "type": "object", "properties": { "state": { "type": "string" } } } } } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/chat": {
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
|
||||
{ "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "format": "int64", "default": 0 }, "description": "Byte offset to resume from" }
|
||||
],
|
||||
"get": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Get chat transcript",
|
||||
"description": "Returns chat text from the given offset. The X-Chat-Offset response header contains the new offset for subsequent requests.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Chat text",
|
||||
"headers": {
|
||||
"X-Chat-Offset": { "schema": { "type": "integer", "format": "int64" }, "description": "New byte offset for next poll" }
|
||||
},
|
||||
"content": { "text/plain": { "schema": { "type": "string" } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/usage": {
|
||||
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
|
||||
"get": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Get token usage",
|
||||
"responses": {
|
||||
"200": { "description": "Usage string", "content": { "application/json": { "schema": { "type": "object", "properties": { "usage": { "type": "string" } } } } } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/cost": {
|
||||
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
|
||||
"get": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Get session cost",
|
||||
"responses": {
|
||||
"200": { "description": "Cost string", "content": { "application/json": { "schema": { "type": "object", "properties": { "cost": { "type": "string" } } } } } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/config": {
|
||||
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
|
||||
"get": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Get session config",
|
||||
"responses": {
|
||||
"200": { "description": "Config as key-value", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": { "type": "string" } } } } }
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Set a config value",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": { "type": "string" },
|
||||
"value": { "type": "string" }
|
||||
},
|
||||
"required": ["key", "value"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
|
||||
"400": { "$ref": "#/components/responses/Error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/context": {
|
||||
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
|
||||
"get": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Get message context (JSONL)",
|
||||
"responses": {
|
||||
"200": { "description": "JSONL messages", "content": { "application/x-ndjson": { "schema": { "type": "string" } } } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/models": {
|
||||
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
|
||||
"get": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "List models for session's backend",
|
||||
"responses": {
|
||||
"200": { "description": "Model list", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } } } } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/peers": {
|
||||
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
|
||||
"get": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "List peers",
|
||||
"responses": {
|
||||
"200": { "description": "Peer IDs", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } } } } }
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Add a peer",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": { "peer_id": { "type": "string" } },
|
||||
"required": ["peer_id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": { "description": "Peer added" },
|
||||
"400": { "$ref": "#/components/responses/Error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/peers/{peer_id}": {
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
|
||||
{ "name": "peer_id", "in": "path", "required": true, "schema": { "type": "string" } }
|
||||
],
|
||||
"delete": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Remove a peer",
|
||||
"responses": {
|
||||
"204": { "description": "Peer removed" },
|
||||
"404": { "$ref": "#/components/responses/Error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/peers/{peer_id}/submit": {
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
|
||||
{ "name": "peer_id", "in": "path", "required": true, "schema": { "type": "string" } }
|
||||
],
|
||||
"post": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "Submit prompt to a peer session",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": { "prompt": { "type": "string" } },
|
||||
"required": ["prompt"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": { "description": "Submitted" },
|
||||
"404": { "$ref": "#/components/responses/Error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/backends": {
|
||||
"get": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "List available backends",
|
||||
"responses": {
|
||||
"200": { "description": "Backend names", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } } } } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/agents": {
|
||||
"get": {
|
||||
"tags": ["dbus"],
|
||||
"summary": "List available agents",
|
||||
"responses": {
|
||||
"200": { "description": "Agent names", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } } } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"SessionSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"state": { "type": "string" },
|
||||
"model": { "type": "string" },
|
||||
"agent": { "type": "string" }
|
||||
},
|
||||
"required": ["id", "state", "model", "agent"]
|
||||
},
|
||||
"CreateSessionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cwd": { "type": "string", "description": "Working directory (defaults to $HOME)" },
|
||||
"backend": { "type": "string", "description": "Backend name (e.g. ollama, anthropic)" },
|
||||
"model": { "type": "string", "description": "Model name" },
|
||||
"agent": { "type": "string", "description": "Agent name (default: 'default')" }
|
||||
}
|
||||
},
|
||||
"Error": {
|
||||
"type": "object",
|
||||
"properties": { "error": { "type": "string" } },
|
||||
"required": ["error"]
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"Error": {
|
||||
"description": "Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/Error" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,15 @@
|
|||
# ollie Architecture
|
||||
|
||||
## Philosophy
|
||||
|
||||
ollie's design philosophy is Emacs: a small, extensible core. The Go runtime (`agent.Agent`) defines what an agent *is* — an event loop, a backend connection, a message history, and three built-in primitives (`shell`, tool registry, skill registry). Everything else — orchestration, scheduling, workflows, UIs — is pushed out to the surrounding environment.
|
||||
|
||||
The primary integration philosophy is Plan 9's "everything is a file." `olliesrv` exposes agent state and behaviors as files in a 9P namespace. Any program that can read and write files can drive an agent: shell scripts, editors, web apps, cron, containers.
|
||||
|
||||
For desktop-native integration, `olliesrv` embeds a D-Bus adapter (`org.ollie.SessionManager`) alongside its 9P interface. The KDE GUI, plasmoid, and web UI connect this way. For D-Bus-only deployments, `-no9p` disables the 9P listener entirely.
|
||||
|
||||
Design principles:
|
||||
- **Small extensible core.** The agent runtime is minimal; capabilities come from composing external scripts.
|
||||
- **Three built-in primitives.** `shell`, tool registry (`tool_load`/`tool_list`/`tool_active`), and skill registry (`skill_load`/`skill_list`/`skill_active`) are the only tools compiled into the core. Everything else is a script.
|
||||
- **Two integration paths.** 9P filesystem (canonical) and/or D-Bus (conventional). Same core, different surfaces, selectable via flags.
|
||||
- **No framework lock-in.** Frontends are decoupled via whichever interface they prefer; the core doesn't know or care.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
Single Go module (`ollie`) with two Git submodules for decoupled frontends:
|
||||
|
||||
```
|
||||
ollie/
|
||||
├── agent/ Agent loop, history, hooks, prompt resolution, commands
|
||||
|
|
@ -36,10 +28,8 @@ ollie/
|
|||
├── cmd/ Binaries:
|
||||
│ ├── olliesrv/ 9P server
|
||||
│ ├── ollie-9p/ 9P client
|
||||
│ ├── ollie-httpgw/ HTTP-to-9P gateway
|
||||
│ └── ollie-remote/ Remote execution server
|
||||
├── kde/ KDE integration (submodule) — plasmoid, GUI, Kate plugin, KRunner, tray
|
||||
├── webui/ Preact SPA browser frontend (submodule)
|
||||
├── agents/ Agent config JSONs (default, coding, orchestrator, worker, ...)
|
||||
├── prompts/ Prompt templates (markdown)
|
||||
├── tools/ Tool scripts (file_read, lsp_*, memory_*, subagent_*, ...)
|
||||
|
|
@ -48,9 +38,7 @@ ollie/
|
|||
├── sandbox/ Sandbox config YAML files
|
||||
└── doc/ Documentation
|
||||
```
|
||||
|
||||
## System Overview
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Frontends["Frontends"]
|
||||
|
|
@ -58,27 +46,22 @@ flowchart TB
|
|||
ACME["acme (Plan 9)"]
|
||||
ELLIE["ellie (Emacs)"]
|
||||
KDE["KDE GUI / Plasmoid"]
|
||||
WEB["Web UI"]
|
||||
end
|
||||
|
||||
subgraph Integration["Integration Layer"]
|
||||
direction LR
|
||||
P9["9P Filesystem"]
|
||||
DBUS["org.ollie.SessionManager (D-Bus)"]
|
||||
end
|
||||
|
||||
subgraph Server["olliesrv"]
|
||||
direction TB
|
||||
NS["9P Namespace\nsession/ · backends · models"]
|
||||
DBA["Embedded D-Bus Adapter"]
|
||||
end
|
||||
|
||||
subgraph Core["Agent Engine (per session)"]
|
||||
LOOP["Agent Loop\n(agent/loop.go)"]
|
||||
SESS["Session State\n(agent/history.go)"]
|
||||
TRV["toolsrv.Server\n· shell execution\n· tool registry\n· landlock sandbox"]
|
||||
end
|
||||
|
||||
subgraph Backends["LLM Backends"]
|
||||
OLLAMA["Ollama"]
|
||||
OPENAI["OpenAI / OpenRouter"]
|
||||
|
|
@ -86,33 +69,24 @@ flowchart TB
|
|||
GEMINI["Gemini"]
|
||||
CW["CodeWhisperer"]
|
||||
end
|
||||
|
||||
TUI --> P9
|
||||
ACME --> P9
|
||||
ELLIE --> P9
|
||||
KDE --> DBUS
|
||||
WEB --> DBUS
|
||||
|
||||
P9 --> NS
|
||||
DBUS --> DBA
|
||||
|
||||
NS --> LOOP
|
||||
DBA --> LOOP
|
||||
|
||||
LOOP <--> SESS
|
||||
LOOP --> TRV
|
||||
LOOP <--> Backends
|
||||
|
||||
TRV --> Shell["sandboxed shell\n(landrun)"]
|
||||
TRV --> Tools["tool scripts\n(OLLIE_TOOLS_PATH)"]
|
||||
```
|
||||
|
||||
## Core Library
|
||||
|
||||
The core is a single Go module (`ollie`) with no binary. Binaries live in `cmd/`.
|
||||
|
||||
### Package Layout
|
||||
|
||||
| Package | Purpose |
|
||||
|---|---|
|
||||
| `agent/` | Agent struct, loop, history, compaction, hooks, commands, prompt resolution, state |
|
||||
|
|
@ -125,15 +99,12 @@ The core is a single Go module (`ollie`) with no binary. Binaries live in `cmd/`
|
|||
| `env/` | Session environment helpers |
|
||||
| `log/` | Structured logging |
|
||||
| `paths/` | XDG path resolution |
|
||||
|
||||
### Key Types
|
||||
|
||||
**`agent.Agent`** — the concrete struct frontends drive:
|
||||
```go
|
||||
type Agent struct {
|
||||
// unexported fields — no public interface
|
||||
}
|
||||
|
||||
func New(cfg AgentConfig) *Agent
|
||||
func (a *Agent) Submit(ctx context.Context, input string)
|
||||
func (a *Agent) Interrupt(cause error) bool
|
||||
|
|
@ -143,7 +114,6 @@ func (a *Agent) WaitChange(ctx context.Context, field, current string) (string,
|
|||
func (a *Agent) Close()
|
||||
// ... model/backend info, CWD, usage, cost, etc.
|
||||
```
|
||||
|
||||
**`backend.Backend`** — LLM provider contract:
|
||||
```go
|
||||
type Backend interface {
|
||||
|
|
@ -155,7 +125,6 @@ type Backend interface {
|
|||
Models(ctx context.Context) []string
|
||||
}
|
||||
```
|
||||
|
||||
**`toolsrv.Runner`** — minimal tool execution interface:
|
||||
```go
|
||||
type Runner interface {
|
||||
|
|
@ -163,21 +132,16 @@ type Runner interface {
|
|||
CallTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error)
|
||||
}
|
||||
```
|
||||
|
||||
**`toolsrv.Server`** — the concrete tool server (implements Runner):
|
||||
```go
|
||||
type Server struct { /* ... */ }
|
||||
```
|
||||
|
||||
### Agent Loop (`agent/loop.go`)
|
||||
|
||||
The loop is the heart of the system. On each turn:
|
||||
|
||||
1. **Stream** — call `backend.ChatStream()` with messages + tool definitions
|
||||
2. **Dispatch** — if the model returns tool calls, execute them via `toolsrv.Server`
|
||||
3. **Update** — append assistant message + tool results to session history
|
||||
4. **Repeat** — loop until the model produces a final text response (no tool calls)
|
||||
|
||||
Safety mechanisms:
|
||||
- **Consecutive error limits**: soft nudge at 5, hard abort at 10
|
||||
- **Replan gate**: after 8 rounds without a PLAN: block, inject a replan nudge
|
||||
|
|
@ -185,11 +149,8 @@ Safety mechanisms:
|
|||
- **Max steps**: configurable per-agent; soft exit when reached
|
||||
- **Transient retry**: up to 3 retries with exponential backoff for rate limits and 5xx
|
||||
- **Context overflow**: triggers automatic compaction and retry
|
||||
|
||||
### Session & Context Management (`agent/history.go`)
|
||||
|
||||
`History` owns the message history as a flat `[]backend.Message` slice. Key behaviors:
|
||||
|
||||
- **TaskState**: structured JSON overlay (objective, plan_step, constraints, last_action, next_decision) injected at the top of every turn
|
||||
- **Compaction**: three-zone strategy when context reaches 50% of model limit:
|
||||
- **Cold zone**: structured task state summary
|
||||
|
|
@ -197,11 +158,8 @@ Safety mechanisms:
|
|||
- **Hot zone**: last 8 messages verbatim
|
||||
- **Result tiers**: tool results classified as Hot (verbatim), Warm (summarized on compaction), or Cold (immediately collapsed)
|
||||
- **Persistence**: sessions saved as JSON to `{sessionsDir}/{id}.json`
|
||||
|
||||
### Hooks (`agent/hooks.go`)
|
||||
|
||||
Lifecycle callbacks executed as shell commands with JSON payload on stdin:
|
||||
|
||||
| Hook | When | Exit codes |
|
||||
|---|---|---|
|
||||
| `agentSpawn` | Session creation | 0=ok, 2=block |
|
||||
|
|
@ -212,17 +170,11 @@ Lifecycle callbacks executed as shell commands with JSON payload on stdin:
|
|||
| `preCompact` | Before compaction | 0=ok |
|
||||
| `postCompact` | After compaction | 0=ok |
|
||||
| `turnError` | On backend error | 0=handled (skip retries) |
|
||||
|
||||
### Prompt Resolution (`agent/prompt_resolver.go`)
|
||||
|
||||
Agent configs declare prompts as a JSON array of shell commands. Each command is executed with `$OLLIE` and `$PWD` available; stdout is concatenated to form the system prompt. This enables dynamic, composable prompts assembled from template fragments.
|
||||
|
||||
## 9P Server (`cmd/olliesrv/`)
|
||||
|
||||
`olliesrv` is the central daemon. It implements a 9P2000 file server that exposes the entire agent namespace:
|
||||
|
||||
### Namespace Layout
|
||||
|
||||
```
|
||||
session/
|
||||
├── new write key=value to create session
|
||||
|
|
@ -261,24 +213,18 @@ session/
|
|||
│ ├── tail exec helper
|
||||
│ └── proc/ detached background processes
|
||||
```
|
||||
|
||||
### 9P Interaction Model
|
||||
|
||||
All agent interaction uses the `ollie-9p` client, which auto-discovers the server via `$NAMESPACE` and identifies the caller via `$OLLIE_UNAME`. The 9P filesystem exposes session state, control, and inter-agent communication as files:
|
||||
- Sessions are directories under `session/`
|
||||
- Prompts, state, chat history, and config are readable/writable files
|
||||
- Permission enforcement uses 9P user principals
|
||||
|
||||
### Permission Model (`fs/session/perm.go`)
|
||||
|
||||
All permissions are declared in a single registry (`Perms`). Key design:
|
||||
- `prompt` is mode `0666` — group+other can write, owner (the agent) cannot
|
||||
- `chat` is mode `0444` — read-only for everyone
|
||||
- `ctl` is mode `0666` — anyone can send control commands
|
||||
- `plan` is mode `0666` — readable by peer agents
|
||||
|
||||
### Session Management (`fs/session/`)
|
||||
|
||||
The `*fs.Tree` IS the session collection. Package functions manage lifecycle:
|
||||
- `NewRoot(cfg)` — create the root tree
|
||||
- `Lookup(tree, id)` — find a session by ID
|
||||
|
|
@ -287,25 +233,16 @@ The `*fs.Tree` IS the session collection. Package functions manage lifecycle:
|
|||
- `KillFromRoot(tree, id)` — kill a session
|
||||
- `RenameFromRoot(tree, old, new)` — rename a session
|
||||
- `Shutdown(tree)` — clean shutdown
|
||||
|
||||
### Embedded D-Bus Adapter
|
||||
|
||||
`olliesrv` includes an embedded D-Bus adapter that claims `org.ollie.SessionManager` on the session bus. This provides:
|
||||
- The same method/signal interface used by KDE frontends
|
||||
- `SessionCreated`, `SessionKilled`, `StateChanged`, `ChatUpdated` signals
|
||||
- Allows KDE GUI, plasmoid, and web UI (via `httpgw`) to interact with sessions without speaking 9P
|
||||
- Lifecycle watchers that bridge session state changes to D-Bus signals
|
||||
|
||||
The adapter is best-effort: if no session bus is available (e.g., headless/container), `olliesrv` continues functioning as a pure 9P server.
|
||||
|
||||
## D-Bus Adapter (`dbus/`)
|
||||
|
||||
The D-Bus adapter is embedded in `olliesrv` (no separate daemon). For D-Bus-only deployments, run `olliesrv -no9p`. This disables the filesystem listener while keeping the full D-Bus interface. Session persistence uses the same `~/.local/share/ollie/sessions/active/` directory.
|
||||
|
||||
## KDE Integration (`kde/`)
|
||||
|
||||
The KDE submodule provides several Qt/QML components that connect to `org.ollie.SessionManager` over D-Bus:
|
||||
|
||||
| Component | Path | Purpose |
|
||||
|---|---|---|
|
||||
| GUI | `kde/gui/` | Standalone Qt window for chat interaction |
|
||||
|
|
@ -313,27 +250,18 @@ The KDE submodule provides several Qt/QML components that connect to `org.ollie.
|
|||
| Kate plugin | `kde/kate/` | In-editor chat pane and ghost completion |
|
||||
| KRunner | `kde/krunner/` | Quick-launch sessions from KRunner |
|
||||
| System tray | `kde/tray/` | Tray icon with session status |
|
||||
|
||||
All KDE components use `QDBusServiceWatcher` to handle daemon restarts gracefully — recreating the `QDBusInterface` when the service reappears and refreshing the session list.
|
||||
|
||||
## Tool System
|
||||
|
||||
### Architecture
|
||||
|
||||
The tool system has exactly one built-in server (`toolsrv.Server`) that exposes three primitives:
|
||||
|
||||
| Primitive | Purpose |
|
||||
|---|---|
|
||||
| `shell` | Run a bash command in a sandbox |
|
||||
| `tool_load`/`tool_list`/`tool_active` | Dynamic tool registry |
|
||||
| `skill_load`/`skill_list`/`skill_active` | Dynamic skill registry |
|
||||
|
||||
All other capabilities are **tool scripts** — executable files discovered from `OLLIE_TOOLS_PATH` (default: `~/.config/ollie/tools/`).
|
||||
|
||||
### Tool Scripts
|
||||
|
||||
Tool scripts are plain executables with a structured header comment declaring their name, description, parameters, and parallelism annotation:
|
||||
|
||||
- `file_read`, `file_write`, `file_edit`, `file_glob`, `file_grep` — filesystem I/O
|
||||
- `lsp_definition`, `lsp_references`, `lsp_rename`, `lsp_symbols`, `lsp_diagnostics`, `lsp_hover`, `lsp_completion` — LSP bridge (Python)
|
||||
- `memory_remember`, `memory_recall` — persistent memory
|
||||
|
|
@ -341,13 +269,9 @@ Tool scripts are plain executables with a structured header comment declaring th
|
|||
- `subagent_spawn`, `subagent_generate` — sub-agent lifecycle
|
||||
- `route` — orchestrator dispatch
|
||||
- `gui_*` — KDE desktop automation (screenshot, windows, clipboard, input, ...)
|
||||
|
||||
Scripts annotated `ollie:parallel read` can be fanned out concurrently by the agent loop.
|
||||
|
||||
### Sandboxing
|
||||
|
||||
Every code execution step is wrapped with [landrun](https://github.com/landlock-lsm/landrun) (Landlock LSM). Configuration is YAML-based:
|
||||
|
||||
```yaml
|
||||
general:
|
||||
best_effort: true
|
||||
|
|
@ -359,13 +283,9 @@ network:
|
|||
enabled: true
|
||||
unrestricted: true
|
||||
```
|
||||
|
||||
Template variables (`{CWD}`, `{HOME}`, `{XDG_*}`, `{OLLIE_*}`) are expanded at runtime. **landrun is mandatory** — there is no unsandboxed fallback. Steps marked `elevated: true` bypass the sandbox via the elevation backend (`x/elevate`).
|
||||
|
||||
## LLM Backends (`backend/`)
|
||||
|
||||
All backends implement the streaming-only `Backend` interface:
|
||||
|
||||
| Backend | Wire Format | Notes |
|
||||
|---|---|---|
|
||||
| `OllamaBackend` | `/api/chat` | Local models, context length from `/api/show` |
|
||||
|
|
@ -374,63 +294,43 @@ All backends implement the streaming-only `Backend` interface:
|
|||
| `GeminiBackend` | Gemini API | Google Gemini models |
|
||||
| `CopilotBackend` | GitHub Copilot API | Copilot Chat |
|
||||
| `CodeWhispererBackend` | CodeWhisperer API | AWS CodeWhisperer |
|
||||
|
||||
Shared infrastructure:
|
||||
- `streamRequest()` — common HTTP streaming with error classification
|
||||
- `RateLimitError`, `TransientError`, `ContextOverflowError`, `ToolUnsupportedError` — typed errors for loop control
|
||||
- `GenerationParams` — unified sampling parameters across all backends
|
||||
|
||||
## Multi-Agent System
|
||||
|
||||
Multi-agent coordination is built entirely on the filesystem primitive:
|
||||
|
||||
### Mechanism
|
||||
|
||||
1. **Spawn**: write session spec to `session/new` → new session directory appears
|
||||
2. **Prompt**: write task to `session/{id}/prompt`
|
||||
3. **Observe**: read `session/{id}/state` or block on `session/{id}/statewait`
|
||||
4. **Result**: child writes to `session/{parent_id}/prompt` when done
|
||||
|
||||
### Delegation Tools
|
||||
|
||||
| Tool | Semantics |
|
||||
|---|---|
|
||||
| `route` | Orchestrator dispatch; selects backend+model for a task |
|
||||
| `subagent_spawn` | Raw session creation with full control |
|
||||
| `subagent_generate` | JIT agent identity generation |
|
||||
|
||||
### Callback Protocol
|
||||
|
||||
```
|
||||
[from={session_id}]
|
||||
STATUS: done|error
|
||||
SUMMARY: <result>
|
||||
ARTIFACTS: <paths>
|
||||
```
|
||||
|
||||
The parent never actively waits — results arrive as queued prompts.
|
||||
|
||||
## HTTP Gateway (`cmd/ollie-httpgw/`)
|
||||
|
||||
`ollie-httpgw` bridges HTTP clients (like the web UI) to the 9P filesystem. It translates REST-style requests into 9P filesystem operations and streams events back as Server-Sent Events.
|
||||
|
||||
This is the secondary integration path for non-LLM consumers (web UIs, dashboards, external tools) that want a conventional HTTP API without speaking 9P directly.
|
||||
|
||||
## Frontends
|
||||
|
||||
Frontends connect via either 9P (filesystem) or D-Bus, depending on preference:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Core["agent.Agent"]
|
||||
AG["Agent Engine"]
|
||||
end
|
||||
|
||||
subgraph Surfaces["Integration Surfaces"]
|
||||
P9["9P Filesystem\n(session/ namespace)"]
|
||||
DB["D-Bus\n(org.ollie.SessionManager)"]
|
||||
end
|
||||
|
||||
subgraph Frontends
|
||||
SH["s/sh (terminal)"]
|
||||
ACME["acme (Plan 9)"]
|
||||
|
|
@ -439,41 +339,30 @@ flowchart LR
|
|||
KP["KDE Plasmoid"]
|
||||
KK["Kate Plugin"]
|
||||
KR["KRunner"]
|
||||
WEB["Web UI"]
|
||||
HTTP["curl / scripts"]
|
||||
end
|
||||
|
||||
AG --> P9
|
||||
AG --> DB
|
||||
|
||||
SH --> P9
|
||||
ACME --> P9
|
||||
EL --> P9
|
||||
HTTP --> P9
|
||||
|
||||
KG --> DB
|
||||
KP --> DB
|
||||
KK --> DB
|
||||
KR --> DB
|
||||
WEB --> DB
|
||||
```
|
||||
|
||||
Frontends are fully decoupled — the core doesn't know which interface they use.
|
||||
|
||||
## Script Namespaces
|
||||
|
||||
User-facing scripts installed to `~/.config/ollie/scripts/{ns}/`:
|
||||
|
||||
| Namespace | Purpose | Key Scripts |
|
||||
|---|---|---|
|
||||
| `s/` | Session management | `sh` (interactive), `b` (one-shot), `bfg` (blocking), `bbg` (background) |
|
||||
| `u/` | Utility compositions | `complete` (code completion), `optimize` (prompt optimization), `cascade`, `escalate` |
|
||||
| `x/` | System plugins | `elevate` (privilege escalation), `prime` (prompt template renderer), `freeloader` |
|
||||
|
||||
## Agent Configuration
|
||||
|
||||
Agent configs are JSON files in `~/.config/ollie/agents/`:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": [
|
||||
|
|
@ -492,11 +381,8 @@ Agent configs are JSON files in `~/.config/ollie/agents/`:
|
|||
"maxSteps": 50
|
||||
}
|
||||
```
|
||||
|
||||
The `prompt` array is the key innovation: each element is a shell command whose stdout is concatenated to form the system prompt. This makes prompts composable, dynamic, and environment-aware.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant F as Frontend
|
||||
|
|
@ -505,17 +391,14 @@ sequenceDiagram
|
|||
participant B as LLM Backend
|
||||
participant T as toolsrv.Server
|
||||
participant SH as sandbox (landrun)
|
||||
|
||||
F->>S: write to session/{id}/prompt
|
||||
S->>A: Submit()
|
||||
activate A
|
||||
|
||||
loop Agent Loop
|
||||
A->>B: ChatStream(messages, tools)
|
||||
activate B
|
||||
B-->>A: stream events
|
||||
deactivate B
|
||||
|
||||
alt has tool calls
|
||||
A->>T: Dispatch(tool, args)
|
||||
activate T
|
||||
|
|
@ -529,14 +412,11 @@ sequenceDiagram
|
|||
break
|
||||
end
|
||||
end
|
||||
|
||||
A-->>S: turn complete
|
||||
deactivate A
|
||||
S-->>F: readable via session/{id}/chat
|
||||
```
|
||||
|
||||
## Extension Points
|
||||
|
||||
| What | How |
|
||||
|---|---|
|
||||
| Add an LLM backend | Implement `backend.Backend` |
|
||||
|
|
@ -546,9 +426,7 @@ sequenceDiagram
|
|||
| Add a skill | Drop a markdown file into `skills/` |
|
||||
| Add an agent persona | Create a JSON config in `agents/` |
|
||||
| Customize sandbox | Create a YAML in `~/.config/ollie/sandbox/` |
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **Two integration surfaces** — 9P filesystem (canonical, Unix philosophy) and D-Bus (conventional, for GUI/web consumers). Both are thin adapters over the same core.
|
||||
2. **Three built-in primitives** — `shell`, tool registry, skill registry. Everything else is a script. This keeps the core small and makes all other tools equal citizens.
|
||||
3. **Mandatory sandboxing** — no unsandboxed fallback. Security is not optional.
|
||||
|
|
|
|||
|
|
@ -1,45 +1,34 @@
|
|||
# Architectural Evolution
|
||||
|
||||
How the Ollie system-of-systems emerged and evolved.
|
||||
~1300 commits over ~3.5 months (Apr 11 – Jul 30, 2026).
|
||||
|
||||
## Timeline
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title Ollie Evolution
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %b %d
|
||||
|
||||
section Foundation
|
||||
Monorepo + submodules :done, 2026-04-11, 14d
|
||||
9P filesystem :done, 2026-04-14, 28d
|
||||
|
||||
section Core Primitives
|
||||
Session lifecycle (session/) :done, 2026-04-20, 30d
|
||||
Tool scripts :done, 2026-05-01, 20d
|
||||
Skills system :done, 2026-05-10, 14d
|
||||
|
||||
section Execution
|
||||
execute_code / shell :done, 2026-05-08, 20d
|
||||
Batch jobs (b/ → session/bfg) :done, 2026-05-15, 25d
|
||||
Sandbox (landrun) :done, 2026-05-20, 40d
|
||||
|
||||
section Multi-Agent
|
||||
Subagent spawn :done, 2026-05-25, 15d
|
||||
Cascade orchestrator :done, 2026-06-05, 20d
|
||||
JIT agent generation :done, 2026-07-10, 10d
|
||||
|
||||
section Frontends
|
||||
TUI + Emacs (ellie) :done, 2026-04-15, 45d
|
||||
Acme (Plan 9) :done, 2026-05-20, 30d
|
||||
Web UI + httpgw :done, 2026-06-01, 25d
|
||||
KDE/Plasma :done, 2026-06-10, 40d
|
||||
|
||||
section Control Planes
|
||||
D-Bus adapter (ollied) :done, 2026-06-25, 20d
|
||||
D-Bus embedded in srv :done, 2026-07-15, 10d
|
||||
|
||||
section Late Evolution
|
||||
Remote execution :done, 2026-07-05, 15d
|
||||
Elevation broker :done, 2026-07-10, 15d
|
||||
|
|
@ -48,52 +37,35 @@ gantt
|
|||
FUSE → 9P client :done, 2026-07-25, 5d
|
||||
The Great Flattening :done, 2026-07-29, 2d
|
||||
```
|
||||
|
||||
## Phase 1: Monorepo Bootstrap (Apr 11)
|
||||
|
||||
Started as independent git repos unified under a monorepo with submodules.
|
||||
Initial components:
|
||||
|
||||
- **agent** — agent loop (Go): backend dispatch, tool execution, session state
|
||||
- **fs/session** — 9P file server exposing agent sessions as a synthetic filesystem
|
||||
- **tui** — terminal UI (later removed)
|
||||
- **el** — Emacs integration (ellie.el)
|
||||
|
||||
Build system was `mkfile` (Plan 9 make), later `Makefile`, finally `justfile`.
|
||||
|
||||
## Phase 2: The 9P Decision (Apr 14–28)
|
||||
|
||||
The defining architectural choice: **every agent primitive is a file**.
|
||||
|
||||
Sessions are directories. Sending a prompt is writing to `session/{id}/prompt`.
|
||||
Reading state is `cat session/{id}/state`. This made the control plane
|
||||
protocol-agnostic — any program that can read/write files can be a frontend.
|
||||
|
||||
Key files crystallized: `prompt`, `chat`, `state`, `ctl`, `cfg`, `statewait`.
|
||||
|
||||
The `session/new` file (write key=value pairs, read back session ID) became the
|
||||
session factory.
|
||||
|
||||
## Phase 3: Tool System (Apr 28 – May 10)
|
||||
|
||||
Tools evolved through several incarnations:
|
||||
|
||||
1. **MCP servers** (denote-mcp, 9beads-mcp) — external processes, heavyweight
|
||||
2. **Shell/Python scripts** in `~/.config/ollie/tools/` — lightweight, sandboxed
|
||||
3. **execute_code** as the single built-in tool — all scripts invoked through it
|
||||
4. **call_tool/pipe** — named tool dispatch and cross-tool pipelines
|
||||
5. **Tool registry** (final form) — dynamic lazy-loading via `tool_list`/`tool_load`/`tool_active`; `execute_code` renamed to `shell`; tools hot-reloadable without server restart
|
||||
|
||||
The file tools (`file_read`, `file_write`, `file_edit`, `file_grep`, `file_glob`)
|
||||
were extracted into standalone Python scripts. Memory (`memory_remember`,
|
||||
`memory_recall`) and reasoning (`reasoning_think`) followed the same pattern.
|
||||
|
||||
MCP servers were removed early. Simple scripts won over complex daemons.
|
||||
|
||||
## Phase 4: Planning System Churn (May – Jul)
|
||||
|
||||
Planning went through the most iterations of any subsystem:
|
||||
|
||||
1. `pl/` directory namespace with flat file-per-task
|
||||
2. `plan_create` / `plan_complete` tools
|
||||
3. **Beads** integration (external issue tracker) — made optional, then dropped
|
||||
|
|
@ -101,18 +73,13 @@ Planning went through the most iterations of any subsystem:
|
|||
5. Inline markdown plans forbidden
|
||||
6. **Final form**: a single `session/{id}/plan` file (markdown checklist), written by
|
||||
the agent, persisted across context compaction
|
||||
|
||||
The lesson: planning needed to be simple, agent-controlled, and not bureaucratic.
|
||||
|
||||
## Phase 5: Multi-Agent Coordination (May 25 – Jun 15)
|
||||
|
||||
Three patterns emerged:
|
||||
|
||||
- **subagent_spawn** — fire-and-forget session creation, parent never blocks
|
||||
- **subagent_generate** — JIT agent config generation (role, constraints, tools)
|
||||
- **cascade** — script-driven fan-out with `-max-workers`, `-retries`, optional
|
||||
synthesis step
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
UP["User prompt"]
|
||||
|
|
@ -123,7 +90,6 @@ flowchart TB
|
|||
CASCADE["u/cascade\n(spawn + throttle)"]
|
||||
CW1["Cascade Worker"]
|
||||
CW2["Cascade Worker"]
|
||||
|
||||
UP --> PARENT
|
||||
PARENT -- subagent_spawn --> W1
|
||||
PARENT -- subagent_spawn --> W2
|
||||
|
|
@ -135,23 +101,18 @@ flowchart TB
|
|||
CASCADE --> CW1
|
||||
CASCADE --> CW2
|
||||
```
|
||||
|
||||
Inter-agent communication uses the filesystem: agents write to each other's
|
||||
`prompt` file.
|
||||
|
||||
## Phase 6: Frontend Proliferation
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Core["agent.Agent"]
|
||||
AG["Agent Engine"]
|
||||
end
|
||||
|
||||
subgraph Surfaces["Integration Surfaces"]
|
||||
P9["9P Filesystem\n(session/ namespace)"]
|
||||
DB["D-Bus\n(org.ollie.SessionManager)"]
|
||||
end
|
||||
|
||||
subgraph Frontends
|
||||
SH["s/sh (terminal)"]
|
||||
ACME["acme (Plan 9)"]
|
||||
|
|
@ -160,35 +121,26 @@ flowchart TB
|
|||
KP["KDE Plasmoid"]
|
||||
KK["Kate Plugin"]
|
||||
KR["KRunner"]
|
||||
WEB["Web UI"]
|
||||
HTTP["curl / scripts"]
|
||||
end
|
||||
|
||||
AG --> P9
|
||||
AG --> DB
|
||||
|
||||
SH --> P9
|
||||
ACME --> P9
|
||||
EL --> P9
|
||||
HTTP --> P9
|
||||
|
||||
KG --> DB
|
||||
KP --> DB
|
||||
KK --> DB
|
||||
KR --> DB
|
||||
WEB --> DB
|
||||
```
|
||||
|
||||
- **s/sh** — shell script frontend (bash, briefly rc, back to bash)
|
||||
- **acme** — Plan 9 editor integration with `Oi` (quick query), `Kmpl` (completion)
|
||||
- **ellie.el** — Emacs with ghost-text completion
|
||||
- **httpgw** — HTTP→9P gateway
|
||||
- **webui** — browser frontend via httpgw
|
||||
- **ollie-kde** — full Plasma integration: plasmoid, KRunner, Kate plugin, standalone GUI, GUI automation tools
|
||||
- **TUI** — removed (Jul) in favor of s/sh and richer GUIs
|
||||
|
||||
## Phase 7: Security Model (May – Jul)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Permissions["9P Permissions"]
|
||||
|
|
@ -196,7 +148,6 @@ flowchart LR
|
|||
AGENT["Agent → restricted"]
|
||||
PEERS["Peers → read-only"]
|
||||
end
|
||||
|
||||
subgraph Execution["Execution Sandbox"]
|
||||
AGT["Agent"]
|
||||
SANDBOX["landrun sandbox\n(restricted fs)"]
|
||||
|
|
@ -204,59 +155,42 @@ flowchart LR
|
|||
ELEV["x/elevate\n(socket broker)"]
|
||||
PRIV["Privileged Action"]
|
||||
end
|
||||
|
||||
AGT --> SANDBOX --> TOOLS
|
||||
TOOLS -- needs escape --> ELEV --> PRIV
|
||||
```
|
||||
|
||||
Evolution:
|
||||
|
||||
1. No sandboxing initially
|
||||
2. YAML-based sandbox configs (landrun) for execute_code
|
||||
3. Per-session 9P identity — agents can't read other sessions' tools
|
||||
4. Elevation broker: socket-based, user-confirmed privilege escalation
|
||||
5. SSH agent proxy added then removed (too much attack surface)
|
||||
6. Final: integrated elevation broker replaces separate superpowerd adapter
|
||||
|
||||
## Phase 8: D-Bus as Second Control Plane (Jun 25 – Jul)
|
||||
|
||||
Originally everything was 9P-only. D-Bus was added for desktop integration:
|
||||
|
||||
1. Separate `ollie-dbus` daemon (ollied)
|
||||
2. Embedded directly into `olliesrv` (9P server got `-no9p` flag)
|
||||
3. KDE uses D-Bus exclusively; acme/sh/el use 9P
|
||||
4. **9P and D-Bus do not share sessions** — independent stores, same core
|
||||
|
||||
## Phase 9: Remote Execution (Jul 5–19)
|
||||
|
||||
Split-brain architecture: orchestration stays local, code execution on a remote
|
||||
machine via SSH.
|
||||
|
||||
- `ollie-remote` binary auto-deployed via SSH bootstrap
|
||||
- Tools embedded in remote binary
|
||||
- Streaming output notifications back to local session
|
||||
- Session persistence across remote restarts
|
||||
|
||||
## Phase 10: Tool Registry (Jul 20–29)
|
||||
|
||||
The final major architectural shift — from "agent knows all tools upfront" to
|
||||
lazy discovery:
|
||||
|
||||
1. Tools embed their own `ollie:prompt` metadata blocks
|
||||
2. `tool_list` — discover available tools and descriptions
|
||||
3. `tool_load` — promote a tool to a native callable
|
||||
4. `tool_active` — introspect loaded tools
|
||||
5. Skills get the same treatment: `skill_list`, `skill_load`, `skill_active`
|
||||
|
||||
This solved system prompt bloat — only load what's needed per task.
|
||||
|
||||
## Phase 11: The Great Flattening (Jul 29–30)
|
||||
|
||||
Largest single-day structural change: **−4,573 lines net** across the codebase.
|
||||
Eliminated accumulated abstractions that no longer served a purpose.
|
||||
|
||||
### Core (−4,400 lines)
|
||||
|
||||
- **Killed `agent.Core` interface** — concrete `*agent.Agent` used directly.
|
||||
Nobody else implemented Core; the interface just added indirection.
|
||||
- **Killed `Dispatcher` indirection** in toolsrv — `toolsrv.Server` called directly.
|
||||
|
|
@ -268,9 +202,7 @@ Eliminated accumulated abstractions that no longer served a purpose.
|
|||
- **Threaded context.Context** properly through daemon → session → agent
|
||||
(replaced ad-hoc interrupt mechanisms with cancellation).
|
||||
- Dead code removal: `GlobalToolNames`, `ExtractReturnSchema`.
|
||||
|
||||
### 9P server (−214 lines)
|
||||
|
||||
- **Killed `mgr/` package entirely** — replaced `Manager` struct with package
|
||||
functions in `fs/session/`. The `*fs.Tree` IS the session collection; `rootState`
|
||||
(unexported) lives in `tree.Data`. CRUD via `NewRoot`, `Lookup`, `Create`,
|
||||
|
|
@ -283,17 +215,13 @@ Eliminated accumulated abstractions that no longer served a purpose.
|
|||
files, FUSE kernel saw 0 bytes and never issued a read.
|
||||
- **Fixed `session.All()`** — was looking at `root.Children()` (empty) instead
|
||||
of `rootState.sessions`. Broke D-Bus ListSessions and GUI session restore.
|
||||
|
||||
### Frontends
|
||||
|
||||
- **acme**: paths updated `s/` → `session/`, agent files route through
|
||||
`session/{id}/agent/{aid}/`.
|
||||
- **ellie.el**: same path migration, added `ellie--agent-dir` for agent ID
|
||||
discovery.
|
||||
- **KDE GUI**: no changes needed (D-Bus operates on Go objects, not paths).
|
||||
|
||||
### New filesystem layout
|
||||
|
||||
```
|
||||
session/
|
||||
├── new write key=value to create session
|
||||
|
|
@ -325,14 +253,11 @@ session/
|
|||
│ ├── tail exec helper
|
||||
│ └── proc/ detached process output
|
||||
```
|
||||
|
||||
### Architecture after (single Go module, no submodules for core/9p)
|
||||
|
||||
```
|
||||
*fs.Tree (root) ← IS the session collection
|
||||
└── .Data = *rootState ← sessions map, config, nextUID
|
||||
└── sessions[id] = *Session ← owns Agent, context, cancel
|
||||
|
||||
Package functions (no Manager):
|
||||
session.NewRoot(cfg) → *fs.Tree
|
||||
session.Lookup(tree, id) → *Session
|
||||
|
|
@ -342,21 +267,15 @@ Package functions (no Manager):
|
|||
session.RenameFromRoot(tree, old, new)
|
||||
session.Shutdown(tree)
|
||||
```
|
||||
|
||||
### SLOC after flattening
|
||||
|
||||
| Component | Lines |
|
||||
|-----------|-------|
|
||||
| agent + backend + toolsrv + session | 10,437 |
|
||||
| fs/session + cmd/olliesrv | 7,925 |
|
||||
| kde gui | 12,331 |
|
||||
| el + acme + httpgw | 1,845 |
|
||||
| **Total** | **32,538** |
|
||||
|
||||
Zero dead exported functions remain (verified via LSP + grep across all repos).
|
||||
|
||||
## Current Topology
|
||||
|
||||
```
|
||||
ollie/ ← single Go module
|
||||
├── agent/ ← agent loop, history, hooks, commands
|
||||
|
|
@ -372,9 +291,8 @@ ollie/ ← single Go module
|
|||
├── log/ ← structured logging
|
||||
├── paths/ ← XDG path resolution
|
||||
├── mount/ ← 9P FUSE mount client
|
||||
├── cmd/ ← binaries (olliesrv, ollie-9p, ollie-httpgw, ollie-remote)
|
||||
├── cmd/ ← binaries (olliesrv, ollie-9p, ollie-remote)
|
||||
├── kde/ ← KDE Plasma (submodule)
|
||||
├── webui/ ← browser frontend (submodule)
|
||||
├── tools/ ← 37 tool scripts (Python)
|
||||
├── scripts/{s,u,x}/ ← filesystem-hosted scripts
|
||||
├── agents/ ← agent configs (JSON)
|
||||
|
|
@ -383,12 +301,9 @@ ollie/ ← single Go module
|
|||
├── sandbox/ ← landrun sandbox profiles (YAML)
|
||||
└── doc/ ← documentation
|
||||
```
|
||||
|
||||
## Principles That Emerged
|
||||
|
||||
1. **Filesystem-as-API** — everything is read/write on synthetic files. No
|
||||
custom protocols needed.
|
||||
2. **Submodule independence** — KDE and webui have their own repos, tests, build.
|
||||
Monorepo coordinates versions.
|
||||
3. **Scripts over servers** — MCP servers removed early. Simple scripts won.
|
||||
4. **Progressive disclosure** — lazy tool/skill loading keeps system prompts
|
||||
|
|
@ -399,9 +314,7 @@ ollie/ ← single Go module
|
|||
Same core, different surfaces.
|
||||
7. **Security by default** — sandboxed execution with explicit elevation,
|
||||
per-session identity.
|
||||
|
||||
## Dead Ends and Reversals
|
||||
|
||||
| What | Why it was removed |
|
||||
|------|-------------------|
|
||||
| MCP servers | Too heavyweight; scripts are simpler and faster |
|
||||
|
|
|
|||
153
doc/USAGE.md
153
doc/USAGE.md
|
|
@ -495,159 +495,6 @@ OLLIE_TOOLS_PATH=~/mnt/toolserver/t
|
|||
OLLIE_TRANSCRIPT_PATH=~/mnt/archive/tr
|
||||
```
|
||||
|
||||
## HTTP gateway
|
||||
|
||||
`ollie-httpgw` is an HTTP gateway that translates REST-style HTTP calls into 9P
|
||||
filesystem operations. It lets any HTTP client — including the web UI, `curl`, or
|
||||
custom scripts — interact with `olliesrv` without a 9P client library.
|
||||
|
||||
### Build
|
||||
|
||||
```sh
|
||||
just httpgw
|
||||
```
|
||||
|
||||
Produces `~/bin/ollie-httpgw`.
|
||||
|
||||
### Start and stop
|
||||
|
||||
Like `olliesrv`, the gateway supports daemon management:
|
||||
|
||||
```sh
|
||||
ollie-httpgw start # start in background (daemonizes)
|
||||
ollie-httpgw fgstart # start in foreground
|
||||
ollie-httpgw stop # stop the daemon
|
||||
ollie-httpgw status # check if running
|
||||
```
|
||||
|
||||
### Run (legacy)
|
||||
|
||||
For backwards compatibility, running without a subcommand starts in foreground:
|
||||
|
||||
```sh
|
||||
ollie-httpgw # connect to local olliesrv via namespace; listen on :8080
|
||||
ollie-httpgw -listen :9090 # different port
|
||||
ollie-httpgw -net tcp -addr host:9564 # connect to remote olliesrv over TCP
|
||||
```
|
||||
|
||||
**Flags**
|
||||
|
||||
| Flag | Default | Description |
|
||||
|---|---|---|
|
||||
| `-listen` | `:8080` | HTTP listen address |
|
||||
| `-service` | `ollie` | 9P service name in the local namespace |
|
||||
| `-net` | `` | Network type (`tcp`, `unix`); empty = use namespace |
|
||||
| `-addr` | `` | 9P server address; required when `-net` is set |
|
||||
|
||||
The gateway must be able to reach `olliesrv`. For a local server started with
|
||||
`olliesrv`, the defaults work as-is. For a remote server started with
|
||||
`olliesrv -tcp :9564`, use `-net tcp -addr remotehost:9564`.
|
||||
|
||||
### HTTP → 9P mapping
|
||||
|
||||
Each HTTP method maps to a 9P operation:
|
||||
|
||||
| Method | 9P operation | Example |
|
||||
|---|---|---|
|
||||
| `GET` | read file / list dir | `GET /s/idx` |
|
||||
| `POST` | write to file | `POST /session/new`, `POST /session/{id}/prompt` |
|
||||
| `DELETE` | remove | `DELETE /session/{id}` |
|
||||
| `PATCH` | rename (JSON `{"name":"..."}`) | `PATCH /session/{id}` |
|
||||
| `HEAD` | stat (metadata in `X-9p-*` headers) | `HEAD /session/{id}` |
|
||||
|
||||
The full API is described in `httpgw/openapi.json` and served live at
|
||||
`GET /openapi.json`.
|
||||
|
||||
### Key endpoints
|
||||
|
||||
```sh
|
||||
GET /s/idx # list sessions (JSON array)
|
||||
GET /session/{id}/cfg # session config (key-value JSON)
|
||||
GET /session/{id}/chat # conversation transcript (plain text)
|
||||
POST /session/new # create session (JSON: {cwd, backend, model, agent})
|
||||
POST /session/{id}/prompt # submit prompt (JSON: {prompt: "..."})
|
||||
POST /session/{id}/ctl # send control command (JSON: {ctl: "stop"})
|
||||
DEL /session/{id} # kill and remove session
|
||||
GET /backends # list available backends
|
||||
GET /a/ # agent configs
|
||||
GET /session/{id}/t/ # tool scripts
|
||||
GET /m/ # memory files
|
||||
GET /sk/ # skills
|
||||
```
|
||||
|
||||
```sh
|
||||
# Examples with curl:
|
||||
curl http://localhost:8080/s/idx
|
||||
curl -X POST http://localhost:8080/session/new \
|
||||
-d '{"cwd":"/home/user/project","backend":"anthropic"}'
|
||||
curl -X POST http://localhost:8080/s/mysession/prompt \
|
||||
-d '{"prompt":"summarize the recent commits"}'
|
||||
```
|
||||
|
||||
## Web UI
|
||||
|
||||
The web UI is a Preact single-page application that talks to `ollie-httpgw`. It
|
||||
provides session management, a chat interface, and a basic 9P filesystem browser
|
||||
in the browser.
|
||||
|
||||
### Build
|
||||
|
||||
```sh
|
||||
just webui
|
||||
```
|
||||
|
||||
Requires Node.js and npm. Output lands in `webui/dist/`.
|
||||
|
||||
### Serve
|
||||
|
||||
The `dist/` folder is a static site. Serve it with any HTTP server and proxy the
|
||||
API paths to `ollie-httpgw`:
|
||||
|
||||
**nginx example:**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 3000;
|
||||
root /path/to/ollie/webui/dist;
|
||||
index index.html;
|
||||
|
||||
location / { try_files $uri /index.html; }
|
||||
|
||||
location ~ ^/(s|a|m|t|sk|backends|openapi\.json)(/|$) {
|
||||
proxy_pass http://localhost:8080;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Python one-liner (local testing only):**
|
||||
|
||||
```sh
|
||||
cd webui/dist && python3 -m http.server 3000
|
||||
# API calls will fail — use the Vite dev server instead for local testing
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
Vite's dev server proxies API paths to httpgw automatically:
|
||||
|
||||
```sh
|
||||
# Terminal 1: gateway
|
||||
ollie-httpgw -listen :8080
|
||||
|
||||
# Terminal 2: dev server (hot-reload, proxied API)
|
||||
cd webui && npm run dev
|
||||
# Open http://localhost:5173
|
||||
```
|
||||
|
||||
The proxy configuration is in `webui/vite.config.ts` and targets `localhost:8080`
|
||||
by default.
|
||||
|
||||
### Features
|
||||
|
||||
- Session list in a sidebar with state, model, backend, and cwd
|
||||
- Create, rename, and kill sessions
|
||||
- Send prompts; streaming via 2-second poll
|
||||
- Stop a running agent
|
||||
- Markdown rendering of assistant responses
|
||||
- 9P filesystem browser (navigate `s/`, `a/`, `m/`, `sk/`)
|
||||
- Two built-in themes: `midnight` (dark) and `acme` (Plan 9-inspired light); choice
|
||||
|
|
|
|||
31
justfile
31
justfile
|
|
@ -1,7 +1,7 @@
|
|||
# Ollie monorepo build system
|
||||
#
|
||||
# Two variants:
|
||||
# just Default (KF6): core, 9p, acme, kde, httpgw, webui, emacs
|
||||
# just Default (KF6): core, 9p, acme, kde, emacs
|
||||
# just kf5 KF5/Qt5 variant (Plasma 5.27)
|
||||
#
|
||||
# Build and install are separate phases. No sudo.
|
||||
|
|
@ -20,9 +20,9 @@ kf5: build-kf5 install-kf5
|
|||
|
||||
# === Build targets ===
|
||||
|
||||
build: core ninep acme httpgw webui kde
|
||||
build: core ninep acme kde
|
||||
|
||||
build-kf5: core ninep acme httpgw webui kde-kf5
|
||||
build-kf5: core ninep acme kde-kf5
|
||||
|
||||
core:
|
||||
@echo === Building ollie-core ===
|
||||
|
|
@ -40,14 +40,6 @@ acme:
|
|||
install -m755 cmd/Ollie/scripts/Oi {{bin}}/Oi
|
||||
install -m755 cmd/Ollie/scripts/Theo {{bin}}/Theo
|
||||
|
||||
httpgw:
|
||||
@echo === Building ollie-httpgw ===
|
||||
go build -o {{bin}}/ollie-httpgw ./cmd/ollie-httpgw/
|
||||
|
||||
webui:
|
||||
@echo === Building ollie-webui ===
|
||||
just -f webui/justfile webui
|
||||
|
||||
ollie-remote:
|
||||
rm -rf cmd/ollie-remote/tools && cp -a contrib/tools cmd/ollie-remote/tools
|
||||
@echo === Building ollie-remote ===
|
||||
|
|
@ -70,7 +62,7 @@ install: test install-data install-scripts install-contrib install-kde install-e
|
|||
|
||||
install-kf5: test install-data install-scripts install-contrib install-kde-kf5 install-el
|
||||
install -m644 contrib/prompts/session-9p.md {{cfg}}/prompts/session.md
|
||||
@echo === Install complete \(KF5\) ===
|
||||
@echo === Install complete (KF5) ===
|
||||
@echo 'Restart Plasma to pick up KDE plugins.'
|
||||
|
||||
install-kde:
|
||||
|
|
@ -193,8 +185,6 @@ install-scripts:
|
|||
install -m755 contrib/scripts/u/optimize {{cfg}}/scripts/u/optimize
|
||||
install -m755 contrib/scripts/u/complete {{cfg}}/scripts/u/complete
|
||||
install -m755 contrib/scripts/u/cascade {{cfg}}/scripts/u/cascade
|
||||
|
||||
|
||||
install -m755 contrib/scripts/x/bd {{cfg}}/scripts/x/bd
|
||||
install -m755 contrib/scripts/x/prime {{cfg}}/scripts/x/prime
|
||||
install -m755 contrib/scripts/x/freeloader {{cfg}}/scripts/x/freeloader
|
||||
|
|
@ -219,7 +209,6 @@ install-contrib:
|
|||
|
||||
uninstall:
|
||||
@echo === Uninstalling ollie ===
|
||||
rm -f {{bin}}/ollie-httpgw
|
||||
rm -f {{bin}}/Ollie {{bin}}/Kmpl {{bin}}/Oi {{bin}}/Theo
|
||||
rm -f {{bin}}/ollie-remount {{bin}}/ollie-watchdog
|
||||
rm -rf {{cfg}}
|
||||
|
|
@ -235,8 +224,7 @@ uninstall:
|
|||
@echo === Uninstall complete ===
|
||||
|
||||
uninstall-kf5:
|
||||
@echo === Uninstalling ollie \(KF5\) ===
|
||||
rm -f {{bin}}/ollie-httpgw
|
||||
@echo === Uninstalling ollie (KF5) ===
|
||||
rm -f {{bin}}/Ollie {{bin}}/Kmpl {{bin}}/Oi {{bin}}/Theo
|
||||
rm -f {{bin}}/ollie-remount {{bin}}/ollie-watchdog
|
||||
rm -rf {{cfg}}
|
||||
|
|
@ -249,7 +237,7 @@ uninstall-kf5:
|
|||
rm -rf ~/.config/emacs/ellie
|
||||
# Plasma env
|
||||
rm -f ~/.config/plasma-workspace/env/99-ollie.sh
|
||||
@echo === Uninstall complete \(KF5\) ===
|
||||
@echo === Uninstall complete (KF5) ===
|
||||
|
||||
# === Test targets ===
|
||||
|
||||
|
|
@ -271,7 +259,6 @@ test-remote:
|
|||
|
||||
clean:
|
||||
go clean ./agent/... ./backend/... 2>/dev/null || true
|
||||
rm -f {{bin}}/ollie-httpgw
|
||||
rm -f {{bin}}/Ollie {{bin}}/Kmpl {{bin}}/Oi {{bin}}/Theo
|
||||
rm -f {{bin}}/ollie-remote
|
||||
just -f kde/justfile clean
|
||||
|
|
@ -282,8 +269,8 @@ help:
|
|||
@echo 'Ollie monorepo build targets:'
|
||||
@echo ' just Build + install (default, KF6)'
|
||||
@echo ' just kf5 Build + install (KF5/Qt5, Plasma 5.27)'
|
||||
@echo ' just build Build only (KF6): core, 9p, acme, kde, httpgw, webui'
|
||||
@echo ' just build-kf5 Build only (KF5): core, 9p, acme, kde-kf5, httpgw, webui'
|
||||
@echo ' just build Build only (KF6): core, 9p, acme, kde'
|
||||
@echo ' just build-kf5 Build only (KF5): core, 9p, acme, kde-kf5'
|
||||
@echo ' just install Install (KF6)'
|
||||
@echo ' just install-kf5 Install (KF5)'
|
||||
@echo ' just test Run all tests (core + 9p)'
|
||||
|
|
@ -293,4 +280,4 @@ help:
|
|||
@echo ' just uninstall-kf5 Remove all installed files (KF5)'
|
||||
@echo ' just clean Clean build artifacts'
|
||||
@echo ''
|
||||
@echo 'Individual: just core | ninep | acme | httpgw | webui | kde | kde-kf5 | ollie-remote'
|
||||
@echo 'Individual: just core | ninep | acme | kde | kde-kf5 | ollie-remote'
|
||||
|
|
|
|||
1
webui
1
webui
|
|
@ -1 +0,0 @@
|
|||
Subproject commit c760012f731704ff7a1a6caad7f5b24e6a76161b
|
||||
Loading…
Reference in New Issue