agentdescent
AGENTS.md203 starsChanged 6 days agoMITView on GitHub
Plugs into evolve via agent=LLMAgent(<completion>) (or run=/propose=). This page is the completion layer that LLMAgent wraps. agentdescent.agents is the general "talk to a model/agent" layer. It is deliberately separate from agentdescent.evolution — how you reach a model has nothing to do with skill evolution, and any application built on the framework can use it. The whole contract is one type: Anything that maps a prompt to text is a completion — an LLM call, a tool-using agent loop, a canned…
- Reads credentials
- Installs packages
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,774 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
The file
We split this agents.md into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 77
Put the export lines in your shell profile to keep them across sessions.
- Very specific: line 280
Document-shaped tasks (answer(question, document, skills)) and the OpenHands container backend have their own layer and their own page: …
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Connecting agents & LLMs
Plugs into
evolveviaagent=LLMAgent(<completion>)(orrun=/propose=). This page is the completion layer thatLLMAgentwraps.
agentdescent.agents is the general "talk to a model/agent" layer. It is
deliberately separate from agentdescent.evolution — how you reach a model has
nothing to do with skill evolution, and any application built on the framework
can use it.
The whole contract is one type:
Completion = Callable[[str], str] # prompt -> textAnything that maps a prompt to text is a completion — an LLM call, a tool-using agent loop, a canned stub. Adapters build completions; higher layers turn a completion into whatever task interface they need.
Module: agentdescent.agents
· API: Completion, claude, openai_compatible, cli_agent, …
· See also: backends for document tasks, and
runners for giving an agent the evolving directory.
Configuring your provider and key
Credentials are read from the environment at call time — they never pass through code, arguments, or a config file the repo owns. Two variables decide everything:
| variable | used by | value |
OPENAI_BASE_URL | openai_compatible | the endpoint's root, e.g. https://api.deepseek.com |
OPENAI_API_KEY | openai_compatible | your key for that endpoint |
ANTHROPIC_API_KEY | claude | your Anthropic key (or run ant auth login) |
DeepSeek
export OPENAI_BASE_URL=https://api.deepseek.com
export OPENAI_API_KEY=sk-...
python -m examples.adas.adas_meta_agent_search --provider openai --model deepseek-v4-flashGLM / Zhipu
export OPENAI_BASE_URL=https://open.bigmodel.cn/api/paas/v4
export OPENAI_API_KEY=...
python -m examples.adas.adas_meta_agent_search --provider openai --model glm-4.6OpenAI — OPENAI_BASE_URL is the default here and may be omitted
export OPENAI_BASE_URL=https://api.openai.com/v1
export OPENAI_API_KEY=sk-...
python -m examples.adas.adas_meta_agent_search --provider openai --model gpt-4.1-miniA local server (vLLM, Ollama, LM Studio) — the key is unused but must be set
export OPENAI_BASE_URL=http://localhost:8000/v1
export OPENAI_API_KEY=not-used
python -m examples.adas.adas_meta_agent_search --provider openai --model my-local-modelClaude — a different variable, and ant auth login works instead
export ANTHROPIC_API_KEY=sk-ant-...
python -m examples.adas.adas_meta_agent_search --provider claude --model claude-haiku-4-5Put the export lines in your shell profile to keep them across sessions. Every
faithful algorithm port also takes a zero-network --dry-run, which prints the
configuration without reading these credentials — the cheapest way to inspect a
run before paying for it:
python -m examples.adas.adas_meta_agent_search --dry-run!!! tip "Check the endpoint before a long run"
--provider openai talks to whatever OPENAI_BASE_URL points at, so a typo
surfaces as an HTTP error rather than a wrong answer. To see what a key can
reach:
```bash
curl -s "$OPENAI_BASE_URL/models" -H "Authorization: Bearer $OPENAI_API_KEY"
```
Common replies: `401` — the key is wrong for this base URL; `402` /
`Insufficient Balance` — the account is out of credit; `404` on
`/chat/completions` — the base URL is missing or has an extra path segment
(most gateways want the version prefix, e.g. `/v1`).Adapters
from agentdescent import claude, from_callable, echo, with_retries
# Claude (needs: pip install anthropic + credentials / `ant auth login`)
model = claude(model="claude-opus-4-8") # or claude-haiku-4-5 for cheap runs
text = model("What is 2+2?")
# Any callable you already have
model = from_callable(lambda prompt: my_llm.generate(prompt))
# Deterministic, no-network stub for tests / dry runs
stub = echo(str.upper) # returns transform(prompt), or the prompt itself
# Wrap any completion with exponential-backoff retries
robust = with_retries(claude(model="claude-haiku-4-5"), attempts=3)claude() accepts a client= to reuse an existing anthropic.Anthropic
instance, and forwards extra kwargs to messages.create.
!!! warning "content: null is not the same as """
On OpenAI-compatible endpoints a reasoning model that spends its whole budget
on reasoning_content answers with JSON null, not an empty string.
openai_compatible normalises that to "", because returning None breaks
the one contract in the package (Completion is prompt -> str) and used to
surface as 'NoneType' object has no attribute 'strip' from inside
LLMAgent — which the engine then retried as a backend transient,
diagnosing a systematic model/parameter mismatch as a flaky endpoint. With the
empty string the warning above fires instead and names the real cause.
HTTP errors carry the provider's own message too (`rate limit: retry in 12s`,
`context length exceeded`), rather than collapsing to `HTTP Error 429`. Extra
keyword arguments — `temperature=0` and any provider-specific field — go
straight into the request body, matching `claude()`.!!! tip "Give a reasoning model room"
A reasoning model spends its budget on internal reasoning before emitting
visible text, so too small a max_tokens yields an empty completion, not a
short answer. Measured on deepseek-v4-flash with the standard reflection
prompt: at max_tokens=1024, 4 of 8 replies came back empty; at 3000, none did.
Both adapters default to **4096**. You are billed for tokens *generated*, not
for the cap, so a generous limit costs nothing — and an empty reflection emits
a `RuntimeWarning` naming this as the likely cause.
**4096 is not always enough.** That figure was measured on a short reflection
prompt; the budget a model needs scales with how much it has to think about.
On ADAS's meta-agent prompt — a whole archive of designs and their fitness —
`deepseek-v4-flash` at 4096 returned empty content on **every** call, burning
the full budget on reasoning each time; at 16384 it answered every time, with
a reply of only ~250 tokens. If a caller's prompts are long or its task is
hard, raise the cap and check: an empty completion scores as a wrong answer,
it does not raise.What did the run cost? — Usage
A Completion is prompt -> text, so the token counts the providers do return
would be thrown away at the adapter boundary. Pass a Usage and they are kept:
from agentdescent import Usage, openai_compatible, metered
usage = Usage()
model = openai_compatible(model="deepseek-v4-flash", usage=usage) # or claude(usage=usage)
evolve(tasks, reward, agent=LLMAgent(model), rounds=10)
print(usage.summary()) # 412 calls, 1,204,881 prompt + 96,004 completion tokens, 903.2s in the model
print(usage.estimated_cost(per_1m_prompt=0.28, per_1m_completion=0.42))calls, failures | attempts made, and how many raised (retries count individually — they cost money) |
prompt_tokens, completion_tokens, total_tokens | real counts from the API response |
seconds | wall-clock spent inside the model |
estimated_cost(...) | you supply the per-million prices; the library ships no price table it would have to keep current |
Usage is safe to share across worker threads. For a backend that is not one of
the built-in providers — a tool-using agent loop, say — wrap it in
metered(completion, usage), which counts calls, failures and time (tokens are
not observable through a plain Completion).
Bringing another provider
There is no provider lock-in — any prompt -> text function is a completion:
def openai_completion(prompt: str) -> str:
resp = openai_client.chat.completions.create(
model="gpt-...", messages=[{"role": "user", "content": prompt}])
return resp.choices[0].message.content
from agentdescent import from_callable
model = from_callable(openai_completion)Using it in skill evolution
agentdescent.evolution consumes a completion through LLMAgent:
from agentdescent import claude
from agentdescent import LLMAgent
agent = LLMAgent(claude(model="claude-haiku-4-5"))claude_agent(model=...) in the evolution engine is just a convenience for
LLMAgent(claude(model)) — the provider code lives here, in agentdescent.agents.
Tool-using agents — the same contract
An agent that acts (Claude Code, Codex, OpenHands, aider, …) differs from an API
model in that it runs commands and edits files before answering. Its call
contract is identical: text in, text out. So they are all Completions, and
everything that takes a completion takes all of them with no special-casing:
from agentdescent import claude, openai_compatible, claude_code, codex, cli_agent
from agentdescent.backends import openhands
claude(model="claude-haiku-4-5") # API model
openai_compatible(model="deepseek-v4-flash") # any OpenAI-compatible endpoint
claude_code() # Claude Code, print mode
codex() # Codex CLI
openhands(model="openai/deepseek-v4-flash") # OpenHands SDK
cli_agent(["my-agent", "--json"]) # anything with a CLI| Factory | What it runs | Needs |
claude(...) / openai_compatible(...) | one model call | anthropic / nothing |
cli_agent(command, ...) | any command-line agent — prompt on argv or stdin, stdout is the answer | that CLI on PATH |
claude_code() / codex() | thin presets over cli_agent | claude / codex CLI |
openhands(...) | a real OpenHands agent (terminal + file_editor) | openhands-ai, Python ≥ 3.12 |
Failures raise AgentError carrying the agent's own stderr, not a bare exit
code, and every CLI agent takes a timeout — an agent that hangs would otherwise
stall the round it belongs to (see round_timeout).
Giving an agent somewhere to work — WorkspaceAgent
Completion stays prompt -> text for everything. An acting agent often needs a
place to act, and a caller that stages files needs to say where, so tool-using
agents add exactly one optional capability:
agent = claude_code()
agent.in_workspace("/tmp/task-17")("summarise report.txt") # runs with that cwdFeature-detect it rather than assuming — plain API models deliberately do not implement it:
from agentdescent import WorkspaceAgent
if isinstance(agent, WorkspaceAgent):
answer = agent.in_workspace(staged_dir)(prompt) # it can grep real files
else:
answer = agent(prompt_with_material_inlined) # fall back to the prompt!!! warning "OpenCode and custom providers"
A CLI worker runs with the host's config directory redirected into the
workspace (isolate=True, the default). For opencode(extra_args=["-m",
"provider/model"]) that means -m reaches only providers built into
OpenCode: a custom provider (an OpenAI-compatible endpoint such as Ark, a
local vLLM, a gateway) is defined in your opencode.json, which the isolated
worker cannot read, and every call fails with an opaque UnknownError. Pass
isolate=False, or keep isolation with
env={"OPENCODE_CONFIG": "/abs/providers.json"} -- a file holding only the
provider block, keys as {env:VAR}. See
Which model a run uses.
Domain adapters and the OpenHands backend — see the backends page
Document-shaped tasks (answer(question, document, skills)) and the OpenHands
container backend have their own layer and their own page:
backends is canonical for document_agent, openhands, and
tool_loop_backend, including the OfficeQA inline-fallback warning. Nothing
about them changes the completion contract on this page.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

