dialectical-framework 2.0.6__tar.gz → 2.0.8__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.6 → dialectical_framework-2.0.8}/PKG-INFO +1 -1
  2. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/pyproject.toml +1 -1
  3. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/advisor.py +59 -27
  4. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/consultant/consultant.py +39 -3
  5. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/conversation_facilitator.py +32 -1
  6. dialectical_framework-2.0.8/src/dialectical_framework/exceptions/provider_errors.py +42 -0
  7. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/views.py +32 -0
  8. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/bedrock_provider.py +157 -34
  9. dialectical_framework-2.0.8/src/dialectical_framework/utils/format_compat.py +176 -0
  10. dialectical_framework-2.0.8/src/dialectical_framework/utils/thinking_compat.py +401 -0
  11. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/use_brain.py +17 -0
  12. dialectical_framework-2.0.6/src/dialectical_framework/utils/thinking_compat.py +0 -203
  13. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/LICENSE +0 -0
  14. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/PATENTS +0 -0
  15. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/README.md +0 -0
  16. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/__init__.py +0 -0
  17. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/__init__.py +0 -0
  18. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/__init__.py +0 -0
  19. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/build_policy.py +0 -0
  20. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/migration.py +0 -0
  21. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/reply_hygiene.py +0 -0
  22. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/system_prompts.py +0 -0
  23. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/tools/__init__.py +0 -0
  24. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/tools/anchor.py +0 -0
  25. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/tools/deepen.py +0 -0
  26. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/tools/explore.py +0 -0
  27. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/tools/ingest.py +0 -0
  28. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/tools/note.py +0 -0
  29. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/tools/record_decision.py +0 -0
  30. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/tools/scoped.py +0 -0
  31. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/advisor/tools/sync.py +0 -0
  32. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/agent_context.py +0 -0
  33. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/__init__.py +0 -0
  34. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/analyst.py +0 -0
  35. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/skills/__init__.py +0 -0
  36. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/skills/anchor_theses.py +0 -0
  37. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/skills/edit_perspective.py +0 -0
  38. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/skills/expand_polarities.py +0 -0
  39. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/skills/find_polarities.py +0 -0
  40. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/skills/introduce_polarity.py +0 -0
  41. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/skills/sketch_tetrad.py +0 -0
  42. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/skills/surface_theses.py +0 -0
  43. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/system_prompts.py +0 -0
  44. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/tools/__init__.py +0 -0
  45. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/analyst/tools/place_statement.py +0 -0
  46. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/app_spec.py +0 -0
  47. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/apps.py +0 -0
  48. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/consultant/__init__.py +0 -0
  49. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/execution_report.py +0 -0
  50. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/explorer/__init__.py +0 -0
  51. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/explorer/explorer.py +0 -0
  52. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/explorer/skills/__init__.py +0 -0
  53. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/explorer/skills/build_wheels.py +0 -0
  54. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/explorer/skills/explore_transformations.py +0 -0
  55. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/explorer/skills/generate_synthesis.py +0 -0
  56. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/explorer/system_prompts.py +0 -0
  57. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/explorer/tools/__init__.py +0 -0
  58. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/explorer/tools/create_nexus.py +0 -0
  59. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/explorer/tools/expand_nexus.py +0 -0
  60. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/explorer/tools/generate_synthesis.py +0 -0
  61. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/explorer/tools/present_exploration.py +0 -0
  62. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/__init__.py +0 -0
  63. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/tools/__init__.py +0 -0
  64. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/tools/add_input.py +0 -0
  65. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/tools/audit_feasibility.py +0 -0
  66. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/tools/create_dx_input.py +0 -0
  67. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/tools/digest_input.py +0 -0
  68. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/tools/discard.py +0 -0
  69. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/tools/get_schema.py +0 -0
  70. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/tools/inspect_node.py +0 -0
  71. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/tools/present_analysis.py +0 -0
  72. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/tools/query_graph.py +0 -0
  73. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/tools/read_digest.py +0 -0
  74. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/orchestrator/tools/read_input.py +0 -0
  75. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/reasonable_concern.py +0 -0
  76. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/stream_events.py +0 -0
  77. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/toolsets.py +0 -0
  78. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/agents/turn_timing.py +0 -0
  79. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/__init__.py +0 -0
  80. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/ac_re_taxonomy.py +0 -0
  81. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/action_extraction.py +0 -0
  82. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/add_input.py +0 -0
  83. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/ai_dto/__init__.py +0 -0
  84. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/ai_dto/statement_dto.py +0 -0
  85. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/ai_dto/statements_deck_dto.py +0 -0
  86. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/antithesis_classification.py +0 -0
  87. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/antithesis_extraction.py +0 -0
  88. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/antithetical_thesis_detection.py +0 -0
  89. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/aspect_classification.py +0 -0
  90. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/aspect_generation.py +0 -0
  91. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/build_status.py +0 -0
  92. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/causality/__init__.py +0 -0
  93. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/causality/causality_estimator.py +0 -0
  94. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/causality/causality_estimator_balanced.py +0 -0
  95. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/causality/causality_estimator_criteria.py +0 -0
  96. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/causality/causality_estimator_desirable.py +0 -0
  97. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/causality/causality_estimator_feasible.py +0 -0
  98. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/causality/causality_estimator_realistic.py +0 -0
  99. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/causality/causality_normalizer.py +0 -0
  100. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/causality/estimator_resolver.py +0 -0
  101. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/causality_estimation.py +0 -0
  102. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/control_statements_check.py +0 -0
  103. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/create_dx_input.py +0 -0
  104. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/create_nexus.py +0 -0
  105. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/decision_coherence_check.py +0 -0
  106. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/decision_confirmation_check.py +0 -0
  107. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/diagonal_oppositions_check.py +0 -0
  108. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/dialectical_context.py +0 -0
  109. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/discard.py +0 -0
  110. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/display_text_edit.py +0 -0
  111. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/expand_nexus.py +0 -0
  112. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/perspective_combination.py +0 -0
  113. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/perspective_validation.py +0 -0
  114. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/positive_ac_re_apex_derivation.py +0 -0
  115. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/record_decision.py +0 -0
  116. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/scoring_scales.py +0 -0
  117. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/source_digest.py +0 -0
  118. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/statement_classification.py +0 -0
  119. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/statement_deduplication.py +0 -0
  120. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/statement_headline.py +0 -0
  121. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/statement_placement.py +0 -0
  122. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/synthesis_generation.py +0 -0
  123. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/tetrad_candidates.py +0 -0
  124. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/tetrad_grounding.py +0 -0
  125. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/tetrad_sketch.py +0 -0
  126. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/thesis_extraction.py +0 -0
  127. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/transformation_audit.py +0 -0
  128. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/transformation_generation.py +0 -0
  129. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/transformation_sketch.py +0 -0
  130. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/concerns/view_sketch.py +0 -0
  131. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/dialectical_reasoning.py +0 -0
  132. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/enums/__init__.py +0 -0
  133. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/enums/causality_preset.py +0 -0
  134. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/enums/di.py +0 -0
  135. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/events/__init__.py +0 -0
  136. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/events/graph_event.py +0 -0
  137. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/events/graph_event_bus.py +0 -0
  138. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/events/progress_event.py +0 -0
  139. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/exceptions/__init__.py +0 -0
  140. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/exceptions/node_errors.py +0 -0
  141. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/exceptions/resolver_errors.py +0 -0
  142. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/__init__.py +0 -0
  143. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/composite_input_resolver.py +0 -0
  144. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/dialexity_input_resolver.py +0 -0
  145. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/estimation_manager.py +0 -0
  146. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/mixins/__init__.py +0 -0
  147. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/mixins/incremental_build_mixin.py +0 -0
  148. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/mixins/intent_mixin.py +0 -0
  149. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/mixins/persistable_mixin.py +0 -0
  150. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/__init__.py +0 -0
  151. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/assessable_entity.py +0 -0
  152. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/base_node.py +0 -0
  153. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/case.py +0 -0
  154. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/cycle.py +0 -0
  155. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/decision.py +0 -0
  156. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/estimation.py +0 -0
  157. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/ideas.py +0 -0
  158. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/input.py +0 -0
  159. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/nexus.py +0 -0
  160. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/note.py +0 -0
  161. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/perspective.py +0 -0
  162. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/polarity.py +0 -0
  163. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/rationale.py +0 -0
  164. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/statement.py +0 -0
  165. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/synthesis.py +0 -0
  166. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/transformation.py +0 -0
  167. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/transition.py +0 -0
  168. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/nodes/wheel.py +0 -0
  169. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationship_manager.py +0 -0
  170. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/__init__.py +0 -0
  171. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/action_reflection_relationship.py +0 -0
  172. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/belongs_to_cycle_relationship.py +0 -0
  173. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/belongs_to_nexus_relationship.py +0 -0
  174. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/changed_to_relationship.py +0 -0
  175. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/contradiction_of_relationship.py +0 -0
  176. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/critiques_relationship.py +0 -0
  177. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/distilled_to_relationship.py +0 -0
  178. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/estimates_relationship.py +0 -0
  179. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/explains_relationship.py +0 -0
  180. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/grounded_in_relationship.py +0 -0
  181. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/has_input_relationship.py +0 -0
  182. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/has_statement_relationship.py +0 -0
  183. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/has_wheel_relationship.py +0 -0
  184. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/immutable_structure.py +0 -0
  185. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/is_source_of_relationship.py +0 -0
  186. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/is_target_of_relationship.py +0 -0
  187. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/negative_side_of_relationship.py +0 -0
  188. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/opposite_direction_relationship.py +0 -0
  189. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/opposite_of_relationship.py +0 -0
  190. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/polarity_relationship.py +0 -0
  191. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/positive_side_of_relationship.py +0 -0
  192. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/provides_relationship.py +0 -0
  193. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/relationships/synthesis_of_relationship.py +0 -0
  194. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/rendering.py +0 -0
  195. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/__init__.py +0 -0
  196. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/case_repository.py +0 -0
  197. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/cycle_repository.py +0 -0
  198. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/decision_repository.py +0 -0
  199. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/input_repository.py +0 -0
  200. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/nexus_repository.py +0 -0
  201. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/node_repository.py +0 -0
  202. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/note_repository.py +0 -0
  203. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/perspective_repository.py +0 -0
  204. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/polarity_repository.py +0 -0
  205. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/schema_repository.py +0 -0
  206. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/statement_repository.py +0 -0
  207. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/transformation_repository.py +0 -0
  208. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/repositories/wheel_repository.py +0 -0
  209. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/scope_context.py +0 -0
  210. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/verbatim_input_resolver.py +0 -0
  211. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/wheel_segment.py +0 -0
  212. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/graph/wheel_segment_polar_pair.py +0 -0
  213. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/protocols/__init__.py +0 -0
  214. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/protocols/has_config.py +0 -0
  215. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/protocols/input_resolver.py +0 -0
  216. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/py.typed +0 -0
  217. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/settings.py +0 -0
  218. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/settings_context.py +0 -0
  219. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/__init__.py +0 -0
  220. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/async_drain.py +0 -0
  221. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/call_census.py +0 -0
  222. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/chunking.py +0 -0
  223. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/concurrency.py +0 -0
  224. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/dc_replace.py +0 -0
  225. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/decompose_probability_uniformly.py +0 -0
  226. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/edge_context.py +0 -0
  227. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/effect_logger.py +0 -0
  228. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/input_context.py +0 -0
  229. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/order_transitions.py +0 -0
  230. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/progress.py +0 -0
  231. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/retry_accounting.py +0 -0
  232. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/src/dialectical_framework/utils/sequence_generation.py +0 -0
  233. {dialectical_framework-2.0.6 → dialectical_framework-2.0.8}/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.6
3
+ Version: 2.0.8
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.6"
3
+ version = "2.0.8"
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
 
@@ -36,6 +36,7 @@ dropped from BOTH the product and the baseline.
36
36
 
37
37
  from __future__ import annotations
38
38
 
39
+ import logging
39
40
  import re
40
41
  from contextlib import aclosing
41
42
  from typing import Any, AsyncGenerator, Optional
@@ -51,7 +52,14 @@ from dialectical_framework.agents.conversation_facilitator import (
51
52
  FROM_SETTINGS, ConversationFacilitator)
52
53
  from dialectical_framework.agents.stream_events import StreamEvent
53
54
  from dialectical_framework.agents.turn_timing import TurnTiming
54
- from dialectical_framework.graph.views import ExplorationView, PerspectiveView
55
+ from dialectical_framework.exceptions.provider_errors import ModelRefusal
56
+ from dialectical_framework.graph.views import (
57
+ ExplorationView,
58
+ FailedDrawView,
59
+ PerspectiveView,
60
+ )
61
+
62
+ logger = logging.getLogger(__name__)
55
63
 
56
64
  #: Tool verbs → mental acts. These are REWRITES, not drops: dropping every
57
65
  #: paragraph that mentions a tool name would also delete the discrimination test
@@ -135,6 +143,16 @@ _TOOL_TOKENS = (
135
143
  )
136
144
 
137
145
 
146
+ def _failed_draw(error: BaseException) -> FailedDrawView:
147
+ """A failed draw's record: the exception's type, the provider's refusal
148
+ category when it is a `ModelRefusal` that carries one, and its text."""
149
+ return FailedDrawView(
150
+ kind=type(error).__name__,
151
+ category=getattr(error, "category", None) if isinstance(error, ModelRefusal) else None,
152
+ message=str(error) or None,
153
+ )
154
+
155
+
138
156
  def _apply_rewrites(section: str) -> tuple[str, list[str]]:
139
157
  """Apply the rewrite table whitespace-insensitively.
140
158
 
@@ -425,8 +443,22 @@ class Consultant:
425
443
  # measured cold-start rate.
426
444
  drawn = await asyncio.gather(*(draw() for _ in range(n)), return_exceptions=True)
427
445
  sketches = [d for d in drawn if not isinstance(d, BaseException)]
446
+ failures = [d for d in drawn if isinstance(d, BaseException)]
447
+ # A failed draw is a finding, not noise: it costs the card a candidate
448
+ # (or its whole comparison, when two fail) and it costs time before it
449
+ # fails. Said once per failure, by type — the host that counted 8 lost
450
+ # draws in 240 on Opus 5.5 had no cause on record because this branch
451
+ # was silent — and carried on the view as `failed_draws`.
452
+ failed_draws = [_failed_draw(e) for e in failures]
453
+ for failed in failed_draws:
454
+ logger.warning(
455
+ "A view-turn draw failed (%d of %d drawn): %s%s: %s",
456
+ len(sketches), n, failed.kind,
457
+ f" ({failed.category})" if failed.category else "",
458
+ failed.message,
459
+ )
428
460
  if not sketches:
429
- raise next(d for d in drawn if isinstance(d, BaseException))
461
+ raise failures[0]
430
462
  # With several draws the framework's own coherence check picks the one
431
463
  # whose FIRST tension holds best (`concerns/tetrad_candidates.py`); a
432
464
  # draw with no complete first tension sorts last. The card a blindspot
@@ -447,7 +479,11 @@ class Consultant:
447
479
  sketch = sketches[judgeable[0]] if judgeable else sketches[0]
448
480
  else:
449
481
  sketch = sketches[0]
450
- view = dataclasses.replace(exploration_view_from_sketch(sketch), runners_up=runners_up)
482
+ view = dataclasses.replace(
483
+ exploration_view_from_sketch(sketch),
484
+ runners_up=runners_up,
485
+ failed_draws=failed_draws,
486
+ )
451
487
  # What the turn leaves behind is NOT what it sent: the long request and
452
488
  # the DTO come off, and the person's ask plus the drawing in words go
453
489
  # on. Kept as a request→prose pair, the long request taught the next
@@ -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
+ )
@@ -326,6 +326,32 @@ class WheelView:
326
326
  return dataclasses.asdict(self)
327
327
 
328
328
 
329
+ @dataclass(frozen=True)
330
+ class FailedDrawView:
331
+ """One best-of-N draw that produced nothing — what failed, by type.
332
+
333
+ A drawn view is best-of-N (`Consultant.exploration_view(attempts=)`), and a
334
+ draw that raises used to vanish: the gather dropped it, nothing logged it,
335
+ and the census never saw a call it could not record. A host measured the
336
+ gap as 8 of 240 draws on Opus 5.5 with no cause on record. `kind` is the
337
+ exception's class name (`ModelRefusal`, `ParseError`, a provider error);
338
+ `category` is the provider's refusal category when the kind is a refusal
339
+ and it sent one; `message` is the exception's text, for a log, never for
340
+ a screen — `without_terminology()` drops it, since a parse error names DTO
341
+ fields.
342
+ """
343
+
344
+ kind: str
345
+ category: Optional[str] = None
346
+ message: Optional[str] = None
347
+
348
+ def without_terminology(self) -> FailedDrawView:
349
+ return dataclasses.replace(self, message=None)
350
+
351
+ def to_dict(self) -> dict[str, Any]:
352
+ return dataclasses.asdict(self)
353
+
354
+
329
355
  @dataclass(frozen=True)
330
356
  class ExplorationView:
331
357
  """The perspectives seen together — an exploration's (a Nexus's) members,
@@ -344,6 +370,11 @@ class ExplorationView:
344
370
  #: host's "again"/"another" can show one without a new reasoning call.
345
371
  #: Empty for a view read off the graph, or drawn once.
346
372
  runners_up: list[PerspectiveView] = field(default_factory=list)
373
+ #: The best-of-N draws that FAILED, in no particular order — so a host can
374
+ #: count them and tell a card chosen among three from one that was the only
375
+ #: draw left. Empty for a view read off the graph. A view with every draw
376
+ #: failed is never returned: the method raises the first failure instead.
377
+ failed_draws: list[FailedDrawView] = field(default_factory=list)
347
378
 
348
379
  def without_terminology(self) -> ExplorationView:
349
380
  return dataclasses.replace(
@@ -351,6 +382,7 @@ class ExplorationView:
351
382
  perspectives=[p.without_terminology() for p in self.perspectives],
352
383
  nexus_hash=None,
353
384
  runners_up=[p.without_terminology() for p in self.runners_up],
385
+ failed_draws=[f.without_terminology() for f in self.failed_draws],
354
386
  )
355
387
 
356
388
  def to_dict(self) -> dict[str, Any]:
@@ -16,10 +16,17 @@ 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
+ structured_output_format,
24
+ with_format_compat,
25
+ )
20
26
  from dialectical_framework.utils.thinking_compat import (
21
27
  learn_thinking_shape_from_error,
22
28
  with_thinking_compat,
29
+ without_earlier_turn_thinking,
23
30
  )
24
31
 
25
32
  if TYPE_CHECKING:
@@ -288,6 +295,62 @@ def _bedrock_model_name(model_id: str) -> str:
288
295
  return model_id.removeprefix("bedrock/").removeprefix("anthropic/")
289
296
 
290
297
 
298
+ class _LearningStreamManager:
299
+ """`messages.stream(...)` that learns the thinking shape from a 400, once.
300
+
301
+ The request is made when the manager is ENTERED (Mirascope's decoder does
302
+ `async with` on the first chunk), before anything has been yielded, so a
303
+ rejected shape can be re-sent without replaying a token — and the streamed
304
+ conversational round is exactly where an unset level goes out as "off",
305
+ so without this a wrong name heuristic would fail every streamed turn until
306
+ some awaited call taught the process. Thinking only: a learned FORMAT mode
307
+ would change the response's parser, which the caller already holds.
308
+ """
309
+
310
+ def __init__(
311
+ self, client: Any, kwargs: dict[str, Any], params: Mapping[str, Any]
312
+ ) -> None:
313
+ self._client = client
314
+ self._kwargs = kwargs
315
+ self._params = params
316
+ self._manager: Any = None
317
+
318
+ def _open(self) -> Any:
319
+ return self._client.messages.stream(
320
+ **with_thinking_compat(self._kwargs["model"], self._kwargs, self._params)
321
+ )
322
+
323
+ async def __aenter__(self) -> Any:
324
+ self._manager = self._open()
325
+ try:
326
+ return await self._manager.__aenter__()
327
+ except Exception as e:
328
+ if not learn_thinking_shape_from_error(self._kwargs["model"], e):
329
+ raise
330
+ self._manager = self._open()
331
+ return await self._manager.__aenter__()
332
+
333
+ async def __aexit__(self, *exc: Any) -> Any:
334
+ return await self._manager.__aexit__(*exc)
335
+
336
+
337
+ def raise_on_refusal(response: Any, model_name: str) -> None:
338
+ """Raise `ModelRefusal` when the provider stopped on a refusal.
339
+
340
+ Raised at the provider, below `use_brain`, so no retry ladder ever re-asks
341
+ it (an unknown exception re-raises there) and the tool loop's resumes —
342
+ Mirascope's own requests, which reach this provider too — are covered.
343
+ """
344
+ if getattr(response, "stop_reason", None) != "refusal":
345
+ return
346
+ details = getattr(response, "stop_details", None)
347
+ raise ModelRefusal(
348
+ model_name,
349
+ category=getattr(details, "category", None),
350
+ explanation=getattr(details, "explanation", None),
351
+ )
352
+
353
+
291
354
  class BedrockAnthropicProvider(AnthropicProvider):
292
355
  """Mirascope v2 provider that routes through AnthropicBedrock client (async-native).
293
356
 
@@ -305,6 +368,65 @@ class BedrockAnthropicProvider(AnthropicProvider):
305
368
  self.async_client = AsyncAnthropicBedrock(timeout=timeout)
306
369
  self._beta_provider = None
307
370
 
371
+ @staticmethod
372
+ def _encode(
373
+ model_id: str,
374
+ messages: Sequence[Message],
375
+ toolkit: AsyncToolkit | Toolkit,
376
+ format: FormatSpec[FormattableT] | None,
377
+ params: Mapping[str, Any],
378
+ ) -> tuple[Any, Any, dict[str, Any]]:
379
+ """Encode a request in the formatting mode this model can take.
380
+
381
+ Forced tool use is re-asked as JSON for a model that refuses it (Claude
382
+ 5.5); see format_compat. Encoding happens here, BEFORE the request is
383
+ built, because the mode decides the request's shape — tools and
384
+ tool_choice for one, system instructions for the other — and the
385
+ decoded response's parser follows the resolved format. A JSON-mode
386
+ call to such a model also carries its schema as a structured output
387
+ (`output_config.format`, see `structured_output_format`). Earlier turns'
388
+ thinking blocks are dropped here too, for a model that binds them to
389
+ their prefix (see `without_earlier_turn_thinking`).
390
+
391
+ A call moved OFF forced tool use is also sent with thinking off unless
392
+ it asked for thinking: forced tool choice is what kept every structured
393
+ call from thinking by default (0 thinking tokens with no parameter,
394
+ 2026-09-24), and JSON mode with nothing sent is the provider's default,
395
+ thinking on. Without this every concern on a 5.5 model would think —
396
+ a cost, latency and behaviour change no figure was taken under.
397
+ `with_thinking_compat` turns "disabled" into the model's own off.
398
+ """
399
+ model_name = _bedrock_model_name(model_id)
400
+ compatible = with_format_compat(model_name, format)
401
+ input_messages, resolved_format, kwargs = _utils.encode_request(
402
+ model_id=model_id,
403
+ messages=messages,
404
+ tools=toolkit,
405
+ format=compatible,
406
+ params=params,
407
+ )
408
+ kwargs["model"] = model_name
409
+ # A structured call is never a round of the tool loop: no tools, and in
410
+ # JSON mode extra system text, so even THIS turn's blocks are bound to a
411
+ # prefix the request no longer has. Drop them all there.
412
+ kwargs["messages"] = without_earlier_turn_thinking(
413
+ model_name, kwargs["messages"], every_turn=format is not None
414
+ )
415
+ if compatible is not format and "thinking" not in kwargs:
416
+ kwargs["thinking"] = {"type": "disabled"}
417
+ # A JSON-mode call to a model whose structured calls go in JSON mode
418
+ # is constrained to its schema (`output_config.format`): JSON asked for
419
+ # in prose came back with trailing commas often enough to cost a whole
420
+ # re-ask per 2.5 cards on Sonnet 5.5 (see format_compat). The thinking
421
+ # layer merges its `effort` into the same `output_config` later.
422
+ constraint = structured_output_format(model_name, compatible)
423
+ if constraint is not None:
424
+ output_config = dict(kwargs.get("output_config") or {})
425
+ output_config["format"] = constraint
426
+ kwargs["output_config"] = output_config
427
+ _fix_cache_breakpoints(kwargs)
428
+ return input_messages, resolved_format, kwargs
429
+
308
430
  async def _create_async(
309
431
  self, kwargs: dict[str, Any], params: Mapping[str, Any]
310
432
  ) -> AnthropicMessage:
@@ -353,18 +475,23 @@ class BedrockAnthropicProvider(AnthropicProvider):
353
475
  **params: Unpack[Params],
354
476
  ) -> AsyncResponse | AsyncResponse[FormattableT]:
355
477
  """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)
478
+ input_messages, resolved_format, kwargs = self._encode(
479
+ model_id, messages, toolkit, format, params
367
480
  )
481
+ try:
482
+ anthropic_response = cast(
483
+ AnthropicMessage, await self._create_async(kwargs, params)
484
+ )
485
+ except Exception as e:
486
+ if not learn_format_mode_from_error(kwargs["model"], format, e):
487
+ raise
488
+ input_messages, resolved_format, kwargs = self._encode(
489
+ model_id, messages, toolkit, format, params
490
+ )
491
+ anthropic_response = cast(
492
+ AnthropicMessage, await self._create_async(kwargs, params)
493
+ )
494
+ raise_on_refusal(anthropic_response, kwargs["model"])
368
495
  include_thoughts = _utils.get_include_thoughts(params)
369
496
  assistant_message, finish_reason, usage = _utils.decode_response(
370
497
  anthropic_response, model_id, include_thoughts=include_thoughts
@@ -393,21 +520,10 @@ class BedrockAnthropicProvider(AnthropicProvider):
393
520
  **params: Unpack[Params],
394
521
  ) -> AsyncStreamResponse | AsyncStreamResponse[FormattableT]:
395
522
  """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,
523
+ input_messages, resolved_format, kwargs = self._encode(
524
+ model_id, messages, toolkit, format, params
402
525
  )
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)
526
+ anthropic_stream = _LearningStreamManager(self.async_client, kwargs, params)
411
527
  include_thoughts = _utils.get_include_thoughts(params)
412
528
  chunk_iterator = _utils.decode_async_stream(
413
529
  anthropic_stream, include_thoughts=include_thoughts
@@ -433,16 +549,23 @@ class BedrockAnthropicProvider(AnthropicProvider):
433
549
  **params: Unpack[Params],
434
550
  ) -> Response | Response[FormattableT]:
435
551
  """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,
552
+ input_messages, resolved_format, kwargs = self._encode(
553
+ model_id, messages, toolkit, format, params
442
554
  )
443
- kwargs["model"] = _bedrock_model_name(model_id)
444
- _fix_cache_breakpoints(kwargs)
445
- anthropic_response = cast(AnthropicMessage, self._create_sync(kwargs, params))
555
+ try:
556
+ anthropic_response = cast(
557
+ AnthropicMessage, self._create_sync(kwargs, params)
558
+ )
559
+ except Exception as e:
560
+ if not learn_format_mode_from_error(kwargs["model"], format, e):
561
+ raise
562
+ input_messages, resolved_format, kwargs = self._encode(
563
+ model_id, messages, toolkit, format, params
564
+ )
565
+ anthropic_response = cast(
566
+ AnthropicMessage, self._create_sync(kwargs, params)
567
+ )
568
+ raise_on_refusal(anthropic_response, kwargs["model"])
446
569
  include_thoughts = _utils.get_include_thoughts(params)
447
570
  assistant_message, finish_reason, usage = _utils.decode_response(
448
571
  anthropic_response, model_id, include_thoughts=include_thoughts