echotools 2.3.19__tar.gz → 2.3.22__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 (183) hide show
  1. {echotools-2.3.19 → echotools-2.3.22}/PKG-INFO +1 -1
  2. {echotools-2.3.19 → echotools-2.3.22}/pyproject.toml +1 -1
  3. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/protocols/entml_thinking.py +22 -18
  4. echotools-2.3.22/src/echotools/exec/fncall/protocols/entml_thinking_history.py +133 -0
  5. {echotools-2.3.19 → echotools-2.3.22}/src/echotools.egg-info/PKG-INFO +1 -1
  6. {echotools-2.3.19 → echotools-2.3.22}/src/echotools.egg-info/SOURCES.txt +2 -0
  7. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_entml_protocol.py +24 -7
  8. echotools-2.3.22/src/tests/test_entml_thinking_history.py +71 -0
  9. {echotools-2.3.19 → echotools-2.3.22}/README.md +0 -0
  10. {echotools-2.3.19 → echotools-2.3.22}/docs/README.md +0 -0
  11. {echotools-2.3.19 → echotools-2.3.22}/setup.cfg +0 -0
  12. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/__init__.py +0 -0
  13. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/cache/__init__.py +0 -0
  14. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/cache/list_cache.py +0 -0
  15. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/cache/memory_cache.py +0 -0
  16. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/config/__init__.py +0 -0
  17. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/config/base.py +0 -0
  18. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/config/center.py +0 -0
  19. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/config/center_notify.py +0 -0
  20. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/config/center_template.py +0 -0
  21. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/config/loader.py +0 -0
  22. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/config/merge.py +0 -0
  23. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/errors/__init__.py +0 -0
  24. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/errors/base.py +0 -0
  25. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/errors/classify.py +0 -0
  26. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/errors/common.py +0 -0
  27. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/errors/http.py +0 -0
  28. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/ids/__init__.py +0 -0
  29. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/ids/generator.py +0 -0
  30. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/io/__init__.py +0 -0
  31. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/io/io_utils.py +0 -0
  32. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/io/printstream.py +0 -0
  33. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/logger/__init__.py +0 -0
  34. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/logger/manager.py +0 -0
  35. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/retry/__init__.py +0 -0
  36. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/base/retry/retry.py +0 -0
  37. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/compat.py +0 -0
  38. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/dispatch/__init__.py +0 -0
  39. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/dispatch/candidate.py +0 -0
  40. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  41. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  42. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/dispatch/race.py +0 -0
  43. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/dispatch/selector.py +0 -0
  44. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/dispatch/usage.py +0 -0
  45. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/__init__.py +0 -0
  46. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  47. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/parsers/stream.py +0 -0
  48. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  49. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  50. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  51. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/prompt/history.py +0 -0
  52. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  53. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  54. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  55. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  56. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  57. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/protocols/entml.py +0 -0
  58. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
  59. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
  60. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/protocols/entml_thinking_parse.py +0 -0
  61. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  62. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  63. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/registry.py +0 -0
  64. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  65. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  66. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  67. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  68. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  69. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  70. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/keys.py +0 -0
  71. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/lifecycle/__init__.py +0 -0
  72. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/lifecycle/manager.py +0 -0
  73. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/lifecycle/updater.py +0 -0
  74. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  75. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/process/__init__.py +0 -0
  76. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/process/port.py +0 -0
  77. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/protocol/__init__.py +0 -0
  78. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/protocol/base.py +0 -0
  79. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/__init__.py +0 -0
  80. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/conpty.py +0 -0
  81. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/local/__init__.py +0 -0
  82. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/local/core.py +0 -0
  83. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/local/proc.py +0 -0
  84. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/local/unix.py +0 -0
  85. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/local/win.py +0 -0
  86. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/sanitize.py +0 -0
  87. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/session/__init__.py +0 -0
  88. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/session/base.py +0 -0
  89. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/session/client.py +0 -0
  90. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/ssh.py +0 -0
  91. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/exec/terminal/tmux.py +0 -0
  92. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/events/__init__.py +0 -0
  93. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/events/bus.py +0 -0
  94. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/events/event.py +0 -0
  95. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/spinner/__init__.py +0 -0
  96. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/spinner/spinner.py +0 -0
  97. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/spinner/spinner_color.py +0 -0
  98. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/tracing/__init__.py +0 -0
  99. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/tracing/context.py +0 -0
  100. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/tracing/span.py +0 -0
  101. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/tracing/tracer.py +0 -0
  102. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/translate.py +0 -0
  103. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/watcher/__init__.py +0 -0
  104. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/watcher/file_watcher.py +0 -0
  105. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/__init__.py +0 -0
  106. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/application.py +0 -0
  107. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/broker.py +0 -0
  108. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/app.js +0 -0
  109. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  110. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  111. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  112. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  113. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  114. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  115. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  116. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  117. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  118. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  119. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/fonts/tailwind.css +0 -0
  120. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/inputbox.css +0 -0
  121. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/inputbox.js +0 -0
  122. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/style.css +0 -0
  123. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/input_box/voicegif.js +0 -0
  124. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/middleware.py +0 -0
  125. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/stats.py +0 -0
  126. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/media/web/utils.py +0 -0
  127. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/files/__init__.py +0 -0
  128. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/files/file_util.py +0 -0
  129. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/network/__init__.py +0 -0
  130. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/network/http_utils.py +0 -0
  131. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/plugin/__init__.py +0 -0
  132. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/plugin/base.py +0 -0
  133. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/plugin/discovery.py +0 -0
  134. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/plugin/registry.py +0 -0
  135. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/plugin/registry_ops.py +0 -0
  136. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/proxy/__init__.py +0 -0
  137. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/proxy/manager.py +0 -0
  138. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/runtime/__init__.py +0 -0
  139. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/runtime/collector.py +0 -0
  140. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/scheduler/__init__.py +0 -0
  141. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/scheduler/scheduler.py +0 -0
  142. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/sdk/__init__.py +0 -0
  143. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/plat/sdk/facade.py +0 -0
  144. {echotools-2.3.19 → echotools-2.3.22}/src/echotools/version.py +0 -0
  145. {echotools-2.3.19 → echotools-2.3.22}/src/echotools.egg-info/dependency_links.txt +0 -0
  146. {echotools-2.3.19 → echotools-2.3.22}/src/echotools.egg-info/requires.txt +0 -0
  147. {echotools-2.3.19 → echotools-2.3.22}/src/echotools.egg-info/top_level.txt +0 -0
  148. {echotools-2.3.19 → echotools-2.3.22}/src/tests/bench_import.py +0 -0
  149. {echotools-2.3.19 → echotools-2.3.22}/src/tests/conftest.py +0 -0
  150. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_cache_retry.py +0 -0
  151. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_completion.py +0 -0
  152. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_config.py +0 -0
  153. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_config_extended.py +0 -0
  154. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_coverage_core.py +0 -0
  155. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_dispatch.py +0 -0
  156. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_dispatch_extended.py +0 -0
  157. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_dispatch_race.py +0 -0
  158. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_entml_thinking_parse.py +0 -0
  159. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_entml_tools.py +0 -0
  160. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_errors.py +0 -0
  161. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_errors_common.py +0 -0
  162. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_events.py +0 -0
  163. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_events_event.py +0 -0
  164. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_facade.py +0 -0
  165. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_fncall.py +0 -0
  166. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_ids.py +0 -0
  167. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_imports.py +0 -0
  168. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_io.py +0 -0
  169. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_keys.py +0 -0
  170. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_lifecycle.py +0 -0
  171. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_logger.py +0 -0
  172. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_merge.py +0 -0
  173. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_plugin.py +0 -0
  174. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_printstream.py +0 -0
  175. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_protocols.py +0 -0
  176. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_proxy.py +0 -0
  177. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_proxy_selector.py +0 -0
  178. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_runtime.py +0 -0
  179. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_scheduler.py +0 -0
  180. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_tracing.py +0 -0
  181. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_usage.py +0 -0
  182. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_web.py +0 -0
  183. {echotools-2.3.19 → echotools-2.3.22}/src/tests/test_web_broker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.19
3
+ Version: 2.3.22
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echotools"
7
- version = "2.3.19"
7
+ version = "2.3.22"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -47,9 +47,23 @@ def normalize_thinking_mode(mode: Any) -> Optional[str]:
47
47
  return None
48
48
 
49
49
 
50
+ def parse_max_thinking_length(value: Any) -> Optional[int]:
51
+ """仅当显式传入正整数时返回长度;否则为 None(不注入标签)。"""
52
+ if value is None:
53
+ return None
54
+ if isinstance(value, str) and not value.strip():
55
+ return None
56
+ try:
57
+ parsed = int(value)
58
+ except (TypeError, ValueError):
59
+ return None
60
+ if parsed <= 0:
61
+ return None
62
+ return parsed
63
+
64
+
50
65
  def _policy_header(mode: str) -> str:
51
66
  labels = {
52
- "off": "off (forced no thinking)",
53
67
  "on": "on (forced thinking)",
54
68
  "auto": "auto (model decides)",
55
69
  }
@@ -59,17 +73,6 @@ def _policy_header(mode: str) -> str:
59
73
  )
60
74
 
61
75
 
62
- def _prompt_off() -> list[str]:
63
- return [
64
- _policy_header("off"),
65
- "",
66
- "You must NOT output any thinking blocks in this response.",
67
- f"Do not write `{_THINKING_BLOCK_OPEN}` or `{_THINKING_BLOCK_CLOSE}`.",
68
- "Respond with only the visible answer text and any "
69
- "`<entml:function_calls>` blocks if tools are needed.",
70
- ]
71
-
72
-
73
76
  def _prompt_on() -> list[str]:
74
77
  return [
75
78
  _policy_header("on"),
@@ -115,10 +118,13 @@ def _prompt_auto() -> list[str]:
115
118
  def build_entml_thinking_section(
116
119
  protocol_options: Optional[Dict[str, Any]] = None,
117
120
  ) -> str:
118
- """按 thinking_mode 构建思考链指令块(off / on / auto 三套 prompt)。"""
121
+ """按 thinking_mode 构建思考链指令块(on / auto);off 时不注入任何内容。"""
119
122
  opts = protocol_options or {}
120
123
  mode = normalize_thinking_mode(opts.get("thinking_mode"))
121
- max_thinking_length = opts.get("max_thinking_length")
124
+ max_thinking_length = parse_max_thinking_length(opts.get("max_thinking_length"))
125
+
126
+ if mode == "off":
127
+ return ""
122
128
 
123
129
  if mode is None and max_thinking_length is None:
124
130
  return ""
@@ -128,12 +134,10 @@ def build_entml_thinking_section(
128
134
  lines.append(f"<entml:thinking_mode>{mode}</entml:thinking_mode>")
129
135
  if max_thinking_length is not None:
130
136
  lines.append(
131
- f"<entml:max_thinking_length>{int(max_thinking_length)}</entml:max_thinking_length>"
137
+ f"<entml:max_thinking_length>{max_thinking_length}</entml:max_thinking_length>"
132
138
  )
133
139
 
134
- if mode == "off":
135
- lines.extend([""] + _prompt_off())
136
- elif mode == "on":
140
+ if mode == "on":
137
141
  lines.extend([""] + _prompt_on())
138
142
  elif mode == "auto":
139
143
  lines.extend([""] + _prompt_auto())
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Mapping, Optional
4
+
5
+ REASONING_KEYS = ("reasoning", "reasoning_content", "reasoning_details")
6
+ HISTORY_FLAG_KEYS = (
7
+ "include_thinking_in_history",
8
+ "pass_thinking",
9
+ "include_thinking",
10
+ "interleaved_history",
11
+ )
12
+ _THINKING_BLOCK_TYPES = frozenset({"thinking", "reasoning", "redacted_thinking"})
13
+
14
+
15
+ def parse_interleaved_history(
16
+ body: Mapping[str, Any],
17
+ extra: Optional[Mapping[str, Any]] = None,
18
+ thinking: Any = None,
19
+ ) -> bool:
20
+ """解析是否将历史 assistant 思考链传给模型(Entropy interleaved_history)。"""
21
+ extra = extra or {}
22
+ if thinking is None:
23
+ thinking = body.get("thinking")
24
+ for key in ("interleaved_history", "include_in_history"):
25
+ if key in body:
26
+ return bool(body[key])
27
+ if key in extra:
28
+ return bool(extra[key])
29
+ if isinstance(thinking, dict):
30
+ for key in ("interleaved_history", "include_in_history"):
31
+ if key in thinking:
32
+ return bool(thinking[key])
33
+ return False
34
+
35
+
36
+ def _thinking_from_content_block(block: Any) -> str:
37
+ if not isinstance(block, dict):
38
+ return ""
39
+ btype = str(block.get("type", "")).strip().lower()
40
+ if btype in ("thinking", "redacted_thinking"):
41
+ val = block.get("thinking") or block.get("data")
42
+ return str(val).strip() if val else ""
43
+ if btype == "reasoning":
44
+ val = block.get("text") or block.get("reasoning")
45
+ return str(val).strip() if val else ""
46
+ return ""
47
+
48
+
49
+ def extract_reasoning_text(msg: Mapping[str, Any]) -> str:
50
+ """从 assistant 消息字段或 Entropy/Anthropic 内容块提取思考文本。"""
51
+ for key in ("reasoning", "reasoning_content"):
52
+ val = msg.get(key)
53
+ if isinstance(val, str) and val.strip():
54
+ return val.strip()
55
+
56
+ details = msg.get("reasoning_details")
57
+ if isinstance(details, list):
58
+ parts: List[str] = []
59
+ for item in details:
60
+ if not isinstance(item, dict):
61
+ continue
62
+ text = item.get("text")
63
+ if text:
64
+ parts.append(str(text))
65
+ if parts:
66
+ return "".join(parts)
67
+
68
+ content = msg.get("content")
69
+ if isinstance(content, list):
70
+ parts = []
71
+ for block in content:
72
+ text = _thinking_from_content_block(block)
73
+ if text:
74
+ parts.append(text)
75
+ if parts:
76
+ return "\n".join(parts)
77
+ return ""
78
+
79
+
80
+ def _strip_thinking_blocks(content: List[Any]) -> List[Any]:
81
+ kept: List[Any] = []
82
+ for block in content:
83
+ if isinstance(block, dict) and str(block.get("type", "")).lower() in _THINKING_BLOCK_TYPES:
84
+ continue
85
+ kept.append(block)
86
+ return kept
87
+
88
+
89
+ def _collapse_text_content(blocks: List[Any]) -> Any:
90
+ if len(blocks) == 1:
91
+ only = blocks[0]
92
+ if isinstance(only, dict) and only.get("type") == "text":
93
+ return only.get("text", "")
94
+ return blocks
95
+
96
+
97
+ def _strip_reasoning_from_message(msg: Dict[str, Any]) -> Dict[str, Any]:
98
+ out = dict(msg)
99
+ for key in REASONING_KEYS:
100
+ out.pop(key, None)
101
+ content = out.get("content")
102
+ if isinstance(content, list):
103
+ stripped = _strip_thinking_blocks(content)
104
+ out["content"] = _collapse_text_content(stripped) if stripped else ""
105
+ return out
106
+
107
+
108
+ def _normalize_message_with_reasoning(msg: Dict[str, Any]) -> Dict[str, Any]:
109
+ out = dict(msg)
110
+ if out.get("role") != "assistant":
111
+ for key in REASONING_KEYS:
112
+ out.pop(key, None)
113
+ return out
114
+
115
+ text = extract_reasoning_text(out)
116
+ if text:
117
+ out["reasoning"] = text
118
+ out.setdefault("reasoning_content", text)
119
+ return out
120
+
121
+
122
+ def apply_thinking_history_policy(
123
+ messages: List[Dict[str, Any]],
124
+ include: bool,
125
+ ) -> List[Dict[str, Any]]:
126
+ """按 interleaved_history 保留或剥离历史思考链。
127
+
128
+ include=True: 保留 reasoning 字段与 content 中的 thinking 块。
129
+ include=False: 仅保留可见回复,剥离思考链。
130
+ """
131
+ if include:
132
+ return [_normalize_message_with_reasoning(dict(m)) for m in messages]
133
+ return [_strip_reasoning_from_message(dict(m)) for m in messages]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.19
3
+ Version: 2.3.22
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
6
  License: MIT
@@ -58,6 +58,7 @@ src/echotools/exec/fncall/protocols/entml.py
58
58
  src/echotools/exec/fncall/protocols/entml_invoke.py
59
59
  src/echotools/exec/fncall/protocols/entml_patterns.py
60
60
  src/echotools/exec/fncall/protocols/entml_thinking.py
61
+ src/echotools/exec/fncall/protocols/entml_thinking_history.py
61
62
  src/echotools/exec/fncall/protocols/entml_thinking_parse.py
62
63
  src/echotools/exec/fncall/protocols/entml_tools.py
63
64
  src/echotools/exec/fncall/protocols/entml_values.py
@@ -151,6 +152,7 @@ src/tests/test_dispatch.py
151
152
  src/tests/test_dispatch_extended.py
152
153
  src/tests/test_dispatch_race.py
153
154
  src/tests/test_entml_protocol.py
155
+ src/tests/test_entml_thinking_history.py
154
156
  src/tests/test_entml_thinking_parse.py
155
157
  src/tests/test_entml_tools.py
156
158
  src/tests/test_errors.py
@@ -72,6 +72,17 @@ def test_build_entml_thinking_section_empty_without_options() -> None:
72
72
  assert build_entml_thinking_section({}) == ""
73
73
 
74
74
 
75
+ def test_parse_max_thinking_length() -> None:
76
+ from echotools.exec.fncall.protocols.entml_thinking import parse_max_thinking_length
77
+
78
+ assert parse_max_thinking_length(None) is None
79
+ assert parse_max_thinking_length("") is None
80
+ assert parse_max_thinking_length(" ") is None
81
+ assert parse_max_thinking_length(0) is None
82
+ assert parse_max_thinking_length(22000) == 22000
83
+ assert parse_max_thinking_length("22000") == 22000
84
+
85
+
75
86
  @pytest.mark.parametrize(
76
87
  "raw,expected",
77
88
  [
@@ -90,11 +101,16 @@ def test_normalize_thinking_mode(raw, expected) -> None:
90
101
 
91
102
 
92
103
  def test_thinking_prompt_off() -> None:
93
- section = build_entml_thinking_section({"thinking_mode": "off"})
94
- assert "<entml:thinking_mode>off</entml:thinking_mode>" in section
95
- assert "forced no thinking" in section
96
- assert "You must NOT output any thinking blocks" in section
97
- assert "MUST output a thinking block" not in section
104
+ assert build_entml_thinking_section({"thinking_mode": "off"}) == ""
105
+ assert build_entml_thinking_section(
106
+ {"thinking_mode": "off", "max_thinking_length": 22000}
107
+ ) == ""
108
+
109
+
110
+ def test_thinking_prompt_on_without_max_length() -> None:
111
+ section = build_entml_thinking_section({"thinking_mode": "on"})
112
+ assert "<entml:thinking_mode>on</entml:thinking_mode>" in section
113
+ assert "<entml:max_thinking_length>" not in section
98
114
 
99
115
 
100
116
  def test_thinking_prompt_on() -> None:
@@ -124,8 +140,9 @@ def test_inject_no_tools_with_thinking_off() -> None:
124
140
  proto,
125
141
  protocol_options={"thinking_mode": "off"},
126
142
  )[0]["content"]
127
- assert "<entml:thinking_mode>off</entml:thinking_mode>" in result
128
- assert "You must NOT output any thinking blocks" in result
143
+ assert "<entml:thinking_mode>" not in result
144
+ assert "<entml:max_thinking_length>" not in result
145
+ assert "You must NOT output any thinking blocks" not in result
129
146
  assert "<current_user_message>\nhi\n</current_user_message>" in result
130
147
 
131
148
 
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from echotools.exec.fncall.protocols.entml_thinking_history import (
4
+ apply_thinking_history_policy,
5
+ extract_reasoning_text,
6
+ parse_interleaved_history,
7
+ )
8
+
9
+
10
+ def test_parse_interleaved_history_from_thinking_object() -> None:
11
+ body = {"thinking": {"mode": "on", "interleaved_history": True}}
12
+ assert parse_interleaved_history(body) is True
13
+ assert parse_interleaved_history({"thinking": {"interleaved_history": False}}) is False
14
+
15
+
16
+ def test_parse_interleaved_history_top_level() -> None:
17
+ assert parse_interleaved_history({"interleaved_history": True}) is True
18
+ assert parse_interleaved_history({}, extra={"interleaved_history": True}) is True
19
+
20
+
21
+ def test_strip_reasoning_fields_when_disabled() -> None:
22
+ msgs = [
23
+ {
24
+ "role": "assistant",
25
+ "content": "answer",
26
+ "reasoning": "thought",
27
+ "reasoning_content": "thought",
28
+ }
29
+ ]
30
+ out = apply_thinking_history_policy(msgs, False)
31
+ assert out[0]["content"] == "answer"
32
+ assert "reasoning" not in out[0]
33
+
34
+
35
+ def test_strip_entropy_thinking_blocks_when_disabled() -> None:
36
+ msgs = [
37
+ {
38
+ "role": "assistant",
39
+ "content": [
40
+ {"type": "thinking", "thinking": "step 1"},
41
+ {"type": "text", "text": "hello"},
42
+ ],
43
+ }
44
+ ]
45
+ out = apply_thinking_history_policy(msgs, False)
46
+ assert out[0]["content"] == "hello"
47
+ assert "reasoning" not in out[0]
48
+
49
+
50
+ def test_preserve_entropy_thinking_blocks_when_enabled() -> None:
51
+ msgs = [
52
+ {
53
+ "role": "assistant",
54
+ "content": [
55
+ {"type": "thinking", "thinking": "step 1"},
56
+ {"type": "text", "text": "hello"},
57
+ ],
58
+ }
59
+ ]
60
+ out = apply_thinking_history_policy(msgs, True)
61
+ assert out[0]["reasoning"] == "step 1"
62
+ assert out[0]["reasoning_content"] == "step 1"
63
+ assert out[0]["content"][0]["type"] == "thinking"
64
+
65
+
66
+ def test_extract_reasoning_text_from_blocks() -> None:
67
+ msg = {
68
+ "role": "assistant",
69
+ "content": [{"type": "thinking", "thinking": "a"}, {"type": "text", "text": "b"}],
70
+ }
71
+ assert extract_reasoning_text(msg) == "a"
File without changes
File without changes
File without changes