Compare commits

..

4 Commits

Author SHA1 Message Date
Greyson LaLonde
c3e2001d52 fix(checkpoint): serialize type[BaseModel] fields as JSON schema
Some checks failed
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Subclass redeclarations of args_schema/response_format dropped the
parent's Annotated PlainSerializer, causing PydanticSerializationError
on model_dump(mode='json'). Replace with @field_serializer decorators
backed by a shared serialize_model_class helper:

- BaseTool: covers RecallMemoryTool, RememberTool, AskQuestionTool,
  DelegateWorkTool, AddImageTool, ReadFileTool
- BaseLLM (check_fields=False): covers LLM, Anthropic, OpenAI, Gemini,
  Bedrock
- LiteAgent.response_format
- A2AConfig / A2AClientConfig response_model
2026-05-23 03:50:24 +08:00
Greyson LaLonde
306f5989b4 fix(checkpoint): avoid orphan task_started on resume scope restore
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
Move scope restoration from Crew-level global push to a per-task push
inside Task via resume_task_scope() in event_context. Fixes orphan
task_started warning, hierarchical resume (manager_agent now eligible
for _resuming), and parallel async resume (each contextvars copy owns
its own scope). Tests added.
2026-05-23 01:20:15 +08:00
Greyson LaLonde
4990041ef7 chore(deps): force starlette>=1.0.1 for PYSEC-2026-161
starlette <1.0.1 has PYSEC-2026-161 (missing Host header validation
poisons request.url.path, bypassing path-based auth). Pulled in as a
transitive of fastapi. Override-dependencies forces the patched
version; lock regenerated against starlette 1.0.1.
2026-05-22 23:33:08 +08:00
Greyson LaLonde
88e95befe7 fix(experimental): allow AgentExecutor restore from checkpoint
llm and prompt were declared required with exclude=True, making the
model un-restorable from its own serialized output. Mirror the
CrewAgentExecutor pattern: make them nullable with default None, keep
exclude=True, and re-attach llm on the resume path alongside the other
re-attached fields. Guard the two prompt-deref sites so the runtime
invariant survives the looser type.
2026-05-22 23:24:12 +08:00
18 changed files with 378 additions and 380 deletions

View File

@@ -16,6 +16,7 @@ from pydantic import (
FilePath,
PrivateAttr,
SecretStr,
field_serializer,
model_validator,
)
from typing_extensions import Self, deprecated
@@ -24,6 +25,7 @@ from crewai.a2a.auth.client_schemes import ClientAuthScheme
from crewai.a2a.auth.server_schemes import ServerAuthScheme
from crewai.a2a.extensions.base import ValidatedA2AExtension
from crewai.a2a.types import ProtocolVersion, TransportType, Url
from crewai.utilities.pydantic_schema_utils import serialize_model_class
try:
@@ -399,6 +401,11 @@ class A2AConfig(BaseModel):
default=None,
description="Optional Pydantic model for structured A2A agent responses",
)
@field_serializer("response_model", when_used="json")
def _serialize_response_model(self, value: Any) -> Any:
return serialize_model_class(value)
fail_fast: bool = Field(
default=True,
description="If True, raise error when agent unreachable; if False, skip",
@@ -488,6 +495,11 @@ class A2AClientConfig(BaseModel):
default=None,
description="Optional Pydantic model for structured A2A agent responses",
)
@field_serializer("response_model", when_used="json")
def _serialize_response_model(self, value: Any) -> Any:
return serialize_model_class(value)
fail_fast: bool = Field(
default=True,
description="If True, raise error when agent unreachable; if False, skip",

View File

@@ -28,7 +28,6 @@ from pydantic import (
ConfigDict,
Field,
PrivateAttr,
ValidationError,
model_validator,
)
from pydantic.functional_serializers import PlainSerializer
@@ -1110,9 +1109,14 @@ class Agent(BaseAgent):
"""
if self.agent_executor is None:
raise RuntimeError("Agent executor is not initialized.")
if not isinstance(self.llm, BaseLLM):
raise RuntimeError(
"LLM must be resolved before updating agent executor parameters."
)
if task is not None:
self.agent_executor.task = task
self.agent_executor.llm = self.llm
self.agent_executor.tools = tools
self.agent_executor.original_tools = raw_tools
self.agent_executor.prompt = prompt
@@ -1412,6 +1416,11 @@ class Agent(BaseAgent):
if _is_resuming_agent_executor(self.agent_executor):
executor = self.agent_executor
if not isinstance(self.llm, BaseLLM):
raise RuntimeError(
"LLM must be resolved before resuming agent executor."
)
executor.llm = self.llm
executor.tools = parsed_tools
executor.tools_names = get_tool_names(parsed_tools)
executor.tools_description = render_text_description_and_args(parsed_tools)
@@ -1692,32 +1701,24 @@ class Agent(BaseAgent):
elif response_format:
raw_output = str(output) if not isinstance(output, str) else output
try:
formatted_result = response_format.model_validate_json(raw_output)
except ValidationError:
# Direct JSON validation failed; fall back to converter-based parsing below.
formatted_result = None
model_schema = generate_model_description(response_format)
schema = json.dumps(model_schema, indent=2)
instructions = I18N_DEFAULT.slice("formatted_task_instructions").format(
output_format=schema
)
if formatted_result is None:
try:
model_schema = generate_model_description(response_format)
schema = json.dumps(model_schema, indent=2)
instructions = I18N_DEFAULT.slice(
"formatted_task_instructions"
).format(output_format=schema)
converter = Converter(
llm=cast(BaseLLM, self.llm),
text=raw_output,
model=response_format,
instructions=instructions,
)
converter = Converter(
llm=cast(BaseLLM, self.llm),
text=raw_output,
model=response_format,
instructions=instructions,
)
conversion_result = converter.to_pydantic()
if isinstance(conversion_result, BaseModel):
formatted_result = conversion_result
except ConverterError:
# Conversion failure is non-fatal; raw output is preserved below.
pass
conversion_result = converter.to_pydantic()
if isinstance(conversion_result, BaseModel):
formatted_result = conversion_result
except ConverterError:
pass
else:
raw_output = str(output) if not isinstance(output, str) else output

View File

@@ -350,10 +350,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
enforce_rpm_limit(self.request_within_rpm_limit)
effective_response_model = (
None if self.original_tools else self.response_model
)
answer = get_llm_response(
llm=cast("BaseLLM", self.llm),
messages=self.messages,
@@ -361,11 +357,11 @@ class CrewAgentExecutor(BaseAgentExecutor):
printer=PRINTER,
from_task=self.task,
from_agent=self.agent,
response_model=effective_response_model,
response_model=self.response_model,
executor_context=self,
verbose=self.agent.verbose,
)
if effective_response_model is not None:
if self.response_model is not None:
try:
if isinstance(answer, BaseModel):
output_json = answer.model_dump_json()
@@ -506,7 +502,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
available_functions=None,
from_task=self.task,
from_agent=self.agent,
response_model=None,
response_model=self.response_model,
executor_context=self,
verbose=self.agent.verbose,
)
@@ -1165,10 +1161,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
enforce_rpm_limit(self.request_within_rpm_limit)
effective_response_model = (
None if self.original_tools else self.response_model
)
answer = await aget_llm_response(
llm=cast("BaseLLM", self.llm),
messages=self.messages,
@@ -1176,12 +1168,12 @@ class CrewAgentExecutor(BaseAgentExecutor):
printer=PRINTER,
from_task=self.task,
from_agent=self.agent,
response_model=effective_response_model,
response_model=self.response_model,
executor_context=self,
verbose=self.agent.verbose,
)
if effective_response_model is not None:
if self.response_model is not None:
try:
if isinstance(answer, BaseModel):
output_json = answer.model_dump_json()
@@ -1322,7 +1314,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
available_functions=None,
from_task=self.task,
from_agent=self.agent,
response_model=None,
response_model=self.response_model,
executor_context=self,
verbose=self.agent.verbose,
)

View File

@@ -443,16 +443,20 @@ class Crew(FlowTrackable, BaseModel):
if node.event.type == "task_started" and node.event.task_id:
started_task_ids.add(node.event.task_id)
is_hierarchical = self.process == Process.hierarchical
resuming_task_agent_roles: set[str] = set()
for task in self.tasks:
if (
task.output is None
and task.agent is not None
and str(task.id) in started_task_ids
):
resuming_task_agent_roles.add(task.agent.role)
if task.output is not None or str(task.id) not in started_task_ids:
continue
executing_agent = self.manager_agent if is_hierarchical else task.agent
if executing_agent is not None:
resuming_task_agent_roles.add(executing_agent.role)
for agent in self.agents:
candidate_agents: list[BaseAgent] = list(self.agents)
if self.manager_agent is not None:
candidate_agents.append(self.manager_agent)
for agent in candidate_agents:
agent.crew = self
executor = agent.agent_executor
if (
@@ -467,7 +471,7 @@ class Crew(FlowTrackable, BaseModel):
agent.agent_executor = None
for task in self.tasks:
if task.agent is not None:
for agent in self.agents:
for agent in candidate_agents:
if agent.role == task.agent.role:
task.agent = agent
if agent.agent_executor is not None and task.output is None:
@@ -536,25 +540,9 @@ class Crew(FlowTrackable, BaseModel):
if state is None:
return
# Restore crew scope and the in-progress task scope. Inner scopes
# (agent, llm, tool) are re-created by the executor on resume.
stack: list[tuple[str, str]] = []
if self._kickoff_event_id:
stack.append((self._kickoff_event_id, "crew_kickoff_started"))
# Find the task_started event for the in-progress task (skipped on resume)
for task in self.tasks:
if task.output is None:
task_id_str = str(task.id)
for node in state.event_record.nodes.values():
if (
node.event.type == "task_started"
and node.event.task_id == task_id_str
):
stack.append((node.event.event_id, "task_started"))
break
break
restore_event_scope(tuple(stack))
# Restore last_event_id and emission counter from the record

View File

@@ -138,6 +138,36 @@ def restore_event_scope(stack: tuple[tuple[str, str], ...]) -> None:
_event_id_stack.set(stack)
def resume_task_scope(task_id: str) -> bool:
"""Push the latest recorded ``task_started`` scope for a task.
Args:
task_id: The task identifier to look up in the active event record.
Returns:
``True`` if a prior scope was pushed; ``False`` otherwise.
"""
from crewai.events.event_bus import crewai_event_bus
state = crewai_event_bus._runtime_state
if state is None:
return False
latest_event_id: str | None = None
latest_seq = -1
for node in list(state.event_record.nodes.values()):
ev = node.event
if ev.type != "task_started" or ev.task_id != task_id:
continue
seq = ev.emission_sequence or 0
if seq > latest_seq:
latest_seq = seq
latest_event_id = ev.event_id
if latest_event_id is None:
return False
push_event_scope(latest_event_id, "task_started")
return True
def push_event_scope(event_id: str, event_type: str = "") -> None:
"""Push an event ID and type onto the scope stack."""
config = _event_context_config.get() or _default_config

View File

@@ -173,8 +173,10 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
executor_type: Literal["experimental"] = "experimental"
suppress_flow_events: bool = True # always suppress for executor
llm: BaseLLM = Field(exclude=True)
prompt: SystemPromptResult | StandardPromptResult = Field(exclude=True)
llm: BaseLLM | None = Field(default=None, exclude=True)
prompt: SystemPromptResult | StandardPromptResult | None = Field(
default=None, exclude=True
)
max_iter: int = Field(default=25, exclude=True)
tools: list[CrewStructuredTool] = Field(default_factory=list, exclude=True)
tools_names: str = Field(default="", exclude=True)
@@ -1224,10 +1226,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
try:
enforce_rpm_limit(self.request_within_rpm_limit)
effective_response_model = (
None if self.original_tools else self.response_model
)
answer = get_llm_response(
llm=self.llm,
messages=list(self.state.messages),
@@ -1235,7 +1233,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
printer=PRINTER,
from_task=self.task,
from_agent=self.agent,
response_model=effective_response_model,
response_model=self.response_model,
executor_context=self,
verbose=self.agent.verbose,
)
@@ -1323,7 +1321,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
available_functions=None,
from_task=self.task,
from_agent=self.agent,
response_model=None,
response_model=self.response_model,
executor_context=self,
verbose=self.agent.verbose,
)
@@ -2589,6 +2587,11 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
self._kickoff_input = inputs.get("input", "")
if self.llm is None or self.prompt is None:
raise RuntimeError(
"AgentExecutor.llm or .prompt is unset; the executor was "
"not fully restored or initialized before execution."
)
if "system" in self.prompt:
from crewai.llms.cache import mark_cache_breakpoint
@@ -2690,6 +2693,11 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
self._kickoff_input = inputs.get("input", "")
if self.llm is None or self.prompt is None:
raise RuntimeError(
"AgentExecutor.llm or .prompt is unset; the executor was "
"not fully restored or initialized before execution."
)
if "system" in self.prompt:
from crewai.llms.cache import mark_cache_breakpoint

View File

@@ -23,7 +23,7 @@ from pydantic import (
BaseModel,
Field,
PrivateAttr,
ValidationError,
field_serializer,
field_validator,
model_validator,
)
@@ -95,7 +95,10 @@ from crewai.utilities.guardrail import process_guardrail, serialize_guardrail_fo
from crewai.utilities.guardrail_types import GuardrailCallable, GuardrailType
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.llm_utils import create_llm
from crewai.utilities.pydantic_schema_utils import generate_model_description
from crewai.utilities.pydantic_schema_utils import (
generate_model_description,
serialize_model_class,
)
from crewai.utilities.token_counter_callback import TokenCalcHandler
from crewai.utilities.tool_utils import execute_tool_and_check_finality
from crewai.utilities.types import LLMMessage
@@ -236,6 +239,11 @@ class LiteAgent(FlowTrackable, BaseModel):
response_format: type[BaseModel] | None = Field(
default=None, description="Pydantic model for structured output"
)
@field_serializer("response_format", when_used="json")
def _serialize_response_format(self, value: Any) -> Any:
return serialize_model_class(value)
verbose: bool = Field(
default=False, description="Whether to print execution details"
)
@@ -640,38 +648,29 @@ class LiteAgent(FlowTrackable, BaseModel):
formatted_result = agent_finish.output
elif active_response_format:
try:
formatted_result = active_response_format.model_validate_json(
str(agent_finish.output)
model_schema = generate_model_description(active_response_format)
schema = json.dumps(model_schema, indent=2)
instructions = I18N_DEFAULT.slice("formatted_task_instructions").format(
output_format=schema
)
except ValidationError:
# Direct JSON validation failed; fall back to converter-based parsing below.
formatted_result = None
if formatted_result is None:
try:
model_schema = generate_model_description(active_response_format)
schema = json.dumps(model_schema, indent=2)
instructions = I18N_DEFAULT.slice(
"formatted_task_instructions"
).format(output_format=schema)
converter = Converter(
llm=self.llm,
text=agent_finish.output,
model=active_response_format,
instructions=instructions,
)
converter = Converter(
llm=self.llm,
text=agent_finish.output,
model=active_response_format,
instructions=instructions,
result = converter.to_pydantic()
if isinstance(result, BaseModel):
formatted_result = result
except ConverterError as e:
if self.verbose:
PRINTER.print(
content=f"Failed to parse output into response format after retries: {e.message}",
color="yellow",
)
result = converter.to_pydantic()
if isinstance(result, BaseModel):
formatted_result = result
except ConverterError as e:
if self.verbose:
PRINTER.print(
content=f"Failed to parse output into response format after retries: {e.message}",
color="yellow",
)
# Calculate token usage metrics
if isinstance(self.llm, BaseLLM):
usage_metrics = self.llm.get_token_usage_summary()

View File

@@ -23,6 +23,7 @@ from pydantic import (
ConfigDict,
Field,
PrivateAttr,
field_serializer,
model_validator,
)
from typing_extensions import TypedDict
@@ -42,6 +43,7 @@ from crewai.events.types.tool_usage_events import (
ToolUsageStartedEvent,
)
from crewai.types.usage_metrics import UsageMetrics
from crewai.utilities.pydantic_schema_utils import serialize_model_class
try:
@@ -159,6 +161,10 @@ class BaseLLM(BaseModel, ABC):
)
additional_params: dict[str, Any] = Field(default_factory=dict)
@field_serializer("response_format", when_used="json", check_fields=False)
def _serialize_response_format(self, value: Any) -> Any:
return serialize_model_class(value)
def __setattr__(self, name: str, value: Any) -> None:
if name in ("stop", "stop_sequences"):
if value is None:

View File

@@ -40,6 +40,7 @@ from crewai.agents.agent_builder.base_agent import BaseAgent, _resolve_agent
from crewai.context import reset_current_task_id, set_current_task_id
from crewai.core.providers.content_processor import process_content
from crewai.events.event_bus import crewai_event_bus
from crewai.events.event_context import resume_task_scope
from crewai.events.types.task_events import (
TaskCompletedEvent,
TaskFailedEvent,
@@ -661,7 +662,10 @@ class Task(BaseModel):
tools = tools or self.tools or []
self.processed_by_agents.add(agent.role)
if not (agent.agent_executor and agent.agent_executor._resuming):
executor = agent.agent_executor
if not (
executor and executor._resuming and resume_task_scope(str(self.id))
):
crewai_event_bus.emit(
self, TaskStartedEvent(context=context, task=self)
)
@@ -783,7 +787,10 @@ class Task(BaseModel):
tools = tools or self.tools or []
self.processed_by_agents.add(agent.role)
if not (agent.agent_executor and agent.agent_executor._resuming):
executor = agent.agent_executor
if not (
executor and executor._resuming and resume_task_scope(str(self.id))
):
crewai_event_bus.emit(
self, TaskStartedEvent(context=context, task=self)
)

View File

@@ -8,7 +8,6 @@ from inspect import Parameter, signature
import json
import threading
from typing import (
Annotated,
Any,
Generic,
ParamSpec,
@@ -22,10 +21,10 @@ from pydantic import (
ConfigDict,
Field,
GetCoreSchemaHandler,
PlainSerializer,
PrivateAttr,
computed_field,
create_model,
field_serializer,
field_validator,
)
from pydantic_core import CoreSchema, core_schema
@@ -145,15 +144,18 @@ class BaseTool(BaseModel, ABC):
default_factory=list,
description="List of environment variables used by the tool.",
)
args_schema: Annotated[
type[PydanticBaseModel],
PlainSerializer(_serialize_schema, return_type=dict | None, when_used="json"),
] = Field(
args_schema: type[PydanticBaseModel] = Field(
default=_ArgsSchemaPlaceholder,
validate_default=True,
description="The schema for the arguments that the tool accepts.",
)
@field_serializer("args_schema", when_used="json")
def _serialize_args_schema(
self, schema: type[PydanticBaseModel] | None
) -> dict[str, Any] | None:
return _serialize_schema(schema)
description_updated: bool = Field(
default=False, description="Flag to check if the description has been updated."
)

View File

@@ -782,6 +782,20 @@ def _inline_top_level_ref(schema: dict[str, Any]) -> dict[str, Any]:
return schema
def serialize_model_class(value: Any) -> Any:
"""Serialize a ``type[BaseModel]`` field value as its JSON schema.
Args:
value: A ``type[BaseModel]`` subclass, ``None``, or another union member.
Returns:
``value.model_json_schema()`` for model classes, ``value`` otherwise.
"""
if isinstance(value, type) and issubclass(value, BaseModel):
return value.model_json_schema()
return value
def create_model_from_schema( # type: ignore[no-any-unimported]
json_schema: dict[str, Any],
*,

View File

@@ -12,7 +12,6 @@ from typing import Any
from unittest.mock import AsyncMock, Mock, patch
import pytest
from pydantic import BaseModel
from crewai.agents.tools_handler import ToolsHandler as _ToolsHandler
from crewai.agents.step_executor import StepExecutor
@@ -109,9 +108,6 @@ class TestAgentExecutorState:
class TestAgentExecutor:
"""Test AgentExecutor class."""
class StructuredResult(BaseModel):
value: str
@pytest.fixture
def mock_dependencies(self):
"""Create mock dependencies for executor."""
@@ -219,49 +215,6 @@ class TestAgentExecutor:
assert result == "check_iteration"
def test_call_llm_and_parse_does_not_pass_response_model_with_tools(
self, mock_dependencies
):
"""Structured output should not be requested during ReAct tool loops."""
executor = _build_executor(
**mock_dependencies,
original_tools=[Mock()],
response_model=self.StructuredResult,
callbacks=[],
)
executor.state.messages = [{"role": "user", "content": "Use a tool"}]
with patch(
"crewai.experimental.agent_executor.get_llm_response",
return_value="Thought: done\nFinal Answer: complete",
) as get_llm_response_mock:
result = executor.call_llm_and_parse()
assert result == "parsed"
assert get_llm_response_mock.call_args.kwargs["response_model"] is None
def test_call_llm_native_tools_does_not_pass_response_model_with_tools(
self, mock_dependencies
):
"""Structured output should not be requested during native tool calls."""
executor = _build_executor(
**mock_dependencies,
original_tools=[Mock()],
response_model=self.StructuredResult,
callbacks=[],
)
executor._openai_tools = [{"type": "function", "function": {"name": "lookup"}}]
executor.state.messages = [{"role": "user", "content": "Use a tool"}]
with patch(
"crewai.experimental.agent_executor.get_llm_response",
return_value="complete",
) as get_llm_response_mock:
result = executor.call_llm_native_tools()
assert result == "native_finished"
assert get_llm_response_mock.call_args.kwargs["response_model"] is None
def test_finalize_success(self, mock_dependencies):
"""Test finalize with valid AgentFinish."""
with patch.object(AgentExecutor, "_show_logs") as mock_show_logs:

View File

@@ -8,8 +8,18 @@ interactions:
[{"description": "Add two numbers together and return the sum.", "name": "add_numbers",
"parameters_json_schema": {"properties": {"a": {"title": "A", "type": "integer"},
"b": {"title": "B", "type": "integer"}}, "required": ["a", "b"], "type": "object",
"additionalProperties": false}}]}], "generationConfig": {"stopSequences": ["\nObservation:"],
"thinkingConfig": {"include_thoughts": true}}}'
"additionalProperties": false}}, {"description": "Use this tool to provide your
final structured response. Call this tool when you have gathered all necessary
information and are ready to provide the final answer in the required format.",
"name": "structured_output", "parameters_json_schema": {"description": "Structured
output for calculation results.", "properties": {"operation": {"description":
"The mathematical operation performed", "title": "Operation", "type": "string"},
"result": {"description": "The result of the calculation", "title": "Result",
"type": "integer"}, "explanation": {"description": "Brief explanation of the
calculation", "title": "Explanation", "type": "string"}}, "required": ["operation",
"result", "explanation"], "title": "CalculationResult", "type": "object", "additionalProperties":
false, "propertyOrdering": ["operation", "result", "explanation"]}}]}], "generationConfig":
{"stopSequences": ["\nObservation:"]}}'
headers:
User-Agent:
- X-USER-AGENT-XXX
@@ -20,13 +30,13 @@ interactions:
connection:
- keep-alive
content-length:
- '784'
- '1592'
content-type:
- application/json
host:
- generativelanguage.googleapis.com
x-goog-api-client:
- google-genai-sdk/1.65.0 gl-python/3.13.3
- google-genai-sdk/1.49.0 gl-python/3.13.3
x-goog-api-key:
- X-GOOG-API-KEY-XXX
method: POST
@@ -34,36 +44,27 @@ interactions:
response:
body:
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
[\n {\n \"text\": \"Okay, here's my thought process on
tackling this simple addition problem:\\n\\n**Calculating 15 + 27**\\n\\nRight,
so I need to calculate 15 plus 27. It's a straightforward arithmetic operation,
and I know I have a dedicated tool for that \u2013 the `add_numbers` function.
No need to reinvent the wheel, I should use the appropriate tool in my toolbox.\\n\\nTo
use the tool effectively, I need to provide the correct inputs. In this case,
I'll set `a` equal to 15, and `b` equal to 27. That should be a piece of cake
for the function. Now I just need to call it!\\n\",\n \"thought\":
true\n },\n {\n \"functionCall\": {\n \"name\":
\"add_numbers\",\n \"args\": {\n \"a\": 15,\n
\ \"b\": 27\n }\n },\n \"thoughtSignature\":
\"CqwBAQw51sfkbjIDS7wS47oc/Bej+AYsUu5Cs+BC2Ae5NMasRmWa/u5Ct426qkpkIpgzGSNjwwfitf1gK93Abse9EGj5m1swXmPU2XSkqhMYMEXZGH1mW2U2XH8zaXHRIAx2aI0O8VbJ3sL8h1lJgbVCkvLa2RyWwY6E8FRPhQOHtrOQEQtfAUtHdJz928j6UEgS818X/7hEwuWsQhIho0frtziX30UlI7yXOeBBWw==\"\n
[\n {\n \"functionCall\": {\n \"name\": \"add_numbers\",\n
\ \"args\": {\n \"a\": 15,\n \"b\":
27\n }\n },\n \"thoughtSignature\": \"CqMFAb4+9vtoEAola3khZd5LD4cccGlQsdVI9cPJGQBURT0qF5Xqp8o1L7oGN4s5trQpk7NPhKe1CYDMXDJueC7zM/zGlcy2daSJAeuTd9pxAbtndEXCGjM/9Nt8QRpvaDV3Ff2bkKSn/JCOJdzsN5m6G5C6BMRGVt8bZyRHelwu7tjCNYiMEvFqVoQIWN6d+CWKkHnbSwOlSUTDXJEcWvUwP82Ou7s68l2k7XNbDWCY5Tt8LUdPgeqjfH15JoEgZUbPxbVKA0ykRln1svfpvQ4Vm3Hn7PL3voWZWGzP5uLnH6JF2M8H6TokSDYZETvlDo5bK1Cx9IzrdUgHkku6gNbct/e53CPEUgqSKbY1VhsLAXAHieT4PKqeMQ4B+7gyCLXHeL6TOGjqSVGBBOQLtF9yCbKbkXa5pPu3+DnPhoOeH7jEPb+bqIWv6rxERErbKhu0IlP+UNBRAAj+wXNDZxQvLnlrlXrLtWllO9wFshr1DzgDgNZSRsPQeVQq2L0bL+KRobCXAfjMpH/8bhxdTI3sgsCtU3+dKwV5Z8Fg6e5oRyBAss8AE2CmYtdnYpt+iss9IT8NlSpI2DcdmVErEFNsebVcSwnr+9YXoESh4O1i8er9lX59hKTBdYXdP2GJ63cq9cSOalzx/doKxA2FzP3QhdV+H11LiUQzsQCXHqv0D+D290z1QoPhpsHEd7b/1EoW7D/2rub4acV8tpUcG2oe/Mj1kzYQoiEwZkgM56JoUs++5+5tWBMW68e4y1AmkyhDTCDkiNIa4noE6AOdNsLjL/+EHvcNFRmayFXXiUShIcMT0WQ9xNriWQP/dbhd6F5K7BKSajdB1391OYeHVmSEzzXYxjnUWXd+jqORQcsiPNIVRQkZI7ZGl6+4exmZsfrKzbFy\"\n
\ }\n ],\n \"role\": \"model\"\n },\n \"finishReason\":
\"STOP\",\n \"index\": 0,\n \"finishMessage\": \"Model generated
function call(s).\"\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
104,\n \"candidatesTokenCount\": 22,\n \"totalTokenCount\": 173,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 104\n
\ }\n ],\n \"thoughtsTokenCount\": 47,\n \"serviceTier\": \"standard\"\n
\ },\n \"modelVersion\": \"gemini-2.5-flash\",\n \"responseId\": \"MpgPapmwO_vN-sAPvrWJmA0\"\n}\n"
202,\n \"candidatesTokenCount\": 22,\n \"totalTokenCount\": 403,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 202\n
\ }\n ],\n \"thoughtsTokenCount\": 179\n },\n \"modelVersion\":
\"gemini-2.5-flash\",\n \"responseId\": \"AlCOadrrK7aVjMcPksrU-A0\"\n}\n"
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Type:
- application/json; charset=UTF-8
Date:
- Thu, 21 May 2026 23:41:40 GMT
- Thu, 12 Feb 2026 22:11:14 GMT
Server:
- scaffolding on HTTPServer2
Server-Timing:
- gfet4t7; dur=1920
- gfet4t7; dur=1417
Transfer-Encoding:
- chunked
Vary:
@@ -74,8 +75,6 @@ interactions:
- X-CONTENT-TYPE-XXX
X-Frame-Options:
- X-FRAME-OPTIONS-XXX
X-Gemini-Service-Tier:
- standard
X-XSS-Protection:
- '0'
status:
@@ -84,7 +83,7 @@ interactions:
- request:
body: '{"contents": [{"parts": [{"text": "\nCurrent Task: Calculate 15 + 27 using
your add_numbers tool. Report the result."}], "role": "user"}, {"parts": [{"functionCall":
{"args": {"a": 15, "b": 27}, "name": "add_numbers"}, "thoughtSignature": "CqwBAQw51sfkbjIDS7wS47oc_Bej-AYsUu5Cs-BC2Ae5NMasRmWa_u5Ct426qkpkIpgzGSNjwwfitf1gK93Abse9EGj5m1swXmPU2XSkqhMYMEXZGH1mW2U2XH8zaXHRIAx2aI0O8VbJ3sL8h1lJgbVCkvLa2RyWwY6E8FRPhQOHtrOQEQtfAUtHdJz928j6UEgS818X_7hEwuWsQhIho0frtziX30UlI7yXOeBBWw=="}],
{"args": {"a": 15, "b": 27}, "name": "add_numbers"}, "thoughtSignature": "CqMFAb4-9vtoEAola3khZd5LD4cccGlQsdVI9cPJGQBURT0qF5Xqp8o1L7oGN4s5trQpk7NPhKe1CYDMXDJueC7zM_zGlcy2daSJAeuTd9pxAbtndEXCGjM_9Nt8QRpvaDV3Ff2bkKSn_JCOJdzsN5m6G5C6BMRGVt8bZyRHelwu7tjCNYiMEvFqVoQIWN6d-CWKkHnbSwOlSUTDXJEcWvUwP82Ou7s68l2k7XNbDWCY5Tt8LUdPgeqjfH15JoEgZUbPxbVKA0ykRln1svfpvQ4Vm3Hn7PL3voWZWGzP5uLnH6JF2M8H6TokSDYZETvlDo5bK1Cx9IzrdUgHkku6gNbct_e53CPEUgqSKbY1VhsLAXAHieT4PKqeMQ4B-7gyCLXHeL6TOGjqSVGBBOQLtF9yCbKbkXa5pPu3-DnPhoOeH7jEPb-bqIWv6rxERErbKhu0IlP-UNBRAAj-wXNDZxQvLnlrlXrLtWllO9wFshr1DzgDgNZSRsPQeVQq2L0bL-KRobCXAfjMpH_8bhxdTI3sgsCtU3-dKwV5Z8Fg6e5oRyBAss8AE2CmYtdnYpt-iss9IT8NlSpI2DcdmVErEFNsebVcSwnr-9YXoESh4O1i8er9lX59hKTBdYXdP2GJ63cq9cSOalzx_doKxA2FzP3QhdV-H11LiUQzsQCXHqv0D-D290z1QoPhpsHEd7b_1EoW7D_2rub4acV8tpUcG2oe_Mj1kzYQoiEwZkgM56JoUs--5-5tWBMW68e4y1AmkyhDTCDkiNIa4noE6AOdNsLjL_-EHvcNFRmayFXXiUShIcMT0WQ9xNriWQP_dbhd6F5K7BKSajdB1391OYeHVmSEzzXYxjnUWXd-jqORQcsiPNIVRQkZI7ZGl6-4exmZsfrKzbFy"}],
"role": "model"}, {"parts": [{"functionResponse": {"name": "add_numbers", "response":
{"result": 42}}}], "role": "user"}], "systemInstruction": {"parts": [{"text":
"You are Calculator. You are a calculator assistant that uses tools to compute
@@ -93,8 +92,18 @@ interactions:
numbers together and return the sum.", "name": "add_numbers", "parameters_json_schema":
{"properties": {"a": {"title": "A", "type": "integer"}, "b": {"title": "B",
"type": "integer"}}, "required": ["a", "b"], "type": "object", "additionalProperties":
false}}]}], "generationConfig": {"stopSequences": ["\nObservation:"], "thinkingConfig":
{"include_thoughts": true}}}'
false}}, {"description": "Use this tool to provide your final structured response.
Call this tool when you have gathered all necessary information and are ready
to provide the final answer in the required format.", "name": "structured_output",
"parameters_json_schema": {"description": "Structured output for calculation
results.", "properties": {"operation": {"description": "The mathematical operation
performed", "title": "Operation", "type": "string"}, "result": {"description":
"The result of the calculation", "title": "Result", "type": "integer"}, "explanation":
{"description": "Brief explanation of the calculation", "title": "Explanation",
"type": "string"}}, "required": ["operation", "result", "explanation"], "title":
"CalculationResult", "type": "object", "additionalProperties": false, "propertyOrdering":
["operation", "result", "explanation"]}}]}], "generationConfig": {"stopSequences":
["\nObservation:"]}}'
headers:
User-Agent:
- X-USER-AGENT-XXX
@@ -105,13 +114,13 @@ interactions:
connection:
- keep-alive
content-length:
- '1249'
- '2725'
content-type:
- application/json
host:
- generativelanguage.googleapis.com
x-goog-api-client:
- google-genai-sdk/1.65.0 gl-python/3.13.3
- google-genai-sdk/1.49.0 gl-python/3.13.3
x-goog-api-key:
- X-GOOG-API-KEY-XXX
method: POST
@@ -119,24 +128,28 @@ interactions:
response:
body:
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
[\n {\n \"text\": \"The sum of 15 and 27 is 42.\"\n }\n
\ ],\n \"role\": \"model\"\n },\n \"finishReason\":
\"STOP\",\n \"index\": 0\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
142,\n \"candidatesTokenCount\": 15,\n \"totalTokenCount\": 157,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 142\n
\ }\n ],\n \"serviceTier\": \"standard\"\n },\n \"modelVersion\":
\"gemini-2.5-flash\",\n \"responseId\": \"NZgPasTKBf3F-sAP4Lu48Ak\"\n}\n"
[\n {\n \"functionCall\": {\n \"name\": \"structured_output\",\n
\ \"args\": {\n \"result\": 42,\n \"explanation\":
\"The sum of 15 and 27 is 42.\",\n \"operation\": \"Addition\"\n
\ }\n },\n \"thoughtSignature\": \"CtYCAb4+9vsKJoVFV1W8ORKk+Likt7GS9CuzuE53V9sbS2gFuiEjJ7ghBqWDG2UrgyRYFjPl6EalXUBnEbEq9rZNYGY27VpcweI1tv6p+477bgz1pmZnL0nfAcrp4nuphL+Ij0nXZQoo5cF4Gk29RQSNy49VRn3eP9eUW0hG7EpkPmfJiUSSDuaQENHN1UBBnFS9QUC+Fw+unnQ10B57fauyiXWNrBUkE2PYqgj5vELa5lVMtk5beh4ydWNnZ04t8gvQniCJ38EWWQr8VAXrSqE156oCBMwkFaFM7huPWHZk53n/HAG/VsQgPayf045STWKWjBzp6uTiwH9pYtoI1LBah3uxVbJRKOzH7HI4U0cHsffQqIIUn8cW4SP1UK/nvAivU1l0p6Bot8KIVJ5vqoF+o2oDmTuZv0HkDo5+UvXRqfsO5AylpUdM+JMGaXVAA7oZNqVPQybw\"\n
\ }\n ],\n \"role\": \"model\"\n },\n \"finishReason\":
\"STOP\",\n \"index\": 0,\n \"finishMessage\": \"Model generated
function call(s).\"\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
240,\n \"candidatesTokenCount\": 39,\n \"totalTokenCount\": 357,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 240\n
\ }\n ],\n \"thoughtsTokenCount\": 78\n },\n \"modelVersion\":
\"gemini-2.5-flash\",\n \"responseId\": \"A1COaaWbKvKGjMcPsN-EkAs\"\n}\n"
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Type:
- application/json; charset=UTF-8
Date:
- Thu, 21 May 2026 23:41:41 GMT
- Thu, 12 Feb 2026 22:11:15 GMT
Server:
- scaffolding on HTTPServer2
Server-Timing:
- gfet4t7; dur=750
- gfet4t7; dur=906
Transfer-Encoding:
- chunked
Vary:
@@ -147,107 +160,6 @@ interactions:
- X-CONTENT-TYPE-XXX
X-Frame-Options:
- X-FRAME-OPTIONS-XXX
X-Gemini-Service-Tier:
- standard
X-XSS-Protection:
- '0'
status:
code: 200
message: OK
- request:
body: '{"contents": [{"parts": [{"text": "The sum of 15 and 27 is 42."}], "role":
"user"}], "systemInstruction": {"parts": [{"text": "Format your final answer
according to the following OpenAPI schema: {\n \"type\": \"json_schema\",\n \"json_schema\":
{\n \"name\": \"CalculationResult\",\n \"strict\": true,\n \"schema\":
{\n \"description\": \"Structured output for calculation results.\",\n \"properties\":
{\n \"operation\": {\n \"description\": \"The mathematical operation
performed\",\n \"title\": \"Operation\",\n \"type\": \"string\"\n },\n \"result\":
{\n \"description\": \"The result of the calculation\",\n \"title\":
\"Result\",\n \"type\": \"integer\"\n },\n \"explanation\":
{\n \"description\": \"Brief explanation of the calculation\",\n \"title\":
\"Explanation\",\n \"type\": \"string\"\n }\n },\n \"required\":
[\n \"operation\",\n \"result\",\n \"explanation\"\n ],\n \"title\":
\"CalculationResult\",\n \"type\": \"object\",\n \"additionalProperties\":
false\n }\n }\n}\n\nIMPORTANT: Preserve the original content exactly as-is.
Do NOT rewrite, paraphrase, or modify the meaning of the content. Only structure
it to match the schema format.\n\nDo not include the OpenAPI schema in the final
output. Ensure the final output does not include any code block markers like
```json or ```python."}], "role": "user"}, "generationConfig": {"responseMimeType":
"application/json", "responseJsonSchema": {"description": "Structured output
for calculation results.", "properties": {"operation": {"description": "The
mathematical operation performed", "title": "Operation", "type": "string"},
"result": {"description": "The result of the calculation", "title": "Result",
"type": "integer"}, "explanation": {"description": "Brief explanation of the
calculation", "title": "Explanation", "type": "string"}}, "required": ["operation",
"result", "explanation"], "title": "CalculationResult", "type": "object", "additionalProperties":
false, "propertyOrdering": ["operation", "result", "explanation"]}, "thinkingConfig":
{"include_thoughts": true}}}'
headers:
User-Agent:
- X-USER-AGENT-XXX
accept:
- '*/*'
accept-encoding:
- ACCEPT-ENCODING-XXX
connection:
- keep-alive
content-length:
- '2247'
content-type:
- application/json
host:
- generativelanguage.googleapis.com
x-goog-api-client:
- google-genai-sdk/1.65.0 gl-python/3.13.3
x-goog-api-key:
- X-GOOG-API-KEY-XXX
method: POST
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
response:
body:
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
[\n {\n \"text\": \"**Analyzing the User's Statement**\\n\\nOkay,
so the user has given me the statement: \\\"The sum of 15 and 27 is 42.\\\"
My task is to break this down into a structured JSON object according to the
`CalculationResult` schema I'm working with. This is straightforward; let's
extract the key pieces:\\n\\nFirst, the **operation**: The word \\\"sum\\\"
is a clear indicator that we're dealing with addition. No ambiguity there.\\n\\nNext,
the **result**: The statement explicitly tells us that the result \\\"is 42\\\".
That's a direct, easily extracted value.\\n\\nFinally, the **explanation**:
I can use the entire original statement, \\\"The sum of 15 and 27 is 42.\\\",
as the explanation. It's concise and perfectly encapsulates the context of
the calculation.\\n\",\n \"thought\": true\n },\n {\n
\ \"text\": \"{\\\"operation\\\":\\\"addition\\\",\\\"result\\\":42,\\\"explanation\\\":\\\"The
sum of 15 and 27 is 42.\\\"}\"\n }\n ],\n \"role\":
\"model\"\n },\n \"finishReason\": \"STOP\",\n \"index\": 0\n
\ }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\": 321,\n \"candidatesTokenCount\":
28,\n \"totalTokenCount\": 480,\n \"promptTokensDetails\": [\n {\n
\ \"modality\": \"TEXT\",\n \"tokenCount\": 321\n }\n ],\n
\ \"thoughtsTokenCount\": 131,\n \"serviceTier\": \"standard\"\n },\n
\ \"modelVersion\": \"gemini-2.5-flash\",\n \"responseId\": \"NZgPasDuKf2VjMcPwc6D4Ak\"\n}\n"
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Type:
- application/json; charset=UTF-8
Date:
- Thu, 21 May 2026 23:41:44 GMT
Server:
- scaffolding on HTTPServer2
Server-Timing:
- gfet4t7; dur=2425
Transfer-Encoding:
- chunked
Vary:
- Origin
- X-Origin
- Referer
X-Content-Type-Options:
- X-CONTENT-TYPE-XXX
X-Frame-Options:
- X-FRAME-OPTIONS-XXX
X-Gemini-Service-Tier:
- standard
X-XSS-Protection:
- '0'
status:

View File

@@ -23,8 +23,17 @@ interactions:
numbers together and return the sum.", "name": "add_numbers", "parameters_json_schema":
{"properties": {"a": {"title": "A", "type": "integer"}, "b": {"title": "B",
"type": "integer"}}, "required": ["a", "b"], "type": "object", "additionalProperties":
false}}]}], "generationConfig": {"stopSequences": ["\nObservation:"], "thinkingConfig":
{"include_thoughts": true}}}'
false}}, {"description": "Use this tool to provide your final structured response.
Call this tool when you have gathered all necessary information and are ready
to provide the final answer in the required format.", "name": "structured_output",
"parameters_json_schema": {"properties": {"operation": {"description": "The
mathematical operation performed", "title": "Operation", "type": "string"},
"result": {"description": "The result of the calculation", "title": "Result",
"type": "integer"}, "explanation": {"description": "Brief explanation of the
calculation", "title": "Explanation", "type": "string"}}, "required": ["operation",
"result", "explanation"], "title": "CalculationResult", "type": "object", "additionalProperties":
false, "propertyOrdering": ["operation", "result", "explanation"]}}]}], "generationConfig":
{"stopSequences": ["\nObservation:"]}}'
headers:
User-Agent:
- X-USER-AGENT-XXX
@@ -35,67 +44,41 @@ interactions:
connection:
- keep-alive
content-length:
- '2016'
- '2763'
content-type:
- application/json
host:
- generativelanguage.googleapis.com
x-goog-api-client:
- google-genai-sdk/1.65.0 gl-python/3.13.3
- google-genai-sdk/1.49.0 gl-python/3.13.12
x-goog-api-key:
- X-GOOG-API-KEY-XXX
method: POST
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-001:generateContent
response:
body:
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
[\n {\n \"text\": \"**My Calculation Process**\\n\\nAlright,
the user wants me to compute 15 + 27. Simple enough. I have a tool specifically
designed for this: `add_numbers(a: int, b: int)`. This is ideal; I'll utilize
that.\\n\\nSo, first step: I need to call the `add_numbers` tool. The inputs
are straightforward: `a` should be 15 and `b` should be 27. I'll execute that
call immediately.\\n\\nOnce I have the result \u2013 the sum \u2013 in hand,
I need to format the output. The user is specific about the structure; it
needs to be a JSON object conforming to that OpenAPI schema. Specifically,
the JSON should include: an \\\"operation\\\" field set to \\\"Addition\\\",
a \\\"result\\\" field populated with the calculated sum, and an \\\"explanation\\\"
field giving some context, which should be \\\"Added 15 and 27 together.\\\"\\n\\nTherefore,
my workflow is: call the tool with the provided numbers, get the sum, and
then construct the final JSON object in the required format.\\n\",\n \"thought\":
true\n },\n {\n \"text\": \"**My Reasoning for
Calculating 15 + 27**\\n\\nOkay, the user wants me to compute 15 plus 27.
No problem; that's straightforward. I've got a dedicated `add_numbers` tool
for this, which is exactly the right approach. Let's see...I need to call
that tool, providing `a=15` and `b=27` as input parameters. Once I receive
the sum, I can move on to formatting the result.\\n\\nNow, the important part:
the output needs to be a JSON object, specifically structured as dictated
by the OpenAPI schema. I need to make sure I include the \\\"operation\\\",
\\\"result\\\", and \\\"explanation\\\" fields. \\\"operation\\\" will be
\\\"Addition\\\", the \\\"result\\\" will naturally be the sum I get from
the tool, and the \\\"explanation\\\" will simply be \\\"Added 15 and 27 together.\\\"
That should cover everything, and the user should get a perfectly formatted
response.\\n\",\n \"thought\": true\n },\n {\n
\ \"functionCall\": {\n \"name\": \"add_numbers\",\n
[\n {\n \"functionCall\": {\n \"name\": \"add_numbers\",\n
\ \"args\": {\n \"a\": 15,\n \"b\":
27\n }\n },\n \"thoughtSignature\": \"CtkGAQw51sf8m1RivBZ7Zp+ZkaqxDdZzSlzepmvlCKak9gl6edIuej/pHxR5dg3qZf89XmHvQ6HigZyzqHcYbSvcRHVGbpNkTr62FC0g10oK5ZEp/r1otLIcgXoVgyFGguJPe/NsfWSX3Uc7ZaYgV0Q2MHBsjUmEHicPH0Pj4Xmbe2I1pK/9DPrzSQqZW3duhLBlBIF9RwZUiltPH6mK+k71l8bN/ebsbbZM18FMXf0wg/7lf3OjvY2wdLDNUD/F2M7T8yfi7NelPWorjIuTGOVWlVRsdGW0QEzuVoyYY7OfbBJC+XsmTumYt+vqgIR3jcQZlA5/3yJdj3e/3mrNmzGt+8VvkjUnu3pz0IUkq2SoTG0+6Y/ajsUI0YA/BFiAXHjrRhH1Nx3ihGWT7E7VzpU/E1ZPFMJIOPLRSpRv8G6ITnjZGthozTZtKLgoHCF7kx4Ni8eVdOh2Us6kY7tYpVabM1dmw0gextEEt+fBMoI+qZGkXdL1YW6SEtQBHh3BGKX8khcrqNvqPZDFzSG0iieMJq7abbEYAIc8zRkeGlWEdX6ES6e+njnFN7JX0Nc32GzOjmpgx9gRhYe+wKonqBQ9RwLLNK+lFuflLTrU3D8jMiPCJyvoRsjdjEc+2JtHXo14ibOVXvZ6oYCHsTEB7f+90/qzcrITESyDBD/rmiT/SqgitBa44MZE9CZ9Ml+BW0xd9FfCy4oLy8w4vszVFDw9Eotr4pEzdCeDeWjMn35taJnWf6jUeF0z/0iyHjbi7XRubJXxI2YuKQ+HRCKX1RFaJWLhmxO4JNBDBqfYZqsO/FefqxjWi2pRzE8U/Upp8Tv/hy1FoN9Abs8W6lPoqgOyEiOcpVkM+u0CgUbf87I1X2EiPpuJF4D9dHlEJqumiPqIGazSLnrjW1qqbM5UpQQuPoTC7q+G092CEnNJBIwrufddZPDfD9rqINpmMa+7OswldKViVaCWR3VsgrSXJj7lVRntCyE2atWxTvtQVnR/JLDdyc98CAUChtAPnC4K/K3OVI4jffQQsHmfeOnTyg0n2VnZ6Yhgo0lMdE4IfMrNOWOuNvHodeHisD2yXjvTCgScO8B3s+EJTvenHMert3nRgjNRmFZ0cRNSjbTeG0UlB9s7Uy0uyrn5ODkKIgEMOdbH9yJU53jInG9boFeMXb1qif47Pc72taZkl6ZaMK4=\"\n
\ }\n ],\n \"role\": \"model\"\n },\n \"finishReason\":
\"STOP\",\n \"index\": 0,\n \"finishMessage\": \"Model generated
function call(s).\"\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
383,\n \"candidatesTokenCount\": 22,\n \"totalTokenCount\": 658,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 383\n
\ }\n ],\n \"thoughtsTokenCount\": 253,\n \"serviceTier\": \"standard\"\n
\ },\n \"modelVersion\": \"gemini-2.5-flash\",\n \"responseId\": \"qagPas37Ba2R-8YPzYzI8AY\"\n}\n"
27\n }\n }\n }\n ],\n \"role\":
\"model\"\n },\n \"finishReason\": \"STOP\",\n \"avgLogprobs\":
4.3579145442760951e-06\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
377,\n \"candidatesTokenCount\": 7,\n \"totalTokenCount\": 384,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 377\n
\ }\n ],\n \"candidatesTokensDetails\": [\n {\n \"modality\":
\"TEXT\",\n \"tokenCount\": 7\n }\n ]\n },\n \"modelVersion\":
\"gemini-2.0-flash-001\",\n \"responseId\": \"vVefaYDSOouXjMcPicLCsQY\"\n}\n"
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Type:
- application/json; charset=UTF-8
Date:
- Fri, 22 May 2026 00:51:56 GMT
- Wed, 25 Feb 2026 20:12:46 GMT
Server:
- scaffolding on HTTPServer2
Server-Timing:
- gfet4t7; dur=3892
- gfet4t7; dur=718
Transfer-Encoding:
- chunked
Vary:
@@ -106,8 +89,6 @@ interactions:
- X-CONTENT-TYPE-XXX
X-Frame-Options:
- X-FRAME-OPTIONS-XXX
X-Gemini-Service-Tier:
- standard
X-XSS-Protection:
- '0'
status:
@@ -131,17 +112,27 @@ interactions:
the schema format.\n\nDo not include the OpenAPI schema in the final output.
Ensure the final output does not include any code block markers like ```json
or ```python."}], "role": "user"}, {"parts": [{"functionCall": {"args": {"a":
15, "b": 27}, "name": "add_numbers"}, "thoughtSignature": "CtkGAQw51sf8m1RivBZ7Zp-ZkaqxDdZzSlzepmvlCKak9gl6edIuej_pHxR5dg3qZf89XmHvQ6HigZyzqHcYbSvcRHVGbpNkTr62FC0g10oK5ZEp_r1otLIcgXoVgyFGguJPe_NsfWSX3Uc7ZaYgV0Q2MHBsjUmEHicPH0Pj4Xmbe2I1pK_9DPrzSQqZW3duhLBlBIF9RwZUiltPH6mK-k71l8bN_ebsbbZM18FMXf0wg_7lf3OjvY2wdLDNUD_F2M7T8yfi7NelPWorjIuTGOVWlVRsdGW0QEzuVoyYY7OfbBJC-XsmTumYt-vqgIR3jcQZlA5_3yJdj3e_3mrNmzGt-8VvkjUnu3pz0IUkq2SoTG0-6Y_ajsUI0YA_BFiAXHjrRhH1Nx3ihGWT7E7VzpU_E1ZPFMJIOPLRSpRv8G6ITnjZGthozTZtKLgoHCF7kx4Ni8eVdOh2Us6kY7tYpVabM1dmw0gextEEt-fBMoI-qZGkXdL1YW6SEtQBHh3BGKX8khcrqNvqPZDFzSG0iieMJq7abbEYAIc8zRkeGlWEdX6ES6e-njnFN7JX0Nc32GzOjmpgx9gRhYe-wKonqBQ9RwLLNK-lFuflLTrU3D8jMiPCJyvoRsjdjEc-2JtHXo14ibOVXvZ6oYCHsTEB7f-90_qzcrITESyDBD_rmiT_SqgitBa44MZE9CZ9Ml-BW0xd9FfCy4oLy8w4vszVFDw9Eotr4pEzdCeDeWjMn35taJnWf6jUeF0z_0iyHjbi7XRubJXxI2YuKQ-HRCKX1RFaJWLhmxO4JNBDBqfYZqsO_FefqxjWi2pRzE8U_Upp8Tv_hy1FoN9Abs8W6lPoqgOyEiOcpVkM-u0CgUbf87I1X2EiPpuJF4D9dHlEJqumiPqIGazSLnrjW1qqbM5UpQQuPoTC7q-G092CEnNJBIwrufddZPDfD9rqINpmMa-7OswldKViVaCWR3VsgrSXJj7lVRntCyE2atWxTvtQVnR_JLDdyc98CAUChtAPnC4K_K3OVI4jffQQsHmfeOnTyg0n2VnZ6Yhgo0lMdE4IfMrNOWOuNvHodeHisD2yXjvTCgScO8B3s-EJTvenHMert3nRgjNRmFZ0cRNSjbTeG0UlB9s7Uy0uyrn5ODkKIgEMOdbH9yJU53jInG9boFeMXb1qif47Pc72taZkl6ZaMK4="}],
"role": "model"}, {"parts": [{"functionResponse": {"name": "add_numbers", "response":
{"result": 42}}}], "role": "user"}], "systemInstruction": {"parts": [{"text":
"You are Calculator. You are a calculator assistant that uses tools to compute
results.\nYour personal goal is: Perform calculations using available tools"}],
"role": "user"}, "tools": [{"functionDeclarations": [{"description": "Add two
numbers together and return the sum.", "name": "add_numbers", "parameters_json_schema":
{"properties": {"a": {"title": "A", "type": "integer"}, "b": {"title": "B",
"type": "integer"}}, "required": ["a", "b"], "type": "object", "additionalProperties":
false}}]}], "generationConfig": {"stopSequences": ["\nObservation:"], "thinkingConfig":
{"include_thoughts": true}}}'
15, "b": 27}, "name": "add_numbers"}}], "role": "model"}, {"parts": [{"functionResponse":
{"name": "add_numbers", "response": {"result": 42}}}], "role": "user"}, {"parts":
[{"text": "Analyze the tool result. If requirements are met, provide the Final
Answer. Otherwise, call the next tool. Deliver only the answer without meta-commentary."}],
"role": "user"}], "systemInstruction": {"parts": [{"text": "You are Calculator.
You are a calculator assistant that uses tools to compute results.\nYour personal
goal is: Perform calculations using available tools"}], "role": "user"}, "tools":
[{"functionDeclarations": [{"description": "Add two numbers together and return
the sum.", "name": "add_numbers", "parameters_json_schema": {"properties": {"a":
{"title": "A", "type": "integer"}, "b": {"title": "B", "type": "integer"}},
"required": ["a", "b"], "type": "object", "additionalProperties": false}}, {"description":
"Use this tool to provide your final structured response. Call this tool when
you have gathered all necessary information and are ready to provide the final
answer in the required format.", "name": "structured_output", "parameters_json_schema":
{"properties": {"operation": {"description": "The mathematical operation performed",
"title": "Operation", "type": "string"}, "result": {"description": "The result
of the calculation", "title": "Result", "type": "integer"}, "explanation": {"description":
"Brief explanation of the calculation", "title": "Explanation", "type": "string"}},
"required": ["operation", "result", "explanation"], "title": "CalculationResult",
"type": "object", "additionalProperties": false, "propertyOrdering": ["operation",
"result", "explanation"]}}]}], "generationConfig": {"stopSequences": ["\nObservation:"]}}'
headers:
User-Agent:
- X-USER-AGENT-XXX
@@ -152,50 +143,42 @@ interactions:
connection:
- keep-alive
content-length:
- '3441'
- '3166'
content-type:
- application/json
host:
- generativelanguage.googleapis.com
x-goog-api-client:
- google-genai-sdk/1.65.0 gl-python/3.13.3
- google-genai-sdk/1.49.0 gl-python/3.13.12
x-goog-api-key:
- X-GOOG-API-KEY-XXX
method: POST
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-001:generateContent
response:
body:
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
[\n {\n \"text\": \"**My Calculation and OpenAPI Formatting**\\n\\nOkay,
so I've just crunched the numbers, 15 plus 27, and the answer, as expected,
is 42. Now comes the formatting, the part I truly appreciate. It's time to
translate this into the precise structure that the OpenAPI schema dictates.\\n\\nLet's
see\u2026 I need to populate three key fields: `operation`, `result`, and
`explanation`. This is straightforward. For `operation`, I'll enter \\\"Addition,\\\"
because, well, that's what I did! For `result`, the answer I painstakingly
produced, 42. And for `explanation`, a concise note, \\\"Added 15 and 27 together.\\\"
Perfect. That should do the trick. Now I can move on to the next task.\\n\",\n
\ \"thought\": true\n },\n {\n \"text\":
\"{\\n \\\"operation\\\": \\\"Addition\\\",\\n \\\"result\\\": 42,\\n \\\"explanation\\\":
\\\"Added 15 and 27 together.\\\"\\n}\",\n \"thoughtSignature\":
\"CrcDAQw51sd+vh7cJvN7WGZKQIJGL6Xtr/+CNnL2r9YWGHv5EBrOm/Qdzr4eK0eyrJhuJGBZg5V5XakEuLMULUvth7stE+FTz16F5Vzx2yKMuM5Ictv/DlI7s4Z5WFaI5HJVJTojOhW9K/1TM8y48+eStJre8Qyz3tOvFg1DPEau6JCk+1uoRil2RoZHxFQpd0LaDhl4sKZkRr1e5c2gtfZiK3NHR3ipbRejIOGjDQah4+q4D6ASTiHZxHCBIjAP4HH8DF40oylF6mQR1WHWezKzvkZ0sz3ydPGExM5+lFOE8z1NUpqH2kJ3PfzdrbX2VccIT+5pWz9NsIW8N1JYbPvb7xKENh7mTI+tmCtuAiO9DTwWUf450OHNWdrHQPxn3MMT10+4RMzKBeDAjEFVZ6M68CyUDTJ0uDtqJZ6SXs3ZdmmLGQY1lAk29r0eklaMJLk226uMUKCXlHKwyDbJVH3+SwGkgU+Am81AiekV5ZWQjfe3gUN8ojOLSNvpAfaf/K8ZvR4uGCEMs2pJ2sqI6sCIE9w9+u2biTattqWtcO+wmPXZ/CwU/ASXHlBqt55cvB2XyFTXO7kQGw==\"\n
\ }\n ],\n \"role\": \"model\"\n },\n \"finishReason\":
\"STOP\",\n \"index\": 0\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
421,\n \"candidatesTokenCount\": 36,\n \"totalTokenCount\": 560,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 421\n
\ }\n ],\n \"thoughtsTokenCount\": 103,\n \"serviceTier\": \"standard\"\n
\ },\n \"modelVersion\": \"gemini-2.5-flash\",\n \"responseId\": \"ragPaseeA6nT_uMP653cmAc\"\n}\n"
[\n {\n \"functionCall\": {\n \"name\": \"structured_output\",\n
\ \"args\": {\n \"result\": 42,\n \"explanation\":
\"15 + 27 = 42\",\n \"operation\": \"addition\"\n }\n
\ }\n }\n ],\n \"role\": \"model\"\n },\n
\ \"finishReason\": \"STOP\",\n \"avgLogprobs\": -0.07498827245500353\n
\ }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\": 421,\n \"candidatesTokenCount\":
18,\n \"totalTokenCount\": 439,\n \"promptTokensDetails\": [\n {\n
\ \"modality\": \"TEXT\",\n \"tokenCount\": 421\n }\n ],\n
\ \"candidatesTokensDetails\": [\n {\n \"modality\": \"TEXT\",\n
\ \"tokenCount\": 18\n }\n ]\n },\n \"modelVersion\": \"gemini-2.0-flash-001\",\n
\ \"responseId\": \"vlefac7bJb6TjMcPzYWh0Ag\"\n}\n"
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Type:
- application/json; charset=UTF-8
Date:
- Fri, 22 May 2026 00:51:59 GMT
- Wed, 25 Feb 2026 20:12:47 GMT
Server:
- scaffolding on HTTPServer2
Server-Timing:
- gfet4t7; dur=2377
- gfet4t7; dur=774
Transfer-Encoding:
- chunked
Vary:
@@ -206,8 +189,6 @@ interactions:
- X-CONTENT-TYPE-XXX
X-Frame-Options:
- X-FRAME-OPTIONS-XXX
X-Gemini-Service-Tier:
- standard
X-XSS-Protection:
- '0'
status:

View File

@@ -11,6 +11,7 @@ from crewai.events.event_context import (
MismatchBehavior,
StackDepthExceededError,
_event_context_config,
_event_id_stack,
EventContextConfig,
get_current_parent_id,
get_enclosing_parent_id,
@@ -21,6 +22,7 @@ from crewai.events.event_context import (
pop_event_scope,
push_event_scope,
reset_last_event_id,
resume_task_scope,
set_last_event_id,
set_triggering_event_id,
triggered_by_scope,
@@ -180,6 +182,91 @@ class TestTriggeredByScope:
assert get_triggering_event_id() is None
class TestResumeTaskScope:
"""Tests for the checkpoint-resume scope helper."""
@pytest.fixture(autouse=True)
def _reset_stack(self) -> None:
_event_id_stack.set(())
def _bind_runtime_state(self, *event_dicts: dict[str, object]):
from crewai.events import crewai_event_bus
from crewai.events.types.task_events import TaskStartedEvent
from crewai.state.event_record import EventRecord
from crewai.state.runtime import RuntimeState
record = EventRecord()
for spec in event_dicts:
ev = TaskStartedEvent(context=None, task=None)
ev.task_id = spec["task_id"] # type: ignore[assignment]
ev.event_id = spec["event_id"] # type: ignore[assignment]
ev.emission_sequence = spec["emission_sequence"] # type: ignore[assignment]
record.add(ev)
state = RuntimeState(root=[])
state._event_record = record
previous = crewai_event_bus._runtime_state
crewai_event_bus._runtime_state = state
return crewai_event_bus, previous
def test_returns_false_when_no_runtime_state(self) -> None:
from crewai.events import crewai_event_bus
previous = crewai_event_bus._runtime_state
crewai_event_bus._runtime_state = None
try:
assert resume_task_scope("any-task") is False
assert _event_id_stack.get() == ()
finally:
crewai_event_bus._runtime_state = previous
def test_returns_false_when_no_matching_event(self) -> None:
bus, previous = self._bind_runtime_state(
{"task_id": "other", "event_id": "e1", "emission_sequence": 1},
)
try:
assert resume_task_scope("missing") is False
assert _event_id_stack.get() == ()
finally:
bus._runtime_state = previous
def test_pushes_latest_event_for_task(self) -> None:
bus, previous = self._bind_runtime_state(
{"task_id": "t1", "event_id": "e1", "emission_sequence": 1},
{"task_id": "t1", "event_id": "e2", "emission_sequence": 5},
{"task_id": "t1", "event_id": "e3", "emission_sequence": 3},
{"task_id": "t2", "event_id": "x1", "emission_sequence": 9},
)
try:
assert resume_task_scope("t1") is True
assert _event_id_stack.get() == (("e2", "task_started"),)
finally:
bus._runtime_state = previous
def test_pairs_cleanly_with_task_completed(self) -> None:
"""The pushed scope must be popped by a matching task_completed."""
from crewai.events import crewai_event_bus
from crewai.events.types.task_events import TaskCompletedEvent
from crewai.tasks.task_output import TaskOutput
push_event_scope("kickoff-1", "crew_kickoff_started")
bus, previous = self._bind_runtime_state(
{"task_id": "t1", "event_id": "started-1", "emission_sequence": 1},
)
try:
assert resume_task_scope("t1") is True
output = TaskOutput(description="d", raw="r", agent="a")
completed = TaskCompletedEvent(output=output, task=None)
completed.task_id = "t1"
crewai_event_bus.emit(None, completed)
crewai_event_bus.flush()
assert _event_id_stack.get() == (("kickoff-1", "crew_kickoff_started"),)
assert completed.started_event_id == "started-1"
finally:
bus._runtime_state = previous
_event_id_stack.set(())
def test_agent_scope_preserved_after_tool_error_event() -> None:
from crewai.events import crewai_event_bus
from crewai.events.types.tool_usage_events import (

View File

@@ -1025,7 +1025,7 @@ def test_gemini_crew_structured_output_with_tools():
role="Calculator",
goal="Perform calculations using available tools",
backstory="You are a calculator assistant that uses tools to compute results.",
llm=LLM(model="google/gemini-2.5-flash"),
llm=LLM(model="google/gemini-2.0-flash-001"),
tools=[add_numbers],
)

View File

@@ -189,6 +189,7 @@ exclude-newer = "3 days"
# authlib <1.6.11 has GHSA-jj8c-mmj3-mmgv (CSRF bypass in cache-based state storage).
# pip <26.1.1 has GHSA-58qw-9mgm-455v (archive handling); OSV considers 26.1.1 unaffected.
# paramiko <5.0.0 has GHSA-r374-rxx8-8654 (SHA-1 in rsakey.py); OSV considers 5.0.0 unaffected. Transitive via composio-core.
# starlette <1.0.1 has PYSEC-2026-161 (missing Host header validation poisons request.url.path, bypassing path-based auth). Transitive via fastapi.
# litellm 1.83.8+ hard-pins openai==2.24.0, missing openai.types.responses used by crewai;
# override to >=2.30.0 (the version litellm 1.83.7 used) until upstream relaxes the pin.
override-dependencies = [
@@ -209,6 +210,7 @@ override-dependencies = [
"authlib>=1.6.11",
"pip>=26.1.1",
"paramiko>=5.0.0",
"starlette>=1.0.1",
]
[tool.uv.workspace]

12
uv.lock generated
View File

@@ -13,9 +13,12 @@ resolution-markers = [
]
[options]
exclude-newer = "2026-05-17T14:20:01.778505Z"
exclude-newer = "2026-05-19T15:27:50.647689Z"
exclude-newer-span = "P3D"
[options.exclude-newer-package]
starlette = "2026-05-22T16:00:00Z"
[manifest]
members = [
"crewai",
@@ -40,6 +43,7 @@ overrides = [
{ name = "pypdf", specifier = ">=6.10.2,<7" },
{ name = "python-multipart", specifier = ">=0.0.27,<1" },
{ name = "rich", specifier = ">=13.7.1" },
{ name = "starlette", specifier = ">=1.0.1" },
{ name = "transformers", marker = "python_full_version >= '3.10'", specifier = ">=5.4.0" },
{ name = "urllib3", specifier = ">=2.7.0" },
{ name = "uv", specifier = ">=0.11.6,<1" },
@@ -8528,15 +8532,15 @@ wheels = [
[[package]]
name = "starlette"
version = "1.0.0"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
sdist = { url = "https://files.pythonhosted.org/packages/08/a3/84e821cc54b4ab50ae6dbc6ac3800a651b65ec35f045cc73785380654057/starlette-1.0.1.tar.gz", hash = "sha256:512399c5f1de7fac99c88572212ded9ddeddef2fb32afa82d724000e88b38f4f", size = 2659596, upload-time = "2026-05-21T21:58:58.433Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
{ url = "https://files.pythonhosted.org/packages/ec/e1/b2df4bc09a1e51ff664c1e17018a4274b42e5e9352e4a478ea540512dc88/starlette-1.0.1-py3-none-any.whl", hash = "sha256:7c0e69b2ee1c848bd54669d908500117a3ee13de603a21427e5c6fc1adf98dcd", size = 72802, upload-time = "2026-05-21T21:58:56.551Z" },
]
[[package]]