echotools 2.3.60__tar.gz → 2.3.62__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 (227) hide show
  1. {echotools-2.3.60 → echotools-2.3.62}/PKG-INFO +1 -1
  2. {echotools-2.3.60 → echotools-2.3.62}/pyproject.toml +1 -1
  3. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/parsers/stream.py +18 -6
  4. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +2 -0
  5. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_think/core.py +10 -0
  6. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_think/parse.py +221 -40
  7. {echotools-2.3.60 → echotools-2.3.62}/src/echotools.egg-info/PKG-INFO +1 -1
  8. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_thinking_parse.py +59 -0
  9. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_fncall_stream_thinking.py +65 -0
  10. {echotools-2.3.60 → echotools-2.3.62}/README.md +0 -0
  11. {echotools-2.3.60 → echotools-2.3.62}/docs/README.md +0 -0
  12. {echotools-2.3.60 → echotools-2.3.62}/setup.cfg +0 -0
  13. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/__init__.py +0 -0
  14. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/cache/__init__.py +0 -0
  15. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/cache/list_cache.py +0 -0
  16. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/cache/memory_cache.py +0 -0
  17. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/__init__.py +0 -0
  18. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/base.py +0 -0
  19. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/center.py +0 -0
  20. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/center_notify.py +0 -0
  21. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/center_template.py +0 -0
  22. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/loader.py +0 -0
  23. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/config/merge.py +0 -0
  24. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/errors/__init__.py +0 -0
  25. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/errors/base.py +0 -0
  26. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/errors/classify.py +0 -0
  27. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/errors/common.py +0 -0
  28. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/errors/http.py +0 -0
  29. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/ids/__init__.py +0 -0
  30. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/ids/generator.py +0 -0
  31. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/io/__init__.py +0 -0
  32. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/io/io_utils.py +0 -0
  33. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/io/printstream.py +0 -0
  34. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/logger/__init__.py +0 -0
  35. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/logger/manager.py +0 -0
  36. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/retry/__init__.py +0 -0
  37. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/base/retry/retry.py +0 -0
  38. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/compat.py +0 -0
  39. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/__init__.py +0 -0
  40. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/candidate.py +0 -0
  41. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  42. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  43. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/race.py +0 -0
  44. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/selector.py +0 -0
  45. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/dispatch/usage.py +0 -0
  46. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/__init__.py +0 -0
  47. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  48. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  49. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  50. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  51. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/prompt/history.py +0 -0
  52. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  53. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  54. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  55. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  56. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  57. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml.py +0 -0
  58. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
  59. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
  60. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_stream_json.py +0 -0
  61. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  62. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  63. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  64. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/registry.py +0 -0
  65. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  66. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  67. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  68. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  69. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  70. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  71. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/keys.py +0 -0
  72. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/lifecycle/__init__.py +0 -0
  73. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/lifecycle/manager.py +0 -0
  74. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/lifecycle/updater.py +0 -0
  75. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  76. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/process/__init__.py +0 -0
  77. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/process/port.py +0 -0
  78. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/protocol/__init__.py +0 -0
  79. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/protocol/base.py +0 -0
  80. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/__init__.py +0 -0
  81. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/conpty.py +0 -0
  82. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/local/__init__.py +0 -0
  83. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/local/core.py +0 -0
  84. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/local/proc.py +0 -0
  85. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/local/unix.py +0 -0
  86. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/local/win.py +0 -0
  87. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/sanitize.py +0 -0
  88. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/session/__init__.py +0 -0
  89. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/session/base.py +0 -0
  90. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/session/client.py +0 -0
  91. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/ssh.py +0 -0
  92. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/exec/terminal/tmux.py +0 -0
  93. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/__init__.py +0 -0
  94. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/charmap.py +0 -0
  95. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/spinner.py +0 -0
  96. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/ui.py +0 -0
  97. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  98. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  99. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  100. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  101. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  102. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  103. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/ui_console.py +0 -0
  104. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/ui_io.py +0 -0
  105. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/ui_log.py +0 -0
  106. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  107. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/ui_text.py +0 -0
  108. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uicore/ui_types.py +0 -0
  109. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  110. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  111. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  112. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  113. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  114. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  115. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  116. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  117. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  118. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  119. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  120. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/events/__init__.py +0 -0
  121. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/events/bus.py +0 -0
  122. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/events/event.py +0 -0
  123. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/spinner/__init__.py +0 -0
  124. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/spinner/spinner.py +0 -0
  125. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/spinner/spinner_color.py +0 -0
  126. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/tracing/__init__.py +0 -0
  127. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/tracing/context.py +0 -0
  128. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/tracing/span.py +0 -0
  129. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/tracing/tracer.py +0 -0
  130. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/translate.py +0 -0
  131. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/watcher/__init__.py +0 -0
  132. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/watcher/file_watcher.py +0 -0
  133. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/__init__.py +0 -0
  134. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/application.py +0 -0
  135. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/broker.py +0 -0
  136. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/app.js +0 -0
  137. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  138. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  139. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  140. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  141. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  142. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  143. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  144. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  145. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  146. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  147. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/inputbox.css +0 -0
  148. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/inputbox.js +0 -0
  149. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/style.css +0 -0
  150. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/input_box/voicegif.js +0 -0
  151. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/middleware.py +0 -0
  152. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/stats.py +0 -0
  153. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/media/web/utils.py +0 -0
  154. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/files/__init__.py +0 -0
  155. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/files/file_util.py +0 -0
  156. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/network/__init__.py +0 -0
  157. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/network/http_utils.py +0 -0
  158. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/plugin/__init__.py +0 -0
  159. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/plugin/base.py +0 -0
  160. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/plugin/discovery.py +0 -0
  161. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/plugin/registry.py +0 -0
  162. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/plugin/registry_ops.py +0 -0
  163. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/proxy/__init__.py +0 -0
  164. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/proxy/manager.py +0 -0
  165. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/runtime/__init__.py +0 -0
  166. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/runtime/collector.py +0 -0
  167. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/scheduler/__init__.py +0 -0
  168. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/scheduler/scheduler.py +0 -0
  169. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/sdk/__init__.py +0 -0
  170. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/plat/sdk/facade.py +0 -0
  171. {echotools-2.3.60 → echotools-2.3.62}/src/echotools/version.py +0 -0
  172. {echotools-2.3.60 → echotools-2.3.62}/src/echotools.egg-info/SOURCES.txt +0 -0
  173. {echotools-2.3.60 → echotools-2.3.62}/src/echotools.egg-info/dependency_links.txt +0 -0
  174. {echotools-2.3.60 → echotools-2.3.62}/src/echotools.egg-info/requires.txt +0 -0
  175. {echotools-2.3.60 → echotools-2.3.62}/src/echotools.egg-info/top_level.txt +0 -0
  176. {echotools-2.3.60 → echotools-2.3.62}/src/tests/bench_import.py +0 -0
  177. {echotools-2.3.60 → echotools-2.3.62}/src/tests/conftest.py +0 -0
  178. {echotools-2.3.60 → echotools-2.3.62}/src/tests/fixtures/__init__.py +0 -0
  179. {echotools-2.3.60 → echotools-2.3.62}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
  180. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_adversarial_stream_splits.py +0 -0
  181. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_cache_retry.py +0 -0
  182. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_completion.py +0 -0
  183. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_config.py +0 -0
  184. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_config_extended.py +0 -0
  185. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_console.py +0 -0
  186. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_coverage_core.py +0 -0
  187. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_dispatch.py +0 -0
  188. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_dispatch_extended.py +0 -0
  189. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_dispatch_race.py +0 -0
  190. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_agent_output_formats.py +0 -0
  191. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_bare_invoke_format.py +0 -0
  192. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_parser_comprehensive.py +0 -0
  193. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_protocol.py +0 -0
  194. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_stream_json.py +0 -0
  195. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
  196. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_thinking_history.py +0 -0
  197. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_tool_parse_leak.py +0 -0
  198. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_tool_pipeline.py +0 -0
  199. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_entml_tools.py +0 -0
  200. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_errors.py +0 -0
  201. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_errors_common.py +0 -0
  202. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_events.py +0 -0
  203. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_events_event.py +0 -0
  204. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_facade.py +0 -0
  205. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_fncall.py +0 -0
  206. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_ids.py +0 -0
  207. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_imports.py +0 -0
  208. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_io.py +0 -0
  209. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_keys.py +0 -0
  210. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_lifecycle.py +0 -0
  211. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_logger.py +0 -0
  212. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_merge.py +0 -0
  213. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_model_branch_matrix.py +0 -0
  214. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_plugin.py +0 -0
  215. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_printstream.py +0 -0
  216. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_protocols.py +0 -0
  217. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_proxy.py +0 -0
  218. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_proxy_selector.py +0 -0
  219. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_runtime.py +0 -0
  220. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_scheduler.py +0 -0
  221. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  222. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_stream_partial_json_parity.py +0 -0
  223. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_stream_realtime_parse.py +0 -0
  224. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_tracing.py +0 -0
  225. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_usage.py +0 -0
  226. {echotools-2.3.60 → echotools-2.3.62}/src/tests/test_web.py +0 -0
  227. {echotools-2.3.60 → echotools-2.3.62}/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.60
3
+ Version: 2.3.62
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echotools"
7
- version = "2.3.60"
7
+ version = "2.3.62"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -12,12 +12,12 @@ from echotools.exec.protocol.base import ToolProtocol
12
12
 
13
13
 
14
14
  # 懒导入:仅 entml 协议需要
15
- def _make_thinking_filter(protocol: ToolProtocol):
15
+ def _make_thinking_filter(protocol: ToolProtocol, *, thinking_enabled: bool = True):
16
16
  if getattr(protocol, "id", None) == "entml":
17
17
  from echotools.exec.fncall.protocols.entml_think.parse import (
18
18
  EntmlThinkingStreamFilter,
19
19
  )
20
- return EntmlThinkingStreamFilter()
20
+ return EntmlThinkingStreamFilter(thinking_enabled=thinking_enabled)
21
21
  return None
22
22
 
23
23
 
@@ -40,9 +40,13 @@ class FncallStreamParser:
40
40
  self,
41
41
  protocol: ToolProtocol,
42
42
  tools: Optional[List[Dict[str, Any]]] = None,
43
+ protocol_options: Optional[Dict[str, Any]] = None,
43
44
  ) -> None:
44
45
  self._protocol = protocol
45
46
  self._tools = tools
47
+ from echotools.exec.fncall.protocols.entml_think.core import is_thinking_enabled
48
+
49
+ self._thinking_enabled = is_thinking_enabled(protocol_options)
46
50
  self._raw_buf: str = ""
47
51
  self._text_parts: List[str] = []
48
52
  self._waiting_tail: str = ""
@@ -51,7 +55,9 @@ class FncallStreamParser:
51
55
  self._state: str = self.WAITING_FOR_TAG
52
56
  self._finalized_result: Optional[Tuple[str, List[Dict[str, Any]]]] = None
53
57
  self._thinking_parts: List[str] = []
54
- self._thinking_filter = _make_thinking_filter(protocol)
58
+ self._thinking_filter = _make_thinking_filter(
59
+ protocol, thinking_enabled=self._thinking_enabled
60
+ )
55
61
  self._emitted_invoke_count: int = 0
56
62
  self._json_stream_encoder = None
57
63
  self._pending_stream_deltas: Deque[Tuple[str, str]] = deque()
@@ -298,7 +304,9 @@ class FncallStreamParser:
298
304
  from echotools.exec.fncall.protocols.entml_think.parse import (
299
305
  invoke_index_inside_unclosed_thinking,
300
306
  )
301
- if invoke_index_inside_unclosed_thinking(combined, pos):
307
+ if invoke_index_inside_unclosed_thinking(
308
+ combined, pos, thinking_enabled=self._thinking_enabled
309
+ ):
302
310
  self._feed_waiting_thinking_plain(combined)
303
311
  return
304
312
  if pos > 0:
@@ -314,7 +322,9 @@ class FncallStreamParser:
314
322
  from echotools.exec.fncall.protocols.entml_think.parse import (
315
323
  has_unclosed_entml_thinking,
316
324
  )
317
- if has_unclosed_entml_thinking(combined):
325
+ if has_unclosed_entml_thinking(
326
+ combined, thinking_enabled=self._thinking_enabled
327
+ ):
318
328
  self._feed_waiting_thinking_plain(combined)
319
329
  return
320
330
 
@@ -633,7 +643,9 @@ class FncallStreamParser:
633
643
  from echotools.exec.fncall.protocols.entml_think.parse import (
634
644
  split_entml_thinking,
635
645
  )
636
- clean_text, more_thinking = split_entml_thinking(clean_text)
646
+ clean_text, more_thinking = split_entml_thinking(
647
+ clean_text, thinking_enabled=self._thinking_enabled
648
+ )
637
649
  if more_thinking:
638
650
  self._thinking_parts.append(more_thinking)
639
651
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from echotools.exec.fncall.protocols.entml_think.core import (
4
4
  build_entml_thinking_section,
5
+ is_thinking_enabled,
5
6
  parse_max_thinking_length,
6
7
  )
7
8
  from echotools.exec.fncall.protocols.entml_think.hist import (
@@ -24,6 +25,7 @@ __all__ = [
24
25
  "extract_reasoning_text",
25
26
  "format_entml_thinking_history_block",
26
27
  "has_unclosed_entml_thinking",
28
+ "is_thinking_enabled",
27
29
  "parse_include_thinking_in_history",
28
30
  "parse_interleaved_history",
29
31
  "parse_max_thinking_length",
@@ -159,6 +159,16 @@ def resolve_thinking_injection(
159
159
  return injection_mode, max_length
160
160
 
161
161
 
162
+ def is_thinking_enabled(protocol_options: Optional[Dict[str, Any]] = None) -> bool:
163
+ """是否开启思考模式(容错与 plain ``<thinking>`` 仅在此为 True 时生效)。
164
+
165
+ 未传 ``protocol_options`` 时视为已开启(兼容旧调用);显式 ``off`` / ``none`` 时关闭。
166
+ """
167
+ if protocol_options is None:
168
+ return True
169
+ return resolve_thinking_injection(protocol_options) is not None
170
+
171
+
162
172
  def _uses_forced_thinking_behavior(injection_mode: str) -> bool:
163
173
  return injection_mode != "auto"
164
174
 
@@ -16,6 +16,7 @@ THINKING_BLOCK_RE = re.compile(
16
16
  )
17
17
 
18
18
  _THINKING_OPEN_PREFIX = "<entml:thinking"
19
+ _PLAIN_THINKING_OPEN_PREFIX = "<thinking"
19
20
  _THINKING_CLOSE = "</entml:thinking>"
20
21
  _FAULT_THINKING_CLOSE = "</thinking>"
21
22
  # 与 thinking 共享 `<entml:` 前缀的其它标签:歧义 holdback 应交由工具流式状态机处理
@@ -116,6 +117,95 @@ def find_ambiguous_entml_tool_prefix(buffer: str) -> int:
116
117
  return earliest
117
118
 
118
119
 
120
+ def _find_plain_thinking_open(text: str, start: int = 0) -> int:
121
+ """返回 plain ``<thinking>`` 开标签起始下标(``<entml:thinking>`` 内不含此子串)。"""
122
+ return text.find(_PLAIN_THINKING_OPEN_PREFIX, start)
123
+
124
+
125
+ def _find_earliest_thinking_open(
126
+ text: str,
127
+ start: int = 0,
128
+ *,
129
+ thinking_enabled: bool = True,
130
+ ) -> Tuple[int, bool]:
131
+ """返回 ``(开标签下标, 是否 plain)``;无则 ``(-1, False)``。"""
132
+ entml_at = text.find(_THINKING_OPEN_PREFIX, start)
133
+ if not thinking_enabled:
134
+ if entml_at < 0:
135
+ return -1, False
136
+ return entml_at, False
137
+ plain_at = _find_plain_thinking_open(text, start)
138
+ if entml_at < 0 and plain_at < 0:
139
+ return -1, False
140
+ if entml_at < 0:
141
+ return plain_at, True
142
+ if plain_at < 0:
143
+ return entml_at, False
144
+ if plain_at < entml_at:
145
+ return plain_at, True
146
+ return entml_at, False
147
+
148
+
149
+ def _find_thinking_close(
150
+ text: str,
151
+ body_start: int,
152
+ *,
153
+ opened_plain: bool,
154
+ thinking_enabled: bool = True,
155
+ ) -> Tuple[int, int]:
156
+ """返回 ``(闭标签下标, 闭标签长度)``;未找到则 ``(-1, 0)``。"""
157
+ if not thinking_enabled:
158
+ close_at = text.find(_THINKING_CLOSE, body_start)
159
+ if close_at >= 0:
160
+ return close_at, len(_THINKING_CLOSE)
161
+ return -1, 0
162
+
163
+ if opened_plain:
164
+ entml_close = text.find(_THINKING_CLOSE, body_start)
165
+ fault_close = text.find(_FAULT_THINKING_CLOSE, body_start)
166
+ candidates: List[Tuple[int, int]] = []
167
+ if entml_close >= 0:
168
+ candidates.append((entml_close, len(_THINKING_CLOSE)))
169
+ if fault_close >= 0:
170
+ candidates.append((fault_close, len(_FAULT_THINKING_CLOSE)))
171
+ if not candidates:
172
+ return -1, 0
173
+ close_at, close_len = min(candidates, key=lambda item: item[0])
174
+ return close_at, close_len
175
+
176
+ entml_close = text.find(_THINKING_CLOSE, body_start)
177
+ fault_close = text.find(_FAULT_THINKING_CLOSE, body_start)
178
+ if entml_close >= 0 and (fault_close < 0 or entml_close <= fault_close):
179
+ return entml_close, len(_THINKING_CLOSE)
180
+ if fault_close < 0:
181
+ return -1, 0
182
+
183
+ after_fault = fault_close + len(_FAULT_THINKING_CLOSE)
184
+ invoke_rel = find_complete_entml_invoke_open(text[after_fault:])
185
+ entml_after = text.find(_THINKING_CLOSE, after_fault)
186
+ if invoke_rel >= 0:
187
+ invoke_abs = after_fault + invoke_rel
188
+ if entml_after < 0 or invoke_abs < entml_after:
189
+ return fault_close, len(_FAULT_THINKING_CLOSE)
190
+ if entml_after >= 0:
191
+ return entml_after, len(_THINKING_CLOSE)
192
+ return -1, 0
193
+
194
+
195
+ def _hold_thinking_open_prefixes(
196
+ buffer: str,
197
+ *,
198
+ thinking_enabled: bool = True,
199
+ ) -> Tuple[str, str]:
200
+ """hold ``<entml:thinking`` 或 plain ``<thinking`` 的真前缀。"""
201
+ safe, hold = _hold_prefix(buffer, _THINKING_OPEN_PREFIX)
202
+ if hold:
203
+ return safe, hold
204
+ if thinking_enabled:
205
+ return _hold_prefix(safe, _PLAIN_THINKING_OPEN_PREFIX)
206
+ return safe, ""
207
+
208
+
119
209
  def _hold_ambiguous_tool_markup(buffer: str) -> Tuple[str, str]:
120
210
  """thinking 块内:hold 可能长成真实工具调用的 markup(非 prose ``<entml:invoke>`` 提及)。"""
121
211
  if not buffer:
@@ -143,68 +233,125 @@ def _hold_ambiguous_tool_markup(buffer: str) -> Tuple[str, str]:
143
233
  return buffer, ""
144
234
 
145
235
 
146
- def has_unclosed_entml_thinking(text: str) -> bool:
147
- """buffer 中是否存在尚未闭合的 <entml:thinking> 块(含开标签未收齐)。"""
236
+ def has_unclosed_entml_thinking(
237
+ text: str,
238
+ *,
239
+ thinking_enabled: bool = True,
240
+ ) -> bool:
241
+ """buffer 中是否存在尚未闭合的 thinking 块(含开标签未收齐)。"""
148
242
  if not text:
149
243
  return False
150
244
  i = 0
151
245
  while i < len(text):
152
- open_at = text.find(_THINKING_OPEN_PREFIX, i)
246
+ open_at, opened_plain = _find_earliest_thinking_open(
247
+ text, i, thinking_enabled=thinking_enabled
248
+ )
153
249
  if open_at < 0:
154
250
  break
155
251
  gt = text.find(">", open_at)
156
252
  if gt < 0:
157
253
  return True
158
- close_at = text.find(_THINKING_CLOSE, gt + 1)
254
+ body_start = gt + 1
255
+ close_at, close_len = _find_thinking_close(
256
+ text,
257
+ body_start,
258
+ opened_plain=opened_plain,
259
+ thinking_enabled=thinking_enabled,
260
+ )
159
261
  if close_at < 0:
160
262
  return True
161
- i = close_at + len(_THINKING_CLOSE)
263
+ i = close_at + close_len
162
264
  max_keep = len(_THINKING_OPEN_PREFIX) - 1
265
+ if thinking_enabled:
266
+ max_keep = max(max_keep, len(_PLAIN_THINKING_OPEN_PREFIX) - 1)
163
267
  check_len = min(len(text), max_keep)
164
268
  for length in range(check_len, 0, -1):
165
269
  suffix = text[-length:]
166
- if not (
167
- _THINKING_OPEN_PREFIX.startswith(suffix) and suffix != _THINKING_OPEN_PREFIX
270
+ if (
271
+ _THINKING_OPEN_PREFIX.startswith(suffix)
272
+ and suffix != _THINKING_OPEN_PREFIX
168
273
  ):
169
- continue
170
- # `<e` / `<entml:` 等对 invoke 同样是真前缀 → 不在此抢占
171
- if any(other.startswith(suffix) for other in _AMBIGUOUS_ENTML_PREFIXES):
172
- continue
173
- return True
274
+ if any(other.startswith(suffix) for other in _AMBIGUOUS_ENTML_PREFIXES):
275
+ continue
276
+ return True
277
+ if thinking_enabled and (
278
+ _PLAIN_THINKING_OPEN_PREFIX.startswith(suffix)
279
+ and suffix != _PLAIN_THINKING_OPEN_PREFIX
280
+ ):
281
+ return True
174
282
  return False
175
283
 
176
284
 
177
- def invoke_index_inside_unclosed_thinking(text: str, invoke_at: int) -> bool:
178
- """``invoke_at`` 是否落在尚未正式 ``</entml:thinking>`` 闭合的块内。
179
-
180
- 已出现容错 ``</thinking>`` 且 invoke 在其后的段落,视为 thinking 外。
181
- """
285
+ def invoke_index_inside_unclosed_thinking(
286
+ text: str,
287
+ invoke_at: int,
288
+ *,
289
+ thinking_enabled: bool = True,
290
+ ) -> bool:
291
+ """``invoke_at`` 是否落在尚未闭合的 thinking 块内。"""
182
292
  if invoke_at < 0:
183
293
  return False
184
- think_open = text.find(_THINKING_OPEN_PREFIX)
294
+ think_open, opened_plain = _find_earliest_thinking_open(
295
+ text, thinking_enabled=thinking_enabled
296
+ )
185
297
  if think_open < 0 or invoke_at <= think_open:
186
298
  return False
187
- entml_close = text.find(_THINKING_CLOSE, think_open)
188
- if entml_close >= 0:
189
- return invoke_at < entml_close
190
- fault_at = text.find(_FAULT_THINKING_CLOSE, think_open)
191
- if fault_at >= 0:
192
- return invoke_at < fault_at + len(_FAULT_THINKING_CLOSE)
193
- return True
299
+ gt = text.find(">", think_open)
300
+ if gt < 0:
301
+ return True
302
+ body_start = gt + 1
303
+ close_at, close_len = _find_thinking_close(
304
+ text,
305
+ body_start,
306
+ opened_plain=opened_plain,
307
+ thinking_enabled=thinking_enabled,
308
+ )
309
+ if close_at < 0:
310
+ return True
311
+ return invoke_at < close_at + close_len
194
312
 
195
313
 
196
- def split_entml_thinking(text: str) -> Tuple[str, str]:
197
- """从文本中剥离 <entml:thinking> 块,返回 (正文, 思考链拼接)。"""
314
+ def split_entml_thinking(
315
+ text: str,
316
+ *,
317
+ thinking_enabled: bool = True,
318
+ ) -> Tuple[str, str]:
319
+ """从文本中剥离 thinking 块,返回 (正文, 思考链拼接)。"""
198
320
  if not text:
199
321
  return "", ""
200
322
 
201
323
  parts: List[str] = []
324
+ clean_parts: List[str] = []
325
+ i = 0
326
+ while i < len(text):
327
+ open_at, opened_plain = _find_earliest_thinking_open(
328
+ text, i, thinking_enabled=thinking_enabled
329
+ )
330
+ if open_at < 0:
331
+ clean_parts.append(text[i:])
332
+ break
333
+
334
+ clean_parts.append(text[i:open_at])
335
+ gt = text.find(">", open_at)
336
+ if gt < 0:
337
+ clean_parts.append(text[open_at:])
338
+ break
339
+
340
+ body_start = gt + 1
341
+ close_at, close_len = _find_thinking_close(
342
+ text,
343
+ body_start,
344
+ opened_plain=opened_plain,
345
+ thinking_enabled=thinking_enabled,
346
+ )
347
+ if close_at < 0:
348
+ clean_parts.append(text[open_at:])
349
+ break
202
350
 
203
- def _collect(match: re.Match[str]) -> str:
204
- parts.append(match.group(1))
205
- return ""
351
+ parts.append(text[body_start:close_at])
352
+ i = close_at + close_len
206
353
 
207
- clean = THINKING_BLOCK_RE.sub(_collect, text)
354
+ clean = "".join(clean_parts)
208
355
  thinking = "\n".join(part.strip() for part in parts if part.strip())
209
356
  return strip_orphan_thinking_close_prefix(clean.strip()), thinking
210
357
 
@@ -241,9 +388,11 @@ class EntmlThinkingStreamFilter:
241
388
  块内正文在收到时即增量输出 thinking,无需等闭合标签。
242
389
  """
243
390
 
244
- def __init__(self) -> None:
391
+ def __init__(self, *, thinking_enabled: bool = True) -> None:
392
+ self._thinking_enabled = thinking_enabled
245
393
  self._pending = ""
246
394
  self._in_block = False
395
+ self._opened_plain = False
247
396
  # 当前 thinking 块是否已输出过任何内容(用于首片去前导空白)
248
397
  self._thinking_started = False
249
398
  # 见到 ``</thinking>`` 后暂存,仅当后续出现 invoke 才视为闭合;否则当纯文本
@@ -253,20 +402,25 @@ class EntmlThinkingStreamFilter:
253
402
  def clear_invoke_handoff_state(self) -> None:
254
403
  """invoke 已交给正文/工具解析后,结束 thinking / fault 观察状态。"""
255
404
  self._in_block = False
405
+ self._opened_plain = False
256
406
  self._fault_watch = False
257
407
  self._fault_buffer = ""
258
408
  self._thinking_started = False
259
409
 
260
410
  def in_open_thinking(self) -> bool:
261
- """思考块已开始且尚未收到 ``</entml:thinking>``(含开/闭标签分片 hold)。"""
411
+ """思考块已开始且尚未闭合(含开/闭标签分片 hold)。"""
262
412
  if self._in_block or self._fault_watch:
263
413
  return True
264
- open_at = self._pending.find(_THINKING_OPEN_PREFIX)
414
+ open_at, _ = _find_earliest_thinking_open(
415
+ self._pending, thinking_enabled=self._thinking_enabled
416
+ )
265
417
  if open_at >= 0:
266
418
  gt = self._pending.find(">", open_at)
267
419
  if gt < 0:
268
420
  return True
269
- _, hold = _hold_prefix(self._pending, _THINKING_OPEN_PREFIX)
421
+ _, hold = _hold_thinking_open_prefixes(
422
+ self._pending, thinking_enabled=self._thinking_enabled
423
+ )
270
424
  if hold:
271
425
  return True
272
426
  return False
@@ -279,6 +433,7 @@ class EntmlThinkingStreamFilter:
279
433
  if invoke_at < len(_FAULT_THINKING_CLOSE):
280
434
  return False
281
435
  self._in_block = False
436
+ self._opened_plain = False
282
437
  self._fault_watch = False
283
438
  self._thinking_started = False
284
439
  self._pending = self._fault_buffer[len(_FAULT_THINKING_CLOSE) :]
@@ -296,6 +451,7 @@ class EntmlThinkingStreamFilter:
296
451
  if emitted:
297
452
  out.append(("thinking", emitted))
298
453
  self._in_block = False
454
+ self._opened_plain = False
299
455
  self._fault_watch = False
300
456
  self._thinking_started = False
301
457
  self._pending = self._fault_buffer[close_at + len(_THINKING_CLOSE) :]
@@ -323,12 +479,15 @@ class EntmlThinkingStreamFilter:
323
479
  out.append(("thinking", emitted))
324
480
  out.append(("content", invoke_block))
325
481
  self._in_block = False
482
+ self._opened_plain = False
326
483
  self._fault_watch = False
327
484
  self._fault_buffer = ""
328
485
  self._thinking_started = False
329
486
  rest = after.lstrip()
330
487
  if rest.startswith(_THINKING_CLOSE):
331
488
  rest = rest[len(_THINKING_CLOSE) :].lstrip()
489
+ if rest.startswith(_FAULT_THINKING_CLOSE):
490
+ rest = rest[len(_FAULT_THINKING_CLOSE) :].lstrip()
332
491
  self._pending = rest
333
492
  return True
334
493
  return False
@@ -361,6 +520,7 @@ class EntmlThinkingStreamFilter:
361
520
  if emitted:
362
521
  out.append(("thinking", emitted))
363
522
  self._in_block = False
523
+ self._opened_plain = False
364
524
  self._fault_watch = False
365
525
  self._fault_buffer = ""
366
526
  self._thinking_started = False
@@ -371,8 +531,19 @@ class EntmlThinkingStreamFilter:
371
531
  return True
372
532
 
373
533
  fault_at = self._pending.find(_FAULT_THINKING_CLOSE)
374
- if fault_at >= 0:
534
+ if fault_at >= 0 and self._thinking_enabled:
375
535
  before = self._pending[:fault_at]
536
+ if self._opened_plain:
537
+ emitted = self._emit_thinking_piece(before)
538
+ if emitted:
539
+ out.append(("thinking", emitted))
540
+ self._in_block = False
541
+ self._opened_plain = False
542
+ self._fault_watch = False
543
+ self._fault_buffer = ""
544
+ self._thinking_started = False
545
+ self._pending = self._pending[fault_at + len(_FAULT_THINKING_CLOSE) :]
546
+ return True
376
547
  emitted = self._emit_thinking_piece(before)
377
548
  if emitted:
378
549
  out.append(("thinking", emitted))
@@ -391,7 +562,7 @@ class EntmlThinkingStreamFilter:
391
562
  return False
392
563
 
393
564
  safe, hold = _hold_prefix(self._pending, _THINKING_CLOSE)
394
- if not hold:
565
+ if not hold and self._thinking_enabled:
395
566
  safe, hold = _hold_prefix(safe, _FAULT_THINKING_CLOSE)
396
567
  if safe:
397
568
  emitted = self._emit_thinking_piece(safe)
@@ -402,9 +573,13 @@ class EntmlThinkingStreamFilter:
402
573
 
403
574
  def _feed_before_block(self, out: List[Tuple[str, str]]) -> bool:
404
575
  """开标签之前或尚未进入块;返回 False 表示应退出 feed 主循环。"""
405
- open_at = self._pending.find(_THINKING_OPEN_PREFIX)
576
+ open_at, opened_plain = _find_earliest_thinking_open(
577
+ self._pending, thinking_enabled=self._thinking_enabled
578
+ )
406
579
  if open_at < 0:
407
- safe, self._pending = _hold_prefix(self._pending, _THINKING_OPEN_PREFIX)
580
+ safe, self._pending = _hold_thinking_open_prefixes(
581
+ self._pending, thinking_enabled=self._thinking_enabled
582
+ )
408
583
  if safe:
409
584
  out.append(("content", safe))
410
585
  return False
@@ -418,6 +593,7 @@ class EntmlThinkingStreamFilter:
418
593
  return False
419
594
 
420
595
  self._in_block = True
596
+ self._opened_plain = opened_plain
421
597
  self._thinking_started = False
422
598
  self._pending = self._pending[gt + 1 :]
423
599
  return True
@@ -471,6 +647,7 @@ class EntmlThinkingStreamFilter:
471
647
  self._fault_buffer = ""
472
648
  self._fault_watch = False
473
649
  self._in_block = False
650
+ self._opened_plain = False
474
651
  self._thinking_started = False
475
652
  if self._in_block:
476
653
  if not self._try_emit_actionable_invoke_from_in_block(out):
@@ -479,9 +656,12 @@ class EntmlThinkingStreamFilter:
479
656
  out.append(("thinking", emitted))
480
657
  self._pending = ""
481
658
  self._in_block = False
659
+ self._opened_plain = False
482
660
  self._thinking_started = False
483
661
  if self._pending:
484
- content, thinking = split_entml_thinking(self._pending)
662
+ content, thinking = split_entml_thinking(
663
+ self._pending, thinking_enabled=self._thinking_enabled
664
+ )
485
665
  if thinking:
486
666
  out.append(("thinking", thinking))
487
667
  if content:
@@ -490,6 +670,7 @@ class EntmlThinkingStreamFilter:
490
670
  out.append(("content", self._pending))
491
671
  self._pending = ""
492
672
  self._in_block = False
673
+ self._opened_plain = False
493
674
  self._fault_watch = False
494
675
  self._fault_buffer = ""
495
676
  self._thinking_started = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.60
3
+ Version: 2.3.62
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -92,3 +92,62 @@ def test_entml_thinking_hold_close_prefix() -> None:
92
92
  content = "".join(t for k, t in parts if k == "content")
93
93
  assert thinking == "ab"
94
94
  assert content == "cd"
95
+
96
+
97
+ def test_plain_thinking_open_entml_close_batch() -> None:
98
+ text = "<thinking>\nplan step\n</entml:thinking>\nanswer"
99
+ content, thinking = split_entml_thinking(text)
100
+ assert thinking == "plan step"
101
+ assert "answer" in content
102
+ assert "<thinking>" not in content
103
+
104
+
105
+ def test_plain_thinking_open_fault_close_batch() -> None:
106
+ text = "<thinking>\nplan step\n</thinking>\nanswer"
107
+ content, thinking = split_entml_thinking(text)
108
+ assert thinking == "plan step"
109
+ assert "answer" in content
110
+
111
+
112
+ def test_plain_thinking_stream_entml_close() -> None:
113
+ filt = EntmlThinkingStreamFilter()
114
+ parts = []
115
+ for chunk in ["<thinking>\nplan", "\n</entml:thinking>\n", "hello"]:
116
+ parts.extend(filt.feed(chunk))
117
+ parts.extend(filt.finalize())
118
+ thinking = "".join(t for k, t in parts if k == "thinking")
119
+ content = "".join(t for k, t in parts if k == "content")
120
+ assert thinking.strip() == "plan"
121
+ assert "hello" in content
122
+
123
+
124
+ def test_plain_thinking_stream_fault_close() -> None:
125
+ filt = EntmlThinkingStreamFilter()
126
+ parts = []
127
+ for chunk in ["<thinking>\nplan\n</thinking>\n", "hello"]:
128
+ parts.extend(filt.feed(chunk))
129
+ parts.extend(filt.finalize())
130
+ thinking = "".join(t for k, t in parts if k == "thinking")
131
+ content = "".join(t for k, t in parts if k == "content")
132
+ assert thinking.strip() == "plan"
133
+ assert "hello" in content
134
+ assert not filt.in_open_thinking()
135
+
136
+
137
+ def test_is_thinking_enabled() -> None:
138
+ from echotools.exec.fncall.protocols.entml_think.core import is_thinking_enabled
139
+
140
+ assert is_thinking_enabled(None)
141
+ assert is_thinking_enabled({"thinking_mode": "on"})
142
+ assert is_thinking_enabled({"thinking_mode": "auto"})
143
+ assert not is_thinking_enabled({"thinking_mode": "off"})
144
+ assert not is_thinking_enabled({"thinking_level": "none"})
145
+
146
+
147
+ def test_plain_thinking_requires_thinking_enabled_batch() -> None:
148
+ text = "<thinking>\nplan\n</thinking>\nanswer"
149
+ _, thinking_on = split_entml_thinking(text, thinking_enabled=True)
150
+ content_off, thinking_off = split_entml_thinking(text, thinking_enabled=False)
151
+ assert thinking_on == "plan"
152
+ assert thinking_off == ""
153
+ assert "<thinking>" in content_off