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
2 changes: 1 addition & 1 deletion examples/demos/procurement_agent/project/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
@workflow.run
@override
async def on_task_create(self, params: CreateTaskParams) -> str:
logger.info(f"Received task create params: {params}")
logger.info(f"Received task create for task {params.task.id}")

self._state = StateModel(input_list=[], turn_number=0)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(self):
@workflow.signal(name=SignalName.RECEIVE_EVENT)
@override
async def on_task_event_send(self, params: SendEventParams) -> None:
logger.info(f"Received task message instruction: {params}")
logger.info(f"Received task message instruction for task {params.task.id}, event {params.event.id}")

# 2. Echo back the client's message to show it in the UI. This is not done by default so the agent developer has full control over what is shown to the user.
await adk.messages.create(task_id=params.task.id, content=params.event.content)
Expand All @@ -51,7 +51,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
@workflow.run
@override
async def on_task_create(self, params: CreateTaskParams) -> None:
logger.info(f"Received task create params: {params}")
logger.info(f"Received task create for task {params.task.id}")

# 1. Acknowledge that the task has been created. Gate this one-time prologue
# on is_continued_run(): run_until_complete below recycles the workflow via
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def __init__(self):
@workflow.signal(name=SignalName.RECEIVE_EVENT)
@override
async def on_task_event_send(self, params: SendEventParams) -> None:
logger.info(f"Received task message instruction: {params}")
logger.info(f"Received task message instruction for task {params.task.id}, event {params.event.id}")

if not params.event.content:
return
Expand Down Expand Up @@ -259,7 +259,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
@workflow.run
@override
async def on_task_create(self, params: CreateTaskParams) -> None:
logger.info(f"Received task create params: {params}")
logger.info(f"Received task create for task {params.task.id}")

# 1. Initialize the state. You can either do this here or in the __init__ method.
# This function is triggered whenever a client creates a task for this agent.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
@workflow.run
@override
async def on_task_create(self, params: CreateTaskParams) -> None:
logger.info(f"Received task create params: {params}")
logger.info(f"Received task create for task {params.task.id}")

self._state = StateModel()
await adk.messages.create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
@workflow.run
@override
async def on_task_create(self, params: CreateTaskParams) -> None:
logger.info(f"Received task create params: {params}")
logger.info(f"Received task create for task {params.task.id}")

# 1. Initialize the state. You can either do this here or in the
# __init__ method. This function is triggered whenever a client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
- If the LLM call fails, Temporal automatically retries it
- The entire conversation is durable and survives process restarts
"""
logger.info(f"Received task message instruction: {params}")
logger.info(f"Received task message instruction for task {params.task.id}, event {params.event.id}")

if self._state is None:
raise ValueError("State is not initialized")
Expand Down Expand Up @@ -253,7 +253,7 @@ async def on_task_create(self, params: CreateTaskParams) -> str:
- Can resume from exact point of failure without losing context
- Scales to handle millions of concurrent agent conversations
"""
logger.info(f"Received task create params: {params}")
logger.info(f"Received task create for task {params.task.id}")

# ============================================================================
# WORKFLOW INITIALIZATION: Initialize State
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def __init__(self):

@workflow.signal(name=SignalName.RECEIVE_EVENT)
async def on_task_event_send(self, params: SendEventParams) -> None:
logger.info(f"Received task message instruction: {params}")
logger.info(f"Received task message instruction for task {params.task.id}, event {params.event.id}")

if self._state is None:
raise ValueError("State is not initialized")
Expand Down Expand Up @@ -329,7 +329,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:

@workflow.run
async def on_task_create(self, params: CreateTaskParams) -> str:
logger.info(f"Received task create params: {params}")
logger.info(f"Received task create for task {params.task.id}")

# Initialize the conversation state with an empty history
self._state = StateModel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
When the agent needs human approval, it calls wait_for_confirmation which spawns
a child workflow that waits for external signals before continuing.
"""
logger.info(f"Received task message instruction: {params}")
logger.info(f"Received task message instruction for task {params.task.id}, event {params.event.id}")

if self._state is None:
raise ValueError("State is not initialized")
Expand Down Expand Up @@ -214,7 +214,7 @@ async def on_task_create(self, params: CreateTaskParams) -> str:
Handles both automated decisions and human approval workflows durably.
To approve waiting actions: temporal workflow signal --workflow-id="child-workflow-id" --name="fulfill_order_signal" --input=true
"""
logger.info(f"Received task create params: {params}")
logger.info(f"Received task create for task {params.task.id}")

# Initialize the conversation state with an empty history
self._state = StateModel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
- The agent interface remains identical to OpenAI examples
- Temporal durability works the same way regardless of model provider
"""
logger.info(f"Received task message instruction: {params}")
logger.info(f"Received task message instruction for task {params.task.id}, event {params.event.id}")

if self._state is None:
raise ValueError("State is not initialized")
Expand Down Expand Up @@ -198,7 +198,7 @@ async def on_task_create(self, params: CreateTaskParams) -> str:
This method runs when the workflow starts and keeps the agent conversation alive.
The pattern is identical to other tutorials - only the model provider changes.
"""
logger.info(f"Received task create params: {params}")
logger.info(f"Received task create for task {params.task.id}")

# Initialize the conversation state
self._state = StateModel(
Expand Down
16 changes: 7 additions & 9 deletions src/agentex/lib/cli/templates/default/project/acp.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ acp = FastACP.create(
# It's a good place to initialize any state or resources needed for the task.
@acp.on_task_event_send
async def handle_task_event_send(params: SendEventParams):
# For this tutorial, we log the parameters sent to the handler
# so you can see where and how messages within a long running task are handled
logger.info(f"Received task event send rpc: {params}")
# For this tutorial, we log the task and event ids so you can see where and how
# messages within a long running task are handled
logger.info(f"Received task event send rpc for task {params.task.id}, event {params.event.id}")

# 1. Echo back the client's message to show it in the UI. This is not done by default so the agent developer has full control over what is shown to the user.
await adk.messages.create(task_id=params.task.id, content=params.event.content)
Expand All @@ -43,14 +43,12 @@ async def handle_task_event_send(params: SendEventParams):

@acp.on_task_cancel
async def handle_task_canceled(params: CancelTaskParams):
# For this tutorial, we print the parameters sent to the handler
# so you can see where and how task cancellation is handled
logger.info(f"Received task cancel rpc: {params}")
# For this tutorial, we log the task id so you can see where and how task cancellation is handled
logger.info(f"Received task cancel rpc for task {params.task.id}")

@acp.on_task_create
async def handle_task_create(params: CreateTaskParams):
# For this tutorial, we log the parameters sent to the handler
# so you can see where and how task creation is handled
# For this tutorial, we log the task id so you can see where and how task creation is handled

# Here is where you can initialize any state or resources needed for the task.
logger.info(f"Received task create rpc: {params}")
logger.info(f"Received task create rpc for task {params.task.id}")
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class {{ workflow_class }}(BaseWorkflow):

@workflow.signal(name=SignalName.RECEIVE_EVENT)
async def on_task_event_send(self, params: SendEventParams) -> None:
logger.info(f"Received task message instruction: {params}")
logger.info(f"Received task message instruction for task {params.task.id}, event {params.event.id}")

content = params.event.content
if not isinstance(content, TextContent):
Expand Down Expand Up @@ -170,7 +170,7 @@ class {{ workflow_class }}(BaseWorkflow):

@workflow.run
async def on_task_create(self, params: CreateTaskParams) -> str:
logger.info(f"Received task create params: {params}")
logger.info(f"Received task create for task {params.task.id}")

# Acknowledge that the task has been created
await adk.messages.create(
Expand Down
4 changes: 2 additions & 2 deletions src/agentex/lib/cli/templates/temporal/project/workflow.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class {{ workflow_class }}(BaseWorkflow):

@workflow.signal(name=SignalName.RECEIVE_EVENT)
async def on_task_event_send(self, params: SendEventParams) -> None:
logger.info(f"Received task message instruction: {params}")
logger.info(f"Received task message instruction for task {params.task.id}, event {params.event.id}")

# 2. Echo back the client's message to show it in the UI. This is not done by default so the agent developer has full control over what is shown to the user.
await adk.messages.create(task_id=params.task.id, content=params.event.content)
Expand All @@ -48,7 +48,7 @@ class {{ workflow_class }}(BaseWorkflow):

@workflow.run
async def on_task_create(self, params: CreateTaskParams) -> str:
logger.info(f"Received task create params: {params}")
logger.info(f"Received task create for task {params.task.id}")

# 1. Acknowledge that the task has been created.
await adk.messages.create(
Expand Down
5 changes: 5 additions & 0 deletions src/agentex/protocol/acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class CreateTaskParams(BaseModel):
request: dict[str, Any] | None = Field(
default=None,
description="Additional request context including headers forwarded to this agent",
repr=False, # forwarded headers can carry end-user credentials; keep them out of str() and logs
)


Expand All @@ -66,6 +67,7 @@ class SendMessageParams(BaseModel):
request: dict[str, Any] | None = Field(
default=None,
description="Additional request context including headers forwarded to this agent",
repr=False, # forwarded headers can carry end-user credentials; keep them out of str() and logs
)


Expand All @@ -85,6 +87,7 @@ class SendEventParams(BaseModel):
request: dict[str, Any] | None = Field(
default=None,
description="Additional request context including headers forwarded to this agent",
repr=False, # forwarded headers can carry end-user credentials; keep them out of str() and logs
)


Expand All @@ -102,6 +105,7 @@ class CancelTaskParams(BaseModel):
request: dict[str, Any] | None = Field(
default=None,
description="Additional request context including headers forwarded to this agent",
repr=False, # forwarded headers can carry end-user credentials; keep them out of str() and logs
)


Expand All @@ -124,6 +128,7 @@ class InterruptTaskParams(BaseModel):
request: dict[str, Any] | None = Field(
default=None,
description="Additional request context including headers forwarded to this agent",
repr=False, # forwarded headers can carry end-user credentials; keep them out of str() and logs
)


Expand Down
65 changes: 65 additions & 0 deletions tests/test_acp_params_repr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Forwarded request headers must stay out of the string form of ACP params.

``BaseACPServer`` copies inbound ``x-*`` headers (which can carry end-user
credentials) into ``params.request``. Agent code commonly logs ``f"{params}"``,
so ``request`` is excluded from ``str()``/``repr()``, while handlers and the
Temporal signal payload (``model_dump()``) still receive it.
"""

from __future__ import annotations

import pytest

from agentex.types.task import Task
from agentex.types.agent import Agent
from agentex.types.event import Event
from agentex.protocol.acp import (
SendEventParams,
CancelTaskParams,
CreateTaskParams,
SendMessageParams,
InterruptTaskParams,
)
from agentex.types.text_content import TextContent

CANARY = "canary-forwarded-header-value"
REQUEST = {"headers": {"x-user-oauth-credentials": CANARY}}

_agent = Agent(
id="agent-1",
name="agent",
description="d",
acp_type="async",
created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z",
)
_task = Task(id="task-1")
_content = TextContent(author="user", content="hi")
_event = Event(id="event-1", agent_id="agent-1", task_id="task-1", sequence_id=1, content=_content)

AnyParams = CreateTaskParams | SendMessageParams | SendEventParams | CancelTaskParams | InterruptTaskParams

PARAMS: list[AnyParams] = [
CreateTaskParams(agent=_agent, task=_task, params={"k": "v"}, request=REQUEST),
SendMessageParams(agent=_agent, task=_task, content=_content, stream=False, request=REQUEST),
SendEventParams(agent=_agent, task=_task, event=_event, request=REQUEST),
CancelTaskParams(agent=_agent, task=_task, request=REQUEST),
InterruptTaskParams(agent=_agent, task=_task, request=REQUEST),
]


@pytest.mark.parametrize("params", PARAMS, ids=lambda p: type(p).__name__)
def test_header_values_absent_from_str_and_repr(params: AnyParams) -> None:
assert CANARY not in str(params)
assert CANARY not in repr(params)
assert "task-1" in str(params)


@pytest.mark.parametrize("params", PARAMS, ids=lambda p: type(p).__name__)
def test_headers_still_reach_handlers_and_payloads(params: AnyParams) -> None:
assert params.request == REQUEST
dumped = params.model_dump()
assert dumped["request"] == REQUEST
assert CANARY in params.model_dump_json()
# TemporalTaskService signals model_dump() and the workflow re-validates it.
assert type(params).model_validate(dumped).request == REQUEST
Loading