dialectical-framework 2.0.5__tar.gz → 2.0.7__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 (233) hide show
  1. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/PKG-INFO +1 -1
  2. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/pyproject.toml +1 -1
  3. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/advisor.py +59 -27
  4. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/conversation_facilitator.py +32 -1
  5. dialectical_framework-2.0.7/src/dialectical_framework/exceptions/provider_errors.py +42 -0
  6. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/bedrock_provider.py +144 -34
  7. dialectical_framework-2.0.7/src/dialectical_framework/utils/format_compat.py +136 -0
  8. dialectical_framework-2.0.7/src/dialectical_framework/utils/thinking_compat.py +401 -0
  9. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/use_brain.py +17 -0
  10. dialectical_framework-2.0.5/src/dialectical_framework/utils/thinking_compat.py +0 -203
  11. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/LICENSE +0 -0
  12. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/PATENTS +0 -0
  13. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/README.md +0 -0
  14. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/__init__.py +0 -0
  15. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/__init__.py +0 -0
  16. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/__init__.py +0 -0
  17. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/build_policy.py +0 -0
  18. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/migration.py +0 -0
  19. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/reply_hygiene.py +0 -0
  20. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/system_prompts.py +0 -0
  21. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/tools/__init__.py +0 -0
  22. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/tools/anchor.py +0 -0
  23. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/tools/deepen.py +0 -0
  24. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/tools/explore.py +0 -0
  25. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/tools/ingest.py +0 -0
  26. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/tools/note.py +0 -0
  27. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/tools/record_decision.py +0 -0
  28. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/tools/scoped.py +0 -0
  29. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/advisor/tools/sync.py +0 -0
  30. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/agent_context.py +0 -0
  31. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/__init__.py +0 -0
  32. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/analyst.py +0 -0
  33. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/skills/__init__.py +0 -0
  34. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/skills/anchor_theses.py +0 -0
  35. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/skills/edit_perspective.py +0 -0
  36. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/skills/expand_polarities.py +0 -0
  37. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/skills/find_polarities.py +0 -0
  38. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/skills/introduce_polarity.py +0 -0
  39. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/skills/sketch_tetrad.py +0 -0
  40. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/skills/surface_theses.py +0 -0
  41. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/system_prompts.py +0 -0
  42. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/tools/__init__.py +0 -0
  43. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/analyst/tools/place_statement.py +0 -0
  44. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/app_spec.py +0 -0
  45. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/apps.py +0 -0
  46. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/consultant/__init__.py +0 -0
  47. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/consultant/consultant.py +0 -0
  48. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/execution_report.py +0 -0
  49. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/explorer/__init__.py +0 -0
  50. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/explorer/explorer.py +0 -0
  51. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/explorer/skills/__init__.py +0 -0
  52. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/explorer/skills/build_wheels.py +0 -0
  53. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/explorer/skills/explore_transformations.py +0 -0
  54. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/explorer/skills/generate_synthesis.py +0 -0
  55. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/explorer/system_prompts.py +0 -0
  56. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/explorer/tools/__init__.py +0 -0
  57. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/explorer/tools/create_nexus.py +0 -0
  58. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/explorer/tools/expand_nexus.py +0 -0
  59. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/explorer/tools/generate_synthesis.py +0 -0
  60. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/explorer/tools/present_exploration.py +0 -0
  61. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/__init__.py +0 -0
  62. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/tools/__init__.py +0 -0
  63. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/tools/add_input.py +0 -0
  64. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/tools/audit_feasibility.py +0 -0
  65. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/tools/create_dx_input.py +0 -0
  66. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/tools/digest_input.py +0 -0
  67. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/tools/discard.py +0 -0
  68. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/tools/get_schema.py +0 -0
  69. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/tools/inspect_node.py +0 -0
  70. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/tools/present_analysis.py +0 -0
  71. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/tools/query_graph.py +0 -0
  72. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/tools/read_digest.py +0 -0
  73. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/orchestrator/tools/read_input.py +0 -0
  74. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/reasonable_concern.py +0 -0
  75. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/stream_events.py +0 -0
  76. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/toolsets.py +0 -0
  77. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/agents/turn_timing.py +0 -0
  78. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/__init__.py +0 -0
  79. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/ac_re_taxonomy.py +0 -0
  80. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/action_extraction.py +0 -0
  81. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/add_input.py +0 -0
  82. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/ai_dto/__init__.py +0 -0
  83. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/ai_dto/statement_dto.py +0 -0
  84. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/ai_dto/statements_deck_dto.py +0 -0
  85. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/antithesis_classification.py +0 -0
  86. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/antithesis_extraction.py +0 -0
  87. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/antithetical_thesis_detection.py +0 -0
  88. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/aspect_classification.py +0 -0
  89. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/aspect_generation.py +0 -0
  90. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/build_status.py +0 -0
  91. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/causality/__init__.py +0 -0
  92. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/causality/causality_estimator.py +0 -0
  93. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/causality/causality_estimator_balanced.py +0 -0
  94. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/causality/causality_estimator_criteria.py +0 -0
  95. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/causality/causality_estimator_desirable.py +0 -0
  96. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/causality/causality_estimator_feasible.py +0 -0
  97. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/causality/causality_estimator_realistic.py +0 -0
  98. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/causality/causality_normalizer.py +0 -0
  99. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/causality/estimator_resolver.py +0 -0
  100. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/causality_estimation.py +0 -0
  101. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/control_statements_check.py +0 -0
  102. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/create_dx_input.py +0 -0
  103. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/create_nexus.py +0 -0
  104. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/decision_coherence_check.py +0 -0
  105. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/decision_confirmation_check.py +0 -0
  106. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/diagonal_oppositions_check.py +0 -0
  107. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/dialectical_context.py +0 -0
  108. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/discard.py +0 -0
  109. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/display_text_edit.py +0 -0
  110. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/expand_nexus.py +0 -0
  111. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/perspective_combination.py +0 -0
  112. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/perspective_validation.py +0 -0
  113. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/positive_ac_re_apex_derivation.py +0 -0
  114. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/record_decision.py +0 -0
  115. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/scoring_scales.py +0 -0
  116. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/source_digest.py +0 -0
  117. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/statement_classification.py +0 -0
  118. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/statement_deduplication.py +0 -0
  119. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/statement_headline.py +0 -0
  120. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/statement_placement.py +0 -0
  121. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/synthesis_generation.py +0 -0
  122. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/tetrad_candidates.py +0 -0
  123. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/tetrad_grounding.py +0 -0
  124. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/tetrad_sketch.py +0 -0
  125. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/thesis_extraction.py +0 -0
  126. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/transformation_audit.py +0 -0
  127. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/transformation_generation.py +0 -0
  128. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/transformation_sketch.py +0 -0
  129. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/concerns/view_sketch.py +0 -0
  130. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/dialectical_reasoning.py +0 -0
  131. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/enums/__init__.py +0 -0
  132. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/enums/causality_preset.py +0 -0
  133. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/enums/di.py +0 -0
  134. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/events/__init__.py +0 -0
  135. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/events/graph_event.py +0 -0
  136. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/events/graph_event_bus.py +0 -0
  137. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/events/progress_event.py +0 -0
  138. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/exceptions/__init__.py +0 -0
  139. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/exceptions/node_errors.py +0 -0
  140. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/exceptions/resolver_errors.py +0 -0
  141. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/__init__.py +0 -0
  142. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/composite_input_resolver.py +0 -0
  143. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/dialexity_input_resolver.py +0 -0
  144. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/estimation_manager.py +0 -0
  145. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/mixins/__init__.py +0 -0
  146. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/mixins/incremental_build_mixin.py +0 -0
  147. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/mixins/intent_mixin.py +0 -0
  148. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/mixins/persistable_mixin.py +0 -0
  149. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/__init__.py +0 -0
  150. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/assessable_entity.py +0 -0
  151. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/base_node.py +0 -0
  152. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/case.py +0 -0
  153. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/cycle.py +0 -0
  154. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/decision.py +0 -0
  155. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/estimation.py +0 -0
  156. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/ideas.py +0 -0
  157. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/input.py +0 -0
  158. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/nexus.py +0 -0
  159. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/note.py +0 -0
  160. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/perspective.py +0 -0
  161. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/polarity.py +0 -0
  162. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/rationale.py +0 -0
  163. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/statement.py +0 -0
  164. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/synthesis.py +0 -0
  165. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/transformation.py +0 -0
  166. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/transition.py +0 -0
  167. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/nodes/wheel.py +0 -0
  168. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationship_manager.py +0 -0
  169. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/__init__.py +0 -0
  170. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/action_reflection_relationship.py +0 -0
  171. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/belongs_to_cycle_relationship.py +0 -0
  172. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/belongs_to_nexus_relationship.py +0 -0
  173. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/changed_to_relationship.py +0 -0
  174. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/contradiction_of_relationship.py +0 -0
  175. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/critiques_relationship.py +0 -0
  176. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/distilled_to_relationship.py +0 -0
  177. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/estimates_relationship.py +0 -0
  178. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/explains_relationship.py +0 -0
  179. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/grounded_in_relationship.py +0 -0
  180. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/has_input_relationship.py +0 -0
  181. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/has_statement_relationship.py +0 -0
  182. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/has_wheel_relationship.py +0 -0
  183. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/immutable_structure.py +0 -0
  184. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/is_source_of_relationship.py +0 -0
  185. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/is_target_of_relationship.py +0 -0
  186. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/negative_side_of_relationship.py +0 -0
  187. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/opposite_direction_relationship.py +0 -0
  188. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/opposite_of_relationship.py +0 -0
  189. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/polarity_relationship.py +0 -0
  190. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/positive_side_of_relationship.py +0 -0
  191. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/provides_relationship.py +0 -0
  192. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/relationships/synthesis_of_relationship.py +0 -0
  193. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/rendering.py +0 -0
  194. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/__init__.py +0 -0
  195. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/case_repository.py +0 -0
  196. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/cycle_repository.py +0 -0
  197. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/decision_repository.py +0 -0
  198. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/input_repository.py +0 -0
  199. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/nexus_repository.py +0 -0
  200. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/node_repository.py +0 -0
  201. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/note_repository.py +0 -0
  202. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/perspective_repository.py +0 -0
  203. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/polarity_repository.py +0 -0
  204. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/schema_repository.py +0 -0
  205. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/statement_repository.py +0 -0
  206. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/transformation_repository.py +0 -0
  207. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/repositories/wheel_repository.py +0 -0
  208. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/scope_context.py +0 -0
  209. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/verbatim_input_resolver.py +0 -0
  210. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/views.py +0 -0
  211. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/wheel_segment.py +0 -0
  212. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/graph/wheel_segment_polar_pair.py +0 -0
  213. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/protocols/__init__.py +0 -0
  214. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/protocols/has_config.py +0 -0
  215. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/protocols/input_resolver.py +0 -0
  216. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/py.typed +0 -0
  217. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/settings.py +0 -0
  218. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/settings_context.py +0 -0
  219. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/__init__.py +0 -0
  220. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/async_drain.py +0 -0
  221. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/call_census.py +0 -0
  222. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/chunking.py +0 -0
  223. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/concurrency.py +0 -0
  224. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/dc_replace.py +0 -0
  225. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/decompose_probability_uniformly.py +0 -0
  226. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/edge_context.py +0 -0
  227. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/effect_logger.py +0 -0
  228. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/input_context.py +0 -0
  229. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/order_transitions.py +0 -0
  230. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/progress.py +0 -0
  231. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/retry_accounting.py +0 -0
  232. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/sequence_generation.py +0 -0
  233. {dialectical_framework-2.0.5 → dialectical_framework-2.0.7}/src/dialectical_framework/utils/utterance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dialectical-framework
3
- Version: 2.0.5
3
+ Version: 2.0.7
4
4
  Summary: A dialectical framework for augmented intelligence. AI reasoning powered with dialectics supports humans in: system optimization (psychology, engineering, business, politics, etc.); dispute resolution (mediation, conflicts, negotiations, etc.); decision-making (dilemmas, challenging situations, win-win, etc.).
5
5
  License: MIT
6
6
  Keywords: dialectics,dialectical-reasoning,synthesis,thesis-antithesis,ai,artificial-intelligence,llm,reasoning-framework,philosophy,logic,argumentation,conflict-resolution,decision-making,critical-thinking,semantic-graph,mirascope,pydantic,perspectives,polarity-reasoning
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dialectical-framework"
3
- version = "2.0.5"
3
+ version = "2.0.7"
4
4
  description = "A dialectical framework for augmented intelligence. AI reasoning powered with dialectics supports humans in: system optimization (psychology, engineering, business, politics, etc.); dispute resolution (mediation, conflicts, negotiations, etc.); decision-making (dilemmas, challenging situations, win-win, etc.)."
5
5
  authors = ["Evaldas Taroza <evaldas@dialexity.com>"]
6
6
  readme = "README.md"
@@ -42,6 +42,7 @@ from dialectical_framework.agents.stream_events import (ResponseComplete,
42
42
  StreamEvent, TextDelta,
43
43
  ToolResult, ToolStart)
44
44
  from dialectical_framework.agents.toolsets import merge_app_tools
45
+ from dialectical_framework.exceptions.provider_errors import ModelRefusal
45
46
  from dialectical_framework.agents.turn_timing import (ClosingOutcome,
46
47
  DeferralOutcome,
47
48
  TurnTiming)
@@ -850,7 +851,13 @@ class Advisor(SettingsAware):
850
851
  # Around the provider round ONLY — the closing seam and the
851
852
  # off-turn scheduling below run outside it on purpose.
852
853
  with speaking(user_message):
853
- result = await self._conversation.submit(ChatResponse, user_message)
854
+ try:
855
+ result = await self._conversation.submit(
856
+ ChatResponse, user_message
857
+ )
858
+ except ModelRefusal:
859
+ await self._settle_refused_turn(user_message)
860
+ raise
854
861
  reply_path_s = (
855
862
  deferred_wait_s
856
863
  + context_render_s
@@ -966,33 +973,37 @@ class Advisor(SettingsAware):
966
973
  # Entered here, before the stream is opened, because the tool runs
967
974
  # inside the generator while it is being iterated.
968
975
  with speaking(user_message):
969
- async with aclosing(
970
- self._conversation.submit_stream(ChatResponse, user_message)
971
- ) as rounds:
972
- async for event in rounds:
973
- if isinstance(event, ResponseComplete):
974
- final_event = event
975
- reply = event.message
976
- # `continue`, so `submit_stream` is asked for one more
977
- # event and runs to its own end instead of being closed
978
- # while suspended at its yield. The seconds read below are
979
- # safe either way — it stamps them as this event passes
980
- # through it, not on exit — but running out is still the
981
- # cleaner exit: the provider's connection is let go of by
982
- # exhaustion rather than by a close unwinding a live frame.
983
- continue
984
- if hygiene is not None:
985
- if isinstance(event, TextDelta):
986
- clean = hygiene.feed(event.text)
987
- if clean:
988
- yield TextDelta(text=clean)
976
+ try:
977
+ async with aclosing(
978
+ self._conversation.submit_stream(ChatResponse, user_message)
979
+ ) as rounds:
980
+ async for event in rounds:
981
+ if isinstance(event, ResponseComplete):
982
+ final_event = event
983
+ reply = event.message
984
+ # `continue`, so `submit_stream` is asked for one more
985
+ # event and runs to its own end instead of being closed
986
+ # while suspended at its yield. The seconds read below are
987
+ # safe either way — it stamps them as this event passes
988
+ # through it, not on exit — but running out is still the
989
+ # cleaner exit: the provider's connection is let go of by
990
+ # exhaustion rather than by a close unwinding a live frame.
989
991
  continue
990
- if isinstance(event, (ToolStart, ToolResult)):
991
- tail = hygiene.flush()
992
- if tail:
993
- yield TextDelta(text=tail)
994
- hygiene = HashCitationFilter()
995
- yield event
992
+ if hygiene is not None:
993
+ if isinstance(event, TextDelta):
994
+ clean = hygiene.feed(event.text)
995
+ if clean:
996
+ yield TextDelta(text=clean)
997
+ continue
998
+ if isinstance(event, (ToolStart, ToolResult)):
999
+ tail = hygiene.flush()
1000
+ if tail:
1001
+ yield TextDelta(text=tail)
1002
+ hygiene = HashCitationFilter()
1003
+ yield event
1004
+ except ModelRefusal:
1005
+ await self._settle_refused_turn(user_message)
1006
+ raise
996
1007
  if hygiene is not None:
997
1008
  tail = hygiene.flush()
998
1009
  if tail:
@@ -2829,6 +2840,27 @@ class Advisor(SettingsAware):
2829
2840
  logger.exception("Accepted-cost ground resolution failed (fail-soft)")
2830
2841
  return None
2831
2842
 
2843
+ async def _settle_refused_turn(self, user_message: str) -> None:
2844
+ """The closing work a refused turn still owes, before the refusal goes on.
2845
+
2846
+ A refusal can arrive AFTER tool rounds: the resume round is the one
2847
+ declined, so `record_decision` may already have written a Decision and
2848
+ `note` may already have kept a Note. The facilitator drops the turn
2849
+ from history, but the graph keeps what the tools wrote — and without
2850
+ this the recorded decision never gets its pathway weave (the seam's
2851
+ MODEL_RECORDED branch is what schedules it) and the noted tensions wait
2852
+ for the next turn to be offered. Only the work the tools already did is
2853
+ followed up: the classifier is not asked about a turn that was refused,
2854
+ so no decision is written here that the model did not write.
2855
+ Fail-soft: the refusal is what the host must see.
2856
+ """
2857
+ try:
2858
+ if self._recorded_decision_this_turn():
2859
+ await self._repair_unrecorded_decision(user_message, "")
2860
+ self._schedule_noted_tensions()
2861
+ except Exception:
2862
+ logger.exception("Closing work after a refused turn failed (fail-soft)")
2863
+
2832
2864
  def _recorded_decision_this_turn(self) -> bool:
2833
2865
  """Did `record_decision` already run, successfully, on this turn?
2834
2866
 
@@ -29,6 +29,7 @@ from dialectical_framework.agents.stream_events import (
29
29
  ToolStart,
30
30
  )
31
31
  from dialectical_framework.agents.turn_timing import ToolRound
32
+ from dialectical_framework.exceptions.provider_errors import ModelRefusal
32
33
  from dialectical_framework.protocols.has_config import SettingsAware
33
34
  from dialectical_framework.utils.call_census import record_call
34
35
  from dialectical_framework.utils.thinking_compat import conversational_round
@@ -37,7 +38,7 @@ from dialectical_framework.utils.retry_accounting import (RetryAccount,
37
38
  from dialectical_framework.utils.use_brain import (prefill_token_kwargs,
38
39
  retry_transient, use_brain)
39
40
 
40
- from mirascope.llm import TextChunk, ThoughtChunk, ToolOutput
41
+ from mirascope.llm import FinishReason, TextChunk, ThoughtChunk, ToolOutput
41
42
 
42
43
  if TYPE_CHECKING:
43
44
  from mirascope.llm import UserContent
@@ -513,6 +514,7 @@ class ConversationFacilitator(SettingsAware):
513
514
  Returns:
514
515
  Structured response matching response_model
515
516
  """
517
+ history_before = len(self._messages)
516
518
  self._messages.append(llm.messages.user(user_content))
517
519
  self.last_tool_calls = []
518
520
  self.last_tool_results = []
@@ -567,6 +569,10 @@ class ConversationFacilitator(SettingsAware):
567
569
  # Mirascope's own request, so nothing in `use_brain` marks it).
568
570
  with conversational_round():
569
571
  response = await response.resume(tool_outputs)
572
+ # Mirascope's own request, below `use_brain`: a provider that
573
+ # does not raise on a refusal itself only says so here.
574
+ if getattr(response, "finish_reason", None) == FinishReason.REFUSAL:
575
+ raise ModelRefusal(getattr(response, "model_id", "unknown"))
570
576
 
571
577
  # Sync full conversation history from the response chain
572
578
  self._messages = list(response.messages)
@@ -580,6 +586,9 @@ class ConversationFacilitator(SettingsAware):
580
586
 
581
587
  # Extract structured response
582
588
  return await self._call_with_response_model(response_model)
589
+ except ModelRefusal:
590
+ self._forget_refused_turn(history_before)
591
+ raise
583
592
  finally:
584
593
  if epoch == self._turn_epoch:
585
594
  self.last_submit_seconds = time.monotonic() - started
@@ -631,6 +640,7 @@ class ConversationFacilitator(SettingsAware):
631
640
  #: with a later reading, or every ordinary turn would silently absorb
632
641
  #: however long the consumer took to come back for the last event.
633
642
  recorded = False
643
+ history_before = len(self._messages)
634
644
  # Held in a name so it can be closed; see the docstring.
635
645
  rounds = self._stream_turn(
636
646
  response_model,
@@ -644,6 +654,9 @@ class ConversationFacilitator(SettingsAware):
644
654
  self.last_submit_seconds = time.monotonic() - started
645
655
  recorded = True
646
656
  yield event
657
+ except ModelRefusal:
658
+ self._forget_refused_turn(history_before)
659
+ raise
647
660
  finally:
648
661
  # Before the close, which can raise: the figure is the point.
649
662
  if not recorded and epoch == self._turn_epoch:
@@ -802,6 +815,13 @@ class ConversationFacilitator(SettingsAware):
802
815
  first_chunk_at=first_chunk_at,
803
816
  yielded_s=yielded_s,
804
817
  )
818
+ if getattr(stream, "finish_reason", None) == FinishReason.REFUSAL:
819
+ # The awaited path raises at the provider; a stream only says so
820
+ # in its last chunk, after any text has been yielded. The host
821
+ # has shown a partial reply and should replace it — the
822
+ # exception is the signal. No `stop_details` on this path.
823
+ raise ModelRefusal(getattr(stream, "model_id", "unknown"))
824
+
805
825
  if self.last_submit_first_delta_s is None and first_delta_at is not None:
806
826
  # From the TURN's start, not this round's: the person has been
807
827
  # waiting since they hit send, through the context re-render's
@@ -900,6 +920,17 @@ class ConversationFacilitator(SettingsAware):
900
920
 
901
921
  # --- Internal helpers ---
902
922
 
923
+ def _forget_refused_turn(self, history_before: int) -> None:
924
+ """Drop the refused turn from history, user message included.
925
+
926
+ A refused exchange left in history is the context the next request is
927
+ refused in again; the provider's guidance on a refusal is to change or
928
+ drop the turn that triggered it. The turn did not happen, so history
929
+ returns to where it stood before it — the host still has the person's
930
+ words and can offer them back for rephrasing.
931
+ """
932
+ del self._messages[history_before:]
933
+
903
934
  #: Answer written into the synthetic tool_result when the tool loop is cut
904
935
  #: short. Addressed to the model, because the model reads it: it must
905
936
  #: understand that the tool did not run and that it should wrap up with
@@ -0,0 +1,42 @@
1
+ """
2
+ Provider verdicts that are not transport faults.
3
+
4
+ `ModelRefusal` is raised when the model stops with ``stop_reason: "refusal"``
5
+ (the provider's policy classifiers intervened). It is deliberately its own
6
+ type: before it existed a refusal reached the framework as an empty or cut-off
7
+ reply, so a structured call re-asked it as a ParseError (ten times) and a
8
+ conversational turn fell through to a second, structured call that was refused
9
+ again. A refusal is the provider's answer to THIS request, so nothing retries
10
+ it; the caller decides — a concern fails soft as on any error, a host shows the
11
+ person something and lets them rephrase.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Optional
17
+
18
+
19
+ class ModelRefusal(RuntimeError):
20
+ """The model declined the request (``stop_reason: "refusal"``).
21
+
22
+ ``category`` and ``explanation`` are the provider's ``stop_details`` when it
23
+ sends them (``"cyber"``, ``"bio"``, or None for an unnamed category); a
24
+ streamed round does not carry them and leaves both None. The explanation
25
+ text is not guaranteed stable by the provider — show it, never branch on it.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ model: str,
31
+ *,
32
+ category: Optional[str] = None,
33
+ explanation: Optional[str] = None,
34
+ ) -> None:
35
+ self.model = model
36
+ self.category = category
37
+ self.explanation = explanation
38
+ detail = f" ({category})" if category else ""
39
+ super().__init__(
40
+ f"Model {model} refused the request{detail}"
41
+ + (f": {explanation}" if explanation else "")
42
+ )
@@ -16,10 +16,16 @@ from mirascope.llm.responses import AsyncResponse, AsyncStreamResponse, Response
16
16
  from typing_extensions import Unpack
17
17
 
18
18
  from dialectical_framework.enums.di import DI
19
+ from dialectical_framework.exceptions.provider_errors import ModelRefusal
19
20
  from dialectical_framework.settings import Settings
21
+ from dialectical_framework.utils.format_compat import (
22
+ learn_format_mode_from_error,
23
+ with_format_compat,
24
+ )
20
25
  from dialectical_framework.utils.thinking_compat import (
21
26
  learn_thinking_shape_from_error,
22
27
  with_thinking_compat,
28
+ without_earlier_turn_thinking,
23
29
  )
24
30
 
25
31
  if TYPE_CHECKING:
@@ -288,6 +294,62 @@ def _bedrock_model_name(model_id: str) -> str:
288
294
  return model_id.removeprefix("bedrock/").removeprefix("anthropic/")
289
295
 
290
296
 
297
+ class _LearningStreamManager:
298
+ """`messages.stream(...)` that learns the thinking shape from a 400, once.
299
+
300
+ The request is made when the manager is ENTERED (Mirascope's decoder does
301
+ `async with` on the first chunk), before anything has been yielded, so a
302
+ rejected shape can be re-sent without replaying a token — and the streamed
303
+ conversational round is exactly where an unset level goes out as "off",
304
+ so without this a wrong name heuristic would fail every streamed turn until
305
+ some awaited call taught the process. Thinking only: a learned FORMAT mode
306
+ would change the response's parser, which the caller already holds.
307
+ """
308
+
309
+ def __init__(
310
+ self, client: Any, kwargs: dict[str, Any], params: Mapping[str, Any]
311
+ ) -> None:
312
+ self._client = client
313
+ self._kwargs = kwargs
314
+ self._params = params
315
+ self._manager: Any = None
316
+
317
+ def _open(self) -> Any:
318
+ return self._client.messages.stream(
319
+ **with_thinking_compat(self._kwargs["model"], self._kwargs, self._params)
320
+ )
321
+
322
+ async def __aenter__(self) -> Any:
323
+ self._manager = self._open()
324
+ try:
325
+ return await self._manager.__aenter__()
326
+ except Exception as e:
327
+ if not learn_thinking_shape_from_error(self._kwargs["model"], e):
328
+ raise
329
+ self._manager = self._open()
330
+ return await self._manager.__aenter__()
331
+
332
+ async def __aexit__(self, *exc: Any) -> Any:
333
+ return await self._manager.__aexit__(*exc)
334
+
335
+
336
+ def raise_on_refusal(response: Any, model_name: str) -> None:
337
+ """Raise `ModelRefusal` when the provider stopped on a refusal.
338
+
339
+ Raised at the provider, below `use_brain`, so no retry ladder ever re-asks
340
+ it (an unknown exception re-raises there) and the tool loop's resumes —
341
+ Mirascope's own requests, which reach this provider too — are covered.
342
+ """
343
+ if getattr(response, "stop_reason", None) != "refusal":
344
+ return
345
+ details = getattr(response, "stop_details", None)
346
+ raise ModelRefusal(
347
+ model_name,
348
+ category=getattr(details, "category", None),
349
+ explanation=getattr(details, "explanation", None),
350
+ )
351
+
352
+
291
353
  class BedrockAnthropicProvider(AnthropicProvider):
292
354
  """Mirascope v2 provider that routes through AnthropicBedrock client (async-native).
293
355
 
@@ -305,6 +367,53 @@ class BedrockAnthropicProvider(AnthropicProvider):
305
367
  self.async_client = AsyncAnthropicBedrock(timeout=timeout)
306
368
  self._beta_provider = None
307
369
 
370
+ @staticmethod
371
+ def _encode(
372
+ model_id: str,
373
+ messages: Sequence[Message],
374
+ toolkit: AsyncToolkit | Toolkit,
375
+ format: FormatSpec[FormattableT] | None,
376
+ params: Mapping[str, Any],
377
+ ) -> tuple[Any, Any, dict[str, Any]]:
378
+ """Encode a request in the formatting mode this model can take.
379
+
380
+ Forced tool use is re-asked as JSON for a model that refuses it (Claude
381
+ 5.5); see format_compat. Encoding happens here, BEFORE the request is
382
+ built, because the mode decides the request's shape — tools and
383
+ tool_choice for one, system instructions for the other — and the
384
+ decoded response's parser follows the resolved format. Earlier turns'
385
+ thinking blocks are dropped here too, for a model that binds them to
386
+ their prefix (see `without_earlier_turn_thinking`).
387
+
388
+ A call moved OFF forced tool use is also sent with thinking off unless
389
+ it asked for thinking: forced tool choice is what kept every structured
390
+ call from thinking by default (0 thinking tokens with no parameter,
391
+ 2026-09-24), and JSON mode with nothing sent is the provider's default,
392
+ thinking on. Without this every concern on a 5.5 model would think —
393
+ a cost, latency and behaviour change no figure was taken under.
394
+ `with_thinking_compat` turns "disabled" into the model's own off.
395
+ """
396
+ model_name = _bedrock_model_name(model_id)
397
+ compatible = with_format_compat(model_name, format)
398
+ input_messages, resolved_format, kwargs = _utils.encode_request(
399
+ model_id=model_id,
400
+ messages=messages,
401
+ tools=toolkit,
402
+ format=compatible,
403
+ params=params,
404
+ )
405
+ kwargs["model"] = model_name
406
+ # A structured call is never a round of the tool loop: no tools, and in
407
+ # JSON mode extra system text, so even THIS turn's blocks are bound to a
408
+ # prefix the request no longer has. Drop them all there.
409
+ kwargs["messages"] = without_earlier_turn_thinking(
410
+ model_name, kwargs["messages"], every_turn=format is not None
411
+ )
412
+ if compatible is not format and "thinking" not in kwargs:
413
+ kwargs["thinking"] = {"type": "disabled"}
414
+ _fix_cache_breakpoints(kwargs)
415
+ return input_messages, resolved_format, kwargs
416
+
308
417
  async def _create_async(
309
418
  self, kwargs: dict[str, Any], params: Mapping[str, Any]
310
419
  ) -> AnthropicMessage:
@@ -353,18 +462,23 @@ class BedrockAnthropicProvider(AnthropicProvider):
353
462
  **params: Unpack[Params],
354
463
  ) -> AsyncResponse | AsyncResponse[FormattableT]:
355
464
  """Always use standard path — bedrock doesn't support beta structured outputs."""
356
- input_messages, resolved_format, kwargs = _utils.encode_request(
357
- model_id=model_id,
358
- messages=messages,
359
- tools=toolkit,
360
- format=format,
361
- params=params,
362
- )
363
- kwargs["model"] = _bedrock_model_name(model_id)
364
- _fix_cache_breakpoints(kwargs)
365
- anthropic_response = cast(
366
- AnthropicMessage, await self._create_async(kwargs, params)
465
+ input_messages, resolved_format, kwargs = self._encode(
466
+ model_id, messages, toolkit, format, params
367
467
  )
468
+ try:
469
+ anthropic_response = cast(
470
+ AnthropicMessage, await self._create_async(kwargs, params)
471
+ )
472
+ except Exception as e:
473
+ if not learn_format_mode_from_error(kwargs["model"], format, e):
474
+ raise
475
+ input_messages, resolved_format, kwargs = self._encode(
476
+ model_id, messages, toolkit, format, params
477
+ )
478
+ anthropic_response = cast(
479
+ AnthropicMessage, await self._create_async(kwargs, params)
480
+ )
481
+ raise_on_refusal(anthropic_response, kwargs["model"])
368
482
  include_thoughts = _utils.get_include_thoughts(params)
369
483
  assistant_message, finish_reason, usage = _utils.decode_response(
370
484
  anthropic_response, model_id, include_thoughts=include_thoughts
@@ -393,21 +507,10 @@ class BedrockAnthropicProvider(AnthropicProvider):
393
507
  **params: Unpack[Params],
394
508
  ) -> AsyncStreamResponse | AsyncStreamResponse[FormattableT]:
395
509
  """Stream responses from Bedrock Anthropic."""
396
- input_messages, resolved_format, kwargs = _utils.encode_request(
397
- model_id=model_id,
398
- messages=messages,
399
- tools=toolkit,
400
- format=format,
401
- params=params,
510
+ input_messages, resolved_format, kwargs = self._encode(
511
+ model_id, messages, toolkit, format, params
402
512
  )
403
- kwargs["model"] = _bedrock_model_name(model_id)
404
- _fix_cache_breakpoints(kwargs)
405
- # Streaming cannot learn from a 400: the error surfaces when the caller
406
- # consumes the iterator, by which point retrying would replay tokens
407
- # already yielded. The name heuristic is applied, and a genuine mismatch
408
- # raises to the caller.
409
- kwargs = with_thinking_compat(kwargs["model"], kwargs, params)
410
- anthropic_stream = self.async_client.messages.stream(**kwargs)
513
+ anthropic_stream = _LearningStreamManager(self.async_client, kwargs, params)
411
514
  include_thoughts = _utils.get_include_thoughts(params)
412
515
  chunk_iterator = _utils.decode_async_stream(
413
516
  anthropic_stream, include_thoughts=include_thoughts
@@ -433,16 +536,23 @@ class BedrockAnthropicProvider(AnthropicProvider):
433
536
  **params: Unpack[Params],
434
537
  ) -> Response | Response[FormattableT]:
435
538
  """Always use standard path — bedrock doesn't support beta structured outputs."""
436
- input_messages, resolved_format, kwargs = _utils.encode_request(
437
- model_id=model_id,
438
- messages=messages,
439
- tools=toolkit,
440
- format=format,
441
- params=params,
539
+ input_messages, resolved_format, kwargs = self._encode(
540
+ model_id, messages, toolkit, format, params
442
541
  )
443
- kwargs["model"] = _bedrock_model_name(model_id)
444
- _fix_cache_breakpoints(kwargs)
445
- anthropic_response = cast(AnthropicMessage, self._create_sync(kwargs, params))
542
+ try:
543
+ anthropic_response = cast(
544
+ AnthropicMessage, self._create_sync(kwargs, params)
545
+ )
546
+ except Exception as e:
547
+ if not learn_format_mode_from_error(kwargs["model"], format, e):
548
+ raise
549
+ input_messages, resolved_format, kwargs = self._encode(
550
+ model_id, messages, toolkit, format, params
551
+ )
552
+ anthropic_response = cast(
553
+ AnthropicMessage, self._create_sync(kwargs, params)
554
+ )
555
+ raise_on_refusal(anthropic_response, kwargs["model"])
446
556
  include_thoughts = _utils.get_include_thoughts(params)
447
557
  assistant_message, finish_reason, usage = _utils.decode_response(
448
558
  anthropic_response, model_id, include_thoughts=include_thoughts
@@ -0,0 +1,136 @@
1
+ """
2
+ Per-model compatibility for how a STRUCTURED call asks for its DTO.
3
+
4
+ Mirascope's default formatting mode is forced tool use (``tool_choice`` of type
5
+ ``tool``, or ``any`` with several tools), and every concern has always used it.
6
+ Claude 5.5 models refuse it on Bedrock — reported 2026-10-08 on Sonnet 5.5 and
7
+ Opus 5.5, and probed the same day on Fable 5.1 (Fable 5 still accepts it): ``tool_choice: type "tool" and "any" are not supported for this
8
+ model`` — so with a 5.5 model configured every structured call 400s: the
9
+ coherence judge, the tetrad writers, every concern. JSON mode works on both
10
+ (it asks for the JSON in the reply instead), and it is the shape the framework
11
+ already uses where a structured call must think (`format_mode="json"`).
12
+
13
+ The mode is therefore chosen by MODEL at the provider, the way
14
+ `thinking_compat` chooses the thinking shape: a request that asked for forced
15
+ tool use, explicitly or by default, is sent in JSON mode to a model that cannot
16
+ take it. A request that asked for another mode (json, strict, parser) is left
17
+ alone. Selection is by model name with a learned fallback — if the heuristic
18
+ misses a future model, its first 400 teaches the process and the call is
19
+ re-encoded once.
20
+
21
+ What this does NOT decide: whether JSON mode is the better default for models
22
+ that accept both. It parses as reliably on a DTO-shaped call and costs less
23
+ prefill (`probe_format_mode_thinking.py`), but the framework's measured figures
24
+ were taken on forced tool use, so switching every model is a measurement, not
25
+ a compatibility fix.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import logging
31
+ import re
32
+ from typing import Any, Optional, get_origin
33
+
34
+ from mirascope import llm
35
+ from mirascope.llm.formatting.format import Format
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ #: The mode substituted for forced tool use on a model that refuses it.
40
+ JSON = "json"
41
+
42
+ #: Model name -> True when it was LEARNED (from a 400) to refuse forced tool use.
43
+ _LEARNED: dict[str, bool] = {}
44
+
45
+ #: ``claude-<family>-<major>[-<minor>]``. The minor is one or two digits NOT
46
+ #: followed by another digit, so a date suffix is never read as a minor:
47
+ #: ``claude-haiku-4-5-20251001`` is 4.5, ``claude-sonnet-5`` is 5.0,
48
+ #: ``claude-sonnet-5-5`` is 5.5, and ``claude-3-5-sonnet`` does not match.
49
+ _FAMILY_VERSION = re.compile(r"claude-([a-z]+)-(\d+)(?:-(\d{1,2})(?!\d))?")
50
+
51
+
52
+ def claude_version(model_name: str) -> Optional[tuple[int, int]]:
53
+ """(major, minor) of a Claude 4+-style model name; None when unreadable."""
54
+ match = _FAMILY_VERSION.search(model_name)
55
+ if not match:
56
+ return None
57
+ return int(match.group(2)), int(match.group(3) or 0)
58
+
59
+
60
+ def claude_family(model_name: str) -> Optional[str]:
61
+ """``sonnet`` / ``opus`` / ... of a Claude 4+-style model name."""
62
+ match = _FAMILY_VERSION.search(model_name)
63
+ return match.group(1) if match else None
64
+
65
+
66
+ def refuses_forced_tool_use(model_name: str) -> bool:
67
+ """True when this model rejects ``tool_choice`` of type ``tool``/``any``."""
68
+ if model_name in _LEARNED:
69
+ return _LEARNED[model_name]
70
+ version = claude_version(model_name)
71
+ if version is None:
72
+ return False
73
+ # Probed 2026-10-08: Sonnet/Opus 5.5 and Fable 5.1 refuse; Fable 5 accepts.
74
+ if claude_family(model_name) in ("fable", "mythos"):
75
+ return version >= (5, 1)
76
+ return version >= (5, 5)
77
+
78
+
79
+ def _forced_tool(format: Any) -> Any:
80
+ """The formattable when `format` asks for forced tool use, else None.
81
+
82
+ A bare DTO class resolves to Mirascope's default mode, which is ``tool``; a
83
+ `Format` says its own mode. An `OutputParser` is never forced tool use.
84
+ """
85
+ if isinstance(format, Format):
86
+ return format.formattable if format.mode == "tool" else None
87
+ # A class, or a parameterised generic (`list[str]`), which is not a `type`
88
+ # but resolves to the default mode exactly as one does.
89
+ if isinstance(format, type) or get_origin(format) is not None:
90
+ return format
91
+ return None
92
+
93
+
94
+ def with_format_compat(model_name: str, format: Any) -> Any:
95
+ """`format` as this model can take it. Pure; None passes through."""
96
+ if format is None or not refuses_forced_tool_use(model_name):
97
+ return format
98
+ formattable = _forced_tool(format)
99
+ if formattable is None:
100
+ return format
101
+ return llm.format(formattable, mode=JSON)
102
+
103
+
104
+ def learn_format_mode_from_error(
105
+ model_name: str, format: Any, error: BaseException
106
+ ) -> bool:
107
+ """Record that this model refuses forced tool use. True if worth retrying once.
108
+
109
+ Only when the failed request asked for forced tool use and the error says
110
+ so; every other error returns False and surfaces unchanged.
111
+ """
112
+ if format is None or _forced_tool(format) is None:
113
+ return False
114
+ message = str(error)
115
+ # The provider's text: 'tool_choice: type "tool" and "any" are not
116
+ # supported for this model.' Matched on the forced types, so a 400 about
117
+ # some other tool_choice field cannot move a capable model to JSON.
118
+ if "tool_choice" not in message or "not supported" not in message:
119
+ return False
120
+ if '"tool"' not in message and '"any"' not in message:
121
+ return False
122
+ if refuses_forced_tool_use(model_name):
123
+ # Already sent as JSON and it still failed — the mode is not the problem.
124
+ return False
125
+ _LEARNED[model_name] = True
126
+ logger.warning(
127
+ "Model %s rejected forced tool use; structured calls go in JSON mode for "
128
+ "the rest of this process. Adjust format_compat if this is a naming gap.",
129
+ model_name,
130
+ )
131
+ return True
132
+
133
+
134
+ def reset_learned_format_modes() -> None:
135
+ """Test seam — the learned map is process-global by design."""
136
+ _LEARNED.clear()