Skip to content
Merged

Dev #32

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Concretely, that means:

## Features

- **Declerative sub-agent pooling (optional)** — with `--use-pool`, the root agent can fan tasks out concurrently to vendor-agnostic sub-agents defined in an optional [agents.yaml](agents.yaml) file via a `dispatch` tool. Each pool entry binds a model _and_ a backend, so dispatch decisions are also compute-location decisions — a frontier model can plan while sensitive work stays on a local model, or a local root can fan out to faster remote SLMs for latency-sensitive tool calls.
- **Declarative sub-agent pooling (optional)** — with `--use-pool`, the root agent can fan tasks out concurrently to vendor-agnostic sub-agents defined in an optional [agents.yaml](agents.yaml) file via a `dispatch` tool. Each pool entry binds a model _and_ a backend, so dispatch decisions are also compute-location decisions — a frontier model can plan while sensitive work stays on a local model, or a local root can fan out to faster remote SLMs for latency-sensitive tool calls.
- **Agent profiles** — drop-in system prompts with permission manifests, base-profile inheritance, and auto-generated profiles via `/profile create`. Bundled profiles: `default`, `coder`, `reviewer`, `writer`, `planner`, `researcher`, `analyst`.
- **Rich built-in tool set** — file ops, shell access, web search/fetch, Wikipedia/GitHub/arXiv search, Git, task tracking, reasoning scratchpad, notebook, and more. Sandbox-locked with shell allowlists, SSRF protection, and sensitive-file gating.
- **Permission system** — write operations and sensitive reads require user approval. Session grants, workspace scoping, and profile-level allow/deny lists.
Expand Down Expand Up @@ -171,7 +171,7 @@ Type `/voice` again (or `Ctrl+Q` to quit the app) to exit voice mode — the mic
| [docs/BACKENDS.md](docs/BACKENDS.md) | Backend setup (Ollama, OpenAI, HuggingFace, Transformers), model tier switching |
| [docs/PROFILES.md](docs/PROFILES.md) | Profile structure, manifests, built-in profiles, creating and loading profiles |
| [docs/AGENT-POOLING.md](docs/AGENT-POOLING.md) | Agent pooling configuration, parsing, and usage |
| [docs/SECURITY.md](docs/SECURITY.md) | Security precidence and settings |
| [docs/SECURITY.md](docs/SECURITY.md) | Security precedence and settings |

## Docker

Expand All @@ -183,7 +183,7 @@ The Compose file runs the OpenAI-compatible API server in a container. The API s
docker-compose up --build
```

The API server listens on `localhost:9734`, mounts `./profiles` and `~/.config/oli` to persist state across restarts, and is ready to accept OpenAI-compatible chat completions requests.
The API server listens on `localhost:9734` (the compose file sets it via the repo's `.env`), mounts `./oli_bot/profiles`, `~/.config/oli`, and `./notes` to persist state across restarts, and is ready to accept OpenAI-compatible chat completions requests.

**Run the TUI agent locally (optional):**

Expand All @@ -205,16 +205,19 @@ oli-server
OLI_API_HOST=0.0.0.0 OLI_API_PORT=9734 oli-server
```

It listens on `0.0.0.0:9734` by default and serves:
It listens on `0.0.0.0:9734` by default (override with `OLI_API_HOST`/`OLI_API_PORT`) and serves:

| Endpoint | Description |
| ------------------------------------------------- | -------------------------------------------- |
| `GET /v1/models` | List the active model |
| `POST /v1/chat/completions` | Non-streaming chat completion |
| `POST /v1/chat/completions` with `"stream": true` | Server-sent-event (SSE) streaming completion |
| `GET /health` | Liveness probe |
| Endpoint | Description |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `GET /v1/models` | List the active model |
| `POST /v1/chat/completions` | Non-streaming chat completion |
| `POST /v1/chat/completions` with `"stream": true` | Server-sent-event (SSE) streaming completion |
| `WS /v1/chat` | Stateful WebSocket — per-connection history, relays every agent event as a typed JSON frame |
| `GET /health` | Liveness probe |

Conversations are **stateless** (like real OpenAI): each `/v1/chat/completions` request carries its full message history. The server holds a single process-private `Agent` instance (backend, tool registrations, MCP wiring) shared across requests, and serializes concurrent in-flight requests in-process. Because there is no human to prompt at permission time, the API auto-allows permission scopes for the current request; offline and dry-run gating from `AppConfig` still apply.
REST conversations are **stateless** (like real OpenAI): each `/v1/chat/completions` request carries its full message history. The server holds a single process-private `Agent` instance (backend, tool registrations, MCP wiring) shared across requests, and serializes concurrent in-flight requests in-process. Because there is no human to prompt at permission time, the API auto-allows permission scopes for the current request; offline and dry-run gating from `AppConfig` still apply.

The `WS /v1/chat` WebSocket is the stateful counterpart for real-time browser UIs: the server keeps a per-connection `messages` history, so a client sends each next turn as `{"content": "..."}` and receives every `AgentEvent` back as a typed JSON frame (`text_chunk`/`thinking`/`tool_call_executing`/`tool_call_result`/`assistant_response`/`usage`/`error`/`done`); `{"action": "clear"}` resets the history. See [docs/API_SERVER.md](docs/API_SERVER.md) for the full frame reference.

### curl

Expand Down
11 changes: 10 additions & 1 deletion agents.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ agent-pools:
backend:
type: ollama
base_url: http://localhost:11434
- name: researcher

- name: reviewer-agent
model: gpt-5-nano
profile: reviewer
backend:
type: openai
base_url: https://api.openai.com/v1
api_key: ${OLI_OPENAI_API_KEY}

- name: researcher-agent
model: Qwen/Qwen3.6-27B
profile: researcher
backend:
Expand Down
14 changes: 7 additions & 7 deletions docs/API_SERVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ oli-server
```

On startup it prints a banner with the resolved backend, model, mode, profile,
and URL. By default it listens on `0.0.0.0:8000`.
and URL. By default it listens on `0.0.0.0:9734`.

## Server configuration

Expand All @@ -26,7 +26,7 @@ settings. Four fields are specific to the server:
| Setting (env var) | Default | Description |
| -------------------------- | ---------- | --------------------------------------------- |
| `api_host` (`OLI_API_HOST`) | `0.0.0.0` | Bind address |
| `api_port` (`OLI_API_PORT`) | `8000` | Listen port |
| `api_port` (`OLI_API_PORT`) | `9734` | Listen port |
| `api_profile` (`OLI_API_PROFILE`) | `default` | Profile loaded at startup (mirrors `--profile`) |
| `api_mode` (`OLI_API_MODE`) | `agent` | Mode: `agent` / `ask` / `chat` / `plan` |

Expand Down Expand Up @@ -80,7 +80,7 @@ Liveness probe. Returns `{"status": "ok"}`.
Lists the single active model.

```bash
curl http://localhost:8000/v1/models
curl http://localhost:9734/v1/models
```

```json
Expand All @@ -102,7 +102,7 @@ curl http://localhost:8000/v1/models
Non-streaming completion:

```bash
curl http://localhost:8000/v1/chat/completions \
curl http://localhost:9734/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-4o",
Expand Down Expand Up @@ -185,7 +185,7 @@ A failed run (backend error, tool error, empty reply) surfaces as
Set `"stream": true`. The server returns `text/event-stream` SSE:

```bash
curl -N http://localhost:8000/v1/chat/completions \
curl -N http://localhost:9734/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"stream": true, "messages": [{"role": "user", "content": "hi"}]}'
```
Expand All @@ -211,7 +211,7 @@ a failed run emits one final SSE frame whose payload is `{"error": {...}}` befor
```python
from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="unused")
client = OpenAI(base_url="http://localhost:9734/v1", api_key="unused")

resp = client.chat.completions.create(
model="gpt-4o",
Expand Down Expand Up @@ -262,7 +262,7 @@ import asyncio, json
import websockets

async def main():
async with websockets.connect("ws://localhost:8000/v1/chat") as ws:
async with websockets.connect("ws://localhost:9734/v1/chat") as ws:
print(await ws.recv()) # {"type": "connected", ...}
await ws.send(json.dumps({"content": "List the todos"}))
while True:
Expand Down
16 changes: 15 additions & 1 deletion oli_bot/backends/upstream_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import List, Optional
from urllib.parse import urlparse

import httpx
from ollama import AsyncClient as OllamaAsyncClient

from ..models import HostConfig
Expand Down Expand Up @@ -165,14 +166,27 @@ def list_servers(self) -> List[HostConfig]:

@staticmethod
async def validate_ollama_url(url: str) -> tuple[bool, str]:
"""NOTE: currently only used to validate ollama URLs"""
"""Validate Ollama API URL by attempting to list models."""
try:
client = OllamaAsyncClient(host=url)
await client.list()
return True, ""
except Exception as e:
return False, str(e)

@staticmethod
async def validate_openai_url(url: str) -> tuple[bool, str]:
"""Validate OpenAI API URL by checking the /v1/models endpoint."""
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{url}/v1/models")
if response.status_code == 200:
return True, ""
else:
return False, f"Unexpected status code: {response.status_code}"
except Exception as e:
return False, str(e)

def seed_default(self, url: str) -> None:
if self.servers:
return
Expand Down
17 changes: 14 additions & 3 deletions oli_bot/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -1521,9 +1521,6 @@ async def _server_add(self, name: str, url: str) -> None:
)
return

# NOTE: this is currently only for Ollama backends.
# Other upstream servers won't get this check when added.
# TODO: consider adding a generic ping/healthcheck for other backends if they support it.
if self.config.backend == "ollama":
ok, err = await UpstreamManager.validate_ollama_url(url)
if not ok:
Expand All @@ -1532,6 +1529,20 @@ async def _server_add(self, name: str, url: str) -> None:
f"[red]Failed to connect to {url}: {err}[/red]",
)
return
elif self.config.backend == "openai":
ok, err = await UpstreamManager.validate_openai_url(url)
if not ok:
self._add_message(
"System",
f"[red]Failed to connect to {url}: {err}[/red]",
)
return
else:
self._add_message(
"System",
f"[red]Adding servers is not supported for backend '{self.config.backend}'[/red]",
)
return

try:
is_first = self.upstream_manager.add_server(name, url)
Expand Down
2 changes: 1 addition & 1 deletion oli_bot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class AppConfig(BaseSettings):

# API Server configuration
api_host: str = Field(default="0.0.0.0")
api_port: int = Field(default=8000)
api_port: int = Field(default=9734)
api_profile: str = Field(default="default")
api_mode: str = Field(default="agent")

Expand Down
56 changes: 40 additions & 16 deletions oli_bot/profiles/coder/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,63 @@
You are a expert software engineer. You write clean, idiomatic code, run tests to
verify your work, and prefer targeted edits over full rewrites.
You are an expert software engineer. You write clean, idiomatic code in
whatever language the project uses, run tests to verify your work, and
prefer targeted edits over full rewrites.

## Principles

- **Read before writing.** Explore the codebase with `glob`, `grep`, `tree`, and
`read_file` before touching anything. Understand the conventions in use.
- **Read before writing.** Explore the codebase with `glob`, `grep`, `tree`,
and `read_file` before touching anything. Identify the language(s),
frameworks, and conventions already in use before writing a single line.
- **Smallest safe change.** Prefer `edit_file` for targeted modifications.
Use `write_file` only when creating new files or when a full rewrite is
genuinely warranted.
- **Verify with tests.** After every non-trivial change, run the relevant test
suite via `run_command`. If tests fail, fix the failure before moving on.
- **Don't guess at APIs.** If unsure how a library works, check installed
source or docs (`run_command` + `grep`, or `fetch` the docs) rather than
hallucinating an interface.
- **Verify with tests.** After every non-trivial change, run the project's
test suite via `run_command` (e.g. its Makefile target, npm/cargo/go/pytest
script, or whatever the project defines). If tests fail, fix the failure
before moving on. If no test suite exists, run the project's build/lint
step instead, and say so.
- **Know when to stop debugging.** If a fix doesn't resolve the failure,
re-diagnose rather than trying small variations blindly. If the same test
still fails after two or three genuine attempts, stop, report what you
tried and what you learned, and ask before continuing — don't keep
looping on your own.
- **Format and lint before calling it done.** If the project has a formatter
or linter configured (e.g. a config file, a package script, or a
pre-commit hook), run it on changed files before finishing. Don't
introduce a formatter/linter if the project doesn't already use one.
- **Don't guess at APIs.** If unsure how a library, framework, or system call
works, check installed source, package manifests, or docs (`run_command` +
`grep`, or `fetch` the docs) rather than hallucinating an interface.
- **Match the ecosystem's conventions.** Follow the idioms, formatting, and
tooling native to the project's language and package manager — don't
impose patterns from a different ecosystem.
- **Leave the codebase better than you found it.** Fix obvious issues you
encounter along the way, but stay focused on the task at hand — avoid
scope creep.

## Workflow

1. **Understand** — read the relevant code, tests, and docs.
1. **Understand** — read the relevant code, tests, and docs; identify the
language, framework, and build/test tooling in use.
2. **Plan** — use `think` to reason about the approach before writing.
3. **Implement** — make changes incrementally; commit logical units of work.
4. **Test** — run the tests; fix failures.
5. **Review** — re-read your own changes; check for regressions, edge cases,
and style inconsistencies.
4. **Test** — run the tests (or build/lint if no tests exist); fix failures.
If a failure persists after a few genuine attempts, stop and report
rather than continuing to iterate blindly.
5. **Review** — re-read your own changes; run the project's formatter/linter
if one exists; check for regressions, edge cases, and style
inconsistencies with the surrounding code.

## Output style

- Be concise in prose. Let the code speak.
- When explaining a change, say *what* changed and *why*, not just *how*.
- Prefer inline code fences with the correct language tag.
- If you cannot complete the task safely (missing context, risky change, unclear
requirements), say so explicitly and ask rather than guessing.
- If you cannot complete the task safely (missing context, risky change,
unclear requirements, ambiguous tooling), say so explicitly and ask rather
than guessing.

## AGENTS.md file for projects

If an AGENTS.md file is available at the root of the project, read it FIRST before doing any discovery! This should give you an overview of the project and what's expected.
If an AGENTS.md file is available at the root of the project, read it FIRST
before doing any discovery! This should give you an overview of the project,
its language/toolchain, and what's expected.
Loading