Skip to content

Commit 75f63f1

Browse files
committed
Merge remote-tracking branch 'origin/fix/google-adk-agents-mcp-connection-leak'
2 parents ba4cd5c + 3a44d24 commit 75f63f1

17 files changed

Lines changed: 389 additions & 28 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ to include examples, links to docs, or any other relevant information.
2020

2121
### Added
2222

23+
- Added experimental SDK payload converter support for values and type hints
24+
decorated with `@transfer_type_convertible(...)` using a `TransferTypeConverter` class.
25+
This lets types with transfer type converters delegate their wire representation to the
26+
configured payload converter, preserving SDK behavior such as serialization
27+
contexts.
2328
- Added `TLSConfig.verification_server_name` to verify the server certificate against a fixed name
2429
instead of the connection's server name. Unlike `domain`, it does not change the TLS SNI or
2530
HTTP/2 authority values, which keep following the connected host, so it can be used when the
@@ -37,6 +42,13 @@ to include examples, links to docs, or any other relevant information.
3742

3843
### Breaking Changes
3944

45+
- Custom workflow runners that construct `WorkflowInstanceDetails` must now pass
46+
`payload_converter_factory` instead of `payload_converter_class`. The factory
47+
returns the already wrapped payload converter that workflow instances should
48+
use.
49+
- System Nexus payload converter helpers added for generated bindings are now
50+
private implementation details, and the remaining public `temporalio.nexus.system`
51+
APIs are marked experimental and subject to change.
4052
- Payload size limits have moved from `DataConverter` to `Client.connect`. Pass
4153
`payload_limits=PayloadLimitsConfig(...)` (now exported from
4254
`temporalio.client`) instead of setting `payload_limits` on `DataConverter`.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ manifest-path = "temporalio/bridge/Cargo.toml"
261261
module-name = "temporalio.bridge.temporal_sdk_bridge"
262262
python-packages = ["temporalio"]
263263
include = ["LICENSE"]
264-
exclude = ["temporalio/bridge/target/**/*"]
264+
exclude = ["temporalio/bridge/target/**/*", "temporalio/bridge/sdk-core/.git"]
265265

266266
[tool.uv]
267267
# Prevent uv commands from building the package by default

scripts/gen_payload_visitor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ async def _visit_nexus_operation_input_payload(
191191
endpoint: str,
192192
payload: Payload,
193193
) -> None:
194-
new_payload = await temporalio.nexus.system.maybe_visit_payload(
194+
new_payload = await temporalio.nexus.system._maybe_visit_payload(
195195
endpoint,
196196
payload,
197197
fs,

temporalio/activity.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
import temporalio.bridge.proto.activity_task
3030
import temporalio.common
3131
import temporalio.converter
32+
from temporalio.converter._payload_converter import (
33+
_TemporalTransferTypePayloadConverter,
34+
)
3235

3336
from .types import CallableType
3437

@@ -238,9 +241,13 @@ def payload_converter(self) -> temporalio.converter.PayloadConverter:
238241
self.payload_converter_class_or_instance,
239242
temporalio.converter.PayloadConverter,
240243
):
241-
self._payload_converter = self.payload_converter_class_or_instance
244+
self._payload_converter = _TemporalTransferTypePayloadConverter.wrap(
245+
self.payload_converter_class_or_instance
246+
)
242247
else:
243-
self._payload_converter = self.payload_converter_class_or_instance()
248+
self._payload_converter = _TemporalTransferTypePayloadConverter.wrap(
249+
self.payload_converter_class_or_instance()
250+
)
244251
return self._payload_converter
245252

246253
@property

temporalio/bridge/_visitor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async def _visit_nexus_operation_input_payload(
6161
endpoint: str,
6262
payload: Payload,
6363
) -> None:
64-
new_payload = await temporalio.nexus.system.maybe_visit_payload(
64+
new_payload = await temporalio.nexus.system._maybe_visit_payload(
6565
endpoint,
6666
payload,
6767
fs,

temporalio/converter/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
JSONTypeConverter,
3434
JSONTypeConverterUnhandled,
3535
PayloadConverter,
36+
TransferTypeConverter,
37+
transfer_type_convertible,
3638
value_to_type,
3739
)
3840
from temporalio.converter._search_attributes import (
@@ -64,6 +66,7 @@
6466
"BinaryPlainPayloadConverter",
6567
"BinaryProtoPayloadConverter",
6668
"CompositePayloadConverter",
69+
"TransferTypeConverter",
6770
"DataConverter",
6871
"DefaultFailureConverter",
6972
"DefaultFailureConverterWithEncodedAttributes",
@@ -79,6 +82,7 @@
7982
"SerializationContext",
8083
"WithSerializationContext",
8184
"WorkflowSerializationContext",
85+
"transfer_type_convertible",
8286
"decode_search_attributes",
8387
"decode_typed_search_attributes",
8488
"default",

temporalio/converter/_data_converter.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
)
2929
from temporalio.converter._payload_converter import (
3030
PayloadConverter,
31+
_TemporalTransferTypePayloadConverter,
3132
)
3233
from temporalio.converter._serialization_context import (
3334
SerializationContext,
@@ -90,9 +91,15 @@ class DataConverter(WithSerializationContext):
9091
"""Singleton default data converter."""
9192

9293
def __post_init__(self) -> None: # noqa: D105
93-
object.__setattr__(self, "payload_converter", self.payload_converter_class())
94+
object.__setattr__(self, "payload_converter", self._new_payload_converter())
9495
object.__setattr__(self, "failure_converter", self.failure_converter_class())
9596

97+
def _new_payload_converter(self) -> PayloadConverter:
98+
"""Create a payload converter instance with SDK transfer type hooks enabled."""
99+
return _TemporalTransferTypePayloadConverter.wrap(
100+
self.payload_converter_class()
101+
)
102+
96103
async def encode(
97104
self, values: Sequence[Any]
98105
) -> list[temporalio.api.common.v1.Payload]:

temporalio/converter/_payload_converter.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from typing import (
2222
Any,
2323
ClassVar,
24+
Generic,
2425
Literal,
2526
NewType,
2627
TypeVar,
@@ -51,6 +52,75 @@
5152
)
5253

5354
_sym_db = google.protobuf.symbol_database.Default()
55+
ValueT = TypeVar("ValueT")
56+
TransferTypeT = TypeVar("TransferTypeT")
57+
_TRANSFER_TYPE_CONVERTER_ATTR = "__temporal_transfer_type_converter"
58+
59+
60+
class TransferTypeConverter(Generic[ValueT, TransferTypeT], ABC):
61+
"""Converter between a user-facing value and a transfer type value.
62+
63+
.. warning::
64+
This API is experimental and subject to change.
65+
"""
66+
67+
transfer_type: type[TransferTypeT] | None = None
68+
"""Optional type hint for the transfer type to use when decoding payloads.
69+
70+
.. warning::
71+
This API is experimental and subject to change.
72+
"""
73+
74+
@abstractmethod
75+
def to_transfer_type(self, value: ValueT) -> TransferTypeT:
76+
"""Convert a user-facing value to its transfer type value.
77+
78+
.. warning::
79+
This API is experimental and subject to change.
80+
"""
81+
raise NotImplementedError
82+
83+
@abstractmethod
84+
def from_transfer_type(self, value: TransferTypeT) -> ValueT:
85+
"""Convert a transfer type value to its user-facing value.
86+
87+
.. warning::
88+
This API is experimental and subject to change.
89+
"""
90+
raise NotImplementedError
91+
92+
93+
class _TransferTypeConvertibleDecorator(Generic[ValueT, TransferTypeT]):
94+
def __init__(
95+
self, converter_type: type[TransferTypeConverter[ValueT, TransferTypeT]]
96+
) -> None:
97+
self._converter_type = converter_type
98+
99+
def __call__(self, cls: type[ValueT]) -> type[ValueT]:
100+
if hasattr(cls, _TRANSFER_TYPE_CONVERTER_ATTR):
101+
raise TypeError("class already has a transfer type converter")
102+
setattr(cls, _TRANSFER_TYPE_CONVERTER_ATTR, self._converter_type())
103+
return cls
104+
105+
106+
def transfer_type_convertible(
107+
converter_type: type[TransferTypeConverter[ValueT, TransferTypeT]],
108+
) -> _TransferTypeConvertibleDecorator[ValueT, TransferTypeT]:
109+
"""Decorate a class with a transfer type converter class.
110+
111+
.. warning::
112+
This API is experimental and subject to change.
113+
"""
114+
return _TransferTypeConvertibleDecorator(converter_type)
115+
116+
117+
def _get_transfer_type_converter(
118+
value_type: object,
119+
) -> TransferTypeConverter[Any, Any] | None:
120+
converter = getattr(value_type, _TRANSFER_TYPE_CONVERTER_ATTR, None)
121+
if isinstance(converter, TransferTypeConverter):
122+
return converter
123+
return None
54124

55125

56126
class PayloadConverter(ABC):
@@ -514,6 +584,74 @@ def from_payload(
514584
raise RuntimeError("Failed parsing") from err
515585

516586

587+
class _TemporalTransferTypePayloadConverter(PayloadConverter, WithSerializationContext):
588+
"""Payload converter wrapper for registered Temporal transfer type converters.
589+
590+
Values with a registered transfer type converter are first converted to their
591+
transfer type value, then encoded by the wrapped payload converter. When
592+
decoding to a type with a registered transfer type converter, the wrapped
593+
converter first decodes the payload to the transfer type value and this wrapper
594+
constructs the requested user-facing type from it.
595+
"""
596+
597+
_inner_payload_converter: PayloadConverter
598+
599+
def __init__(self, inner_payload_converter: PayloadConverter) -> None:
600+
"""Create a Temporal transfer type payload converter."""
601+
self._inner_payload_converter = inner_payload_converter
602+
603+
@staticmethod
604+
def wrap(payload_converter: PayloadConverter) -> PayloadConverter:
605+
"""Wrap a payload converter unless it is already wrapped."""
606+
if isinstance(payload_converter, _TemporalTransferTypePayloadConverter):
607+
return payload_converter
608+
return _TemporalTransferTypePayloadConverter(payload_converter)
609+
610+
def to_payloads(
611+
self, values: Sequence[Any]
612+
) -> list[temporalio.api.common.v1.Payload]:
613+
"""See base class."""
614+
transfer_type_values: list[Any] = []
615+
for value in values:
616+
converter = _get_transfer_type_converter(type(value))
617+
if converter is not None:
618+
value = converter.to_transfer_type(value)
619+
transfer_type_values.append(value)
620+
return self._inner_payload_converter.to_payloads(transfer_type_values)
621+
622+
def from_payloads(
623+
self,
624+
payloads: Sequence[temporalio.api.common.v1.Payload],
625+
type_hints: list[type] | None = None,
626+
) -> list[Any]:
627+
"""See base class."""
628+
if type_hints is None:
629+
return self._inner_payload_converter.from_payloads(payloads, None)
630+
converters = [
631+
_get_transfer_type_converter(type_hint) for type_hint in type_hints
632+
]
633+
inner_type_hints = [
634+
converter.transfer_type if converter is not None else type_hint
635+
for converter, type_hint in zip(converters, type_hints)
636+
]
637+
values = self._inner_payload_converter.from_payloads(
638+
payloads, typing.cast("list[type]", inner_type_hints)
639+
)
640+
return [
641+
converter.from_transfer_type(value) if converter is not None else value
642+
for value, converter in zip(values, converters)
643+
]
644+
645+
def with_context(self, context: SerializationContext) -> Self:
646+
"""Return a new instance with context set on the inner converter."""
647+
if not isinstance(self._inner_payload_converter, WithSerializationContext):
648+
return self
649+
inner_payload_converter = self._inner_payload_converter.with_context(context)
650+
if inner_payload_converter is self._inner_payload_converter:
651+
return self
652+
return type(self)(inner_payload_converter)
653+
654+
517655
class AdvancedJSONEncoder(json.JSONEncoder):
518656
"""Advanced JSON encoder.
519657

0 commit comments

Comments
 (0)