jaf-py 2.4.5__tar.gz → 2.4.6__tar.gz

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 (130) hide show
  1. {jaf_py-2.4.5 → jaf_py-2.4.6}/PKG-INFO +1 -1
  2. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/engine.py +169 -65
  3. jaf_py-2.4.6/jaf/core/guardrails.py +666 -0
  4. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/types.py +83 -1
  5. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf_py.egg-info/PKG-INFO +1 -1
  6. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf_py.egg-info/SOURCES.txt +1 -0
  7. {jaf_py-2.4.5 → jaf_py-2.4.6}/pyproject.toml +1 -1
  8. {jaf_py-2.4.5 → jaf_py-2.4.6}/LICENSE +0 -0
  9. {jaf_py-2.4.5 → jaf_py-2.4.6}/README.md +0 -0
  10. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/__init__.py +0 -0
  11. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/__init__.py +0 -0
  12. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/agent.py +0 -0
  13. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/agent_card.py +0 -0
  14. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/client.py +0 -0
  15. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/examples/__init__.py +0 -0
  16. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/examples/client_example.py +0 -0
  17. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/examples/integration_example.py +0 -0
  18. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/examples/rag_demo/__init__.py +0 -0
  19. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/examples/server_demo/__init__.py +0 -0
  20. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/examples/server_example.py +0 -0
  21. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/__init__.py +0 -0
  22. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/cleanup.py +0 -0
  23. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/factory.py +0 -0
  24. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/providers/__init__.py +0 -0
  25. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/providers/composite.py +0 -0
  26. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/providers/in_memory.py +0 -0
  27. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/providers/postgres.py +0 -0
  28. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/providers/redis.py +0 -0
  29. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/serialization.py +0 -0
  30. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/tests/__init__.py +0 -0
  31. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/tests/run_comprehensive_tests.py +0 -0
  32. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/tests/test_cleanup.py +0 -0
  33. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/tests/test_serialization.py +0 -0
  34. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/tests/test_stress_concurrency.py +0 -0
  35. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/tests/test_task_lifecycle.py +0 -0
  36. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/memory/types.py +0 -0
  37. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/protocol.py +0 -0
  38. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/server.py +0 -0
  39. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/standalone_client.py +0 -0
  40. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/tests/__init__.py +0 -0
  41. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/tests/run_tests.py +0 -0
  42. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/tests/test_agent.py +0 -0
  43. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/tests/test_client.py +0 -0
  44. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/tests/test_integration.py +0 -0
  45. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/tests/test_protocol.py +0 -0
  46. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/tests/test_types.py +0 -0
  47. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/a2a/types.py +0 -0
  48. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/cli.py +0 -0
  49. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/__init__.py +0 -0
  50. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/agent_tool.py +0 -0
  51. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/analytics.py +0 -0
  52. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/composition.py +0 -0
  53. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/errors.py +0 -0
  54. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/parallel_agents.py +0 -0
  55. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/performance.py +0 -0
  56. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/proxy.py +0 -0
  57. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/proxy_helpers.py +0 -0
  58. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/state.py +0 -0
  59. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/streaming.py +0 -0
  60. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/tool_results.py +0 -0
  61. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/tools.py +0 -0
  62. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/tracing.py +0 -0
  63. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/core/workflows.py +0 -0
  64. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/exceptions.py +0 -0
  65. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/memory/__init__.py +0 -0
  66. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/memory/approval_storage.py +0 -0
  67. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/memory/factory.py +0 -0
  68. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/memory/providers/__init__.py +0 -0
  69. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/memory/providers/in_memory.py +0 -0
  70. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/memory/providers/postgres.py +0 -0
  71. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/memory/providers/redis.py +0 -0
  72. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/memory/types.py +0 -0
  73. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/memory/utils.py +0 -0
  74. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/plugins/__init__.py +0 -0
  75. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/plugins/base.py +0 -0
  76. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/policies/__init__.py +0 -0
  77. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/policies/handoff.py +0 -0
  78. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/policies/validation.py +0 -0
  79. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/providers/__init__.py +0 -0
  80. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/providers/mcp.py +0 -0
  81. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/providers/model.py +0 -0
  82. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/server/__init__.py +0 -0
  83. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/server/main.py +0 -0
  84. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/server/server.py +0 -0
  85. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/server/types.py +0 -0
  86. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/utils/__init__.py +0 -0
  87. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/utils/attachments.py +0 -0
  88. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/utils/document_processor.py +0 -0
  89. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/visualization/__init__.py +0 -0
  90. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/visualization/example.py +0 -0
  91. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/visualization/functional_core.py +0 -0
  92. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/visualization/graphviz.py +0 -0
  93. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/visualization/imperative_shell.py +0 -0
  94. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf/visualization/types.py +0 -0
  95. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf_py.egg-info/dependency_links.txt +0 -0
  96. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf_py.egg-info/entry_points.txt +0 -0
  97. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf_py.egg-info/requires.txt +0 -0
  98. {jaf_py-2.4.5 → jaf_py-2.4.6}/jaf_py.egg-info/top_level.txt +0 -0
  99. {jaf_py-2.4.5 → jaf_py-2.4.6}/setup.cfg +0 -0
  100. {jaf_py-2.4.5 → jaf_py-2.4.6}/setup.py +0 -0
  101. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_a2a_deep.py +0 -0
  102. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_a2a_examples.py +0 -0
  103. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_api_reference_examples.py +0 -0
  104. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_attachments.py +0 -0
  105. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_callback_system_examples.py +0 -0
  106. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_coffee_tool.py +0 -0
  107. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_conversation_id_fix.py +0 -0
  108. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_deployment_examples.py +0 -0
  109. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_docs_code_examples.py +0 -0
  110. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_engine.py +0 -0
  111. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_engine_manual.py +0 -0
  112. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_error_handling_examples.py +0 -0
  113. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_getting_started_examples.py +0 -0
  114. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_math_tool.py +0 -0
  115. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_mcp_comprehensive.py +0 -0
  116. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_mcp_docs.py +0 -0
  117. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_mcp_real_functionality.py +0 -0
  118. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_mcp_transports.py +0 -0
  119. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_memory_system_examples.py +0 -0
  120. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_model_providers_examples.py +0 -0
  121. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_property_based.py +0 -0
  122. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_proxy_simple.py +0 -0
  123. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_redis_fixes.py +0 -0
  124. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_redis_memory.py +0 -0
  125. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_server_api_examples.py +0 -0
  126. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_session_continuity.py +0 -0
  127. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_streamable_http_mcp_example.py +0 -0
  128. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_timeout_functionality.py +0 -0
  129. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_tool_integration.py +0 -0
  130. {jaf_py-2.4.5 → jaf_py-2.4.6}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jaf-py
3
- Version: 2.4.5
3
+ Version: 2.4.6
4
4
  Summary: A purely functional agent framework with immutable state and composable tools - Python implementation
5
5
  Author: JAF Contributors
6
6
  Maintainer: JAF Contributors
@@ -32,6 +32,8 @@ from .types import (
32
32
  Interruption,
33
33
  GuardrailEvent,
34
34
  GuardrailEventData,
35
+ GuardrailViolationEvent,
36
+ GuardrailViolationEventData,
35
37
  MemoryEvent,
36
38
  MemoryEventData,
37
39
  OutputParseEvent,
@@ -61,6 +63,15 @@ from .types import (
61
63
  ToolCallFunction,
62
64
  ToolCallStartEvent,
63
65
  ToolCallStartEventData,
66
+ Guardrail,
67
+ ValidValidationResult,
68
+ InvalidValidationResult,
69
+ )
70
+ from .guardrails import (
71
+ build_effective_guardrails,
72
+ execute_input_guardrails_sequential,
73
+ execute_input_guardrails_parallel,
74
+ execute_output_guardrails,
64
75
  )
65
76
 
66
77
 
@@ -399,36 +410,6 @@ async def _run_internal(
399
410
  if resumed:
400
411
  return resumed
401
412
 
402
- # Check initial input guardrails on first turn
403
- if state.turn_count == 0:
404
- first_user_message = next((m for m in state.messages if m.role == ContentRole.USER or m.role == 'user'), None)
405
- if first_user_message and config.initial_input_guardrails:
406
- for guardrail in config.initial_input_guardrails:
407
- if config.on_event:
408
- config.on_event(GuardrailEvent(data=GuardrailEventData(
409
- guardrail_name=getattr(guardrail, '__name__', 'unknown_guardrail'),
410
- content=get_text_content(first_user_message.content)
411
- )))
412
- if asyncio.iscoroutinefunction(guardrail):
413
- result = await guardrail(get_text_content(first_user_message.content))
414
- else:
415
- result = guardrail(get_text_content(first_user_message.content))
416
-
417
- if not result.is_valid:
418
- if config.on_event:
419
- config.on_event(GuardrailEvent(data=GuardrailEventData(
420
- guardrail_name=getattr(guardrail, '__name__', 'unknown_guardrail'),
421
- content=get_text_content(first_user_message.content),
422
- is_valid=False,
423
- error_message=result.error_message
424
- )))
425
- return RunResult(
426
- final_state=state,
427
- outcome=ErrorOutcome(error=InputGuardrailTripwire(
428
- reason=result.error_message or "Input guardrail failed"
429
- ))
430
- )
431
-
432
413
  # Check max turns
433
414
  max_turns = config.max_turns or 50
434
415
  if state.turn_count >= max_turns:
@@ -445,6 +426,105 @@ async def _run_internal(
445
426
  outcome=ErrorOutcome(error=AgentNotFound(agent_name=state.current_agent_name))
446
427
  )
447
428
 
429
+ # Determine if agent has advanced guardrails configuration
430
+ has_advanced_guardrails = bool(
431
+ current_agent.advanced_config and
432
+ current_agent.advanced_config.guardrails and
433
+ (current_agent.advanced_config.guardrails.input_prompt or
434
+ current_agent.advanced_config.guardrails.output_prompt or
435
+ current_agent.advanced_config.guardrails.require_citations)
436
+ )
437
+
438
+ print('[JAF:ENGINE] Debug guardrails setup:', {
439
+ 'agent_name': current_agent.name,
440
+ 'has_advanced_config': bool(current_agent.advanced_config),
441
+ 'has_advanced_guardrails': has_advanced_guardrails,
442
+ 'initial_input_guardrails': len(config.initial_input_guardrails or []),
443
+ 'final_output_guardrails': len(config.final_output_guardrails or [])
444
+ })
445
+
446
+ # Build effective guardrails
447
+ effective_input_guardrails: List[Guardrail] = []
448
+ effective_output_guardrails: List[Guardrail] = []
449
+
450
+ if has_advanced_guardrails:
451
+ result = await build_effective_guardrails(current_agent, config)
452
+ effective_input_guardrails, effective_output_guardrails = result
453
+ else:
454
+ effective_input_guardrails = list(config.initial_input_guardrails or [])
455
+ effective_output_guardrails = list(config.final_output_guardrails or [])
456
+
457
+ # Execute input guardrails on first turn
458
+ input_guardrails_to_run = (effective_input_guardrails
459
+ if state.turn_count == 0 and effective_input_guardrails
460
+ else [])
461
+
462
+ print('[JAF:ENGINE] Input guardrails to run:', {
463
+ 'turn_count': state.turn_count,
464
+ 'effective_input_length': len(effective_input_guardrails),
465
+ 'input_guardrails_to_run_length': len(input_guardrails_to_run),
466
+ 'has_advanced_guardrails': has_advanced_guardrails
467
+ })
468
+
469
+ if input_guardrails_to_run and state.turn_count == 0:
470
+ first_user_message = next((m for m in state.messages if m.role == ContentRole.USER or m.role == 'user'), None)
471
+ if first_user_message:
472
+ if has_advanced_guardrails:
473
+ execution_mode = (current_agent.advanced_config.guardrails.execution_mode
474
+ if current_agent.advanced_config and current_agent.advanced_config.guardrails
475
+ else 'parallel')
476
+
477
+ if execution_mode == 'sequential':
478
+ guardrail_result = await execute_input_guardrails_sequential(
479
+ input_guardrails_to_run, first_user_message, config
480
+ )
481
+ if not guardrail_result.is_valid:
482
+ return RunResult(
483
+ final_state=state,
484
+ outcome=ErrorOutcome(error=InputGuardrailTripwire(
485
+ reason=getattr(guardrail_result, 'error_message', 'Input guardrail violation')
486
+ ))
487
+ )
488
+ else:
489
+ # Parallel execution with LLM call overlap
490
+ guardrail_result = await execute_input_guardrails_parallel(
491
+ input_guardrails_to_run, first_user_message, config
492
+ )
493
+ if not guardrail_result.is_valid:
494
+ print(f"🚨 Input guardrail violation: {getattr(guardrail_result, 'error_message', 'Unknown violation')}")
495
+ return RunResult(
496
+ final_state=state,
497
+ outcome=ErrorOutcome(error=InputGuardrailTripwire(
498
+ reason=getattr(guardrail_result, 'error_message', 'Input guardrail violation')
499
+ ))
500
+ )
501
+ else:
502
+ # Legacy guardrails path
503
+ print('[JAF:ENGINE] Using LEGACY guardrails path with', len(input_guardrails_to_run), 'guardrails')
504
+ for guardrail in input_guardrails_to_run:
505
+ if config.on_event:
506
+ config.on_event(GuardrailEvent(data=GuardrailEventData(
507
+ guardrail_name=getattr(guardrail, '__name__', 'unknown_guardrail'),
508
+ content=get_text_content(first_user_message.content)
509
+ )))
510
+ if asyncio.iscoroutinefunction(guardrail):
511
+ result = await guardrail(get_text_content(first_user_message.content))
512
+ else:
513
+ result = guardrail(get_text_content(first_user_message.content))
514
+
515
+ if not result.is_valid:
516
+ if config.on_event:
517
+ config.on_event(GuardrailViolationEvent(data=GuardrailViolationEventData(
518
+ stage='input',
519
+ reason=getattr(result, 'error_message', 'Input guardrail failed')
520
+ )))
521
+ return RunResult(
522
+ final_state=state,
523
+ outcome=ErrorOutcome(error=InputGuardrailTripwire(
524
+ reason=getattr(result, 'error_message', 'Input guardrail failed')
525
+ ))
526
+ )
527
+
448
528
  # Agent debugging logs removed for performance
449
529
 
450
530
  # Get model name
@@ -752,13 +832,27 @@ async def _run_internal(
752
832
  )))
753
833
 
754
834
  # Check final output guardrails
755
- if config.final_output_guardrails:
756
- for guardrail in config.final_output_guardrails:
757
- if config.on_event:
758
- config.on_event(GuardrailEvent(data=GuardrailEventData(
759
- guardrail_name=getattr(guardrail, '__name__', 'unknown_guardrail'),
760
- content=output_data
761
- )))
835
+ if has_advanced_guardrails:
836
+ # Use new advanced system
837
+ output_guardrail_result = await execute_output_guardrails(
838
+ effective_output_guardrails, output_data, config
839
+ )
840
+ if not output_guardrail_result.is_valid:
841
+ return RunResult(
842
+ final_state=replace(state, messages=new_messages),
843
+ outcome=ErrorOutcome(error=OutputGuardrailTripwire(
844
+ reason=getattr(output_guardrail_result, 'error_message', 'Output guardrail violation')
845
+ ))
846
+ )
847
+ else:
848
+ # Legacy system
849
+ if effective_output_guardrails:
850
+ for guardrail in effective_output_guardrails:
851
+ if config.on_event:
852
+ config.on_event(GuardrailEvent(data=GuardrailEventData(
853
+ guardrail_name=getattr(guardrail, '__name__', 'unknown_guardrail'),
854
+ content=output_data
855
+ )))
762
856
  if asyncio.iscoroutinefunction(guardrail):
763
857
  result = await guardrail(output_data)
764
858
  else:
@@ -766,16 +860,14 @@ async def _run_internal(
766
860
 
767
861
  if not result.is_valid:
768
862
  if config.on_event:
769
- config.on_event(GuardrailEvent(data=GuardrailEventData(
770
- guardrail_name=getattr(guardrail, '__name__', 'unknown_guardrail'),
771
- content=output_data,
772
- is_valid=False,
773
- error_message=result.error_message
863
+ config.on_event(GuardrailViolationEvent(data=GuardrailViolationEventData(
864
+ stage='output',
865
+ reason=getattr(result, 'error_message', 'Output guardrail failed')
774
866
  )))
775
867
  return RunResult(
776
868
  final_state=replace(state, messages=new_messages, approvals=state.approvals),
777
869
  outcome=ErrorOutcome(error=OutputGuardrailTripwire(
778
- reason=result.error_message or "Output guardrail failed"
870
+ reason=getattr(result, 'error_message', 'Output guardrail failed')
779
871
  ))
780
872
  )
781
873
 
@@ -799,32 +891,44 @@ async def _run_internal(
799
891
  )
800
892
  else:
801
893
  # No output codec, return content as string
802
- if config.final_output_guardrails:
803
- for guardrail in config.final_output_guardrails:
804
- if config.on_event:
805
- config.on_event(GuardrailEvent(data=GuardrailEventData(
806
- guardrail_name=getattr(guardrail, '__name__', 'unknown_guardrail'),
807
- content=get_text_content(assistant_message.content)
808
- )))
809
- if asyncio.iscoroutinefunction(guardrail):
810
- result = await guardrail(get_text_content(assistant_message.content))
811
- else:
812
- result = guardrail(get_text_content(assistant_message.content))
813
-
814
- if not result.is_valid:
894
+ if has_advanced_guardrails:
895
+ # Use new advanced system
896
+ output_guardrail_result = await execute_output_guardrails(
897
+ effective_output_guardrails, get_text_content(assistant_message.content), config
898
+ )
899
+ if not output_guardrail_result.is_valid:
900
+ return RunResult(
901
+ final_state=replace(state, messages=new_messages),
902
+ outcome=ErrorOutcome(error=OutputGuardrailTripwire(
903
+ reason=getattr(output_guardrail_result, 'error_message', 'Output guardrail violation')
904
+ ))
905
+ )
906
+ else:
907
+ # Legacy system
908
+ if effective_output_guardrails:
909
+ for guardrail in effective_output_guardrails:
815
910
  if config.on_event:
816
911
  config.on_event(GuardrailEvent(data=GuardrailEventData(
817
912
  guardrail_name=getattr(guardrail, '__name__', 'unknown_guardrail'),
818
- content=get_text_content(assistant_message.content),
819
- is_valid=False,
820
- error_message=result.error_message
913
+ content=get_text_content(assistant_message.content)
821
914
  )))
822
- return RunResult(
823
- final_state=replace(state, messages=new_messages, approvals=state.approvals),
824
- outcome=ErrorOutcome(error=OutputGuardrailTripwire(
825
- reason=result.error_message or "Output guardrail failed"
826
- ))
827
- )
915
+ if asyncio.iscoroutinefunction(guardrail):
916
+ result = await guardrail(get_text_content(assistant_message.content))
917
+ else:
918
+ result = guardrail(get_text_content(assistant_message.content))
919
+
920
+ if not result.is_valid:
921
+ if config.on_event:
922
+ config.on_event(GuardrailViolationEvent(data=GuardrailViolationEventData(
923
+ stage='output',
924
+ reason=getattr(result, 'error_message', 'Output guardrail failed')
925
+ )))
926
+ return RunResult(
927
+ final_state=replace(state, messages=new_messages, approvals=state.approvals),
928
+ outcome=ErrorOutcome(error=OutputGuardrailTripwire(
929
+ reason=getattr(result, 'error_message', 'Output guardrail failed')
930
+ ))
931
+ )
828
932
 
829
933
  return RunResult(
830
934
  final_state=replace(state, messages=new_messages, turn_count=state.turn_count + 1, approvals=state.approvals),