dispatch_agents 0.9.0__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.
Files changed (43) hide show
  1. agentservice/__init__.py +0 -0
  2. agentservice/py.typed +0 -0
  3. agentservice/v1/__init__.py +0 -0
  4. agentservice/v1/message_pb2.py +41 -0
  5. agentservice/v1/message_pb2.pyi +22 -0
  6. agentservice/v1/message_pb2_grpc.py +4 -0
  7. agentservice/v1/request_response_pb2.py +46 -0
  8. agentservice/v1/request_response_pb2.pyi +54 -0
  9. agentservice/v1/request_response_pb2_grpc.py +4 -0
  10. agentservice/v1/service_pb2.py +43 -0
  11. agentservice/v1/service_pb2.pyi +6 -0
  12. agentservice/v1/service_pb2_grpc.py +129 -0
  13. dispatch_agents/__init__.py +281 -0
  14. dispatch_agents/agent_service.py +135 -0
  15. dispatch_agents/config.py +490 -0
  16. dispatch_agents/contrib/__init__.py +1 -0
  17. dispatch_agents/contrib/claude/__init__.py +246 -0
  18. dispatch_agents/contrib/openai/__init__.py +167 -0
  19. dispatch_agents/events.py +986 -0
  20. dispatch_agents/grpc_server.py +565 -0
  21. dispatch_agents/instrument.py +217 -0
  22. dispatch_agents/integrations/__init__.py +1 -0
  23. dispatch_agents/integrations/github/README.md +9 -0
  24. dispatch_agents/integrations/github/__init__.py +4268 -0
  25. dispatch_agents/invocation.py +25 -0
  26. dispatch_agents/llm.py +1017 -0
  27. dispatch_agents/llm_langchain.py +394 -0
  28. dispatch_agents/logging_config.py +133 -0
  29. dispatch_agents/mcp.py +266 -0
  30. dispatch_agents/memory.py +264 -0
  31. dispatch_agents/models.py +748 -0
  32. dispatch_agents/proxy/__init__.py +6 -0
  33. dispatch_agents/proxy/server.py +1137 -0
  34. dispatch_agents/proxy/sse_utils.py +76 -0
  35. dispatch_agents/py.typed +0 -0
  36. dispatch_agents/resources.py +68 -0
  37. dispatch_agents/version.py +19 -0
  38. dispatch_agents-0.9.0.dist-info/METADATA +20 -0
  39. dispatch_agents-0.9.0.dist-info/RECORD +43 -0
  40. dispatch_agents-0.9.0.dist-info/WHEEL +4 -0
  41. dispatch_agents-0.9.0.dist-info/licenses/LICENSE +191 -0
  42. dispatch_agents-0.9.0.dist-info/licenses/LICENSE-3rdparty.csv +12 -0
  43. dispatch_agents-0.9.0.dist-info/licenses/NOTICE +5 -0
@@ -0,0 +1,135 @@
1
+ """gRPC client for the AgentService.
2
+
3
+ This module provides a high-level async client for invoking agents via gRPC.
4
+ """
5
+
6
+ import grpc
7
+
8
+ from agentservice.v1 import (
9
+ request_response_pb2,
10
+ service_pb2_grpc,
11
+ )
12
+
13
+ __all__ = [
14
+ "AgentServiceClient",
15
+ ]
16
+
17
+
18
+ class AgentServiceClient:
19
+ """Async gRPC client for the AgentService.
20
+
21
+ This client wraps the generated gRPC stub to provide a clean, typed interface
22
+ for invoking agents.
23
+
24
+ Example:
25
+ >>> from agentservice.v1 import request_response_pb2
26
+ >>> from google.protobuf import json_format, struct_pb2
27
+ >>> async with AgentServiceClient("localhost:50051") as client:
28
+ ... payload = struct_pb2.Value()
29
+ ... json_format.ParseDict({"query": "Hello"}, payload)
30
+ ... request = request_response_pb2.InvokeRequest(
31
+ ... agent_name="my-agent",
32
+ ... org_id="my-org",
33
+ ... topic="user.query",
34
+ ... payload=payload,
35
+ ... uid="msg-123",
36
+ ... trace_id="trace-456",
37
+ ... sender_id="user-1",
38
+ ... ts="2025-01-01T00:00:00Z",
39
+ ... )
40
+ ... response = await client.invoke(request)
41
+ ... print(response.result)
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ target: str,
47
+ *,
48
+ insecure: bool = False,
49
+ credentials: grpc.ChannelCredentials | None = None,
50
+ ):
51
+ """Initialize the AgentService client.
52
+
53
+ Args:
54
+ target: The gRPC server address (e.g., "localhost:50051")
55
+ insecure: If True, use an insecure channel (for development only)
56
+ credentials: Optional gRPC channel credentials for TLS/mTLS
57
+ """
58
+ self.target = target
59
+ self.insecure = insecure
60
+ self.credentials = credentials
61
+ self._channel: grpc.aio.Channel | None = None
62
+ self._stub: service_pb2_grpc.AgentServiceStub | None = None
63
+
64
+ async def __aenter__(self) -> "AgentServiceClient":
65
+ """Enter async context and establish gRPC channel."""
66
+ if self.insecure:
67
+ self._channel = grpc.aio.insecure_channel(self.target)
68
+ else:
69
+ if self.credentials is None:
70
+ # Default to SSL with system root certificates
71
+ self.credentials = grpc.ssl_channel_credentials()
72
+ self._channel = grpc.aio.secure_channel(self.target, self.credentials)
73
+
74
+ self._stub = service_pb2_grpc.AgentServiceStub(self._channel)
75
+ return self
76
+
77
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
78
+ """Exit async context and close gRPC channel."""
79
+ if self._channel:
80
+ await self._channel.close()
81
+ return False
82
+
83
+ async def invoke(
84
+ self,
85
+ request: request_response_pb2.InvokeRequest,
86
+ *,
87
+ timeout: float | None = None,
88
+ ) -> request_response_pb2.InvokeResponse:
89
+ """Invoke an agent and await the result.
90
+
91
+ Args:
92
+ request: The InvokeRequest protobuf message
93
+ timeout: Optional timeout in seconds for the RPC call
94
+
95
+ Returns:
96
+ InvokeResponse protobuf message containing the agent's result
97
+
98
+ Raises:
99
+ grpc.RpcError: If the gRPC call fails
100
+ """
101
+ if self._stub is None:
102
+ raise RuntimeError(
103
+ "Client not initialized. Use 'async with AgentServiceClient(...)' context manager."
104
+ )
105
+
106
+ # Make the gRPC call
107
+ response = await self._stub.Invoke(request, timeout=timeout)
108
+ return response
109
+
110
+ async def health_check(
111
+ self,
112
+ request: request_response_pb2.HealthCheckRequest,
113
+ *,
114
+ timeout: float | None = None,
115
+ ) -> request_response_pb2.HealthCheckResponse:
116
+ """Check agent health status.
117
+
118
+ Args:
119
+ request: The HealthCheckRequest protobuf message
120
+ timeout: Optional timeout in seconds for the RPC call
121
+
122
+ Returns:
123
+ HealthCheckResponse protobuf message containing the agent's health status
124
+
125
+ Raises:
126
+ grpc.RpcError: If the gRPC call fails
127
+ """
128
+ if self._stub is None:
129
+ raise RuntimeError(
130
+ "Client not initialized. Use 'async with AgentServiceClient(...)' context manager."
131
+ )
132
+
133
+ # Make the gRPC call
134
+ response = await self._stub.HealthCheck(request, timeout=timeout)
135
+ return response
@@ -0,0 +1,490 @@
1
+ """Configuration models for dispatch.yaml files.
2
+
3
+ This module defines the schema for agent deployment configuration,
4
+ shared between CLI and backend.
5
+ """
6
+
7
+ import os
8
+ from enum import StrEnum
9
+ from typing import Any
10
+
11
+ from pydantic import BaseModel, Field, field_validator, model_validator
12
+
13
+ from dispatch_agents.resources import _parse_cpu, _parse_memory
14
+
15
+ # Env var names managed by the platform — users must not override these.
16
+ RESERVED_ENV_VARS: frozenset[str] = frozenset(
17
+ {
18
+ "BACKEND_URL",
19
+ "DISPATCH_API_KEY",
20
+ "DISPATCH_NAMESPACE",
21
+ "DISPATCH_AGENT_NAME",
22
+ "DISPATCH_AGENT_VERSION",
23
+ "MCP_CONFIG_JSON",
24
+ "MCP_GATEWAY_URL",
25
+ }
26
+ )
27
+
28
+
29
+ class VolumeMode(StrEnum):
30
+ """Volume access mode for persistent storage.
31
+
32
+ Currently only read_write_many is supported, but this enum allows
33
+ for future expansion to other modes like read_only, read_write_once, etc.
34
+ """
35
+
36
+ READ_WRITE_MANY = "read_write_many"
37
+
38
+
39
+ class VolumeConfig(BaseModel):
40
+ """Configuration for a persistent storage volume.
41
+
42
+ Volumes provide persistent storage that survives container restarts
43
+ and redeployments. Data is isolated per-agent.
44
+
45
+ Example:
46
+ volumes:
47
+ - name: plans
48
+ mountPath: /data/plans
49
+ mode: read_write_many
50
+ """
51
+
52
+ name: str = Field(
53
+ ...,
54
+ description="Unique name for the volume (used for identification and cleanup)",
55
+ min_length=1,
56
+ max_length=63,
57
+ pattern=r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$",
58
+ )
59
+ mount_path: str = Field(
60
+ ...,
61
+ alias="mountPath",
62
+ description="Path where the volume will be mounted inside the container (must be within /data)",
63
+ )
64
+ mode: VolumeMode = Field(
65
+ ...,
66
+ description="Access mode for the volume (required)",
67
+ )
68
+
69
+ @field_validator("mount_path")
70
+ @classmethod
71
+ def _validate_mount_path(cls, v: str) -> str:
72
+ """Ensure mount path is within /data directory."""
73
+ if not v.startswith("/data"):
74
+ raise ValueError(
75
+ f"mountPath must be within /data directory, got: {v}. "
76
+ "Example: /data/plans or /data"
77
+ )
78
+ # Normalize path (remove trailing slashes, etc.)
79
+ return os.path.normpath(v)
80
+
81
+ model_config = {"populate_by_name": True}
82
+
83
+
84
+ class SecretConfig(BaseModel):
85
+ """Configuration for a secret to be injected as an environment variable.
86
+
87
+ Secrets are retrieved from the secrets manager and injected into the
88
+ container as environment variables at runtime.
89
+
90
+ Example:
91
+ secrets:
92
+ - name: OPENAI_API_KEY
93
+ secret_id: /shared/openai-api-key
94
+ """
95
+
96
+ name: str = Field(
97
+ ...,
98
+ description="Environment variable name for the secret",
99
+ min_length=1,
100
+ )
101
+ secret_id: str = Field(
102
+ ...,
103
+ description="Path to the secret in secrets manager",
104
+ min_length=1,
105
+ )
106
+
107
+
108
+ class MCPServerConfig(BaseModel):
109
+ """Configuration for an MCP server to connect to.
110
+
111
+ Example:
112
+ mcp_servers:
113
+ - server: datadog
114
+ """
115
+
116
+ server: str = Field(
117
+ ...,
118
+ description="MCP server installation name from the registry",
119
+ min_length=1,
120
+ )
121
+
122
+
123
+ def _get_valid_memory_for_cpu(cpu_units: int) -> list[int] | None:
124
+ """Get valid memory values (in MB) for a given CPU value.
125
+
126
+ Returns None if the CPU value is not valid.
127
+
128
+ Valid CPU/Memory combinations for AWS ECS Fargate:
129
+ https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html
130
+ """
131
+ # CPU units: 1024 = 1 vCPU, Memory: MB
132
+ valid_combinations: dict[int, list[int]] = {
133
+ 256: [512, 1024, 2048],
134
+ 512: [1024, 2048, 3072, 4096],
135
+ 1024: [2048, 3072, 4096, 5120, 6144, 7168, 8192],
136
+ 2048: [
137
+ 4096,
138
+ 5120,
139
+ 6144,
140
+ 7168,
141
+ 8192,
142
+ 9216,
143
+ 10240,
144
+ 11264,
145
+ 12288,
146
+ 13312,
147
+ 14336,
148
+ 15360,
149
+ 16384,
150
+ ],
151
+ 4096: [
152
+ 8192,
153
+ 9216,
154
+ 10240,
155
+ 11264,
156
+ 12288,
157
+ 13312,
158
+ 14336,
159
+ 15360,
160
+ 16384,
161
+ 17408,
162
+ 18432,
163
+ 19456,
164
+ 20480,
165
+ 21504,
166
+ 22528,
167
+ 23552,
168
+ 24576,
169
+ 25600,
170
+ 26624,
171
+ 27648,
172
+ 28672,
173
+ 29696,
174
+ 30720,
175
+ ],
176
+ 8192: list(range(16384, 61441, 4096)), # 16GB to 60GB in 4GB increments
177
+ 16384: list(range(32768, 122881, 8192)), # 32GB to 120GB in 8GB increments
178
+ }
179
+ return valid_combinations.get(cpu_units)
180
+
181
+
182
+ def _get_valid_cpu_values() -> list[int]:
183
+ """Get list of valid CPU values (in internal units where 1024 = 1 vCPU)."""
184
+ return [256, 512, 1024, 2048, 4096, 8192, 16384]
185
+
186
+
187
+ def _format_cpu(cpu_units: int) -> str:
188
+ """Format internal CPU units to Kubernetes-style string.
189
+
190
+ Examples:
191
+ 256 -> "250m"
192
+ 512 -> "500m"
193
+ 1024 -> "1"
194
+ 2048 -> "2"
195
+ """
196
+ if cpu_units >= 1024 and cpu_units % 1024 == 0:
197
+ # Whole cores
198
+ return str(cpu_units // 1024)
199
+ else:
200
+ # Millicores
201
+ millicores = int(cpu_units * 1000 / 1024)
202
+ return f"{millicores}m"
203
+
204
+
205
+ def _format_memory(memory_mb: int) -> str:
206
+ """Format memory in MB to Kubernetes-style string.
207
+
208
+ Examples:
209
+ 512 -> "512Mi"
210
+ 1024 -> "1Gi"
211
+ 2048 -> "2Gi"
212
+ """
213
+ if memory_mb >= 1024 and memory_mb % 1024 == 0:
214
+ # Use Gi for whole gibibytes
215
+ return f"{memory_mb // 1024}Gi"
216
+ else:
217
+ return f"{memory_mb}Mi"
218
+
219
+
220
+ class ResourceLimits(BaseModel):
221
+ """CPU and memory limits for a container.
222
+
223
+ CPU is specified in Kubernetes format:
224
+ - Millicores: "250m", "500m", "1000m"
225
+ - Cores: "0.25", "0.5", "1", "2"
226
+
227
+ Memory is specified in Kubernetes format:
228
+ - Mebibytes: "512Mi", "1024Mi"
229
+ - Gibibytes: "1Gi", "2Gi"
230
+
231
+ Example:
232
+ limits:
233
+ cpu: "500m"
234
+ memory: "1Gi"
235
+ """
236
+
237
+ cpu: str = Field(
238
+ default="250m",
239
+ description="CPU (e.g., '250m', '500m', '1', '2')",
240
+ )
241
+ memory: str = Field(
242
+ default="2Gi",
243
+ description="Memory (e.g., '512Mi', '1Gi', '2Gi')",
244
+ )
245
+
246
+ @field_validator("cpu")
247
+ @classmethod
248
+ def _validate_cpu(cls, v: str) -> str:
249
+ """Parse and validate CPU value."""
250
+ try:
251
+ cpu_units = _parse_cpu(v)
252
+ except (ValueError, TypeError) as e:
253
+ raise ValueError(f"Invalid CPU format '{v}': {e}") from e
254
+
255
+ valid_cpu_values = _get_valid_cpu_values()
256
+ if cpu_units not in valid_cpu_values:
257
+ valid_cpus = [_format_cpu(c) for c in sorted(valid_cpu_values)]
258
+ raise ValueError(
259
+ f"Invalid CPU value '{v}' ({cpu_units} units). "
260
+ f"Must be one of: {valid_cpus}"
261
+ )
262
+ return v
263
+
264
+ @field_validator("memory")
265
+ @classmethod
266
+ def _validate_memory(cls, v: str) -> str:
267
+ """Parse and validate memory value."""
268
+ try:
269
+ memory_mb = _parse_memory(v)
270
+ except (ValueError, TypeError) as e:
271
+ raise ValueError(f"Invalid memory format '{v}': {e}") from e
272
+
273
+ # Minimum memory limit: 256 MB (required for sidecar overhead)
274
+ min_memory_mb = 256
275
+ if memory_mb < min_memory_mb:
276
+ raise ValueError(
277
+ f"Memory value '{v}' ({memory_mb} MB) is below minimum allowed "
278
+ f"of {_format_memory(min_memory_mb)}."
279
+ )
280
+
281
+ # Maximum memory limit: 16GB (16384 MB)
282
+ max_memory_mb = 16384
283
+ if memory_mb > max_memory_mb:
284
+ raise ValueError(
285
+ f"Memory value '{v}' ({memory_mb} MB) exceeds maximum allowed "
286
+ f"of {_format_memory(max_memory_mb)}. "
287
+ "Contact support if you need more resources."
288
+ )
289
+ return v
290
+
291
+ @model_validator(mode="after")
292
+ def _validate_combination(self) -> "ResourceLimits":
293
+ """Validate that CPU and memory form a valid combination."""
294
+ cpu_units = _parse_cpu(self.cpu)
295
+ memory_mb = _parse_memory(self.memory)
296
+
297
+ valid_memory_values = _get_valid_memory_for_cpu(cpu_units) or []
298
+ if memory_mb not in valid_memory_values:
299
+ valid_memory_strs = [_format_memory(m) for m in valid_memory_values]
300
+ raise ValueError(
301
+ f"Invalid resource combination: CPU {self.cpu} with memory {self.memory}. "
302
+ f"For CPU {self.cpu}, valid memory values are: {valid_memory_strs}"
303
+ )
304
+ return self
305
+
306
+
307
+ class ResourceConfig(BaseModel):
308
+ """Configuration for agent container resources.
309
+
310
+ Resources are expressed as limits.
311
+
312
+ Example:
313
+ resources:
314
+ limits:
315
+ cpu: "500m"
316
+ memory: "1Gi"
317
+ """
318
+
319
+ limits: ResourceLimits = Field(
320
+ default_factory=ResourceLimits,
321
+ description="Resource limits (CPU and memory)",
322
+ )
323
+
324
+
325
+ class DispatchConfig(BaseModel):
326
+ """Configuration model for dispatch.yaml files.
327
+
328
+ This model defines the complete schema for agent deployment configuration.
329
+ It supports validation, serialization, and provides clear documentation
330
+ for all configuration options.
331
+
332
+ Example dispatch.yaml:
333
+ namespace: skunkworks
334
+ agent_name: my-agent
335
+ entrypoint: agent.py
336
+ base_image: python:3.13-slim
337
+ env:
338
+ LOG_LEVEL: debug
339
+ MY_APP_MODE: production
340
+ volumes:
341
+ - name: data
342
+ mountPath: /data
343
+ mode: read_write_many
344
+ secrets:
345
+ - name: OPENAI_API_KEY
346
+ secret_id: /shared/openai-api-key
347
+ resources:
348
+ limits:
349
+ cpu: "500m"
350
+ memory: "1Gi"
351
+ """
352
+
353
+ namespace: str | None = Field(
354
+ default=None,
355
+ description="Namespace for agent deployment (required for deployment)",
356
+ )
357
+ agent_name: str | None = Field(
358
+ default=None,
359
+ description="Unique name for the agent",
360
+ )
361
+ entrypoint: str | None = Field(
362
+ default=None,
363
+ description="Python file containing agent handlers (default: agent.py)",
364
+ )
365
+ base_image: str | None = Field(
366
+ default=None,
367
+ description="Base Docker image for the agent container",
368
+ )
369
+ system_packages: list[str] | None = Field(
370
+ default=None,
371
+ description="Additional system packages to install (apt packages)",
372
+ )
373
+ local_dependencies: dict[str, str] | None = Field(
374
+ default=None,
375
+ description="Local path dependencies to bundle (name -> path mapping)",
376
+ )
377
+ env: dict[str, str] | None = Field(
378
+ default=None,
379
+ description="Plain environment variables to inject into the container (non-secret)",
380
+ )
381
+
382
+ @field_validator("env", mode="before")
383
+ @classmethod
384
+ def _check_env_values_are_strings(cls, v: dict | None) -> dict[str, str] | None:
385
+ """Ensure all env values are strings.
386
+
387
+ YAML parses unquoted ``false`` as bool, ``123`` as int, etc.
388
+ Since env vars are always strings, require explicit quoting.
389
+ """
390
+ if not v or not isinstance(v, dict):
391
+ return v
392
+ non_string = {
393
+ k: type(val).__name__ for k, val in v.items() if not isinstance(val, str)
394
+ }
395
+ if non_string:
396
+ examples = ", ".join(
397
+ f'{k} (got {t}, wrap in quotes: "{v[k]}")'
398
+ for k, t in sorted(non_string.items())
399
+ )
400
+ raise ValueError(f"All env values must be strings. {examples}")
401
+ return v
402
+
403
+ secrets: list[SecretConfig] | None = Field(
404
+ default=None,
405
+ description="Secrets to inject as environment variables",
406
+ )
407
+ volumes: list[VolumeConfig] | None = Field(
408
+ default=None,
409
+ description="Persistent storage volumes to mount",
410
+ )
411
+ mcp_servers: list[MCPServerConfig] | None = Field(
412
+ default=None,
413
+ description="MCP servers to connect to from the registry",
414
+ )
415
+ resources: ResourceConfig = Field(
416
+ default_factory=ResourceConfig,
417
+ description="Container resource limits (CPU and memory)",
418
+ )
419
+
420
+ @field_validator("env")
421
+ @classmethod
422
+ def _validate_env(cls, v: dict[str, str] | None) -> dict[str, str] | None:
423
+ """Reject env vars that collide with platform-managed names."""
424
+ if not v:
425
+ return v
426
+ collisions = RESERVED_ENV_VARS & v.keys()
427
+ if collisions:
428
+ raise ValueError(
429
+ f"Cannot set reserved environment variable(s): "
430
+ f"{', '.join(sorted(collisions))}. "
431
+ "These are managed by the Dispatch platform."
432
+ )
433
+ return v
434
+
435
+ @model_validator(mode="after")
436
+ def _validate_env_secrets_no_overlap(self) -> "DispatchConfig":
437
+ """Ensure no env var name also appears as a secret name."""
438
+ if not self.env or not self.secrets:
439
+ return self
440
+ env_names = set(self.env.keys())
441
+ secret_names = {s.name for s in self.secrets}
442
+ overlap = env_names & secret_names
443
+ if overlap:
444
+ raise ValueError(
445
+ f"Environment variable(s) {', '.join(sorted(overlap))} "
446
+ "defined in both 'env' and 'secrets'. "
447
+ "Use 'secrets' for sensitive values or 'env' for non-secret values, not both."
448
+ )
449
+ return self
450
+
451
+ def to_yaml_dict(self) -> dict[str, Any]:
452
+ """Convert to dictionary suitable for YAML serialization.
453
+
454
+ Excludes None values and converts nested models to dicts.
455
+ """
456
+ result: dict[str, Any] = {}
457
+
458
+ if self.namespace is not None:
459
+ result["namespace"] = self.namespace
460
+ if self.agent_name is not None:
461
+ result["agent_name"] = self.agent_name
462
+ if self.entrypoint is not None:
463
+ result["entrypoint"] = self.entrypoint
464
+ if self.base_image is not None:
465
+ result["base_image"] = self.base_image
466
+ if self.system_packages:
467
+ result["system_packages"] = self.system_packages
468
+ if self.local_dependencies:
469
+ result["local_dependencies"] = self.local_dependencies
470
+ if self.env:
471
+ result["env"] = dict(self.env)
472
+ if self.secrets:
473
+ result["secrets"] = [
474
+ {"name": s.name, "secret_id": s.secret_id} for s in self.secrets
475
+ ]
476
+ if self.volumes:
477
+ result["volumes"] = [
478
+ {"name": v.name, "mountPath": v.mount_path, "mode": v.mode.value}
479
+ for v in self.volumes
480
+ ]
481
+ # Always include resources since it has defaults
482
+ limits_dict: dict[str, str] = {
483
+ "cpu": self.resources.limits.cpu,
484
+ "memory": self.resources.limits.memory,
485
+ }
486
+ result["resources"] = {"limits": limits_dict}
487
+
488
+ return result
489
+
490
+ model_config = {"populate_by_name": True}
@@ -0,0 +1 @@
1
+ """Contrib packages for integrating with third-party Agent SDKs."""