Feature·Tools & Agents·LLM Primitives
The loop is the language.
Tools are lambdas with schemas. Agents are system prompts with a turn limit. The tool-dispatch loop, retries, cost caps, and provider fallback live in the runtime — your code stays the size of its idea.
Eight chat providers · retries with backoff · scoped budgets · single binary
The agent loop, visualized
What the runtime handles for you.
Every agent script grows the same loop. In Sema it's not code you write — it's what agent/run does when you call it.
llm/with-budget caps spend for the scope; calls that would exceed it failllm/with-fallback tries the next provider if one failsdeftool
A tool is a lambda with a schema.
Define a function the LLM can call. The name, description, and parameter schema are sent to the model automatically. No JSON schema objects to build by hand, no dispatch table to maintain.
- Typed parameters. Declare types inline with
:string,:number,:boolean— the runtime builds the JSON schema for you. - Self-documenting. The description string is what the model sees. Write it like a doc comment.
- Inspection.
tool/name,tool/description,tool/parameters,tool?— tools are first-class values.
(deftool read-file "Read a file's contents" {:path {:type :string :description "File path"}} (lambda (path) (file/read path))) (deftool run-command "Run a shell command" {:command {:type :string}} (lambda (command) (:stdout (shell "sh" "-c" command))))
defagent
An agent is a config, not a class.
System prompt, tools, model, and a turn limit (optional, defaults to 10). agent/run handles the loop — calling tools, feeding results back, stopping at the limit or when the model has a final answer.
- One call to run.
(agent/run coder "Find TODOs")returns the final answer string. - Observe tool calls. Pass
:on-tool-callto watch each tool start and end — for logging, UIs, or debugging. - Full history. Pass an options map and get
:response+:messages— the complete conversation for chaining or inspection.
(defagent coder {:system "You are a coding assistant." :tools [read-file run-command] :model "claude-sonnet-4-6" :max-turns 10}) (llm/with-budget {:max-cost-usd 0.50} (lambda () (agent/run coder "Find TODOs in src/"))) ;; => "Found 3 TODOs: src/main.rs:42, ..." ;; cost: $0.03 · 4 tool calls · 2 turns
The runtime, in one screen
Everything you'd otherwise hand-roll.
These are forms — scoped expressions that wrap your code. They can't be forgotten on a late-night code path.
(llm/with-budget {:max-cost-usd 1.00} f)hard spend cap, scoped to a block(llm/with-cache {:ttl 3600} f)response cache — dev loops stop costing money(llm/with-fallback [:anthropic :openai] f)provider failover, in order(llm/with-rate-limit 5 f)token-bucket rate limiting, requests/sec(llm/extract {:amount {:type :number}} text)typed maps back, not strings to re-parse(llm/with-cassette "tape.jsonl" f)record & replay — deterministic testsEight chat providers plus embedding providers, configured from environment variables — set the key and go. Browse the LLM reference →
Conversations as data
Fork it. Diff it. Replay it.
A conversation is an immutable value. Every operation — say, set-system, filter — returns a new conversation. The original is never modified. Branch from any point and explore two directions simultaneously.
- Immutable history.
conversation/sayreturns a new value — the old one is untouched. - Branch with fork.
conversation/forkcreates an independent copy at any point. Explore alternatives without losing the thread. - Inspect everything.
conversation/messages,conversation/last-reply,conversation/token-count,conversation/cost— all first-class.
(define conv (conversation/new {:model "claude-sonnet-4-6"})) (define conv (conversation/say conv "Remember: the secret is 7")) ;; Fork and explore two paths (define path-a (conversation/say (conversation/fork conv) "What about Python?")) (define path-b (conversation/say (conversation/fork conv) "What about Rust?")) ;; conv, path-a, path-b — all independent
Error recovery
Tools that throw don't abort.
A tool that raises an error, isn't found, or gets bad arguments doesn't crash the agent. The error is fed back to the model as the tool result — so it can correct itself and continue. The loop is bounded by :max-turns and aborts after 5 consecutive tool errors.
- Self-correcting loop. The model sees the error message and adjusts its next call — wrong file path, bad command, missing argument.
- Bounded by design.
:max-turnscaps the total loop; 5 consecutive tool errors abort. No infinite loops. - Observe everything.
:on-tool-callfires on each tool start and end, with duration and result preview.
The argument
The same agent, twice.
A coding agent with a tool loop, retries, and a spend limit. Once with an SDK, once in Sema.
import anthropic, time client = anthropic.Anthropic() TOOLS = [{ "name": "read_file", "description": "Read a file", "input_schema": { "type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"], }, }, { "name": "run_command", "input_schema": { # ...same again }, }] def call_with_retry(messages, attempt=0): try: return client.messages.create( model=MODEL, max_tokens=4096, tools=TOOLS, messages=messages) except anthropic.RateLimitError: if attempt > 5: raise time.sleep(2 ** attempt) return call_with_retry(messages, attempt + 1) def dispatch(name, args): if name == "read_file": return open(args["path"]).read() if name == "run_command": # subprocess, capture stdout+stderr... messages = [{"role": "user", "content": task}] for turn in range(10): resp = call_with_retry(messages) track_cost(resp.usage) # you wrote this too if resp.stop_reason != "tool_use": break results = [] for block in resp.content: if block.type == "tool_use": results.append({ "type": "tool_result", "tool_use_id": block.id, "content": dispatch(block.name, block.input), }) messages.append(...)
(deftool read-file "Read a file's contents" {:path {:type :string}} (lambda (path) (file/read path))) (deftool run-command "Run a shell command" {:command {:type :string}} (lambda (command) (:stdout (shell "sh" "-c" command)))) (defagent coder {:system "You are a coding assistant." :tools [read-file run-command] :max-turns 10}) (llm/with-budget {:max-cost-usd 0.50} (lambda () (agent/run coder "Find TODOs in src/")))
Build an agent in ten lines.
Install Sema, define a tool, run an agent. The loop is already there.