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
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,6 @@ jobs:
run: |
python3 -m venv /tmp/pydantic-ai
/tmp/pydantic-ai/bin/pip install --no-cache-dir \
'pydantic-ai[mcp]==1.102.0' pyyaml==6.0.2 composio==0.13.1 \
'pydantic-ai==2.13.0' pyyaml==6.0.2 composio==0.13.1 \
pydantic-ai-skills==0.11.0 pytest==9.1.1
/tmp/pydantic-ai/bin/pytest tests/test_run_pydantic_build_agent.py
11 changes: 7 additions & 4 deletions api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# version bumps go through PR review so we can vet the changelog.
ENV PYDANTIC_AI_VENV=/opt/pydantic-ai
ENV PYDANTIC_AI_PY="${PYDANTIC_AI_VENV}/bin/python3"
# `[mcp]` pulls in the MCP client deps so MCPServerStreamableHTTP is
# available — that's how Composio exposes its toolkits to pydantic-ai
# at run time. `composio` is the Python SDK we use to spin up a
# pydantic-ai 2.x bundles the MCP client deps (MCPToolset) by default —
# that's how Composio exposes its toolkits to pydantic-ai at run time.
# 2.10.0+ is required for Anthropic `pause_turn` continuation: without it,
# WebSearch runs on Claude die with a 400 ("web_search tool use without a
# corresponding web_search_tool_result block") when the API pauses a long
# server-tool turn. `composio` is the Python SDK we use to spin up a
# Composio session keyed by workspace + toolkit list and read the
# MCP URL back. All three are pinned so a rebuild of a given image
# tag is reproducible — Composio in particular ships frequently and
# an unpinned bump could break connection-using agents on the next
# rebuild. Rev versions via PR so we can vet the changelog.
RUN python3 -m venv "${PYDANTIC_AI_VENV}" && \
"${PYDANTIC_AI_VENV}/bin/pip" install --no-cache-dir \
'pydantic-ai[mcp]==1.102.0' pyyaml==6.0.2 composio==0.13.1 \
'pydantic-ai==2.13.0' pyyaml==6.0.2 composio==0.13.1 \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v2 bump also changes streamed tool-result events from .result to .part (event.tool_call_id carries the id). make_stream_handler() still reads FunctionToolResultEvent.result, so under 2.x live progress can mark successful function tool returns as ok: false until the final captured steps arrive. The version bump should include that handler migration so live run steps don't show false failures.

pydantic-ai-skills==0.11.0

# Extra deps for agent sidecar tool modules (`tools_module:`). Ships
Expand Down
22 changes: 13 additions & 9 deletions api/scripts/run_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1120,9 +1120,6 @@ def build_agent(
retries = spec.get("retries")
if isinstance(retries, int):
kwargs["retries"] = retries
instrument = spec.get("instrument")
if isinstance(instrument, bool):
kwargs["instrument"] = instrument
if toolsets:
kwargs["toolsets"] = toolsets
# Sidecar Python functions from the agent's `tools_module:`. These
Expand All @@ -1139,6 +1136,13 @@ def build_agent(
if capabilities:
kwargs["capabilities"] = capabilities

# `instrument: true` — pydantic-ai 2.x replaced Agent(instrument=...) with
# the Instrumentation capability.
if spec.get("instrument") is True:
from pydantic_ai.capabilities import Instrumentation

kwargs.setdefault("capabilities", []).append(Instrumentation())

# ScaleDown prompt compression — opt-in per agent via `scaledown:`, and only
# when the workspace set a key. Any non-`off` mode attaches a history
# processor that optimizes each model request (old history = context to
Expand All @@ -1155,12 +1159,13 @@ def build_agent(
file=sys.stderr,
)
if sd_enabled and _scaledown_key():
kwargs["history_processors"] = [
_make_scaledown_processor(sd_rate, sd_min_chars)
]
from pydantic_ai.capabilities import ProcessHistory

kwargs.setdefault("capabilities", []).append(
ProcessHistory(_make_scaledown_processor(sd_rate, sd_min_chars))
)
except Exception as e: # noqa: BLE001 — never block a run on compression setup
print(f"[scaledown] setup skipped: {e}", file=sys.stderr)
kwargs.pop("history_processors", None)

return Agent(model, **kwargs)

Expand All @@ -1170,8 +1175,7 @@ def build_composio_toolset(
):
"""Create a Composio Tool Router session for the declared toolkits
and wrap it in an MCPToolset so pydantic-ai can call the tools.
(`MCPToolset` is the v1.x replacement for `MCPServerStreamableHTTP`;
streamable HTTP is its default transport for HTTP URLs.)
(Streamable HTTP is `MCPToolset`'s default transport for HTTP URLs.)

Only entries with source="composio" are folded into the session;
native-MCP entries are handled by `build_native_mcp_toolsets`
Expand Down
28 changes: 28 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,34 @@ def test_build_agent_attaches_websearch_capability() -> None:
assert isinstance(agent, Agent)


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=...)).
monkeypatch.setenv("TAS_SCALEDOWN_API_KEY", "test-scaledown-key")
agent = run_pydantic.build_agent(
{
"name": "compressed",
"model": "anthropic:claude-sonnet-4-5",
"instructions": "Reply briefly.",
"scaledown": {"mode": "on"},
}
)
assert isinstance(agent, Agent)


def test_build_agent_with_instrument_true() -> None:
# `instrument: true` maps to the Instrumentation capability in 2.x.
agent = run_pydantic.build_agent(
{
"name": "instrumented",
"model": "anthropic:claude-sonnet-4-5",
"instructions": "Reply briefly.",
"instrument": True,
}
)
assert isinstance(agent, Agent)


def test_uncached_input_excludes_cache_halves() -> None:
from types import SimpleNamespace

Expand Down
20 changes: 16 additions & 4 deletions web/src/lib/api-v1/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,10 +446,22 @@ async function finishTask(args: {
},
});
if (!res.ok) {
const status = res.error.kind === "http" && (res.error.status === 401 || res.error.status === 403)
? 502
: 502;
return { ok: false, status, error: `Tembo Coding Agent rejected the request (${res.error.kind})` };
// Include the upstream status + response body so a failing dispatch is
// diagnosable from the caller's error alone. Kind-only ("http") proved
// undebuggable in the field. Returning the body to the authorized caller
// matches what the chat UI already shows (formatCapError); it's LOGGING
// the body that #44 forbids — it can echo the submitted prompt.
const detail =
res.error.kind === "http"
? `HTTP ${res.error.status}: ${res.error.body.slice(0, 300) || "(no body)"}`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One wrinkle: requestAgentChangeSystem() reuses this path for learning batches, and the scheduler logs res.error on failure. If CAP echoes the submitted prompt in the body, this return value now writes that body to server logs. Consider keeping the body only for request-backed API/MCP responses and returning a sanitized detail for the system/learning path.

: res.error.kind === "network"
? `network: ${res.error.message}`
: res.error.kind;
return {
ok: false,
status: 502,
error: `Tembo Coding Agent rejected the request (${detail})`,
};
}

if (args.ctx.workspace.commitMode === "direct") {
Expand Down
Loading