agentenv-framework-protocol 0.1.269__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,449 @@
1
+ """Discovery and validation for A2A extension implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Callable, Iterable, Mapping
7
+ from dataclasses import dataclass
8
+ from types import UnionType
9
+ from typing import Any, Union, get_args, get_origin, get_type_hints
10
+
11
+ from pydantic import BaseModel
12
+
13
+ from .extensions import (
14
+ _HANDLER_BINDING,
15
+ ExtensionActivation,
16
+ ExtensionDefinition,
17
+ HandlerBinding,
18
+ ImplementationOwner,
19
+ OperationDefinition,
20
+ _validate_reserved_definition,
21
+ )
22
+
23
+
24
+ def _validate_handler_signature(
25
+ handler: Callable,
26
+ request_model: type[BaseModel] | None,
27
+ *,
28
+ label: str,
29
+ allow_request_supertype: bool = False,
30
+ ) -> None:
31
+ if not inspect.iscoroutinefunction(handler):
32
+ raise ValueError(f"handler for {label} must be an async function")
33
+ parameters = list(inspect.signature(handler).parameters.values())
34
+ if request_model is None:
35
+ if parameters:
36
+ raise ValueError(f"handler for {label} must not accept a request argument")
37
+ return
38
+ if len(parameters) != 1 or parameters[0].kind not in (
39
+ inspect.Parameter.POSITIONAL_ONLY,
40
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
41
+ ):
42
+ raise ValueError(
43
+ f"handler for {label} must accept exactly one typed request argument"
44
+ )
45
+ parameter = parameters[0]
46
+ try:
47
+ annotation = get_type_hints(handler).get(parameter.name, parameter.annotation)
48
+ except (NameError, TypeError):
49
+ annotation = parameter.annotation
50
+ if isinstance(annotation, str):
51
+ annotation = handler.__globals__.get(annotation, annotation)
52
+ is_union = isinstance(annotation, UnionType) or get_origin(annotation) is Union
53
+ accepted = get_args(annotation) if is_union else (annotation,)
54
+ valid_annotation = request_model in accepted
55
+ if allow_request_supertype:
56
+ valid_annotation = any(
57
+ isinstance(model, type)
58
+ and issubclass(model, BaseModel)
59
+ and issubclass(request_model, model)
60
+ for model in accepted
61
+ )
62
+ if not valid_annotation:
63
+ raise ValueError(
64
+ f"handler for {label} must annotate its request argument as "
65
+ f"{request_model.__name__}"
66
+ )
67
+
68
+
69
+ def _deep_merge(base: dict[str, Any], overlay: Mapping[str, Any]) -> dict[str, Any]:
70
+ result = dict(base)
71
+ for key, value in overlay.items():
72
+ if isinstance(value, Mapping) and isinstance(result.get(key), Mapping):
73
+ result[key] = _deep_merge(dict(result[key]), value)
74
+ else:
75
+ result[key] = value
76
+ return result
77
+
78
+
79
+ def _effective_methods(method: str) -> tuple[str, ...]:
80
+ method = method.upper()
81
+ return (method, "HEAD") if method == "GET" else (method,)
82
+
83
+
84
+ @dataclass(frozen=True, slots=True)
85
+ class RegisteredOperation:
86
+ definition: OperationDefinition
87
+ handler: Callable | None
88
+ variant_handlers: Mapping[str, Callable]
89
+ enabled_optional_variants: frozenset[str]
90
+ request_model_override: type[BaseModel] | None = None
91
+ overridden: bool = False
92
+
93
+ def select_handler(
94
+ self, payload: Mapping[str, Any]
95
+ ) -> tuple[Callable | None, str | None]:
96
+ variant = None
97
+ if self.definition.request is not None:
98
+ variant = self.definition.request.select_variant(
99
+ payload, self.enabled_optional_variants
100
+ )
101
+ return self.variant_handlers.get(variant, self.handler), variant
102
+
103
+ def request_model(self, variant: str | None) -> type[BaseModel] | None:
104
+ if self.definition.request is None:
105
+ return None
106
+ if variant is None and self.request_model_override is not None:
107
+ return self.request_model_override
108
+ return self.definition.request.model_for_variant(variant)
109
+
110
+
111
+ @dataclass(frozen=True, slots=True)
112
+ class RegisteredExtension:
113
+ activation: ExtensionActivation
114
+ operations: Mapping[str, RegisteredOperation]
115
+
116
+ @property
117
+ def definition(self) -> ExtensionDefinition:
118
+ return self.activation.definition
119
+
120
+ def to_card(self) -> dict[str, Any]:
121
+ definition = self.definition
122
+ params: dict[str, Any] = {}
123
+ if definition.endpoint is not None:
124
+ params["endpoint"] = definition.endpoint
125
+ methods: dict[str, Any] = {}
126
+ configured_methods = self.activation.wire_params.get("methods", {})
127
+ for operation in self.operations.values():
128
+ spec = operation.definition
129
+ method: dict[str, Any] = {"method": spec.method}
130
+ if definition.endpoint != spec.path:
131
+ method["endpoint"] = spec.path
132
+ configured_method = configured_methods.get(spec.name, {})
133
+ if spec.request is not None and "request" not in configured_method:
134
+ method["request"] = spec.request.to_card(
135
+ operation.enabled_optional_variants
136
+ )
137
+ if spec.response is not None:
138
+ method["response"] = spec.response.to_card()
139
+ methods[spec.name] = method
140
+ if methods:
141
+ params["methods"] = methods
142
+ params = _deep_merge(params, self.activation.wire_params)
143
+ card = {
144
+ "uri": definition.uri,
145
+ "description": (
146
+ self.activation.description
147
+ if self.activation.description is not None
148
+ else definition.description
149
+ ),
150
+ "params": params,
151
+ }
152
+ if self.activation.required is not None:
153
+ card["required"] = self.activation.required
154
+ return card
155
+
156
+
157
+ class ExtensionRegistry:
158
+ """Resolved extensions, handlers, routes, and generated card entries."""
159
+
160
+ def __init__(self, extensions: Iterable[RegisteredExtension]) -> None:
161
+ self.extensions = tuple(extensions)
162
+ self._by_uri = {
163
+ extension.definition.uri: extension for extension in self.extensions
164
+ }
165
+ if len(self._by_uri) != len(self.extensions):
166
+ raise ValueError("duplicate extension URI")
167
+
168
+ seen_routes: dict[tuple[str, str], tuple[str, str]] = {}
169
+ for extension in self.extensions:
170
+ for operation in extension.operations.values():
171
+ identity = (extension.definition.uri, operation.definition.name)
172
+ for method in _effective_methods(operation.definition.method):
173
+ route = (operation.definition.path, method)
174
+ previous = seen_routes.get(route)
175
+ if previous is not None:
176
+ raise ValueError(
177
+ f"route {route[1]} {route[0]} is shared by "
178
+ f"{previous} and {identity}"
179
+ )
180
+ seen_routes[route] = identity
181
+ self._routes = seen_routes
182
+
183
+ def reject_framework_route_collisions(
184
+ self, routes: Mapping[tuple[str, str], str]
185
+ ) -> None:
186
+ """Reject extension operations that would intercept framework routes."""
187
+ for (path, method), owner in routes.items():
188
+ for effective_method in _effective_methods(method):
189
+ operation = self._routes.get((path, effective_method))
190
+ if operation is not None:
191
+ raise ValueError(
192
+ f"extension operation {operation} conflicts with {owner} "
193
+ f"route {effective_method} {path}"
194
+ )
195
+
196
+ def extension(self, uri: str) -> RegisteredExtension | None:
197
+ return self._by_uri.get(uri)
198
+
199
+ def card_extensions(self) -> list[dict[str, Any]]:
200
+ return [extension.to_card() for extension in self.extensions]
201
+
202
+ def conformance(self) -> dict[str, Any]:
203
+ overrides = []
204
+ for extension in self.extensions:
205
+ for operation in extension.operations.values():
206
+ if operation.overridden:
207
+ overrides.append(
208
+ {
209
+ "uri": extension.definition.uri,
210
+ "operation": operation.definition.name,
211
+ }
212
+ )
213
+ return {"standard_operation_overrides": overrides}
214
+
215
+
216
+ def _discover_bindings(agent: Any) -> list[tuple[HandlerBinding, Callable]]:
217
+ discovered = []
218
+ for name in dir(agent):
219
+ if name.startswith("__"):
220
+ continue
221
+ static_member = inspect.getattr_static(agent, name)
222
+ binding = getattr(static_member, _HANDLER_BINDING, None)
223
+ if binding is not None:
224
+ member = getattr(agent, name)
225
+ discovered.append((binding, member))
226
+ return discovered
227
+
228
+
229
+ def _feature_for_operation(
230
+ definition: ExtensionDefinition, operation_name: str
231
+ ) -> str | None:
232
+ for feature_name, group in definition.optional_features.items():
233
+ if operation_name in group.operations:
234
+ return feature_name
235
+ return None
236
+
237
+
238
+ def build_registry(
239
+ agent: Any,
240
+ declared: Iterable[ExtensionActivation | ExtensionDefinition] = (),
241
+ *,
242
+ sdk_handlers: Mapping[tuple[str, str], Callable] | None = None,
243
+ request_model_overrides: Mapping[tuple[str, str], type[BaseModel]] | None = None,
244
+ ) -> ExtensionRegistry:
245
+ """Build one validated registry from declarations and decorated methods."""
246
+ sdk_handlers = sdk_handlers or {}
247
+ request_model_overrides = request_model_overrides or {}
248
+ activations: dict[str, ExtensionActivation] = {}
249
+ for declaration in declared:
250
+ if isinstance(declaration, ExtensionDefinition):
251
+ activation = ExtensionActivation(declaration)
252
+ elif isinstance(declaration, ExtensionActivation):
253
+ activation = declaration
254
+ else:
255
+ raise TypeError(
256
+ "extensions must contain ExtensionDefinition or ExtensionActivation"
257
+ )
258
+ _validate_reserved_definition(activation.definition)
259
+ uri = activation.definition.uri
260
+ if uri in activations:
261
+ raise ValueError(f"extension {uri} is enabled more than once")
262
+ activations[uri] = activation
263
+
264
+ operation_handlers: dict[tuple[str, str], tuple[Callable, bool]] = {}
265
+ variant_handlers: dict[tuple[str, str, str], Callable] = {}
266
+ definitions: dict[str, ExtensionDefinition] = {
267
+ uri: activation.definition for uri, activation in activations.items()
268
+ }
269
+
270
+ for binding, handler in _discover_bindings(agent):
271
+ _validate_reserved_definition(binding.extension)
272
+ uri = binding.extension.uri
273
+ existing_definition = definitions.get(uri)
274
+ if existing_definition is not None and existing_definition != binding.extension:
275
+ raise ValueError(
276
+ f"conflicting definitions for extension {uri}; define one shared "
277
+ "ExtensionDefinition and bind each operation with @extension(...)"
278
+ )
279
+ definitions[uri] = binding.extension
280
+ # A decorated handler is itself an implementation declaration, so it
281
+ # activates both standard and custom extension definitions. An explicit
282
+ # activation, when present, retains its configuration and metadata.
283
+ activations.setdefault(uri, ExtensionActivation(binding.extension))
284
+ operation = binding.extension.operation(binding.operation)
285
+ if binding.variant is not None:
286
+ if operation.request is None:
287
+ raise ValueError(f"{uri}.{operation.name} has no request variants")
288
+ try:
289
+ variant = operation.request.variant(binding.variant)
290
+ except KeyError as exc:
291
+ raise ValueError(
292
+ f"unknown request variant {binding.variant!r} for {uri}.{operation.name}"
293
+ ) from exc
294
+ variant_owner = variant.implementation or operation.implementation
295
+ if variant_owner is not ImplementationOwner.RUNTIME:
296
+ raise ValueError(
297
+ f"{uri}.{operation.name}.{variant.name} is not runtime-backed"
298
+ )
299
+ key = (uri, binding.operation, binding.variant)
300
+ if key in variant_handlers:
301
+ raise ValueError(
302
+ f"multiple handlers for {uri}.{operation.name}.{binding.variant}"
303
+ )
304
+ variant_handlers[key] = handler
305
+ continue
306
+
307
+ key = (uri, binding.operation)
308
+ if key in operation_handlers:
309
+ raise ValueError(f"multiple handlers for {uri}.{operation.name}")
310
+ operation_handlers[key] = (
311
+ handler,
312
+ operation.implementation is ImplementationOwner.SDK,
313
+ )
314
+
315
+ registered: list[RegisteredExtension] = []
316
+ for uri, activation in activations.items():
317
+ definition = definitions[uri]
318
+ enabled_features = set(activation.features)
319
+
320
+ for feature_name, group in definition.optional_features.items():
321
+ implemented = {
322
+ operation_name
323
+ for operation_name in group.operations
324
+ if (uri, operation_name) in operation_handlers
325
+ or any(
326
+ variant_uri == uri and variant_operation == operation_name
327
+ for variant_uri, variant_operation, _ in variant_handlers
328
+ )
329
+ }
330
+ if implemented:
331
+ if group.required_together and implemented != set(group.operations):
332
+ missing = set(group.operations) - implemented
333
+ raise ValueError(
334
+ f"feature {uri}.{feature_name} is incomplete; missing {sorted(missing)}"
335
+ )
336
+ enabled_features.add(feature_name)
337
+
338
+ active_operations: dict[str, OperationDefinition] = dict(
339
+ definition.core_operations
340
+ )
341
+ for feature_name in enabled_features:
342
+ active_operations.update(
343
+ definition.optional_features[feature_name].operations
344
+ )
345
+
346
+ resolved_operations: dict[str, RegisteredOperation] = {}
347
+ for operation_key, operation in active_operations.items():
348
+ bound = operation_handlers.get((uri, operation_key))
349
+ sdk_handler = sdk_handlers.get((uri, operation_key))
350
+ if bound is not None:
351
+ handler, overridden = bound
352
+ else:
353
+ handler = sdk_handler
354
+ overridden = False
355
+
356
+ enabled_variants = set(activation.variants.get(operation_key, frozenset()))
357
+ per_variant: dict[str, Callable] = {}
358
+ if operation.request is not None and operation.request.variants:
359
+ for variant in operation.request.variants:
360
+ variant_handler = variant_handlers.get(
361
+ (uri, operation_key, variant.name)
362
+ )
363
+ if variant_handler is not None:
364
+ per_variant[variant.name] = variant_handler
365
+ if not variant.support_required:
366
+ enabled_variants.add(variant.name)
367
+
368
+ for variant in operation.request.enabled_variants(enabled_variants):
369
+ selected = per_variant.get(variant.name, handler)
370
+ if selected is not None:
371
+ continue
372
+ variant_owner = variant.implementation or operation.implementation
373
+ owner = (
374
+ "runtime"
375
+ if variant_owner is ImplementationOwner.RUNTIME
376
+ else "SDK"
377
+ )
378
+ raise ValueError(
379
+ f"missing {owner} handler for "
380
+ f"{uri}.{operation.name}.{variant.name}"
381
+ )
382
+
383
+ for variant in operation.request.enabled_variants(enabled_variants):
384
+ selected = per_variant.get(variant.name, handler)
385
+ assert selected is not None
386
+ _validate_handler_signature(
387
+ selected,
388
+ variant.model,
389
+ label=f"{uri}.{operation.name}.{variant.name}",
390
+ allow_request_supertype=(
391
+ selected is sdk_handler and not overridden
392
+ ),
393
+ )
394
+ elif handler is None:
395
+ owner = (
396
+ "runtime"
397
+ if operation.implementation is ImplementationOwner.RUNTIME
398
+ else "SDK"
399
+ )
400
+ raise ValueError(f"missing {owner} handler for {uri}.{operation.name}")
401
+ else:
402
+ request_model = request_model_overrides.get(
403
+ (uri, operation_key),
404
+ operation.request.model if operation.request is not None else None,
405
+ )
406
+ _validate_handler_signature(
407
+ handler,
408
+ request_model,
409
+ label=f"{uri}.{operation.name}",
410
+ allow_request_supertype=handler is sdk_handler and not overridden,
411
+ )
412
+
413
+ resolved_operations[operation_key] = RegisteredOperation(
414
+ definition=operation,
415
+ handler=handler,
416
+ variant_handlers=per_variant,
417
+ enabled_optional_variants=frozenset(enabled_variants),
418
+ request_model_override=request_model_overrides.get(
419
+ (uri, operation_key)
420
+ ),
421
+ overridden=overridden,
422
+ )
423
+
424
+ active_sdk_operations = {
425
+ name
426
+ for name, operation in resolved_operations.items()
427
+ if operation.definition.implementation is ImplementationOwner.SDK
428
+ }
429
+ overridden_sdk_operations = {
430
+ name
431
+ for name in active_sdk_operations
432
+ if resolved_operations[name].overridden
433
+ }
434
+ if (
435
+ overridden_sdk_operations
436
+ and overridden_sdk_operations != active_sdk_operations
437
+ ):
438
+ sdk_backed = active_sdk_operations - overridden_sdk_operations
439
+ raise ValueError(
440
+ f"SDK operations for extension {uri} must be overridden together; "
441
+ f"overridden={sorted(overridden_sdk_operations)}, "
442
+ f"SDK-backed={sorted(sdk_backed)}"
443
+ )
444
+
445
+ registered.append(
446
+ RegisteredExtension(activation=activation, operations=resolved_operations)
447
+ )
448
+
449
+ return ExtensionRegistry(registered)
@@ -0,0 +1,39 @@
1
+ """Versioned public task contracts for A2A agent run methods."""
2
+
3
+ from .v1 import (
4
+ AgentConfig,
5
+ AgentRunResult,
6
+ DataPart,
7
+ FilePart,
8
+ NativeTrajectory,
9
+ TaskError,
10
+ TaskOutcome,
11
+ TaskPart,
12
+ TaskProgress,
13
+ TaskRequest,
14
+ TaskResult,
15
+ TaskResultBuilder,
16
+ TaskStreamItem,
17
+ TextPart,
18
+ Usage,
19
+ WriteOnly,
20
+ )
21
+
22
+ __all__ = [
23
+ "AgentConfig",
24
+ "AgentRunResult",
25
+ "DataPart",
26
+ "FilePart",
27
+ "NativeTrajectory",
28
+ "TaskError",
29
+ "TaskOutcome",
30
+ "TaskPart",
31
+ "TaskProgress",
32
+ "TaskRequest",
33
+ "TaskResult",
34
+ "TaskResultBuilder",
35
+ "TaskStreamItem",
36
+ "TextPart",
37
+ "Usage",
38
+ "WriteOnly",
39
+ ]