abstractcore 2.20.0__tar.gz → 2.20.2__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 (327) hide show
  1. {abstractcore-2.20.0 → abstractcore-2.20.2}/PKG-INFO +1 -1
  2. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/__init__.py +16 -2
  3. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/client.py +38 -0
  4. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/context.py +44 -3
  5. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/models.py +38 -0
  6. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/capability_defaults.py +36 -2
  7. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/email_cli.py +71 -2
  8. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/manager.py +33 -0
  9. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/model_catalog.py +52 -2
  10. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/route_engines.py +37 -1
  11. abstractcore-2.20.2/abstractcore/providers/capability_host.py +72 -0
  12. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/app.py +7 -1
  13. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/host_profile.py +18 -0
  14. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/version.py +1 -1
  15. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore.egg-info/PKG-INFO +1 -1
  16. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore.egg-info/SOURCES.txt +1 -0
  17. {abstractcore-2.20.0 → abstractcore-2.20.2}/LICENSE +0 -0
  18. {abstractcore-2.20.0 → abstractcore-2.20.2}/README.md +0 -0
  19. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/__init__.py +0 -0
  20. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/__init__.py +0 -0
  21. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/__main__.py +0 -0
  22. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/app_config_utils.py +0 -0
  23. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/deepsearch.py +0 -0
  24. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/extractor.py +0 -0
  25. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/intent.py +0 -0
  26. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/judge.py +0 -0
  27. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/summarizer.py +0 -0
  28. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/architectures/__init__.py +0 -0
  29. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/architectures/detection.py +0 -0
  30. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/architectures/enums.py +0 -0
  31. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/architectures/response_postprocessing.py +0 -0
  32. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/architectures/thinking_controls.py +0 -0
  33. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/OCRA.ttf +0 -0
  34. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/OCRB.ttf +0 -0
  35. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/OCRBL.ttf +0 -0
  36. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/README.md +0 -0
  37. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/abstractframework_identity.json +0 -0
  38. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/architecture_formats.json +0 -0
  39. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/bloc-schema.jsonld +0 -0
  40. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/model_capabilities.json +0 -0
  41. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/model_capabilities.schema.json +0 -0
  42. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/model_downloads_catalog.json +0 -0
  43. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/model_downloads_catalog.schema.json +0 -0
  44. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/semantic_models.md +0 -0
  45. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/session_schema.json +0 -0
  46. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/__init__.py +0 -0
  47. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/errors.py +0 -0
  48. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/host.py +0 -0
  49. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/music_selectors.py +0 -0
  50. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/registry.py +0 -0
  51. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/scene3d_selectors.py +0 -0
  52. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/types.py +0 -0
  53. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/vision_catalog.py +0 -0
  54. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/__init__.py +0 -0
  55. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/bodystructure.py +0 -0
  56. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/errors.py +0 -0
  57. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/imap_codec.py +0 -0
  58. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/legacy.py +0 -0
  59. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/limits.py +0 -0
  60. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/oauth.py +0 -0
  61. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/policy.py +0 -0
  62. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/search.py +0 -0
  63. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/store.py +0 -0
  64. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/vault.py +0 -0
  65. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/__init__.py +0 -0
  66. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/analytics.py +0 -0
  67. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/cache.py +0 -0
  68. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/config.py +0 -0
  69. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/exceptions.py +0 -0
  70. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/glyph_processor.py +0 -0
  71. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/optimizer.py +0 -0
  72. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/orchestrator.py +0 -0
  73. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/pil_text_renderer.py +0 -0
  74. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/quality.py +0 -0
  75. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/text_formatter.py +0 -0
  76. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/vision_compressor.py +0 -0
  77. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/__init__.py +0 -0
  78. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/engines.py +0 -0
  79. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/host_jobs.py +0 -0
  80. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/main.py +0 -0
  81. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/model_materializer.py +0 -0
  82. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/model_verify.py +0 -0
  83. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/models_engines_cli.py +0 -0
  84. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/provider_profiles.py +0 -0
  85. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/recommendations.py +0 -0
  86. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/vision_config.py +0 -0
  87. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/console/__init__.py +0 -0
  88. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/console/theme_sync.py +0 -0
  89. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/console/themes.py +0 -0
  90. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/console/web.py +0 -0
  91. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/__init__.py +0 -0
  92. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/bloc_kv.py +0 -0
  93. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/bloc_metadata.py +0 -0
  94. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/cached_session.py +0 -0
  95. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/degeneration.py +0 -0
  96. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/endpoint_damping.py +0 -0
  97. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/enums.py +0 -0
  98. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/factory.py +0 -0
  99. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/file_blocs.py +0 -0
  100. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/file_boxes.py +0 -0
  101. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/generate_contract.py +0 -0
  102. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/interface.py +0 -0
  103. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/multimodal_generation.py +0 -0
  104. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/output_specs.py +0 -0
  105. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/retry.py +0 -0
  106. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/session.py +0 -0
  107. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/types.py +0 -0
  108. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/download.py +0 -0
  109. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/embeddings/__init__.py +0 -0
  110. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/embeddings/manager.py +0 -0
  111. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/embeddings/models.py +0 -0
  112. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/endpoint/__init__.py +0 -0
  113. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/endpoint/app.py +0 -0
  114. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/events/__init__.py +0 -0
  115. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/exceptions/__init__.py +0 -0
  116. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/mcp/__init__.py +0 -0
  117. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/mcp/client.py +0 -0
  118. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/mcp/factory.py +0 -0
  119. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/mcp/naming.py +0 -0
  120. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/mcp/stdio_client.py +0 -0
  121. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/mcp/tool_source.py +0 -0
  122. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/__init__.py +0 -0
  123. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/auto_handler.py +0 -0
  124. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/base.py +0 -0
  125. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/capabilities.py +0 -0
  126. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/delivery.py +0 -0
  127. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/enrichment.py +0 -0
  128. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/handlers/__init__.py +0 -0
  129. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/handlers/anthropic_handler.py +0 -0
  130. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/handlers/local_handler.py +0 -0
  131. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/handlers/openai_handler.py +0 -0
  132. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/openai_parts.py +0 -0
  133. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/pdf_routing.py +0 -0
  134. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/__init__.py +0 -0
  135. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/audio_processor.py +0 -0
  136. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/direct_pdf_processor.py +0 -0
  137. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/glyph_pdf_processor.py +0 -0
  138. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/image_processor.py +0 -0
  139. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/office_processor.py +0 -0
  140. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/pdf_processor.py +0 -0
  141. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/text_processor.py +0 -0
  142. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/video_processor.py +0 -0
  143. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/types.py +0 -0
  144. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/utils/__init__.py +0 -0
  145. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/utils/image_scaler.py +0 -0
  146. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/utils/video_frames.py +0 -0
  147. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/vision_fallback.py +0 -0
  148. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/processing/__init__.py +0 -0
  149. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/processing/basic_deepsearch.py +0 -0
  150. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/processing/basic_extractor.py +0 -0
  151. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/processing/basic_intent.py +0 -0
  152. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/processing/basic_judge.py +0 -0
  153. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/processing/basic_summarizer.py +0 -0
  154. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/__init__.py +0 -0
  155. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/_http.py +0 -0
  156. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/anthropic_provider.py +0 -0
  157. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/base.py +0 -0
  158. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/endpoint_context.py +0 -0
  159. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/generation_cancel.py +0 -0
  160. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/generation_progress.py +0 -0
  161. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/hf_residency.py +0 -0
  162. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/huggingface_provider.py +0 -0
  163. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/lmstudio_provider.py +0 -0
  164. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_model_rules.py +0 -0
  165. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_native_cache.py +0 -0
  166. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_native_session.py +0 -0
  167. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_prefill_observer.py +0 -0
  168. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_provider.py +0 -0
  169. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_qwen4.py +0 -0
  170. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_residency.py +0 -0
  171. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_runtime.py +0 -0
  172. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_vision_addon.py +0 -0
  173. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/model_capabilities.py +0 -0
  174. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/model_config_fingerprint.py +0 -0
  175. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/ollama_provider.py +0 -0
  176. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/openai_compatible_provider.py +0 -0
  177. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/openai_provider.py +0 -0
  178. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/openrouter_provider.py +0 -0
  179. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/portkey_provider.py +0 -0
  180. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/process_residency.py +0 -0
  181. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/registry.py +0 -0
  182. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/speculation.py +0 -0
  183. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/streaming.py +0 -0
  184. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/tokenizer_fingerprint.py +0 -0
  185. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/vllm_provider.py +0 -0
  186. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/weights_fingerprint.py +0 -0
  187. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/py.typed +0 -0
  188. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/__init__.py +0 -0
  189. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/__main__.py +0 -0
  190. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/audio_endpoints.py +0 -0
  191. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/camera_endpoints.py +0 -0
  192. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/capability_generation.py +0 -0
  193. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/console_routes.py +0 -0
  194. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/credentials.py +0 -0
  195. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/email_routes.py +0 -0
  196. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/first_run.py +0 -0
  197. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/host_routes.py +0 -0
  198. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/scene3d_endpoints.py +0 -0
  199. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/session_routes.py +0 -0
  200. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/vision_endpoints.py +0 -0
  201. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/structured/__init__.py +0 -0
  202. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/structured/handler.py +0 -0
  203. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/structured/retry.py +0 -0
  204. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/structured/schema_compat.py +0 -0
  205. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/testing/__init__.py +0 -0
  206. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/testing/mailserver.py +0 -0
  207. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/__init__.py +0 -0
  208. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/abstractignore.py +0 -0
  209. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/arg_canonicalizer.py +0 -0
  210. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/arg_coercion.py +0 -0
  211. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/browser_tools.py +0 -0
  212. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/code_analysis.py +0 -0
  213. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/common_tools.py +0 -0
  214. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/comms_tools.py +0 -0
  215. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/core.py +0 -0
  216. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/email_digests.py +0 -0
  217. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/fetch_url_ssrf.py +0 -0
  218. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/handler.py +0 -0
  219. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/inventory.py +0 -0
  220. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/parser.py +0 -0
  221. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/process_tree.py +0 -0
  222. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/registry.py +0 -0
  223. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/risk_facts.py +0 -0
  224. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/shell_session.py +0 -0
  225. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/shell_tools.py +0 -0
  226. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/syntax_rewriter.py +0 -0
  227. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/tag_rewriter.py +0 -0
  228. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/telegram_tdlib.py +0 -0
  229. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/telegram_tools.py +0 -0
  230. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/wire_naming.py +0 -0
  231. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/__init__.py +0 -0
  232. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/async_stream.py +0 -0
  233. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/cli.py +0 -0
  234. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/context_calibration.py +0 -0
  235. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/context_estimate.py +0 -0
  236. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/data_registry.py +0 -0
  237. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/display.py +0 -0
  238. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/file_filters.py +0 -0
  239. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/hostinfo.py +0 -0
  240. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/identity.py +0 -0
  241. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/install_settings.py +0 -0
  242. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/jsonish.py +0 -0
  243. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/memory.py +0 -0
  244. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/message_preprocessor.py +0 -0
  245. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/model_cache.py +0 -0
  246. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/model_fit.py +0 -0
  247. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/residency.py +0 -0
  248. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/self_fixes.py +0 -0
  249. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/structured_logging.py +0 -0
  250. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/token_utils.py +0 -0
  251. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/trace_export.py +0 -0
  252. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/truncation.py +0 -0
  253. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/vlm_token_calculator.py +0 -0
  254. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/windows_dll.py +0 -0
  255. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/workspace_paths.py +0 -0
  256. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore.egg-info/dependency_links.txt +0 -0
  257. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore.egg-info/entry_points.txt +0 -0
  258. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore.egg-info/requires.txt +0 -0
  259. {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore.egg-info/top_level.txt +0 -0
  260. {abstractcore-2.20.0 → abstractcore-2.20.2}/pyproject.toml +0 -0
  261. {abstractcore-2.20.0 → abstractcore-2.20.2}/setup.cfg +0 -0
  262. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_abstractendpoint_singleton_provider.py +0 -0
  263. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_agentic_cli_compatibility.py +0 -0
  264. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_basic_session.py +0 -0
  265. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv.py +0 -0
  266. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv_cache_dtype.py +0 -0
  267. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv_endpoint.py +0 -0
  268. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv_engine_fingerprint.py +0 -0
  269. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv_model_config_fingerprint.py +0 -0
  270. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv_tokenizer_fingerprint.py +0 -0
  271. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv_weights_fingerprint.py +0 -0
  272. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_metadata_json_extract.py +0 -0
  273. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_cached_session_kv_mode.py +0 -0
  274. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_cached_session_kv_thinking_prefix.py +0 -0
  275. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_capabilities_registry.py +0 -0
  276. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_capabilities_registry_preferred_backends_from_config.py +0 -0
  277. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_complete_integration.py +0 -0
  278. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_comprehensive_events.py +0 -0
  279. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_console_web.py +0 -0
  280. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_core_components.py +0 -0
  281. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_display_rule.py +0 -0
  282. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_email_digests.py +0 -0
  283. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_embedding_model_token_warnings.py +0 -0
  284. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_enhanced_prompt.py +0 -0
  285. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_environment_variable_tool_call_tags.py +0 -0
  286. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_factory.py +0 -0
  287. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_file_bloc_store_unit.py +0 -0
  288. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_file_boxes_cached_session.py +0 -0
  289. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_final_accuracy.py +0 -0
  290. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_final_comprehensive.py +0 -0
  291. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_final_graceful_errors.py +0 -0
  292. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_generate_contract.py +0 -0
  293. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_generate_response_reasoning_property_unit.py +0 -0
  294. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_generate_with_outputs.py +0 -0
  295. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_graceful_fallback.py +0 -0
  296. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_import_debug.py +0 -0
  297. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_import_safety.py +0 -0
  298. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_incremental_thinking_tag_stripper.py +0 -0
  299. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_install_hints_no_bare_packages.py +0 -0
  300. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_install_settings.py +0 -0
  301. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_integrated_functionality.py +0 -0
  302. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_llms_sources.py +0 -0
  303. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_mcp_integration.py +0 -0
  304. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_mcp_stdio_client.py +0 -0
  305. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_multimodal_generate_output.py +0 -0
  306. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_openai_prompt_cache_key_passthrough.py +0 -0
  307. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_output_specs.py +0 -0
  308. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_packaging_assets.py +0 -0
  309. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_packaging_extras.py +0 -0
  310. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_prompt_cache_api.py +0 -0
  311. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_prompt_cache_bloc_composition.py +0 -0
  312. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_prompt_cache_bloc_e2e_gate.py +0 -0
  313. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_prompt_cache_control_plane.py +0 -0
  314. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_prompt_cache_prepare_modules_thinking_http.py +0 -0
  315. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_retry_observability.py +0 -0
  316. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_retry_strategy.py +0 -0
  317. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_seed_determinism.py +0 -0
  318. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_seed_temperature_basic.py +0 -0
  319. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_sensory_prompting.py +0 -0
  320. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_server_prompt_cache_control_plane_proxy.py +0 -0
  321. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_server_prompt_cache_key_passthrough.py +0 -0
  322. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_session_reasoning_persistence_unit.py +0 -0
  323. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_structured_output_truncation_retry.py +0 -0
  324. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_text_only_model_experience.py +0 -0
  325. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_unload_memory.py +0 -0
  326. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_user_scenario_validation.py +0 -0
  327. {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_voice_host_credentials.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractcore
3
- Version: 2.20.0
3
+ Version: 2.20.2
4
4
  Summary: Unified interface to all LLM providers with essential infrastructure for tool calling, streaming, and model management
5
5
  Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
6
6
  Maintainer-email: Laurent-Philippe Albou <contact@abstractcore.ai>
@@ -23,8 +23,8 @@ credentials), `oauth` (OAuth2 / XOAUTH2), `store` (settings + secret + status),
23
23
  (the guarded send), `errors` (typed errors), `legacy` (one-time import of pre-2.20 settings).
24
24
  """
25
25
 
26
- from .client import DEFAULT_MAX_MESSAGE_BYTES, MAX_LIST_LIMIT, EmailClient, tls_context
27
- from .context import EmailContext, guarded_send
26
+ from .client import DEFAULT_MAX_MESSAGE_BYTES, MAX_LIST_LIMIT, EmailClient, auto_submitted_value, tls_context
27
+ from .context import EmailContext, guarded_send, mark_automatic
28
28
  from .errors import (
29
29
  EmailAgentToolsOff,
30
30
  EmailAttachmentNotFound,
@@ -54,6 +54,11 @@ from .errors import (
54
54
  )
55
55
  from .limits import SendRateLimiter
56
56
  from .models import (
57
+ AUTO_SUBMITTED_GENERATED,
58
+ AUTO_SUBMITTED_HEADER,
59
+ AUTO_SUBMITTED_REPLIED,
60
+ AUTO_SUBMITTED_VALUES,
61
+ AUTOMATION_MARKER_HEADER,
57
62
  Attachment,
58
63
  AttachmentInfo,
59
64
  EmailAccount,
@@ -68,6 +73,7 @@ from .models import (
68
73
  SendLimits,
69
74
  SendResult,
70
75
  SmtpSettings,
76
+ automation_marker_value,
71
77
  )
72
78
  from .oauth import (
73
79
  BUILTIN_CLIENTS,
@@ -87,6 +93,14 @@ from .store import EmailAccountStore, EmailSettings
87
93
  from .vault import SecretVault
88
94
 
89
95
  __all__ = [
96
+ "AUTO_SUBMITTED_GENERATED",
97
+ "AUTO_SUBMITTED_HEADER",
98
+ "AUTO_SUBMITTED_REPLIED",
99
+ "AUTO_SUBMITTED_VALUES",
100
+ "AUTOMATION_MARKER_HEADER",
101
+ "auto_submitted_value",
102
+ "automation_marker_value",
103
+ "mark_automatic",
90
104
  "DEFAULT_MAX_MESSAGE_BYTES",
91
105
  "MAX_LIST_LIMIT",
92
106
  "BUILTIN_CLIENTS",
@@ -61,6 +61,9 @@ from .errors import (
61
61
  classify_smtp_code,
62
62
  )
63
63
  from .models import (
64
+ AUTO_SUBMITTED_HEADER,
65
+ AUTO_SUBMITTED_VALUES,
66
+ AUTOMATION_MARKER_HEADER,
64
67
  Attachment,
65
68
  AttachmentInfo,
66
69
  EmailAccount,
@@ -72,6 +75,7 @@ from .models import (
72
75
  OutgoingMessage,
73
76
  SendResult,
74
77
  ServerSettings,
78
+ automation_marker_value,
75
79
  )
76
80
  from .oauth import OAuthTokenProvider, xoauth2_string
77
81
  from .policy import normalize_address, parse_recipients
@@ -87,6 +91,7 @@ MAX_LIST_LIMIT = 100
87
91
 
88
92
  _SUMMARY_HEADERS = (
89
93
  "FROM TO CC SUBJECT DATE MESSAGE-ID REPLY-TO IN-REPLY-TO IMPORTANCE X-PRIORITY PRIORITY LIST-UNSUBSCRIBE"
94
+ " AUTO-SUBMITTED X-ABSTRACTFRAMEWORK-AUTOMATION"
90
95
  )
91
96
  _SUMMARY_ITEMS = f"(UID FLAGS RFC822.SIZE INTERNALDATE BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS ({_SUMMARY_HEADERS})])"
92
97
  _DETAIL_ITEMS = "(UID FLAGS RFC822.SIZE INTERNALDATE BODYSTRUCTURE BODY.PEEK[HEADER])"
@@ -348,6 +353,22 @@ def x_priority_value(header_value: str) -> Optional[int]:
348
353
  return None
349
354
 
350
355
 
356
+ def auto_submitted_value(header_value: str) -> Optional[str]:
357
+ """`Auto-Submitted:` (RFC 3834) as its lower-cased keyword without parameters.
358
+
359
+ `auto-replied; owner-email="a@b"` -> "auto-replied"; absent or empty -> None. The keyword
360
+ is returned as sent (an extension keyword stays itself); a value that is not a keyword
361
+ (spaces, punctuation) is returned as "unknown" (present and not "no").
362
+ """
363
+
364
+ v = str(header_value or "").split(";", 1)[0].strip().lower()
365
+ if not v:
366
+ return None
367
+ if all(c.isascii() and (c.isalnum() or c in "-_.") for c in v):
368
+ return v
369
+ return "unknown"
370
+
371
+
351
372
  def _structure(attrs: dict) -> Optional[MimePart]:
352
373
  tok = attrs.get("BODYSTRUCTURE")
353
374
  if not isinstance(tok, list):
@@ -733,6 +754,8 @@ class EmailClient:
733
754
  x_priority=x_priority_value(_header(msg, "X-Priority")),
734
755
  priority=priority_value(_header(msg, "Priority")),
735
756
  list_unsubscribe=bool(_header(msg, "List-Unsubscribe")),
757
+ auto_submitted=auto_submitted_value(_header(msg, AUTO_SUBMITTED_HEADER)),
758
+ framework_marker=automation_marker_value(_header(msg, AUTOMATION_MARKER_HEADER)),
736
759
  )
737
760
 
738
761
  def _cap(self, max_message_bytes: Optional[int]) -> int:
@@ -1194,6 +1217,21 @@ class EmailClient:
1194
1217
  mime["In-Reply-To"] = message.in_reply_to
1195
1218
  if message.references:
1196
1219
  mime["References"] = " ".join(message.references)
1220
+ if message.auto_submitted:
1221
+ if message.auto_submitted not in AUTO_SUBMITTED_VALUES:
1222
+ raise EmailInvalidMessage(
1223
+ f"Auto-Submitted must be one of {', '.join(AUTO_SUBMITTED_VALUES)}, got {message.auto_submitted!r}.",
1224
+ "Use auto-generated for automatic mail, auto-replied for an automatic answer to one message.",
1225
+ )
1226
+ mime[AUTO_SUBMITTED_HEADER] = message.auto_submitted
1227
+ if message.automation_marker:
1228
+ marker = automation_marker_value(message.automation_marker)
1229
+ if not marker:
1230
+ raise EmailInvalidMessage(
1231
+ "The automation marker must be one line of printable ASCII (at most 200 characters).",
1232
+ "Give an identifier such as occurrence:<run id>.",
1233
+ )
1234
+ mime[AUTOMATION_MARKER_HEADER] = marker
1197
1235
  except (ValueError, TypeError) as exc:
1198
1236
  raise EmailInvalidMessage(
1199
1237
  "A message header is not valid (line breaks are not allowed in headers).",
@@ -18,13 +18,22 @@ policy and the limits run here, always, for every caller.
18
18
  from __future__ import annotations
19
19
 
20
20
  import ssl
21
- from dataclasses import dataclass, field
21
+ from dataclasses import dataclass, field, replace
22
22
  from typing import Any, Callable, Dict, Optional
23
23
 
24
24
  from .client import DEFAULT_MAX_MESSAGE_BYTES, EmailClient
25
25
  from .errors import EmailDisabled, EmailInvalidMessage, EmailNotConfigured
26
26
  from .limits import SendRateLimiter
27
- from .models import EmailAccount, EmailSecret, OutgoingMessage, SendLimits, SendResult
27
+ from .models import (
28
+ AUTO_SUBMITTED_GENERATED,
29
+ AUTO_SUBMITTED_REPLIED,
30
+ EmailAccount,
31
+ EmailSecret,
32
+ OutgoingMessage,
33
+ SendLimits,
34
+ SendResult,
35
+ automation_marker_value,
36
+ )
28
37
  from .oauth import OAuthTokenProvider
29
38
  from .policy import PolicyDecision, RecipientPolicy, evaluate, parse_recipients
30
39
 
@@ -46,6 +55,11 @@ class EmailContext:
46
55
  # are not fetched; the read returns a typed skip record instead (never a cut body).
47
56
  max_message_bytes: int = DEFAULT_MAX_MESSAGE_BYTES
48
57
  on_sent: Optional[Callable[[Dict[str, Any]], None]] = field(default=None, repr=False)
58
+ # Set by a host for a context used by something that sends automatically (an automation
59
+ # occurrence, a notification): every message sent through it carries RFC 3834
60
+ # `Auto-Submitted` and the framework marker (`X-AbstractFramework-Automation: <value>`),
61
+ # so this account's own mail watcher never admits it (no self-triggering loop).
62
+ automation_marker: str = ""
49
63
 
50
64
  def __repr__(self) -> str:
51
65
  return f"EmailContext(address={self.account.address!r}, enabled={self.enabled}, source={self.source!r})"
@@ -81,8 +95,27 @@ class EmailContext:
81
95
  return guarded_send(self, message)
82
96
 
83
97
 
98
+ def mark_automatic(message: OutgoingMessage, marker: str) -> OutgoingMessage:
99
+ """`message` with the framework marker and RFC 3834 `Auto-Submitted` (kept when set).
100
+
101
+ `Auto-Submitted` is `auto-replied` for an answer to one message (In-Reply-To set), else
102
+ `auto-generated`.
103
+ """
104
+
105
+ marker_value = automation_marker_value(marker)
106
+ if not marker_value:
107
+ raise EmailInvalidMessage(
108
+ "The automation marker must be one line of printable ASCII (at most 200 characters).",
109
+ "Give an identifier such as occurrence:<run id>.",
110
+ )
111
+ auto = message.auto_submitted or (AUTO_SUBMITTED_REPLIED if message.in_reply_to else AUTO_SUBMITTED_GENERATED)
112
+ return replace(message, auto_submitted=auto, automation_marker=message.automation_marker or marker_value)
113
+
114
+
84
115
  def guarded_send(ctx: EmailContext, message: OutgoingMessage) -> SendResult:
85
116
  ctx.require_enabled()
117
+ if ctx.automation_marker:
118
+ message = mark_automatic(message, ctx.automation_marker)
86
119
  if not ctx.account.can_send:
87
120
  raise EmailNotConfigured(
88
121
  "This account has no SMTP (send) settings.",
@@ -100,7 +133,15 @@ def guarded_send(ctx: EmailContext, message: OutgoingMessage) -> SendResult:
100
133
  raise
101
134
  if ctx.on_sent is not None:
102
135
  try:
103
- ctx.on_sent({"message_id": result.message_id, "accepted": list(result.accepted), "refused": dict(result.refused)})
136
+ ctx.on_sent(
137
+ {
138
+ "message_id": result.message_id,
139
+ "accepted": list(result.accepted),
140
+ "refused": dict(result.refused),
141
+ "auto_submitted": message.auto_submitted,
142
+ "automation_marker": message.automation_marker,
143
+ }
144
+ )
104
145
  except Exception:
105
146
  pass
106
147
  return result
@@ -486,6 +486,32 @@ class Attachment:
486
486
  return b""
487
487
 
488
488
 
489
+ # RFC 3834 section 5: a message sent automatically carries `Auto-Submitted: auto-generated`
490
+ # (not a response to one message) or `auto-replied` (a response to one message); mail
491
+ # written by a person carries none (or `no`). Responders must not answer such messages.
492
+ AUTO_SUBMITTED_HEADER = "Auto-Submitted"
493
+ AUTO_SUBMITTED_GENERATED = "auto-generated"
494
+ AUTO_SUBMITTED_REPLIED = "auto-replied"
495
+ AUTO_SUBMITTED_VALUES = (AUTO_SUBMITTED_GENERATED, AUTO_SUBMITTED_REPLIED)
496
+ # The framework's own marker on every message it sends automatically (notifications, digests,
497
+ # send-email actions, agent sends from automations): the value names what sent it (e.g.
498
+ # `occurrence:<run id>`, `notification:<key>`). A mail watcher never admits a message that
499
+ # carries it and comes from its own account, so an automation can never trigger itself.
500
+ AUTOMATION_MARKER_HEADER = "X-AbstractFramework-Automation"
501
+ MAX_AUTOMATION_MARKER = 200
502
+
503
+
504
+ def automation_marker_value(value: Any) -> str:
505
+ """A marker value as sent: one line of printable ASCII, at most 200 characters, else ""."""
506
+
507
+ text = str(value or "").strip()
508
+ if not text or len(text) > MAX_AUTOMATION_MARKER:
509
+ return ""
510
+ if any(not (32 <= ord(c) < 127) for c in text):
511
+ return ""
512
+ return text
513
+
514
+
489
515
  @dataclass(frozen=True)
490
516
  class OutgoingMessage:
491
517
  to: Tuple[str, ...] = ()
@@ -497,6 +523,11 @@ class OutgoingMessage:
497
523
  attachments: Tuple[Attachment, ...] = ()
498
524
  in_reply_to: str = ""
499
525
  references: Tuple[str, ...] = ()
526
+ # RFC 3834 `Auto-Submitted` ("auto-generated" | "auto-replied"; "" = written by a person)
527
+ # and the framework marker (`X-AbstractFramework-Automation`, "" = none). Set by the
528
+ # sender for automatic mail, or stamped by `guarded_send` from `EmailContext.automation_marker`.
529
+ auto_submitted: str = ""
530
+ automation_marker: str = ""
500
531
 
501
532
  @property
502
533
  def recipients(self) -> List[str]:
@@ -564,6 +595,11 @@ class MessageSummary:
564
595
  x_priority: Optional[int] = None
565
596
  priority: Optional[str] = None
566
597
  list_unsubscribe: bool = False
598
+ # RFC 3834 `Auto-Submitted`, lower-cased keyword without parameters ("no", "auto-generated",
599
+ # "auto-replied", or an extension keyword); None when the header is absent or empty.
600
+ auto_submitted: Optional[str] = None
601
+ # The framework marker header's value (`X-AbstractFramework-Automation`); "" when absent.
602
+ framework_marker: str = ""
567
603
 
568
604
  @property
569
605
  def seen(self) -> bool:
@@ -592,6 +628,8 @@ class MessageSummary:
592
628
  "x_priority": self.x_priority,
593
629
  "priority": self.priority,
594
630
  "list_unsubscribe": self.list_unsubscribe,
631
+ "auto_submitted": self.auto_submitted,
632
+ "framework_marker": self.framework_marker,
595
633
  }
596
634
 
597
635
 
@@ -625,6 +625,32 @@ def _host_platform(host: Mapping[str, Any]) -> Tuple[str, str, Optional[str]]:
625
625
  return os_id or "unknown", arch or "unknown", accelerator if isinstance(accelerator, str) else None
626
626
 
627
627
 
628
+ def recommended_engine_missing_reason(provider: Any, host: Mapping[str, Any]) -> Optional[str]:
629
+ """Why a recommended provider's IN-PROCESS engine is not installed in this install, or None.
630
+
631
+ Read from the host profile's `engines_installed` (utils/host_profile.py, from
632
+ `route_engines.provider_engine_installed`): the light install profile has no MLX, MLX-Gen
633
+ or PyTorch, so a recommendation naming one would fail at first use (0.7.0 end-to-end: a
634
+ 128 GB Mac on the light profile got mlx/Qwen3.8-Flash-Next-4bit as its text default and
635
+ every call, voice included, failed with "MLX dependencies not installed"). A host mapping
636
+ without that key (a synthetic host, an older profile) is not judged. The sentence is
637
+ `route_engines.route_engine_missing`'s own (it names the install setting to add).
638
+ Configured routes are judged by `route_engines.routes_engine_missing` (`engine_missing`),
639
+ never folded into `route_unavailable`.
640
+ """
641
+
642
+ installed = host.get("engines_installed")
643
+ pid = str(provider or "").strip().lower()
644
+ if not isinstance(installed, Mapping) or installed.get(pid) is not False:
645
+ return None
646
+ from .route_engines import route_engine_missing
647
+
648
+ flag = route_engine_missing(pid)
649
+ if flag is not None and flag.get("reason"):
650
+ return str(flag["reason"])
651
+ return f"the {pid} engine is not installed in this Python environment (the light install profile has no local engines)"
652
+
653
+
628
654
  def recommended_route_unavailable_reason(
629
655
  provider: Any, host: Mapping[str, Any], key: Optional[str] = None
630
656
  ) -> Optional[str]:
@@ -632,7 +658,8 @@ def recommended_route_unavailable_reason(
632
658
 
633
659
  `key` (the route) only words the sentence ("MLX-Gen video generation").
634
660
  Engine support only: the memory gate of fit-gated routes is
635
- `recommended_unavailable_routes`.
661
+ `recommended_unavailable_routes`; whether THIS install has the engine is
662
+ `recommended_engine_missing_reason`.
636
663
  """
637
664
 
638
665
  from .engines import _support
@@ -746,7 +773,8 @@ def _unavailable_reasons(
746
773
  routes: Mapping[str, CapabilityRouteDefault], downloads: Mapping[str, Mapping[str, str]], host: Mapping[str, Any]
747
774
  ) -> Dict[str, str]:
748
775
  """`{key: reason}` for every recommended row this host cannot run: its
749
- engine has no build here, or (fit-gated rows) its model does not fit."""
776
+ engine has no build here, this install does not have its in-process engine
777
+ (`recommended_engine_missing_reason`), or (fit-gated rows) its model does not fit."""
750
778
 
751
779
  out: Dict[str, str] = {}
752
780
  for key, route in routes.items():
@@ -757,6 +785,12 @@ def _unavailable_reasons(
757
785
  reason = f"{reason}; {next_step}"
758
786
  out[key] = reason
759
787
  continue
788
+ # The host runs the engine, but this install does not have it (the light profile):
789
+ # never recommend (seed, apply-recommended) a route that fails at first use.
790
+ reason = recommended_engine_missing_reason(route.provider, host)
791
+ if reason:
792
+ out[key] = reason
793
+ continue
760
794
  if key in _FIT_GATED_ROUTES:
761
795
  reason = _fit_gate_reason(key, downloads[key], host)
762
796
  if reason:
@@ -13,7 +13,11 @@
13
13
  abstractcore email registered-address me@example.com
14
14
 
15
15
  Credentials are direct parameters (`--password <value>`, `--client-secret <value>`) and are
16
- stored encrypted; they are never printed. Every verb takes `--json` (the `email_settings_v1`
16
+ stored encrypted; they are never printed. A script or program passes them on stdin instead, so
17
+ the secret never appears on a command line (where other local users can read it while the
18
+ command runs): `--password-stdin` / `--client-secret-stdin` read exactly one line from stdin
19
+ (the trailing newline is removed, nothing else), e.g.
20
+ `printf '%s\n' "$PW" | abstractcore email connect --address me@example.com ... --password-stdin`. Every verb takes `--json` (the `email_settings_v1`
17
21
  document of `EmailAccountStore.public()`, or `{ok: false, error: {code, cause, fix}}`).
18
22
  Exit codes: 0 ok, 1 error, 2 refused (a failed connection test, or `disconnect` without
19
23
  `--yes`).
@@ -187,6 +191,61 @@ def _obtain_oauth_tokens(args: argparse.Namespace, oauth: Any, client_secret: st
187
191
  return loop.finish(timeout_s=float(args.oauth_timeout))
188
192
 
189
193
 
194
+ def _read_stdin_secret(flag: str, what: str) -> str:
195
+ """One secret from stdin: exactly one line, the trailing newline removed (nothing else)."""
196
+ from abstractcore.comms.email import EmailInvalidSettings
197
+
198
+ stream = sys.stdin
199
+ if stream is None:
200
+ raise EmailInvalidSettings(
201
+ f"{flag} was given but this process has no stdin.",
202
+ f"Pipe the {what} into the command (printf '%s\\n' \"$SECRET\" | abstractcore email connect ... {flag}).",
203
+ )
204
+ line = stream.readline()
205
+ if line.endswith("\n"):
206
+ line = line[:-1]
207
+ if line.endswith("\r"):
208
+ line = line[:-1]
209
+ if not line:
210
+ raise EmailInvalidSettings(
211
+ f"{flag} read an empty {what} from stdin.",
212
+ f"Write the {what} as the first line of stdin (printf '%s\\n' \"$SECRET\" | abstractcore email connect ... {flag}).",
213
+ )
214
+ return line
215
+
216
+
217
+ def _stdin_secrets(args: argparse.Namespace) -> None:
218
+ """Resolve `--password-stdin` / `--client-secret-stdin` into args.password / args.client_secret."""
219
+ from abstractcore.comms.email import EmailInvalidSettings
220
+
221
+ if args.password_stdin and args.client_secret_stdin:
222
+ raise EmailInvalidSettings(
223
+ "--password-stdin and --client-secret-stdin cannot be combined (stdin carries one secret).",
224
+ "Use --password-stdin for a password account, or --client-secret-stdin with --oauth.",
225
+ )
226
+ if args.password_stdin:
227
+ if args.password:
228
+ raise EmailInvalidSettings(
229
+ "--password and --password-stdin were both given.",
230
+ "Give the password once: --password <value>, or --password-stdin with the password on stdin.",
231
+ )
232
+ if args.oauth:
233
+ raise EmailInvalidSettings("--password-stdin is not used with --oauth.", "Remove --password-stdin; OAuth2 signs in through the browser.")
234
+ args.password = _read_stdin_secret("--password-stdin", "password")
235
+ if args.client_secret_stdin:
236
+ if args.client_secret:
237
+ raise EmailInvalidSettings(
238
+ "--client-secret and --client-secret-stdin were both given.",
239
+ "Give the client secret once: --client-secret <value>, or --client-secret-stdin with the secret on stdin.",
240
+ )
241
+ if not args.oauth:
242
+ raise EmailInvalidSettings(
243
+ "--client-secret-stdin is only used with --oauth.",
244
+ "Add --oauth google|microsoft|custom, or remove --client-secret-stdin.",
245
+ )
246
+ args.client_secret = _read_stdin_secret("--client-secret-stdin", "client secret")
247
+
248
+
190
249
  def cmd_connect(args: argparse.Namespace) -> int:
191
250
  from abstractcore.comms.email import (
192
251
  EmailAccount,
@@ -203,6 +262,7 @@ def cmd_connect(args: argparse.Namespace) -> int:
203
262
 
204
263
  as_json = bool(args.json)
205
264
  try:
265
+ _stdin_secrets(args)
206
266
  preset = provider_preset(args.oauth, tenant=args.tenant or "") if args.oauth and args.oauth != "custom" else {}
207
267
  address = (args.address or "").strip() or (args.username or "").strip()
208
268
  if not address:
@@ -254,7 +314,8 @@ def cmd_connect(args: argparse.Namespace) -> int:
254
314
  if not args.password:
255
315
  raise EmailInvalidSettings(
256
316
  "The password is missing.",
257
- "Give --password <value> (an app password for providers with two-step verification), or --oauth <provider>.",
317
+ "Give --password <value> (an app password for providers with two-step verification; "
318
+ "--password-stdin reads it from stdin), or --oauth <provider>.",
258
319
  )
259
320
  account = EmailAccount.build(
260
321
  address=address, username=args.username or address, imap=imap, smtp=smtp,
@@ -475,6 +536,10 @@ def build_parser() -> argparse.ArgumentParser:
475
536
  c.add_argument("--display-name", help="Name shown to recipients")
476
537
  c.add_argument("--username", help="Sign-in user name (default: the address)")
477
538
  c.add_argument("--password", help="Password or app password (stored encrypted)")
539
+ c.add_argument(
540
+ "--password-stdin", action="store_true",
541
+ help="Read the password from stdin (one line; for scripts: the password never appears on a command line)",
542
+ )
478
543
  c.add_argument("--imap-host")
479
544
  c.add_argument("--imap-port", type=int)
480
545
  c.add_argument("--imap-security", choices=("ssl", "starttls"))
@@ -490,6 +555,10 @@ def build_parser() -> argparse.ArgumentParser:
490
555
  c.add_argument("--oauth", choices=("google", "microsoft", "custom"), help="Sign in with OAuth2 instead of a password")
491
556
  c.add_argument("--client-id", help="Your own OAuth client id (default: the built-in AbstractFramework client, when one is registered for the provider)")
492
557
  c.add_argument("--client-secret", help="Your own OAuth client secret (stored encrypted)")
558
+ c.add_argument(
559
+ "--client-secret-stdin", action="store_true",
560
+ help="Read the OAuth client secret from stdin (one line; the secret never appears on a command line)",
561
+ )
493
562
  c.add_argument("--tenant", help="Microsoft tenant (default common)")
494
563
  c.add_argument("--oauth-flow", choices=("device", "loopback"), help="device code (Microsoft default) or browser on this machine (Google default)")
495
564
  c.add_argument("--token-endpoint")
@@ -117,6 +117,34 @@ _PROVIDER_MODEL_PREFIXES = {
117
117
  }
118
118
 
119
119
 
120
+ def route_provider_changed(stored: Any, requested: Any) -> bool:
121
+ """True when a route write names a provider other than the stored one (case-insensitive).
122
+
123
+ Such a write that names no options drops the old engine's speculation request
124
+ (`options_after_provider_change`; AbstractGateway's route save applies the same rule).
125
+ """
126
+
127
+ if requested is None:
128
+ return False
129
+ old = str(stored or "").strip().lower()
130
+ new = str(requested or "").strip().lower()
131
+ return bool(old) and old != new
132
+
133
+
134
+ def options_after_provider_change(options: Any) -> Dict[str, Any]:
135
+ """The stored route options a write that moves the route to ANOTHER provider keeps.
136
+
137
+ A speculation REQUEST (`speculation: {mode: native_mtp, ...}` or `true`) names how the old
138
+ engine loads the model (an MLX MTP head, a drafter), so it is dropped; `speculation: false`
139
+ (off) is valid for every engine and is kept, like every other option.
140
+ """
141
+
142
+ out = dict(options) if isinstance(options, dict) else {}
143
+ if "speculation" in out and out["speculation"] is not False:
144
+ out.pop("speculation")
145
+ return out
146
+
147
+
120
148
  def _split_provider_model(value: str, *, default_provider: str) -> Tuple[str, str]:
121
149
  raw = str(value or "").strip()
122
150
  if not raw:
@@ -2032,6 +2060,11 @@ class ConfigurationManager:
2032
2060
  task=task,
2033
2061
  )
2034
2062
  merged_options = options if isinstance(options, dict) else stored.get("options")
2063
+ if not isinstance(options, dict) and route_provider_changed(stored.get("provider"), provider):
2064
+ # A write that moves the route to another provider and names no options drops the
2065
+ # old engine's speculation request (0.7.0 end-to-end: an LM Studio text route kept
2066
+ # the MLX tier's `speculation: native_mtp`); every other option is kept.
2067
+ merged_options = options_after_provider_change(merged_options)
2035
2068
  return self.set_capability_default(
2036
2069
  kind,
2037
2070
  modality,
@@ -141,6 +141,10 @@ APPLE_TEXT_TIERS: Tuple[Dict[str, Any], ...] = (
141
141
  "plain": "mlx-community/Qwen3.8-Flash-Next-4bit", "mtp": "Jundot/Qwen3.8-Flash-Next-oQ4e-mtp"},
142
142
  )
143
143
  _TIER_PROVIDER = "mlx"
144
+ # The tier's engine when this install has no MLX (the light profile): LM Studio serves the same
145
+ # rows (`qwen/qwen3.5-9b@q4_k_m`, `qwen/qwen3.8-27b@q4_k_m`) from its own app, nothing to
146
+ # install in this Python (`recommended_text_model`, basis `apple_silicon_engine_fallback`).
147
+ _SERVER_TIER_PROVIDER = "lmstudio"
144
148
 
145
149
  # The portable text model on an engine that runs where LM Studio does not
146
150
  # (Intel Macs). Same catalog row as the portable default; Ollama's bare tag
@@ -292,7 +296,9 @@ def recommended_text_model(
292
296
  model the id the route stores (the served id; for MLX the repo id)
293
297
  options route options: the portable route's MTP policy
294
298
  (`speculation`), overlaid with the artifact's own options
295
- basis `apple_silicon_tiers`, `portable_default`,
299
+ basis `apple_silicon_tiers`, `apple_silicon_engine_fallback` (Apple
300
+ silicon without MLX in this install: the tier's LM Studio build,
301
+ else the nearest smaller tier's), `portable_default`,
296
302
  `portable_engine_fallback` (the portable engine has no build
297
303
  on this host, e.g. LM Studio on an Intel Mac), or
298
304
  `no_supported_engine` (neither has one, e.g. FreeBSD: the
@@ -319,7 +325,51 @@ def recommended_text_model(
319
325
  accelerator = str(profile.get("accelerator") or "none")
320
326
  memory = _memory_gib(profile)
321
327
  use_mtp = MTP_RECOMMENDED if mtp is None else bool(mtp)
328
+ from .model_materializer import split_artifact as _split_artifact
329
+
330
+ mlx_missing = None
322
331
  if accelerator == "metal":
332
+ from .capability_defaults import recommended_engine_missing_reason
333
+
334
+ mlx_missing = recommended_engine_missing_reason(_TIER_PROVIDER, profile)
335
+ if accelerator == "metal" and mlx_missing:
336
+ # The tiers are MLX builds, and this install has no MLX (the light profile): the
337
+ # tier's model on LM Studio (a server route, nothing to install in this Python), else
338
+ # the nearest SMALLER tier that has an LM Studio build (Flash-Next has none) -- never
339
+ # an engine that fails at first use (0.7.0 end-to-end, light install on a 128 GB Mac).
340
+ tier, rule = _apple_tier(memory)
341
+ order = list(APPLE_TEXT_TIERS)
342
+ start = order.index(tier)
343
+ chosen = None
344
+ for candidate in reversed(order[: start + 1]):
345
+ for seed_row in _load_seed_cached().get("rows") or []:
346
+ if seed_row.get("id") != candidate["row"]:
347
+ continue
348
+ art0 = next((a for a in seed_row.get("artifacts") or [] if a.get("provider") == _SERVER_TIER_PROVIDER), None)
349
+ if art0 is not None:
350
+ chosen = (candidate, seed_row, art0)
351
+ break
352
+ if chosen:
353
+ break
354
+ if chosen is None:
355
+ raise LookupError("no Apple silicon tier row has an LM Studio build")
356
+ cand, row, art = chosen
357
+ which = (
358
+ "the tier's LM Studio build" if cand is tier
359
+ else f"the LM Studio build of the nearest smaller tier ({cand['row']}; {tier['row']} has no LM Studio build)"
360
+ )
361
+ out = {
362
+ "provider": _SERVER_TIER_PROVIDER,
363
+ "artifact": str(art["artifact"]),
364
+ "model": _split_artifact(str(art["artifact"]))[0], # LM Studio serves the id without `@quant`
365
+ # The host-wide MTP policy, as on the portable LM Studio route of every other host.
366
+ "options": dict(_portable_text_default()["options"], **json.loads(json.dumps(art.get("options") or {}))),
367
+ "catalog_id": row["id"],
368
+ "basis": "apple_silicon_engine_fallback",
369
+ "tier": f"{rule}: {which}, because the tier's MLX build cannot run in this install ({mlx_missing.rstrip('.')})",
370
+ "mtp": False,
371
+ }
372
+ elif accelerator == "metal":
323
373
  tier, rule = _apple_tier(memory)
324
374
  provider, artifact = _TIER_PROVIDER, _tier_artifact(tier, use_mtp)
325
375
  row, art = _seed_row_and_artifact(str(tier["row"]), provider, artifact)
@@ -1664,7 +1714,7 @@ def catalog(
1664
1714
  if arts:
1665
1715
  tier = tier_by_row.get(seed_row["id"]) if accelerator == "metal" else None
1666
1716
  forced = (_TIER_PROVIDER, _tier_artifact(tier)) if tier is not None else None
1667
- if text_pick["basis"] == "portable_engine_fallback" and seed_row["id"] == text_pick["catalog_id"]:
1717
+ if text_pick["basis"] in ("portable_engine_fallback", "apple_silicon_engine_fallback") and seed_row["id"] == text_pick["catalog_id"]:
1668
1718
  forced = (text_pick["provider"], text_pick["artifact"])
1669
1719
  if forced is not None and not any((a["provider"], a["artifact"]) == forced for a in arts):
1670
1720
  forced = None # an engine filter (`engine=ollama`) removed the tier artifact
@@ -49,7 +49,13 @@ import shlex
49
49
  from importlib import metadata
50
50
  from typing import Any, Dict, Mapping, Optional, Tuple
51
51
 
52
- __all__ = ["ABSTRACTVOICE_ENGINE_RUNTIME_FLOOR", "route_engine_missing", "routes_engine_missing"]
52
+ __all__ = [
53
+ "ABSTRACTVOICE_ENGINE_RUNTIME_FLOOR",
54
+ "provider_engine_installed",
55
+ "provider_engines_installed",
56
+ "route_engine_missing",
57
+ "routes_engine_missing",
58
+ ]
53
59
 
54
60
  # The first AbstractVoice with `abstractvoice.engine_runtime` (the public
55
61
  # runtime probe). Mirrors the floor of the `voice` extra in pyproject.toml;
@@ -219,6 +225,36 @@ def _dist_version(dist: str) -> str:
219
225
  return "(unknown version)"
220
226
 
221
227
 
228
+ # In-process providers a RECOMMENDATION may name, and what makes each usable in this Python
229
+ # environment (the host profile's `engines_installed`, read by the recommendation so the light
230
+ # install profile is never handed an engine it does not have). Voice engines are AbstractVoice's
231
+ # own answer (`route_engine_missing(..., key=)`), not judged here: they ship with the light
232
+ # profile and asking needs AbstractVoice imported.
233
+ def provider_engine_installed(provider: Any) -> Optional[bool]:
234
+ """True / False for an in-process provider a recommendation can name (mlx, mlx-gen,
235
+ diffusers, acestep), None for any other provider (not judged). Lookups only."""
236
+
237
+ pid = str(provider or "").strip().lower()
238
+ if pid == "mlx":
239
+ return _importable("mlx_lm")
240
+ if pid == "mlx-gen":
241
+ return _distributed("abstractvision") and _distributed("mlx-gen")
242
+ if pid in ("diffusers", "acestep"):
243
+ return _importable("torch")
244
+ return None
245
+
246
+
247
+ def provider_engines_installed() -> Dict[str, bool]:
248
+ """`{provider: installed}` for every provider `provider_engine_installed` judges."""
249
+
250
+ out: Dict[str, bool] = {}
251
+ for pid in ("mlx", "mlx-gen", "diffusers", "acestep"):
252
+ value = provider_engine_installed(pid)
253
+ if value is not None:
254
+ out[pid] = bool(value)
255
+ return out
256
+
257
+
222
258
  def route_engine_missing(provider: Any, model: Any = None, key: Any = None) -> Optional[Dict[str, Any]]:
223
259
  """`{engine, name, reason, install[, engine_row]}` when the in-process
224
260
  engine behind this route is not installed here, else None.