Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/pydantic-ai-version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.87.0
1.103.0
1 change: 1 addition & 0 deletions packages/sdk/docs/reference/features.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Vibes is designed to stay current with Pydantic AI - an AI agent automatically
| Toolset reuse | Share toolsets across agents | ✅ | [Toolsets](/concepts/toolsets) | `Toolset` is a plain interface - pass the same instance to multiple agents |
| Runtime swap | Replace toolsets during testing | ✅ | [Testing](/concepts/testing) | `agent.override({ toolsets: [...] }).run(prompt)` |
| `PreparedToolset` | Modify entire tool list before each step | ✅ | [Toolsets](/concepts/toolsets) | `new PreparedToolset(inner, (ctx, tools) => tools)` - dynamic per-turn |
| Prepare callback warning | Warn when `prepare` callback returns `None` (v1.103.0) | ✅ | [Toolsets](/concepts/toolsets) | `PreparedToolset` logs a warning and falls back to inner tools on `null`/`undefined` |
| `ApprovalRequiredToolset` | Enforce human approval on a toolset | ✅ | [Human-in-the-Loop](/concepts/human-in-the-loop) | `new ApprovalRequiredToolset(inner)` - all tools get `requiresApproval` |
| `WrapperToolset` | Custom execution behaviour around a toolset | ✅ | [Toolsets](/concepts/toolsets) | `class MyWrapper extends WrapperToolset { callTool(...) { ... } }` |
| `ExternalToolset` | Deferred execution outside agent process | ✅ | [Human-in-the-Loop](/concepts/human-in-the-loop) | `new ExternalToolset([{ name, description, jsonSchema }])` - schema-only |
Expand Down
9 changes: 8 additions & 1 deletion packages/sdk/lib/toolsets/prepared_toolset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export class PreparedToolset<TDeps = undefined> implements Toolset<TDeps> {

async tools(ctx: RunContext<TDeps>): Promise<ToolDefinition<TDeps>[]> {
const innerTools = await this._inner.tools(ctx);
return this._prepare(ctx, innerTools);
const preparedTools = await this._prepare(ctx, innerTools);
if (preparedTools === null || preparedTools === undefined) {
console.warn(
"PreparedToolset.prepare returned null/undefined; falling back to inner tools.",
);
return innerTools;
}
return preparedTools;
}
}
30 changes: 30 additions & 0 deletions packages/sdk/tests/prepared_toolset_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,33 @@ Deno.test("PreparedToolset - prepare called on every turn", async () => {
// called at least twice.
assertEquals(prepareCallCount >= 2, true);
});

Deno.test("PreparedToolset - warns and falls back when prepare returns nullish", async () => {
let capturedNames: string[] = [];
let warning: string | undefined;
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => {
warning = args.map(String).join(" ");
};
try {
const model = new MockLanguageModelV3({
doGenerate: (opts) => {
capturedNames = toolNames(opts);
return Promise.resolve(textResponse("done"));
},
});

const inner = new FunctionToolset([makeTool("fallback_tool")]);
const prepared = new PreparedToolset(
inner,
() => undefined as unknown as import("../mod.ts").ToolDefinition[],
);
const agent = new Agent({ model, toolsets: [prepared] });
await agent.run("go");

assertEquals(capturedNames.includes("fallback_tool"), true);
assertEquals(warning?.includes("PreparedToolset.prepare returned null/undefined"), true);
} finally {
console.warn = originalWarn;
}
});