A minimal coding agent named after Agent K from Men in Black.
Features • Quick Start • Tutorial • How k Was Built • Build Your Own • Commands • Reference
Features
- Minimal. One file, stdlib, no frameworks.
- OpenAI Responses and Chat Completions APIs. Native function calling with self-contained request history; select the API your provider supports.
- Tool loop.
read,write,edit,bash— the four operations that cover most coding tasks. - Context-mode compaction. Large tool outputs are truncated to a useful head/tail preview; the full text is saved to disk alongside the session.
- Session persistence. Every conversation is saved to
~/.k/sessions/<id>.json. Resume any session by ID. - Skills. Load Markdown skill files from
~/.agents/skills/to inject system instructions dynamically. - Terminal UI. Pi-inspired truecolor light/dark themes, pinned status line (showing model, tokens, git branch, cwd), colored Markdown rendering, multiline prompts.
- Portable. Works with OpenAI-compatible providers that support either Responses or Chat Completions — OpenAI, OpenRouter, xAI, local LLMs, and OpenCode Zen.
Quick Start
# Prerequisites: Python ≥3.12, uv (or pip)
uv sync
cp .env.example .env # add OPENAI_API_KEY
uv run python main.py
# Try it instantly (no clone needed):
uvx --from mini-agent-k k
# Or install permanently via pip/uv:
pip install mini-agent-k
# then:
k
PyPI distribution:
mini-agent-k. The executable is namedk.
Resume a session:
uv run python main.py --resume 20260727-120000-ab12cd
Tutorial
Using k as a Coding Agent
When you run k, you get a prompt (❯). Type a coding task and the agent will reason over it, call tools (read, write, edit, bash), and return an answer. Press Ctrl+J to add a line break; press Enter to submit the complete prompt.
Example session:
❯ read the main.py file and summarize the architecture
k | /home/user/project | main | model: openrouter/free | tokens: 1234 | context: 567
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
The project is a single-file coding agent with:
• OpenAI Responses API loop
• 4 tools: read, write, edit, bash
• TTY-aware terminal UI with light/dark themes
• Session persistence under ~/.k/sessions/
• Skills system for injecting system instructions
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
You can chain multiple tasks in the same session — the conversation history is preserved.
Passing images: k's read tool detects image files (jpg, png, gif, webp) and sends them as base64 attachments to the model. The model can inspect screenshots, diagrams, or UI mockups.
Working with Skills
Skills are Markdown files in ~/.agents/skills/<name>/SKILL.md. They act as additional system instructions injected per session.
~/.agents/skills/
python/
SKILL.md # "Follow PEP 8, use type hints..."
review/
SKILL.md # "Focus on security and edge cases..."
In-session commands:
| Command | Action |
|---|---|
/skills |
List available skills |
/skill <name> |
Load a skill for this session |
/skill clear |
Remove all loaded skills |
Resuming Sessions
Sessions are saved automatically after every completed answer. Find the session ID with:
ls ~/.k/sessions/
Resume with:
uv run python main.py --resume <session-id>
Themes
uv run python main.py --theme dark # dark mode
OPENAI_MODEL_NAME=gpt-4o uv run python main.py # custom model
For OpenCode Zen, configure its OpenAI-compatible endpoint and use Chat Completions mode:
OPENAI_BASE_URL=https://opencode.ai/zen/v1 \
OPENAI_API_KEY="$OPENCODE_API_KEY" \
OPENAI_MODEL_NAME=<zen-model-id> \
K_API_MODE=chat-completions \
uv run python main.py
Set defaults via environment variables:
K_THEME—light(default) ordarkOPENAI_MODEL_NAME— model name (default:openrouter/free)K_API_MODE—responses(default) orchat-completions; use the latter for OpenAI-compatible providers without a Responses endpoint, including OpenCode Zen.
Context-mode for Large Outputs
When a tool call returns more than 12,000 characters, k truncates it to a 6,000-char head and 2,000-char tail with a notice:
[context-mode: output compacted from 45000 chars]
<first 6000 chars>
...
<last 2000 chars>
Full output saved: ~/.k/sessions/<id>-artifacts/bash-142530-abc123.txt
The full output is always saved to an artifact file next to the session. You can re-read it with read if the model needs the full content.
How k Was Built (Commit by Commit)
k started as a 50-line experiment and grew into ~400 lines of production-ish agent. Here's the story of every meaningful commit — what was added and why.
1. The Seed — Minimal Agent Loop
198628e init commit
The first commit was a bare OpenAI Responses API loop: send a prompt, parse tool calls, dispatch them, feed results back. Two tools (read, bash), no state, no TUI. Just the loop — the hardest part.
# The essence hasn't changed
response = client.responses.create(model=MODEL, instructions=SYSTEM_PROMPT, input=history, tools=TOOLS)
while True:
calls = [item for item in response.output if item.type == "function_call"]
if not calls:
print(response.output_text)
break
# dispatch, feed back, loop
2. More Tools, Structured Output
28e512d feat: add native local tools
Added write and edit — the 4-tool set that covers almost every coding task. read gained numbered line output so the model could reference line numbers in edits. edit uses exact string replacement (rejecting ambiguous matches) — the simplest correct implementation.
78d2f71 feat: add native tool calling tui
First TUI: colored output for tool calls (blue), errors (red), answers. The model's tool invocations were printed so the user could see what the agent was doing.
3. The Status Line
c3d7e2a chore: rename agent to k
977ee63 feat: show model and token usage
51ac10e feat: show cli context in status
1da8232 fix: keep cli running after each task
47296e1 feat: pin cli statusline
b4680b3 docs: describe pinned cli statusline
9dd8752 feat: clear terminal at startup before rendering pinned status
bf6395e fix: preserve prompt position when repainting status
dad5c42 render 'k' in status lines with reversed foreground (bg=accent)
The status line evolved over several commits. The key trick: ANSI escape sequences set a scroll region (\033[2;Nr) so row 1 stays pinned while rows 2+ scroll. Each task repaints the status without moving the input cursor (\033[s / \033[u).
k | /repo | main | model: openrouter/free | tokens: 1234 | context: 567
4. Clean Exits and Commands
2eef8bc feat: handle cli interruption cleanly
4aca4e9 feat: save and resume sessions
1d79899 feat: add slash command handling
Ctrl+C / Ctrl+D / blank input all exit cleanly. Sessions serialize the full history array to ~/.k/sessions/<id>.json — resume with --resume. Slash commands (/quit, later /skills, /skill) were wired in.
5. Session Persistence
4aca4e9 feat: save and resume sessions
bbc8ed7 feat: show k sunglasses banner
73c5415 feat: compact large tool outputs
Sessions save automatically after every completed answer. The session ID is a timestamp + random suffix: 20260727-120000-ab12cd. Large tool outputs (>12KB) are truncated with a head/tail preview and the full output is saved to an artifact file next to the session JSON.
6. Terminal Polish
36a5eb5 docs: document minimal agent usage
1b65489 docs: add VHS demo recording
b161b48 feat: render markdown with accessible terminal colors
4b92598 fix: preserve terminal theme contrast
54fd651 feat: add pi-inspired terminal palettes
Markdown rendering came next: headings, bold, italic, code, lists, blockquotes, fences. Two color palettes (light/dark), Pi-inspired. The demo GIF was recorded with VHS (assets/demo.tape).
7. Skills System
966e20e docs: add skills support design
f8f386d feat: add skill discovery
e0ec168 feat: add session skill commands
53a24a7 docs: document skill commands
7e48984 ignore files
9ce9ce7 refactor: remove built-in skills, only ~/.agents/skills/
8afabc0 fix: discover skills from ~/.agent/skills/<name>/SKILL.md
750fb36 fix: correct skills dir to .agents
Skills are Markdown files in ~/.agents/skills/<name>/SKILL.md. They get appended to the system prompt as additional instructions. No plugin system, no DSL — just text injected into the prompt. The directory path was iterated a few times (~/.agent/ → ~/.agents/).
The Pattern
Every feature followed the same path:
- Add the minimum that works (one file, no deps)
- Ship it — the simplest version first
- Iterate on edges — error handling, edge cases, UX polish
- Document — explain the design, not just the API
No premature abstractions, no plugin system, no database, no async framework. ~400 lines total.
Build Your Own
k is designed as both a tool and a reference implementation. The entire agent is one Python file (~400 lines) — here's how to build a minimal coding agent from scratch.
The Core Loop
import json
from openai import OpenAI
client = OpenAI()
tools = [...] # tool definitions
history = [{"role": "user", "content": task}]
response = client.responses.create(
model="openrouter/free",
instructions="You are a coding assistant.",
input=history,
tools=tools,
)
while True:
calls = [item for item in response.output
if item.type == "function_call"]
if not calls:
print(response.output_text)
break
for call in calls:
result = dispatch_tool(call.name, call.arguments)
history.append(call)
history.append(result)
response = client.responses.create(
model="openrouter/free",
instructions="You are a coding assistant.",
input=history,
tools=tools,
)
That's it. The loop is: send input → execute tool calls → feed results back → repeat until the model answers in plain text.
Tools: the 4 You Need
A coding agent needs exactly 4 tools to do almost everything:
| Tool | What it does |
|---|---|
read |
Read file contents (text as numbered lines, images as base64) |
write |
Create or overwrite files |
edit |
Replace exactly one occurrence of text in a file |
bash |
Run shell commands (git, grep, tests, ls) |
Keep tool definitions small and strict. Set strict: true and additionalProperties: false in the JSON schema so the model doesn't hallucinate parameters.
{
"type": "function",
"name": "read",
"description": "Read file contents.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "File path to read"},
"offset": {"type": "integer", "description": "Start line (1-indexed)", "default": 1},
"limit": {"type": "integer", "description": "Max lines to read", "default": 2000},
},
"required": ["path"],
"additionalProperties": False,
},
"strict": True,
}
Session Persistence
Save history so the user can resume:
import json
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4
SESSIONS_DIR = Path.home() / ".k" / "sessions"
def save_session(session: dict) -> None:
path = SESSIONS_DIR / f"{session['id']}.json"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(session, indent=2))
def load_session(session_id: str) -> dict:
return json.loads((SESSIONS_DIR / f"{session_id}.json").read_text())
Handling Large Outputs
Model context windows are finite. When tool output exceeds a threshold, compact it:
MAX_OUTPUT = 12_000
def compact_output(text: str, label: str, session_id: str) -> str:
if len(text) <= MAX_OUTPUT:
return text
# Save full output to disk
artifact_dir = SESSIONS_DIR / f"{session_id}-artifacts"
artifact_dir.mkdir(parents=True, exist_ok=True)
path = artifact_dir / f"{label}-{uuid4().hex[:6]}.txt"
path.write_text(text, encoding="utf-8")
# Return useful preview
return f"[compacted from {len(text)} chars]\n{text[:6000]}\n...\n{text[-2000:]}"
Adding a Terminal UI (Optional)
A pinned status line (row 1) that stays visible while the scrollable area (rows 2+) contains conversation:
import shutil
def pin_status(text: str) -> None:
if not sys.stdout.isatty():
return
rows = shutil.get_terminal_size().lines
# Reserve row 1 for status, rows 2+ for scrollable content
sys.stdout.write(f"\033[2;{rows}r\033[1;1H\033[2K{text}\n")
sys.stdout.write("\033[2;1H")
sys.stdout.flush()
ANSI escape sequence breakdown:
\033[2;Nr— set scroll region (row 2 to N)\033[1;1H— move cursor to row 1\033[2K— clear entire row
Skills Injection
Allow users to load custom system instructions from Markdown files:
def load_skills(names: list[str], base_prompt: str) -> str:
skills_dir = Path.home() / ".agents" / "skills"
blocks = []
for name in names:
path = skills_dir / name / "SKILL.md"
if path.exists():
blocks.append(f"### {name}\n{path.read_text()}")
if blocks:
return base_prompt + "\n\n## Active skills\n\n" + "\n\n".join(blocks)
return base_prompt
Key Decisions
- Self-contained history vs
previous_response_id:ksends the full history on every turn. It supports both Responses and Chat Completions formats, so providers only need to implement one of those APIs. - No streaming: Blocking
responses.create()calls keep the agent loop simple. Streaming adds complexity with no functional benefit for a tool-calling agent. - One file, no framework: The entire agent is ~400 lines. There's no plugin system, no database, no async framework. Add structure only when the file becomes hard to change.
Your First Custom Agent (Minimal Example)
"""minimal_agent.py — your first coding agent in <100 lines."""
import json, subprocess, sys
from openai import OpenAI
client = OpenAI()
def bash(command):
return subprocess.run(command, shell=True, capture_output=True, text=True).stdout
def read(path):
with open(path) as f:
return f.read()
TOOLS = [{
"type": "function", "name": "bash",
"description": "Run a shell command",
"parameters": {"type": "object", "properties": {"command": {"type": "string"}},
"required": ["command"], "additionalProperties": False},
"strict": True,
}, {
"type": "function", "name": "read",
"description": "Read a file",
"parameters": {"type": "object", "properties": {"path": {"type": "string"}},
"required": ["path"], "additionalProperties": False},
"strict": True,
}]
HANDLERS = {"bash": bash, "read": read}
history = [{"role": "user", "content": sys.argv[1]}]
response = client.responses.create(
model="openrouter/free",
instructions="You are a coding assistant. Use tools to answer.",
input=history, tools=TOOLS,
)
while True:
calls = [i for i in response.output if i.type == "function_call"]
if not calls:
print(response.output_text)
break
for call in calls:
result = HANDLERS[call.name](**json.loads(call.arguments))
history.append(call)
history.append({"type": "function_call_output", "call_id": call.call_id, "output": str(result)})
response = client.responses.create(
model="openrouter/free",
instructions="You are a coding assistant.",
input=history, tools=TOOLS,
)
Usage: python minimal_agent.py "find all python files larger than 10KB"
Commands
| Command | Action |
|---|---|
/quit |
Exit without calling the model |
/skills |
List available skills in ~/.agents/skills/ |
/skill <name> |
Load a skill for this session |
/skill clear |
Clear all loaded skills |
| Ctrl+J | Insert a line break in the current prompt |
| blank prompt / EOF (Ctrl+D) | Exit cleanly |
| Ctrl+C | Interrupt / exit |
Reference
Project Structure
k/
├── main.py # ~400 lines, the entire agent
├── test_main.py # pytest test suite
├── pyproject.toml # uv/pip project config
├── assets/
│ ├── agentk.jpg # banner image
│ └── k-demo.gif # demo animation
└── .env # OPENAI_API_KEY (not committed)
Environment Variables
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
— | API key for the LLM provider |
OPENAI_MODEL_NAME |
openrouter/free |
Model identifier |
K_API_MODE |
responses |
responses or chat-completions |
K_THEME |
light |
Color theme (light or dark) |
Session Files
~/.k/sessions/<id>.json # conversation history + skills
~/.k/sessions/<id>-artifacts/ # compacted tool outputs
Dependencies
openai>=2.48.0— OpenAI Responses and Chat Completions APIspython-dotenv>=1.2.2—.envloading
No web framework, no database, no async runtime, no plugins.
License
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 mini_agent_k-0.0.1.tar.gz.
File metadata
- Download URL: mini_agent_k-0.0.1.tar.gz
- Upload date:
- Size: 3.9 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0cd7fa7c0ee6dd9aff5a0a16becab70c03f9c26cf0dc1ca62382b6f1cab8a145
|
|
| MD5 |
d91ae12d0812f8ec615ec08c95a51d50
|
|
| BLAKE2b-256 |
76d94f810c4070c1126023f4b7d96fadcef2371d2cbd3ef1139f04c3d35f104d
|
File details
Details for the file mini_agent_k-0.0.1-py3-none-any.whl.
File metadata
- Download URL: mini_agent_k-0.0.1-py3-none-any.whl
- Upload date:
- Size: 16.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ccf65ae2bcc3c075d15d8f99ee06b8cd16ae47f4c91aa9d9a36d75f428dd1914
|
|
| MD5 |
329cd9c404277bdff19ff92109574042
|
|
| BLAKE2b-256 |
cadec0d3a175eafd14fe62b079bfe498c216cf0ff7ebf877535f97fefb957c20
|