codeforge-dev 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devcontainer/.env +22 -0
- package/.devcontainer/CHANGELOG.md +197 -0
- package/.devcontainer/CLAUDE.md +117 -0
- package/.devcontainer/README.md +222 -0
- package/.devcontainer/config/main-system-prompt.md +502 -0
- package/.devcontainer/config/settings.json +47 -0
- package/.devcontainer/devcontainer.json +94 -0
- package/.devcontainer/features/README.md +113 -0
- package/.devcontainer/features/agent-browser/README.md +65 -0
- package/.devcontainer/features/agent-browser/devcontainer-feature.json +23 -0
- package/.devcontainer/features/agent-browser/install.sh +79 -0
- package/.devcontainer/features/ast-grep/README.md +24 -0
- package/.devcontainer/features/ast-grep/devcontainer-feature.json +24 -0
- package/.devcontainer/features/ast-grep/install.sh +51 -0
- package/.devcontainer/features/ccstatusline/README.md +296 -0
- package/.devcontainer/features/ccstatusline/devcontainer-feature.json +19 -0
- package/.devcontainer/features/ccstatusline/install.sh +290 -0
- package/.devcontainer/features/ccusage/README.md +205 -0
- package/.devcontainer/features/ccusage/devcontainer-feature.json +38 -0
- package/.devcontainer/features/ccusage/install.sh +132 -0
- package/.devcontainer/features/claude-code/README.md +498 -0
- package/.devcontainer/features/claude-code/config/settings.json +36 -0
- package/.devcontainer/features/claude-code/config/system-prompt.md +118 -0
- package/.devcontainer/features/claude-code/config/world-building-sp.md +1432 -0
- package/.devcontainer/features/claude-code/devcontainer-feature.json +42 -0
- package/.devcontainer/features/claude-code/install.sh +466 -0
- package/.devcontainer/features/claude-monitor/README.md +74 -0
- package/.devcontainer/features/claude-monitor/devcontainer-feature.json +38 -0
- package/.devcontainer/features/claude-monitor/install.sh +99 -0
- package/.devcontainer/features/lsp-servers/README.md +85 -0
- package/.devcontainer/features/lsp-servers/devcontainer-feature.json +40 -0
- package/.devcontainer/features/lsp-servers/install.sh +116 -0
- package/.devcontainer/features/mcp-qdrant/CHANGES.md +399 -0
- package/.devcontainer/features/mcp-qdrant/README.md +474 -0
- package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +57 -0
- package/.devcontainer/features/mcp-qdrant/install.sh +295 -0
- package/.devcontainer/features/mcp-qdrant/poststart-hook.sh +129 -0
- package/.devcontainer/features/mcp-reasoner/README.md +177 -0
- package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +20 -0
- package/.devcontainer/features/mcp-reasoner/install.sh +177 -0
- package/.devcontainer/features/mcp-reasoner/poststart-hook.sh +67 -0
- package/.devcontainer/features/notify-hook/README.md +86 -0
- package/.devcontainer/features/notify-hook/devcontainer-feature.json +23 -0
- package/.devcontainer/features/notify-hook/install.sh +38 -0
- package/.devcontainer/features/splitrail/README.md +140 -0
- package/.devcontainer/features/splitrail/devcontainer-feature.json +34 -0
- package/.devcontainer/features/splitrail/install.sh +129 -0
- package/.devcontainer/features/tree-sitter/README.md +138 -0
- package/.devcontainer/features/tree-sitter/devcontainer-feature.json +52 -0
- package/.devcontainer/features/tree-sitter/install.sh +173 -0
- package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +106 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-file.py +101 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +137 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/SKILL.md +387 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/cli-flags-and-output.md +312 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/sdk-and-mcp.md +569 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/SKILL.md +309 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/compose-services.md +438 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/dockerfile-patterns.md +340 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/SKILL.md +412 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/container-lifecycle.md +388 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/resources-and-security.md +444 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/SKILL.md +344 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/middleware-and-lifespan.md +254 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/pydantic-models.md +245 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/routing-and-dependencies.md +255 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/sse-and-streaming.md +318 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/SKILL.md +345 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/agents-and-tools.md +271 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/models-and-streaming.md +422 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/SKILL.md +220 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/cross-vendor-principles.md +139 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/patterns-and-antipatterns.md +376 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/skill-authoring-patterns.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/SKILL.md +329 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/advanced-queries.md +314 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/javascript-patterns.md +323 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/python-patterns.md +354 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/schema-and-pragmas.md +326 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/SKILL.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/ai-sdk-svelte.md +128 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/component-patterns.md +332 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/layercake.md +203 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/migration-guide.md +350 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/runes-and-reactivity.md +328 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/spa-and-routing.md +262 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/svelte-dnd-action.md +181 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/SKILL.md +414 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/fastapi-testing.md +411 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/svelte-testing.md +538 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +110 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +108 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272create-pr.md +337 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272new.md +166 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272review-commit.md +290 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272work.md +257 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +184 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +6 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +14 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +989 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +33 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +71 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +68 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +120 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +133 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +253 -0
- package/.devcontainer/scripts/setup-aliases.sh +80 -0
- package/.devcontainer/scripts/setup-config.sh +28 -0
- package/.devcontainer/scripts/setup-irie-claude.sh +32 -0
- package/.devcontainer/scripts/setup-plugins.sh +80 -0
- package/.devcontainer/scripts/setup.sh +58 -0
- package/LICENSE.txt +674 -0
- package/README.md +267 -0
- package/package.json +44 -0
- package/setup.js +83 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Agents and Tools -- Deep Dive
|
|
2
|
+
|
|
3
|
+
## 1. Agent Constructor Details
|
|
4
|
+
|
|
5
|
+
The `Agent` class is generic: `Agent[AgentDepsT, OutputDataT]`. The full constructor accepts:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from pydantic_ai import Agent, Tool
|
|
9
|
+
|
|
10
|
+
agent = Agent(
|
|
11
|
+
model="anthropic:claude-sonnet-4-20250514", # or Model instance, or None
|
|
12
|
+
output_type=str, # BaseModel, dataclass, TypedDict, list, ToolOutput, etc.
|
|
13
|
+
instructions="Be concise.", # static str or Callable[[RunContext], str]
|
|
14
|
+
system_prompt="Always respond in JSON.", # static, preserved in history
|
|
15
|
+
deps_type=type(None), # type of the deps object
|
|
16
|
+
name="my-agent", # optional display name
|
|
17
|
+
model_settings={"temperature": 0}, # default ModelSettings
|
|
18
|
+
retries=1, # default tool retry count
|
|
19
|
+
end_strategy="early", # "early" or "exhaustive"
|
|
20
|
+
tools=[my_func, Tool(other_func, takes_ctx=False)],
|
|
21
|
+
tool_timeout=30.0, # global tool execution timeout in seconds
|
|
22
|
+
)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Dynamic Instructions
|
|
26
|
+
|
|
27
|
+
Instructions can be a callable that receives `RunContext` and returns a string. This enables per-run customization based on dependencies:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
agent = Agent(
|
|
31
|
+
"openai:gpt-5",
|
|
32
|
+
deps_type=UserContext,
|
|
33
|
+
instructions=lambda ctx: f"The current user is {ctx.deps.username}. Respond in {ctx.deps.language}.",
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The `@agent.instructions` decorator is equivalent:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
@agent.instructions
|
|
41
|
+
def build_instructions(ctx: RunContext[UserContext]) -> str:
|
|
42
|
+
return f"The current user is {ctx.deps.username}."
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Instructions differ from `system_prompt` in one critical way: instructions are re-evaluated per run and excluded when `message_history` is provided. System prompts are static and always included.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. Tool Registration via Constructor
|
|
50
|
+
|
|
51
|
+
Tools can be registered via decorators or passed directly to the constructor. Constructor registration is useful when tools are defined in separate modules:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from pydantic_ai import Agent, Tool
|
|
55
|
+
|
|
56
|
+
async def search_docs(ctx: RunContext[Deps], query: str) -> str:
|
|
57
|
+
"""Search the documentation."""
|
|
58
|
+
return await ctx.deps.search(query)
|
|
59
|
+
|
|
60
|
+
def get_time() -> str:
|
|
61
|
+
"""Get the current UTC time."""
|
|
62
|
+
return datetime.utcnow().isoformat()
|
|
63
|
+
|
|
64
|
+
agent = Agent(
|
|
65
|
+
"openai:gpt-5",
|
|
66
|
+
deps_type=Deps,
|
|
67
|
+
tools=[
|
|
68
|
+
search_docs, # auto-detects RunContext
|
|
69
|
+
Tool(get_time, takes_ctx=False), # explicit: no context
|
|
70
|
+
Tool(search_docs, name="doc_search"), # custom name
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The `Tool` wrapper accepts:
|
|
76
|
+
- **`takes_ctx`**: Explicitly declare whether the function expects `RunContext` as its first parameter.
|
|
77
|
+
- **`name`** / **`description`**: Override the function name or docstring.
|
|
78
|
+
- **`retries`**: Override the agent-level retry count.
|
|
79
|
+
- **`prepare`**: Dynamic preparation function.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 3. Prepare Functions for Dynamic Tool Sets
|
|
84
|
+
|
|
85
|
+
A `prepare` function runs before each model request, allowing tools to be included, excluded, or modified based on the current run state:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from pydantic_ai import Agent, RunContext
|
|
89
|
+
from pydantic_ai.tools import ToolDefinition
|
|
90
|
+
|
|
91
|
+
async def filter_admin_tools(
|
|
92
|
+
ctx: RunContext[UserDeps], tool_def: ToolDefinition
|
|
93
|
+
) -> ToolDefinition | None:
|
|
94
|
+
"""Only include this tool if the user has admin role."""
|
|
95
|
+
if ctx.deps.user_role != "admin":
|
|
96
|
+
return None # exclude tool from this run
|
|
97
|
+
return tool_def # include unmodified
|
|
98
|
+
|
|
99
|
+
@agent.tool(prepare=filter_admin_tools)
|
|
100
|
+
async def delete_user(ctx: RunContext[UserDeps], user_id: int) -> str:
|
|
101
|
+
"""Delete a user account permanently."""
|
|
102
|
+
await ctx.deps.db.execute("DELETE FROM users WHERE id = $1", user_id)
|
|
103
|
+
return f"Deleted user {user_id}"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Returning `None` removes the tool. Returning a modified `ToolDefinition` changes the schema or description sent to the model. The agent-level `prepare_tools` parameter applies a prepare function to all tools.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 4. Result Types and Validators
|
|
111
|
+
|
|
112
|
+
### Output Type Patterns
|
|
113
|
+
|
|
114
|
+
**Single Pydantic model** -- the most common pattern:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
class WeatherReport(BaseModel):
|
|
118
|
+
city: str
|
|
119
|
+
temperature_c: float
|
|
120
|
+
conditions: str
|
|
121
|
+
|
|
122
|
+
agent = Agent("openai:gpt-5", output_type=WeatherReport)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Union of types** -- the model chooses which type to return:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
agent = Agent("openai:gpt-5", output_type=[WeatherReport, ErrorReport])
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Each type becomes a separate tool the model can call. The first successful tool call ends the run (with `end_strategy="early"`).
|
|
132
|
+
|
|
133
|
+
**Named tool outputs** -- control the tool names:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from pydantic_ai import ToolOutput
|
|
137
|
+
|
|
138
|
+
agent = Agent(
|
|
139
|
+
"openai:gpt-5",
|
|
140
|
+
output_type=[
|
|
141
|
+
ToolOutput(WeatherReport, name="return_weather", description="Return weather data"),
|
|
142
|
+
ToolOutput(ErrorReport, name="return_error", description="Report an error"),
|
|
143
|
+
],
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**NativeOutput** -- use the model's built-in structured output (JSON mode):
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from pydantic_ai import NativeOutput
|
|
151
|
+
|
|
152
|
+
agent = Agent("openai:gpt-5", output_type=NativeOutput(WeatherReport))
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Not all models support native structured output. Falls back to tool-based output automatically.
|
|
156
|
+
|
|
157
|
+
**PromptedOutput** -- inject the schema into the prompt instead of using tools:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from pydantic_ai import PromptedOutput
|
|
161
|
+
|
|
162
|
+
agent = Agent(
|
|
163
|
+
"openai:gpt-5",
|
|
164
|
+
output_type=PromptedOutput(WeatherReport, template="Respond with JSON matching: {schema}"),
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**TextOutput** -- process plain text through a function:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from pydantic_ai import TextOutput
|
|
172
|
+
|
|
173
|
+
def parse_csv(text: str) -> list[str]:
|
|
174
|
+
return [line.strip() for line in text.split(",")]
|
|
175
|
+
|
|
176
|
+
agent = Agent("openai:gpt-5", output_type=TextOutput(parse_csv))
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Output functions** -- the model calls a function to produce the result:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
def save_report(title: str, content: str, priority: int) -> str:
|
|
183
|
+
"""Save a report to the database."""
|
|
184
|
+
db.save(Report(title=title, content=content, priority=priority))
|
|
185
|
+
return f"Saved: {title}"
|
|
186
|
+
|
|
187
|
+
agent = Agent("openai:gpt-5", output_type=[save_report])
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Result Validators
|
|
191
|
+
|
|
192
|
+
Validators run after the model produces output. They can transform the output or reject it:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
@agent.output_validator
|
|
196
|
+
async def validate_weather(ctx: RunContext[Deps], output: WeatherReport) -> WeatherReport:
|
|
197
|
+
if output.temperature_c < -90 or output.temperature_c > 60:
|
|
198
|
+
raise ModelRetry("Temperature is outside realistic range. Check the data.")
|
|
199
|
+
return output
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Raising `ModelRetry` sends the error message back to the model for self-correction. The validator reruns up to `output_retries` times (defaults to `retries`).
|
|
203
|
+
|
|
204
|
+
During streaming, `ctx.partial_output` is `True` for intermediate validations. Skip side effects when partial:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
@agent.output_validator
|
|
208
|
+
async def validate_with_side_effect(ctx: RunContext[Deps], output: Report) -> Report:
|
|
209
|
+
if not ctx.partial_output:
|
|
210
|
+
await ctx.deps.audit_log.record(output)
|
|
211
|
+
return output
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 5. AgentRunResult
|
|
217
|
+
|
|
218
|
+
The return value of `agent.run()` and `agent.run_sync()`:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
result = await agent.run("prompt", deps=deps)
|
|
222
|
+
|
|
223
|
+
result.output # typed OutputDataT
|
|
224
|
+
result.usage() # RunUsage(requests=N, input_tokens=..., output_tokens=...)
|
|
225
|
+
result.all_messages() # full conversation history including system prompts
|
|
226
|
+
result.new_messages() # only messages from this run (for conversation continuation)
|
|
227
|
+
result.run_id # unique identifier for this run
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Conversation Continuation
|
|
231
|
+
|
|
232
|
+
Pass `message_history` to continue a previous conversation:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
result1 = await agent.run("What's the weather in Paris?", deps=deps)
|
|
236
|
+
result2 = await agent.run(
|
|
237
|
+
"What about London?",
|
|
238
|
+
deps=deps,
|
|
239
|
+
message_history=result1.all_messages(),
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
When `message_history` is provided, `instructions` are not included (they were already in the history). `system_prompt` is always included.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 6. ModelRetry Mechanics
|
|
248
|
+
|
|
249
|
+
When a tool raises `ModelRetry(message)`:
|
|
250
|
+
|
|
251
|
+
1. The framework sends the error message back to the model as a tool-call error response.
|
|
252
|
+
2. The model generates a new tool call (presumably with corrected arguments).
|
|
253
|
+
3. The tool runs again with the new arguments.
|
|
254
|
+
4. This repeats up to `retries` times (per-tool or per-agent setting).
|
|
255
|
+
5. If retries are exhausted, the underlying `ValidationError` or `ModelRetry` propagates as an exception.
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from pydantic_ai import ModelRetry
|
|
259
|
+
|
|
260
|
+
@agent.tool(retries=3)
|
|
261
|
+
async def query_api(ctx: RunContext[Deps], endpoint: str, params: dict) -> str:
|
|
262
|
+
"""Query the external API."""
|
|
263
|
+
if not endpoint.startswith("/api/"):
|
|
264
|
+
raise ModelRetry("Endpoint must start with /api/. Available: /api/users, /api/orders")
|
|
265
|
+
response = await ctx.deps.http.get(endpoint, params=params)
|
|
266
|
+
if response.status_code == 404:
|
|
267
|
+
raise ModelRetry(f"Endpoint {endpoint} returned 404. Try a different endpoint.")
|
|
268
|
+
return response.text
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`ModelRetry` is distinct from Python exceptions -- it signals a recoverable error that the model can fix by adjusting its tool-call arguments.
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# Models and Streaming -- Deep Dive
|
|
2
|
+
|
|
3
|
+
## 1. Model Configuration
|
|
4
|
+
|
|
5
|
+
### Model Identifiers
|
|
6
|
+
|
|
7
|
+
PydanticAI uses string identifiers in the format `provider:model-name`:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from pydantic_ai import Agent
|
|
11
|
+
|
|
12
|
+
# Anthropic
|
|
13
|
+
agent = Agent("anthropic:claude-sonnet-4-20250514")
|
|
14
|
+
|
|
15
|
+
# OpenAI
|
|
16
|
+
agent = Agent("openai:gpt-5")
|
|
17
|
+
|
|
18
|
+
# Google Gemini
|
|
19
|
+
agent = Agent("google-gla:gemini-2.0-flash")
|
|
20
|
+
|
|
21
|
+
# Groq
|
|
22
|
+
agent = Agent("groq:llama-3.3-70b-versatile")
|
|
23
|
+
|
|
24
|
+
# OpenAI-compatible endpoints (Ollama, vLLM, etc.)
|
|
25
|
+
from pydantic_ai.models.openai import OpenAIChatModel
|
|
26
|
+
model = OpenAIChatModel("llama3", base_url="http://localhost:11434/v1")
|
|
27
|
+
agent = Agent(model)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Override the model at run time without changing the agent definition:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
result = await agent.run("prompt", model="openai:gpt-5")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### ModelSettings
|
|
37
|
+
|
|
38
|
+
`ModelSettings` is a `TypedDict` controlling generation parameters. All fields are optional:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from pydantic_ai import Agent, ModelSettings
|
|
42
|
+
|
|
43
|
+
agent = Agent(
|
|
44
|
+
"openai:gpt-5",
|
|
45
|
+
model_settings=ModelSettings(
|
|
46
|
+
temperature=0.0,
|
|
47
|
+
max_tokens=2000,
|
|
48
|
+
top_p=0.95,
|
|
49
|
+
seed=42,
|
|
50
|
+
timeout=30.0,
|
|
51
|
+
parallel_tool_calls=True,
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Precedence (lowest to highest):
|
|
57
|
+
1. Model defaults
|
|
58
|
+
2. `Agent(model_settings=...)` -- agent-level
|
|
59
|
+
3. `agent.run(model_settings=...)` -- per-run
|
|
60
|
+
|
|
61
|
+
Available fields: `temperature`, `max_tokens`, `top_p`, `timeout`, `seed`, `parallel_tool_calls`, `presence_penalty`, `frequency_penalty`, `logit_bias`, `stop_sequences`, `extra_headers`, `extra_body`.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 2. FallbackModel
|
|
66
|
+
|
|
67
|
+
Sequence multiple models for resilience. The framework tries each model in order and switches on API errors:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from pydantic_ai.models.fallback import FallbackModel
|
|
71
|
+
|
|
72
|
+
fallback = FallbackModel(
|
|
73
|
+
"anthropic:claude-sonnet-4-20250514",
|
|
74
|
+
"openai:gpt-5",
|
|
75
|
+
"google-gla:gemini-2.0-flash",
|
|
76
|
+
)
|
|
77
|
+
agent = Agent(fallback)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Behavior:
|
|
81
|
+
- Only `ModelHTTPError` and `ModelAPIError` trigger fallback by default.
|
|
82
|
+
- Validation errors (bad tool arguments, output schema mismatch) use the retry mechanism, not fallback.
|
|
83
|
+
- If all models fail, raises `FallbackExceptionGroup` containing all individual exceptions.
|
|
84
|
+
- Custom trigger: pass `fallback_on` to specify which exception types trigger fallback.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from pydantic_ai.exceptions import ModelHTTPError
|
|
88
|
+
|
|
89
|
+
fallback = FallbackModel(
|
|
90
|
+
"anthropic:claude-sonnet-4-20250514",
|
|
91
|
+
"openai:gpt-5",
|
|
92
|
+
fallback_on=(ModelHTTPError, TimeoutError),
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 3. Streaming
|
|
99
|
+
|
|
100
|
+
### Text Streaming
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
async with agent.run_stream("Tell me a story") as result:
|
|
104
|
+
# Cumulative text (each iteration includes all previous text)
|
|
105
|
+
async for text in result.stream_text():
|
|
106
|
+
print(text)
|
|
107
|
+
|
|
108
|
+
# Delta mode (each iteration is only the new chunk)
|
|
109
|
+
async for delta in result.stream_text(delta=True):
|
|
110
|
+
print(delta, end="", flush=True)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Structured Output Streaming
|
|
114
|
+
|
|
115
|
+
When the agent has a Pydantic output type, stream partially-validated objects:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from pydantic import BaseModel
|
|
119
|
+
|
|
120
|
+
class Story(BaseModel):
|
|
121
|
+
title: str
|
|
122
|
+
chapters: list[str]
|
|
123
|
+
|
|
124
|
+
agent = Agent("openai:gpt-5", output_type=Story)
|
|
125
|
+
|
|
126
|
+
async with agent.run_stream("Write a story") as result:
|
|
127
|
+
async for partial_story in result.stream_output(debounce_by=0.1):
|
|
128
|
+
# partial_story is a Story instance with whatever fields have been parsed
|
|
129
|
+
print(partial_story.title if hasattr(partial_story, 'title') else "...")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The `debounce_by` parameter (default `0.1` seconds) batches rapid chunks to reduce Pydantic validation overhead. Set to `None` for maximum responsiveness.
|
|
133
|
+
|
|
134
|
+
### Sync Streaming
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
with agent.run_stream_sync("Tell me a story") as result:
|
|
138
|
+
for delta in result.stream_text(delta=True):
|
|
139
|
+
print(delta, end="", flush=True)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### StreamedRunResult API
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
class StreamedRunResult:
|
|
146
|
+
# Streaming iterators
|
|
147
|
+
async def stream_text(delta: bool = False, debounce_by: float | None = 0.1) -> AsyncIterator[str]
|
|
148
|
+
async def stream_output(debounce_by: float | None = 0.1) -> AsyncIterator[OutputDataT]
|
|
149
|
+
async def get_output() -> OutputDataT # consume stream, return final result
|
|
150
|
+
|
|
151
|
+
# State
|
|
152
|
+
is_complete: bool
|
|
153
|
+
|
|
154
|
+
# Messages and usage
|
|
155
|
+
def all_messages() -> list[ModelMessage]
|
|
156
|
+
def new_messages() -> list[ModelMessage]
|
|
157
|
+
def usage() -> RunUsage
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 4. VercelAIAdapter
|
|
163
|
+
|
|
164
|
+
`VercelAIAdapter` bridges PydanticAI agents to Vercel AI SDK frontends. It translates PydanticAI's streaming events into the Vercel AI Data Stream Protocol.
|
|
165
|
+
|
|
166
|
+
### FastAPI Integration
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from fastapi import FastAPI, Request, Response
|
|
170
|
+
from pydantic_ai import Agent
|
|
171
|
+
from pydantic_ai.ui.vercel_ai import VercelAIAdapter
|
|
172
|
+
|
|
173
|
+
app = FastAPI()
|
|
174
|
+
agent = Agent("anthropic:claude-sonnet-4-20250514", instructions="Be helpful.")
|
|
175
|
+
|
|
176
|
+
@app.post("/chat")
|
|
177
|
+
async def chat(request: Request) -> Response:
|
|
178
|
+
return await VercelAIAdapter.dispatch_request(request, agent=agent)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`dispatch_request` handles the full lifecycle:
|
|
182
|
+
1. Parses the Vercel AI SDK request body (messages, model settings).
|
|
183
|
+
2. Converts frontend messages to PydanticAI's `ModelMessage` format.
|
|
184
|
+
3. Runs the agent in streaming mode.
|
|
185
|
+
4. Encodes PydanticAI events as Vercel AI Data Stream Protocol SSE.
|
|
186
|
+
5. Returns a `StreamingResponse`.
|
|
187
|
+
|
|
188
|
+
### With Dependencies
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
@app.post("/chat")
|
|
192
|
+
async def chat(request: Request, db: DB) -> Response:
|
|
193
|
+
return await VercelAIAdapter.dispatch_request(
|
|
194
|
+
request,
|
|
195
|
+
agent=agent,
|
|
196
|
+
deps=AppDeps(db=db),
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Manual Usage (Non-Starlette)
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
adapter = VercelAIAdapter(agent=agent, run_input=request_data, accept="text/event-stream")
|
|
204
|
+
event_stream = adapter.run_stream() # AsyncIterator[VercelAIEvent]
|
|
205
|
+
sse_stream = adapter.encode_stream(event_stream) # AsyncIterator[str] (SSE-formatted)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Message Conversion
|
|
209
|
+
|
|
210
|
+
Convert between PydanticAI and Vercel AI message formats:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
# Frontend messages -> PydanticAI messages
|
|
214
|
+
pydantic_messages = VercelAIAdapter.load_messages(ui_messages)
|
|
215
|
+
|
|
216
|
+
# PydanticAI messages -> frontend messages
|
|
217
|
+
ui_messages = VercelAIAdapter.dump_messages(pydantic_messages)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Svelte Frontend
|
|
221
|
+
|
|
222
|
+
```svelte
|
|
223
|
+
<script>
|
|
224
|
+
import { useChat } from '@ai-sdk/svelte';
|
|
225
|
+
const { messages, input, handleSubmit, isLoading } = useChat({ api: '/chat' });
|
|
226
|
+
</script>
|
|
227
|
+
|
|
228
|
+
<div>
|
|
229
|
+
{#each $messages as message}
|
|
230
|
+
<p><strong>{message.role}:</strong> {message.content}</p>
|
|
231
|
+
{/each}
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<form onsubmit={handleSubmit}>
|
|
235
|
+
<input bind:value={$input} placeholder="Type a message..." />
|
|
236
|
+
<button type="submit" disabled={$isLoading}>Send</button>
|
|
237
|
+
</form>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The `useChat` hook manages conversation state, streaming, and message history automatically. The protocol translation is fully handled by `VercelAIAdapter` on the backend.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 5. TestModel
|
|
245
|
+
|
|
246
|
+
`TestModel` is a procedural model that calls all registered tools and generates data matching output schemas without making real API calls:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from pydantic_ai.models.test import TestModel
|
|
250
|
+
|
|
251
|
+
with agent.override(model=TestModel()):
|
|
252
|
+
result = agent.run_sync("test prompt")
|
|
253
|
+
assert isinstance(result.output, CityInfo)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`TestModel` behavior:
|
|
257
|
+
- Calls each registered tool once with generated arguments matching the tool's parameter schema.
|
|
258
|
+
- If the agent has a structured `output_type`, generates a response matching the output schema.
|
|
259
|
+
- If the output type is `str`, returns `"Final response"` (or a custom string).
|
|
260
|
+
|
|
261
|
+
Custom text output:
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
with agent.override(model=TestModel(custom_output_text="Paris is beautiful")):
|
|
265
|
+
result = agent.run_sync("Tell me about Paris")
|
|
266
|
+
assert result.output == "Paris is beautiful"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Override for Dependency Swapping
|
|
270
|
+
|
|
271
|
+
`agent.override()` replaces both model and dependencies:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
mock_deps = AppDeps(db=FakeDB(), cache=FakeCache(), api_key="test")
|
|
275
|
+
|
|
276
|
+
with agent.override(model=TestModel(), deps=mock_deps):
|
|
277
|
+
result = agent.run_sync("test")
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 6. FunctionModel
|
|
283
|
+
|
|
284
|
+
Full control over model responses via a callback function:
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
from pydantic_ai.models.function import FunctionModel, AgentInfo
|
|
288
|
+
from pydantic_ai.messages import ModelResponse, TextPart, ToolCallPart
|
|
289
|
+
|
|
290
|
+
def my_model(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
|
|
291
|
+
# info.function_tools lists available tools with their schemas
|
|
292
|
+
if info.function_tools:
|
|
293
|
+
tool = info.function_tools[0]
|
|
294
|
+
return ModelResponse(parts=[
|
|
295
|
+
ToolCallPart(tool_name=tool.name, args={"query": "test"})
|
|
296
|
+
])
|
|
297
|
+
return ModelResponse(parts=[TextPart("mock response")])
|
|
298
|
+
|
|
299
|
+
with agent.override(model=FunctionModel(my_model)):
|
|
300
|
+
result = await agent.run("test")
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The callback receives:
|
|
304
|
+
- **`messages`**: The full conversation history as `list[ModelMessage]`.
|
|
305
|
+
- **`info`**: An `AgentInfo` object with `function_tools` (list of tool definitions), `output_tools` (output schema tools), `model_settings`, and `allow_text_output`.
|
|
306
|
+
|
|
307
|
+
### Async FunctionModel
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
async def async_model(messages, info: AgentInfo) -> ModelResponse:
|
|
311
|
+
await asyncio.sleep(0.1) # simulate latency
|
|
312
|
+
return ModelResponse(parts=[TextPart("async mock")])
|
|
313
|
+
|
|
314
|
+
with agent.override(model=FunctionModel(async_model)):
|
|
315
|
+
result = await agent.run("test")
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Streaming FunctionModel
|
|
319
|
+
|
|
320
|
+
Return a `StreamedResponse` for streaming tests:
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
from pydantic_ai.models.function import FunctionModel, StreamedResponse
|
|
324
|
+
|
|
325
|
+
def streaming_model(messages, info):
|
|
326
|
+
return StreamedResponse(
|
|
327
|
+
model_name="test-model",
|
|
328
|
+
timestamp=datetime.now(),
|
|
329
|
+
)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 7. Usage Tracking
|
|
335
|
+
|
|
336
|
+
### RunUsage
|
|
337
|
+
|
|
338
|
+
Every run returns usage statistics:
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
result = await agent.run("prompt")
|
|
342
|
+
usage = result.usage()
|
|
343
|
+
|
|
344
|
+
print(usage.requests) # number of API round-trips
|
|
345
|
+
print(usage.input_tokens) # total input tokens across all requests
|
|
346
|
+
print(usage.output_tokens) # total output tokens
|
|
347
|
+
print(usage.total_tokens) # input + output
|
|
348
|
+
print(usage.tool_calls) # number of tool executions
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### UsageLimits
|
|
352
|
+
|
|
353
|
+
Enforce hard limits on resource consumption:
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
from pydantic_ai import UsageLimits
|
|
357
|
+
|
|
358
|
+
result = await agent.run(
|
|
359
|
+
"prompt",
|
|
360
|
+
usage_limits=UsageLimits(
|
|
361
|
+
request_limit=10, # max API requests (default 50)
|
|
362
|
+
input_tokens_limit=10000,
|
|
363
|
+
output_tokens_limit=5000,
|
|
364
|
+
total_tokens_limit=15000,
|
|
365
|
+
tool_calls_limit=20,
|
|
366
|
+
),
|
|
367
|
+
)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Exceeding any limit raises `UsageLimitExceeded`. The default `request_limit` is 50 -- this prevents infinite tool-call loops.
|
|
371
|
+
|
|
372
|
+
### Aggregating Usage Across Runs
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
total_usage = RunUsage()
|
|
376
|
+
|
|
377
|
+
for prompt in prompts:
|
|
378
|
+
result = await agent.run(prompt)
|
|
379
|
+
total_usage += result.usage()
|
|
380
|
+
|
|
381
|
+
print(f"Total tokens: {total_usage.total_tokens}")
|
|
382
|
+
print(f"Total requests: {total_usage.requests}")
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 8. Capturing Messages for Assertions
|
|
388
|
+
|
|
389
|
+
Use `capture_run_messages` to inspect the full message exchange in tests:
|
|
390
|
+
|
|
391
|
+
```python
|
|
392
|
+
from pydantic_ai import capture_run_messages
|
|
393
|
+
|
|
394
|
+
with capture_run_messages() as messages:
|
|
395
|
+
result = await agent.run("What's the weather?", deps=deps)
|
|
396
|
+
|
|
397
|
+
# messages is a list of ModelMessage objects
|
|
398
|
+
assert any("weather" in str(m) for m in messages)
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
This captures all messages exchanged during the run, including system prompts, user messages, tool calls, tool responses, and the final output. Useful for verifying that the agent called the expected tools with the expected arguments.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## 9. Preventing Accidental Real Requests
|
|
406
|
+
|
|
407
|
+
In test suites, set the global guard at the module or conftest level:
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
# conftest.py
|
|
411
|
+
import pydantic_ai.models
|
|
412
|
+
pydantic_ai.models.ALLOW_MODEL_REQUESTS = False
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Any attempt to use a real model (not `TestModel` or `FunctionModel`) raises an error immediately. Use `agent.override()` to swap in test models:
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
def test_agent_behavior():
|
|
419
|
+
with agent.override(model=TestModel()):
|
|
420
|
+
result = agent.run_sync("test")
|
|
421
|
+
assert result.output is not None
|
|
422
|
+
```
|