Getting started: Python¶
The Python package is a parity port of the TypeScript reference implementation. Every pinned semantic (budgets, clear-then-save memory, structured auto-strategy, HITL resume) matches the fixed TS behavior; see cross-language notes per topic.
Install¶
From PyPI:
Or from the repository (development install with test extras):
Requirements: Python >= 3.11. Runtime dependencies are pydantic and httpx only. Import as framework_agents.
Environment¶
load_config() reads the same keys as the TS loadConfig(), with the same defaults and the same walk-up .env loading (cwd, then three parents; FRAMEWORK_ENV_PATH overrides). TogetherAI wins when TOGETHER_API_KEY is set.
| Key | Purpose |
|---|---|
TOGETHER_API_KEY / TOGETHER_BASE_URL / TOGETHER_MODEL |
TogetherAI (OpenAI-compatible, default chat) |
OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL |
OpenAI fallback |
ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL / ANTHROPIC_MODEL |
Anthropic native API |
OLLAMA_BASE_URL / OLLAMA_MODEL |
local Ollama (no key) |
init_chat_model(spec) resolves provider:model strings: together: (default for bare ids), openai:, anthropic:, ollama:, custom: (needs base_url + api_key).
First agent + first tool¶
define_tool takes name, description, a pydantic schema (the validated argument shape — its JSON Schema reaches the provider wire; without it tools are advertised zero-arg), and execute:
import asyncio
from pydantic import BaseModel
from framework_agents import Agent, AgentOptions, define_tool, init_chat_model
class Weather(BaseModel):
city: str
async def get_weather(args: Weather, ctx) -> dict:
return {"city": args.city, "tempC": 21}
agent = Agent(AgentOptions(
name="assistant",
model=init_chat_model("together:Qwen/Qwen3.5-9B", max_tokens=2500, stream=True),
tools=[define_tool(
name="get_weather",
description="Current weather for a city",
schema=Weather,
execute=get_weather,
)],
system_prompt="You are a concise weather assistant.",
max_steps=6, # default 6
))
result = asyncio.run(agent.run("Weather in Tokyo?"))
print(result.text, result.tool_calls, result.usage)
The second execute parameter is an optional ToolContext (agent_name, state, invocation_state) — see agents & tools.
Streaming¶
agent.stream() returns an async iterator of the same typed events as TS (step | token | thinking | tool_call | tool_result | done). Python generators cannot return a final value, so the finished AgentResult is reachable on the stream wrapper via .result:
stream = agent.stream("Weather in Tokyo?")
async for event in stream:
if event["type"] == "token":
print(event["delta"], end="")
elif event["type"] == "tool_call":
print("calling", event["name"])
elif event["type"] == "done":
print("\nfinal:", event["text"])
result = stream.result # captured per stream at the done event
collect_stream(stream) buckets both events and result. Details in streaming.
Budgets¶
Caps are enforced after every model call on cumulative usage:
from framework_agents import AgentOptions, TokenBudgets
agent = Agent(AgentOptions(
model=init_chat_model("together:Qwen/Qwen3.5-9B"),
budgets=TokenBudgets(
max_tokens=8000, # reasoning tokens are a subset of output, never added
max_cost_usd=0.05, # needs a known price or budgets.prices
deadline_ms=60_000,
),
max_tool_calls=10, # separate option; exceeded → BUDGET_EXCEEDED
))
Exceeding any cap raises AgentError("BUDGET_EXCEEDED", retryable=False). See budgets & pricing.
Run the offline test suite¶
The parity suite is fully offline (mock/scripted models):
Live tests are gated behind a marker and hit the real Together API:
Live examples live in examples/ — see live examples.
Next steps¶
Notebook vs script vs async code — which form to use¶
| Where you are | Use |
|---|---|
| Jupyter / notebook cell | await agent.run("hi") directly (top-level await works in notebooks), or agent.run_sync("hi") for blocking one-liners |
| Plain script (no event loop) | agent.run_sync("hi") or asyncio.run(agent.run("hi")) — asyncio.run THROWS inside notebooks |
Inside async def code |
await agent.run("hi") — never call run_sync there |
Helpers:
result = agent.run_sync("hi") # -> AgentResult
for event in agent.stream_sync("hi"): # -> StreamEvent dicts
print(event["type"])
run_sync / stream_sync detect the running loop: scripts get asyncio.run
on the spot; notebooks execute on a private loop in a worker thread, so
Jupyter's own loop is never nested (verified both work from inside a running
loop — which is what a notebook cell is).
"Do I have to await everything?"¶
Only what touches the network or disk - which is the same rule the TypeScript
package follows (await agent.run(...) works there too; Python just needs a
one-line event loop at your entry point, the equivalent of JS top-level await).
Always await / async for |
Always plain |
|---|---|
run / stream (async for) / resume / pause / branch / edit_message |
constructors (Agent, options dataclasses, providers) |
Memory: load / save / clear ; Storage: all ops ; Checkpointer: all ops |
define_tool + every middleware factory |
provider complete ; streamed tools inside the loop call |
init_chat_model / load_config / price_for / usage_cost |
orchestration / workflows / swarm / evals / conformance (run_*) |
message factories + serialize_messages / text_of |
A2A client send / get_card / stream ; server start / stop |
state.get / set / to_json |
semantic store ops, review_ontology / propose_shapes / adopt_shapes / migrate_t_box |
quads_to_json_ld / json_ld_to_quads / parse_skill_file / match_skills |
Notes:
- Tool
executeis awaited by the loop - you writeasync def weather(...), you never await it yourself. Same forAgentStream:async forover it, the final result lands instream.result(oragent.run's return). - There is deliberately no sync-mode clone of the API: a sync/async duality
would maintain two inconsistent surfaces. If you want a blocking one-liner at
the top level, it is just
asyncio.run(agent.run("hi")).