Metadata-Version: 2.4
Name: aic-agent
Version: 1.0.2
Summary: AI-Commander: a ralph-loop AI agent giving LLMs shell access via function calling (TUI + CLI)
Author-email: ortegaalfredo <ortegaalfredo@gmail.com>
License: Apache-2.0
Project-URL: Homepage, https://github.com/ortegaalfredo/AICommander
Project-URL: Repository, https://github.com/ortegaalfredo/AICommander
Project-URL: Issues, https://github.com/ortegaalfredo/AICommander/issues
Keywords: ai,agent,llm,shell,openai,tui,textual
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: openai>=2.0.0
Requires-Dist: context-compressor-llm>=0.1.0
Requires-Dist: ddgs>=8.0.0
Requires-Dist: textual>=8.0.0
Requires-Dist: rich>=14.2.0
Requires-Dist: pyte>=0.8.2
Requires-Dist: py-landlock>=0.1.0; sys_platform == "linux"
Provides-Extra: dev
Requires-Dist: build>=1.0.0; extra == "dev"
Requires-Dist: twine>=5.0.0; extra == "dev"
Requires-Dist: pytest>=8.0.0; extra == "dev"
Dynamic: license-file

# AI-Commander

**PyPI package:** [`aic-agent`](https://pypi.org/project/aic-agent/) — install with `pip install aic-agent` (the command is `aic`).

AI-Commander is a **ralph-loop AI agent** that provides shell access and task
planning capabilities to Large Language Models through function calling. It
runs a tight agent loop: it plans, executes shell commands, observes the
results, and iterates until the requested task is complete — all while you stay
in control through an approval gate.

It supports two presentation modes:

- **Default (TUI)** — a [Textual](https://textual.textualize.io/)-based terminal
  UI with split panels (prompt, console, agent output, status footer) and an
  `/autoapprove` toggle for the approval gate.
- **`--nogui`** — the original direct-CLI behaviour (stdout/stderr, no TUI).

![AI-Commander TUI](aic.png)

---

## Table of Contents

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Usage](#usage)
- [Command-line options](#command-line-options)
- [How it works](#how-it-works)
- [The TUI](#the-tui)
  - [Layout](#layout)
  - [Slash commands](#slash-commands)
  - [Keyboard shortcuts](#keyboard-shortcuts)
  - [Prompt history](#prompt-history)
- [The approval gate](#the-approval-gate)
- [Context compression](#context-compression)
  - [How the budget works](#how-the-budget-works)
  - [The `truncate` algorithm](#the-truncate-algorithm)
  - [The `context-compressor-llm` algorithm](#the-context-compressor-llm-algorithm)
  - [Choosing an algorithm](#choosing-an-algorithm)
- [Session persistence](#session-persistence)
  - [The session file format](#the-session-file-format)
  - [Editing a session by hand](#editing-a-session-by-hand)
  - [Multiple instances in one directory](#multiple-instances-in-one-directory)
  - [Crash recovery semantics](#crash-recovery-semantics)
- [Sandboxing (Linux)](#sandboxing-linux)
- [Fast mode](#fast-mode)
- [Debugging](#debugging)
- [Project structure](#project-structure)
- [License](#license)

---

## Features

- **Ralph-loop agent** — repeatedly plans, executes, and observes until the
  task is done.
- **Shell access** — runs arbitrary commands and captures their output.
- **Web search** — using the duck-duck-go `ddgs` Python package (installed via requirements.txt).
- **Approval gate** — review each command before it runs, or auto-approve.
- **Command timeouts** — prevent runaway commands (configurable).
- **Output limits** — command output is capped and truncated with a sentinel so
  the model never receives unbounded text.
- **Context compression** — two pluggable algorithms keep the conversation
  history within the model's prompt budget.
- **OS-level sandbox** — optional [Landlock](https://landlock.io/) sandbox on
  Linux to restrict writes to the working directory, `/tmp`, `/dev`, and
  `/dev/pts`.
- **Two presentation modes** — full TUI or plain CLI (`--nogui`).
- **Prompt history** — readline-style history with reverse incremental search
  (Ctrl+R) in the TUI.
- **Session persistence** — the complete conversation history is mirrored to a
  hand-editable JSON file (`.aicsession`) after every step and reloaded on every
  startup, so a crash or Ctrl+C loses nothing already produced.

---

## Requirements

- Python 3.9+
- An OpenAI-compatible API endpoint (any provider that exposes the
  `/chat/completions` interface).

---

## Installation

### From PyPI (or from source)

```bash
pip install aic-agent
```

This installs the `aic` command plus every runtime dependency, including the
Textual TUI and the `context-compressor-llm` context compressor, so both the
TUI and LLM-based compression work out of the box. (`py-landlock` is installed
only on Linux; elsewhere the agent simply runs unsandboxed.)

Upgrade with:

```bash
pip install --upgrade aic-agent
```

### From a checkout of this repository

```bash
git clone https://github.com/ortegaalfredo/AICommander
cd AICommander
python3 -m venv venv
source venv/bin/activate
pip install .
```

Or, to install in editable mode while hacking on the code:

```bash
pip install -e .
```

Alternatively, install only the dependencies and run the script directly:

```bash
pip install -r requirements.txt
python3 aic.py ...
```
> **Note:** The `py-landlock` package is only used on Linux and
> is optional — the agent runs unsandboxed if it is not installed.

---

## Usage

AI-Commander is a single-file script. Run it from the repository root with
`python3 aic.py`, or use the `aic` command installed by the
[`aic-agent`](https://pypi.org/project/aic-agent/) package:

```bash
# Installed via pip (package: aic-agent)
aic \
  --api-base https://api.example.com/v1 \
  --model your-model-name \
  --api-key YOUR_API_KEY \
  "your task request"

# From a repository checkout
python3 aic.py \
  --api-base https://api.example.com/v1 \
  --model your-model-name \
  --api-key YOUR_API_KEY \
  "your task request"
```

The TUI starts with no request and waits for you to type one in the prompt
panel. In `--nogui` mode a request is required on the command line.

### Command-line options

| Option | Description |
| ------ | ----------- |
| `--api-base` | API base URL (required). |
| `--model` | Model name (required). |
| `--api-key` | API key (required). |
| `--auto-approve` | Auto-approve command execution (for testing). |
| `--no-thinking` | Hide thinking tokens from output. |
| `--timeout` | Command timeout in seconds (default: `120`). |
| `--max-prompt-len` | Maximum prompt length in tokens (default: `80000`). |
| `--max-output-bytes` | Maximum output bytes returned from commands (default: `10240`). |
| `--max-steps` | Maximum number of agent loop steps before stopping (default: `500`). |
| `--debug` | Enable debug mode (dump conversation history on truncation). |
| `--nogui` | Run in direct CLI mode without TUI (original behaviour). No session file is read or written unless `--session` is given. |
| `--session` | Session file mirroring the conversation history (default: `./.aicsession`). |
| `--no-session` | Disable session persistence (nothing is written or loaded). |
| `--disable-sandbox` | Disable the OS-level Landlock sandbox (Linux only). |
| `--compress-alg` | Context-compression algorithm (`context-compressor-llm` or `truncate`). |
| `--compress-target` | Fraction of the prompt budget retained as headroom (default: `0.4`). |
| `--fast` | Fast mode: force `truncate` compression and a smaller system prompt, for slow inference systems. |
| `--reasoning-effort` | Reasoning effort for the LLM (e.g. `low`, `medium`, `high`). |
| `request` | The task request (positional, required in `--nogui` mode). |

### Example

```bash
# Run in the TUI (default)
python3 aic.py --api-base https://api.example.com/v1 \
  --model gpt-4o --api-key sk-... "List all files in this directory"

# Run in plain CLI mode
python3 aic.py --nogui --api-base https://api.example.com/v1 \
  --model gpt-4o --api-key sk-... "Summarize the contents of aic.py"

# Auto-approve all commands (useful for testing / scripting)
python3 aic.py --auto-approve --api-base https://api.example.com/v1 \
  --model gpt-4o --api-key sk-... "Set up a new Python project"

# Use the truncation algorithm and a smaller, faster prompt
python3 aic.py --fast --api-base https://api.example.com/v1 \
  --model gpt-4o --api-key sk-... "List all files"
```

---

## How it works

1. The agent receives your task request.
2. It plans the next step and may emit *thinking* tokens.
3. It proposes shell commands, which you approve or auto-approve.
4. Commands run (optionally sandboxed and time-limited), and their output is
   fed back to the model.
5. The loop repeats until the task is considered complete.

All I/O is routed through the `EventSink` abstraction, so the same agent logic
runs unchanged in both the TUI and `--nogui` modes.

The agent exposes a single tool, `execute_bash(command)`, which runs the
command in a pseudo-terminal (PTY). The PTY gives the command a real terminal
environment (so interactive tools and `tput`-style programs work) and lets the
agent capture output, enforce a timeout, and kill the whole process group when
needed. Output is streamed live to the console panel and the final result is
truncated to `--max-output-bytes` with the literal sentinel
`output too long: truncated` appended when the limit is exceeded.

---

## The TUI

### Layout

The default Textual interface is a Norton-Commander-style split screen:

- **Left panel — Agent Output**: the model's streamed responses, thinking
  tokens, and status messages.
- **Right panel — Console Output / Shell**: two tabs. *Console Output* shows
  the commands the agent runs and their live output; *Shell* is an embedded
  interactive shell you can use yourself.
- **Bottom — Prompt input**: type a task or a slash command.
- **Status footer**: model, current step, live context-window estimate,
  cumulative token total, tokens/second, session id, auto-approve state,
  pending-approval indicator, and sandbox status.

The agent runs in a background thread and communicates with the UI through a
queue; the UI never blocks on the agent.

### Slash commands

Type any of the following in the prompt panel (they are UI controls, so they
are **not** added to prompt history):

| Command | Aliases | Description |
| ------- | ------- | ----------- |
| `/autoapprove` | `/aa` | Toggle auto-approve on/off for this session. When ON, commands run without asking. |
| `/approve` | — | Approve the currently pending command. |
| `/reject` | — | Reject the currently pending command. |
| `/suggest <text>` | — | Reject the pending command **with a steering suggestion** that is fed back to the agent (e.g. `/suggest use a different approach`). |
| `/clear` | `/new` | Clear the conversation history and start a fresh session (resets tokens, step count, and session id). The session file is emptied too, so nothing is resurrected on the next startup. |
| `/session` | — | Show the session file, session id, instance number, and what is currently saved on disk. |
| `/reload` | — | Re-read the session file, picking up edits made to its `messages` array outside AI-Commander (requires a stopped agent). |
| `/stop` | — | Stop the running agent. |
| `/quit` | `/exit` | Stop the agent and exit the application. |

> **Note:** When a command is pending approval, you can also approve/reject it
> with the modal dialog (Y/N keys) or the on-screen buttons. The `/approve`,
> `/reject`, and `/suggest` commands are an alternative to the dialog.

### Keyboard shortcuts

| Key | Action |
| --- | ------ |
| `Up` / `Ctrl+P` | Previous prompt in history (readline-style). |
| `Down` / `Ctrl+N` | Next prompt in history. |
| `Ctrl+R` | Open reverse incremental search over prompt history. |
| `Tab` | Switch between the Console Output and Shell tabs. |
| `Ctrl+C` / `Escape` | Stop the agent and quit. |
| `Y` / `Enter` | Approve a pending command (in the approval dialog). |
| `N` / `Escape` | Reject a pending command (in the approval dialog). |

### Prompt history

Prompt history is persisted to `~/.aic_history` (up to 1000 entries) and
loaded at startup. Up/Down navigation preserves your in-progress line
(readline semantics), and `Ctrl+R` opens a modal reverse search that filters
history newest-first; `Ctrl+R` cycles to the next older match, `Enter` accepts
a match back into the prompt, and `Escape`/`Ctrl+G` cancels.

---

## The approval gate

By default every command the agent proposes must be approved before it runs.
This keeps you in control of what executes on your system.

- **Approve** — the command runs.
- **Reject** — the command is skipped and the agent is told it was skipped.
- **Reject with a suggestion** — the command is skipped and your suggestion is
  injected into the conversation as a user message steering the agent
  (e.g. `/suggest use a different approach`).

You can toggle auto-approve at any time with `/autoapprove` (or `/aa`), or
start with `--auto-approve`. When auto-approve is ON, all commands run without
asking — useful for testing and scripting, but be ready to `/stop` if a command
misbehaves.

---

## Context compression

Long agent sessions accumulate conversation history. When the estimated token
count of the history exceeds the prompt budget, AI-Commander compresses it so
the request fits inside the model's context window. Compression is triggered
automatically at the start of each agent step and is a no-op when the history
is under budget.

### How the budget works

The prompt budget is `--max-prompt-len` (default 80,000 tokens). For models
that accept a `max_tokens` request parameter (all non-`gpt` models), the budget
is reduced by the output reservation (`max_tokens`, 8,000) so the prompt leaves
room for the completion inside the context window:

```
prompt_budget = max_prompt_len - max_tokens   (for non-gpt models)
prompt_budget = max_prompt_len                (for gpt models)
```

Token counts are estimated locally with a conservative heuristic (~3 characters
per token plus per-message overhead). Exact numbers come from the API's usage
stats once each call finishes and are shown in the status bar.

### The `truncate` algorithm

`--compress-alg truncate` (or `--fast`) is the simpler, cheaper algorithm. It
always preserves the **system prompt** (index 0) and the **first user
instruction** (index 1) so the agent never forgets its primary objective. It
works in two passes:

1. **Condense oversized tool outputs** in place (oldest first), replacing
   verbose command results with `<condensed tool output>`.
2. **Drop the oldest messages** (from index 2 onward) if condensation alone
   isn't enough.

Both passes stop once the retained history fits within
`--compress-target` (default 40%) of the budget, leaving headroom so several
new turns fit before the next compression. Truncating down to the *full*
budget would make every following prompt exceed the limit again and re-compress
on each step, slowly losing more history than necessary.

### The `context-compressor-llm` algorithm

`--compress-alg context-compressor-llm` (the default) uses the
[`context-compressor-llm`](https://pypi.org/project/context-compressor-llm/)
package, which implements a Factory.ai-style **anchored-summary incremental
compressor**. When the non-system log exceeds the budget it:

1. **Evicts the oldest prefix** of the conversation.
2. **Folds it into a persistent `AnchoredSummary`** via an LLM call on the
   evicted segment only (so the summarizer call stays short on slow-prefill
   systems).
3. **Retains the newest suffix** — an append-only, KV-cache-friendly layout.

The system prompt is kept **byte-identical** at the front of the retained
context so unchanged prefixes are never re-prefilled (important on systems with
slow prompt processing / prefix caching). The first user instruction is also
preserved verbatim, and a fallback user message is appended if aggressive
compression would leave no user query at all (some endpoints reject that).

If the `context-compressor-llm` package is not installed, this algorithm falls
back to `truncate`.

### Choosing an algorithm

| Algorithm | Cost | Context quality | Best for |
| --------- | ---- | --------------- | -------- |
| `truncate` | Cheap (no extra LLM calls) | Loses detail from old tool outputs and messages | Fast local inference, short tasks, `--fast` mode |
| `context-compressor-llm` | One LLM summarizer call per compression | Preserves a running summary of evicted history | Long tasks, slow-prefill systems, KV-cache-friendly setups |

Use `--compress-target` to control how much headroom is retained (default
`0.4` = 40%). A higher value keeps more history but triggers compression sooner;
a lower value compresses harder but loses more context.

---

## Session persistence

AI-Commander mirrors the **complete conversation history** to a JSON file and
reloads it on every startup, so nothing already produced is lost to a crash, a
`kill`, a closed terminal, or `Ctrl+C`.

- **Where** — `.aicsession` in the current working directory by default;
  override with `--session <path>` or turn persistence off with
  `--no-session`. Because the file lives in the working directory, it stays
  writable inside the Landlock sandbox.
- **When it is written** — after the user request, before every LLM call, right
  after each assistant turn, after every tool result, and again on exit
  (`atexit` plus `SIGTERM`/`SIGHUP` handlers). Writes are atomic
  (temp file → `fsync` → `os.replace`), so the file is never half-written.
- **What is reloaded** — the message history verbatim, plus the session id,
  step count, and cumulative token usage. `--nogui` resumes the conversation
  where the previous run stopped; the TUI restores history at startup and shows
  the session file in the status bar.
- **A corrupt file is never destroyed** — unparseable JSON is moved to
  `.aicsession.corrupt.<timestamp>` and the run starts clean.

The status line at startup reports what happened, e.g.
`[SESSION] Persisting session to /project/.aicsession` or
`[SESSION] Restored 14 message(s) from .aicsession (session 20260828_134851, step 4)`.

### The session file format

Plain, indented, UTF-8 JSON — deliberately simple enough to read and edit:

```json
{
  "aic_session_version": 1,
  "session_id": "20260828_134851",
  "updated_at": "2026-08-28T13:52:58",
  "reason": "tool_result",
  "model": "gpt-4o",
  "api_base": "https://api.example.com/v1",
  "cwd": "/home/me/project",
  "instance_number": 1,
  "step_count": 4,
  "usage": {
    "input_tokens": 5321,
    "output_tokens": 812,
    "cached_tokens": 4096,
    "billable_tokens": 2037
  },
  "messages": [
    { "role": "system", "content": "You are an expert planning..." },
    { "role": "user", "content": "summarize this repo" },
    { "role": "assistant", "content": null, "tool_calls": [
      { "id": "call_1", "type": "function",
        "function": { "name": "execute_bash",
                      "arguments": "{\"command\": \"ls\"}" } } ] },
    { "role": "tool", "tool_call_id": "call_1", "content": "aic.py\nREADME.md" }
  ]
}
```

`messages` is the raw OpenAI message list, saved **exactly** as the agent holds
it (same roles, `tool_calls`, and `tool_call_id`s). `reason` records which
checkpoint produced the file (`user_request`, `before_llm_call`,
`assistant_message`, `tool_result`, `loop_terminated`, `process_exit`,
`signal_15`, `interrupted`, `cleared`, …), which makes it easy to tell where a
crashed run stopped.

### Editing a session by hand

Because the `messages` array is stored verbatim, a session can be reshaped with
any text editor or JSON tool:

```bash
# drop the last 6 turns, then resume
python3 - <<'EOF'
import json
d = json.load(open(".aicsession"))
d["messages"] = d["messages"][:-6]
json.dump(d, open(".aicsession", "w"), indent=2)
EOF
python3 aic.py --api-base ... "continue from here"
```

- Change, insert, or delete turns; edits are picked up on the next startup, or
  immediately inside the TUI with `/reload`.
- A **bare JSON array** of messages is accepted in place of the full object, so
  you can paste a transcript straight into the file.
- Invalid content is repaired rather than rejected: a missing system prompt is
  prepended, orphan `tool` messages are dropped, and a trailing assistant turn
  with no tool calls is removed (the API rejects prompts ending on an assistant
  turn). Each repair is reported so nothing changes silently.
- If the file ended on an assistant turn that **did** issue tool calls (the
  process died mid-command), the tool calls are answered with a
  `[SESSION RECOVERED]` notice stating the command was **not** re-executed — the
  agent is told not to assume it had any effect, rather than replaying it.

### Multiple instances in one directory

Each process claims its session file with an `flock` on a sidecar
`.aicsession.lock` (the data file itself is atomically replaced, so the lock
lives beside it). Launching a second AI-Commander in the same directory warns
and shifts to a numbered file instead of clobbering the first:

```
[SESSION] WARNING: another AI-Commander instance is already using .aicsession
          in this directory; this instance uses .aicsession2 instead (instance 2)
```

Third instance → `.aicsession3`, and so on. The lock is released by the kernel
when a process exits or dies, so a crashed run can never permanently squatter a
name. `--session <path>` participates in the same numbering
(`<path>2`, `<path>3`, …).

### Crash recovery semantics

| Failure | What the file holds | On restart |
| ------- | ------------------- | ---------- |
| `Ctrl+C` / unhandled exception | History through the last completed step, re-saved in `finally` | Resumed verbatim |
| `kill` / terminal closed | Same, via the `SIGTERM`/`SIGHUP` handlers | Resumed verbatim |
| `kill -9` / power loss / segfault | Everything up to the last completed assistant turn or tool result | Resumed; an in-flight tool call is closed with a "not re-executed" notice |
| Hand-broken JSON | Previous good file kept, bad copy saved as `.corrupt.*` | Starts clean |

At most the single in-flight turn — never earlier history — can be missing, and
the only thing never replayed automatically is a shell command that had not
finished when the process died.

---

## Sandboxing (Linux)

By default, on Linux, the agent attempts to enable an OS-level
[Landlock](https://landlock.io/) sandbox using `py-landlock`. When active,
writes are restricted to the current working directory, `/tmp`, `/dev`, and
`/dev/pts`. The status is shown in the agent output panel (green when active,
red when unsandboxed).

### What the sandbox restricts

- **Writes** are limited to the current working directory (recursively),
  `/tmp`, `/dev`, and `/dev/pts`.
- **Reads and execution** are allowed anywhere.
- **Network access** is preserved (so `curl`, `wget`, and the OpenAI client
  still work).
- `/dev` and `/dev/pts` (plus the `IOCTL_DEV` right, Landlock ABI v5) are
  required for PTY allocation.

The sandbox is applied **before** the agent thread starts, so the Landlock
domain covers the whole process — including every command the agent runs.

### Enabling / disabling

- The sandbox is attempted automatically on Linux unless `--disable-sandbox`
  is given.
- If `py-landlock` is not installed, the agent simply runs unsandboxed.
- The sandbox is **best-effort** (`strict=False`): on older kernels or unusual
  configurations it degrades gracefully rather than failing hard.

### When it is not applied

The sandbox is only applied on Linux. On other platforms (macOS, Windows) the
agent runs unsandboxed and reports that status at startup.

> **Tip:** The status bar and the startup banner show whether the sandbox is
> active. If you see `[red]Sandbox: off[/red]`, either you passed
> `--disable-sandbox`, `py-landlock` is missing, or you are not on Linux.

---

## Fast mode

`--fast` is designed for local inference and low-latency setups. It:

- Forces the `truncate` context-compression algorithm (no extra LLM
  summarizer calls).
- Uses a smaller, faster system prompt that drops the verbose persistence
  policy, internet-access recipe, PTY notes, and "think carefully" guidance.

This reduces prompt size and latency at the cost of less-detailed operating
instructions for the model.

---

## Debugging

- `--debug` dumps the full conversation history to `commander-debug.txt` at
  each step (before the LLM call), including per-message roles, tool calls,
  and estimated token counts. Useful for diagnosing context-compression or
  prompt-format issues.
- The status bar shows a live estimate of the current context window and the
  cumulative session token total (excluding prompt-cached tokens, which are
  typically not billed).
- The `tests/` directory contains a test suite for truncation and core
  functionality. See `tests/test_truncation.py` for usage.

---

## Project structure

```
├── aic.py            # The entire agent (single-file implementation)
├── requirements.txt  # Python dependencies
├── README.md         # This file
├── tests/            # Test suite (truncation + core helpers)
└── LICENSE           # Apache 2.0
```

---

## License

This project is licensed under the [Apache License 2.0](LICENSE).
