Skip to content

Commit 0ea57e9

Browse files
feat: add ultra-minimal implementations (71-210 LOC)
Multi-language micro agents that run anywhere: - nano.py (71 LOC) - Python, zero deps, runs on Pi - nano-minimal.ts (85 LOC) - TypeScript, raw HTTP, no SDK - nano.ts (210 LOC) - TypeScript with Anthropic SDK All implement the core agent loop with 5 tools: read_file, write_file, edit_file, bash, list_dir Vision: Same agent runs on desktop, server, Pi, embedded, robots Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 66da470 commit 0ea57e9

File tree

4 files changed

+410
-12
lines changed

4 files changed

+410
-12
lines changed

ROADMAP.md

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -229,22 +229,54 @@ Inspiration: Aider voice support
229229
### 4.1 Multi-Language Implementations
230230
```
231231
Priority: P1 - Community growth
232+
Core insight: The agent loop is <100 LOC in any language!
232233
```
233234

234-
- [ ] **Python Implementation** (~2K LOC)
235-
- Target: Data scientists, ML engineers
236-
- Jupyter notebook integration
237-
- pip installable
238-
239-
- [ ] **Rust Implementation** (~4K LOC)
235+
#### Current Implementations (DONE!)
236+
| File | LOC | Language | Dependencies | Platforms |
237+
|------|-----|----------|--------------|-----------|
238+
| `nano.py` | **71** | Python | None (stdlib) | All (Pi, embedded, WASM) |
239+
| `nano-minimal.ts` | **85** | TypeScript | None (fetch) | Node 18+, Bun, Deno |
240+
| `nano.ts` | **210** | TypeScript | Anthropic SDK | Node, Bun |
241+
| `src/` | ~5K | TypeScript | Full deps | Desktop, server |
242+
243+
#### Feature Tiers
244+
| Tier | LOC | Features |
245+
|------|-----|----------|
246+
| **Micro** | 70-100 | 5 tools, single-turn, zero deps |
247+
| **Mini** | 200-300 | 7 tools, agent loop, REPL |
248+
| **Standard** | 500-1K | 10+ tools, sessions, multi-provider |
249+
| **Full** | 2-3K | MCP, LSP, plugins, hooks |
250+
251+
#### Planned Implementations
252+
- [x] **Python Micro** (71 LOC) - ✅ DONE
253+
- Zero dependencies, runs on Raspberry Pi
254+
- `python nano.py "your prompt"`
255+
256+
- [x] **TypeScript Micro** (85 LOC) - ✅ DONE
257+
- Zero SDK deps, uses raw fetch
258+
- `bun nano-minimal.ts "your prompt"`
259+
260+
- [ ] **Rust Micro** (~100 LOC)
240261
- Target: Embedded, IoT, robotics
241-
- no_std support for microcontrollers
242-
- Cross-compile for ARM
262+
- no_std variant for microcontrollers
263+
- Cross-compile for ARM, ESP32
264+
265+
- [ ] **Go Micro** (~90 LOC)
266+
- Target: Cloud/DevOps, single binary
267+
- Docker-native, K8s friendly
268+
269+
- [ ] **Zig Micro** (~150 LOC)
270+
- Target: Embedded, WASM, game dev
271+
- C interop for legacy systems
272+
273+
- [ ] **C Micro** (~300 LOC)
274+
- Target: Bare metal, MCU
275+
- Minimal memory footprint
243276

244-
- [ ] **Go Implementation** (~3K LOC)
245-
- Target: Cloud/DevOps
246-
- Single binary deployment
247-
- Docker-native
277+
- [ ] **Lua Micro** (~80 LOC)
278+
- Target: Neovim plugin
279+
- Native Lua integration
248280

249281
### 4.2 IDE Integrations
250282
```

nano-minimal.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* nano-opencode minimal: Zero-dependency AI agent (~150 LOC)
4+
* Works with Node.js 18+, Bun, Deno - any device with fetch
5+
*
6+
* Usage: ANTHROPIC_API_KEY=sk-... bun nano-minimal.ts "read package.json"
7+
*/
8+
9+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs';
10+
import { execFileSync } from 'child_process';
11+
12+
// === CONFIG ===
13+
const API_KEY = process.env.ANTHROPIC_API_KEY || '';
14+
const MODEL = process.env.MODEL || 'claude-sonnet-4-20250514';
15+
const API_URL = 'https://api.anthropic.com/v1/messages';
16+
17+
// === TOOLS ===
18+
const TOOLS = [
19+
{ name: 'read_file', description: 'Read file', input_schema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
20+
{ name: 'write_file', description: 'Write file', input_schema: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } },
21+
{ name: 'edit_file', description: 'Edit file', input_schema: { type: 'object', properties: { path: { type: 'string' }, old_string: { type: 'string' }, new_string: { type: 'string' } }, required: ['path', 'old_string', 'new_string'] } },
22+
{ name: 'bash', description: 'Run command', input_schema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } },
23+
{ name: 'list_dir', description: 'List directory', input_schema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
24+
];
25+
26+
// === TOOL EXECUTION ===
27+
function run(name: string, i: Record<string, string>): string {
28+
try {
29+
if (name === 'read_file') return existsSync(i.path) ? readFileSync(i.path, 'utf-8') : 'File not found';
30+
if (name === 'write_file') { writeFileSync(i.path, i.content); return 'OK'; }
31+
if (name === 'edit_file') {
32+
const c = readFileSync(i.path, 'utf-8');
33+
if (!c.includes(i.old_string)) return 'old_string not found';
34+
writeFileSync(i.path, c.replace(i.old_string, i.new_string));
35+
return 'OK';
36+
}
37+
if (name === 'bash') return execFileSync('sh', ['-c', i.command], { encoding: 'utf-8', timeout: 30000 }).slice(0, 50000);
38+
if (name === 'list_dir') return readdirSync(i.path || '.').map(f => `${statSync(`${i.path || '.'}/${f}`).isDirectory() ? 'd' : '-'} ${f}`).join('\n');
39+
return 'Unknown tool';
40+
} catch (e) { return `Error: ${e}`; }
41+
}
42+
43+
// === API CALL ===
44+
interface Message { role: 'user' | 'assistant'; content: unknown }
45+
interface Block { type: string; id?: string; name?: string; input?: Record<string, string>; text?: string }
46+
47+
async function call(messages: Message[]): Promise<{ content: Block[]; stop_reason: string }> {
48+
const res = await fetch(API_URL, {
49+
method: 'POST',
50+
headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY, 'anthropic-version': '2023-06-01' },
51+
body: JSON.stringify({ model: MODEL, max_tokens: 8192, tools: TOOLS, messages, system: 'You are a coding assistant. Use tools to help.' }),
52+
});
53+
if (!res.ok) throw new Error(`API error: ${res.status} ${await res.text()}`);
54+
return res.json();
55+
}
56+
57+
// === AGENT LOOP ===
58+
async function agent(prompt: string): Promise<string> {
59+
const messages: Message[] = [{ role: 'user', content: prompt }];
60+
61+
while (true) {
62+
const res = await call(messages);
63+
messages.push({ role: 'assistant', content: res.content });
64+
65+
if (res.stop_reason !== 'tool_use') {
66+
return res.content.filter(b => b.type === 'text').map(b => b.text).join('');
67+
}
68+
69+
const results = res.content.filter(b => b.type === 'tool_use').map(b => {
70+
console.log(`⚡ ${b.name}`);
71+
const r = run(b.name!, b.input!);
72+
console.log(r.slice(0, 100) + (r.length > 100 ? '...' : ''));
73+
return { type: 'tool_result', tool_use_id: b.id, content: r };
74+
});
75+
76+
messages.push({ role: 'user', content: results });
77+
}
78+
}
79+
80+
// === MAIN ===
81+
const prompt = process.argv.slice(2).join(' ');
82+
if (!prompt) { console.log('Usage: bun nano-minimal.ts "your prompt"'); process.exit(1); }
83+
if (!API_KEY) { console.log('Set ANTHROPIC_API_KEY'); process.exit(1); }
84+
85+
agent(prompt).then(console.log).catch(e => console.error('Error:', e));

nano.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env python3
2+
"""
3+
nano-opencode: Minimal AI coding agent in Python (~100 LOC)
4+
Works on any device with Python 3.8+ (Raspberry Pi, embedded, etc.)
5+
6+
Usage: ANTHROPIC_API_KEY=sk-... python nano.py "read package.json"
7+
"""
8+
9+
import os, sys, json, subprocess
10+
from pathlib import Path
11+
from urllib.request import Request, urlopen
12+
13+
API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
14+
MODEL = os.environ.get("MODEL", "claude-sonnet-4-20250514")
15+
API_URL = "https://api.anthropic.com/v1/messages"
16+
17+
TOOLS = [
18+
{"name": "read_file", "description": "Read file", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
19+
{"name": "write_file", "description": "Write file", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
20+
{"name": "edit_file", "description": "Edit file", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_string": {"type": "string"}, "new_string": {"type": "string"}}, "required": ["path", "old_string", "new_string"]}},
21+
{"name": "bash", "description": "Run command", "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
22+
{"name": "list_dir", "description": "List directory", "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
23+
]
24+
25+
def run(name: str, i: dict) -> str:
26+
try:
27+
if name == "read_file":
28+
return Path(i["path"]).read_text() if Path(i["path"]).exists() else "File not found"
29+
if name == "write_file":
30+
Path(i["path"]).write_text(i["content"]); return "OK"
31+
if name == "edit_file":
32+
p = Path(i["path"]); c = p.read_text()
33+
if i["old_string"] not in c: return "old_string not found"
34+
p.write_text(c.replace(i["old_string"], i["new_string"], 1)); return "OK"
35+
if name == "bash":
36+
return subprocess.run(i["command"], shell=True, capture_output=True, text=True, timeout=30).stdout[:50000]
37+
if name == "list_dir":
38+
p = Path(i.get("path", "."))
39+
return "\n".join(f"{'d' if x.is_dir() else '-'} {x.name}" for x in p.iterdir())
40+
return "Unknown tool"
41+
except Exception as e:
42+
return f"Error: {e}"
43+
44+
def call(messages: list) -> dict:
45+
data = json.dumps({"model": MODEL, "max_tokens": 8192, "tools": TOOLS, "messages": messages, "system": "You are a coding assistant. Use tools to help."}).encode()
46+
req = Request(API_URL, data=data, headers={"Content-Type": "application/json", "x-api-key": API_KEY, "anthropic-version": "2023-06-01"})
47+
with urlopen(req) as res:
48+
return json.loads(res.read())
49+
50+
def agent(prompt: str) -> str:
51+
messages = [{"role": "user", "content": prompt}]
52+
while True:
53+
res = call(messages)
54+
messages.append({"role": "assistant", "content": res["content"]})
55+
if res["stop_reason"] != "tool_use":
56+
return "".join(b.get("text", "") for b in res["content"] if b["type"] == "text")
57+
results = []
58+
for b in res["content"]:
59+
if b["type"] == "tool_use":
60+
print(f"⚡ {b['name']}")
61+
r = run(b["name"], b["input"])
62+
print(r[:100] + ("..." if len(r) > 100 else ""))
63+
results.append({"type": "tool_result", "tool_use_id": b["id"], "content": r})
64+
messages.append({"role": "user", "content": results})
65+
66+
if __name__ == "__main__":
67+
if len(sys.argv) < 2:
68+
print("Usage: python nano.py 'your prompt'"); sys.exit(1)
69+
if not API_KEY:
70+
print("Set ANTHROPIC_API_KEY"); sys.exit(1)
71+
print(agent(" ".join(sys.argv[1:])))

0 commit comments

Comments
 (0)