AI-Commander
PyPI package: 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-based terminal
UI with split panels (prompt, console, agent output, status footer) and an
/autoapprovetoggle for the approval gate. --nogui— the original direct-CLI behaviour (stdout/stderr, no TUI).
Table of Contents
- Features
- Requirements
- Installation
- Usage
- Command-line options
- How it works
- The TUI
- The approval gate
- Context compression
- Session persistence
- Sandboxing (Linux)
- Fast mode
- Debugging
- Project structure
- 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
ddgsPython 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 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/completionsinterface).
Installation
From PyPI (or from source)
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:
pip install --upgrade aic-agent
From a checkout of this repository
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:
pip install -e .
Alternatively, install only the dependencies and run the script directly:
pip install -r requirements.txt
python3 aic.py ...
Note: The
py-landlockpackage 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 package:
# 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
# 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
- The agent receives your task request.
- It plans the next step and may emit thinking tokens.
- It proposes shell commands, which you approve or auto-approve.
- Commands run (optionally sandboxed and time-limited), and their output is fed back to the model.
- 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/suggestcommands 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:
- Condense oversized tool outputs in place (oldest first), replacing
verbose command results with
<condensed tool output>. - 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
package, which implements a Factory.ai-style anchored-summary incremental
compressor. When the non-system log exceeds the budget it:
- Evicts the oldest prefix of the conversation.
- Folds it into a persistent
AnchoredSummaryvia an LLM call on the evicted segment only (so the summarizer call stays short on slow-prefill systems). - 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 —
.aicsessionin 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
(
atexitplusSIGTERM/SIGHUPhandlers). 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.
--noguiresumes 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:
{
"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_ids). 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:
# 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
toolmessages 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 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). /devand/dev/pts(plus theIOCTL_DEVright, 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-sandboxis given. - If
py-landlockis 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-landlockis missing, or you are not on Linux.
Fast mode
--fast is designed for local inference and low-latency setups. It:
- Forces the
truncatecontext-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
--debugdumps the full conversation history tocommander-debug.txtat 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. Seetests/test_truncation.pyfor 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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aic_agent-1.0.2.tar.gz.
File metadata
- Download URL: aic_agent-1.0.2.tar.gz
- Upload date:
- Size: 92.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b7079f2279a62eeee99d554c28ba7c63e8f2c1f8e7c21ab71b3d20da4d2516a1
|
|
| MD5 |
43c608b43e1efb86ddf0011f11f513ea
|
|
| BLAKE2b-256 |
2148964832e1729c2cfd44250ecdc2ef8c741925425121bea16d1161f800c883
|
File details
Details for the file aic_agent-1.0.2-py3-none-any.whl.
File metadata
- Download URL: aic_agent-1.0.2-py3-none-any.whl
- Upload date:
- Size: 67.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4fa8a0149ff3ee1cba9a851e6ef832cc26ab5d9b2746c6620b243dba0530ff86
|
|
| MD5 |
1405047f7591a6445c5325bcb45b6802
|
|
| BLAKE2b-256 |
fe90584c8d04aa43bc389c1a5eed04760fe816b22a5b908f70c12667126f4af8
|