Skip to content

Commit bc51c52

Browse files
authored
fix(temporal): deduplicate tracing and skip replay logs (#524)
Temporal workers registered tracing twice, and workflow replay emitted duplicate business logs. Keep tracing on the client and add replay-aware logging with workflow and run IDs. Changes: - Preserve inherited tracing and business-interceptor order. - Suppress replay logs without changing startup or activity logging. - Adopt the logger in SDK workflow sites and all Temporal templates. - Cover tracing composition, replay, and logging fields with tests. - Document adoption for existing agents. Linear: AGX1-1113
1 parent 2bd031e commit bc51c52

14 files changed

Lines changed: 360 additions & 18 deletions

File tree

adk/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ This automatically pulls in [`agentex-client`](../) (the slim Stainless-generate
2727

2828
The two packages contribute disjoint files to the `agentex.*` namespace — `agentex/lib/*` ships only from `agentex-sdk`.
2929

30+
## Workflow logging
31+
32+
Use the workflow logger in Temporal workflow code:
33+
34+
```python
35+
from agentex.lib.core.temporal.logging import make_workflow_logger
36+
37+
logger = make_workflow_logger(__name__)
38+
```
39+
40+
It suppresses logs while Temporal replays recorded history and adds top-level
41+
`workflow_id` and `run_id` fields during workflow execution. It preserves the
42+
message, caller fields, and exception details. Outside workflows, including in
43+
activities, it behaves like the ordinary SDK logger.
44+
45+
New Temporal templates use this helper. Existing agents must replace their own
46+
workflow loggers to get the same behavior. This does not create trace context or
47+
add trace IDs to workflows that lack it. Temporal's worker diagnostics still report
48+
replay failures.
49+
3050
## Repo layout
3151

3252
This package is hand-authored and lives at `adk/` inside [scaleapi/scale-agentex-python](https://github.com/scaleapi/scale-agentex-python). Stainless codegen never touches `adk/**` — it's outside the generated surface. The sibling `agentex-client` package lives at the repo root and IS Stainless-generated.

src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ from temporalio import workflow
2727
from agentex.lib import adk
2828
from agentex.lib.types.acp import SendEventParams, CreateTaskParams
2929
from agentex.lib.types.tracing import SGPTracingProcessorConfig
30-
from agentex.lib.utils.logging import make_logger
30+
from agentex.lib.core.temporal.logging import make_workflow_logger
3131
from agentex.types.text_content import TextContent
3232
from agentex.lib.environment_variables import EnvironmentVariables
3333
from agentex.lib.core.temporal.types.workflow import SignalName
@@ -52,7 +52,7 @@ if environment_variables.WORKFLOW_NAME is None:
5252
if environment_variables.AGENT_NAME is None:
5353
raise ValueError("Environment variable AGENT_NAME is not set")
5454

55-
logger = make_logger(__name__)
55+
logger = make_workflow_logger(__name__)
5656

5757

5858
@workflow.defn(name=environment_variables.WORKFLOW_NAME)

src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ from temporalio import workflow
2929
from agentex.lib import adk
3030
from agentex.lib.types.acp import SendEventParams, CreateTaskParams
3131
from agentex.lib.types.tracing import SGPTracingProcessorConfig
32-
from agentex.lib.utils.logging import make_logger
32+
from agentex.lib.core.temporal.logging import make_workflow_logger
3333
from agentex.types.text_content import TextContent
3434
from agentex.lib.environment_variables import EnvironmentVariables
3535
from agentex.lib.core.temporal.types.workflow import SignalName
@@ -54,7 +54,7 @@ if environment_variables.WORKFLOW_NAME is None:
5454
if environment_variables.AGENT_NAME is None:
5555
raise ValueError("Environment variable AGENT_NAME is not set")
5656

57-
logger = make_logger(__name__)
57+
logger = make_workflow_logger(__name__)
5858

5959
MODEL = os.environ.get("CODEX_MODEL", "o4-mini")
6060

src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ from project.graph import GRAPH_NAME, build_graph
3737
from agentex.lib.adk import emit_langgraph_messages
3838
from agentex.protocol.acp import SendEventParams, CreateTaskParams
3939
from agentex.lib.types.tracing import SGPTracingProcessorConfig
40-
from agentex.lib.utils.logging import make_logger
40+
from agentex.lib.core.temporal.logging import make_workflow_logger
4141
from agentex.types.text_content import TextContent
4242
from agentex.lib.environment_variables import EnvironmentVariables
4343
from agentex.lib.core.temporal.types.workflow import SignalName
@@ -64,7 +64,7 @@ if environment_variables.WORKFLOW_NAME is None:
6464
if environment_variables.AGENT_NAME is None:
6565
raise ValueError("Environment variable AGENT_NAME is not set")
6666

67-
logger = make_logger(__name__)
67+
logger = make_workflow_logger(__name__)
6868

6969

7070
@workflow.defn(name=environment_variables.WORKFLOW_NAME)

src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ from agentex.lib import adk
77
from agentex.protocol.acp import CreateTaskParams, SendEventParams
88
from agentex.lib.core.temporal.workflows.workflow import BaseWorkflow
99
from agentex.lib.core.temporal.types.workflow import SignalName
10-
from agentex.lib.utils.logging import make_logger
10+
from agentex.lib.core.temporal.logging import make_workflow_logger
1111
from agentex.types.text_content import TextContent
1212
from agentex.lib.environment_variables import EnvironmentVariables
1313
from agents import Agent, Runner, set_trace_processors
@@ -44,7 +44,7 @@ if environment_variables.WORKFLOW_NAME is None:
4444
if environment_variables.AGENT_NAME is None:
4545
raise ValueError("Environment variable AGENT_NAME is not set")
4646

47-
logger = make_logger(__name__)
47+
logger = make_workflow_logger(__name__)
4848

4949
# Setup tracing for SGP (Scale GenAI Platform)
5050
# This enables visibility into your agent's execution in the SGP dashboard

src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ from project.agent import TaskDeps, temporal_agent
2525
from agentex.lib import adk
2626
from agentex.protocol.acp import SendEventParams, CreateTaskParams
2727
from agentex.lib.types.tracing import SGPTracingProcessorConfig
28-
from agentex.lib.utils.logging import make_logger
28+
from agentex.lib.core.temporal.logging import make_workflow_logger
2929
from agentex.types.text_content import TextContent
3030
from agentex.lib.environment_variables import EnvironmentVariables
3131
from agentex.lib.core.temporal.types.workflow import SignalName
@@ -55,7 +55,7 @@ if environment_variables.WORKFLOW_NAME is None:
5555
if environment_variables.AGENT_NAME is None:
5656
raise ValueError("Environment variable AGENT_NAME is not set")
5757

58-
logger = make_logger(__name__)
58+
logger = make_workflow_logger(__name__)
5959

6060

6161
@workflow.defn(name=environment_variables.WORKFLOW_NAME)

src/agentex/lib/cli/templates/temporal/project/workflow.py.j2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ from agentex.lib import adk
66
from agentex.protocol.acp import CreateTaskParams, SendEventParams
77
from agentex.lib.core.temporal.workflows.workflow import BaseWorkflow
88
from agentex.lib.core.temporal.types.workflow import SignalName
9-
from agentex.lib.utils.logging import make_logger
9+
from agentex.lib.core.temporal.logging import make_workflow_logger
1010
from agentex.types.text_content import TextContent
1111
from agentex.lib.environment_variables import EnvironmentVariables
1212

@@ -18,7 +18,7 @@ if environment_variables.WORKFLOW_NAME is None:
1818
if environment_variables.AGENT_NAME is None:
1919
raise ValueError("Environment variable AGENT_NAME is not set")
2020

21-
logger = make_logger(__name__)
21+
logger = make_workflow_logger(__name__)
2222

2323
@workflow.defn(name=environment_variables.WORKFLOW_NAME)
2424
class {{ workflow_class }}(BaseWorkflow):
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, override
4+
from collections.abc import MutableMapping
5+
6+
from temporalio import workflow
7+
8+
from agentex.lib.utils.logging import make_logger
9+
10+
11+
class WorkflowLoggerAdapter(workflow.LoggerAdapter):
12+
"""Skip workflow replay logs and add IDs without changing non-workflow logs."""
13+
14+
@override
15+
def isEnabledFor(self, level: int) -> bool:
16+
if not workflow.in_workflow():
17+
return self.logger.isEnabledFor(level)
18+
return super().isEnabledFor(level)
19+
20+
@override
21+
def process(self, msg: Any, kwargs: MutableMapping[str, Any]) -> tuple[Any, MutableMapping[str, Any]]:
22+
if workflow.in_workflow():
23+
info = workflow.info()
24+
kwargs["extra"] = {
25+
"workflow_id": info.workflow_id,
26+
"run_id": info.run_id,
27+
**(kwargs.get("extra") or {}),
28+
}
29+
return msg, kwargs
30+
31+
32+
def make_workflow_logger(name: str) -> WorkflowLoggerAdapter:
33+
"""Create an SDK logger that suppresses replay and adds workflow/run IDs."""
34+
return WorkflowLoggerAdapter(make_logger(name), {})

src/agentex/lib/core/temporal/plugins/openai_agents/interceptors/context_interceptor.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
)
2323
from temporalio.converter import default
2424

25+
from agentex.lib.core.temporal.logging import WorkflowLoggerAdapter
26+
2527
# Set up logging
26-
logger = logging.getLogger("context.interceptor")
28+
logger = WorkflowLoggerAdapter(logging.getLogger("context.interceptor"), {})
2729

2830
# Global context variables that models can read
2931
# These are thread-safe and work across async boundaries

src/agentex/lib/core/temporal/workers/worker.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,8 @@ async def run(
273273
max_concurrent_activities=self.max_concurrent_activities,
274274
build_id=str(uuid.uuid4()),
275275
debug_mode=debug_enabled, # Disable deadlock detection in debug mode
276-
# Tracing interceptor OUTERMOST so business interceptors (and the spans
277-
# they create) nest under the propagated workflow/activity span.
278-
interceptors=[*temporal_tracing_interceptors(), *self.interceptors],
276+
# Temporal inherits client tracing before these business interceptors.
277+
interceptors=self.interceptors,
279278
)
280279

281280
logger.info(f"Starting workers for task queue: {self.task_queue}")

0 commit comments

Comments
 (0)