Skip to content
Merged
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
30 changes: 25 additions & 5 deletions api/scripts/run_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1082,15 +1082,38 @@ def build_agent(
name = spec.get("name")
if isinstance(name, str) and name.strip():
kwargs["name"] = name
# Provider-adaptive capabilities from `capabilities:` (e.g. WebSearch).
# Built before model settings because WebSearch changes which settings are
# legal (see below).
capabilities = _build_capabilities(spec)
has_web_search = any(type(c).__name__ == "WebSearch" for c in capabilities)

# Default to SEQUENTIAL tool calls. parallel_tool_calls=False is a real,
# API-level limiter (OpenAI parallel_tool_calls / Anthropic
# disable_parallel_tool_use): the model emits one tool call per turn instead
# of fanning out many at once, which is what was getting Attio (and other
# providers) rate-limited. An agent that genuinely needs parallel calls can
# opt back in with model_settings.parallel_tool_calls: true in its spec.
# EXCEPT WebSearch on Anthropic: the current web_search tool runs with
# server-side programmatic tool calling, and the API rejects
# `tool_choice.disable_parallel_tool_use: true` combined with it (400) as
# soon as the agent also has any client/MCP tool. Skip the default there —
# pydantic-ai only emits disable_parallel_tool_use when the key is present.
anthropic_web_search = (
has_web_search and isinstance(model, str) and model.startswith("anthropic:")
)
model_settings = spec.get("model_settings")
ms = dict(model_settings) if isinstance(model_settings, dict) else {}
ms.setdefault("parallel_tool_calls", False)
if not anthropic_web_search:
ms.setdefault("parallel_tool_calls", False)
elif "parallel_tool_calls" in ms:
print(
"[capabilities] WebSearch on Anthropic is incompatible with the "
"parallel_tool_calls setting (programmatic tool calling); "
"dropping it for this run",
file=sys.stderr,
)
ms.pop("parallel_tool_calls", None)
# Anthropic prompt caching. An agentic run re-sends the whole prompt every
# step; without caching the big static prefix — system instructions + the
# MCP/Composio tool schemas — is re-billed at full input rate on each of the
Expand Down Expand Up @@ -1129,10 +1152,7 @@ def build_agent(
if tools:
kwargs["tools"] = tools

# Provider-adaptive capabilities from `capabilities:` (e.g. WebSearch).
# These are model-side abilities (not MCP/sidecar) — pydantic-ai uses the
# provider's native implementation where available, with a local fallback.
capabilities = _build_capabilities(spec)
# Capabilities were built up top (before model settings); attach them here.
if capabilities:
kwargs["capabilities"] = capabilities

Expand Down
37 changes: 37 additions & 0 deletions api/tests/test_run_pydantic_build_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,43 @@ def test_build_agent_attaches_websearch_capability() -> None:
assert isinstance(agent, Agent)


def test_websearch_on_anthropic_skips_parallel_tool_calls_setting() -> None:
# Anthropic's web_search tool uses server-side programmatic tool calling,
# which the API rejects in combination with disable_parallel_tool_use
# (400 as soon as the agent also has a client/MCP tool). The sequential
# default must not apply — and an explicit spec value must be dropped —
# for WebSearch agents on Anthropic, while non-WebSearch agents keep it.
ws = run_pydantic.build_agent(
{
"name": "searcher",
"model": "anthropic:claude-sonnet-5",
"instructions": "Search.",
"capabilities": ["WebSearch"],
}
)
assert "parallel_tool_calls" not in ws.model_settings

ws_explicit = run_pydantic.build_agent(
{
"name": "searcher-explicit",
"model": "anthropic:claude-sonnet-5",
"instructions": "Search.",
"capabilities": ["WebSearch"],
"model_settings": {"parallel_tool_calls": False},
}
)
assert "parallel_tool_calls" not in ws_explicit.model_settings

plain = run_pydantic.build_agent(
{
"name": "plain",
"model": "anthropic:claude-sonnet-5",
"instructions": "Reply.",
}
)
assert plain.model_settings["parallel_tool_calls"] is False


def test_build_agent_with_scaledown_enabled(monkeypatch: pytest.MonkeyPatch) -> None:
# scaledown attaches its compressor as a ProcessHistory capability
# (pydantic-ai 2.x dropped Agent(history_processors=...)).
Expand Down
14 changes: 8 additions & 6 deletions web/src/lib/cap-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import type { CommitMode } from "@/lib/commit-mode-constants";

// Thin client for the Tembo Coding Agent Platform task API. The task
// endpoints live under the **/public-api** namespace and authenticate
// with the workspace's Tembo API key as `Authorization: Bearer` — the
// bare `/task/create` path hits a different internal auth gate that
// rejects the public key ("Invalid token"). POSTs a free-text prompt +
// repo URL to POST /public-api/task/create and returns a task record
// with an htmlUrl the user can follow; the task is what opens the PR.
// with the workspace's Tembo API key as `Authorization: Bearer`. POSTs
// a free-text prompt + repo URL to POST /public-api/session/create and
// returns a task record with an htmlUrl the user can follow; the task
// is what opens the PR. CAP renamed the mount from /public-api/task to
// /public-api/session with no alias (tembo/monorepo#9519, 2026-07-16);
// the old path falls through to a catch-all that 400s with
// {"error":{"message":"invalid request path"}}.

const DEFAULT_TEMBO_API_URL = "https://api.tembo.io";

Expand Down Expand Up @@ -61,7 +63,7 @@ export async function createTemboTask(args: {
queueRightAway: true,
};

const url = `${baseUrl}/public-api/task/create`;
const url = `${baseUrl}/public-api/session/create`;
// Breadcrumb only — never log `body`: it embeds the prompt (run input/output,
// user data) and would leak to plaintext container logs / aggregators (#44).
console.log("[cap] POST", url);
Expand Down
Loading