diff --git a/README.md b/README.md index 77bcf8db..0233febe 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ async def contact_mothership(query: str) -> str: async def main() -> None: - model = ai.get_model("gateway:anthropic/claude-sonnet-4") + model = ai.get_model("anthropic/claude-sonnet-4") agent = ai.agent(tools=[contact_mothership]) messages = [ @@ -105,7 +105,7 @@ Built-in tools execute on the provider side and arrive as part of the stream: async with ai.stream( model, [ai.user_message("Latest Formula 1 results?")], - tools=[ai.anthropic.tools.web_search(max_uses=3)], + tools=[ai.providers.anthropic.tools.web_search(max_uses=3)], ) as s: async for event in s: if isinstance(event, ai.events.TextDelta): diff --git a/docs/ai-python/content/docs/basics/agents.mdx b/docs/ai-python/content/docs/basics/agents.mdx index 904851d7..594ff64e 100644 --- a/docs/ai-python/content/docs/basics/agents.mdx +++ b/docs/ai-python/content/docs/basics/agents.mdx @@ -6,8 +6,7 @@ summary: Create agents, run the default loop, inspect results, and understand mu --- Use an agent when the model needs to call tools and continue with the tool -results. The default agent loop is built from the same primitives you can use -directly: `ai.stream`, `ToolRunner`, messages, and events. +results. ## Create an agent @@ -15,21 +14,8 @@ An agent wraps `ai.stream` in a loop. It streams model output, executes requeste tools, appends tool results to history, and repeats until the model returns a final assistant message. -```python -agent = ai.agent(tools=[contact_mothership]) - -async with agent.run(model, messages) as stream: - async for event in stream: - if isinstance(event, ai.events.TextDelta): - print(event.chunk, end="", flush=True) -``` - -Use `ai.agent` for the default loop. Subclass `ai.Agent` and override -`async def loop()` when you need to change control flow. - -## Run the default loop - -Create an agent with tools, then call `agent.run`: +Subclass `ai.Agent` and override `async def loop()` when you need to change +control flow. ```python title="agent_loop.py" import asyncio @@ -64,17 +50,19 @@ if __name__ == "__main__": asyncio.run(main()) ``` -## Inspect the run result - The stream yields model events and agent events. After the run finishes, `stream.messages` contains the updated history, and `stream.output` contains the final assistant output. +Unline `ai.stream`, every `agent.run` can produce multiple messages alternating +between `"user"`/`"tool"` and `"assistant"`, representing turns in the LLM request +and response cycle. + ## Understand multi-turn behavior -Each loop turn streams one assistant message. If the message contains tool -calls, the agent executes them, appends one tool-result message, and starts the -next model turn. +Each loop turn has one `ai.stream` that produces one assistant message. If the +message contains tool calls, the agent executes them, appends one tool-result +message, and starts the next model turn. ```python async with agent.run(model, messages) as stream: @@ -109,7 +97,7 @@ async with agent.run( model, [ai.user_message("Return a JSON mothership forecast.")], output_type=Forecast, - params={"temperature": 0}, + params=ai.InferenceRequestParams().with_temperature(0), ) as stream: async for event in stream: if isinstance(event, ai.events.TextDelta): @@ -118,22 +106,3 @@ async with agent.run( forecast = stream.output print(forecast.eta) ``` - -## Choose agents or direct streaming - -Use `ai.stream` when you want one model response and you will handle any tool -calls yourself. Use an agent when the SDK should execute Python tools, append -tool results, and keep looping until the assistant returns a final answer. - -```python -# Direct stream: inspect tool calls yourself. -async with ai.stream(model, messages, tools=[get_weather.tool]) as stream: - async for event in stream: - ... - -# Agent: execute registered tools. -agent = ai.agent(tools=[get_weather]) -async with agent.run(model, messages) as stream: - async for event in stream: - ... -``` diff --git a/docs/ai-python/content/docs/basics/ai-sdk-ui.mdx b/docs/ai-python/content/docs/basics/ai-sdk-ui.mdx index 25b195f2..49393763 100644 --- a/docs/ai-python/content/docs/basics/ai-sdk-ui.mdx +++ b/docs/ai-python/content/docs/basics/ai-sdk-ui.mdx @@ -5,42 +5,18 @@ type: guide summary: Parse UI messages, apply approvals, stream SSE responses, set headers, and rebuild UI history. --- -The AI SDK UI adapter converts between AI SDK UI messages and the Python -runtime message/event types. +This adapter provides integration with the UI part of the sister AI SDK +in TypeScript. -## Parse UI messages +AI SDK UI provides `useChat`, which handles streaming complexity on the +client side, such as collecting message chunks and optimistically updating +the UI when the user submits a new message. -Accept `UIMessage` values from an AI SDK UI client, then convert them to -runtime messages: +The AI SDK UI's data model is significantly different from the AI SDK for +Python's, which is why you need to convert events and messages back and forth +using the adapter. -```python -class ChatRequest(pydantic.BaseModel): - messages: list[ai.agents.ui.ai_sdk.UIMessage] - - -@app.post("/chat") -async def chat(request: ChatRequest): - messages, approvals = ai.agents.ui.ai_sdk.to_messages(request.messages) -``` - -`to_messages` also extracts approval responses from tool parts. - -## Apply approval responses - -Register extracted approvals before resuming the agent: - -```python -messages, approvals = ai.agents.ui.ai_sdk.to_messages(request.messages) -ai.agents.ui.ai_sdk.apply_approvals(approvals) - -async with chat_agent.run(model, messages) as stream: - ... -``` - -The hook registry stores each approval until the matching tool-approval hook -runs. - -## Stream SSE responses +## Outbound streaming and message history Use `to_sse` to convert agent events to AI SDK UI stream chunks: @@ -61,18 +37,6 @@ async def chat(request: ChatRequest) -> fastapi.responses.StreamingResponse: ) ``` -## Set response headers - -Return the adapter headers on every streamed response: - -```python -headers = ai.agents.ui.ai_sdk.UI_MESSAGE_STREAM_HEADERS -``` - -These headers identify the response as an AI SDK UI message stream. - -## Rebuild UI history - Convert stored runtime messages back to AI SDK UI messages for history endpoints: @@ -86,8 +50,6 @@ async def get_chat(session_id: str): The adapter groups assistant, tool, and internal hook messages into one assistant UI message. -## Support streaming tool output - Generator tools and subagents emit `PartialToolCallResult` events. The UI adapter folds those partial values into the corresponding tool output: @@ -101,4 +63,37 @@ async def draft_mothership_reply(topic: str) -> ai.StreamingTextTool: ``` For subagents, `ai.SubAgentTool` streams nested events and stores the nested -assistant message as the tool output. +transcript as a `MessageBundle`. The model sees the nested agent's final +assistant text. + +## Inbound message updates + +Accept `UIMessage` values from an AI SDK UI client, then convert them to +runtime messages: + +```python +class ChatRequest(pydantic.BaseModel): + messages: list[ai.agents.ui.ai_sdk.UIMessage] + + +@app.post("/chat") +async def chat(request: ChatRequest): + messages, approvals = ai.agents.ui.ai_sdk.to_messages(request.messages) +``` + +`to_messages` also extracts approval responses from tool parts. + +## Tool approvals + +Register extracted approvals before resuming the agent: + +```python +messages, approvals = ai.agents.ui.ai_sdk.to_messages(request.messages) +ai.agents.ui.ai_sdk.apply_approvals(approvals) + +async with chat_agent.run(model, messages) as stream: + ... +``` + +The hook registry stores each approval until the matching tool-approval hook +runs. diff --git a/docs/ai-python/content/docs/basics/custom-loops.mdx b/docs/ai-python/content/docs/basics/custom-loops.mdx index 615f448f..de5d2af9 100644 --- a/docs/ai-python/content/docs/basics/custom-loops.mdx +++ b/docs/ai-python/content/docs/basics/custom-loops.mdx @@ -5,104 +5,51 @@ type: guide summary: Override the agent loop to customize scheduling, routing, history updates, logging, and tool execution. --- -Custom loops use the same primitives as the default loop. Override `Agent.loop` -when you need to change scheduling, logging, routing, or persistence. +Override `loop` when you need custom tool dispatch, logging, durability, or +branching. While the SDK defines the loop as a Python async generator, it still +uses a few framework components. Reuse those to retain framework-controlled +behavior. -## When to customize the loop +## Explore the standard loop shape -Override `loop` when you need custom scheduling, logging, durability, or -branching. Keep the same pattern when you still want SDK-managed history and -tool resolution. - -## Use the standard loop shape - -The default loop keeps running while the last message needs more work. On each +The default loop keeps running while there is more work to do. On each turn, it streams the model response and schedules tool calls: ```python class CustomAgent(ai.Agent): - async def loop(self, context: ai.Context): + # ai.Context keeps track of message history, run inputs, and other per-run data + async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.AgentEvent]: while context.keep_running(): + # call ai.stream with whatever model, tools, and messages the user has + # passed to agent.run async with ( ai.stream(context=context) as stream, + # ai.ToolRunner is responsible for concurrent tool scheduling and + # streaming, as well as graceful handling of hook interruptions ai.ToolRunner() as tool_runner, + ): + # ai.util.merge interleaves two streams together, so the agent + # can fire off tools as they arrive in the stream rather than + # waiting for the model to stop streaming; and also start streaming + # the streaming tools. async for event in ai.util.merge(stream, tool_runner.events()): yield event if isinstance(event, ai.events.ToolEnd): + # context.resolve looks up the Python function for the + # tool name in the model's tool call tool_call = context.resolve(event.tool_call) + # schedule the tool for concurrent execution + # using the tool runner tool_runner.schedule(tool_call) + # add new messages to message history stored in the context. + # this works with internal replay machinery to enable serverless + # execution. context.add(stream.message) context.add(tool_runner.get_tool_message()) ``` -## Resolve and schedule tool calls - -`ToolEnd` means the model finished emitting a tool call. Resolve it through the -context, then schedule it with `ToolRunner`: - -```python -if isinstance(event, ai.events.ToolEnd): - tool_call = context.resolve(event.tool_call) - tool_runner.schedule(tool_call) -``` - -`context.resolve` validates that the agent has an executable tool registered -for the model's `tool_name`. - -## Add messages to history - -After a stream turn finishes, add the assistant message and any tool-result -message: - -```python -context.add(stream.message) -context.add(tool_runner.get_tool_message()) -``` - -`context.add` skips replayed assistant messages, so resume flows can call it -without duplicating history. - -## Run tools sequentially - -`ToolRunner.schedule` runs active tools concurrently. To run tools one at a -time, collect the calls during the model stream, then execute them in order: - -```python -pending: list[ai.ToolCall] = [] - -async for event in stream: - yield event - if isinstance(event, ai.events.ToolEnd): - pending.append(context.resolve(event.tool_call)) - -for tool_call in pending: - result = await tool_call() - yield result - tool_runner.add_result(result) -``` - -This keeps the same tool-message aggregation path while preserving execution -order. - -## Add logging or routing - -Custom loops can inspect events before yielding or scheduling: - -```python -if isinstance(event, ai.events.ToolEnd): - call = event.tool_call - print(f"tool: {call.tool_name}({call.tool_args})") - tool_runner.schedule(context.resolve(call)) -``` - -You can also route specific tools through custom wrappers: - -```python -if tool_call.name == "contact_mothership": - tool_runner.schedule(GatedToolCall(tool_call)) -else: - tool_runner.schedule(tool_call) -``` +You can modify most of the loop and extend `CustomAgent` to fit your application's needs, +and the SDK will keep working as long as the loop is still an async generator of events. diff --git a/docs/ai-python/content/docs/basics/durable-execution.mdx b/docs/ai-python/content/docs/basics/durable-execution.mdx new file mode 100644 index 00000000..2897a6ff --- /dev/null +++ b/docs/ai-python/content/docs/basics/durable-execution.mdx @@ -0,0 +1,108 @@ +--- +title: Durable Execution +description: Run agent loops inside durable workflow systems. +type: guide +summary: Build custom loops with durable model calls, serialized messages, tool dispatch, and external resume points. +--- + +Use durable execution when a run must survive process restarts, worker moves, or +long waits. + +The SDK currently does not provide a built-in durability solution, so you will +have to create a custom loop (or copy one from the example). This section will +use terminology and the general shape of Vercel Workflows for simplicity; same +approach can be applied to other durable execution frameworks (e.g. Temporal). + +## Why durability is different + +The core idea of durable execution is to make your code deterministic and +replayable by isolating side-effects and non-deterministic work inside *steps* +(or *activities*), i.e. functions that accept JSON inputs and produce JSON +outputs. Every successful step gets recorded in the event log. The rest of the +code turns into a deterministic orchestration *workflow*, that can replay +results of completed steps from the event log as many times as necessary. + +When applied to the agent, this idea turns it into a *workflow*, with `ai.stream` +and tool calls wrapped in *steps*. The SDK exposes all the necessary primitives +and ensures that their inputs and outputs can round-trip through JSON. + +Another important caveat to durable execution is that it normally does not +support async generators. Depending on the framework, additional work may be +required for your streaming setup. + +## Example: Vercel Workflows + +Wrap the model call in a step that returns the final assistant `Message`, +and wrap tools in steps before decorating them with `@ai.tool`. + +```python +@workflow.step +async def llm_step( + model_data: dict[str, object], + messages_data: list[dict[str, object]], + tools_data: list[dict[str, object]], +) -> dict[str, object]: + model = ai.Model.model_validate(model_data) + messages = [ + ai.messages.Message.model_validate(message) + for message in messages_data + ] + tools = [ai.Tool.model_validate(tool) for tool in tools_data] + + async with ai.stream(model, messages, tools=tools) as stream: + async for _event in stream: + pass + + return stream.message.model_dump(mode="json") + + +@ai.tool +@workflow.step +async def ask_mothership(question: str) -> str: + """Ask the mothership for a status update.""" + response = await mothership_client.ask(question) + return response.summary +``` + +Then use a custom loop that calls the model step, schedules tool work, and +stores the resulting messages: + +```python +class DurableAgent(ai.Agent): + async def loop(self, context: ai.Context): + while context.keep_running(): + result = await llm_step( + context.model.model_dump(mode="json"), + [ + message.model_dump(mode="json") + for message in context.messages + ], + [ + tool.model_dump(mode="json") + for tool in context.tools + ], + ) + + assistant_message = ai.messages.Message.model_validate(result) + context.add(assistant_message) + + async with ai.ToolRunner() as runner: + for tool_call in assistant_message.tool_calls: + runner.schedule(context.resolve(tool_call)) + + async for event in runner.events(): + yield event + + context.add(runner.get_tool_message()) +``` + +This loop does not need `ai.util.merge`, because `llm_step` returns a complete +assistant message before tools are scheduled. In a streaming loop, `merge` +interleaves model events and tool results while the model is still producing +output. + +Consider using the serverless hook flow for approvals. When a pending `HookEvent` +appears, call `ai.abort_pending_hook(event.hook)`, persist `stream.messages`, +and return the approval request to the client. On the next workflow turn, call +`ai.resolve_hook(...)` before `agent.run(...)`. The SDK replays the interrupted +assistant turn and continues when the hook reads the pre-registered resolution. diff --git a/docs/ai-python/content/docs/basics/human-in-the-loop.mdx b/docs/ai-python/content/docs/basics/human-in-the-loop.mdx index 85fe158c..8db7af0e 100644 --- a/docs/ai-python/content/docs/basics/human-in-the-loop.mdx +++ b/docs/ai-python/content/docs/basics/human-in-the-loop.mdx @@ -5,8 +5,9 @@ type: guide summary: Use tool approvals, manual hooks, cancellation, denial, and serverless resume flows. --- -Hooks let an agent suspend while your application waits for a decision. Tool -approvals are the built-in hook workflow. +Tool approval in the SDK is a high-level API built on top of hooks. Hooks +suspend the loop until your application resolves them externally with some +payload. ## Require tool approval @@ -22,18 +23,6 @@ async def notify_mothership(message: str) -> str: When the model calls the tool, the agent emits a hook event. Resolve it with `ai.resolve_hook`: -```python -ai.resolve_hook( - "approve_tool_call_id_here", - ai.tools.ToolApproval(granted=True, reason="approved"), -) -``` - -## Resolve approvals in a live app - -Listen for pending hook events and resolve the matching hook from another task, -request handler, or UI callback: - ```python async with agent.run(model, messages) as stream: async for event in stream: @@ -50,68 +39,85 @@ ai.resolve_hook( ) ``` -## Build manual hooks - -Hooks let an agent suspend while your application waits for external input, -such as a human approval: +Return a denial by resolving the hook with `granted=False`: ```python -approval = await ai.hook( - "approve_contact_mothership", - payload=ai.tools.ToolApproval, - metadata={"tool": "contact_mothership"}, +ai.resolve_hook( + "approve_tool_call_id_here", + ai.tools.ToolApproval(granted=False, reason="not allowed"), ) ``` -Resolve the hook from another part of your application: +Cancel a live hook when the waiting workflow should stop: ```python -ai.resolve_hook( - "approve_contact_mothership", - {"granted": True, "reason": "approved"}, -) +await ai.cancel_hook("approve_tool_call_id_here", reason="client disconnected") ``` -Use hooks when a tool or workflow needs a decision that cannot happen inside the -model call. +## Build custom hooks -## Deny or cancel work - -Return a denial by resolving the hook with `granted=False`: +Use `ai.hook` to define your own custom hooks to suspend execution +and deliver external information into the loop: ```python -ai.resolve_hook( - "approve_tool_call_id_here", - ai.tools.ToolApproval(granted=False, reason="not allowed"), +class CustomPayload(pydantic.BaseModel): + foo: int + +approval = await ai.hook( + "some_hook", + payload=CustomPayload, + metadata={"kek": "pek"}, ) ``` -Cancel a live hook when the waiting workflow should stop: +Resolve the hook from another part of your application: ```python -await ai.cancel_hook("approve_tool_call_id_here", reason="client disconnected") +ai.resolve_hook( + "some_hook", + {"foo": 999}, +) ``` ## Resume in serverless flows -For serverless or resumable flows, keep the pending hook part from the emitted -event, call `ai.abort_pending_hook(hook_part)` to end the current run, persist -`stream.messages`, and call `ai.resolve_hook` before replaying the agent. - -## Persist approval state +Serverless setups can't suspend on `await` in the same way as long-running +servers. For that reason, the SDK provides an alternative way for handling +hook resolutions. -Persist `stream.messages` when a run stops on a pending hook. When the user -responds, restore those messages, register the resolution, and run the same -agent again: +Using the serverless flow only requires you to update the `agent.run` callsite +(e.g. your serverless endpoint code). ```python -messages, approvals = ai.agents.ui.ai_sdk.to_messages(ui_messages) -ai.agents.ui.ai_sdk.apply_approvals(approvals) - +# pre-register tool approvals +for approval in approvals: + ai.resolve_hook( + approval.hook_id, + ai.tools.ToolApproval.model_validate(approval.data) + ) + +# start (or replay pre-hook part of) a run async with agent.run(model, messages) as stream: async for event in stream: - ... + if ( + isinstance(event, ai.events.HookEvent) + and event.hook.status == "pending" + ): + # interrupt the loop + ai.abort_pending_hook(event.hook) + ``` -The agent marks interrupted assistant turns for replay, so the resumed run can -dispatch the original tool call without asking the model to emit it again. +When handling the hook event, call `abort_pending_hook` to interrupt the loop +and surface your tool approvals (or custom hook-related work) to the client. + +The `ToolRunner` will handle multiple concurrent aborts by waiting for all +scheduled tasks to complete or get aborted. That way you can run approval-gated +tools on serverless concurrently. + +Once the client has gathered payloads for all hooks, it will re-enter via the +same endpoint, which will then pre-register hook resolutions *before* calling +`agent.run` again. The loop will replay results of LLM calls and tool results +from the first run without redoing non-deterministic and duplicating side-effects. +When it hits the hook for which it has a pre-registered resolution, it will +continue through without suspending. diff --git a/docs/ai-python/content/docs/basics/index.mdx b/docs/ai-python/content/docs/basics/index.mdx index 82a8d4bd..4344cbbc 100644 --- a/docs/ai-python/content/docs/basics/index.mdx +++ b/docs/ai-python/content/docs/basics/index.mdx @@ -34,7 +34,8 @@ Start with the model call, then add the pieces your app needs: Read providers, messages, streaming, tools, and agents first. Then add custom loops, subagents, human-in-the-loop workflows, and UI integration when your app -needs those control points. +needs those control points. Use Reference when you need the exact public API +shape for a symbol. ## Example map diff --git a/docs/ai-python/content/docs/basics/messages-and-events.mdx b/docs/ai-python/content/docs/basics/messages-and-events.mdx index 9eb8db61..eabf2ad6 100644 --- a/docs/ai-python/content/docs/basics/messages-and-events.mdx +++ b/docs/ai-python/content/docs/basics/messages-and-events.mdx @@ -5,13 +5,40 @@ type: guide summary: Construct messages, add files, read outputs, serialize history, handle events, and track usage. --- -Messages are Pydantic models. Events are also Pydantic models, so you can -pattern-match, serialize, and persist them when your app needs that. +Messages and events make up AI SDK for Python's data model. Events are used +to communicate transient streaming state, while messages are used to accumulate, +store, and pass around non-streaming state. + +Both are Pydantic models, so you can pattern-match, serialize, and persist them +when your app needs that. ## Build messages -Messages are the conversation history you send to the model. Use message -builders for the common roles: +Messages store the conversation history you send to the model. Each message +represents a turn in the conversation and is made up of `Part` values: + +```python +class Message: + role: "system" | "user" | "assistant" | "tool" | "internal" + parts: list[Part] + ... + + +Part = ( + TextPart + | ReasoningPart + | ToolCallPart + | ToolResultPart + | BuiltinToolCallPart + | BuiltinToolReturnPart + | FilePart + | HookPart +) + +``` + +Use built-in factory functions to quickly construct frequently used shapes of +messages: ```python messages = [ @@ -20,10 +47,17 @@ messages = [ ] ``` +Messages round-trip through Pydantic: + +```python +encoded = [message.model_dump(mode="json") for message in stream.messages] +restored = [ai.messages.Message.model_validate(item) for item in encoded] +``` + ## Add files and multimodal input -Each message contains parts. Strings become text parts. File parts let you send -images, audio, or documents when the provider supports them. +File parts let you send images, audio, or documents when the provider supports +them. ```python message = ai.user_message( @@ -32,7 +66,7 @@ message = ai.user_message( ) ``` -## Read message output +## Handle message output Assistant messages expose common part collections: @@ -45,30 +79,17 @@ print(message.tool_calls) print(message.files) ``` -Use `get_output` for a final assistant message. With no type, it returns text. -With a Pydantic model, it validates the text as JSON: +When working with structured outputs, use `get_output` to get the final +assistant message wrapped into the provided Pydantic model. ```python -answer = message.get_output() forecast = message.get_output(Forecast) ``` -## Serialize and restore messages - -Messages round-trip through Pydantic JSON: - -```python -encoded = [message.model_dump(mode="json") for message in stream.messages] -restored = [ai.messages.Message.model_validate(item) for item in encoded] -``` - -Persist `stream.messages` after an agent run when you want to continue the -conversation later. - ## Handle stream events -Streams and agents both yield event objects from `ai.events`. Most applications -start by handling `TextDelta`: +Streams and agents both yield event objects from `ai.events`. Applications can +handle each kind of event as they see fit: ```python if isinstance(event, ai.events.TextDelta): @@ -80,6 +101,43 @@ arrive through `ToolStart`, `ToolDelta`, and `ToolEnd` events. Provider-executed tools use the `BuiltinToolStart`, `BuiltinToolDelta`, `BuiltinToolEnd`, and `BuiltinToolResult` events. +```text +StreamStart + + # text output + TextStart + TextDelta ... + TextEnd + + # reasoning output + ReasoningStart + ReasoningDelta ... + ReasoningEnd + + # host-executed tool call + ToolStart + ToolDelta ... + ToolEnd + + # provider-executed tool call + BuiltinToolStart + BuiltinToolDelta ... + BuiltinToolEnd + BuiltinToolResult + + # generated file + FileEvent + +StreamEnd +``` + +```text +# Agent runs can also interleave: +ToolCallResult # emitted after a Python tool finishes +PartialToolCallResult # emitted while a streaming tool or subagent yields +HookEvent # emitted when a hook is pending, resolved, or cancelled +``` + ```python async with ai.stream(model, messages, tools=tools) as stream: async for event in stream: @@ -93,6 +151,9 @@ Agents can also emit tool results, hook events, and partial tool output. You can ignore events you do not need, or route them into your application UI, observability pipeline, or durable workflow. +Every event also carries a reference to a `ai.Message` that has partial content +accumulated from the stream *so far*. + ## Track usage Providers attach usage to events when they report it. The latest value is also @@ -107,3 +168,17 @@ async with ai.stream(model, messages) as stream: print(stream.usage) print(stream.message.usage) ``` + +## Validate message history + +Before provider calls, the SDK prepares message history by stripping internal +messages, removing non-model parts, repairing invalid tool args to `{}`, and +inserting error results for missing tool calls when possible. Use strict +validation in tests or import pipelines: + +```python +from ai.types import integrity + + +integrity.prepare_messages(messages, mode="strict") +``` diff --git a/docs/ai-python/content/docs/basics/meta.json b/docs/ai-python/content/docs/basics/meta.json index 0dd6f54d..494f0fb1 100644 --- a/docs/ai-python/content/docs/basics/meta.json +++ b/docs/ai-python/content/docs/basics/meta.json @@ -8,6 +8,7 @@ "tools", "agents", "custom-loops", + "durable-execution", "subagents-and-multi-agent", "human-in-the-loop", "ai-sdk-ui" diff --git a/docs/ai-python/content/docs/basics/providers.mdx b/docs/ai-python/content/docs/basics/providers.mdx index 36264fa7..8fe5b172 100644 --- a/docs/ai-python/content/docs/basics/providers.mdx +++ b/docs/ai-python/content/docs/basics/providers.mdx @@ -10,9 +10,8 @@ Models are lightweight references that point at a provider. ## Create a model -A model is a lightweight reference to a provider model. Create one with -`ai.get_model`. If you omit the provider prefix, the model routes through AI -Gateway: +Use `ai.get_model` to create a model. If you omit the provider prefix, the model +will use AI Gateway as a provider: ```python model = ai.get_model("anthropic/claude-sonnet-4") @@ -36,22 +35,18 @@ model = ai.get_model("openai:gpt-5") ## Configure credentials -The default gateway route reads `AI_GATEWAY_API_KEY`: +Providers read their provider-specific keys: ```bash title="Terminal" export AI_GATEWAY_API_KEY="your_access_token_here" -``` - -Direct providers read their provider-specific keys: - -```bash title="Terminal" export OPENAI_API_KEY="your_access_token_here" export ANTHROPIC_API_KEY="your_access_token_here" ``` -## Override base URLs +## Use an explicit provider -Pass `base_url` when you create an explicit provider: +If your application requires a provider with custom configuration, +use `get_provider` to get a `Provider` instance: ```python provider = ai.get_provider( @@ -60,15 +55,10 @@ provider = ai.get_provider( api_key="your_access_token_here", ) -model = ai.Model("local-model", provider=provider) +model = ai.Model(id="local-model", provider=provider) ``` -OpenAI and Anthropic direct providers also read `OPENAI_BASE_URL` and -`ANTHROPIC_BASE_URL`. - -## Use an explicit client - -Pass an upstream client when your app owns transport configuration: +You can pass an upstream client to the provider: ```python import httpx @@ -82,7 +72,7 @@ provider = ai.get_provider( api_key="your_access_token_here", client=client, ) -model = ai.Model("local-model", provider=provider) +model = ai.Model(id="local-model", provider=provider) ``` Close explicit providers when your app shuts down: @@ -120,12 +110,14 @@ except ai.ProviderError as exc: Pass request-scoped provider options with `params`: ```python -params = { - "providerOptions": { - "gateway": {"sort": "cost"}, - "anthropic": {"speed": "fast"}, +params = ai.InferenceRequestParams( + extra_body={ + "providerOptions": { + "gateway": {"sort": "cost"}, + "anthropic": {"speed": "fast"}, + } } -} +) async with ai.stream(model, messages, params=params) as stream: async for event in stream: @@ -135,3 +127,23 @@ async with ai.stream(model, messages, params=params) as stream: Provider options pass through to the selected provider. Check the provider documentation for supported fields. + +## Add a provider + +Custom providers subclass `ai.Provider`. A provider owns configuration, +clients, model listing, connection checks, and wire translation through its +protocol: + +```python +class AcmeProvider(ai.Provider[AcmeClient]): + handles = ("acme",) + + async def list_models(self) -> list[str]: + ... + + async def probe(self, model: ai.Model) -> None: + ... +``` + +Most applications only need `ai.get_provider` and `ai.Model`. Add a provider +when you need a new upstream API or custom wire protocol. diff --git a/docs/ai-python/content/docs/basics/streaming.mdx b/docs/ai-python/content/docs/basics/streaming.mdx index 9cee1b9b..6a3a7c77 100644 --- a/docs/ai-python/content/docs/basics/streaming.mdx +++ b/docs/ai-python/content/docs/basics/streaming.mdx @@ -29,13 +29,15 @@ async def main() -> None: async for event in stream: if isinstance(event, ai.events.TextDelta): print(event.chunk, end="", flush=True) - print() if __name__ == "__main__": asyncio.run(main()) ``` +One call to `ai.stream` produces one `ai.Message`. + + ## Read the final message The stream aggregates events into a final assistant message: @@ -43,21 +45,25 @@ The stream aggregates events into a final assistant message: ```python async with ai.stream(model, messages) as stream: async for event in stream: - if isinstance(event, ai.events.TextDelta): - print(event.chunk, end="", flush=True) + pass message = stream.message # Final message text = stream.text # Unwrapped text from the final message usage = stream.usage ``` -Use `stream.message` when you need to append the assistant turn to your own -history. Use `stream.text` when you only need the final text. +If a provider stream ends before its finish event, iteration raises +`ai.errors.ProviderIncompleteResponseError`. The partial message is still +available on `stream.message`. + +The stream updates `stream.message` as events arrive. Text and reasoning use +start, delta, and end events. Tool calls use `ToolStart`, `ToolDelta`, and +`ToolEnd`. Generated files arrive as `FileEvent` and are added to the final +message. ## Use structured output -Pass a Pydantic model as `output_type` when you want the final text parsed as -JSON: +Pass a Pydantic model as `output_type` when you want to use structured outputs: ```python title="structured_output.py" import asyncio @@ -93,34 +99,27 @@ if __name__ == "__main__": `stream.output` returns text by default. When you pass `output_type`, it returns an instance of that Pydantic model after the stream finishes. -## Pass tool schemas without an agent +## Pass tools -`ai.stream` does not execute function tools. Use it when you want to inspect -tool calls and manage execution yourself. Use an agent when you want the SDK to +`ai.stream` accepts a list of `ai.Tool`, however, it does not have any +framework-side tool execution machinery. Use an agent when you want the SDK to execute requested tools and continue the loop. -## Use provider-executed tools - -Provider-executed tools run inside the provider or gateway. They appear in the +Provider-executed tools run on provider's side. They appear in the stream as built-in tool events and do not need a Python function: ```python -tools = [ai.providers.anthropic.tools.web_search(max_uses=3)] +tools = [ai.providers.ai_gateway.tools.perplexity_search(max_results=5)] async with ai.stream(model, messages, tools=tools) as stream: async for event in stream: - if isinstance(event, ai.events.BuiltinToolEnd): - print(event.tool_call.tool_name) - elif isinstance(event, ai.events.BuiltinToolResult): - print(event.result.result) - elif isinstance(event, ai.events.TextDelta): - print(event.chunk, end="", flush=True) -``` - -When you route through AI Gateway, you can also pass gateway tools: - -```python -tools = [ai.providers.ai_gateway.tools.perplexity_search(max_results=5)] + match event: + case ai.events.BuiltinToolEnd(tool_call): + print(tool_call.tool_name) + case ai.events.BuiltinToolResult(result): + print(result.result) + case ai.events.TextDelta(chunk): + print(chunk, end="", flush=True) ``` ## Handle files from a stream @@ -134,8 +133,8 @@ async with ai.stream(model, messages) as stream: if isinstance(event, ai.events.FileEvent): print(event.media_type, event.filename) -for file in stream.message.files: - print(file.media_type) +for f in stream.message.files: + print(f.media_type) ``` Use `ai.generate` for dedicated image and video models: diff --git a/docs/ai-python/content/docs/basics/subagents-and-multi-agent.mdx b/docs/ai-python/content/docs/basics/subagents-and-multi-agent.mdx index 854bf535..fff4b685 100644 --- a/docs/ai-python/content/docs/basics/subagents-and-multi-agent.mdx +++ b/docs/ai-python/content/docs/basics/subagents-and-multi-agent.mdx @@ -1,8 +1,8 @@ --- -title: Subagents and Multi-Agent +title: Subagents description: Compose agents and stream nested agent output. type: guide -summary: Run subagents as tools, stream nested output, run agents in parallel, route labels, and fan in results. +summary: Run subagents as tools, stream nested output. --- Subagents are regular agents used inside tools or custom loops. Their events @@ -10,7 +10,8 @@ can stream through the parent run while their final text becomes model input. ## Run a subagent as a tool -Use `ai.SubAgentTool` when a tool should stream events from another agent: +In order for the SDK to correctly handle subagent's outputs, you need to +annotate the tool's output type with `ai.SubAgentTool`: ```python mothership_model = ai.get_model("anthropic/claude-sonnet-4") @@ -30,10 +31,9 @@ async def ask_mothership(topic: str) -> ai.SubAgentTool: yield event ``` -The parent stream receives the sub-agent events. The parent model sees the final -assistant text from the sub-agent as the tool result. - -## Stream subagent output +This allows the framework to stream partial outputs, keep nested chat history +in `ai.MessageBundle` (which is a marker type for `list[ai.Message]`), and +pass the final output to the parent agent. `ai.SubAgentTool` declares the aggregator for nested agent events. The parent consumer receives each nested event as `PartialToolCallResult.value`: @@ -48,7 +48,7 @@ async with orchestrator.run(model, messages) as stream: print(event.chunk, end="", flush=True) ``` -## Run agents in parallel +## Run streams in parallel Use `ai.yield_from` inside a custom loop to run branches concurrently and forward their events: @@ -72,10 +72,8 @@ async with ( ) ``` -## Route labeled output - -`yield_from` wraps forwarded events in `PartialToolCallResult` with the label -you pass: +`yield_from` wraps events in `PartialToolCallResult` with the label +you pass, and forwards them outside via the internal runtime queue: ```python if isinstance(event, ai.events.PartialToolCallResult): @@ -84,25 +82,3 @@ if isinstance(event, ai.events.PartialToolCallResult): elif event.label == "data_centers": route_to_data_center_panel(event.value) ``` - -## Fan in results - -After parallel branches finish, send their returned text into a final summary -turn: - -```python -combined = ( - f"Mothership: {mothership_text}\n" - f"Data centers: {data_center_text}" -) - -async with summary_agent.run( - model, - [ - ai.system_message("Summarize the branch reports."), - ai.user_message(combined), - ], -) as summary: - async for event in summary: - yield event -``` diff --git a/docs/ai-python/content/docs/basics/tools.mdx b/docs/ai-python/content/docs/basics/tools.mdx index 420d8142..f890204f 100644 --- a/docs/ai-python/content/docs/basics/tools.mdx +++ b/docs/ai-python/content/docs/basics/tools.mdx @@ -5,10 +5,10 @@ type: guide summary: Define function tools, design schemas, handle tool errors, use schema-only tools, provider tools, and MCP tools. --- -Tools can be schema-only declarations for direct streaming, executable Python -functions for agents, or provider-executed tools that run outside your process. +AI SDK for Python supports multiple types of tools, including function tools +defined in code using `@ai.tool`, as well as built-in provider-side tools. -## Define function tools +## Declare function tools Decorate an async function with `@ai.tool`: @@ -25,35 +25,9 @@ async def contact_mothership(query: str) -> str: The tool name comes from the function name. The model receives the function parameters as a JSON schema and the docstring as the tool description. -## Design tool schemas - -The function signature becomes the tool schema. The docstring becomes the tool -description the model sees. - -```python -@ai.tool -async def scan_sector(sector: str, depth: int = 1) -> str: - """Scan a mothership sector at the requested depth.""" - return f"{sector}: clear at depth {depth}" -``` - -The model receives `sector` as a required string and `depth` as an optional -integer with a default. - -## Validate arguments - -Tool arguments validate through the generated Pydantic model before your -function runs: - -```python -@ai.tool -async def set_alert_level(level: int) -> str: - """Set the mothership alert level.""" - return f"Alert level set to {level}" -``` - -If the model sends `{"level": "high"}`, validation fails and the agent returns -an error tool result instead of calling the function. +Function tools will only be automatically executed by the SDK when used +in the context of the agent. Provider-side tools will always get executed +by the provider, even when passed to `ai.stream`. ## Handle tool errors @@ -71,7 +45,60 @@ async with agent.run(model, messages) as stream: The original exception is available on `event.exception` for logging. -## Use schema-only tools +## Declare tools that stream output + +AI SDK for Python supports tools that are async-generators. You might want to +use those when your tools need to return partial output, e.g. when wrapping +subagents. + +Streaming tools use aggregators. An aggregator solves two problems: the tool can +yield many values over time, but the agent still needs one final tool result; +and your app may want a rich stored result while the model needs a simpler value +on the next turn. + +The core interface is: + +```python +class Aggregator[Item, Result, ModelInput]: + def feed(self, item: Item) -> None: ... + def snapshot(self) -> Result: ... + + @classmethod + def to_model_input(cls, snapshot: Result) -> ModelInput: ... +``` + +Use `ai.StreamingTextTool` when yielded strings should concatenate into the +tool result: + +```python +@ai.tool +async def draft_reply(topic: str) -> ai.StreamingTextTool: + """Draft a reply.""" + yield "Checking " + yield "records for " + yield topic +``` + +Use `ai.StreamingStatusTool[T]` when intermediate yields are progress updates +and the last yielded value is the final result. + +Use `ai.SubAgentTool` when a tool streams events from a nested agent. The stored +result is a message bundle, and the model sees the final assistant text. + +The agent emits `PartialToolCallResult` events for those values, then sends +the aggregated result back to the model on the next turn. + +## Understand tool anatomy + +`ai.Tool` represents tools of all kinds in the SDK. + +Each tool carries an optional `spec`, which is a description consumed by the +model, as well as a `tool_config` that contains tool-specific execution config +(e.g. retry policy). + +`AgentTool` wraps `ai.Tool` together with its corresponding Python function, +which allows `agent.run` to find and call that function when the model requests +to do so. Pass `ai.Tool` objects directly to `ai.stream` when you want the model to emit tool calls but you do not want the SDK to execute them: @@ -80,7 +107,7 @@ tool calls but you do not want the SDK to execute them: tool = ai.Tool( kind="function", name="contact_mothership", - args=ai.tools.FunctionToolArgs( + spec=ai.tools.ToolSpec( description="Contact the mothership.", params={ "type": "object", @@ -130,6 +157,10 @@ tools = [ The Model Context Protocol (MCP) adapter converts server tools into agent tools: +```bash +uv add "ai[mcp]" +``` + ```python tools = await ai.mcp.get_http_tools( "http://localhost:3000/mcp", diff --git a/docs/ai-python/content/docs/core-framework/adapters-and-providers.mdx b/docs/ai-python/content/docs/core-framework/adapters-and-providers.mdx deleted file mode 100644 index ee29581c..00000000 --- a/docs/ai-python/content/docs/core-framework/adapters-and-providers.mdx +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: Adapters and Providers -description: Understand provider objects and adapter dispatch. -type: conceptual -summary: Learn how providers, clients, adapters, and wire translations connect models to APIs. ---- - -Providers own configuration and clients. Adapters translate framework messages, -tools, params, and generated files to provider wire formats. - -## Provider objects - -`Model` stores a model ID, adapter key, and provider reference: - -```python -provider = ai.get_provider("openai") -model = ai.Model("gpt-5.4", provider=provider) -``` - -`ai.get_model` resolves a provider from a model ID and defaults unprefixed IDs -to AI Gateway: - -```python -model = ai.get_model("anthropic/claude-sonnet-4") -``` - -## Client creation - -Providers create upstream clients from API keys, base URLs, headers, and env -vars. You can also pass a client that your app owns: - -```python -provider = ai.get_provider( - "openai", - base_url="http://localhost:1234/v1", - api_key="your_access_token_here", - client=http_client, -) -``` - -## Adapter registry - -Provider subclasses register handles such as `openai`, `anthropic`, and -`vercel`. `get_provider` uses models.dev metadata to choose the provider class -for a provider ID. - -## Stream adapters - -Stream adapters implement `provider.stream`. They convert messages and tools to -the provider request, then yield public events: - -```python -async for event in model.provider.stream(model, messages, tools=tools): - yield event -``` - -OpenAI-compatible providers use the OpenAI chat-completions protocol. -Anthropic-compatible providers use the Anthropic messages protocol. AI Gateway -uses the Language Model v3 stream protocol. - -## Generate adapters - -`ai.generate` prepares messages and calls `provider.generate`. AI Gateway -supports image and video generation: - -```python -result = await ai.generate( - model, - [ai.user_message("A mothership over the ocean.")], - ai.ImageParams(n=1), -) -``` - -## Provider wire translation - -Adapters translate the same internal parts differently per provider: - -- `TextPart` and `FilePart` become user content. -- `ToolCallPart` becomes provider tool-call wire data. -- `ToolResultPart.get_model_input()` becomes the tool result sent back to the - model. -- Provider-executed tools become provider tool declarations. - -## Custom adapters - -Add a provider by subclassing `Provider`, setting `handles`, and implementing -`from_modelsdev_provider`, `stream`, `list_models`, and `probe`. Implement -`generate` only if the provider supports non-streaming media generation. diff --git a/docs/ai-python/content/docs/core-framework/agent-loop.mdx b/docs/ai-python/content/docs/core-framework/agent-loop.mdx deleted file mode 100644 index 2ceee08d..00000000 --- a/docs/ai-python/content/docs/core-framework/agent-loop.mdx +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: Agent Loop -description: Understand the default agent loop and history lifecycle. -type: conceptual -summary: Learn how agents stream model output, dispatch tools, update history, and decide when to stop. ---- - -`Agent.loop` is the default control flow for model calls and tool execution. It -is intentionally small so you can override it when needed. - -## Default loop lifecycle - -```python -while context.keep_running(): - async with ( - ai.stream(context=context) as stream, - ai.ToolRunner() as tool_runner, - ): - async for event in ai.util.merge(stream, tool_runner.events()): - yield event - if isinstance(event, ai.events.ToolEnd): - tool_runner.schedule(context.resolve(event.tool_call)) - - context.add(stream.message) - context.add(tool_runner.get_tool_message()) -``` - -## Context state - -`Context` holds the model, messages, model-facing tool schemas, structured -output type, request params, and the private executable tool registry. - -```python -context.model -context.messages -context.tools -context.params -``` - -## Keep-running rules - -`context.keep_running()` returns `True` while the last message still needs work: - -- No messages means stop. -- A final assistant message means stop. -- A pending hook result means stop until the hook resolves. -- A replay-marked assistant message means dispatch its tool calls again. - -## Message history updates - -Each model turn adds one assistant message. Each tool batch adds one tool -message: - -```python -context.add(stream.message) -context.add(tool_runner.get_tool_message()) -``` - -Replay-marked assistant messages are skipped to avoid duplicate history. - -## Tool-result turns - -`ToolRunner` collects `ToolCallResult` events and merges their result parts into -one `role="tool"` message: - -```python -tool_message = tool_runner.get_tool_message() -context.add(tool_message) -``` - -The next model turn receives that tool message as context. - -## Final output - -`AgentStream.output` reads the final assistant message. With no `output_type`, -it returns text. With `output_type`, it validates JSON into that Pydantic model: - -```python -async with agent.run(model, messages, output_type=Forecast) as stream: - async for event in stream: - ... - -forecast = stream.output -``` diff --git a/docs/ai-python/content/docs/core-framework/hooks.mdx b/docs/ai-python/content/docs/core-framework/hooks.mdx deleted file mode 100644 index 144958e6..00000000 --- a/docs/ai-python/content/docs/core-framework/hooks.mdx +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: Hooks -description: Understand hook suspension, resolution, cancellation, and resume. -type: conceptual -summary: Learn how hooks suspend agent work, emit state, accept resolutions, and support replay. ---- - -Hooks are runtime suspension points. They emit internal messages so clients can -render pending, resolved, or cancelled state. - -## Hook lifecycle - -```python -approval = await ai.hook( - "approve_contact_mothership", - payload=ai.tools.ToolApproval, - metadata={"tool": "contact_mothership"}, -) -``` - -The hook checks for a pre-registered resolution first. If none exists, it emits -a pending `HookEvent` and waits on a live future. - -## Live hook registry - -Live hooks are stored by label while an agent run is active. `resolve_hook` -settles the waiting future: - -```python -ai.resolve_hook( - "approve_contact_mothership", - {"granted": True, "reason": "approved"}, -) -``` - -The run removes live hook entries when the hook resolves or the run finishes. - -## Pending resolutions - -When no live hook exists, `resolve_hook` stores the resolution for a later -replay: - -```python -ai.resolve_hook( - "approve_contact_mothership", - ai.tools.ToolApproval(granted=True, reason="approved"), -) -``` - -The next matching `ai.hook` consumes that value and returns immediately. - -## Hook events - -Hooks emit `HookEvent` objects. The event message has role `internal` and -contains a `HookPart`: - -```python -if isinstance(event, ai.events.HookEvent): - print(event.hook.hook_id, event.hook.status) -``` - -## Abort and resume - -Serverless handlers can abort a run after a pending hook event: - -```python -if event.hook.status == "pending": - ai.abort_pending_hook(event.hook) -``` - -Persist the messages, collect the user's response, call `resolve_hook`, and run -the agent again with the restored messages. - -## Cancellation - -Cancel a live hook when the waiting workflow should stop: - -```python -await ai.cancel_hook("approve_contact_mothership", reason="client disconnected") -``` - -Cancellation emits a hook event with `status="cancelled"`. - -## Cleanup - -The runtime tracks hook labels for each run. When the run exits, it removes live -hooks and pending resolutions for those labels, then closes scoped Model Context -Protocol (MCP) connections. diff --git a/docs/ai-python/content/docs/core-framework/index.mdx b/docs/ai-python/content/docs/core-framework/index.mdx deleted file mode 100644 index d7f2830f..00000000 --- a/docs/ai-python/content/docs/core-framework/index.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Core Framework -description: Understand the internals of streams, agents, tools, hooks, and runtime behavior. -type: conceptual -summary: Learn how the core framework coordinates stream aggregation, agent loops, tools, hooks, history, adapters, and runtime state. ---- - -The core framework is the set of small pieces behind the public APIs. Providers -produce events, streams aggregate those events into messages, agents decide -when to call tools, and hooks suspend work until external input arrives. - -## Architecture overview - -The runtime has four main layers: - -- Providers translate `Message` lists and `Tool` schemas to remote APIs. -- `ai.stream` wraps provider events and builds the assistant message. -- `Agent.loop` runs the stream -> tool -> stream cycle. -- The runtime queue carries agent events, hook events, and partial tool output - to the consumer. - -## Data flow - -```python -messages = [ai.user_message("Ask the mothership for launch status.")] - -async with agent.run(model, messages) as stream: - async for event in stream: - ... - -updated_history = stream.messages -``` - -The flow is: - -1. The model receives prepared messages and tool schemas. -2. The stream yields text, reasoning, tool-call, built-in-tool, file, and usage - events. -3. The stream aggregates events into one assistant message. -4. The agent resolves tool calls, runs tools, appends tool results, and repeats. - -## Extension points - -- Use `params` for request-scoped provider options. -- Define `@ai.tool` functions for agent-executed tools. -- Use provider tool factories for provider-executed tools. -- Override `Agent.loop` when scheduling or history updates need custom logic. -- Add hooks when the loop needs external input. -- Use the AI SDK UI adapter to translate events to UI stream parts. - -## Internal state boundaries - -Messages are the durable boundary. Streams, tool runners, live hook futures, -and provider clients are runtime state. Persist `stream.messages`, not live -runtime objects. diff --git a/docs/ai-python/content/docs/core-framework/message-history.mdx b/docs/ai-python/content/docs/core-framework/message-history.mdx deleted file mode 100644 index b48cf8b0..00000000 --- a/docs/ai-python/content/docs/core-framework/message-history.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Message History -description: Understand message parts, internal state, validation, repair, and replay. -type: conceptual -summary: Learn the invariants the framework maintains for model-ready message history. ---- - -Message history is the durable contract between your app, providers, tools, and -resume flows. - -## Message and part model - -Each message has a role and typed parts: - -```python -message = ai.user_message( - "Inspect this mothership diagram.", - ai.file_part(image_bytes, media_type="image/png"), -) -``` - -Common parts include text, reasoning, tool calls, tool results, provider tool -returns, hook state, and files. - -## Internal messages - -Hooks emit `role="internal"` messages with `HookPart` values. They are useful -for UI state and resume flows, but providers do not receive them. - -```python -if message.role == "internal": - hook = message.parts[0] -``` - -## Tool-call invariants - -Provider APIs expect every assistant tool call to have a matching tool result -before the next user or assistant message: - -```python -[ - ai.assistant_message(tool_call_part), - ai.tool_message(tool_result_part), -] -``` - -Duplicate tool-call IDs, duplicate result IDs, and orphaned tool results are -fatal integrity errors. - -## Automatic history repair - -Before provider calls, the framework prepares message history in `auto` mode. It -strips internal messages, removes non-model parts, repairs invalid tool args to -`{}`, and inserts error results for missing tool calls when possible. - -## Strict validation - -Use strict validation in tests or import pipelines when you want repairable -issues to raise: - -```python -from ai.types import integrity - - -integrity.prepare_messages(messages, mode="strict") -``` - -## Replay markers - -Replay markers are runtime-only flags on assistant messages. They let a resumed -agent dispatch existing tool calls without another model request: - -```python -if messages[-1].replay: - async with ai.stream(model, messages) as stream: - ... -``` - -Replay flags are excluded from JSON serialization. - -## Persisted history - -Persist Pydantic JSON, then restore with `Message.model_validate`: - -```python -encoded = [message.model_dump(mode="json") for message in stream.messages] -restored = [ai.messages.Message.model_validate(item) for item in encoded] -``` - -Persist messages after each completed or suspended run. Recreate live runtime -objects, providers, and hooks on the next request. diff --git a/docs/ai-python/content/docs/core-framework/meta.json b/docs/ai-python/content/docs/core-framework/meta.json deleted file mode 100644 index 05e8ea75..00000000 --- a/docs/ai-python/content/docs/core-framework/meta.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "title": "Core Framework", - "description": "Understand the internals of streams, agents, tools, hooks, and runtime behavior.", - "pages": [ - "streams", - "agent-loop", - "tool-dispatch", - "hooks", - "streaming-tools", - "message-history", - "adapters-and-providers" - ] -} diff --git a/docs/ai-python/content/docs/core-framework/streaming-tools.mdx b/docs/ai-python/content/docs/core-framework/streaming-tools.mdx deleted file mode 100644 index 72634a84..00000000 --- a/docs/ai-python/content/docs/core-framework/streaming-tools.mdx +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: Streaming Tools -description: Understand async-generator tools and aggregation. -type: conceptual -summary: Learn how streaming tools emit partial output while producing a final model-facing result. ---- - -Async-generator tools yield values while they run. An aggregator turns those -values into the final tool result that the model sees on the next turn. - -## Async-generator tools - -Async-generator tools can stream partial output while they run. Use -`ai.StreamingTextTool` when yielded strings should be concatenated into the -tool result: - -```python -@ai.tool -async def draft_mothership_reply(topic: str) -> ai.StreamingTextTool: - """Draft a reply from the mothership.""" - yield "Consulting " - yield "the " - yield "mothership..." -``` - -## Partial tool results - -Partial yields appear as `ai.events.PartialToolCallResult` events. The model -sees the aggregated result on the next turn. - -## Aggregators - -An aggregator receives each yielded value, keeps a snapshot for consumers, and -converts that snapshot to model-facing input: - -```python -from collections.abc import AsyncGenerator -from typing import Annotated - -import ai - - -type Lines = Annotated[ - AsyncGenerator[str], - ai.agents.Aggregate(ai.agents.ConcatAggregator, delim="\n"), -] - - -@ai.tool -async def list_mothership_tasks() -> Lines: - """List pending mothership tasks.""" - yield "Calibrate antenna" - yield "Check orbit" -``` - -## Model-facing input - -`ToolResultPart.result` stores the rich snapshot. `ToolResultPart.get_model_input` -returns the value sent back to the model: - -```python -result_part = event.results[0] -print(result_part.result) -print(result_part.get_model_input()) -``` - -For most tools, those values are the same. For subagents, the result can contain -messages while the model input is the final assistant text. - -## Rich snapshots - -Aggregators can preserve more than text. `MessageAggregator` stores nested -messages from a subagent. The rich snapshot type is -`ai.types.messages.MessageBundle`: - -```python -if isinstance(event, ai.events.ToolCallResult): - result = event.results[0].result - if isinstance(result, ai.types.messages.MessageBundle): - print(result.messages[-1].text) -``` - -## Streaming text tools - -Use `ai.StreamingTextTool` when every yielded string should be concatenated: - -```python -@ai.tool -async def draft_mothership_reply(topic: str) -> ai.StreamingTextTool: - """Draft a reply from the mothership.""" - yield "The " - yield "mothership " - yield f"reports on {topic}." -``` - -## Status tools - -Use `ai.StreamingStatusTool[T]` when intermediate yields are progress updates -and the last yielded value is the final result: - -```python -@ai.tool -async def check_alignment() -> ai.StreamingStatusTool[str]: - """Check orbital alignment.""" - yield "opening channel" - yield "checking telemetry" - yield "alignment stable" -``` - -## Subagent tools - -Use `ai.SubAgentTool` when a tool delegates work to another agent: - -```python -@ai.tool -async def ask_mothership(topic: str) -> ai.SubAgentTool: - """Ask the mothership subagent.""" - subagent = ai.agent() - async with subagent.run( - model, - [ai.user_message(topic)], - ) as stream: - async for event in stream: - yield event -``` - -The parent stream receives the nested events. The parent model receives the -subagent's final assistant text. diff --git a/docs/ai-python/content/docs/core-framework/streams.mdx b/docs/ai-python/content/docs/core-framework/streams.mdx deleted file mode 100644 index a45c36ef..00000000 --- a/docs/ai-python/content/docs/core-framework/streams.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Streams -description: Understand stream events and aggregation. -type: conceptual -summary: Learn how provider events become an aggregated assistant message. ---- - -`ai.stream` turns provider events into a stateful `Stream` object. Iteration -returns events; the stream also keeps the in-progress assistant message. - -## Stream lifecycle - -```python -async with ai.stream(model, messages, tools=tools) as stream: - async for event in stream: - ... - -message = stream.message -``` - -On entry, the framework prepares message history and creates a provider request. -On exit, it closes the underlying async generator. - -## Event aggregation - -`Stream.__anext__` receives one provider event, updates `stream.message`, and -then returns a copy of the event with the current message attached. - -```python -async for event in stream: - print(event.kind, event.message.text) -``` - -## Text and reasoning blocks - -Text and reasoning use start, delta, and end events. Deltas append to the part -with the same block ID: - -```python -if isinstance(event, ai.events.TextDelta): - print(event.chunk, end="", flush=True) -elif isinstance(event, ai.events.ReasoningDelta): - record_reasoning(event.chunk) -``` - -## Tool-call blocks - -Function tool calls stream as `ToolStart`, `ToolDelta`, and `ToolEnd`. -`ToolEnd.tool_call` is the complete `ToolCallPart`: - -```python -if isinstance(event, ai.events.ToolEnd): - call = event.tool_call - print(call.tool_name, call.tool_args) -``` - -## Built-in tool blocks - -Provider-executed tools use separate built-in events. The host does not execute -these calls: - -```python -if isinstance(event, ai.events.BuiltinToolResult): - print(event.result.tool_name, event.result.result) -``` - -## File events - -Generated files arrive as `FileEvent` and become `FilePart` values on the -assistant message: - -```python -if isinstance(event, ai.events.FileEvent): - print(event.media_type, event.filename) -``` - -## Usage and provider metadata - -Usage and provider metadata are latest-wins fields. Events may carry them, and -the stream copies usage onto the assistant message: - -```python -if event.usage is not None: - print(event.usage) -``` - -## Replay streams - -When the last message has `replay=True`, `ai.stream` does not call the provider. -It emits synthetic replay tool-end events from the existing assistant message -so resume flows can dispatch the same tool calls again. diff --git a/docs/ai-python/content/docs/core-framework/tool-dispatch.mdx b/docs/ai-python/content/docs/core-framework/tool-dispatch.mdx deleted file mode 100644 index 22344e6a..00000000 --- a/docs/ai-python/content/docs/core-framework/tool-dispatch.mdx +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: Tool Dispatch -description: Understand tool binding, validation, scheduling, and results. -type: conceptual -summary: Learn how tool schemas become executable calls and how tool results flow back into history. ---- - -Tool dispatch starts with a streamed `ToolCallPart` and ends with a -`ToolResultPart` in message history. - -## Tool schema creation - -`@ai.tool` inspects the async function signature and creates a Pydantic -validator plus a model-facing `Tool` declaration: - -```python -@ai.tool -async def contact_mothership(query: str) -> str: - """Contact the mothership for important decisions.""" - return "Soon." -``` - -The tool name is the function name. The docstring becomes the description. - -## Bound tool calls - -When the model emits a tool call, `context.resolve` binds the streamed -`ToolCallPart` to the registered `AgentTool`: - -```python -if isinstance(event, ai.events.ToolEnd): - tool_call = context.resolve(event.tool_call) -``` - -The bound call exposes `id`, `name`, `fn`, and validated `kwargs`. - -## Argument validation - -Arguments are JSON-decoded from `tool_args` and validated before the function -runs: - -```python -kwargs = tool_call.kwargs -``` - -Validation failures return an error tool result, so the agent can continue with -the error in history. - -## Concurrent scheduling - -`ToolRunner.schedule` starts each call in a task group. `ToolRunner.events()` -yields results as tasks finish: - -```python -tool_runner.schedule(context.resolve(event.tool_call)) -``` - -This lets multiple tool calls from one assistant turn run concurrently. - -## Tool result aggregation - -Each successful call creates a `ToolResultPart`. `ToolRunner.get_tool_message` -merges all collected results into one tool message: - -```python -message = tool_runner.get_tool_message() -context.add(message) -``` - -## Error results - -Tool exceptions are caught and converted to error results: - -```python -if isinstance(event, ai.events.ToolCallResult) and event.exception: - log_exception(event.exception) -``` - -The model receives a string error result. The event keeps the original -exception for logging. - -## Approval-gated dispatch - -Tools declared with `require_approval=True` are wrapped in an approval hook. -The call runs only after the hook resolves with `granted=True`: - -```python -@ai.tool(require_approval=True) -async def notify_mothership(message: str) -> str: - """Notify the mothership.""" - return f"Sent: {message}" -``` - -If the hook is denied, the agent returns an error tool result. If the run aborts -while the hook is pending, the pending result marks the history for replay. diff --git a/docs/ai-python/content/docs/index.mdx b/docs/ai-python/content/docs/index.mdx index 07f1c717..55deabe9 100644 --- a/docs/ai-python/content/docs/index.mdx +++ b/docs/ai-python/content/docs/index.mdx @@ -21,6 +21,13 @@ async Python. uv add ai ``` +AI Gateway is the default route for unprefixed model IDs. Configure the gateway +key before running the examples: + +```bash title="Terminal" +export AI_GATEWAY_API_KEY="your_access_token_here" +``` + Then import the package in Python: ```python @@ -44,7 +51,7 @@ async def contact_mothership(query: str) -> str: async def main() -> None: model = ai.get_model("anthropic/claude-sonnet-4") - agent = ai.agent(tools=[contact_mothership]) + agent = ai.Agent(tools=[contact_mothership]) messages = [ ai.system_message( @@ -106,10 +113,10 @@ After iteration, `s.message`, `s.text`, `s.tool_calls`, `s.output`, and ## What's next -- **Core concepts**: Learn the primitives that shape the SDK. -- **Streaming**: Stream model responses and inspect events. -- **Agents**: Add tools, customize the loop, and handle approvals. -- **Samples**: Focused, single-file examples live in +- **Basics**: Learn the main workflows for providers, messages, streams, + tools, agents, hooks, and UI integrations. +- **Reference**: Look up public APIs by the names you import and call. +- **Examples**: Focused, single-file examples live in [`examples/`](https://github.com/vercel-labs/ai-python/tree/main/examples). - **End-to-end demos**: [`fastapi-vite`](https://github.com/vercel-labs/ai-python/tree/main/examples/fastapi-vite) diff --git a/docs/ai-python/content/docs/meta.json b/docs/ai-python/content/docs/meta.json index 54d6837f..91e946f5 100644 --- a/docs/ai-python/content/docs/meta.json +++ b/docs/ai-python/content/docs/meta.json @@ -5,6 +5,6 @@ "pages": [ "index", "basics", - "core-framework" + "reference" ] } diff --git a/docs/ai-python/content/docs/reference/ai.agents/index.mdx b/docs/ai-python/content/docs/reference/ai.agents/index.mdx new file mode 100644 index 00000000..87c083d5 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.agents/index.mdx @@ -0,0 +1,46 @@ +--- +title: "ai.agents" +description: "Reference for advanced agent namespace APIs." +type: reference +summary: "Reference for public APIs that are intentionally used from ai.agents." +--- + +Most agent APIs are documented as top-level `ai` exports. Use `ai.agents` +for advanced agent namespace APIs and types that are not promoted to the +top-level namespace. + +## Aggregators + +Aggregators collect yielded values from streaming tools and decide what value is +stored and what value is sent back to the model. + +APIs: + +- `Aggregate` +- `yield_from` +- `SimpleAggregator` +- `ConcatAggregator` +- `LastAggregator` +- `MessageAggregator` + +Custom aggregators implement `ai.events.Aggregator`. + +`yield_from` forwards events from a nested async generator and returns the +nested generator result. + +```python +result = await ai.agents.yield_from(generator) +``` + +## Tool Calls + +- `BoundToolCall` +- `ToolCallCallable` + +## GatedToolCall + +`GatedToolCall` wraps a tool call that requires approval or another gate before +execution. + +Use it in custom agent loops when you need to delay or externally approve a +scheduled tool call. diff --git a/docs/ai-python/content/docs/reference/ai.agents/meta.json b/docs/ai-python/content/docs/reference/ai.agents/meta.json new file mode 100644 index 00000000..af9cfc48 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.agents/meta.json @@ -0,0 +1,7 @@ +{ + "title": "ai.agents", + "description": "Reference for advanced agent namespace APIs.", + "pages": [ + "ui" + ] +} diff --git a/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/approvals.mdx b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/approvals.mdx new file mode 100644 index 00000000..75e4db39 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/approvals.mdx @@ -0,0 +1,20 @@ +--- +title: "ai.agents.ui.ai_sdk approvals" +description: Apply AI SDK UI approval responses. +type: reference +summary: Reference for ApprovalResponse, extract_approvals, and apply_approvals. +--- + +Approval helpers bridge AI SDK UI tool approval responses into the hook +registry. + +## APIs + +- `ApprovalResponse` +- `extract_approvals` +- `apply_approvals` + +```python +approvals = ai.agents.ui.ai_sdk.extract_approvals(ui_messages) +ai.agents.ui.ai_sdk.apply_approvals(approvals) +``` diff --git a/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/inbound-messages.mdx b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/inbound-messages.mdx new file mode 100644 index 00000000..5511295d --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/inbound-messages.mdx @@ -0,0 +1,15 @@ +--- +title: "ai.agents.ui.ai_sdk.to_messages" +description: Convert UI messages to runtime messages. +type: reference +summary: Reference for inbound AI SDK UI message conversion. +--- + +`to_messages` converts AI SDK UI messages into runtime messages and extracts +approval responses. + +```python +messages, approvals = ai.agents.ui.ai_sdk.to_messages(ui_messages) +``` + +Call `apply_approvals` before resuming an agent run with extracted approvals. diff --git a/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/index.mdx b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/index.mdx new file mode 100644 index 00000000..fea3c252 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/index.mdx @@ -0,0 +1,20 @@ +--- +title: "ai.agents.ui.ai_sdk" +description: Reference for AI SDK UI message and SSE adapters. +type: reference +summary: Reference for ai.agents.ui.ai_sdk. +--- + +`ai.agents.ui.ai_sdk` converts between AI SDK UI message streams and the Python +runtime. + +## APIs + +- `UIMessage` +- `to_messages` +- `extract_approvals` +- `apply_approvals` +- `to_sse` +- `to_stream` +- `to_ui_messages` +- `UI_MESSAGE_STREAM_HEADERS` diff --git a/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/meta.json b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/meta.json new file mode 100644 index 00000000..f984c94f --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/meta.json @@ -0,0 +1,11 @@ +{ + "title": "ai.agents.ui.ai_sdk", + "description": "Reference for AI SDK UI adapters.", + "pages": [ + "ui-message", + "inbound-messages", + "approvals", + "outbound-stream", + "outbound-messages" + ] +} diff --git a/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/outbound-messages.mdx b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/outbound-messages.mdx new file mode 100644 index 00000000..acd6ce36 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/outbound-messages.mdx @@ -0,0 +1,14 @@ +--- +title: "ai.agents.ui.ai_sdk.to_ui_messages" +description: Convert runtime messages to AI SDK UI messages. +type: reference +summary: Reference for ai.agents.ui.ai_sdk.to_ui_messages. +--- + +`to_ui_messages` converts stored runtime messages back to AI SDK UI messages. + +```python +ui_messages = ai.agents.ui.ai_sdk.to_ui_messages(messages) +``` + +Use it when loading durable conversation history for a UI client. diff --git a/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/outbound-stream.mdx b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/outbound-stream.mdx new file mode 100644 index 00000000..5fb1d7c2 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/outbound-stream.mdx @@ -0,0 +1,20 @@ +--- +title: "ai.agents.ui.ai_sdk outbound stream" +description: Convert agent streams to AI SDK UI stream parts. +type: reference +summary: Reference for to_sse, to_stream, and UI_MESSAGE_STREAM_HEADERS. +--- + +Use outbound stream helpers to send an agent run to an AI SDK UI client. + +```python +async for chunk in ai.agents.ui.ai_sdk.to_sse(agent_stream): + yield chunk +``` + +```python +async for part in ai.agents.ui.ai_sdk.to_stream(agent_stream): + yield part +``` + +Return `UI_MESSAGE_STREAM_HEADERS` on streamed AI SDK UI responses. diff --git a/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/ui-message.mdx b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/ui-message.mdx new file mode 100644 index 00000000..682117a1 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.agents/ui/ai-sdk/ui-message.mdx @@ -0,0 +1,16 @@ +--- +title: "ai.agents.ui.ai_sdk.UIMessage" +description: Parse AI SDK UI messages. +type: reference +summary: Reference for ai.agents.ui.ai_sdk.UIMessage. +--- + +Use `UIMessage` as the request model for AI SDK UI clients. + +```python +class ChatRequest(pydantic.BaseModel): + messages: list[ai.agents.ui.ai_sdk.UIMessage] +``` + +AI SDK UI messages use a `parts` array. Tool parts can carry approval state and +provider metadata. diff --git a/docs/ai-python/content/docs/reference/ai.agents/ui/meta.json b/docs/ai-python/content/docs/reference/ai.agents/ui/meta.json new file mode 100644 index 00000000..35bb2cd9 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.agents/ui/meta.json @@ -0,0 +1,7 @@ +{ + "title": "ai.agents.ui", + "description": "Reference for agent UI adapters.", + "pages": [ + "ai-sdk" + ] +} diff --git a/docs/ai-python/content/docs/reference/ai.providers/ai-gateway/index.mdx b/docs/ai-python/content/docs/reference/ai.providers/ai-gateway/index.mdx new file mode 100644 index 00000000..09161bfb --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.providers/ai-gateway/index.mdx @@ -0,0 +1,63 @@ +--- +title: "ai.providers.ai_gateway" +description: Reference for AI Gateway provider APIs. +type: reference +summary: Reference for ai.providers.ai_gateway. +--- + +`ai.providers.ai_gateway` contains the Vercel AI Gateway provider, protocol, +params, tools, and error mapping. + +```python +model = ai.get_model("gateway:anthropic/claude-sonnet-4") +ids = await ai.get_provider("vercel").list_models() +``` + +## GatewayProvider + +`GatewayProvider` implements `Provider` for Vercel AI Gateway. + +Default configuration uses `AI_GATEWAY_API_KEY`. + +```python +provider = ai.get_provider("vercel") +model = ai.Model(id="anthropic/claude-sonnet-4", provider=provider) +``` + +The provider uses the Gateway v3 protocol by default, owns the Gateway client, +and supports model listing, probing, streaming, and media generation. + +## GatewayV3Protocol + +`GatewayV3Protocol` translates SDK messages, tools, params, and generated files +to the AI Gateway v3 wire format. + +Use it directly only when you need a protocol override. Most code should let +`GatewayProvider` choose the default protocol. + +## GatewayParams + +AI Gateway params configure gateway-specific request behavior. + +Fields: + +- `quota_entity_id` +- `zero_data_retention` +- `hipaa_compliant` +- `disallow_prompt_training` +- `byok` +- `provider_timeouts` + +Pass `GatewayParams` through `InferenceRequestParams.provider_params`. + +## ProviderTimeoutsParams + +`ProviderTimeoutsParams` configures per-provider timeout behavior. + +Fields: + +- `byok`: Per-provider BYOK attempt timeouts in milliseconds. + +## Tools + +Provider-executed AI Gateway tools are documented on the child `tools` page. diff --git a/docs/ai-python/content/docs/reference/ai.providers/ai-gateway/meta.json b/docs/ai-python/content/docs/reference/ai.providers/ai-gateway/meta.json new file mode 100644 index 00000000..302fee68 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.providers/ai-gateway/meta.json @@ -0,0 +1,7 @@ +{ + "title": "ai_gateway", + "description": "Reference for AI Gateway provider APIs.", + "pages": [ + "tools" + ] +} diff --git a/docs/ai-python/content/docs/reference/ai.providers/ai-gateway/tools.mdx b/docs/ai-python/content/docs/reference/ai.providers/ai-gateway/tools.mdx new file mode 100644 index 00000000..9b0227a5 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.providers/ai-gateway/tools.mdx @@ -0,0 +1,19 @@ +--- +title: "tools" +description: Reference for AI Gateway provider-executed tools. +type: reference +summary: Reference for AI Gateway provider tool helpers. +--- + +AI Gateway tool helpers create provider-executed `ai.Tool` declarations. + +## Tools + +- `perplexity_search` +- `parallel_search` + +## Option models + +- `SourcePolicy` +- `Excerpts` +- `FetchPolicy` diff --git a/docs/ai-python/content/docs/reference/ai.providers/anthropic/index.mdx b/docs/ai-python/content/docs/reference/ai.providers/anthropic/index.mdx new file mode 100644 index 00000000..345a1ab9 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.providers/anthropic/index.mdx @@ -0,0 +1,53 @@ +--- +title: "ai.providers.anthropic" +description: Reference for Anthropic-compatible provider APIs. +type: reference +summary: Reference for ai.providers.anthropic. +--- + +`ai.providers.anthropic` contains the Anthropic-compatible provider, protocol, +and provider-executed tools. + +```python +provider = ai.get_provider("anthropic") +model = ai.Model(id="claude-sonnet-4-6", provider=provider) +``` + +The optional upstream Anthropic SDK loads lazily when the provider creates or +uses an SDK client. + +## AnthropicCompatibleProvider + +`AnthropicCompatibleProvider` implements `Provider` for Anthropic-compatible +APIs. + +Default configuration for the `anthropic` provider uses: + +- `ANTHROPIC_API_KEY` +- `ANTHROPIC_BASE_URL` + +Pass `base_url`, `api_key`, or a custom client through `get_provider` when you +need explicit configuration. + +```python +provider = ai.get_provider( + "anthropic", + base_url="https://anthropic.example.com", +) +model = ai.Model(id="claude-sonnet-4-6", provider=provider) +``` + +The provider supports model listing, probing, streaming, provider-executed +tools, and custom Anthropic-compatible clients. + +## AnthropicMessagesProtocol + +`AnthropicMessagesProtocol` translates SDK messages and params to the +Anthropic Messages API wire format. + +The provider uses this protocol by default. Use it directly only when you need +a protocol override. + +## Tools + +Provider-executed Anthropic tools are documented on the child `tools` page. diff --git a/docs/ai-python/content/docs/reference/ai.providers/anthropic/meta.json b/docs/ai-python/content/docs/reference/ai.providers/anthropic/meta.json new file mode 100644 index 00000000..f78384aa --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.providers/anthropic/meta.json @@ -0,0 +1,7 @@ +{ + "title": "anthropic", + "description": "Reference for Anthropic-compatible provider APIs.", + "pages": [ + "tools" + ] +} diff --git a/docs/ai-python/content/docs/reference/ai.providers/anthropic/tools.mdx b/docs/ai-python/content/docs/reference/ai.providers/anthropic/tools.mdx new file mode 100644 index 00000000..dc2fcd16 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.providers/anthropic/tools.mdx @@ -0,0 +1,24 @@ +--- +title: "tools" +description: Reference for Anthropic provider-executed tools. +type: reference +summary: Reference for Anthropic provider tool helpers. +--- + +Anthropic tool helpers create provider-executed `ai.Tool` declarations. + +## Tools + +- `web_search` +- `web_fetch` +- `code_execution` +- `computer_use` +- `text_editor` +- `bash` +- `memory` + +## Option models and constants + +- `UserLocation` +- `Citations` +- `BETA_HEADERS` diff --git a/docs/ai-python/content/docs/reference/ai.providers/index.mdx b/docs/ai-python/content/docs/reference/ai.providers/index.mdx new file mode 100644 index 00000000..2012cfa5 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.providers/index.mdx @@ -0,0 +1,19 @@ +--- +title: "ai.providers" +description: Reference for provider APIs. +type: reference +summary: Reference for ai.providers. +--- + +`ai.get_provider` is the usual entry point for application code. Use +`ai.providers` when you need provider classes, provider protocols, or +provider-specific namespaces. + +## Public APIs + +- `get_provider` +- `Provider` +- `ProviderProtocol` +- `OpenAICompatibleProvider` +- `AnthropicCompatibleProvider` +- `GatewayProvider` diff --git a/docs/ai-python/content/docs/reference/ai.providers/meta.json b/docs/ai-python/content/docs/reference/ai.providers/meta.json new file mode 100644 index 00000000..fdac8296 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.providers/meta.json @@ -0,0 +1,9 @@ +{ + "title": "ai.providers", + "description": "Reference for provider APIs.", + "pages": [ + "ai-gateway", + "openai", + "anthropic" + ] +} diff --git a/docs/ai-python/content/docs/reference/ai.providers/openai/index.mdx b/docs/ai-python/content/docs/reference/ai.providers/openai/index.mdx new file mode 100644 index 00000000..f825b50b --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.providers/openai/index.mdx @@ -0,0 +1,57 @@ +--- +title: "ai.providers.openai" +description: Reference for OpenAI-compatible provider APIs. +type: reference +summary: Reference for ai.providers.openai. +--- + +`ai.providers.openai` contains the OpenAI-compatible provider, protocols, and +provider-executed tools. + +```python +provider = ai.get_provider("openai") +model = ai.Model(id="gpt-5", provider=provider) +``` + +The optional upstream OpenAI SDK loads lazily when the provider creates or uses +an SDK client. + +## OpenAICompatibleProvider + +`OpenAICompatibleProvider` implements `Provider` for OpenAI-compatible APIs. + +Default configuration for the `openai` provider uses: + +- `OPENAI_API_KEY` +- `OPENAI_BASE_URL` + +Pass `base_url`, `api_key`, or a custom client through `get_provider` when you +need explicit configuration. + +```python +provider = ai.get_provider( + "openai", + base_url="http://localhost:11434/v1", +) +model = ai.Model(id="llama3", provider=provider) +``` + +The provider supports model listing, probing, streaming, provider-executed +tools, and custom OpenAI-compatible clients. + +## Protocols + +OpenAI-compatible protocols translate SDK messages and params to OpenAI wire +formats. + +Types: + +- `OpenAIResponsesProtocol` +- `OpenAIChatCompletionsProtocol` + +The provider chooses a default protocol for the configured provider. Use these +classes directly only when you need a protocol override. + +## Tools + +Provider-executed OpenAI tools are documented on the child `tools` page. diff --git a/docs/ai-python/content/docs/reference/ai.providers/openai/meta.json b/docs/ai-python/content/docs/reference/ai.providers/openai/meta.json new file mode 100644 index 00000000..45088200 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.providers/openai/meta.json @@ -0,0 +1,7 @@ +{ + "title": "openai", + "description": "Reference for OpenAI-compatible provider APIs.", + "pages": [ + "tools" + ] +} diff --git a/docs/ai-python/content/docs/reference/ai.providers/openai/tools.mdx b/docs/ai-python/content/docs/reference/ai.providers/openai/tools.mdx new file mode 100644 index 00000000..182cca33 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai.providers/openai/tools.mdx @@ -0,0 +1,28 @@ +--- +title: "tools" +description: Reference for OpenAI provider-executed tools. +type: reference +summary: Reference for OpenAI provider tool helpers. +--- + +OpenAI tool helpers create provider-executed `ai.Tool` declarations. + +## Tools + +- `web_search` +- `web_search_preview` +- `file_search` +- `code_interpreter` +- `image_generation` +- `local_shell` +- `shell` +- `apply_patch` +- `mcp` +- `tool_search` + +## Option models + +- `WebSearchUserLocation` +- `WebSearchFilters` +- `FileSearchRanking` +- `CodeInterpreterContainer` diff --git a/docs/ai-python/content/docs/reference/ai/agent.mdx b/docs/ai-python/content/docs/reference/ai/agent.mdx new file mode 100644 index 00000000..aa941271 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai/agent.mdx @@ -0,0 +1,61 @@ +--- +title: "Agent" +description: Run the default agent loop with tools. +type: reference +summary: Reference for ai.Agent. +--- + +`Agent` streams model output, dispatches Python tools, appends tool results to +history, and repeats until the model returns a final assistant message. + +```python +agent = ai.Agent(tools=[...]) +agent.tools +``` + +`agent.tools` returns a copy of registered executable tools. + +## run + +```python +async with agent.run( + model, + messages, + output_type=None, + params=None, +) as stream: + async for event in stream: + ... +``` + +Arguments: + +- `model`: `ai.Model`. +- `messages`: initial list of `ai.messages.Message`. +- `output_type`: optional Pydantic model for final JSON output. +- `params`: optional `ai.InferenceRequestParams`. + +## AgentStream + +`Agent.run` yields an `AgentStream`. Read the final output after iteration. + +```python +stream.context +stream.messages +stream.output +``` + +## loop + +Override `Agent.loop(context)` to customize control flow. The default loop uses +`ai.stream`, `ToolRunner`, `Context.resolve`, and `Context.add`. + +```python +class CustomAgent(ai.Agent): + async def loop(self, context: ai.Context): + while context.keep_running(): + ... +``` + +Keep messages as the durable boundary. Streams, tool runners, hook futures, and +provider clients are runtime state. diff --git a/docs/ai-python/content/docs/reference/ai/generate.mdx b/docs/ai-python/content/docs/reference/ai/generate.mdx new file mode 100644 index 00000000..79d780d8 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai/generate.mdx @@ -0,0 +1,35 @@ +--- +title: "generate" +description: Generate non-streaming media responses. +type: reference +summary: Reference for ai.generate. +--- + +`ai.generate` calls non-streaming generation APIs, such as image and +video models, and returns a `Message`. + +```python +result = await ai.generate( + model, + [ai.user_message("A watercolor mothership over a quiet city.")], + ai.ImageParams(n=1, aspect_ratio="16:9"), +) +``` + +## Function + +```python +await ai.generate(model, messages, params) +``` + +## Arguments + +- `model`: `ai.Model`. +- `messages`: list of `ai.messages.Message`. +- `params`: `ai.ImageParams` or `ai.VideoParams`. + +## Return value + +`ai.generate` returns an assistant `Message`. Read generated files through +`message.files` or use the normal message helpers for text, reasoning, and +metadata. diff --git a/docs/ai-python/content/docs/reference/ai/get-model.mdx b/docs/ai-python/content/docs/reference/ai/get-model.mdx new file mode 100644 index 00000000..db033605 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai/get-model.mdx @@ -0,0 +1,26 @@ +--- +title: "get_model" +description: Resolve a model reference from an id. +type: reference +summary: Reference for ai.get_model. +--- + +`get_model` resolves a model id to a `Model`. + +```python +model = ai.get_model("anthropic/claude-sonnet-4") +model = ai.get_model("openai:gpt-5") +model = ai.get_model() +``` + +Unprefixed ids route through AI Gateway. Calling `get_model()` with no argument +reads `AI_SDK_DEFAULT_MODEL`. + +## Arguments + +- `model_id`: Optional model id. Provider-prefixed ids use `provider:model`. + Gateway model ids can use `provider/model`. + +## Return value + +Returns `ai.Model`. diff --git a/docs/ai-python/content/docs/reference/ai/get-provider.mdx b/docs/ai-python/content/docs/reference/ai/get-provider.mdx new file mode 100644 index 00000000..f6de5162 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai/get-provider.mdx @@ -0,0 +1,20 @@ +--- +title: "get_provider" +description: Resolve and configure providers. +type: reference +summary: Reference for ai.get_provider. +--- + +`get_provider` resolves a provider by id. + +```python +provider = ai.get_provider("openai") +provider = ai.get_provider( + "openai", + base_url="http://localhost:1234/v1", + api_key="your_access_token_here", +) +``` + +Known providers include AI Gateway, OpenAI-compatible providers, and +Anthropic-compatible providers. diff --git a/docs/ai-python/content/docs/reference/ai/index.mdx b/docs/ai-python/content/docs/reference/ai/index.mdx new file mode 100644 index 00000000..d72ad32f --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai/index.mdx @@ -0,0 +1,359 @@ +--- +title: "ai" +description: "Reference for top-level ai exports." +type: reference +summary: "Full record of names imported directly from ai." +--- + +`ai` is the application-facing namespace. This page mirrors the public names +re-exported from `ai.__all__`. + +```python +import ai + +model = ai.get_model("anthropic/claude-sonnet-4") +messages = [ai.user_message("Hello")] +``` + +## Linked pages + +These top-level exports have their own pages under `ai`. + +- [`stream`](/docs/reference/ai/stream): Call a model and iterate response + events. The same page documents `Stream`. +- [`generate`](/docs/reference/ai/generate): Call non-streaming media + generation models. +- [`get_model`](/docs/reference/ai/get-model): Resolve a model id into a + `Model`. +- [`get_provider`](/docs/reference/ai/get-provider): Resolve and configure a + provider. +- [`@ai.tool`](/docs/reference/ai/tool-decorator): Define executable tools from + Python functions. +- [`Agent`](/docs/reference/ai/agent): Run the default agent loop. + +## Module aliases + +These module aliases are also re-exported from `ai`. + +- [`errors`](/docs/reference/errors): Error classes and HTTP error helpers. +- [`events`](/docs/reference/events): Stream, agent, tool, and hook events. +- [`messages`](/docs/reference/messages): Message and message part models. +- `models`: Model namespace used by the top-level model exports on this page. +- [`mcp`](/docs/reference/mcp): MCP tool loading helpers. +- [`providers`](/docs/reference/ai.providers): Provider classes and + provider-specific namespaces. +- [`tools`](/docs/reference/tools): Model-facing tool schema types. +- [`util`](/docs/reference/util): Async utility helpers. + +## Models + +### Model + +`Model` identifies what to call. Providers own credentials, clients, +endpoints, model listing, and wire translation. + +```python +model = ai.Model(id="gpt-5", provider=provider) +model.id +model.provider +model.protocol +model.with_protocol(protocol) +``` + +`Model` is a lightweight reference. It does not own network state. + +Fields: + +- `id`: Provider model id. +- `provider`: Provider instance. +- `protocol`: Optional provider protocol override. + +Methods: + +- `with_protocol(protocol)`: Return a copy that uses a specific provider + protocol. + +### probe + +`probe` asks the model provider to verify that a model exists and is reachable. + +```python +await ai.probe(model) +``` + +## Providers + +### Provider + +`Provider` is the base class for provider instances. Providers own credentials, +clients, endpoints, model listing, and wire translation. + +```python +provider.name +provider.base_url +provider.api_key +provider.headers +provider.protocol +await provider.list_models() +await provider.probe(model) +``` + +Subclasses implement provider-specific configuration and clients. Application +code usually gets a provider with `get_provider`. + +### ProviderProtocol + +`ProviderProtocol` translates messages, tools, params, and generated files to +provider wire formats. + +Provider instances use a protocol for `stream` and `generate` calls. + +```python +protocol.stream( + client, + model, + messages, + tools=tools, + params=params, + provider=provider.name, +) +await protocol.generate(client, model, messages, params, provider=provider.name) +``` + +## Request Params + +Model params are top-level `ai` types. + +Use `InferenceRequestParams` with `stream` and `Agent.run`. Use `ImageParams` +and `VideoParams` with `generate`. + +```python +params = ai.InferenceRequestParams().with_temperature(0) +async with ai.stream(model, messages, params=params) as stream: + ... +``` + +Request params: + +- `InferenceRequestParams`: Inference request options. +- `ProviderServiceParams`: Provider service tier options. +- `ReasoningParams`: Provider reasoning or thinking options. +- `OutputParams`: Output token, include, verbosity, and reasoning summary + options. +- `CacheParams`: Prompt cache options. +- `ContextManagementParams`: Server-side context management options. +- `TokenThreshold`: Token count used as a trigger threshold. + +Sampling params: + +- `TemperatureSamplerParams` +- `TopKSamplerParams` +- `TopPSamplerParams` +- `MinPSamplerParams` +- `RepetitionPenaltyParams` +- `SeedSamplerParams` +- `RandomSeed` +- `RANDOM` +- `DEFAULT` +- `UNSET` +- `ModelProviderDefault` +- `Unset` + +Tool calling params: + +- `ToolCallingParams` +- `ToolChoiceMode` +- `ToolSelection` +- `ToolRef` + +Routing params: + +- `RoutingParams` +- `RoutingTarget` +- `RoutingTargetChain` +- `GeoRegion` +- `CloudRegion` +- `ProviderRankingStrategy` +- `GLOBAL` + +Media generation params: + +```python +ai.ImageParams( + n=1, + size=None, + aspect_ratio=None, + seed=None, + provider_options={}, +) + +ai.VideoParams( + n=1, + aspect_ratio=None, + resolution=None, + duration=None, + fps=None, + seed=None, + provider_options={}, +) +``` + +## Messages + +Message builders create `Message` values. + +```python +ai.system_message("You are concise.") +ai.user_message("Hello", ai.file_part(data, media_type="image/png")) +ai.assistant_message("Hi") +ai.tool_message(tool_call_id="tc_1", result="done", tool_name="lookup") +``` + +Message builder exports: + +- `system_message` +- `user_message` +- `assistant_message` +- `tool_message` + +Part builders create message part values. + +```python +ai.text_part("hello") +ai.file_part(data, media_type="image/png", filename="image.png") +ai.thinking("reasoning text") +ai.content_output("caption", ai.file_part(png_bytes, media_type="image/png")) +ai.tool_result_part("tc_1", result={"ok": True}, tool_name="lookup") +``` + +Part builder exports: + +- `text_part` +- `file_part` +- `thinking` +- `content_output` +- `tool_result_part` + +## Tools and Agents + +### agent + +`agent` creates an `Agent`. + +```python +agent = ai.agent(tools=[contact_mothership]) +``` + +Arguments: + +- `tools`: Optional `AgentTool` values from `tool` and schema-only `Tool` + declarations for provider-executed tools. + +Returns `Agent`. + +### AgentTool + +`AgentTool` binds a model-facing `Tool` declaration to an executable Python +function. + +```python +tool.name +tool.tool +tool.fn +tool.validator +tool.require_approval +``` + +Pass `AgentTool` values to `agent(tools=[...])`. + +### Context + +Custom loops use `Context` to resolve model tool calls and `ToolRunner` to run +them. + +```python +context.model +context.messages +context.tools +context.output_type +context.params +``` + +Useful methods: + +- `keep_running()`: Return `True` while the last message still needs work. +- `resolve(tool_call)`: Convert model tool call parts into executable + `ToolCall` objects. +- `add(message)`: Append messages to history. + +### ToolCall + +`ToolCall` is the executable runtime object produced by `Context.resolve`. + +```python +tool_call.id +tool_call.name +tool_call.fn +tool_call.kwargs +result = await tool_call() +``` + +### ToolRunner + +`ToolRunner` schedules tool calls and collects their result messages. + +```python +async with ai.ToolRunner() as runner: + runner.schedule(tool_call) + async for result in runner.events(): + ... + message = runner.get_tool_message() +``` + +Use `add_result(result)` when a custom loop executes a tool itself but still +wants the runner to aggregate the result message. + +### Streaming tool aliases + +Async-generator tools can yield partial output while they run. + +- `StreamingTextTool`: Concatenate yielded strings. +- `StreamingStatusTool[T]`: Treat intermediate yields as status updates and + the last yielded value as the final result. +- `SubAgentTool`: Forward nested agent events and use the nested final text as + model input. + +### Tool result helpers + +```python +ai.tool_result(tool_call_id="tc_1", tool_name="lookup", result={"ok": True}) +ai.pending_tool_result(hook_part, tool_call_id="tc_1", tool_name="lookup") +``` + +Exports: + +- `tool_result`: Create a `ToolCallResult`. +- `pending_tool_result`: Create a pending hook placeholder result. + +### Hooks + +Hooks let an agent pause while another process or UI supplies a decision. + +```python +approval = await ai.hook( + "approve_contact_mothership", + payload=ai.tools.ToolApproval, + metadata={"tool": "contact_mothership"}, +) +``` + +Hook exports: + +- `hook`: Emit a pending hook event and wait for a matching resolution. +- `resolve_hook`: Resolve a live or future hook. +- `abort_pending_hook`: Mark a serialized pending hook as aborted. +- `cancel_hook`: Cancel a live hook by label. + +## Errors + +Top-level error exports are documented in [`ai.errors`](/docs/reference/errors). diff --git a/docs/ai-python/content/docs/reference/ai/meta.json b/docs/ai-python/content/docs/reference/ai/meta.json new file mode 100644 index 00000000..56627c36 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai/meta.json @@ -0,0 +1,12 @@ +{ + "title": "ai", + "description": "Reference for top-level ai exports.", + "pages": [ + "stream", + "generate", + "get-model", + "get-provider", + "tool-decorator", + "agent" + ] +} diff --git a/docs/ai-python/content/docs/reference/ai/stream.mdx b/docs/ai-python/content/docs/reference/ai/stream.mdx new file mode 100644 index 00000000..dfb72018 --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai/stream.mdx @@ -0,0 +1,81 @@ +--- +title: "stream" +description: Stream model responses and inspect the aggregated result. +type: reference +summary: Reference for ai.stream, Stream, stream events, aggregation, and replay. +--- + +`ai.stream` calls a model and returns an async context manager whose value is a +`Stream`. + +```python +async with ai.stream(model, messages, tools=tools) as stream: + async for event in stream: + ... +``` + +## Function + +```python +ai.stream( + model, + messages, + *, + tools=None, + output_type=None, + params=None, +) +``` + +You can also pass `context=` from an agent loop: + +```python +async with ai.stream(context=context) as stream: + ... +``` + +Pass either `model` and `messages`, or `context=`, not both. + +## Arguments + +- `model`: `ai.Model`. +- `messages`: list of `ai.messages.Message`. +- `tools`: optional model-facing `ai.Tool` declarations. +- `output_type`: optional Pydantic model for JSON output validation. +- `params`: optional `ai.InferenceRequestParams`. + +## Stream + +`Stream` is an async iterator of `ai.events.Event` values. It aggregates events +into an assistant message while you iterate. + +```python +message = stream.message +text = stream.text +tool_calls = stream.tool_calls +usage = stream.usage +output = stream.output +``` + +`stream.output` returns text by default. When `output_type` is set, it validates +the final text as JSON and returns the parsed Pydantic model. + +## Event Aggregation + +Text and reasoning blocks are built from start, delta, and end events. Tool +calls are built from `ToolStart`, `ToolDelta`, and `ToolEnd`. Provider-executed +tools use built-in tool events. Generated files arrive as `FileEvent`. + +Each yielded event has the current aggregated `event.message`. + +## Replay + +If the last input message has `replay=True`, `ai.stream` does not call the +provider. It emits replay tool-end events from the existing assistant message +so resumable agent flows can dispatch the same tool calls again. + +## Errors + +If the provider stream ends before a finish event, iteration raises +`ai.errors.ProviderIncompleteResponseError`. The partial message is still on +`stream.message`. diff --git a/docs/ai-python/content/docs/reference/ai/tool-decorator.mdx b/docs/ai-python/content/docs/reference/ai/tool-decorator.mdx new file mode 100644 index 00000000..b1062c6b --- /dev/null +++ b/docs/ai-python/content/docs/reference/ai/tool-decorator.mdx @@ -0,0 +1,35 @@ +--- +title: "@ai.tool" +description: Define executable tools from Python functions. +type: reference +summary: Reference for the ai.tool decorator. +--- + +`@ai.tool` turns an async Python function into an executable `AgentTool`. + +```python +@ai.tool +async def contact_mothership(query: str) -> str: + """Contact the mothership.""" + return "Soon." +``` + +## Forms + +```python +@ai.tool +async def name(...) -> Result: ... + +@ai.tool(require_approval=True) +async def name(...) -> Result: ... + +@ai.tool(aggregator=...) +async def name(...) -> AsyncGenerator[Item]: ... +``` + +The function name becomes the tool name. The docstring becomes the tool +description. The function signature becomes a Pydantic validator and JSON +schema. + +For model-facing tool declarations and provider-executed tools, use +[`ai.tools`](/docs/reference/tools). diff --git a/docs/ai-python/content/docs/reference/errors.mdx b/docs/ai-python/content/docs/reference/errors.mdx new file mode 100644 index 00000000..11546e3f --- /dev/null +++ b/docs/ai-python/content/docs/reference/errors.mdx @@ -0,0 +1,70 @@ +--- +title: "ai.errors" +description: Reference for SDK error types. +type: reference +summary: Reference for ai.errors. +--- + +`ai.errors` contains the framework and provider error hierarchy. + +## Base Errors + +Base errors are not tied to a provider HTTP response. + +- `AIError` +- `ConfigurationError` +- `InstallationError` +- `UnsupportedProviderError` + +## Provider Errors + +Provider errors represent failures raised by model providers. + +- `ProviderError` +- `ProviderNotConfiguredError` +- `ProviderAPIError` +- `ProviderConnectionError` +- `ProviderTimeoutError` +- `ProviderResponseError` +- `ProviderIncompleteResponseError` + +`ProviderAPIError` includes `request_id`, `http_context`, `body`, `code`, +`param`, `type`, and `is_retryable`. + +## Provider Status Errors + +Status errors represent provider responses with non-success HTTP status codes. + +- `ProviderStatusError` +- `ProviderBadRequestError` +- `ProviderAuthenticationError` +- `ProviderPermissionDeniedError` +- `ProviderNotFoundError` +- `ProviderModelNotFoundError` +- `ProviderConflictError` +- `ProviderRequestTooLargeError` +- `ProviderUnprocessableEntityError` +- `ProviderRateLimitError` +- `ProviderInternalServerError` +- `ProviderServiceUnavailableError` +- `ProviderDeadlineExceededError` +- `ProviderOverloadedError` + +## HTTPErrorContext + +`HTTPErrorContext` exposes normalized HTTP context from provider errors. + +```python +context.status_code +context.request +context.response +``` + +## http_status_to_provider_status_error_class + +`http_status_to_provider_status_error_class` returns the provider status error +class for an HTTP status code. + +```python +error_cls = ai.errors.http_status_to_provider_status_error_class(429) +``` diff --git a/docs/ai-python/content/docs/reference/events.mdx b/docs/ai-python/content/docs/reference/events.mdx new file mode 100644 index 00000000..59327762 --- /dev/null +++ b/docs/ai-python/content/docs/reference/events.mdx @@ -0,0 +1,98 @@ +--- +title: "ai.events" +description: Reference for event classes and event helpers. +type: reference +summary: Reference for ai.events. +--- + +Streams and agents yield event objects from `ai.events`. This is the public +event-model namespace, and events are Pydantic models. + +## Stream Events + +Model stream events inherit from `BaseEvent`. + +```python +event.message +event.usage +event.provider_metadata +``` + +Text events: + +- `StreamStart` +- `TextStart` +- `TextDelta` +- `TextEnd` +- `StreamEnd` + +Reasoning events: + +- `ReasoningStart` +- `ReasoningDelta` +- `ReasoningEnd` + +Tool events: + +- `ToolStart` +- `ToolDelta` +- `ToolEnd` +- `BuiltinToolStart` +- `BuiltinToolDelta` +- `BuiltinToolEnd` +- `BuiltinToolResult` + +File and hook events: + +- `FileEvent` +- `HookSuspension` +- `HookResolution` + +Stream event unions: + +- `Event` +- `DiscriminatedEvent` + +## Agent Events + +Agents can emit event values that are not raw provider stream events. + +- `ToolCallResult`: Carries the tool result message and result parts. +- `PartialToolCallResult`: Carries values yielded by streaming tools and + `yield_from`. +- `HookEvent`: Carries an internal hook message and `HookPart`. + +Agent event unions: + +- `AgentMessageEvent` +- `AgentEvent` +- `TerminalEvent` + +## Aggregator + +`Aggregator[Item, Result, ModelInput]` is the interface used by streaming tools +and `yield_from`. + +```python +class MyAggregator(ai.events.Aggregator[Item, Result, ModelInput]): + def feed(self, item: Item) -> None: ... + def snapshot(self) -> Result: ... + + @classmethod + def to_model_input(cls, snapshot: Result) -> ModelInput: ... +``` + +`snapshot()` is the rich value stored in tool results. `get_model_input()` is +the value sent back to the model. + +## replay_message_events + +`replay_message_events` synthesizes stream events from a complete `Message`. + +```python +async for event in ai.events.replay_message_events(message): + ... +``` + +Use it when cached, durable, or test messages need to flow through code that +expects stream events. diff --git a/docs/ai-python/content/docs/reference/index.mdx b/docs/ai-python/content/docs/reference/index.mdx new file mode 100644 index 00000000..46c36de7 --- /dev/null +++ b/docs/ai-python/content/docs/reference/index.mdx @@ -0,0 +1,13 @@ +--- +title: "Reference" +description: "Public API reference for the AI SDK for Python." +type: reference +summary: "Look up public APIs by the names you import and call." +--- + +Reference pages are organized by public module. + +Start with `ai` for names imported directly from the top-level package, such +as `ai.stream`, `ai.Agent`, and `ai.Tool`. Use sibling module sections such as +`ai.events`, `ai.messages`, `ai.providers`, and `ai.agents` for public +submodule APIs. diff --git a/docs/ai-python/content/docs/reference/mcp.mdx b/docs/ai-python/content/docs/reference/mcp.mdx new file mode 100644 index 00000000..bffee0b3 --- /dev/null +++ b/docs/ai-python/content/docs/reference/mcp.mdx @@ -0,0 +1,57 @@ +--- +title: "ai.mcp" +description: Reference for MCP tool loading APIs. +type: reference +summary: Reference for ai.mcp. +--- + +`ai.mcp` loads MCP server tools as `AgentTool` values. + +Install the MCP extra before using these helpers: + +```bash +uv add "ai[mcp]" +``` + +## get_stdio_tools + +`get_stdio_tools` starts an MCP server subprocess and returns `AgentTool` +values that can be passed to `ai.agent`. + +```python +tools = await ai.mcp.get_stdio_tools( + "npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp" +) +``` + +Arguments: + +- `command`: Subprocess command. +- `*args`: Subprocess arguments. +- `env`: Optional environment variables. +- `cwd`: Optional working directory. +- `tool_prefix`: Optional prefix for tool names. + +## get_http_tools + +`get_http_tools` connects to an MCP HTTP server and returns `AgentTool` values +that can be passed to `ai.agent`. + +```python +tools = await ai.mcp.get_http_tools("https://example.com/mcp") +``` + +Arguments: + +- `url`: MCP server endpoint. +- `headers`: Optional HTTP headers. +- `tool_prefix`: Optional prefix for tool names. + +Use `tool_prefix` when multiple servers expose overlapping tool names. + +## close_connections + +`close_connections` closes pooled MCP connections for the active context. + +Agent runs clean up MCP connections automatically. Call this helper only when +you manage MCP tool lifetimes manually. diff --git a/docs/ai-python/content/docs/reference/messages.mdx b/docs/ai-python/content/docs/reference/messages.mdx new file mode 100644 index 00000000..9215659b --- /dev/null +++ b/docs/ai-python/content/docs/reference/messages.mdx @@ -0,0 +1,102 @@ +--- +title: "ai.messages" +description: Reference for message models and message parts. +type: reference +summary: Reference for ai.messages. +--- + +`ai.messages` is the public message-model namespace. Messages are Pydantic +models and durable state shared by model calls, agents, tools, UI adapters, +and resume flows. + +## Message + +`Message` carries one conversation item. + +```python +message.role +message.parts +message.id +message.turn_id +message.usage +message.provider_metadata +message.replay +``` + +Roles are `user`, `assistant`, `system`, `tool`, and `internal`. + +Convenience properties: + +```python +message.text +message.reasoning +message.tool_calls +message.tool_results +message.builtin_tool_calls +message.builtin_tool_returns +message.files +message.images +message.videos +message.get_output(output_type=None) +``` + +`get_output()` returns text by default. With a Pydantic model, it validates the +message text as JSON and returns that model. + +## Parts + +Message parts store typed content inside a `Message`. + +Common parts: + +- `TextPart`: Text content. +- `FilePart`: File, image, document, audio, or video content. +- `ReasoningPart`: Model reasoning text. + +Tool and runtime parts: + +- `ToolCallPart`: A host-executed tool call requested by the model. +- `ToolResultPart`: The result for a host-executed tool call. +- `BuiltinToolCallPart`: A provider-executed tool call. +- `BuiltinToolReturnPart`: A provider-executed tool result. +- `HookPart`: A hook suspension, resolution, or cancellation. + +`ToolResultPart.get_model_input()` returns the value sent back to the model. For +most tools this is the same as `result`. Aggregator-backed tools can store a +rich `result` while sending a simpler model-facing value. + +## Message Bundles + +Message bundle types are used when a tool or adapter needs to carry multiple +message-layer values as one result. + +- `MessageBundle`: A tuple of `Message` values. +- `ContentOutput`: A multipart tool result containing text and file parts. +- `ContentPart`: The text-or-file part union used by `ContentOutput`. +- `SpecialToolResult`: The special result union for multipart content and + message bundles. +- `ResultKind`: The coarse tag for a tool result shape: `error`, `json`, or + `special`. + +## Message IDs + +`generate_id` creates SDK-style IDs for messages and parts. + +```python +message_id = ai.messages.generate_id("msg") +``` + +Pass a prefix to control the ID family. If no prefix is supplied, the helper +returns an unprefixed generated ID. + +## Serialization + +Messages serialize through Pydantic. + +```python +encoded = [message.model_dump(mode="json") for message in messages] +restored = [ai.messages.Message.model_validate(item) for item in encoded] +``` + +Persist messages after completed or suspended runs. Recreate providers, hooks, +streams, and other live runtime objects on the next request. diff --git a/docs/ai-python/content/docs/reference/meta.json b/docs/ai-python/content/docs/reference/meta.json new file mode 100644 index 00000000..5ca76191 --- /dev/null +++ b/docs/ai-python/content/docs/reference/meta.json @@ -0,0 +1,16 @@ +{ + "title": "Reference", + "description": "API reference for public AI SDK for Python APIs.", + "pages": [ + "ai", + "ai.providers", + "ai.agents", + "tools", + "messages", + "events", + "errors", + "mcp", + "types", + "util" + ] +} diff --git a/docs/ai-python/content/docs/reference/tools.mdx b/docs/ai-python/content/docs/reference/tools.mdx new file mode 100644 index 00000000..52bca91d --- /dev/null +++ b/docs/ai-python/content/docs/reference/tools.mdx @@ -0,0 +1,67 @@ +--- +title: "ai.tools" +description: Reference for schema tool types and approval payloads. +type: reference +summary: Reference for ai.tools. +--- + +`ai.tools` defines model-facing tool declarations and tool approval payloads. + +Schema-only tools and provider-executed tools are both represented as `ai.Tool` +values. Pass them to `ai.stream(..., tools=[...])` or to an agent when the +provider should receive the declaration. + +Provider-specific built-in tool factories live under provider namespaces such +as `ai.providers.openai.tools`, `ai.providers.anthropic.tools`, and +`ai.providers.ai_gateway.tools`. + +## Tool + +`Tool` is the model-facing declaration used by providers. + +Fields: + +- `kind`: `function` or `provider`. +- `name`: Tool name exposed to the model. +- `spec`: Function tool schema. +- `tool_config`: Provider-executed tool configuration. +- `require_approval`: Whether the tool call needs approval before execution. + +Function tools require `spec`. Provider tools require `tool_config.id` and do +not accept `spec`. + +## ToolSpec + +`ToolSpec` contains the provider-facing schema for a host-executed function +tool. + +Fields: + +- `description` +- `params` + +## ToolConfig + +`ToolConfig` stores provider-facing tool options. + +Fields: + +- `id`: Canonical provider tool id. +- `args`: Provider wire arguments as plain snake_case data. + +Use it with `ai.Tool` when a provider-executed tool needs provider-specific +configuration. + +## ToolApproval + +`ToolApproval` is a Pydantic model used with hooks and UI adapters when a tool +call needs external approval. + +```python +approval = await ai.hook("approve_tool", payload=ai.tools.ToolApproval) +``` + +Fields: + +- `granted` +- `reason` diff --git a/docs/ai-python/content/docs/reference/types.mdx b/docs/ai-python/content/docs/reference/types.mdx new file mode 100644 index 00000000..02e89d95 --- /dev/null +++ b/docs/ai-python/content/docs/reference/types.mdx @@ -0,0 +1,73 @@ +--- +title: "ai.types" +description: "Reference for lower-level public type helper modules." +type: reference +summary: "Reference for public type helpers that are not top-level imports." +--- + +Most message, event, and tool APIs are documented under the root aliases +`ai.messages`, `ai.events`, and `ai.tools`. Use `ai.types` for lower-level +helper modules that do not have a shorter public alias. + +## ai.types.media + +`ai.types.media` contains URL, data URL, media type inference, and magic-byte +detection helpers. + +URL helpers: + +- `is_url` +- `is_downloadable_url` +- `split_data_url` + +Encoding helpers: + +- `data_to_base64` +- `data_to_data_url` + +Media type helpers: + +- `infer_media_type` +- `detect_media_type` +- `detect_image_media_type` +- `detect_audio_media_type` + +## ai.types.integrity + +Before provider calls, the SDK prepares message history. It strips internal +messages, removes non-model parts, repairs invalid tool args to `{}`, and +inserts error results for missing tool calls when possible. + +```python +from ai.types import integrity + + +prepared = integrity.prepare_messages(messages) +``` + +Use strict validation when you want repairable issues to raise. + +```python +integrity.prepare_messages(messages, mode="strict") +``` + +APIs: + +- `prepare_messages` +- `IntegrityError` + +## ai.types.usage + +`Usage` is normalized token usage from a single model call. + +```python +usage.input_tokens +usage.output_tokens +usage.reasoning_tokens +usage.cache_read_tokens +usage.cache_write_tokens +usage.raw +usage.total_tokens +``` + +Use `usage_a + usage_b` to accumulate usage across calls. diff --git a/docs/ai-python/content/docs/reference/util.mdx b/docs/ai-python/content/docs/reference/util.mdx new file mode 100644 index 00000000..7fba1440 --- /dev/null +++ b/docs/ai-python/content/docs/reference/util.mdx @@ -0,0 +1,45 @@ +--- +title: "ai.util" +description: Reference for async utility helpers. +type: reference +summary: Reference for ai.util. +--- + +`ai.util` contains asynchronous utility primitives used by the SDK. + +## merge + +`merge` consumes multiple async iterables concurrently and yields items as they +become available. + +```python +async for event in ai.util.merge(stream, tool_runner.events()): + ... +``` + +## decouple + +`decouple` separates producer and consumer timing for an async iterable. + +Use it when a producer should continue running while another task consumes +items at a different pace. + +## Queues + +Queue primitives: + +- `AsyncIterableQueue` +- `MultiWaiter` + +`AsyncIterableQueue` is an async iterable queue that can be closed. `MultiWaiter` +waits for the first completed item across multiple async sources. + +## Lifecycle Helpers + +Lifecycle helpers: + +- `unwrap_generator_exit` +- `maybe_aclosing` + +Use these helpers when implementing custom async generators or safely closing +optional async resources. diff --git a/skills/ai-python-basics/SKILL.md b/skills/ai-python-basics/SKILL.md new file mode 100644 index 00000000..fe62c06e --- /dev/null +++ b/skills/ai-python-basics/SKILL.md @@ -0,0 +1,96 @@ +--- +name: ai-python-basics +description: Use for AI SDK for Python basics. Configure a model, make messages, stream, declare tools, build a basic agent. +metadata: + sdk-version: "0.2.1" +--- + +# ai-python-basics + +Requires Python 3.12 or later. Install with `uv add ai`. + +Use `import ai`. + +For gateway-routed model IDs, set `AI_GATEWAY_API_KEY`. + +Core pieces: + +- `Model` selects the provider and model. +- Messages are typed Python objects. +- `ai.stream` makes one model call and returns one assistant message. +- `ai.Agent` wraps `ai.stream` in a loop that executes Python tools and manages + history. + +Use gateway model IDs unless you need a direct provider: + +```python +model = ai.get_model("anthropic/claude-sonnet-4") +``` + +Direct providers need extras: + +```bash +uv add "ai[openai]" +uv add "ai[anthropic]" +``` + +Messages are typed Python objects: + +```python +messages = [ + ai.system_message("Be concise."), + ai.user_message("Write a haiku about rain."), +] +``` + +Minimal agent happy path: + +```python +import asyncio + +import ai + + +@ai.tool +async def get_weather(city: str) -> str: + """Get the weather for a city.""" + return "Sunny" + + +async def main() -> None: + model = ai.get_model("anthropic/claude-sonnet-4") + agent = ai.agent(tools=[get_weather]) + messages = [ + ai.system_message("Use tools when useful."), + ai.user_message("What is the weather in San Francisco?"), + ] + + async with agent.run(model, messages) as run: + async for event in run: + if isinstance(event, ai.events.TextDelta): + print(event.chunk, end="", flush=True) + + answer = run.output + history = run.messages + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +For one model call without Python tool execution, use `ai.stream`: + +```python +async with ai.stream(model, messages) as stream: + async for event in stream: + if isinstance(event, ai.events.TextDelta): + print(event.chunk, end="", flush=True) + +answer = stream.output +messages.append(stream.message) +``` + +Use `ai-python-custom-loop`, `ai-python-subagents`, +`ai-python-streaming-tools`, `ai-python-serverless-execution`, +`ai-python-durable-execution`, `ai-python-ui-adapter`, and +`ai-python-custom-provider` for advanced patterns. diff --git a/skills/ai-python-custom-loop/SKILL.md b/skills/ai-python-custom-loop/SKILL.md new file mode 100644 index 00000000..373258cb --- /dev/null +++ b/skills/ai-python-custom-loop/SKILL.md @@ -0,0 +1,43 @@ +--- +name: ai-python-custom-loop +description: Use when building custom agent loops. Modify tool dispatch, history management, hooks, control flow. +metadata: + sdk-version: "0.2.1" +--- + +# ai-python-custom-loop + +Keep the default shape unless you must change control flow: + +```python +class MyAgent(ai.Agent): + async def loop(self, context: ai.Context): + while context.keep_running(): + async with ( + ai.stream(context=context) as stream, + ai.ToolRunner() as runner, + ): + async for event in ai.util.merge(stream, runner.events()): + yield event + + if isinstance(event, ai.events.ToolEnd): + runner.schedule(context.resolve(event.tool_call)) + + context.add(stream.message) + context.add(runner.get_tool_message()) +``` + +Rules: + +- Call `context.keep_running()` at the top of each turn. +- Use `ai.stream(context=context)` so model, messages, tools, output type, and params stay together. +- Yield events from the loop. `Agent.run` hides replay events from callers. +- On `ToolEnd`, use `context.resolve(event.tool_call)`. It handles validation, approval gates, and cached replay results. +- Do not call `tool.fn` directly unless you also handle validation, approvals, and cached results. +- Schedule resolved calls with `ToolRunner.schedule(...)`. +- `ToolRunner.schedule(...)` also accepts a zero-arg async callable that returns `ai.events.ToolCallResult`. +- If you make a result yourself, use `runner.add_result(ai.tool_result(...))`. +- Add `stream.message`, then `runner.get_tool_message()`. `context.add(...)` skips replay messages. +- Every tool call must get one tool result. +- For hooks, let `context.resolve(...)` build the gated call. Use `ai-python-serverless-execution` for request boundaries. +- For durable calls, keep this shape and wrap only model or tool I/O. Use `ai-python-durable-execution`. diff --git a/skills/ai-python-custom-provider/SKILL.md b/skills/ai-python-custom-provider/SKILL.md new file mode 100644 index 00000000..ce8d3f0d --- /dev/null +++ b/skills/ai-python-custom-provider/SKILL.md @@ -0,0 +1,95 @@ +--- +name: ai-python-custom-provider +description: Use for implementing custom providers in AI SDK for Python. +metadata: + sdk-version: "0.2.1" +--- + +# ai-python-custom-provider + +Providers emit model events. They do not run Python tools. `ai.stream` collects +events into a `Message`. `ai.Agent` adds tool execution, hooks, and replay. + +Minimal shape: + +```python +from collections.abc import AsyncGenerator, Sequence +from typing import Any, Literal + +import pydantic +import ai + + +class MyProtocol(ai.ProviderProtocol[Any]): + protocol_class_id: Literal["my_protocol"] = "my_protocol" + + def stream( + self, + client: Any, + model: ai.Model, + messages: list[ai.messages.Message], + *, + tools: Sequence[ai.tools.Tool] | None = None, + output_type: type[pydantic.BaseModel] | None = None, + params: ai.InferenceRequestParams | None = None, + provider: str, + ) -> AsyncGenerator[ai.events.Event]: + return self._stream(client, model, messages, tools=tools) + + async def _stream( + self, + client: Any, + model: ai.Model, + messages: list[ai.messages.Message], + *, + tools: Sequence[ai.tools.Tool] | None, + ) -> AsyncGenerator[ai.events.Event]: + yield ai.events.StreamStart() + yield ai.events.TextStart(block_id="text") + yield ai.events.TextDelta(block_id="text", chunk="Hello") + yield ai.events.TextEnd(block_id="text") + yield ai.events.StreamEnd() + + +class MyProvider(ai.Provider[Any]): + provider_class_id: Literal["my_provider"] = "my_provider" + name: str = "my" + default_base_url: str = "https://example.invalid" + + def __init__(self, *, client: Any) -> None: + super().__init__() + self._set_client(client) + + def default_protocol(self) -> ai.ProviderProtocol[Any]: + return MyProtocol() + + async def list_models(self) -> list[str]: + return ["my-model"] + + async def probe(self, model: ai.Model) -> None: + return None + + +model = ai.Model(id="my-model", provider=MyProvider(client=client)) +``` + +For Python tool calls, emit `ToolStart`, `ToolDelta`, and `ToolEnd`: + +```python +yield ai.events.ToolStart(tool_call_id=tcid, tool_name=name) +yield ai.events.ToolDelta(tool_call_id=tcid, chunk=args_json) +yield ai.events.ToolEnd( + tool_call_id=tcid, + tool_call=ai.messages.DUMMY_TOOL_CALL, +) +``` + +The stream collector fills `event.tool_call` with the aggregated tool call. +Then `Agent` resolves and runs the tool. + +If the provider runs its own built-in tool, emit `BuiltinToolStart`, +`BuiltinToolDelta`, `BuiltinToolEnd`, and `BuiltinToolResult` instead. + +Do not implement a custom provider for normal app configuration. Prefer +`ai.get_provider(...)`, `ai.get_model(...)`, or a protocol override unless you +are adding a new upstream API adapter. diff --git a/skills/ai-python-durable-execution/SKILL.md b/skills/ai-python-durable-execution/SKILL.md new file mode 100644 index 00000000..d16c5cfc --- /dev/null +++ b/skills/ai-python-durable-execution/SKILL.md @@ -0,0 +1,107 @@ +--- +name: ai-python-durable-execution +description: Use when adding durable execution to AI SDK for Python, building durable agent loops, or serializing messages across workflow steps. +metadata: + sdk-version: "0.2.1" +--- + +# ai-python-durable-execution + +Use durable execution when an agent run must survive restarts, worker moves, or +long waits. + +The SDK does not provide durability by itself. Build a custom `Agent.loop`, and +put side effects inside durable steps: + +- model calls +- tool I/O +- approval or resume boundaries + +Keep the workflow replayable. Durable steps should take JSON inputs and return +JSON outputs. + +Serialize messages like this: + +```python +data = message.model_dump(mode="json") +message = ai.messages.Message.model_validate(data) +``` + +## Model Step + +A durable model step should drain `ai.stream(...)` inside the step and return one +complete assistant `Message`. + +```python +@workflow.step +async def llm_step( + model_data: dict[str, object], + messages_data: list[dict[str, object]], + tools_data: list[dict[str, object]], +) -> dict[str, object]: + model = ai.Model.model_validate(model_data) + messages = [ + ai.messages.Message.model_validate(message) + for message in messages_data + ] + tools = [ai.Tool.model_validate(tool) for tool in tools_data] + + async with ai.stream(model, messages, tools=tools) as stream: + async for _event in stream: + pass + + if stream.message is None: + raise RuntimeError("LLM stream ended without a message") + + return stream.message.model_dump(mode="json") +``` + +## Durable Tools + +Prefer wrapping the tool body in the durable step: + +```python +@ai.tool +@workflow.step +async def ask_mothership(question: str) -> str: + response = await mothership_client.ask(question) + return response.summary +``` + +If the workflow system needs separate activity dispatch, schedule a zero-arg +callable that returns `ai.tool_result(...)`. Do not call `tool.fn` directly. + +## Agent Loop + +Use the model step result as a complete message. Do not wrap it in `ai.Stream`, +`ai.events.replay_message_events`, or `ai.util.merge`, those utilities are +used for fluent dispatch in non-durable applications, which is impossible +in a workflow setting since streams are considered side-effects. + +```python +class DurableAgent(ai.Agent): + async def loop(self, context: ai.Context): + while context.keep_running(): + result = await llm_step( + context.model.model_dump(mode="json"), + [m.model_dump(mode="json") for m in context.messages], + [t.model_dump(mode="json") for t in context.tools], + ) + + assistant_message = ai.messages.Message.model_validate(result) + context.add(assistant_message) + + async with ai.ToolRunner() as runner: + for tool_call in assistant_message.tool_calls: + runner.schedule(context.resolve(tool_call)) + + async for event in runner.events(): + yield event + + context.add(runner.get_tool_message()) +``` + +This pattern does not stream model tokens to the caller. That is usually the +right tradeoff for durable workflows, because many durable systems do not support +async generators. You can build a queue-based side channel for streaming; however, +that kind of stream can't be used to dispatch tools and affect control flow directly. diff --git a/skills/ai-python-serverless-execution/SKILL.md b/skills/ai-python-serverless-execution/SKILL.md new file mode 100644 index 00000000..3753266d --- /dev/null +++ b/skills/ai-python-serverless-execution/SKILL.md @@ -0,0 +1,100 @@ +--- +name: ai-python-serverless-execution +description: Use when building serverless AI SDK for Python endpoints, handling hook approvals, aborting pending hooks, or resuming runs across requests. +metadata: + sdk-version: "0.2.1" +--- + +# ai-python-serverless-execution + +Use this when working in a serverless setup, e.g. Vercel Fluid Compute. + +The only major difference in serverless is processing tool approvals +and other hooks. Since you can't keep the hook future alive, you need +to stop the run, save messages, then start a later request with the +hook resolution pre-registered. + +## Tool Approval + +Mark approval-gated tools with `require_approval=True`: + +```python +@ai.tool(require_approval=True) +async def delete_file(path: str) -> str: + return f"Deleted {path}" +``` + +## First Request + +When a pending hook appears, send it to the client and call +`ai.abort_pending_hook(...)`. + +Keep draining the stream. Do not break after the first hook. This lets sibling +tools finish or get marked pending, and makes `stream.messages` complete. + +```python +pending_hooks = [] + +async with agent.run(model, messages) as stream: + async for event in stream: + if ( + isinstance(event, ai.events.HookEvent) + and event.hook.status == "pending" + ): + pending_hooks.append(event.hook) + ai.abort_pending_hook(event.hook) + + yield event + +saved_messages = [ + message.model_dump(mode="json") + for message in stream.messages +] +save_messages(saved_messages) +save_pending_hook_ids([hook.hook_id for hook in pending_hooks]) +``` + +## Resume Request + +Load the saved messages, pre-register hook resolutions, then call `agent.run`. + +```python +messages = [ + ai.messages.Message.model_validate(message) + for message in load_messages() +] + +for approval in approvals: + ai.resolve_hook( + approval.hook_id, + ai.tools.ToolApproval( + granted=approval.granted, + reason=approval.reason, + ), + ) + +async with agent.run(model, messages) as stream: + async for event in stream: + yield event + +save_messages([ + message.model_dump(mode="json") + for message in stream.messages +]) +``` + +Call `ai.resolve_hook(...)` before `agent.run(...)`. Do not ask the model to +make the tool call again. + +`Agent.run` prepares saved interrupted messages for replay. Completed sibling +tool results are reused, pending hooks receive the pre-registered resolution, +and replay-only events are hidden from the caller. + +## Rules + +- Use normal `agent.run(...)`; serverless resume usually does not need a custom loop. +- If you do write a custom loop, use `context.resolve(...)`, `ToolRunner`, and + `context.add(...)` so approvals and replay keep working. +- For custom hooks, pre-register with `ai.resolve_hook(hook_id, data, payload=PayloadType)`. +- For AI SDK UI clients, use `ai-python-ui-adapter` for message conversion, + approval responses, and SSE. diff --git a/skills/ai-python-streaming-tools/SKILL.md b/skills/ai-python-streaming-tools/SKILL.md new file mode 100644 index 00000000..b305fb12 --- /dev/null +++ b/skills/ai-python-streaming-tools/SKILL.md @@ -0,0 +1,117 @@ +--- +name: ai-python-streaming-tools +description: Use for AI SDK for Python async-generator tools, streaming tool output, subagent tools, PartialToolCallResult events, and custom tool aggregation. +metadata: + sdk-version: "0.2.1" +--- + +# ai-python-streaming-tools + +Use async-generator tools when a tool should show progress while it runs. + +A streaming tool yields many values, but the agent still needs one final tool +result for the next model turn. The return type tells the SDK how to combine +those yields. + +## Text Chunks + +Use `ai.StreamingTextTool` when yielded strings should concatenate into the +final tool result. + +```python +@ai.tool +async def draft_reply(topic: str) -> ai.StreamingTextTool: + yield "Checking records for " + yield topic +``` + +The caller sees each yield as `ai.events.PartialToolCallResult`. The model later +sees one string: `"Checking records for {topic}"`. + +## Progress Then Result + +Use `ai.StreamingStatusTool[T]` when the last yielded value is the tool result. + +```python +@ai.tool +async def ask_mothership(question: str) -> ai.StreamingStatusTool[str]: + yield "connecting" + yield "transmitting" + yield f"The mothership says: {question} is under review." +``` + +The progress values stream to the caller. The model sees only the final yielded +value. + +## Subagents + +Use `ai.SubAgentTool` when a tool runs another agent and streams its events. + +```python +@ai.tool +async def research(topic: str) -> ai.SubAgentTool: + researcher = ai.agent() + + messages = [ + ai.system_message("Research briefly."), + ai.user_message(topic), + ] + + async with researcher.run(model, messages) as stream: + async for event in stream: + yield event +``` + +The parent caller receives nested events as `PartialToolCallResult.value`. + +```python +async with agent.run(model, messages) as stream: + async for event in stream: + if isinstance(event, ai.events.PartialToolCallResult): + if isinstance(event.value, ai.events.TextDelta): + yield event.value.chunk +``` + +`SubAgentTool` stores a typed `MessageBundle` as the tool result. The parent +model sees the nested agent's final assistant text, not the raw bundle. + +When saving history, keep the typed message data: + +```python +data = message.model_dump(mode="json") +message = ai.messages.Message.model_validate(data) +``` + +Do not stringify `MessageBundle` or drop `result_kind`. + +## Custom Aggregation + +Prefer the aliases above. If you need custom aggregation, use either +`@ai.tool(aggregator=...)` or an `Annotated` return type. Do not use both. + +```python +from collections.abc import AsyncGenerator +from typing import Annotated + +JoinedLines = Annotated[ + AsyncGenerator[str], + ai.agents.Aggregate(ai.agents.ConcatAggregator, delim="\n"), +] + + +@ai.tool +async def outline(topic: str) -> JoinedLines: + yield f"# {topic}" + yield "- first point" +``` + +Custom aggregators implement `ai.events.Aggregator`. + +## Rules + +- Streaming tools must be async generators. +- Every streaming tool needs an aggregator, usually from the return type alias. +- Consume live output from `ai.events.PartialToolCallResult`. +- The final aggregated value is sent back to the model as a normal tool result. +- In custom agent loops, keep `ToolRunner` events flowing; otherwise partial + tool output will not reach the caller. diff --git a/skills/ai-python-subagents/SKILL.md b/skills/ai-python-subagents/SKILL.md new file mode 100644 index 00000000..7359c00f --- /dev/null +++ b/skills/ai-python-subagents/SKILL.md @@ -0,0 +1,42 @@ +--- +name: ai-python-subagents +description: Use for the subagent-as-a-tool pattern. +metadata: + sdk-version: "0.2.1" +--- + +# ai-python-subagents + +Use a subagent tool when the parent model should choose when to call another +agent. + +```python +@ai.tool +async def research(topic: str) -> ai.SubAgentTool: + child = ai.agent(tools=[lookup]) + messages = [ + ai.system_message("Research briefly."), + ai.user_message(topic), + ] + + async with child.run(model, messages) as stream: + async for event in stream: + yield event +``` + +The parent model sees the child agent's final assistant text as the tool +result. The caller sees child events as `ai.events.PartialToolCallResult`. + +Render nested output from `event.value`: + +```python +if isinstance(event, ai.events.PartialToolCallResult): + if isinstance(event.value, ai.events.TextDelta): + print(event.value.chunk, end="", flush=True) +``` + +Do not append child messages to the parent history yourself. The tool result +stores the child transcript as a `MessageBundle`. + +For `MessageBundle`, preliminary output, and streaming tool details, use +`ai-python-streaming-tools`. diff --git a/skills/ai-python-ui-adapter/SKILL.md b/skills/ai-python-ui-adapter/SKILL.md new file mode 100644 index 00000000..4a0b5df3 --- /dev/null +++ b/skills/ai-python-ui-adapter/SKILL.md @@ -0,0 +1,65 @@ +--- +name: ai-python-ui-adapter +description: Use when connecting AI SDK for Python streams to AI SDK UI useChat clients. +metadata: + sdk-version: "0.2.1" +--- + +# ai-python-ui-adapter + +Frontend: + +```tsx +const chat = useChat({ + transport: new DefaultChatTransport({ api: "/api/chat" }), + sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses, +}); +``` + +Use `chat.sendMessage(...)` to send user input. Use +`chat.addToolApprovalResponse(...)` from approval buttons. + +Backend request: + +```python +class ChatRequest(pydantic.BaseModel): + messages: list[ai.agents.ui.ai_sdk.UIMessage] + +messages, approvals = ai.agents.ui.ai_sdk.to_messages(request.messages) +ai.agents.ui.ai_sdk.apply_approvals(approvals) +``` + +Backend stream: + +```python +async def body(): + async with agent.run(model, messages) as stream: + async def events(): + async for event in stream: + if ( + isinstance(event, ai.events.HookEvent) + and event.hook.status == "pending" + ): + ai.abort_pending_hook(event.hook) + yield event + + async for chunk in ai.agents.ui.ai_sdk.to_sse(events()): + yield chunk + +return StreamingResponse( + body(), + headers=ai.agents.ui.ai_sdk.UI_MESSAGE_STREAM_HEADERS, +) +``` + +The adapter handles `UIMessage` parsing, message IDs, tool state, approvals, +subagent `MessageBundle` values, and AI SDK UI stream events. + +You handle the HTTP route, auth, storage, session lookup, frontend rendering, +and when to abort pending hooks. + +For saved UI history, use: + +```python +ui_messages = ai.agents.ui.ai_sdk.to_ui_messages(messages) +``` diff --git a/skills/ai/SKILL.md b/skills/ai/SKILL.md deleted file mode 100644 index 885a58d9..00000000 --- a/skills/ai/SKILL.md +++ /dev/null @@ -1,503 +0,0 @@ ---- -name: ai -description: Python `ai` SDK — models, providers, streams, events, tools, agents, hooks, MCP, AI SDK UI, structured output, and media generation ---- - -# ai - -Use this skill when working with the Python `ai` SDK. - -```bash -uv add ai -``` - -Direct OpenAI-compatible and Anthropic-compatible providers require optional -extras: `uv add "ai[openai]"` or `uv add "ai[anthropic]"`. AI Gateway works -with the base package. - -```python -import ai -``` - -## Quick start - -```python -import asyncio -import ai - - -@ai.tool -async def get_weather(city: str) -> str: - """Get current weather for a city.""" - return f"Sunny, 72F in {city}" - - -async def main() -> None: - model = ai.get_model("gateway:anthropic/claude-sonnet-4") - agent = ai.agent(tools=[get_weather]) - - messages = [ - ai.system_message("You are a helpful weather assistant."), - ai.user_message("What's the weather in Tokyo?"), - ] - - async with agent.run(model, messages) as stream: - async for event in stream: - if isinstance(event, ai.events.TextDelta): - print(event.chunk, end="", flush=True) - - print(stream.output) - - -if __name__ == "__main__": - asyncio.run(main()) -``` - -`ai.stream(...)` and `agent.run(...)` are async context managers. Iterate events -inside the context. After iteration, read final state from the stream object. - -## Models and providers - -```python -model = ai.get_model() # reads AI_SDK_DEFAULT_MODEL -model = ai.get_model("anthropic/claude-sonnet-4") # unprefixed: gateway route -model = ai.get_model("gateway:anthropic/claude-sonnet-4") -model = ai.get_model("openai:gpt-5.4") # direct provider route -model = ai.get_model("anthropic:claude-sonnet-4-6") -``` - -- Gateway credentials use `AI_GATEWAY_API_KEY`. -- Direct providers use provider-specific env vars such as `OPENAI_API_KEY` and - `ANTHROPIC_API_KEY`. -- Use `ai.get_provider(...)` when you need a custom base URL, API key, headers, - or client. -- Use `await ai.probe(model)` to check credentials and model availability. - -```python -provider = ai.get_provider( - "openai", - base_url="http://localhost:1234/v1", - api_key="your_access_token_here", -) -model = ai.Model("local-model", provider=provider) - -models = await ai.get_provider("anthropic").list_models() -``` - -Request-scoped provider options go through `params`: - -```python -params = { - "providerOptions": { - "gateway": {"sort": "cost"}, - "anthropic": {"speed": "fast"}, - } -} - -async with ai.stream(model, messages, params=params) as stream: - async for event in stream: - ... -``` - -## Messages and events - -Messages are Pydantic models with typed parts. Use builders for common roles -and parts: - -```python -ai.system_message("Be concise.") -ai.user_message("Describe this image:", ai.file_part(image_bytes, media_type="image/png")) -ai.assistant_message(ai.thinking("scratchpad"), "Final answer") -ai.tool_result_part("tc-1", result={"temp": 72}, tool_name="get_weather") -ai.tool_message(tool_call_id="tc-1", result=72, tool_name="get_weather") -``` - -Common message properties: - -- `message.text`, `message.reasoning`. -- `message.tool_calls`, `message.tool_results`. -- `message.builtin_tool_calls`, `message.builtin_tool_returns`. -- `message.files`, `message.images`, `message.videos`. -- `message.get_output()` or `message.get_output(MyModel)`. - -Streams and agents yield event objects from `ai.events`: - -```python -async with ai.stream(model, messages, tools=tools) as stream: - async for event in stream: - if isinstance(event, ai.events.TextDelta): - print(event.chunk, end="", flush=True) - elif isinstance(event, ai.events.ToolEnd): - print(event.tool_call.tool_name, event.tool_call.tool_args) - elif isinstance(event, ai.events.ToolCallResult): - for result in event.results: - print(result.tool_name, result.result) - elif isinstance(event, ai.events.HookEvent): - print(event.hook.hook_id, event.hook.status) - elif isinstance(event, ai.events.PartialToolCallResult): - print(event.label, event.value) -``` - -After iteration: - -```python -stream.message # final assistant message for ai.stream -stream.messages # updated agent history for agent.run -stream.text # text output for ai.stream -stream.output # text or parsed Pydantic output -stream.tool_calls # function tool calls from ai.stream -stream.usage # latest reported usage -``` - -Serialize and restore history with Pydantic JSON: - -```python -encoded = [message.model_dump(mode="json") for message in stream.messages] -restored = [ai.messages.Message.model_validate(item) for item in encoded] -``` - -## Direct streaming - -Use `ai.stream` when you want one model response and will handle any function -tool calls yourself: - -```python -async with ai.stream(model, messages, tools=[get_weather.tool]) as stream: - async for event in stream: - if isinstance(event, ai.events.TextDelta): - print(event.chunk, end="", flush=True) - -for call in stream.tool_calls: - print(call.tool_name, call.tool_args) -``` - -Use structured output with a Pydantic model: - -```python -import pydantic - - -class Forecast(pydantic.BaseModel): - city: str - temperature: float - - -async with ai.stream(model, messages, output_type=Forecast) as stream: - async for event in stream: - ... - -forecast = stream.output -``` - -## Tools - -A function tool is an async Python function decorated with `@ai.tool`. The -function name becomes the tool name, the docstring becomes the description, and -the signature becomes a Pydantic-validated JSON schema. - -```python -@ai.tool -async def scan_sector(sector: str, depth: int = 1) -> str: - """Scan a sector at the requested depth.""" - return f"{sector}: clear at depth {depth}" -``` - -Use schema-only tools with `ai.stream` when the SDK should not execute them: - -```python -tool = ai.Tool( - kind="function", - name="get_weather", - args=ai.tools.FunctionToolArgs( - description="Get current weather for a city.", - params={ - "type": "object", - "properties": {"city": {"type": "string"}}, - "required": ["city"], - }, - ), -) -``` - -Provider-executed tools run outside your process: - -```python -tools = [ai.providers.anthropic.tools.web_search(max_uses=3)] - -async with ai.stream(model, messages, tools=tools) as stream: - async for event in stream: - if isinstance(event, ai.events.BuiltinToolResult): - print(event.result.tool_name, event.result.result) -``` - -Tool validation failures and exceptions become `ToolCallResult` events with -error result parts. The original exception is on `event.exception` for logging. - -```python -if isinstance(event, ai.events.ToolCallResult) and event.exception: - log_exception(event.exception) -``` - -## Streaming tools - -Async-generator tools yield partial values while they run. An aggregator turns -those values into the final tool result the model sees. - -```python -@ai.tool -async def draft_reply(topic: str) -> ai.StreamingTextTool: - """Draft a reply.""" - yield "Checking " - yield f"records for {topic}." -``` - -```python -@ai.tool -async def fetch(url: str) -> ai.StreamingStatusTool[str]: - """Fetch a URL with status updates.""" - yield "connecting" - yield "downloading" - yield body # last yield is the tool result -``` - -```python -@ai.tool -async def research(topic: str) -> ai.SubAgentTool: - """Research a topic with a subagent.""" - subagent = ai.agent(tools=[...]) - async with subagent.run(model, [ai.user_message(topic)]) as stream: - async for event in stream: - yield event -``` - -For custom aggregation, annotate an async-generator return type with -`Annotated[AsyncGenerator[T], ai.agents.Aggregate(...)]`. Built-in -aggregators: `ai.agents.ConcatAggregator`, `ai.agents.LastAggregator`, and -`ai.agents.MessageAggregator`. - -## Agents - -Use an agent when the SDK should execute Python tools, append tool results, and -continue until the assistant returns a final answer. - -```python -agent = ai.agent(tools=[get_weather]) - -async with agent.run(model, messages) as stream: - async for event in stream: - if isinstance(event, ai.events.TextDelta): - print(event.chunk, end="", flush=True) - -history = stream.messages -answer = stream.output -``` - -Pass structured output and provider params through `agent.run`: - -```python -async with agent.run( - model, - [ai.user_message("Return a JSON forecast.")], - output_type=Forecast, - params={"temperature": 0}, -) as stream: - async for event in stream: - ... - -forecast = stream.output -``` - -## Custom agent loops - -Subclass `ai.Agent` and override `loop` for custom scheduling, routing, -logging, persistence, or approval logic. - -```python -from collections.abc import AsyncGenerator - - -class CustomAgent(ai.Agent): - async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.AgentEvent]: - while context.keep_running(): - async with ( - ai.stream(context=context) as stream, - ai.ToolRunner() as tool_runner, - ): - async for event in ai.util.merge(stream, tool_runner.events()): - yield event - - if isinstance(event, ai.events.ToolEnd): - tool_call = context.resolve(event.tool_call) - tool_runner.schedule(tool_call) - - context.add(stream.message) - context.add(tool_runner.get_tool_message()) -``` - -Loop helpers: `context.model`, `context.messages`, `context.tools`, -`context.output_type`, `context.params`, `context.resolve(...)`, -`context.keep_running()`, and `context.add(...)`. - -## Multi-agent - -Use `ai.SubAgentTool` for agent-as-tool workflows. Use `ai.yield_from(...)` -inside custom loops to fan out streams and forward nested events as -`PartialToolCallResult` values with labels. - -```python -async with ( - researcher.run(model, research_messages) as research_stream, - analyst.run(model, analyst_messages) as analyst_stream, -): - research_text, analyst_text = await asyncio.gather( - ai.yield_from( - research_stream, - label="researcher", - aggregator=ai.agents.MessageAggregator, - ), - ai.yield_from( - analyst_stream, - label="analyst", - aggregator=ai.agents.MessageAggregator, - ), - ) -``` - -Route labels in the consumer: - -```python -if isinstance(event, ai.events.PartialToolCallResult): - if event.label == "researcher": - route_research(event.value) -``` - -## Hooks - -Hooks are runtime suspension points. Tool approvals are the built-in workflow. - -```python -@ai.tool(require_approval=True) -async def delete_file(path: str) -> str: - """Delete a file.""" - ... -``` - -The default loop gates each call behind an approval hook with label -`approve_{tool_call_id}` and payload `ai.tools.ToolApproval`. - -```python -async with agent.run(model, messages) as stream: - async for event in stream: - if isinstance(event, ai.events.HookEvent) and event.hook.status == "pending": - ai.resolve_hook( - event.hook.hook_id, - ai.tools.ToolApproval(granted=True, reason="approved"), - ) -``` - -Resolve with `granted=False` to deny the call and return an error tool result. - -Manual hooks block until resolved in live flows: - -```python -approval = await ai.hook( - "approve_send_email", - payload=ai.tools.ToolApproval, - metadata={"tool": "send_email"}, -) -``` - -Resolve or cancel from another task, request handler, or UI callback: - -```python -ai.resolve_hook("approve_send_email", {"granted": True, "reason": "approved"}) -await ai.cancel_hook("approve_send_email", reason="client disconnected") -``` - -Hooks emit `HookEvent` objects. Their messages use `role="internal"` and contain -`HookPart` values. - -Serverless resume flow: - -```python -async with agent.run(model, messages) as stream: - async for event in stream: - if isinstance(event, ai.events.HookEvent) and event.hook.status == "pending": - ai.abort_pending_hook(event.hook) - yield event - -persist(stream.messages) - -# Later, restore messages, pre-register the resolution, and rerun. -ai.resolve_hook(hook_id, ai.tools.ToolApproval(granted=True, reason="approved")) -``` - -## MCP - -MCP adapters return `AgentTool` objects usable in `ai.agent(...)`. - -```python -tools = await ai.mcp.get_http_tools( - "https://mcp.example.com/mcp", - headers={"Authorization": "Bearer token"}, - tool_prefix="docs", -) - -tools = await ai.mcp.get_stdio_tools( - "npx", - "-y", - "@anthropic/mcp-server-filesystem", - "/tmp", - tool_prefix="fs", -) - -agent = ai.agent(tools=tools) -``` - -## AI SDK UI adapter - -Use `ai.agents.ui.ai_sdk` to convert between AI SDK UI messages and Python -runtime messages/events. - -```python -class ChatRequest(pydantic.BaseModel): - messages: list[ai.agents.ui.ai_sdk.UIMessage] - - -@app.post("/chat") -async def chat(request: ChatRequest): - messages, approvals = ai.agents.ui.ai_sdk.to_messages(request.messages) - ai.agents.ui.ai_sdk.apply_approvals(approvals) - - async def stream_response(): - async with chat_agent.run(model, messages) as stream: - async for chunk in ai.agents.ui.ai_sdk.to_sse(stream): - yield chunk - - return fastapi.responses.StreamingResponse( - stream_response(), - headers=ai.agents.ui.ai_sdk.UI_MESSAGE_STREAM_HEADERS, - ) -``` - -Use `ai.agents.ui.ai_sdk.to_ui_messages(messages)` to rebuild UI history from -stored runtime messages. - -For serverless approvals, monitor `HookEvent` before passing events to `to_sse` -and call `ai.abort_pending_hook(event.hook)` on pending hooks. - -## Media generation - -Use `ai.generate` for dedicated image and video models: - -```python -image_message = await ai.generate( - ai.get_model("gateway:google/imagen-4.0-generate-001"), - [ai.user_message("A watercolor mothership over a quiet city.")], - ai.ImageParams(n=1, aspect_ratio="16:9"), -) - -image = image_message.images[0] -``` - -For video generation, pass `ai.VideoParams(...)` and read `message.videos`.