agentrun-sdk 0.1.2__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.

Potentially problematic release.


This version of agentrun-sdk might be problematic. Click here for more details.

Files changed (115) hide show
  1. agentrun_operation_sdk/cli/__init__.py +1 -0
  2. agentrun_operation_sdk/cli/cli.py +19 -0
  3. agentrun_operation_sdk/cli/common.py +21 -0
  4. agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
  5. agentrun_operation_sdk/cli/runtime/commands.py +203 -0
  6. agentrun_operation_sdk/client/client.py +75 -0
  7. agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
  8. agentrun_operation_sdk/operations/runtime/configure.py +101 -0
  9. agentrun_operation_sdk/operations/runtime/launch.py +82 -0
  10. agentrun_operation_sdk/operations/runtime/models.py +31 -0
  11. agentrun_operation_sdk/services/runtime.py +152 -0
  12. agentrun_operation_sdk/utils/logging_config.py +72 -0
  13. agentrun_operation_sdk/utils/runtime/config.py +94 -0
  14. agentrun_operation_sdk/utils/runtime/container.py +280 -0
  15. agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
  16. agentrun_operation_sdk/utils/runtime/schema.py +56 -0
  17. agentrun_sdk/__init__.py +7 -0
  18. agentrun_sdk/agent/__init__.py +25 -0
  19. agentrun_sdk/agent/agent.py +696 -0
  20. agentrun_sdk/agent/agent_result.py +46 -0
  21. agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
  22. agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
  23. agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
  24. agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
  25. agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
  26. agentrun_sdk/agent/state.py +97 -0
  27. agentrun_sdk/event_loop/__init__.py +9 -0
  28. agentrun_sdk/event_loop/event_loop.py +499 -0
  29. agentrun_sdk/event_loop/streaming.py +319 -0
  30. agentrun_sdk/experimental/__init__.py +4 -0
  31. agentrun_sdk/experimental/hooks/__init__.py +15 -0
  32. agentrun_sdk/experimental/hooks/events.py +123 -0
  33. agentrun_sdk/handlers/__init__.py +10 -0
  34. agentrun_sdk/handlers/callback_handler.py +70 -0
  35. agentrun_sdk/hooks/__init__.py +49 -0
  36. agentrun_sdk/hooks/events.py +80 -0
  37. agentrun_sdk/hooks/registry.py +247 -0
  38. agentrun_sdk/models/__init__.py +10 -0
  39. agentrun_sdk/models/anthropic.py +432 -0
  40. agentrun_sdk/models/bedrock.py +649 -0
  41. agentrun_sdk/models/litellm.py +225 -0
  42. agentrun_sdk/models/llamaapi.py +438 -0
  43. agentrun_sdk/models/mistral.py +539 -0
  44. agentrun_sdk/models/model.py +95 -0
  45. agentrun_sdk/models/ollama.py +357 -0
  46. agentrun_sdk/models/openai.py +436 -0
  47. agentrun_sdk/models/sagemaker.py +598 -0
  48. agentrun_sdk/models/writer.py +449 -0
  49. agentrun_sdk/multiagent/__init__.py +22 -0
  50. agentrun_sdk/multiagent/a2a/__init__.py +15 -0
  51. agentrun_sdk/multiagent/a2a/executor.py +148 -0
  52. agentrun_sdk/multiagent/a2a/server.py +252 -0
  53. agentrun_sdk/multiagent/base.py +92 -0
  54. agentrun_sdk/multiagent/graph.py +555 -0
  55. agentrun_sdk/multiagent/swarm.py +656 -0
  56. agentrun_sdk/py.typed +1 -0
  57. agentrun_sdk/session/__init__.py +18 -0
  58. agentrun_sdk/session/file_session_manager.py +216 -0
  59. agentrun_sdk/session/repository_session_manager.py +152 -0
  60. agentrun_sdk/session/s3_session_manager.py +272 -0
  61. agentrun_sdk/session/session_manager.py +73 -0
  62. agentrun_sdk/session/session_repository.py +51 -0
  63. agentrun_sdk/telemetry/__init__.py +21 -0
  64. agentrun_sdk/telemetry/config.py +194 -0
  65. agentrun_sdk/telemetry/metrics.py +476 -0
  66. agentrun_sdk/telemetry/metrics_constants.py +15 -0
  67. agentrun_sdk/telemetry/tracer.py +563 -0
  68. agentrun_sdk/tools/__init__.py +17 -0
  69. agentrun_sdk/tools/decorator.py +569 -0
  70. agentrun_sdk/tools/executor.py +137 -0
  71. agentrun_sdk/tools/loader.py +152 -0
  72. agentrun_sdk/tools/mcp/__init__.py +13 -0
  73. agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
  74. agentrun_sdk/tools/mcp/mcp_client.py +423 -0
  75. agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
  76. agentrun_sdk/tools/mcp/mcp_types.py +63 -0
  77. agentrun_sdk/tools/registry.py +607 -0
  78. agentrun_sdk/tools/structured_output.py +421 -0
  79. agentrun_sdk/tools/tools.py +217 -0
  80. agentrun_sdk/tools/watcher.py +136 -0
  81. agentrun_sdk/types/__init__.py +5 -0
  82. agentrun_sdk/types/collections.py +23 -0
  83. agentrun_sdk/types/content.py +188 -0
  84. agentrun_sdk/types/event_loop.py +48 -0
  85. agentrun_sdk/types/exceptions.py +81 -0
  86. agentrun_sdk/types/guardrails.py +254 -0
  87. agentrun_sdk/types/media.py +89 -0
  88. agentrun_sdk/types/session.py +152 -0
  89. agentrun_sdk/types/streaming.py +201 -0
  90. agentrun_sdk/types/tools.py +258 -0
  91. agentrun_sdk/types/traces.py +5 -0
  92. agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
  93. agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
  94. agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
  95. agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
  96. agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
  97. agentrun_wrapper/__init__.py +11 -0
  98. agentrun_wrapper/_utils/__init__.py +6 -0
  99. agentrun_wrapper/_utils/endpoints.py +16 -0
  100. agentrun_wrapper/identity/__init__.py +5 -0
  101. agentrun_wrapper/identity/auth.py +211 -0
  102. agentrun_wrapper/memory/__init__.py +6 -0
  103. agentrun_wrapper/memory/client.py +1697 -0
  104. agentrun_wrapper/memory/constants.py +103 -0
  105. agentrun_wrapper/memory/controlplane.py +626 -0
  106. agentrun_wrapper/py.typed +1 -0
  107. agentrun_wrapper/runtime/__init__.py +13 -0
  108. agentrun_wrapper/runtime/app.py +473 -0
  109. agentrun_wrapper/runtime/context.py +34 -0
  110. agentrun_wrapper/runtime/models.py +25 -0
  111. agentrun_wrapper/services/__init__.py +1 -0
  112. agentrun_wrapper/services/identity.py +192 -0
  113. agentrun_wrapper/tools/__init__.py +6 -0
  114. agentrun_wrapper/tools/browser_client.py +325 -0
  115. agentrun_wrapper/tools/code_interpreter_client.py +186 -0
@@ -0,0 +1,473 @@
1
+ """Bedrock AgentCore base implementation.
2
+
3
+ Provides a Starlette-based web server that wraps user functions as HTTP endpoints.
4
+ """
5
+
6
+ import asyncio
7
+ import contextvars
8
+ import inspect
9
+ import json
10
+ import logging
11
+ import threading
12
+ import time
13
+ import uuid
14
+ from concurrent.futures import ThreadPoolExecutor
15
+ from typing import Any, Callable, Dict, Optional
16
+
17
+ from starlette.applications import Starlette
18
+ from starlette.responses import JSONResponse, Response, StreamingResponse
19
+ from starlette.routing import Route
20
+
21
+ from .context import BedrockAgentCoreContext, RequestContext
22
+ from .models import (
23
+ ACCESS_TOKEN_HEADER,
24
+ SESSION_HEADER,
25
+ TASK_ACTION_CLEAR_FORCED_STATUS,
26
+ TASK_ACTION_FORCE_BUSY,
27
+ TASK_ACTION_FORCE_HEALTHY,
28
+ TASK_ACTION_JOB_STATUS,
29
+ TASK_ACTION_PING_STATUS,
30
+ PingStatus,
31
+ )
32
+
33
+ # Request context for logging
34
+ request_id_context: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("request_id", default=None)
35
+
36
+
37
+ class RequestContextFormatter(logging.Formatter):
38
+ """Custom formatter that includes request ID in log messages."""
39
+
40
+ def format(self, record):
41
+ """Format log record with request ID context."""
42
+ request_id = request_id_context.get()
43
+ if request_id:
44
+ record.request_id = f"[{request_id}] "
45
+ else:
46
+ record.request_id = ""
47
+ return super().format(record)
48
+
49
+
50
+ class BedrockAgentCoreApp(Starlette):
51
+ """Bedrock AgentCore application class that extends Starlette for AI agent deployment."""
52
+
53
+ def __init__(self, debug: bool = False):
54
+ """Initialize Bedrock AgentCore application.
55
+
56
+ Args:
57
+ debug: Enable debug actions for task management (default: False)
58
+ """
59
+ self.handlers: Dict[str, Callable] = {}
60
+ self._ping_handler: Optional[Callable] = None
61
+ self._active_tasks: Dict[int, Dict[str, Any]] = {}
62
+ self._task_counter_lock: threading.Lock = threading.Lock()
63
+ self._forced_ping_status: Optional[PingStatus] = None
64
+ self._last_status_update_time: float = time.time()
65
+ self._invocation_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="invocation")
66
+ self._invocation_semaphore = asyncio.Semaphore(2)
67
+
68
+ routes = [
69
+ Route("/invocations", self._handle_invocation, methods=["POST"]),
70
+ Route("/ping", self._handle_ping, methods=["GET"]),
71
+ ]
72
+ super().__init__(routes=routes)
73
+ self.debug = debug # Set after super().__init__ to avoid override
74
+
75
+ self.logger = logging.getLogger("bedrock_agentcore.app")
76
+ if not self.logger.handlers:
77
+ handler = logging.StreamHandler()
78
+ formatter = RequestContextFormatter("%(asctime)s - %(name)s - %(levelname)s - %(request_id)s%(message)s")
79
+ handler.setFormatter(formatter)
80
+ self.logger.addHandler(handler)
81
+ self.logger.setLevel(logging.INFO)
82
+
83
+ def entrypoint(self, func: Callable) -> Callable:
84
+ """Decorator to register a function as the main entrypoint.
85
+
86
+ Args:
87
+ func: The function to register as entrypoint
88
+
89
+ Returns:
90
+ The decorated function with added serve method
91
+ """
92
+ self.handlers["main"] = func
93
+ func.run = lambda port=8080, host=None: self.run(port, host)
94
+ return func
95
+
96
+ def ping(self, func: Callable) -> Callable:
97
+ """Decorator to register a custom ping status handler.
98
+
99
+ Args:
100
+ func: The function to register as ping status handler
101
+
102
+ Returns:
103
+ The decorated function
104
+ """
105
+ self._ping_handler = func
106
+ return func
107
+
108
+ def async_task(self, func: Callable) -> Callable:
109
+ """Decorator to track async tasks for ping status.
110
+
111
+ When a function is decorated with @async_task, it will:
112
+ - Set ping status to HEALTHY_BUSY while running
113
+ - Revert to HEALTHY when complete
114
+ """
115
+ if not asyncio.iscoroutinefunction(func):
116
+ raise ValueError("@async_task can only be applied to async functions")
117
+
118
+ async def wrapper(*args, **kwargs):
119
+ task_id = self.add_async_task(func.__name__)
120
+
121
+ try:
122
+ self.logger.debug("Starting async task: %s", func.__name__)
123
+ start_time = time.time()
124
+ result = await func(*args, **kwargs)
125
+ duration = time.time() - start_time
126
+ self.logger.info("Async task completed: %s (%.3fs)", func.__name__, duration)
127
+ return result
128
+ except Exception as e:
129
+ duration = time.time() - start_time
130
+ self.logger.error(
131
+ "Async task failed: %s (%.3fs) - %s: %s", func.__name__, duration, type(e).__name__, e
132
+ )
133
+ raise
134
+ finally:
135
+ self.complete_async_task(task_id)
136
+
137
+ wrapper.__name__ = func.__name__
138
+ return wrapper
139
+
140
+ def get_current_ping_status(self) -> PingStatus:
141
+ """Get current ping status (forced > custom > automatic)."""
142
+ current_status = None
143
+
144
+ if self._forced_ping_status is not None:
145
+ current_status = self._forced_ping_status
146
+ elif self._ping_handler:
147
+ try:
148
+ result = self._ping_handler()
149
+ if isinstance(result, str):
150
+ current_status = PingStatus(result)
151
+ else:
152
+ current_status = result
153
+ except Exception as e:
154
+ self.logger.warning(
155
+ "Custom ping handler failed, falling back to automatic: %s: %s", type(e).__name__, e
156
+ )
157
+
158
+ if current_status is None:
159
+ current_status = PingStatus.HEALTHY_BUSY if self._active_tasks else PingStatus.HEALTHY
160
+ if not hasattr(self, "_last_known_status") or self._last_known_status != current_status:
161
+ self._last_known_status = current_status
162
+ self._last_status_update_time = time.time()
163
+
164
+ return current_status
165
+
166
+ def force_ping_status(self, status: PingStatus):
167
+ """Force ping status to a specific value."""
168
+ self._forced_ping_status = status
169
+
170
+ def clear_forced_ping_status(self):
171
+ """Clear forced status and resume automatic."""
172
+ self._forced_ping_status = None
173
+
174
+ def get_async_task_info(self) -> Dict[str, Any]:
175
+ """Get info about running async tasks."""
176
+ running_jobs = []
177
+ for t in self._active_tasks.values():
178
+ try:
179
+ running_jobs.append(
180
+ {"name": t.get("name", "unknown"), "duration": time.time() - t.get("start_time", time.time())}
181
+ )
182
+ except Exception as e:
183
+ self.logger.warning("Caught exception, continuing...: %s", e)
184
+ continue
185
+
186
+ return {"active_count": len(self._active_tasks), "running_jobs": running_jobs}
187
+
188
+ def add_async_task(self, name: str, metadata: Optional[Dict] = None) -> int:
189
+ """Register an async task for interactive health tracking.
190
+
191
+ This method provides granular control over async task lifecycle,
192
+ allowing developers to interactively start tracking tasks for health monitoring.
193
+ Use this when you need precise control over when tasks begin and end.
194
+
195
+ Args:
196
+ name: Human-readable task name for monitoring
197
+ metadata: Optional additional task metadata
198
+
199
+ Returns:
200
+ Task ID for tracking and completion
201
+
202
+ Example:
203
+ task_id = app.add_async_task("file_processing", {"file": "data.csv"})
204
+ # ... do background work ...
205
+ app.complete_async_task(task_id)
206
+ """
207
+ with self._task_counter_lock:
208
+ task_id = hash(str(uuid.uuid4())) # Generate truly unique hash-based ID
209
+
210
+ # Register task start with same structure as @async_task decorator
211
+ task_info = {"name": name, "start_time": time.time()}
212
+ if metadata:
213
+ task_info["metadata"] = metadata
214
+
215
+ self._active_tasks[task_id] = task_info
216
+
217
+ self.logger.info("Async task started: %s (ID: %s)", name, task_id)
218
+ return task_id
219
+
220
+ def complete_async_task(self, task_id: int) -> bool:
221
+ """Mark an async task as complete for interactive health tracking.
222
+
223
+ This method provides granular control over async task lifecycle,
224
+ allowing developers to interactively complete tasks for health monitoring.
225
+ Call this when your background work finishes.
226
+
227
+ Args:
228
+ task_id: Task ID returned from add_async_task
229
+
230
+ Returns:
231
+ True if task was found and completed, False otherwise
232
+
233
+ Example:
234
+ task_id = app.add_async_task("file_processing")
235
+ # ... do background work ...
236
+ completed = app.complete_async_task(task_id)
237
+ """
238
+ with self._task_counter_lock:
239
+ task_info = self._active_tasks.pop(task_id, None)
240
+ if task_info:
241
+ task_name = task_info.get("name", "unknown")
242
+ duration = time.time() - task_info.get("start_time", time.time())
243
+
244
+ self.logger.info("Async task completed: %s (ID: %s, Duration: %.2fs)", task_name, task_id, duration)
245
+ return True
246
+ else:
247
+ self.logger.warning("Attempted to complete unknown task ID: %s", task_id)
248
+ return False
249
+
250
+ def _build_request_context(self, request) -> RequestContext:
251
+ """Build request context and setup auth if present."""
252
+ try:
253
+ agent_identity_token = request.headers.get(ACCESS_TOKEN_HEADER) or request.headers.get(
254
+ ACCESS_TOKEN_HEADER.lower()
255
+ )
256
+ if agent_identity_token:
257
+ BedrockAgentCoreContext.set_workload_access_token(agent_identity_token)
258
+ session_id = request.headers.get(SESSION_HEADER) or request.headers.get(SESSION_HEADER.lower())
259
+ return RequestContext(session_id=session_id)
260
+ except Exception as e:
261
+ self.logger.warning("Failed to build request context: %s: %s", type(e).__name__, e)
262
+ return RequestContext(session_id=None)
263
+
264
+ def _takes_context(self, handler: Callable) -> bool:
265
+ try:
266
+ params = list(inspect.signature(handler).parameters.keys())
267
+ return len(params) >= 2 and params[1] == "context"
268
+ except Exception:
269
+ return False
270
+
271
+ async def _handle_invocation(self, request):
272
+ request_id = str(uuid.uuid4())[:8]
273
+ request_id_context.set(request_id)
274
+ start_time = time.time()
275
+
276
+ try:
277
+ payload = await request.json()
278
+ self.logger.debug("Processing invocation request")
279
+
280
+ if self.debug:
281
+ task_response = self._handle_task_action(payload)
282
+ if task_response:
283
+ duration = time.time() - start_time
284
+ self.logger.info("Debug action completed (%.3fs)", duration)
285
+ return task_response
286
+
287
+ handler = self.handlers.get("main")
288
+ if not handler:
289
+ self.logger.error("No entrypoint defined")
290
+ return JSONResponse({"error": "No entrypoint defined"}, status_code=500)
291
+
292
+ request_context = self._build_request_context(request)
293
+ takes_context = self._takes_context(handler)
294
+
295
+ handler_name = handler.__name__ if hasattr(handler, "__name__") else "unknown"
296
+ self.logger.debug("Invoking handler: %s", handler_name)
297
+ result = await self._invoke_handler(handler, request_context, takes_context, payload)
298
+
299
+ duration = time.time() - start_time
300
+ if inspect.isgenerator(result):
301
+ self.logger.info("Returning streaming response (generator) (%.3fs)", duration)
302
+ return StreamingResponse(self._sync_stream_with_error_handling(result), media_type="text/event-stream")
303
+ elif inspect.isasyncgen(result):
304
+ self.logger.info("Returning streaming response (async generator) (%.3fs)", duration)
305
+ return StreamingResponse(self._stream_with_error_handling(result), media_type="text/event-stream")
306
+
307
+ self.logger.info("Invocation completed successfully (%.3fs)", duration)
308
+ # Use safe serialization for consistency with streaming paths
309
+ safe_json_string = self._safe_serialize_to_json_string(result)
310
+ return Response(safe_json_string, media_type="application/json")
311
+
312
+ except json.JSONDecodeError as e:
313
+ duration = time.time() - start_time
314
+ self.logger.warning("Invalid JSON in request (%.3fs): %s", duration, e)
315
+ return JSONResponse({"error": "Invalid JSON", "details": str(e)}, status_code=400)
316
+ except Exception as e:
317
+ duration = time.time() - start_time
318
+ self.logger.exception("Invocation failed (%.3fs)", duration)
319
+ return JSONResponse({"error": str(e)}, status_code=500)
320
+
321
+ def _handle_ping(self, request):
322
+ try:
323
+ status = self.get_current_ping_status()
324
+ self.logger.debug("Ping request - status: %s", status.value)
325
+ return JSONResponse({"status": status.value, "time_of_last_update": int(self._last_status_update_time)})
326
+ except Exception as e:
327
+ self.logger.error("Ping endpoint failed: %s: %s", type(e).__name__, e)
328
+ return JSONResponse({"status": PingStatus.HEALTHY.value, "time_of_last_update": int(time.time())})
329
+
330
+ def run(self, port: int = 8080, host: Optional[str] = None):
331
+ """Start the Bedrock AgentCore server.
332
+
333
+ Args:
334
+ port: Port to serve on, defaults to 8080
335
+ host: Host to bind to, auto-detected if None
336
+ """
337
+ import os
338
+
339
+ import uvicorn
340
+
341
+ if host is None:
342
+ if os.path.exists("/.dockerenv") or os.environ.get("DOCKER_CONTAINER"):
343
+ host = "0.0.0.0" # nosec B104 - Docker needs this to expose the port
344
+ else:
345
+ host = "127.0.0.1"
346
+ uvicorn.run(self, host=host, port=port)
347
+
348
+ async def _invoke_handler(self, handler, request_context, takes_context, payload):
349
+ if self._invocation_semaphore.locked():
350
+ return JSONResponse({"error": "Server busy - maximum concurrent requests reached"}, status_code=503)
351
+
352
+ async with self._invocation_semaphore:
353
+ try:
354
+ args = (payload, request_context) if takes_context else (payload,)
355
+ if asyncio.iscoroutinefunction(handler):
356
+ return await handler(*args)
357
+ else:
358
+ loop = asyncio.get_event_loop()
359
+ return await loop.run_in_executor(self._invocation_executor, handler, *args)
360
+ except Exception as e:
361
+ handler_name = getattr(handler, "__name__", "unknown")
362
+ self.logger.error("Handler '%s' execution failed: %s: %s", handler_name, type(e).__name__, e)
363
+ raise
364
+
365
+ def _handle_task_action(self, payload: dict) -> Optional[JSONResponse]:
366
+ """Handle task management actions if present in payload."""
367
+ action = payload.get("_agent_core_app_action")
368
+ if not action:
369
+ return None
370
+
371
+ self.logger.debug("Processing debug action: %s", action)
372
+
373
+ try:
374
+ actions = {
375
+ TASK_ACTION_PING_STATUS: lambda: JSONResponse(
376
+ {
377
+ "status": self.get_current_ping_status().value,
378
+ "time_of_last_update": int(self._last_status_update_time),
379
+ }
380
+ ),
381
+ TASK_ACTION_JOB_STATUS: lambda: JSONResponse(self.get_async_task_info()),
382
+ TASK_ACTION_FORCE_HEALTHY: lambda: (
383
+ self.force_ping_status(PingStatus.HEALTHY),
384
+ self.logger.info("Ping status forced to Healthy"),
385
+ JSONResponse({"forced_status": "Healthy"}),
386
+ )[2],
387
+ TASK_ACTION_FORCE_BUSY: lambda: (
388
+ self.force_ping_status(PingStatus.HEALTHY_BUSY),
389
+ self.logger.info("Ping status forced to HealthyBusy"),
390
+ JSONResponse({"forced_status": "HealthyBusy"}),
391
+ )[2],
392
+ TASK_ACTION_CLEAR_FORCED_STATUS: lambda: (
393
+ self.clear_forced_ping_status(),
394
+ self.logger.info("Forced ping status cleared"),
395
+ JSONResponse({"forced_status": "Cleared"}),
396
+ )[2],
397
+ }
398
+
399
+ if action in actions:
400
+ response = actions[action]()
401
+ self.logger.debug("Debug action '%s' completed successfully", action)
402
+ return response
403
+
404
+ self.logger.warning("Unknown debug action requested: %s", action)
405
+ return JSONResponse({"error": f"Unknown action: {action}"}, status_code=400)
406
+
407
+ except Exception as e:
408
+ self.logger.error("Debug action '%s' failed: %s: %s", action, type(e).__name__, e)
409
+ return JSONResponse({"error": "Debug action failed", "details": str(e)}, status_code=500)
410
+
411
+ async def _stream_with_error_handling(self, generator):
412
+ """Wrap async generator to handle errors and convert to SSE format."""
413
+ try:
414
+ async for value in generator:
415
+ yield self._convert_to_sse(value)
416
+ except Exception as e:
417
+ self.logger.error("Error in async streaming: %s: %s", type(e).__name__, e)
418
+ error_event = {
419
+ "error": str(e),
420
+ "error_type": type(e).__name__,
421
+ "message": "An error occurred during streaming",
422
+ }
423
+ yield self._convert_to_sse(error_event)
424
+
425
+ def _safe_serialize_to_json_string(self, obj):
426
+ """Safely serialize object directly to JSON string with progressive fallback handling.
427
+
428
+ This method eliminates double JSON encoding by returning the JSON string directly,
429
+ avoiding the test-then-encode pattern that leads to redundant json.dumps() calls.
430
+ Used by both streaming and non-streaming responses for consistent behavior.
431
+
432
+ Returns:
433
+ str: JSON string representation of the object
434
+ """
435
+ try:
436
+ # First attempt: direct JSON serialization with Unicode support
437
+ return json.dumps(obj, ensure_ascii=False)
438
+ except (TypeError, ValueError, UnicodeEncodeError):
439
+ try:
440
+ # Second attempt: convert to string, then JSON encode the string
441
+ return json.dumps(str(obj), ensure_ascii=False)
442
+ except Exception as e:
443
+ # Final fallback: JSON encode error object with ASCII fallback for problematic Unicode
444
+ self.logger.warning("Failed to serialize object: %s: %s", type(e).__name__, e)
445
+ error_obj = {"error": "Serialization failed", "original_type": type(obj).__name__}
446
+ return json.dumps(error_obj, ensure_ascii=False)
447
+
448
+ def _convert_to_sse(self, obj) -> bytes:
449
+ """Convert object to Server-Sent Events format using safe serialization.
450
+
451
+ Args:
452
+ obj: Object to convert to SSE format
453
+
454
+ Returns:
455
+ bytes: SSE-formatted data ready for streaming
456
+ """
457
+ json_string = self._safe_serialize_to_json_string(obj)
458
+ sse_data = f"data: {json_string}\n\n"
459
+ return sse_data.encode("utf-8")
460
+
461
+ def _sync_stream_with_error_handling(self, generator):
462
+ """Wrap sync generator to handle errors and convert to SSE format."""
463
+ try:
464
+ for value in generator:
465
+ yield self._convert_to_sse(value)
466
+ except Exception as e:
467
+ self.logger.error("Error in sync streaming: %s: %s", type(e).__name__, e)
468
+ error_event = {
469
+ "error": str(e),
470
+ "error_type": type(e).__name__,
471
+ "message": "An error occurred during streaming",
472
+ }
473
+ yield self._convert_to_sse(error_event)
@@ -0,0 +1,34 @@
1
+ """Request context models for Bedrock AgentCore Server.
2
+
3
+ Contains metadata extracted from HTTP requests that handlers can optionally access.
4
+ """
5
+
6
+ from contextvars import ContextVar
7
+ from typing import Optional
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class RequestContext(BaseModel):
13
+ """Request context containing metadata from HTTP requests."""
14
+
15
+ session_id: Optional[str] = Field(None)
16
+
17
+
18
+ class BedrockAgentCoreContext:
19
+ """Context manager for Bedrock AgentCore."""
20
+
21
+ _workload_access_token: ContextVar[str] = ContextVar("workload_access_token")
22
+
23
+ @classmethod
24
+ def set_workload_access_token(cls, token: str):
25
+ """Set the workload access token in the context."""
26
+ cls._workload_access_token.set(token)
27
+
28
+ @classmethod
29
+ def get_workload_access_token(cls) -> Optional[str]:
30
+ """Get the workload access token from the context."""
31
+ try:
32
+ return cls._workload_access_token.get()
33
+ except LookupError:
34
+ return None
@@ -0,0 +1,25 @@
1
+ """Models for BedrockAgentCore runtime.
2
+
3
+ Contains data models and enums used throughout the runtime system.
4
+ """
5
+
6
+ from enum import Enum
7
+
8
+
9
+ class PingStatus(str, Enum):
10
+ """Ping status enum for health check responses."""
11
+
12
+ HEALTHY = "Healthy"
13
+ HEALTHY_BUSY = "HealthyBusy"
14
+
15
+
16
+ # Header constants
17
+ SESSION_HEADER = "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id"
18
+ ACCESS_TOKEN_HEADER = "WorkloadAccessToken" # nosec
19
+
20
+ # Task action constants
21
+ TASK_ACTION_PING_STATUS = "ping_status"
22
+ TASK_ACTION_JOB_STATUS = "job_status"
23
+ TASK_ACTION_FORCE_HEALTHY = "force_healthy"
24
+ TASK_ACTION_FORCE_BUSY = "force_busy"
25
+ TASK_ACTION_CLEAR_FORCED_STATUS = "clear_forced_status"
@@ -0,0 +1 @@
1
+ """External service integrations for BedrockAgentCore Runtime SDK."""