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: 2 additions & 0 deletions docs/wayflowcore/source/core/api/agentspec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ This event listener makes WayFlow components emit traces according to the Agent
.. _agentspeceventlistener:
.. autoclass:: wayflowcore.agentspec.tracing.AgentSpecEventListener

.. autofunction:: wayflowcore.agentspec.tracing.dump_tracing_model


Custom Components
=================
Expand Down
6 changes: 6 additions & 0 deletions docs/wayflowcore/source/core/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ New features
Improvements
^^^^^^^^^^^^

* **Improved Agent Spec tracing compatibility**

Agent Spec tracing exports now serialize WayFlow Agent Spec plugin components with the
proper plugin context and report a valid flow end branch when a flow finishes through a
transition to ``None``.

* **Scoped opt-in for authless MCP clients**

Added ``authless_mcp_enabled()`` as a scoped context manager for local or test MCP clients
Expand Down
12 changes: 12 additions & 0 deletions docs/wayflowcore/source/core/code_examples/howto_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,15 @@ def subtract(a: float, b: float) -> float:
conversation.append_user_message("Compute 2+3")
status = conversation.execute()
# .. end-##_Enable_Agent_Spec_Tracing

# .. start-##_Dump_Agent_Spec_Tracing
from pyagentspec.tracing.events import AgentExecutionStart as AgentSpecAgentExecutionStart
from wayflowcore.agentspec.tracing import dump_tracing_model

# Use this helper when a custom Agent Spec span processor/exporter needs to serialize
# Agent Spec tracing events or spans that may contain WayFlow extension/plugin components.
agentspec_agent = AgentSpecExporter().to_component(agent)
serialized_event = dump_tracing_model(
AgentSpecAgentExecutionStart(agent=agentspec_agent, inputs={})
)
# .. end-##_Dump_Agent_Spec_Tracing
9 changes: 9 additions & 0 deletions docs/wayflowcore/source/core/howtoguides/howto_tracing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ Here's an example of how to use it in your code.
:start-after: .. start-##_Enable_Agent_Spec_Tracing
:end-before: .. end-##_Enable_Agent_Spec_Tracing

If you implement a custom Agent Spec span processor or exporter and need to serialize Agent Spec
tracing events/spans emitted by WayFlow, use ``dump_tracing_model`` so WayFlow's Agent Spec plugin
components are dumped with the correct serialization context.

.. literalinclude:: ../code_examples/howto_tracing.py
:language: python
:start-after: .. start-##_Dump_Agent_Spec_Tracing
:end-before: .. end-##_Dump_Agent_Spec_Tracing


Agent Spec Exporting/Loading
============================
Expand Down
15 changes: 15 additions & 0 deletions wayflowcore/src/wayflowcore/agentspec/_legacy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright © 2025, 2026 Oracle and/or its affiliates.
#
# This software is under the Apache License 2.0
# (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or Universal Permissive License
# (UPL) 1.0 (LICENSE-UPL or https://oss.oracle.com/licenses/upl), at your option.


def _resolve_legacy_configurations(serialized_config: str) -> str:
"""
Normalize legacy Agent Spec component names before deserialization.
"""
return serialized_config.replace(
"PluginSwarmToolRequestAndCallsTransform",
"PluginToolRequestAndCallsTransform",
)
4 changes: 2 additions & 2 deletions wayflowcore/src/wayflowcore/agentspec/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
PluginCoalesceSystemMessagesTransform,
PluginReactMergeToolRequestAndCallsTransform,
PluginRemoveEmptyNonUserMessageTransform,
PluginSwarmToolRequestAndCallsTransform,
PluginToolRequestAndCallsTransform,
messagetransform_deserialization_plugin,
messagetransform_serialization_plugin,
)
Expand Down Expand Up @@ -225,9 +225,9 @@
"contextprovider_deserialization_plugin",
"PluginAppendTrailingSystemMessageToUserMessageTransform",
"PluginCoalesceSystemMessagesTransform",
"PluginToolRequestAndCallsTransform",
"PluginRemoveEmptyNonUserMessageTransform",
"PluginReactMergeToolRequestAndCallsTransform",
"PluginSwarmToolRequestAndCallsTransform",
"messagetransform_serialization_plugin",
"messagetransform_deserialization_plugin",
"PluginPromptTemplate",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,8 @@ class PluginReactMergeToolRequestAndCallsTransform(MessageTransform):
"""Simple message processor that joins tool requests and calls into a python-like message"""


class PluginSwarmToolRequestAndCallsTransform(MessageTransform):
"""Format Tool requests as Agent messages and Tool results as User messages to have a simple User/Agent
sequence of messages."""
class PluginToolRequestAndCallsTransform(MessageTransform):
"""Format tool requests as agent messages and tool results as user messages."""


class PluginCanonicalizationMessageTransform(MessageTransform):
Expand Down Expand Up @@ -98,7 +97,7 @@ class PluginSplitPromptOnMarkerMessageTransform(MessageTransform):
PluginAppendTrailingSystemMessageToUserMessageTransform.__name__: PluginAppendTrailingSystemMessageToUserMessageTransform,
PluginLlamaMergeToolRequestAndCallsTransform.__name__: PluginLlamaMergeToolRequestAndCallsTransform,
PluginReactMergeToolRequestAndCallsTransform.__name__: PluginReactMergeToolRequestAndCallsTransform,
PluginSwarmToolRequestAndCallsTransform.__name__: PluginSwarmToolRequestAndCallsTransform,
PluginToolRequestAndCallsTransform.__name__: PluginToolRequestAndCallsTransform,
PluginCanonicalizationMessageTransform.__name__: PluginCanonicalizationMessageTransform,
PluginSplitPromptOnMarkerMessageTransform.__name__: PluginSplitPromptOnMarkerMessageTransform,
},
Expand All @@ -111,7 +110,7 @@ class PluginSplitPromptOnMarkerMessageTransform(MessageTransform):
PluginAppendTrailingSystemMessageToUserMessageTransform.__name__: PluginAppendTrailingSystemMessageToUserMessageTransform,
PluginLlamaMergeToolRequestAndCallsTransform.__name__: PluginLlamaMergeToolRequestAndCallsTransform,
PluginReactMergeToolRequestAndCallsTransform.__name__: PluginReactMergeToolRequestAndCallsTransform,
PluginSwarmToolRequestAndCallsTransform.__name__: PluginSwarmToolRequestAndCallsTransform,
PluginToolRequestAndCallsTransform.__name__: PluginToolRequestAndCallsTransform,
PluginCanonicalizationMessageTransform.__name__: PluginCanonicalizationMessageTransform,
PluginSplitPromptOnMarkerMessageTransform.__name__: PluginSplitPromptOnMarkerMessageTransform,
},
Expand Down
3 changes: 3 additions & 0 deletions wayflowcore/src/wayflowcore/agentspec/runtimeloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pyagentspec.serialization.types import ComponentsRegistryT as AgentSpecComponentsRegistryT
from typing_extensions import TypeAlias

from wayflowcore.agentspec._legacy import _resolve_legacy_configurations
from wayflowcore.agentspec.components.mcp import PluginStdioTransport
from wayflowcore.component import Component as RuntimeComponent
from wayflowcore.serialization.plugins import WayflowDeserializationPlugin
Expand Down Expand Up @@ -280,6 +281,7 @@ def load_json(
... )

"""
serialized_assistant = _resolve_legacy_configurations(serialized_assistant)
deserializer = AgentSpecDeserializer(
plugins=self._get_all_agentspec_plugins(),
allowed_components=self.allowed_components,
Expand Down Expand Up @@ -459,6 +461,7 @@ def load_yaml(
... )

"""
serialized_assistant = _resolve_legacy_configurations(serialized_assistant)
deserializer = AgentSpecDeserializer(
plugins=self._get_all_agentspec_plugins(),
allowed_components=self.allowed_components,
Expand Down
58 changes: 54 additions & 4 deletions wayflowcore/src/wayflowcore/agentspec/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@
# (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or Universal Permissive License
# (UPL) 1.0 (LICENSE-UPL or https://oss.oracle.com/licenses/upl), at your option.
import json
from typing import Dict, Optional, Union, cast
from typing import Dict, List, Optional, Union, cast

from pyagentspec import Component as AgentSpecComponent
from pyagentspec.agent import Agent as AgentSpecAgent
from pyagentspec.flows.flow import Flow as AgentSpecFlow
from pyagentspec.flows.node import Node as AgentSpecNode
from pyagentspec.llms import LlmConfig as AgentSpecLlmConfig
from pyagentspec.llms import LlmGenerationConfig
from pyagentspec.serialization import ComponentSerializationPlugin
from pyagentspec.tools import Tool as AgentSpecTool
from pyagentspec.tracing._basemodel import _TracingSerializationContextImpl
from pyagentspec.tracing.events import AgentExecutionEnd as AgentSpecAgentExecutionEnd
from pyagentspec.tracing.events import AgentExecutionStart as AgentSpecAgentExecutionStart
from pyagentspec.tracing.events import Event as AgentSpecEvent
from pyagentspec.tracing.events import ExceptionRaised as AgentSpecExceptionRaised
from pyagentspec.tracing.events import FlowExecutionEnd as AgentSpecFlowExecutionEnd
from pyagentspec.tracing.events import FlowExecutionStart as AgentSpecFlowExecutionStart
Expand Down Expand Up @@ -56,18 +59,51 @@
)
from wayflowcore.events.eventlistener import EventListener
from wayflowcore.executors.executionstatus import FinishedStatus
from wayflowcore.serialization.plugins import WayflowSerializationPlugin
from wayflowcore.tracing.span import LlmGenerationSpan, get_active_span_stack, get_current_span


def _create_tracing_serialization_context(
plugins: Optional[List[Union[ComponentSerializationPlugin, WayflowSerializationPlugin]]] = None,
) -> _TracingSerializationContextImpl:
"""
Build a tracing serialization context that knows how to dump WayFlow Agent Spec extension components.
"""
exporter = AgentSpecExporter(plugins=plugins)
return _TracingSerializationContextImpl(plugins=exporter._get_all_agentspec_plugins())


def dump_tracing_model(
model: Union[AgentSpecEvent, AgentSpecSpan],
mask_sensitive_information: bool = True,
plugins: Optional[List[Union[ComponentSerializationPlugin, WayflowSerializationPlugin]]] = None,
) -> Dict[str, object]:
"""
Serialize an Agent Spec tracing span/event using the WayFlow Agent Spec serialization plugins.
"""
if not isinstance(model, (AgentSpecEvent, AgentSpecSpan)):
raise TypeError("dump_tracing_model only supports Agent Spec tracing events and spans.")
return model.model_dump(
mask_sensitive_information=mask_sensitive_information,
context=_create_tracing_serialization_context(plugins=plugins),
)


class AgentSpecEventListener(EventListener):
"""Event listener that emits traces according to the Open Agent Spec Tracing standard"""

def __init__(self) -> None:
def __init__(
self,
plugins: Optional[
List[Union[ComponentSerializationPlugin, WayflowSerializationPlugin]]
] = None,
) -> None:
super().__init__()
self.plugins = plugins
# We keep track of the mapping between the wayflow span (id) and the corresponding agent spec span
self.agentspec_spans_registry: Dict[str, AgentSpecSpan] = {}
# As we need to store agent spec objects in the agent spec spans and events, we need to perform conversions
self.agentspec_exporter: AgentSpecExporter = AgentSpecExporter()
self.agentspec_exporter: AgentSpecExporter = AgentSpecExporter(plugins=plugins)
# We keep a registry of conversions, so that we do not repeat the conversion for the same object twice
self.agentspec_components_registry: Dict[str, AgentSpecComponent] = {}
# Track last assistant message id and a robust mapping tool_request_id -> assistant message id.
Expand All @@ -76,6 +112,16 @@ def __init__(self) -> None:
self._last_assistant_message_id: Union[str, None] = None
self._tool_to_message: Dict[str, Optional[str]] = {}

def dump_tracing_model(
self, model: Union[AgentSpecEvent, AgentSpecSpan], mask_sensitive_information: bool = True
) -> Dict[str, object]:
"""Serialize an Agent Spec tracing span/event with the listener's plugin set."""
return dump_tracing_model(
model=model,
mask_sensitive_information=mask_sensitive_information,
plugins=self.plugins,
)

def _convert_to_agentspec(self, component: Component) -> AgentSpecComponent:
if component.id not in self.agentspec_components_registry:
self.agentspec_components_registry[component.id] = self.agentspec_exporter.to_component(
Expand Down Expand Up @@ -340,7 +386,11 @@ def __call__(self, event: Event) -> None:
AgentSpecFlow, self._convert_to_agentspec(event.conversational_component)
)
if isinstance(event.execution_status, FinishedStatus):
branch_selected = event.execution_status.complete_step_name
branch_selected = (
event.execution_status.complete_step_name
or event.execution_status._final_step_name
or ""
)
outputs = event.execution_status.output_values
else:
branch_selected = ""
Expand Down
3 changes: 3 additions & 0 deletions wayflowcore/src/wayflowcore/executors/_flowexecutor.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,7 @@ async def _execute_flow(
logger.debug("Interrupts received: %s", execution_interrupts)
flow_state = conversation.state
last_complete_step_name_executed = None
last_step_name_executed = None

try:

Expand All @@ -756,6 +757,7 @@ async def _execute_flow(
)

current_step = flow_state.flow.steps[flow_state.current_step_name]
last_step_name_executed = flow_state.current_step_name
if isinstance(current_step, CompleteStep):
last_complete_step_name_executed = flow_state.current_step_name

Expand Down Expand Up @@ -918,6 +920,7 @@ async def _execute_flow(
return FinishedStatus(
output_values=outputs,
complete_step_name=last_complete_step_name_executed,
_final_step_name=last_step_name_executed,
_conversation_id=conversation.id,
)

Expand Down
8 changes: 6 additions & 2 deletions wayflowcore/src/wayflowcore/executors/executionstatus.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ class FinishedStatus(ExecutionStatus):
output_values: Dict[str, Any]
"""The outputs produced by the agent or flow returning this execution status."""
complete_step_name: Optional[str] = None
"""The name of the last step reached if the flow returning this execution status transitioned \
"""The name of the last step reached if the flow returning this execution status transitioned
to a ``CompleteStep``, otherwise ``None``."""
_final_step_name: Optional[str] = None
"""The name of the last executed step, including flows that end via a transition to ``None``."""

@property
def _requires_yielding(self) -> bool:
Expand All @@ -56,6 +58,7 @@ def _serialize_to_dict(self, serialization_context: "SerializationContext") -> D
return {
"output_values": self.output_values,
"complete_step_name": self.complete_step_name,
"_final_step_name": self._final_step_name,
"_conversation_id": self._conversation_id,
"id": self.id,
}
Expand All @@ -66,7 +69,8 @@ def _deserialize_from_dict(
) -> "SerializableObject":
return FinishedStatus(
output_values=input_dict["output_values"],
complete_step_name=input_dict["complete_step_name"],
complete_step_name=input_dict.get("complete_step_name"),
_final_step_name=input_dict.get("_final_step_name", input_dict.get("final_step_name")),
_conversation_id=input_dict.get("_conversation_id", None),
id=input_dict.get("id") or IdGenerator.get_or_generate_id(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,5 +203,5 @@
"_PythonMergeToolRequestAndCallsTransform",
"_ReactMergeToolRequestAndCallsTransform",
"_TokenConsumptionEvent",
"_ToolRequestAndCallsTransform",
"ToolRequestAndCallsTransform",
}
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@
PluginSplitPromptOnMarkerMessageTransform as AgentSpecPluginSplitPromptOnMarkerMessageTransform,
)
from wayflowcore.agentspec.components.transforms import (
PluginSwarmToolRequestAndCallsTransform as AgentSpecPluginSwarmToolRequestAndCallsTransform,
PluginToolRequestAndCallsTransform as AgentSpecPluginToolRequestAndCallsTransform,
)
from wayflowcore.contextproviders.constantcontextprovider import (
ConstantContextProvider as RuntimeConstantContextProvider,
Expand Down Expand Up @@ -445,8 +445,8 @@
from wayflowcore.swarm import HandoffMode as RuntimeHandoffMode
from wayflowcore.swarm import Swarm as RuntimeSwarm
from wayflowcore.templates import PromptTemplate as RuntimePromptTemplate
from wayflowcore.templates._swarmtemplate import (
_ToolRequestAndCallsTransform as RuntimeSwarmToolRequestAndCallsTransform,
from wayflowcore.templates.agenticpatterntemplate import (
ToolRequestAndCallsTransform as RuntimeToolRequestAndCallsTransform,
)
from wayflowcore.templates.llamatemplates import (
_LlamaMergeToolRequestAndCallsTransform as RuntimeLlamaMergeToolRequestAndCallsTransform,
Expand Down Expand Up @@ -1921,8 +1921,8 @@ class SupportsTimeoutKwargs(TypedDict, total=False):
return RuntimeReactMergeToolRequestAndCallsTransform(
**self._get_component_arguments(agentspec_component)
)
elif isinstance(agentspec_component, AgentSpecPluginSwarmToolRequestAndCallsTransform):
return RuntimeSwarmToolRequestAndCallsTransform(
elif isinstance(agentspec_component, AgentSpecPluginToolRequestAndCallsTransform):
return RuntimeToolRequestAndCallsTransform(
**self._get_component_arguments(agentspec_component)
)
elif isinstance(agentspec_component, AgentSpecPluginCanonicalizationMessageTransform):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@
PluginSplitPromptOnMarkerMessageTransform as AgentSpecPluginSplitPromptOnMarkerMessageTransform,
)
from wayflowcore.agentspec.components.transforms import (
PluginSwarmToolRequestAndCallsTransform as AgentSpecPluginSwarmToolRequestAndCallsTransform,
PluginToolRequestAndCallsTransform as AgentSpecPluginToolRequestAndCallsTransform,
)
from wayflowcore.contextproviders import ContextProvider as RuntimeContextProvider
from wayflowcore.contextproviders.constantcontextprovider import (
Expand Down Expand Up @@ -447,8 +447,8 @@
)
from wayflowcore.swarm import Swarm as RuntimeSwarm
from wayflowcore.templates import PromptTemplate as RuntimePromptTemplate
from wayflowcore.templates._swarmtemplate import (
_ToolRequestAndCallsTransform as RuntimeSwarmToolRequestAndCallsTransform,
from wayflowcore.templates.agenticpatterntemplate import (
ToolRequestAndCallsTransform as RuntimeToolRequestAndCallsTransform,
)
from wayflowcore.templates.llamatemplates import (
_LlamaMergeToolRequestAndCallsTransform as RuntimeLlamaMergeToolRequestAndCallsTransform,
Expand Down Expand Up @@ -1741,9 +1741,9 @@ def _messagetransform_convert_to_agentspec(
runtime_messagetransform
),
)
elif isinstance(runtime_messagetransform, RuntimeSwarmToolRequestAndCallsTransform):
return AgentSpecPluginSwarmToolRequestAndCallsTransform(
name="swarmtoolrequestandcalls_messagetransform",
elif isinstance(runtime_messagetransform, RuntimeToolRequestAndCallsTransform):
return AgentSpecPluginToolRequestAndCallsTransform(
name="toolrequestandcalls_messagetransform",
metadata=_create_agentspec_metadata_from_runtime_component(
runtime_messagetransform
),
Expand Down
Loading