autoagent-core 0.11.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.
- autoagent_core-0.11.0/LICENSE +21 -0
- autoagent_core-0.11.0/PKG-INFO +468 -0
- autoagent_core-0.11.0/README.md +441 -0
- autoagent_core-0.11.0/autoagent/__init__.py +137 -0
- autoagent_core-0.11.0/autoagent/agent.py +1246 -0
- autoagent_core-0.11.0/autoagent/approval.py +371 -0
- autoagent_core-0.11.0/autoagent/dynamic.py +256 -0
- autoagent_core-0.11.0/autoagent/errors.py +99 -0
- autoagent_core-0.11.0/autoagent/evolution.py +421 -0
- autoagent_core-0.11.0/autoagent/http.py +190 -0
- autoagent_core-0.11.0/autoagent/logging.py +96 -0
- autoagent_core-0.11.0/autoagent/mcp.py +456 -0
- autoagent_core-0.11.0/autoagent/memory.py +278 -0
- autoagent_core-0.11.0/autoagent/orchestrator.py +470 -0
- autoagent_core-0.11.0/autoagent/otel.py +214 -0
- autoagent_core-0.11.0/autoagent/pipeline.py +80 -0
- autoagent_core-0.11.0/autoagent/providers/__init__.py +36 -0
- autoagent_core-0.11.0/autoagent/providers/anthropic.py +250 -0
- autoagent_core-0.11.0/autoagent/providers/base.py +33 -0
- autoagent_core-0.11.0/autoagent/providers/gemini.py +251 -0
- autoagent_core-0.11.0/autoagent/providers/openai.py +238 -0
- autoagent_core-0.11.0/autoagent/providers/routing.py +111 -0
- autoagent_core-0.11.0/autoagent/registry.py +339 -0
- autoagent_core-0.11.0/autoagent/sandbox.py +599 -0
- autoagent_core-0.11.0/autoagent/schema.py +397 -0
- autoagent_core-0.11.0/autoagent/trace.py +255 -0
- autoagent_core-0.11.0/autoagent/workspace.py +262 -0
- autoagent_core-0.11.0/autoagent_core.egg-info/PKG-INFO +468 -0
- autoagent_core-0.11.0/autoagent_core.egg-info/SOURCES.txt +69 -0
- autoagent_core-0.11.0/autoagent_core.egg-info/dependency_links.txt +1 -0
- autoagent_core-0.11.0/autoagent_core.egg-info/requires.txt +13 -0
- autoagent_core-0.11.0/autoagent_core.egg-info/top_level.txt +1 -0
- autoagent_core-0.11.0/pyproject.toml +82 -0
- autoagent_core-0.11.0/setup.cfg +4 -0
- autoagent_core-0.11.0/tests/test_agent.py +213 -0
- autoagent_core-0.11.0/tests/test_agent_as_tool.py +101 -0
- autoagent_core-0.11.0/tests/test_agent_cancel.py +154 -0
- autoagent_core-0.11.0/tests/test_agent_dynamic_prompt.py +118 -0
- autoagent_core-0.11.0/tests/test_agent_parallel_tools.py +109 -0
- autoagent_core-0.11.0/tests/test_agent_post_turn_hook.py +204 -0
- autoagent_core-0.11.0/tests/test_agent_streaming.py +229 -0
- autoagent_core-0.11.0/tests/test_approval.py +163 -0
- autoagent_core-0.11.0/tests/test_bridge.py +148 -0
- autoagent_core-0.11.0/tests/test_concurrency.py +179 -0
- autoagent_core-0.11.0/tests/test_docker_sandbox.py +132 -0
- autoagent_core-0.11.0/tests/test_dynamic.py +251 -0
- autoagent_core-0.11.0/tests/test_dynamic_parser_property.py +75 -0
- autoagent_core-0.11.0/tests/test_evolution.py +309 -0
- autoagent_core-0.11.0/tests/test_http.py +189 -0
- autoagent_core-0.11.0/tests/test_logging.py +84 -0
- autoagent_core-0.11.0/tests/test_mcp.py +93 -0
- autoagent_core-0.11.0/tests/test_memory.py +459 -0
- autoagent_core-0.11.0/tests/test_orchestrator.py +266 -0
- autoagent_core-0.11.0/tests/test_otel.py +179 -0
- autoagent_core-0.11.0/tests/test_pipeline.py +166 -0
- autoagent_core-0.11.0/tests/test_provider_parity.py +261 -0
- autoagent_core-0.11.0/tests/test_providers.py +595 -0
- autoagent_core-0.11.0/tests/test_providers_security.py +74 -0
- autoagent_core-0.11.0/tests/test_registry.py +292 -0
- autoagent_core-0.11.0/tests/test_registry_async.py +60 -0
- autoagent_core-0.11.0/tests/test_registry_validation.py +169 -0
- autoagent_core-0.11.0/tests/test_routing_provider.py +273 -0
- autoagent_core-0.11.0/tests/test_runstate.py +195 -0
- autoagent_core-0.11.0/tests/test_sandbox.py +251 -0
- autoagent_core-0.11.0/tests/test_sandbox_bypasses.py +128 -0
- autoagent_core-0.11.0/tests/test_schema.py +284 -0
- autoagent_core-0.11.0/tests/test_schema_serialization.py +138 -0
- autoagent_core-0.11.0/tests/test_summarizing_memory_and_budget.py +154 -0
- autoagent_core-0.11.0/tests/test_tool_policy.py +191 -0
- autoagent_core-0.11.0/tests/test_trace.py +588 -0
- autoagent_core-0.11.0/tests/test_workspace.py +151 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alyce
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autoagent-core
|
|
3
|
+
Version: 0.11.0
|
|
4
|
+
Summary: LLM agent library with tool use, dynamic tool generation, and software evolution capabilities
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: jsonschema>=4.0
|
|
16
|
+
Provides-Extra: otel
|
|
17
|
+
Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-timeout>=2.1; extra == "dev"
|
|
23
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# autoagent
|
|
29
|
+
|
|
30
|
+
**A minimal, auditable LLM agent core for Python. Not a framework.**
|
|
31
|
+
|
|
32
|
+
`autoagent` is a small library that gives you the agent loop — LLM ↔ tools, done right —
|
|
33
|
+
and the safety rails around it, without pulling in a framework's worldview. You can read
|
|
34
|
+
the entire core in an hour, and every boundary your agent has is **Python code you wrote**,
|
|
35
|
+
not a prompt you hope it respects.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from autoagent import Agent
|
|
39
|
+
|
|
40
|
+
agent = Agent.from_model("gemini", "gemini-3.5-flash")
|
|
41
|
+
|
|
42
|
+
@agent.tool
|
|
43
|
+
def add(a: int, b: int) -> int:
|
|
44
|
+
"""Add two integers."""
|
|
45
|
+
return a + b
|
|
46
|
+
|
|
47
|
+
result = agent.run("What is 21 + 21?")
|
|
48
|
+
print(result.output) # "42"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That's the whole API for the common case. The JSON schema for `add` is generated from the
|
|
52
|
+
type annotations. The loop, retries, and provider wire formats are handled for you.
|
|
53
|
+
|
|
54
|
+
Need live output? Streaming is a plain sync iterator — no async ceremony:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
for ev in agent.run_stream("What is 21 + 21?"):
|
|
58
|
+
if ev.type == "text":
|
|
59
|
+
print(ev.text, end="", flush=True) # token deltas as they arrive
|
|
60
|
+
elif ev.type == "tool_start":
|
|
61
|
+
print(f"\n[calling {ev.tool_name}…]")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Why another agent library?
|
|
67
|
+
|
|
68
|
+
Because most of them are **frameworks**: they want to own your prompts, your memory, your
|
|
69
|
+
control flow, and your dependency tree. When something misbehaves at 2 a.m., you're reading
|
|
70
|
+
someone else's abstraction stack instead of your own code.
|
|
71
|
+
|
|
72
|
+
`autoagent`'s theses:
|
|
73
|
+
|
|
74
|
+
1. **The agent must be readable.** The core loop is a few hundred lines of plain Python.
|
|
75
|
+
No metaclasses, no callbacks-on-callbacks, no YAML.
|
|
76
|
+
2. **Bounding is code, not prompts.** File access goes through `ProjectWorkspace`
|
|
77
|
+
(allowlists, anti path-traversal, write history + rollback). Generated tools run in a
|
|
78
|
+
**sandbox** (Docker isolation when available, hardened AST denylist otherwise) and are
|
|
79
|
+
promoted to native only through a **hash-based manifest a human approves**.
|
|
80
|
+
3. **Zero dependencies for the core.** Python ≥ 3.10, `urllib`, `dataclasses`. No SDKs —
|
|
81
|
+
each provider adapter speaks the wire format directly (~100 lines each).
|
|
82
|
+
4. **Multi-provider without ceremony.** OpenAI, Anthropic, DeepSeek, Gemini. A provider is
|
|
83
|
+
one method: `complete(LLMRequest) -> LLMResponse`. Write your own in 50 lines.
|
|
84
|
+
5. **Synchronous and deterministic — with real streaming.** The loop is sync by design —
|
|
85
|
+
*you* choose your concurrency model (`threading`, `asyncio.to_thread`, a queue). No
|
|
86
|
+
colored functions imposed on your codebase. Streaming is a plain **sync iterator**:
|
|
87
|
+
`for event in agent.run_stream(prompt): …` yields text deltas and tool events as they
|
|
88
|
+
happen (SSE under the hood, all four providers).
|
|
89
|
+
|
|
90
|
+
## What's in the box
|
|
91
|
+
|
|
92
|
+
| Capability | What it gives you |
|
|
93
|
+
|---|---|
|
|
94
|
+
| **Tool schema autogen** | `@agent.tool` reads annotations + docstring → strict JSON schema (`Literal` → enums, `additionalProperties: false` by default) |
|
|
95
|
+
| **`ProjectWorkspace`** | bounded reads/writes: extension allowlists, anti path-traversal, change history, rollback |
|
|
96
|
+
| **Dynamic tools + sandbox** | the agent can write its own tools; they run in Docker (or a hardened subprocess), never natively without human promotion via a hash manifest |
|
|
97
|
+
| **`TraceEmitter`** | typed lifecycle events (`run_start`, `tool_call_*`, `run_end`…) with span/parent IDs → JSONL and/or callback; **secret redaction built in** (Bearer tokens, API keys never hit your logs) |
|
|
98
|
+
| **Streaming** | `run_stream()` / `run_messages_stream()` yield `StreamEvent`s (text deltas, `tool_start`/`tool_end`, corrections) as plain sync iterators — SSE wire streaming in all four providers |
|
|
99
|
+
| **`Memory` protocol** | two methods (`compact`, `recall`) — duck-typed, `@runtime_checkable`. Built-ins: `BufferMemory` (hard cap) and **`SummarizingMemory`** (old turns folded into an *incremental* LLM summary — bounded context without losing established facts — plus dependency-free lexical `recall`). Ship your own vector store for semantic recall; `agent.register_recall_tool()` lets the agent query its memory as a tool |
|
|
100
|
+
| **`RoutingProvider`** | per-request dispatch across providers: text-only turns go to a cheap model, turns carrying images route to a vision-capable one — behind the standard provider interface, invisible to `Agent` |
|
|
101
|
+
| **`post_turn_hook`** | host-side verification loop: inspect what the agent did, inject a correction message, hard-capped iterations — validation in code, not vibes |
|
|
102
|
+
| **`cancel_token`** | cooperative cancellation between LLM turns (`threading.Event`) |
|
|
103
|
+
| **Multimodal** | `ImageAttachment` on messages, serialized per provider |
|
|
104
|
+
| **`agent.as_tool()`** | the minimal multi-agent primitive: expose an agent as a tool of another (supervisor/specialist in two lines); stateless delegation, failures surface as tool errors, delegation cost (`tokens`, `steps`) reported to the parent |
|
|
105
|
+
| **`token_budget`** | hard cap on a run's cumulative token usage (`TokenBudgetExceeded`); per-call `TokenUsage` on results and `done` events — never invented, only what the provider reports |
|
|
106
|
+
| **`parallel_tool_calls`** | opt-in: multiple tool calls in one turn execute on a thread pool (I/O-bound latency win), transcript order stays deterministic |
|
|
107
|
+
| **`Orchestrator`** | for host-driven flows (guided forms, CATI questionnaires): *your* state machine decides every step; the LLM only interprets answers and rephrases prompts |
|
|
108
|
+
| **`MCPClient`** | mount any MCP server's tools as ordinary agent tools (stdio transport, zero dependencies): `mcp.mount(agent, prefix=, include=)` — server schemas validated by the registry before every call |
|
|
109
|
+
| **`OTelTraceExporter`** | the trace tree becomes real OpenTelemetry spans (`agent.run → llm → tool.<name>`) for Jaeger / Tempo / Langfuse / Phoenix; `opentelemetry-api` is an *optional* extra, the core stays dependency-free |
|
|
110
|
+
| **`RunState` + `resume`** | durable runs: a JSON checkpoint after every completed step; resume after a crash, a restart, or past a raised `max_steps` / `token_budget` (`exc.state` is ready to resume) |
|
|
111
|
+
| **`tool_policy`** | one hook for allow / deny / **human approval** / quota-audit, checked for every tool call *before* any side effect; a crashing policy denies (fail-closed); `ApprovalRequired` pauses the run with a resumable snapshot |
|
|
112
|
+
| **`EvolutionRuntime`** | let an agent modify a live project: read state, propose a module, run validation, roll back on failure |
|
|
113
|
+
|
|
114
|
+
## Quickstart
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install autoagent-core # installs as `import autoagent`
|
|
118
|
+
# or from source:
|
|
119
|
+
git clone <this-repo>
|
|
120
|
+
cd autoagent
|
|
121
|
+
pip install jsonschema # the core's only extra; examples may need more
|
|
122
|
+
export GEMINI_API_KEY=... # or OPENAI_API_KEY / ANTHROPIC_API_KEY / DEEPSEEK_API_KEY
|
|
123
|
+
python examples/demo_autoagent.py # the demo below (French prompts/comments)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The demo that carries the argument — one scenario, two files:
|
|
127
|
+
|
|
128
|
+
**[`examples/demo_autoagent.py`](examples/demo_autoagent.py)** (55 lines of code) is a
|
|
129
|
+
**three-agent hierarchy**: an orchestrator delegates two log files to two specialist
|
|
130
|
+
agents (`as_tool()`), **cross-checks their findings against each other** (each
|
|
131
|
+
gateway-502 server error must match one FAILED payment), audits the raw files with its
|
|
132
|
+
own tools when in doubt, then **saves its validated report through a `ProjectWorkspace`**
|
|
133
|
+
fenced to `_out/` + markdown only. The whole delegation tree lands in one trace file via
|
|
134
|
+
a shared `TraceEmitter` — and the script ends by proving the fence deterministically,
|
|
135
|
+
attempting what an agent might: `../demo_autoagent.py` → *Path escapes workspace*,
|
|
136
|
+
`virus.exe` → *extension blocked*, `C:/Windows/x.md` → *absolute paths not allowed*.
|
|
137
|
+
Boundaries you can demo, because they're code.
|
|
138
|
+
|
|
139
|
+
**[`examples/demo_pure_python.py`](examples/demo_pure_python.py)** (164 lines of code) is
|
|
140
|
+
the **same system with no library** — same model, same three agents, same validated
|
|
141
|
+
answer. Everything the library did for free, hand-rolled and annotated: the generic agent
|
|
142
|
+
loop, every tool schema, the provider wire format + retries, delegation with its failure
|
|
143
|
+
contract, the write fence, and a trace tree (without secret redaction).
|
|
144
|
+
|
|
145
|
+
| same behavior, same answer | with autoagent | pure Python |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| lines of code | **55** | **164** |
|
|
148
|
+
| …that you must maintain per provider | no (4 providers included) | yes |
|
|
149
|
+
|
|
150
|
+
### A tool-using agent with memory, tracing, and a verification hook
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import threading
|
|
154
|
+
from autoagent import Agent, BufferMemory, Message, ModelConfig, TraceEmitter, create_provider
|
|
155
|
+
|
|
156
|
+
provider = create_provider(ModelConfig(provider="openai", model="gpt-4o-mini"))
|
|
157
|
+
|
|
158
|
+
def must_have_saved(ctx) -> Message | None:
|
|
159
|
+
"""Host-side check: force another turn if the agent never wrote the file."""
|
|
160
|
+
if not any(tc.name == "write_file" for tc in ctx.tool_calls):
|
|
161
|
+
return Message(role="user", content="You never called write_file. Save your work.")
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
with TraceEmitter(file="run.jsonl") as trace:
|
|
165
|
+
agent = Agent(
|
|
166
|
+
provider,
|
|
167
|
+
system_prompt="You are a careful refactoring assistant.",
|
|
168
|
+
max_steps=12,
|
|
169
|
+
memory=BufferMemory(max_messages=30),
|
|
170
|
+
trace=trace,
|
|
171
|
+
post_turn_hook=must_have_saved,
|
|
172
|
+
max_corrections_per_run=1,
|
|
173
|
+
)
|
|
174
|
+
result = agent.run("Refactor ./api.py", cancel_token=threading.Event())
|
|
175
|
+
|
|
176
|
+
print(result.output, result.steps)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Verdicts as tools (a pattern we use in production)
|
|
180
|
+
|
|
181
|
+
Instead of asking the model for "strict JSON" and parsing it with regexes, expose the
|
|
182
|
+
decision as tools — the model *cannot* answer malformed:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
verdict = {"decided": False}
|
|
186
|
+
|
|
187
|
+
@agent.tool
|
|
188
|
+
def approve(reason: str = "") -> dict:
|
|
189
|
+
"""The last exchange is fine."""
|
|
190
|
+
verdict.update(decided=True, ok=True, reason=reason)
|
|
191
|
+
return {"recorded": True}
|
|
192
|
+
|
|
193
|
+
@agent.tool
|
|
194
|
+
def request_fix(problem: str, instruction: str) -> dict:
|
|
195
|
+
"""Something is actually wrong; give the voice agent a short corrective instruction."""
|
|
196
|
+
verdict.update(decided=True, ok=False, problem=problem, instruction=instruction)
|
|
197
|
+
return {"recorded": True}
|
|
198
|
+
|
|
199
|
+
agent.run(state_and_transcript)
|
|
200
|
+
if verdict.get("ok") is False:
|
|
201
|
+
deliver_instruction(verdict["instruction"])
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This exact pattern supervises a real-time **phone bot** (Gemini Live voice loop): the
|
|
205
|
+
supervisor agent runs in a thread, checks every turn against the call state and the
|
|
206
|
+
caller's past-call memory (via `register_recall_tool`), and injects corrections — without
|
|
207
|
+
ever blocking the audio. The sync core made that trivial: `asyncio.to_thread(supervisor.review, …)`.
|
|
208
|
+
|
|
209
|
+
### Compose agents in two lines (`as_tool`)
|
|
210
|
+
|
|
211
|
+
No crew DSL, no choreography framework — an agent is just a tool of another agent:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
expert = Agent(cheap_provider, system_prompt="You are a traffic-count analyst…")
|
|
215
|
+
supervisor.add_tool(expert.as_tool(
|
|
216
|
+
name="analyze_counts",
|
|
217
|
+
description="Delegate traffic-count questions to the analyst.",
|
|
218
|
+
))
|
|
219
|
+
# Delegation is stateless, failures surface as tool errors (never crash the parent),
|
|
220
|
+
# and the parent sees the cost: {"output": …, "steps": 3, "tokens": 812}.
|
|
221
|
+
# Share one TraceEmitter and the whole hierarchy lands in a single trace tree.
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Keep costs bounded
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
agent = Agent(provider, token_budget=50_000) # hard cap per run — TokenBudgetExceeded beyond
|
|
228
|
+
result = agent.run("…")
|
|
229
|
+
print(result.usage.total_tokens) # provider-reported, never invented
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Mount an MCP server's tools (two lines)
|
|
233
|
+
|
|
234
|
+
The entire MCP tool ecosystem, without wrappers — the server runs as a local
|
|
235
|
+
subprocess (stdio) and each of its tools becomes a regular autoagent tool,
|
|
236
|
+
validated against the server's own JSON Schema before anything is sent:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from autoagent import MCPClient
|
|
240
|
+
|
|
241
|
+
with MCPClient(["npx", "-y", "@modelcontextprotocol/server-filesystem", "."]) as mcp:
|
|
242
|
+
mcp.mount(agent, prefix="fs_", include={"read_text_file", "list_directory"})
|
|
243
|
+
agent.run("List the project files and summarize the README.")
|
|
244
|
+
# isError results surface as tool errors (the model reacts, nothing crashes);
|
|
245
|
+
# transport failures raise MCPError with the server's stderr attached.
|
|
246
|
+
# Prefer include={...}: mounting 3 precise tools beats mounting 40.
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Survive crashes: checkpoint / resume
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
import json, pathlib
|
|
253
|
+
from autoagent import RunState
|
|
254
|
+
|
|
255
|
+
CKPT = pathlib.Path("run_state.json")
|
|
256
|
+
result = agent.run(mission, checkpoint=lambda s: CKPT.write_text(json.dumps(s.to_dict())))
|
|
257
|
+
|
|
258
|
+
# …process died? restart and continue where it stopped:
|
|
259
|
+
state = RunState.from_dict(json.loads(CKPT.read_text()))
|
|
260
|
+
result = agent.resume(state)
|
|
261
|
+
|
|
262
|
+
# Budget ran out mid-run? The exception carries a ready-to-resume snapshot:
|
|
263
|
+
# except TokenBudgetExceeded as exc: agent.token_budget *= 2; agent.resume(exc.state)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Approval gates: pause on sensitive tools, resume after a human decides
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from autoagent import Agent, ApprovalRequired
|
|
270
|
+
|
|
271
|
+
def policy(ctx): # every tool call passes here first
|
|
272
|
+
if "filesystem.write" not in (ctx.spec.permissions if ctx.spec else []):
|
|
273
|
+
return None # allow
|
|
274
|
+
if ctx.call.id not in approvals: # your store, keyed by call id
|
|
275
|
+
raise ApprovalRequired(f"{ctx.call.name}({ctx.call.arguments})")
|
|
276
|
+
|
|
277
|
+
agent = Agent(provider, tool_policy=policy)
|
|
278
|
+
try:
|
|
279
|
+
result = agent.run("Clean up the old logs.")
|
|
280
|
+
except ApprovalRequired as exc:
|
|
281
|
+
save(exc.state.to_dict()) # nothing executed yet — resumable JSON
|
|
282
|
+
notify_operator(exc.calls)
|
|
283
|
+
# …operator approves → agent.resume(state) runs the tool exactly once and finishes.
|
|
284
|
+
# Return a str from the policy to DENY with a reason the model can react to.
|
|
285
|
+
# A crashing policy DENIES (fail-closed): this hook is a security boundary.
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### See your runs in Jaeger / Langfuse (OpenTelemetry)
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from autoagent import TraceEmitter, OTelTraceExporter # pip install autoagent[otel]
|
|
292
|
+
|
|
293
|
+
with OTelTraceExporter() as exporter: # uses the global OTel tracer
|
|
294
|
+
trace = TraceEmitter(file="trace.jsonl", on_event=exporter) # JSONL + OTel spans
|
|
295
|
+
agent = Agent(provider, trace=trace)
|
|
296
|
+
agent.run("…")
|
|
297
|
+
# agent.run → llm → tool.<name>, with durations, statuses and redacted payloads.
|
|
298
|
+
# A broken OTel backend can never break the agent loop.
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Recipes
|
|
302
|
+
|
|
303
|
+
### A file-editing agent that can't escape its box
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from autoagent import Agent, ProjectWorkspace
|
|
307
|
+
|
|
308
|
+
ws = ProjectWorkspace("./my_app/src", allowed_write_extensions={".py", ".json"})
|
|
309
|
+
|
|
310
|
+
@agent.tool
|
|
311
|
+
def read_file(path: str) -> dict:
|
|
312
|
+
return ws.read_file(path)
|
|
313
|
+
|
|
314
|
+
@agent.tool(permissions=["filesystem.write"])
|
|
315
|
+
def write_file(path: str, content: str, reason: str = "") -> dict:
|
|
316
|
+
return ws.write_file(path, content, reason) # history kept per change
|
|
317
|
+
|
|
318
|
+
@agent.tool(permissions=["filesystem.write"])
|
|
319
|
+
def rollback() -> dict:
|
|
320
|
+
return ws.rollback_last_change() # tests failed? undo.
|
|
321
|
+
|
|
322
|
+
agent.run("Rename the config loader and update its imports.")
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
If the model tries `write_file("/etc/passwd", …)` or `../../secrets.env`, the workspace
|
|
326
|
+
raises — the model sees `{"error": "Path escapes workspace"}` and course-corrects. Every
|
|
327
|
+
write is journaled (`ws.list_changes()`) and individually revertible. **The boundary is
|
|
328
|
+
code you can unit-test, not a system-prompt plea.**
|
|
329
|
+
|
|
330
|
+
### A chat that never forgets (in a bounded context)
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
from autoagent import Agent, SummarizingMemory
|
|
334
|
+
|
|
335
|
+
agent = Agent(
|
|
336
|
+
provider,
|
|
337
|
+
memory=SummarizingMemory(cheap_provider, max_messages=40, keep_recent=12),
|
|
338
|
+
)
|
|
339
|
+
agent.register_recall_tool() # the model gets a `recall(query)` tool
|
|
340
|
+
|
|
341
|
+
# 500 messages later: old turns live in an *incrementally updated* LLM summary
|
|
342
|
+
# (one cheap call per compaction — never a full re-synthesis), the last 12 stay
|
|
343
|
+
# verbatim, and when the user asks "what was that flag I mentioned yesterday?"
|
|
344
|
+
# the model calls recall("flag") over the folded history by itself.
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
If the summarizer call ever fails, compaction is skipped for that turn — context grows
|
|
348
|
+
temporarily instead of being silently truncated. Failure modes are boring on purpose.
|
|
349
|
+
|
|
350
|
+
### Route images to a vision model, keep text on the cheap one
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
from autoagent.providers.routing import RoutingProvider
|
|
354
|
+
|
|
355
|
+
agent = Agent(RoutingProvider(
|
|
356
|
+
default=create_provider(ModelConfig(provider="deepseek", model="deepseek-chat")),
|
|
357
|
+
vision=create_provider(ModelConfig(provider="gemini", model="gemini-3.5-flash")),
|
|
358
|
+
))
|
|
359
|
+
# Text turns → DeepSeek. A turn carrying an ImageAttachment → Gemini.
|
|
360
|
+
# Past image parts are stripped for the text model (which would crash on them).
|
|
361
|
+
# The Agent never knows: it's just an LLMProvider.
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Custom policies are one lambda away: `router=lambda req: big if is_long(req) else small`.
|
|
365
|
+
|
|
366
|
+
### The agent writes its own tools — without owning your machine
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
from autoagent import Agent, DynamicToolBuilder
|
|
370
|
+
|
|
371
|
+
agent = Agent(manager_provider, max_dynamic_tools_per_run=3)
|
|
372
|
+
agent.enable_dynamic_tools(DynamicToolBuilder(coder_provider, tools_dir="./tools_dyn"))
|
|
373
|
+
|
|
374
|
+
agent.run("Read ./access.log and give me the top 5 most-hit URLs.")
|
|
375
|
+
# The model decides it needs a counter → calls create_python_tool(...)
|
|
376
|
+
# → a second LLM writes the code → AST denylist screens it (no eval/exec,
|
|
377
|
+
# imports filtered by declared permissions) → it runs in a sandbox
|
|
378
|
+
# (Docker when available, hardened subprocess otherwise)
|
|
379
|
+
# → promotion to native execution requires a HUMAN adding its hash
|
|
380
|
+
# to the tool manifest. Convenience without the YOLO.
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Host-driven flows the LLM cannot derail
|
|
384
|
+
|
|
385
|
+
For questionnaires, guided forms, onboarding — where skipping a step is a bug, not
|
|
386
|
+
creativity — `Orchestrator` inverts the roles: **your state machine** owns the flow, the
|
|
387
|
+
LLM only interprets answers and rephrases prompts:
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
from autoagent.orchestrator import Orchestrator, Step
|
|
391
|
+
|
|
392
|
+
answers = {}
|
|
393
|
+
def current_steps(): # your code decides what's next — always
|
|
394
|
+
todo = [f for f in ("name", "age", "city") if f not in answers]
|
|
395
|
+
return [Step(id=f, payload={"ask": f}) for f in todo[:2]]
|
|
396
|
+
|
|
397
|
+
def record(step_id, value): # your code validates — return an error string to reject
|
|
398
|
+
answers[step_id] = value
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
orch = Orchestrator(provider, current_steps=current_steps, record=record)
|
|
402
|
+
for ev in orch.turn("I'm Ana and I'm 30"):
|
|
403
|
+
if ev.type == "text": print(ev.text, end="") # streamed, rephrased nicely
|
|
404
|
+
elif ev.type == "recorded": log(ev.step_id, ev.value) # name AND age, one utterance
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
We run this pattern in production against a live phone line: the deterministic flow
|
|
408
|
+
records the answers, a supervisor agent (the "verdicts as tools" pattern above) audits
|
|
409
|
+
every turn in parallel, and `SummarizingMemory`-style per-caller memory makes the bot
|
|
410
|
+
recognize people who call back.
|
|
411
|
+
|
|
412
|
+
## How it compares
|
|
413
|
+
|
|
414
|
+
Honest positioning — these tools optimize for different things, and several of them are
|
|
415
|
+
excellent at what they do:
|
|
416
|
+
|
|
417
|
+
| | **autoagent** | LangChain / LangGraph | CrewAI | AutoGen | OpenAI Agents SDK | smolagents |
|
|
418
|
+
|---|---|---|---|---|---|---|
|
|
419
|
+
| **Core size** | ~6k LOC total, core loop readable in an hour | very large | large | large | medium | small |
|
|
420
|
+
| **Core dependencies** | **0** (stdlib) + `jsonschema` | many | many | many | `openai` sdk | `huggingface_hub` etc. |
|
|
421
|
+
| **Providers** | OpenAI, Anthropic, DeepSeek, Gemini — raw wire, no SDKs | very many (via integrations) | via LiteLLM | via extensions | OpenAI-first | via LiteLLM |
|
|
422
|
+
| **Control flow** | your Python (`run` / `run_messages` / `Orchestrator` for host-driven flows) | graphs/chains DSL | role/crew abstraction | multi-agent conversation | handoffs | code-as-actions |
|
|
423
|
+
| **Security model** | **code-level**: bounded workspace, Docker/AST sandbox, hash-manifest tool approval | per-integration | limited | limited | guardrails (model-level) | sandboxed code exec |
|
|
424
|
+
| **Observability** | typed trace events + **built-in secret redaction**, zero deps | LangSmith (SaaS) | ext. | ext. | OpenAI tracing | basic |
|
|
425
|
+
| **Streaming** | ✅ sync iterators (`run_stream`, text deltas + tool events, SSE in all providers) | ✅ | ✅ | ✅ | ✅ | partial |
|
|
426
|
+
| **Async (asyncio)** | ❌ sync by design — wrap with threads (`asyncio.to_thread`) | ✅ | ✅ | ✅ | ✅ | partial |
|
|
427
|
+
| **Multi-agent** | ✅ minimal primitive: `agent.as_tool()` (supervisor → specialist delegation, shared trace tree); no crew/choreography DSL | ✅ graphs | ✅ core feature | ✅ core feature | ✅ handoffs | ✅ `managed_agents` (hierarchical) |
|
|
428
|
+
| **Memory / RAG** | buffer + **incremental summarizing memory** + lexical recall built-in; vector store = bring your own (2-method protocol) | ✅ full RAG stack | ✅ | partial | partial | partial |
|
|
429
|
+
| **Multi-provider routing** | ✅ per-request (text → cheap model, images → vision model) | via config | via LiteLLM | via config | ❌ OpenAI-first | via LiteLLM |
|
|
430
|
+
| **MCP tools** | ✅ zero-dep stdio client, server schemas validated locally | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
431
|
+
| **Durable runs (checkpoint/resume)** | ✅ JSON `RunState` per step, `resume()` after crash or raised budget | ✅ LangGraph checkpointers | ✅ Flows `@persist` | partial (state save/load) | ✅ `RunState` to/from JSON + HITL interruptions | ❌ |
|
|
432
|
+
| **OpenTelemetry export** | ✅ optional extra, spans mirror the trace tree | via ext. | via ext. | ✅ | via ext. (tracing processors) | ✅ via openinference |
|
|
433
|
+
| **Best when** | you want to **own and audit** the loop; embed agents in an existing app; strict tool bounding | you want the ecosystem | role-played crews fast | conversational multi-agent research | you're all-in on OpenAI | HF ecosystem, code agents |
|
|
434
|
+
|
|
435
|
+
### When you should NOT use autoagent
|
|
436
|
+
|
|
437
|
+
- You need a **native asyncio pipeline** (the loop and streaming are sync iterators — wrapping
|
|
438
|
+
them in threads is easy, but if your whole stack is `async/await`, friction adds up).
|
|
439
|
+
- You want **turn-key vector RAG** or hundreds of prebuilt integrations — LangChain's ecosystem is unmatched.
|
|
440
|
+
- You need rich **multi-agent choreography** (roles, negotiation, group chats) as a first-class
|
|
441
|
+
framework feature — here you get one honest primitive (`as_tool`) and compose the rest.
|
|
442
|
+
- You don't want to write any Python around your agent.
|
|
443
|
+
|
|
444
|
+
If, instead, your agent is a **component inside a real application** — where you need to
|
|
445
|
+
know exactly what it can touch, log exactly what it did, and debug it by reading code —
|
|
446
|
+
that's the niche this library is built for.
|
|
447
|
+
|
|
448
|
+
## Design notes
|
|
449
|
+
|
|
450
|
+
- **One loop to understand**: send history + tool specs → LLM answers text (done) or tool
|
|
451
|
+
calls → execute locally → append results → repeat, hard-capped by `max_steps`.
|
|
452
|
+
- **Errors are data**: a tool that raises returns `{"error": "…"}` to the model, which
|
|
453
|
+
gets a chance to recover. A broken `Memory` or hook is logged and bypassed — never fatal.
|
|
454
|
+
- **Every preview is redacted**: trace payloads and recall snippets pass through the same
|
|
455
|
+
secret-scrubbing filter (`Bearer …`, `api_key`, `?key=` patterns).
|
|
456
|
+
- **Providers are boring on purpose**: request in, response out. `reasoning_content`
|
|
457
|
+
round-tripping (DeepSeek thinking / o-series) and `max_completion_tokens` quirks are
|
|
458
|
+
handled inside the adapter so your code stays clean.
|
|
459
|
+
|
|
460
|
+
## Project status
|
|
461
|
+
|
|
462
|
+
Used in production internally (survey/phone-bot supervision at Alyce). API surface is
|
|
463
|
+
small and stable; version-tagged features are documented in [the developer guide](autoagent-dev-doc.md).
|
|
464
|
+
Contributions welcome — especially provider adapters, `Memory` backends, and sandbox hardening.
|
|
465
|
+
|
|
466
|
+
## License
|
|
467
|
+
|
|
468
|
+
MIT (see `LICENSE`).
|