Skip to content
Merged
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
5 changes: 3 additions & 2 deletions GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@ settings belong in the global config.
## 4. Sandbox (Docker, optional)

Tool execution runs inside an isolated Docker container **by default** for
`odek run`, `odek continue`, `odek repl`, and `odek serve`. If you don't have
Docker installed, opt out explicitly:
`odek run`, `odek repl`, and `odek serve`. `odek continue` pins the session's
stored sandbox bit rather than applying a new default. If you don't have
Docker installed, opt out explicitly on `run`/`repl`/`serve`:

```bash
export ODEK_NO_SANDBOX=1 # add to ~/.zshrc / ~/.bashrc
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
One binary. One loop. Zero frameworks. ReAct (Reasoning + Acting) — think, therefore act.

```bash
# Install (requires Go ≥ 1.25.13 — see "Build requirements" below)
go install github.com/BackendStack21/odek/cmd/odek@latest
# Install from a v2 release tag (requires Go ≥ 1.25.13 — see GETTING_STARTED.md).
# Do not use @latest: Go ignores v2 tags on this module path and would
# install an older v1 release.
go install github.com/BackendStack21/odek/cmd/odek@v2.11.2

# Use (provider env key — DEEPSEEK_API_KEY for the default provider)
export DEEPSEEK_API_KEY=sk-...
Expand All @@ -26,7 +28,7 @@ odek is not a framework. It's a **runtime** — the smallest possible surface ar

| | odek | Python agents (LangChain, CrewAI, etc.) |
|---|---|---|
| Dependencies | **5.** 2× 21no.de, 3× golang.org/x | 200+ packages |
| Dependencies | **6.** 3× 21no.de, 3× golang.org/x | 200+ packages |
| Binary size | ~11 MB static | 50-200 MB with venv |
| Startup | **Instant** | 2-10s (Python imports) |
| Sandbox | **Default-on** Docker sandbox (`--no-sandbox` to opt out) | Requires manual Docker setup |
Expand Down
31 changes: 31 additions & 0 deletions cmd/odek/compaction_wire_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import (
"os"
"strings"
"testing"
)

// TestLongLivedAgents_CopyResolvedCompaction pins that serve and Telegram
// pass resolved.Compaction into odek.Config. Those surfaces parse the flag
// / config bit; omitting the field left rolling compaction at the library
// default (off) while GET /api/config still reported it on.
func TestLongLivedAgents_CopyResolvedCompaction(t *testing.T) {
for _, name := range []string{"serve.go", "telegram.go"} {
b, err := os.ReadFile(name)
if err != nil {
t.Fatalf("read %s: %v", name, err)
}
found := false
for _, line := range strings.Split(string(b), "\n") {
s := strings.TrimSpace(line)
if strings.HasPrefix(s, "Compaction:") && strings.Contains(s, "resolved.Compaction") {
found = true
break
}
}
if !found {
t.Errorf("%s must copy resolved.Compaction onto odek.Config", name)
}
}
}
1 change: 1 addition & 0 deletions cmd/odek/init_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func TestLocalConfigTemplate_RemainsProjectSafe(t *testing.T) {
`"provider"`, `"providers"`, `"api_key"`, `"base_url"`, `"llm"`, `"system"`, `"dangerous"`, `"memory"`,
`"guard"`, `"maintenance"`, `"telegram"`, `"web_search"`,
`"embedding"`, `"sessions"`, `"trusted_proxies"`, `"profiles"`,
`"subagent"`, `"max_concurrent"`, `"catchup"`,
`"sandbox"`, `"compaction"`, `"limits"`,
`"prompt_caching"`, `"stream"`, `"announce_budget"`,
} {
Expand Down
9 changes: 1 addition & 8 deletions cmd/odek/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1583,17 +1583,10 @@ const localConfigTemplate = `{
"max_lazy_slots": 5,
"verbose": false
},
"subagent": {
"max_concurrency": 3,
"timeout_seconds": 1800,
"max_iterations": 15
},
"mcp_servers": {},
"schedules": {
"enabled": true,
"max_concurrent": 2,
"timezone": "UTC",
"catchup": false
"timezone": "UTC"
}
}`

Expand Down
5 changes: 5 additions & 0 deletions cmd/odek/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,8 @@ func serveCmd(args []string) error {
promptCaching = boolPtr(false)
case "--compaction":
compaction = boolPtr(true)
case "--no-compaction":
compaction = boolPtr(false)
case "--announce-budget":
announceBudget = boolPtr(true)
case "--no-announce-budget":
Expand Down Expand Up @@ -693,6 +695,8 @@ Flags:
--sandbox-user user Container user (e.g. 1000:1000)
--prompt-caching Enable prompt caching (default: on)
--no-prompt-caching Disable prompt caching
--compaction Enable rolling compaction (default: on)
--no-compaction Disable rolling compaction
--announce-budget Enable parent budget-awareness hints (default: on)
--no-announce-budget Disable parent budget-awareness hints
--stream Stream LLM responses live to the Web UI (default: on)
Expand Down Expand Up @@ -978,6 +982,7 @@ func newServeAgent(resolved config.ResolvedConfig, system string, runKey string,
// Disable with --no-prompt-caching / ODEK_PROMPT_CACHING=false /
// config "prompt_caching": false. Library odek.New stays opt-in.
PromptCaching: resolved.PromptCaching,
Compaction: resolved.Compaction,
AnnounceBudget: &resolved.AnnounceBudget,
// Live streaming: forward SSE fragments to the browser as
// thinking_delta / token_delta events (docs/STREAMING.md). Default
Expand Down
1 change: 1 addition & 0 deletions cmd/odek/telegram.go
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,7 @@ func handleChatMessage(
ToolFilter: odek.ToolFilterConfig{Enabled: resolved.Tools.Enabled, Disabled: resolved.Tools.Disabled},
Renderer: rend,
PromptCaching: resolved.PromptCaching,
Compaction: resolved.Compaction,
AnnounceBudget: &resolved.AnnounceBudget,
MemoryConfig: resolved.Memory,
MemoryDir: expandHome("~/.odek/memory"),
Expand Down
18 changes: 11 additions & 7 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ type Config struct {
// stream (schema odek.event/v1): run_started, iteration_completed,
// tool_call_started/completed/failed, session_saved,
// context_trimmed, budget_exceeded, plan_created, plan_updated,
// plan_blocked, run_completed, run_failed.
// plan_blocked, subagent_denied, subagent_spawned,
// subagent_completed, subagent_concurrency_wait,
// run_completed, run_failed.
// Dispatch is non-blocking (buffered, drop-on-full) and
// panic-isolated — a slow or panicking handler can never stall
// or crash the loop. Events never contain raw tool arguments
Expand Down Expand Up @@ -400,17 +402,19 @@ agent, err := odek.New(odek.Config{
Use `RunWithMessages` to continue conversations across turns, loading prior message history:

```go
import "github.com/BackendStack21/odek/internal/session"

// First turn
answer, messages, err := agent.RunWithMessages(ctx, []llm.Message{
answer, messages, err := agent.RunWithMessages(ctx, []session.Message{
{Role: "user", Content: "Read the main.go file"},
})

// Second turn — continue the conversation
messages = append(messages, llm.Message{Role: "user", Content: "Now refactor it"})
messages = append(messages, session.Message{Role: "user", Content: "Now refactor it"})
answer, messages, err = agent.RunWithMessages(ctx, messages)

// Third turn — continue again
messages = append(messages, llm.Message{Role: "user", Content: "Add error handling"})
messages = append(messages, session.Message{Role: "user", Content: "Add error handling"})
answer, messages, err = agent.RunWithMessages(ctx, messages)
```

Expand All @@ -427,7 +431,7 @@ sess, _ := store.Create(messages, "deepseek-v4-flash", "Refactor auth")
// Later...
sess, _ := store.Load("20260520-abc123")
msgs := sess.GetMessages()
msgs = append(msgs, llm.Message{Role: "user", Content: "Add tests"})
msgs = append(msgs, session.Message{Role: "user", Content: "Add tests"})
answer, allMsgs, err := agent.RunWithMessages(ctx, msgs)
store.Append(sess.ID, allMsgs[len(msgs):])
```
Expand Down Expand Up @@ -588,8 +592,8 @@ Memory is enabled by default when odek loads a config file with memory settings.
agent, _ := odek.New(odek.Config{
Model: "deepseek-v4-flash",
APIKey: os.Getenv("DEEPSEEK_API_KEY"),
// Memory is enabled via config file (~/.odek/config.json or ./odek.json)
// In CLI mode, the --memory flag enables it automatically
// Memory is enabled via ~/.odek/config.json (the memory section is
// operator-only — ./odek.json cannot set it)
})

// Each turn — memory manager is nil if disabled
Expand Down
23 changes: 11 additions & 12 deletions docs/CHEATSHEET.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ odek memory extended pending # List atoms pending review
odek memory extended confirm <id> # Approve a pending-review atom
odek memory extended forget <id> # Delete an atom

# Sandbox (ON by default for run/continue/repl — see Sandbox section)
# Sandbox (ON by default for run/repl/serve; continue pins — see Sandbox section)
odek run --sandbox "build safely" # Explicit: hard-fails if Docker is unavailable
odek run --no-sandbox "quick task" # Explicit opt-out
odek serve --sandbox --sandbox-readonly --sandbox-network none
Expand Down Expand Up @@ -86,7 +86,7 @@ odek run --events-jsonl events.jsonl --events-include-args "task" # + raw (reda
}
```

Priority: `~/.odek/config.json` ← `./odek.json` ← `ODEK_*` env ← CLI flags. (The `dangerous` section is operator-only: a project `./odek.json` cannot set it, so a cloned repo can't lower its own guardrails.)
Priority: `~/.odek/secrets.env` ← `~/.odek/config.json` ← `./odek.json` ← `ODEK_*` env ← CLI flags. (The `dangerous` section is operator-only: a project `./odek.json` cannot set it, so a cloned repo can't lower its own guardrails.)

### Risk Classes & Approvals

Expand Down Expand Up @@ -184,7 +184,7 @@ docker run -d --name searxng -p 8888:8080 \
searxng/searxng:2026.6.8-f3fab143b
```

Then point odek at it (global `~/.odek/config.json` or project `./odek.json`):
Then point odek at it in global `~/.odek/config.json` (`web_search` in `./odek.json` is ignored):

```json
{ "web_search": { "base_url": "http://127.0.0.1:8888" } }
Expand All @@ -201,8 +201,8 @@ instance, `server.limiter: false` (drops the Redis/Valkey dependency).
```
~/.odek/memory/
├── facts/
│ ├── user.md → User profile (cap: 1,500 chars)
│ └── env.md → Environment facts (cap: 2,500 chars)
│ ├── user.md → User profile (default cap: 4,000 chars)
│ └── env.md → Environment facts (default cap: 8,000 chars)
├── project-facts/ → Per-project overlays (optional)
└── episodes/
├── <session-id>.md → LLM-extracted session summaries
Expand Down Expand Up @@ -261,7 +261,7 @@ delegate_tasks tasks=[{goal: "task A", context: "..."}, {goal: "task B"}]

## Sandbox

**On by default** for `odek run` / `odek continue` / `odek repl` — the container is the control for "agent ran attacker-controlled code", so isolation is what you get unless you deliberately give it up.
**On by default** for `odek run` / `odek repl` / `odek serve` — the container is the control for "agent ran attacker-controlled code", so isolation is what you get unless you deliberately give it up.

```bash
odek run "install deps" # default-on: sandboxed when Docker is up
Expand All @@ -273,21 +273,21 @@ odek repl --sandbox --sandbox-memory 2g --sandbox-cpus 2

- **Implicit default + Docker unavailable** (or unapproved project `Dockerfile.odek`) → degrades to unsandboxed with a loud notice, instead of breaking Docker-less machines.
- **`ODEK_REQUIRE_SANDBOX=1`** → any unsandboxed outcome is fatal, including explicit opt-outs (the hard constraint outranks contradictory flags).
- `odek continue` pins the session's original sandbox posture — no mid-conversation containment flips.
- `odek continue` pins the session's original sandbox posture — no mid-conversation containment flips. It does not accept `--no-sandbox`; override with `ODEK_SANDBOX=false` / trusted `"sandbox": false`. `ODEK_NO_SANDBOX=1` does not override the pin.

Flags: `--sandbox`, `--no-sandbox`, `--sandbox-image`, `--sandbox-network`, `--sandbox-readonly`, `--sandbox-memory`, `--sandbox-cpus`, `--sandbox-user`.
Flags (`run` / `repl` / `serve`): `--sandbox`, `--no-sandbox`, `--sandbox-image`, `--sandbox-network`, `--sandbox-readonly`, `--sandbox-memory`, `--sandbox-cpus`, `--sandbox-user`.

Env vars: `ODEK_SANDBOX=true`, `ODEK_SANDBOX_IMAGE`, `ODEK_SANDBOX_NETWORK`, `ODEK_NO_SANDBOX=1`, `ODEK_REQUIRE_SANDBOX=1`, etc.

> **Project config approval:** sandbox knobs set in `./odek.json` (`sandbox_env`, `sandbox_image`, `sandbox_network`, `sandbox_volumes`) require an interactive approval prompt. Use `ODEK_APPROVE_PROJECT_SANDBOX=1` in CI/scripts, or set sandbox config via `~/.odek/config.json` / env vars / CLI flags instead. A project config can enable the sandbox but never disable it.

Default network: `bridge` (internet access). Set `none` for air-gapped execution.
Default network: `none` (air-gapped). Set `bridge` for internet access.

## Telegram Bot

- Requires `ODEK_TELEGRAM_BOT_TOKEN` env var
- Slash commands: `/start`, `/help`, `/new`, `/plan`, `/plans`, `/plan_view`, `/plan_delete`, `/plan_resume`, `/plan_status`, `/sessions`, `/resume`, `/prune`, `/stats`, `/stop`, `/mode`, `/restart`
- Plans: stored as `~/.odek/plans/<slug>.md`; `/plan` generates via agent, `/plan_resume` injects most recent plan into session; `/plan_status` shows the agent's structured loop plan (distinct concept — see docs/PLANNING.md)
- Slash commands: `/start`, `/help`, `/new`, `/plan`, `/plans`, `/plan_view`, `/plan_delete`, `/plan_resume`, `/plan_status`, `/sessions`, `/resume`, `/prune`, `/stats`, `/jobs`, `/stop`, `/mode`, `/restart`, `/schedules`, `/schedule`
- Plans: stored as `~/.odek/plans/chat<chatID>/<slug>.md`; `/plan` generates via agent, `/plan_resume` injects most recent plan into session; `/plan_status` shows the agent's structured loop plan (distinct concept — see docs/PLANNING.md)
- Voice messages: automatically processed via `DownloadVoice` → OGG files in `~/.odek/media/`
- Photos: automatically processed via `DownloadPhoto` → JPG files in `~/.odek/media/`
- Conversations persist across bot restarts (`tg-<chatID>` sessions)
Expand Down Expand Up @@ -351,7 +351,6 @@ odek mcp --sandbox
| `ODEK_ANNOUNCE_BUDGET` | announce_budget (default on; parent hints, not `subagent.announce_budget`) |
| `ODEK_STREAM` | stream (default on) |
| `ODEK_MAX_CONCURRENCY` | max_concurrency |
| `ODEK_CTX` | ctx (comma-separated file paths) |
| `DEEPSEEK_API_KEY` | `providers.deepseek` (default provider) |
| `OPENAI_API_KEY` | `providers.openai` (also DeepSeek leftover) |
| `ANTHROPIC_API_KEY` | `providers.anthropic` |
Expand Down
Loading
Loading