Skip to content
Open
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
3 changes: 3 additions & 0 deletions examples/tutorials/00_sync/010_multiturn/project/acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ async def handle_message_send(
# Convert task messages to OpenAI Agents SDK format
input_list = convert_task_messages_to_oai_agents_inputs(task_messages)

# Append the current user message (not yet persisted in task history)
input_list.append({"role": "user", "content": params.content.content})

# Run the agent
result = await Runner.run(test_agent, input_list, run_config=run_config)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
"""

import os
import uuid

import pytest
from test_utils.sync import validate_text_in_string, collect_streaming_response

from agentex import Agentex
from agentex.types import TextContent, TextContentParam
from agentex.types.agent_rpc_params import ParamsCreateTaskRequest, ParamsSendMessageRequest
from agentex.lib.sdk.fastacp.base.base_acp_server import uuid

# Configuration from environment variables
AGENTEX_API_BASE_URL = os.environ.get("AGENTEX_API_BASE_URL", "http://localhost:5003")
Expand Down
4 changes: 4 additions & 0 deletions examples/tutorials/00_sync/020_streaming/project/acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ async def handle_message_send(
content="Hey, sorry I'm unable to respond to your message because you're running this example without an OpenAI API key. Please set the OPENAI_API_KEY environment variable to run this example. Do this by either by adding a .env file to the project/ directory or by setting the environment variable in your terminal.",
),
)
return

# Try to retrieve the state. If it doesn't exist, create it.
task_state = await adk.state.get_by_task_and_agent(task_id=params.task.id, agent_id=params.agent.id)
Expand Down Expand Up @@ -86,6 +87,9 @@ async def handle_message_send(
# Convert task messages to OpenAI Agents SDK format
input_list = convert_task_messages_to_oai_agents_inputs(task_messages)

# Append the current user message (not yet persisted in task history)
input_list.append({"role": "user", "content": params.content.content})

# Run the agent and stream the events
result = Runner.run_streamed(test_agent, input_list, run_config=run_config)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
"""

import os
import uuid

import pytest
from test_utils.sync import validate_text_in_string, collect_streaming_response

from agentex import Agentex
from agentex.types import TextContent, TextContentParam
from agentex.types.agent_rpc_params import ParamsCreateTaskRequest, ParamsSendMessageRequest
from agentex.lib.sdk.fastacp.base.base_acp_server import uuid

# Configuration from environment variables
AGENTEX_API_BASE_URL = os.environ.get("AGENTEX_API_BASE_URL", "http://localhost:5003")
Expand Down
3 changes: 1 addition & 2 deletions examples/tutorials/00_sync/030_langgraph/project/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from project.tools import TOOLS
from agentex.lib.adk import create_checkpointer

MODEL_NAME = "gpt-5"
MODEL_NAME = "gpt-4o"
SYSTEM_PROMPT = """You are a helpful AI assistant with access to tools.

Current date and time: {timestamp}
Expand All @@ -46,7 +46,6 @@ async def create_graph():
"""
llm = ChatOpenAI(
model=MODEL_NAME,
reasoning={"effort": "high", "summary": "auto"},
)
llm_with_tools = llm.bind_tools(TOOLS)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
"""

import os
import uuid

import pytest
from test_utils.sync import validate_text_in_string, collect_streaming_response

from agentex import Agentex
from agentex.types import TextContent, TextContentParam
from agentex.types.agent_rpc_params import ParamsCreateTaskRequest, ParamsSendMessageRequest
from agentex.lib.sdk.fastacp.base.base_acp_server import uuid

# Configuration from environment variables
AGENTEX_API_BASE_URL = os.environ.get("AGENTEX_API_BASE_URL", "http://localhost:5003")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ async def handle_event_send(params: SendEventParams):
content="Hey, sorry I'm unable to respond to your message because you're running this example without an OpenAI API key. Please set the OPENAI_API_KEY environment variable to run this example. Do this by either by adding a .env file to the project/ directory or by setting the environment variable in your terminal.",
),
)
return

#########################################################
# 5. (👋) Retrieve the task state.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ async def test_send_event_and_stream(self, client: AsyncAgentex, agent_id: str):
task = task_response.result
assert task is not None

await asyncio.sleep(1) # wait for state to be initialized
# Check initial state
states = await client.states.list(agent_id=agent_id, task_id=task.id)
assert len(states) == 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ async def handle_event_send(params: SendEventParams):
content="Hey, sorry I'm unable to respond to your message because you're running this example without an OpenAI API key. Please set the OPENAI_API_KEY environment variable to run this example. Do this by either by adding a .env file to the project/ directory or by setting the environment variable in your terminal.",
),
)
return

#########################################################
# 5. Retrieve the task state.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ async def handle_event_send(params: SendEventParams):
),
parent_span_id=span.id if span else None,
)
return

#########################################################
# 7. Call an LLM to respond to the user's message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ async def handle_event_send(params: SendEventParams):
),
parent_span_id=span.id if span else None,
)
return

#########################################################
# (👋) Call an LLM to respond to the user's message using custom streaming
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
"""

import os
import uuid

import pytest
import pytest_asyncio

from agentex import AsyncAgentex
from agentex.types import TextContentParam
from agentex.types.agent_rpc_params import ParamsCreateTaskRequest
from agentex.lib.sdk.fastacp.base.base_acp_server import uuid

# Configuration from environment variables
AGENTEX_API_BASE_URL = os.environ.get("AGENTEX_API_BASE_URL", "http://localhost:5003")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
),
parent_span_id=span.id if span else None,
)
return

# Call an LLM to respond to the user's message
# When send_as_agent_task_message=True, returns a TaskMessage
Expand All @@ -234,13 +235,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
"to provide accurate and well-reasoned responses."
),
parent_span_id=span.id if span else None,
model="gpt-5",
model_settings=ModelSettings(
# Include reasoning items in the response (IDs, summaries)
# response_include=["reasoning.encrypted_content"],
# Ask the model to include a short reasoning summary
reasoning=Reasoning(effort="medium", summary="detailed"),
),
model="gpt-4o",
# tools=[CALCULATOR_TOOL],
)
if self._state:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
),
parent_span_id=span.id if span else None,
)
return

# Call an LLM to respond to the user's message
# When send_as_agent_task_message=True, returns a TaskMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
self._state.input_list.append(msg)

# Set span output for tracing - include full state
span.output = self._state.model_dump()
if span:
span.output = self._state.model_dump()

@workflow.run
async def on_task_create(self, params: CreateTaskParams) -> str:
Expand Down
15 changes: 14 additions & 1 deletion examples/tutorials/run_agent_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,19 @@ run_test() {
cd "$tutorial_path" || return 1


# Determine pytest command - use built wheel if available (same wheel used to start agent)
local pytest_cmd="uv run pytest"
if [ "$BUILD_CLI" = true ]; then
local wheel_file=$(ls /home/runner/work/*/*/dist/agentex_sdk-*.whl 2>/dev/null | head -n1)
if [[ -z "$wheel_file" ]]; then
# Fallback for local development
wheel_file=$(ls "${SCRIPT_DIR}/../../dist/agentex_sdk-*.whl" 2>/dev/null | head -n1)
fi
if [[ -n "$wheel_file" ]]; then
pytest_cmd="uv run --with $wheel_file pytest"
fi
fi

# Run the tests with retry mechanism
local max_retries=5
local retry_count=0
Expand All @@ -270,7 +283,7 @@ run_test() {
fi

# Stream pytest output directly in real-time
uv run pytest tests/test_agent.py -v -s
$pytest_cmd tests/test_agent.py -v -s
exit_code=$?

if [ $exit_code -eq 0 ]; then
Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@ dependencies = [
"typer>=0.16,<0.17",
"questionary>=2.0.1,<3",
"rich>=13.9.2,<14",
"fastapi>=0.115.0,<0.116",
"fastapi>=0.115.0", # upper bound removed — CVE-2025-62727 fix needs starlette>=0.49.1
"starlette>=0.49.1", # CVE-2025-62727 (HIGH) — floor 0.49.1 patches vuln
"tornado>=6.5.5", # CVE-2026-31958 (HIGH) — floor pin ensures patched release
"uvicorn>=0.31.1",
"watchfiles>=0.24.0,<1.0",
"python-on-whales>=0.73.0,<0.74",
"pyyaml>=6.0.2,<7",
"jsonschema>=4.23.0,<5",
"jsonref>=1.1.0,<2",
"temporalio>=1.18.2,<2",
"opentelemetry-api>=1.11.1,<2",
"opentelemetry-sdk>=1.11.1,<2",
"aiohttp>=3.10.10,<4",
"redis>=5.2.0,<6",
"litellm>=1.66.0,<2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ async def on_tool_start(self, context: RunContextWrapper, agent: Agent, tool: To
logger.warning(f"Failed to parse tool arguments: {tool_context.tool_arguments}")
tool_arguments = {}

await workflow.execute_activity_method(
await workflow.execute_activity(
stream_lifecycle_content,
args=[
self.task_id,
Expand Down Expand Up @@ -167,7 +167,7 @@ async def on_tool_end(
else f"call_{id(tool)}"
)

await workflow.execute_activity_method(
await workflow.execute_activity(
stream_lifecycle_content,
args=[
self.task_id,
Expand Down Expand Up @@ -195,7 +195,7 @@ async def on_handoff(
from_agent: The agent transferring control
to_agent: The agent receiving control
"""
await workflow.execute_activity_method(
await workflow.execute_activity(
stream_lifecycle_content,
args=[
self.task_id,
Expand Down
3 changes: 1 addition & 2 deletions src/agentex/lib/core/temporal/workers/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
DefaultPayloadConverter,
CompositePayloadConverter,
JSONPlainPayloadConverter,
_JSONTypeConverterUnhandled,
)
from temporalio.contrib.openai_agents import OpenAIAgentsPlugin

Expand All @@ -45,7 +44,7 @@ def default(self, o: Any) -> Any:

class DateTimeJSONTypeConverter(JSONTypeConverter):
@override
def to_typed_value(self, hint: type, value: Any) -> Any | None | _JSONTypeConverterUnhandled:
def to_typed_value(self, hint: type, value: Any) -> Any:
if hint == datetime.datetime:
return datetime.datetime.fromisoformat(value)
return JSONTypeConverter.Unhandled
Expand Down
30 changes: 19 additions & 11 deletions src/agentex/lib/sdk/fastacp/base/base_acp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
import uvicorn
from fastapi import FastAPI, Request
from pydantic import TypeAdapter, ValidationError
from starlette.types import Send, Scope, ASGIApp, Receive
from fastapi.responses import StreamingResponse
from starlette.middleware.base import BaseHTTPMiddleware

from agentex.lib.types.acp import (
RPC_SYNC_METHODS,
Expand Down Expand Up @@ -43,17 +43,25 @@
task_message_update_adapter = TypeAdapter(TaskMessageUpdate)


class RequestIDMiddleware(BaseHTTPMiddleware):
"""Middleware to extract or generate request IDs and add them to logs and response headers"""
class RequestIDMiddleware:
"""Pure ASGI middleware to extract or generate request IDs and set them in the logging context.

async def dispatch(self, request: Request, call_next): # type: ignore[override]
# Extract request ID from header or generate a new one if there isn't one
request_id = request.headers.get("x-request-id") or uuid.uuid4().hex
# Store request ID in request state for access in handlers
ctx_var_request_id.set(request_id)
# Process request
response = await call_next(request)
return response
Implemented as a pure ASGI middleware (rather than BaseHTTPMiddleware) so that it never
buffers the response body. BaseHTTPMiddleware's call_next() silently swallows
StreamingResponse bodies in several starlette versions, which caused message/send handlers
to return result=null through the Agentex server proxy.
"""

def __init__(self, app: ASGIApp) -> None:
self.app = app

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "http":
headers = dict(scope.get("headers", []))
raw_request_id = headers.get(b"x-request-id", b"")
request_id = raw_request_id.decode() if raw_request_id else uuid.uuid4().hex
Copy link

Choose a reason for hiding this comment

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

P1 decode() can raise UnicodeDecodeError on malformed headers

raw_request_id.decode() uses UTF-8 by default. If a client (or an upstream proxy) sends a non-UTF-8 byte sequence in the x-request-id header, this will raise an unhandled UnicodeDecodeError that propagates through the ASGI stack, causing a 500 for the request.

The HTTP/1.1 spec (RFC 7230) specifies that header field values are ISO-8859-1 / Latin-1, so decode('latin-1') is both spec-correct and will never raise an exception (every byte sequence is valid Latin-1).

Suggested change
request_id = raw_request_id.decode() if raw_request_id else uuid.uuid4().hex
request_id = raw_request_id.decode("latin-1") if raw_request_id else uuid.uuid4().hex
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agentex/lib/sdk/fastacp/base/base_acp_server.py
Line: 62

Comment:
**`decode()` can raise `UnicodeDecodeError` on malformed headers**

`raw_request_id.decode()` uses UTF-8 by default. If a client (or an upstream proxy) sends a non-UTF-8 byte sequence in the `x-request-id` header, this will raise an unhandled `UnicodeDecodeError` that propagates through the ASGI stack, causing a 500 for the request.

The HTTP/1.1 spec (RFC 7230) specifies that header field values are ISO-8859-1 / Latin-1, so `decode('latin-1')` is both spec-correct and will never raise an exception (every byte sequence is valid Latin-1).

```suggestion
            request_id = raw_request_id.decode("latin-1") if raw_request_id else uuid.uuid4().hex
```

How can I resolve this? If you propose a fix, please make it concise.

ctx_var_request_id.set(request_id)
await self.app(scope, receive, send)


class BaseACPServer(FastAPI):
Expand Down
Loading
Loading