agent-aimee 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -0,0 +1,240 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-aimee
3
+ Version: 0.1.0
4
+ Summary: A very minimal LLM agent loop with tools and skills
5
+ Project-URL: Homepage, https://github.com/Fortyseven/AgentAImee
6
+ Project-URL: Source Code, https://github.com/Fortyseven/AgentAImee
7
+ Project-URL: Issues, https://github.com/Fortyseven/AgentAImee/issues
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: httpx>=0.27
19
+ Description-Content-Type: text/markdown
20
+
21
+ # AgentAImee
22
+
23
+ A very minimal LLM agent loop for Python, with tools and skills. One runtime
24
+ dependency (`httpx`). Drop it into an existing project; you own the config,
25
+ the tools, and the behavior. Aimee gives you the loop, the OpenAI-compatible
26
+ client, skill discovery, AGENTS.md support, and callback hooks.
27
+
28
+ - **Model**: any OpenAI-compatible `chat/completions` endpoint via
29
+ `OPENAI_API_BASE` + `OPENAI_API_KEY` (no `openai` package).
30
+ - **Tools**: built-in `read`, `write`, `edit`, `bash` — **all opt-in** — plus
31
+ your own tools.
32
+ - **Skills**: `SKILL.md` directories; a catalog is injected into the system
33
+ prompt and the agent reads the full skill on demand.
34
+ - **Hooks**: extend the run with callbacks — stream tokens, approve/modify
35
+ tool calls, observe turns, handle errors.
36
+ - **Sync or async**: `agent.run(task)` works in any script; `await
37
+ agent.run_async(task)` for async code.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ uv add agent-aimee # or: pip install agent-aimee
43
+ ```
44
+
45
+ ```python
46
+ from aimee import Aimee, AimeeConfig # module name is `aimee`
47
+ ```
48
+
49
+ ## Development
50
+
51
+ ```bash
52
+ uv sync
53
+ uv run pytest -q # test suite (no network needed)
54
+ ```
55
+
56
+ ## Quickstart
57
+
58
+ ```bash
59
+ export OPENAI_API_BASE=https://api.openai.com/v1 # or any compatible gateway
60
+ export OPENAI_API_KEY=sk-...
61
+ uv run python examples/basic.py "your task here" # or --repl
62
+ ```
63
+
64
+ The example wires everything up: multi-root workspace, a skills directory,
65
+ AGENTS.md, three basic tools, and console hooks.
66
+
67
+ ## Usage
68
+
69
+ ```python
70
+ from pathlib import Path
71
+ from aimee import Aimee, AimeeConfig
72
+ from aimee.tools import basic_tools
73
+
74
+ config = AimeeConfig(
75
+ roots=[Path.cwd(), Path.home() / ".myapp"], # workspace roots (first = primary)
76
+ skills_dirs=[Path.home() / ".myapp" / "skills"],
77
+ model="gpt-4o-mini", # default: "default"
78
+ )
79
+ agent = Aimee(config, tools=basic_tools()) # read, write, edit, bash
80
+ # agent = Aimee(config, tools=[read()]) # or just the tools you want
81
+
82
+ report = agent.run("Summarize AGENTS.md") # sync (also safe inside a running loop)
83
+ # report = await agent.run_async("...") # async
84
+
85
+ print(report.final_text, report.turns, report.tool_calls, report.usage)
86
+ ```
87
+
88
+ `RunReport` fields: `final_text`, `turns`, `tool_calls`, `usage` (tokens,
89
+ when the provider reports them), `truncated` (max turns hit), `messages`
90
+ (full transcript).
91
+
92
+ ## Sessions (multi-turn memory)
93
+
94
+ `agent.run(task)` is stateless — every run starts fresh. For a conversation
95
+ that remembers, create a session:
96
+
97
+ ```python
98
+ session = agent.session()
99
+ session.run("hello, who am I talking to?")
100
+ session.run("what did I just ask?") # the model sees the whole prior exchange
101
+
102
+ session.history # snapshot: [system, user, assistant, user, assistant, ...]
103
+ session.clear() # start over (a fresh system prompt is built on the next run)
104
+ ```
105
+
106
+ - A session is bound to one agent (sharing its tools/hooks/config); one agent
107
+ can hold many independent sessions at once.
108
+ - `session.run()` / `await session.run_async()` mirror `Aimee.run()` /
109
+ `run_async()` — same hooks fire, same `RunReport` comes back (with `messages`
110
+ being the full session history).
111
+ - Tool calls and their results are part of the history too, so the model can
112
+ reference earlier tool output in later turns.
113
+ - History is unbounded by design: long conversations will eventually hit the
114
+ model's context limit — `session.clear()` resets when that gets close.
115
+ (`history` returns a snapshot; subclass or wrap the session for custom
116
+ trimming strategies.)
117
+
118
+ ## Configuration (`AimeeConfig`)
119
+
120
+ | Field | Default | Meaning |
121
+ | --- | --- | --- |
122
+ | `roots` | `[Path.cwd()]` | Workspace roots, in priority order (see below). All paths are configurable here. |
123
+ | `model` | `"default"` | Model name passed to the endpoint. |
124
+ | `api_base` | `$OPENAI_API_BASE` → `https://api.openai.com/v1` | Endpoint base URL. |
125
+ | `api_key` | `$OPENAI_API_KEY` | Bearer token. |
126
+ | `system_prompt` | built-in minimal prompt | Replaces the base prompt. |
127
+ | `agents_md` | nearest `AGENTS.md` walking up from `roots` | Explicit AGENTS.md path. |
128
+ | `skills_dirs` | `[]` | Directories containing `SKILL.md` skill folders. |
129
+ | `max_turns` | `30` | Model-call budget per run. |
130
+ | `temperature` / `max_tokens` | `None` | Passed through when set. |
131
+ | `bash_timeout` | `120` | Default shell timeout (seconds). |
132
+ | `output_limit` | `100_000` | Tool output truncation (chars). |
133
+
134
+ **Multi-root rules.** `roots[0]` is the primary root. `read`/`edit` resolve
135
+ relative paths against roots in order (first existing file wins); `write`
136
+ updates the first root that contains the file, and creates new files under
137
+ the primary root; `bash` runs with the primary root as cwd; AGENTS.md
138
+ discovery walks up from each root in order until one is found.
139
+
140
+ ## Tools
141
+
142
+ Tools are OpenAI function-calling specs plus a handler:
143
+
144
+ ```python
145
+ from aimee import Tool
146
+
147
+ Tool(
148
+ name="current_time",
149
+ description="Get the current local date and time (ISO format).",
150
+ parameters={"type": "object", "properties": {}}, # JSON Schema
151
+ handler=lambda args, ctx: "...", # sync or async; returns str
152
+ )
153
+ agent.add_tool(tool)
154
+ ```
155
+
156
+ `ctx` is a `ToolContext` giving handlers the same workspace view as the
157
+ built-ins: `ctx.roots`, `ctx.resolve(path, must_exist=...)`,
158
+ `ctx.config.bash_timeout`, `ctx.config.output_limit`.
159
+
160
+ Built-ins (opt-in): `basic_tools()` → all four, `basic_tools(["read", "edit"])`
161
+ → a subset, or import individually: `from aimee.tools import read, write, edit, bash`.
162
+
163
+ | Tool | Behavior |
164
+ | --- | --- |
165
+ | `read(path, offset?, limit?)` | Numbered lines (paged), or directory listing. |
166
+ | `write(path, content)` | Create/overwrite; parent dirs created. |
167
+ | `edit(path, old_text, new_text)` | Exact-string replace; `old_text` must match exactly once. |
168
+ | `bash(command, timeout?)` | Shell in the primary root; `[exit N]` + combined output, truncated. |
169
+
170
+ > **Security note.** `bash` has no built-in approval. The `on_tool_call` hook
171
+ > is the safety mechanism — see `examples/custom_tool.py` for a hook that
172
+ > denies commands containing `rm `. Don't enable `bash` without one.
173
+
174
+ ## Hooks
175
+
176
+ Register any object defining any subset of these methods (sync or async),
177
+ via `Aimee(..., hooks=[...])` or `agent.add_hook(obj)`:
178
+
179
+ | Hook | Purpose |
180
+ | --- | --- |
181
+ | `on_delta(chunk: str)` | One streamed chunk of assistant text (streaming on by default). |
182
+ | `on_tool_call(name, args) -> ToolDecision \| None` | Approval gate. `None` = allow; `ToolDecision.deny(reason)` blocks (reason goes to the model); `ToolDecision.modify(new_args)` replaces args. First non-None decision wins. |
183
+ | `on_tool_result(name, args, result)` | After a tool finished (including denials/errors). |
184
+ | `on_turn(turn, response)` | After each completed model response. |
185
+ | `on_error(error)` | Tool errors (run continues, error text goes to the model) and API errors (run aborts). |
186
+ | `on_done(report)` | Once, when the run finishes. |
187
+
188
+ ## Skills
189
+
190
+ A skill is a directory with a `SKILL.md`:
191
+
192
+ ```
193
+ myapp/skills/
194
+ └── pdf-extract/
195
+ └── SKILL.md
196
+ ```
197
+
198
+ ```markdown
199
+ ---
200
+ name: pdf-extract
201
+ description: Extract text from PDF files. Use when the task involves PDFs.
202
+ ---
203
+ Full instructions here. The agent reads this file (via the read tool)
204
+ before using the skill.
205
+ ```
206
+
207
+ Set `skills_dirs=[...]`; Aimee injects only the catalog (name, description,
208
+ path) into the system prompt, so skills cost almost nothing until used.
209
+ Frontmatter is parsed minimally (flat `key: value`, no PyYAML).
210
+
211
+ ## AGENTS.md
212
+
213
+ If `agents_md` is unset, Aimee finds the nearest `AGENTS.md` walking up from
214
+ each root (in root order) and includes it under a "Project instructions"
215
+ heading. Give an explicit path to override.
216
+
217
+ ## Tests
218
+
219
+ ```bash
220
+ uv run pytest -q
221
+ ```
222
+
223
+ The suite is fully offline: the HTTP client is tested against
224
+ `httpx.MockTransport`, and the agent loop against a scripted fake model.
225
+
226
+ ## Project layout
227
+
228
+ ```
229
+ src/aimee/
230
+ agent.py # the loop + Aimee facade (sync/async)
231
+ client.py # OpenAI-compatible client (chat + SSE streaming)
232
+ config.py # AimeeConfig
233
+ hooks.py # hook dispatch (sync/async)
234
+ prompt.py # system prompt assembly (base + AGENTS.md + skills)
235
+ skills.py # SKILL.md discovery + catalog
236
+ types.py # messages, responses, reports
237
+ tools/ # base primitives + built-in read/write/edit/bash
238
+ examples/ # basic.py, custom_tool.py + workspace fixtures
239
+ tests/ # offline test suite
240
+ ```
@@ -0,0 +1,136 @@
1
+ # Aimee — Minimal LLM Agent Loop Library (Python)
2
+
3
+ ## Context
4
+
5
+ New Python library **Aimee**: a very minimal LLM agent loop supporting tools and skills, designed to be dropped into *any* existing Python project. The client owns configuration and tool management; Aimee provides the loop, the OpenAI-compatible client, skills, AGENTS.md support, and callback hooks for extension.
6
+
7
+ ### Decisions (confirmed with user)
8
+
9
+ - **Deps**: one runtime dep — `httpx`. OpenAI integration implemented by hand (chat completions + SSE streaming), no `openai` package.
10
+ - **API shape**: sync-first public API over an async core. `Aimee.run(task)` works in any script (runs the async core on a dedicated background event-loop thread, so it also works inside an already-running loop). `await aimee.run_async(task)` for async callers. Tools and hooks may be sync *or* async callables.
11
+ - **Streaming**: token-level `on_delta` hook (SSE deltas) *plus* turn-level complete messages.
12
+ - **Skills**: `SKILL.md` dirs (YAML frontmatter `name`/`description` + markdown body). Aimee scans configurable skill dirs, injects a **catalog** (name — description — path) into the system prompt, agent reads the full file on demand via `read`. Frontmatter parsed by hand (2-line `key: value` parser) — no PyYAML dep.
13
+ - **Python**: 3.10+.
14
+ - **Multi-root workspace**: `roots: list[Path]` (default `[Path.cwd()]`) — clients can add secondary locations like `~/.myapp`; per-root resolution rules defined in config section. (rev 2)
15
+ - **Model**: default `model="default"` (many OpenAI-compatible gateways route on that name). (rev 2)
16
+ - **Tool selection**: built-in tools are individually importable factories (`read()`, `write()`, `edit()`, `bash()`) plus `basic_tools(names=None)` for subsets. (rev 2)
17
+
18
+ ## Approach
19
+
20
+ ### Package layout (`src` layout, uv-managed, hatchling)
21
+
22
+ ```
23
+ pyproject.toml # deps: httpx; dev: pytest, ruff
24
+ README.md
25
+ src/aimee/
26
+ __init__.py # exports: Aimee, AimeeConfig, Tool, ToolContext, RunReport, ToolDecision, Skill, load_skills
27
+ types.py # Message, ChatResponse, TokenUsage, RunReport, ToolDecision
28
+ config.py # AimeeConfig (dataclass)
29
+ client.py # OpenAIClient (httpx): chat() + stream chat, SSE parsing, auth header
30
+ agent.py # async agent loop + Aimee sync/async facade
31
+ hooks.py # hook protocols + sync/async dispatch helper
32
+ skills.py # SKILL.md discovery + frontmatter parse + catalog rendering
33
+ prompt.py # system prompt assembly: base + AGENTS.md + skills catalog
34
+ tools/
35
+ __init__.py # read(), write(), edit(), bash() factories + basic_tools(names=None) (all opt-in)
36
+ base.py # Tool dataclass (name, description, parameters JSON-schema, handler) + ToolContext
37
+ fs.py # read, write, edit
38
+ bash.py # bash
39
+ examples/
40
+ basic.py # drop-in example: config, basic tools, skills, AGENTS.md, prints deltas, mini REPL
41
+ custom_tool.py # custom tool registration + tool-approval hook demo
42
+ tests/
43
+ test_client.py
44
+ test_agent.py
45
+ test_tools.py
46
+ test_skills.py
47
+ test_prompt.py
48
+ test_hooks.py
49
+ ```
50
+
51
+ ### Core design
52
+
53
+ **`AimeeConfig`** (dataclass, all optional with sane defaults):
54
+ - `roots: list[Path] = [Path.cwd()]` — one or more workspace roots. `roots[0]` is the primary root; extra roots (e.g. `~/.myapp`) extend where the agent may operate. Resolution semantics: `read`/`edit` try each root in order and use the first where the file **exists**; `write` uses the first root where the file exists, else `roots[0]`; `bash` runs with `cwd=roots[0]`; AGENTS.md walk-up tries each root in order (first found wins)
55
+ - `model: str = "default"`
56
+ - `api_base: str | None` — defaults to env `OPENAI_API_BASE`
57
+ - `api_key: str | None` — defaults to env `OPENAI_API_KEY`
58
+ - `system_prompt: str | None` — client-provided base prompt (replaces built-in minimal one)
59
+ - `agents_md: Path | None = None` — explicit AGENTS.md; else walk up from each `roots` entry in order (first found wins, stop at filesystem root)
60
+ - `skills_dirs: list[Path] = []`
61
+ - `max_turns: int = 30`
62
+ - `temperature: float | None = None`, `max_tokens: int | None = None`
63
+ - `bash_timeout: int = 120`, `output_limit: int = 100_000` (tool output truncation)
64
+
65
+ **`OpenAIClient`** (`client.py`, httpx):
66
+ - `POST {api_base}/chat/completions` with `Authorization: Bearer {key}`, model, messages, tools (OpenAI function-calling schema).
67
+ - `chat(...)` → full `ChatResponse`; `stream_chat(...)` → iterator of SSE deltas (hand-parsed: `data:` lines, `[DONE]`, content/tool_call arg deltas accumulated per index).
68
+ - HTTP errors → `AimeeError` with status + body snippet. No retries (keep minimal; client can wrap).
69
+
70
+ **`Aimee` facade** (`agent.py`):
71
+ - `__init__(config=None, *, client=None, tools=(), hooks=())` — `client` injectable (test seam).
72
+ - `add_tool(Tool)`, `add_hook(obj)`; `run(task) -> RunReport` (sync), `async run_async(task) -> RunReport`.
73
+ - Loop: build system prompt once (base + AGENTS.md + skills catalog) → append user message → call model with tools → if `tool_calls`: dispatch each (approval hook → execute → `role:"tool"` result message) → repeat until no tool calls or `max_turns` (→ report `truncated=True`).
74
+ - Tool execution errors are returned to the model as `Error: ...` content (so it can recover) **and** fired to `on_error`.
75
+ - `RunReport`: `final_text`, `turns`, `tool_calls` count, `usage` (accumulated `TokenUsage` when provider supplies it), `truncated`, `messages` (full transcript).
76
+
77
+ **Hooks** (`hooks.py`) — duck-typed; register any object exposing any subset of:
78
+ - `on_delta(chunk: str)` — streaming tokens
79
+ - `on_tool_call(name, args: dict) -> ToolDecision | None` — approval gate: `None`/`allow()` = run; `ToolDecision.deny(reason)` = skip, reason goes to model; `ToolDecision.modify(new_args)` = run with replaced args
80
+ - `on_tool_result(name, args, result: str)`
81
+ - `on_turn(turn: int, response: ChatResponse)`
82
+ - `on_error(error: Exception)`
83
+ - `on_done(report: RunReport)`
84
+ - All may be sync or async; dispatch helper handles both.
85
+
86
+ **Built-in tools** (all opt-in; each tool is a parameterless factory, and clients can pick exactly what they want):
87
+ - `from aimee.tools import read, write, edit, bash, basic_tools`
88
+ - `Aimee(tools=[read()])` — just `read`; `Aimee(tools=basic_tools())` — all four; `Aimee(tools=basic_tools(["read", "edit"]))` — named subset.
89
+ - Handler signature is `handler(args: dict, ctx: ToolContext) -> str`; Aimee builds `ToolContext` (roots, output_limit, bash_timeout, config) per call, so built-ins and client custom tools share the same workspace view.
90
+ - `read(path, offset=1, limit=2000)` — numbered lines, relative paths resolve per the multi-root rule above, truncation notice when capped.
91
+ - `write(path, content)` — create/overwrite, mkdir parents.
92
+ - `edit(path, old_text, new_text)` — exact string replace; `old_text` must match **exactly once** (error message lists match count otherwise).
93
+ - `bash(command, timeout?)` — run in `roots[0]`, shell, combined stdout+stderr truncated to `output_limit`, returns `[exit N]\n<output>`. No built-in approval — the `on_tool_call` hook is the safety mechanism (documented).
94
+
95
+ **System prompt** (`prompt.py`): built-in minimal base (identity + "use tools when needed") or `config.system_prompt`, then `# AGENTS.md` section (explicit path, else first found walking up from `roots` in order) under a heading, then `# Skills` catalog block:
96
+ ```
97
+ - name — description
98
+ path/to/SKILL.md (read before use)
99
+ ```
100
+
101
+ ### Tests (pytest)
102
+
103
+ - `test_client.py` — `httpx.MockTransport`: request shape, auth header, base URL join; `ChatResponse` parse incl. tool_calls; SSE stream delta accumulation (content + tool arg deltas); HTTP error → `AimeeError`.
104
+ - `test_agent.py` — scripted fake model (inject via `client=`): no-tools → done in 1 turn; tool call → result → second turn → done; denied tool call returns reason to model; `max_turns` truncation; usage accumulation; `run()` (sync) and `run_async()` parity.
105
+ - `test_tools.py` — `tmp_path`: read (numbering, offset/limit, missing file), write (parents created, overwrite), edit (unique match, no-match, multi-match error), bash (echo, nonzero exit, cwd, timeout), multi-root resolution (read/edit across `roots`, write defaulting to `roots[0]`).
106
+ - `test_skills.py` — frontmatter parse (valid, missing description, no frontmatter), discovery across multiple dirs, catalog rendering.
107
+ - `test_prompt.py` — AGENTS.md explicit path, walk-up discovery (tmp tree), nearest-wins, skills block present/absent, custom system_prompt replacement.
108
+ - `test_hooks.py` — sync + async hooks all dispatched; deny/modify decisions.
109
+
110
+ ### Examples
111
+
112
+ - `examples/basic.py` — reads `OPENAI_API_BASE`/`KEY` from env, builds `AimeeConfig` (`roots=[Path.cwd(), Path.home()/".myapp"]`-style multi-root config, skills dir pointing at a `skills/` folder next to the example containing one tiny SKILL.md, `AGENTS.md` next to it), registers `basic_tools(["read", "write", "edit"])`, an `on_delta` hook that prints tokens, runs one task (or `--repl` for a tiny stdin loop), prints `RunReport`.
113
+ - `examples/custom_tool.py` — registers a single custom tool via `Tool(...)` using `ToolContext`, plus an `on_tool_call` hook that denies `bash` calls containing a marker string; prints report.
114
+
115
+ ## Files to modify
116
+
117
+ All new (empty repo): tree above. No git repo exists yet — initialize `git init` + `.gitignore` (uv: `.venv`, `__pycache__`, `dist/`) as step 1.
118
+
119
+ ## Steps
120
+
121
+ - [x] 1. `git init`, `.gitignore`, `pyproject.toml` (uv, hatchling, `httpx>=0.27`, pytest+ruff dev group), `uv sync`
122
+ - [x] 2. `types.py` + `config.py`
123
+ - [x] 3. `client.py` (OpenAIClient, SSE parsing) + `test_client.py`
124
+ - [x] 4. `hooks.py` + `prompt.py` + `agent.py` (loop + facade) + `test_agent.py`, `test_prompt.py`, `test_hooks.py`
125
+ - [x] 5. `skills.py` + `test_skills.py`
126
+ - [x] 6. `tools/` (base, fs, bash) + `test_tools.py`
127
+ - [x] 7. `examples/basic.py` + `examples/custom_tool.py` (+ their `AGENTS.md`/skill fixtures)
128
+ - [x] 8. `README.md` (quickstart, config/hook/tool/skill reference, security note on bash)
129
+ - [x] 9. `uv run ruff check` + `ruff format`, full pytest green
130
+
131
+ ## Verification
132
+
133
+ - `uv run pytest -q` — all green.
134
+ - `uv run ruff check .` + `uv run ruff format --check .` clean.
135
+ - Manual (needs real key): `OPENAI_API_BASE=... OPENAI_API_KEY=... uv run python examples/basic.py "List the skills you have and read AGENTS.md, then summarize both."` — expect skill catalog + AGENTS.md content reflected in answer; `--repl` for interactive.
136
+ - `uv build` produces wheel + sdist without error (drop-in readiness check).
@@ -0,0 +1,220 @@
1
+ # AgentAImee
2
+
3
+ A very minimal LLM agent loop for Python, with tools and skills. One runtime
4
+ dependency (`httpx`). Drop it into an existing project; you own the config,
5
+ the tools, and the behavior. Aimee gives you the loop, the OpenAI-compatible
6
+ client, skill discovery, AGENTS.md support, and callback hooks.
7
+
8
+ - **Model**: any OpenAI-compatible `chat/completions` endpoint via
9
+ `OPENAI_API_BASE` + `OPENAI_API_KEY` (no `openai` package).
10
+ - **Tools**: built-in `read`, `write`, `edit`, `bash` — **all opt-in** — plus
11
+ your own tools.
12
+ - **Skills**: `SKILL.md` directories; a catalog is injected into the system
13
+ prompt and the agent reads the full skill on demand.
14
+ - **Hooks**: extend the run with callbacks — stream tokens, approve/modify
15
+ tool calls, observe turns, handle errors.
16
+ - **Sync or async**: `agent.run(task)` works in any script; `await
17
+ agent.run_async(task)` for async code.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ uv add agent-aimee # or: pip install agent-aimee
23
+ ```
24
+
25
+ ```python
26
+ from aimee import Aimee, AimeeConfig # module name is `aimee`
27
+ ```
28
+
29
+ ## Development
30
+
31
+ ```bash
32
+ uv sync
33
+ uv run pytest -q # test suite (no network needed)
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ ```bash
39
+ export OPENAI_API_BASE=https://api.openai.com/v1 # or any compatible gateway
40
+ export OPENAI_API_KEY=sk-...
41
+ uv run python examples/basic.py "your task here" # or --repl
42
+ ```
43
+
44
+ The example wires everything up: multi-root workspace, a skills directory,
45
+ AGENTS.md, three basic tools, and console hooks.
46
+
47
+ ## Usage
48
+
49
+ ```python
50
+ from pathlib import Path
51
+ from aimee import Aimee, AimeeConfig
52
+ from aimee.tools import basic_tools
53
+
54
+ config = AimeeConfig(
55
+ roots=[Path.cwd(), Path.home() / ".myapp"], # workspace roots (first = primary)
56
+ skills_dirs=[Path.home() / ".myapp" / "skills"],
57
+ model="gpt-4o-mini", # default: "default"
58
+ )
59
+ agent = Aimee(config, tools=basic_tools()) # read, write, edit, bash
60
+ # agent = Aimee(config, tools=[read()]) # or just the tools you want
61
+
62
+ report = agent.run("Summarize AGENTS.md") # sync (also safe inside a running loop)
63
+ # report = await agent.run_async("...") # async
64
+
65
+ print(report.final_text, report.turns, report.tool_calls, report.usage)
66
+ ```
67
+
68
+ `RunReport` fields: `final_text`, `turns`, `tool_calls`, `usage` (tokens,
69
+ when the provider reports them), `truncated` (max turns hit), `messages`
70
+ (full transcript).
71
+
72
+ ## Sessions (multi-turn memory)
73
+
74
+ `agent.run(task)` is stateless — every run starts fresh. For a conversation
75
+ that remembers, create a session:
76
+
77
+ ```python
78
+ session = agent.session()
79
+ session.run("hello, who am I talking to?")
80
+ session.run("what did I just ask?") # the model sees the whole prior exchange
81
+
82
+ session.history # snapshot: [system, user, assistant, user, assistant, ...]
83
+ session.clear() # start over (a fresh system prompt is built on the next run)
84
+ ```
85
+
86
+ - A session is bound to one agent (sharing its tools/hooks/config); one agent
87
+ can hold many independent sessions at once.
88
+ - `session.run()` / `await session.run_async()` mirror `Aimee.run()` /
89
+ `run_async()` — same hooks fire, same `RunReport` comes back (with `messages`
90
+ being the full session history).
91
+ - Tool calls and their results are part of the history too, so the model can
92
+ reference earlier tool output in later turns.
93
+ - History is unbounded by design: long conversations will eventually hit the
94
+ model's context limit — `session.clear()` resets when that gets close.
95
+ (`history` returns a snapshot; subclass or wrap the session for custom
96
+ trimming strategies.)
97
+
98
+ ## Configuration (`AimeeConfig`)
99
+
100
+ | Field | Default | Meaning |
101
+ | --- | --- | --- |
102
+ | `roots` | `[Path.cwd()]` | Workspace roots, in priority order (see below). All paths are configurable here. |
103
+ | `model` | `"default"` | Model name passed to the endpoint. |
104
+ | `api_base` | `$OPENAI_API_BASE` → `https://api.openai.com/v1` | Endpoint base URL. |
105
+ | `api_key` | `$OPENAI_API_KEY` | Bearer token. |
106
+ | `system_prompt` | built-in minimal prompt | Replaces the base prompt. |
107
+ | `agents_md` | nearest `AGENTS.md` walking up from `roots` | Explicit AGENTS.md path. |
108
+ | `skills_dirs` | `[]` | Directories containing `SKILL.md` skill folders. |
109
+ | `max_turns` | `30` | Model-call budget per run. |
110
+ | `temperature` / `max_tokens` | `None` | Passed through when set. |
111
+ | `bash_timeout` | `120` | Default shell timeout (seconds). |
112
+ | `output_limit` | `100_000` | Tool output truncation (chars). |
113
+
114
+ **Multi-root rules.** `roots[0]` is the primary root. `read`/`edit` resolve
115
+ relative paths against roots in order (first existing file wins); `write`
116
+ updates the first root that contains the file, and creates new files under
117
+ the primary root; `bash` runs with the primary root as cwd; AGENTS.md
118
+ discovery walks up from each root in order until one is found.
119
+
120
+ ## Tools
121
+
122
+ Tools are OpenAI function-calling specs plus a handler:
123
+
124
+ ```python
125
+ from aimee import Tool
126
+
127
+ Tool(
128
+ name="current_time",
129
+ description="Get the current local date and time (ISO format).",
130
+ parameters={"type": "object", "properties": {}}, # JSON Schema
131
+ handler=lambda args, ctx: "...", # sync or async; returns str
132
+ )
133
+ agent.add_tool(tool)
134
+ ```
135
+
136
+ `ctx` is a `ToolContext` giving handlers the same workspace view as the
137
+ built-ins: `ctx.roots`, `ctx.resolve(path, must_exist=...)`,
138
+ `ctx.config.bash_timeout`, `ctx.config.output_limit`.
139
+
140
+ Built-ins (opt-in): `basic_tools()` → all four, `basic_tools(["read", "edit"])`
141
+ → a subset, or import individually: `from aimee.tools import read, write, edit, bash`.
142
+
143
+ | Tool | Behavior |
144
+ | --- | --- |
145
+ | `read(path, offset?, limit?)` | Numbered lines (paged), or directory listing. |
146
+ | `write(path, content)` | Create/overwrite; parent dirs created. |
147
+ | `edit(path, old_text, new_text)` | Exact-string replace; `old_text` must match exactly once. |
148
+ | `bash(command, timeout?)` | Shell in the primary root; `[exit N]` + combined output, truncated. |
149
+
150
+ > **Security note.** `bash` has no built-in approval. The `on_tool_call` hook
151
+ > is the safety mechanism — see `examples/custom_tool.py` for a hook that
152
+ > denies commands containing `rm `. Don't enable `bash` without one.
153
+
154
+ ## Hooks
155
+
156
+ Register any object defining any subset of these methods (sync or async),
157
+ via `Aimee(..., hooks=[...])` or `agent.add_hook(obj)`:
158
+
159
+ | Hook | Purpose |
160
+ | --- | --- |
161
+ | `on_delta(chunk: str)` | One streamed chunk of assistant text (streaming on by default). |
162
+ | `on_tool_call(name, args) -> ToolDecision \| None` | Approval gate. `None` = allow; `ToolDecision.deny(reason)` blocks (reason goes to the model); `ToolDecision.modify(new_args)` replaces args. First non-None decision wins. |
163
+ | `on_tool_result(name, args, result)` | After a tool finished (including denials/errors). |
164
+ | `on_turn(turn, response)` | After each completed model response. |
165
+ | `on_error(error)` | Tool errors (run continues, error text goes to the model) and API errors (run aborts). |
166
+ | `on_done(report)` | Once, when the run finishes. |
167
+
168
+ ## Skills
169
+
170
+ A skill is a directory with a `SKILL.md`:
171
+
172
+ ```
173
+ myapp/skills/
174
+ └── pdf-extract/
175
+ └── SKILL.md
176
+ ```
177
+
178
+ ```markdown
179
+ ---
180
+ name: pdf-extract
181
+ description: Extract text from PDF files. Use when the task involves PDFs.
182
+ ---
183
+ Full instructions here. The agent reads this file (via the read tool)
184
+ before using the skill.
185
+ ```
186
+
187
+ Set `skills_dirs=[...]`; Aimee injects only the catalog (name, description,
188
+ path) into the system prompt, so skills cost almost nothing until used.
189
+ Frontmatter is parsed minimally (flat `key: value`, no PyYAML).
190
+
191
+ ## AGENTS.md
192
+
193
+ If `agents_md` is unset, Aimee finds the nearest `AGENTS.md` walking up from
194
+ each root (in root order) and includes it under a "Project instructions"
195
+ heading. Give an explicit path to override.
196
+
197
+ ## Tests
198
+
199
+ ```bash
200
+ uv run pytest -q
201
+ ```
202
+
203
+ The suite is fully offline: the HTTP client is tested against
204
+ `httpx.MockTransport`, and the agent loop against a scripted fake model.
205
+
206
+ ## Project layout
207
+
208
+ ```
209
+ src/aimee/
210
+ agent.py # the loop + Aimee facade (sync/async)
211
+ client.py # OpenAI-compatible client (chat + SSE streaming)
212
+ config.py # AimeeConfig
213
+ hooks.py # hook dispatch (sync/async)
214
+ prompt.py # system prompt assembly (base + AGENTS.md + skills)
215
+ skills.py # SKILL.md discovery + catalog
216
+ types.py # messages, responses, reports
217
+ tools/ # base primitives + built-in read/write/edit/bash
218
+ examples/ # basic.py, custom_tool.py + workspace fixtures
219
+ tests/ # offline test suite
220
+ ```
@@ -0,0 +1,9 @@
1
+ # Example workspace
2
+
3
+ This is the Aimee example workspace. A few ground rules:
4
+
5
+ - `secondary/` is a second workspace root (multi-root demo). It holds
6
+ `secret-note.txt` — read it when asked.
7
+ - Safety: never run destructive shell commands (anything containing `rm `).
8
+ The client in `custom_tool.py` also enforces this via an approval hook.
9
+ - Be concise.
@@ -0,0 +1,5 @@
1
+ # JOKE
2
+
3
+ Why do programmers prefer dark mode?
4
+
5
+ Because light attracts bugs.