echotools 2.3.60__tar.gz → 2.3.61__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.61}/PKG-INFO +1 -1
  2. {echotools-2.3.60 → echotools-2.3.61}/pyproject.toml +1 -1
  3. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/protocols/entml_think/parse.py +152 -34
  4. {echotools-2.3.60 → echotools-2.3.61}/src/echotools.egg-info/PKG-INFO +1 -1
  5. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_entml_thinking_parse.py +40 -0
  6. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_fncall_stream_thinking.py +33 -0
  7. {echotools-2.3.60 → echotools-2.3.61}/README.md +0 -0
  8. {echotools-2.3.60 → echotools-2.3.61}/docs/README.md +0 -0
  9. {echotools-2.3.60 → echotools-2.3.61}/setup.cfg +0 -0
  10. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/__init__.py +0 -0
  11. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/cache/__init__.py +0 -0
  12. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/cache/list_cache.py +0 -0
  13. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/cache/memory_cache.py +0 -0
  14. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/config/__init__.py +0 -0
  15. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/config/base.py +0 -0
  16. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/config/center.py +0 -0
  17. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/config/center_notify.py +0 -0
  18. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/config/center_template.py +0 -0
  19. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/config/loader.py +0 -0
  20. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/config/merge.py +0 -0
  21. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/errors/__init__.py +0 -0
  22. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/errors/base.py +0 -0
  23. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/errors/classify.py +0 -0
  24. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/errors/common.py +0 -0
  25. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/errors/http.py +0 -0
  26. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/ids/__init__.py +0 -0
  27. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/ids/generator.py +0 -0
  28. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/io/__init__.py +0 -0
  29. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/io/io_utils.py +0 -0
  30. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/io/printstream.py +0 -0
  31. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/logger/__init__.py +0 -0
  32. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/logger/manager.py +0 -0
  33. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/retry/__init__.py +0 -0
  34. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/base/retry/retry.py +0 -0
  35. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/compat.py +0 -0
  36. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/dispatch/__init__.py +0 -0
  37. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/dispatch/candidate.py +0 -0
  38. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  39. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  40. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/dispatch/race.py +0 -0
  41. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/dispatch/selector.py +0 -0
  42. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/dispatch/usage.py +0 -0
  43. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/__init__.py +0 -0
  44. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  45. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/parsers/stream.py +0 -0
  46. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  47. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  48. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  49. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/prompt/history.py +0 -0
  50. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  51. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  52. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  53. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  54. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  55. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/protocols/entml.py +0 -0
  56. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
  57. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
  58. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/protocols/entml_stream_json.py +0 -0
  59. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  60. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
  61. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  62. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  63. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  64. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/registry.py +0 -0
  65. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  66. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  67. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  68. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  69. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  70. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  71. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/keys.py +0 -0
  72. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/lifecycle/__init__.py +0 -0
  73. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/lifecycle/manager.py +0 -0
  74. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/lifecycle/updater.py +0 -0
  75. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  76. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/process/__init__.py +0 -0
  77. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/process/port.py +0 -0
  78. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/protocol/__init__.py +0 -0
  79. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/protocol/base.py +0 -0
  80. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/__init__.py +0 -0
  81. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/conpty.py +0 -0
  82. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/local/__init__.py +0 -0
  83. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/local/core.py +0 -0
  84. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/local/proc.py +0 -0
  85. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/local/unix.py +0 -0
  86. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/local/win.py +0 -0
  87. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/sanitize.py +0 -0
  88. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/session/__init__.py +0 -0
  89. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/session/base.py +0 -0
  90. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/session/client.py +0 -0
  91. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/ssh.py +0 -0
  92. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/exec/terminal/tmux.py +0 -0
  93. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/__init__.py +0 -0
  94. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/charmap.py +0 -0
  95. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/spinner.py +0 -0
  96. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/ui.py +0 -0
  97. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  98. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  99. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  100. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  101. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  102. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  103. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uicore/ui_console.py +0 -0
  104. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uicore/ui_io.py +0 -0
  105. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uicore/ui_log.py +0 -0
  106. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  107. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uicore/ui_text.py +0 -0
  108. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uicore/ui_types.py +0 -0
  109. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  110. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  111. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  112. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  113. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  114. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  115. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  116. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  117. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  118. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  119. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  120. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/events/__init__.py +0 -0
  121. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/events/bus.py +0 -0
  122. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/events/event.py +0 -0
  123. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/spinner/__init__.py +0 -0
  124. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/spinner/spinner.py +0 -0
  125. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/spinner/spinner_color.py +0 -0
  126. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/tracing/__init__.py +0 -0
  127. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/tracing/context.py +0 -0
  128. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/tracing/span.py +0 -0
  129. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/tracing/tracer.py +0 -0
  130. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/translate.py +0 -0
  131. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/watcher/__init__.py +0 -0
  132. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/watcher/file_watcher.py +0 -0
  133. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/__init__.py +0 -0
  134. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/application.py +0 -0
  135. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/broker.py +0 -0
  136. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/app.js +0 -0
  137. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  138. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  139. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  140. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  141. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  142. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  143. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  144. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  145. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  146. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  147. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/inputbox.css +0 -0
  148. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/inputbox.js +0 -0
  149. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/style.css +0 -0
  150. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/input_box/voicegif.js +0 -0
  151. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/middleware.py +0 -0
  152. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/stats.py +0 -0
  153. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/media/web/utils.py +0 -0
  154. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/files/__init__.py +0 -0
  155. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/files/file_util.py +0 -0
  156. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/network/__init__.py +0 -0
  157. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/network/http_utils.py +0 -0
  158. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/plugin/__init__.py +0 -0
  159. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/plugin/base.py +0 -0
  160. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/plugin/discovery.py +0 -0
  161. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/plugin/registry.py +0 -0
  162. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/plugin/registry_ops.py +0 -0
  163. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/proxy/__init__.py +0 -0
  164. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/proxy/manager.py +0 -0
  165. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/runtime/__init__.py +0 -0
  166. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/runtime/collector.py +0 -0
  167. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/scheduler/__init__.py +0 -0
  168. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/scheduler/scheduler.py +0 -0
  169. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/sdk/__init__.py +0 -0
  170. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/plat/sdk/facade.py +0 -0
  171. {echotools-2.3.60 → echotools-2.3.61}/src/echotools/version.py +0 -0
  172. {echotools-2.3.60 → echotools-2.3.61}/src/echotools.egg-info/SOURCES.txt +0 -0
  173. {echotools-2.3.60 → echotools-2.3.61}/src/echotools.egg-info/dependency_links.txt +0 -0
  174. {echotools-2.3.60 → echotools-2.3.61}/src/echotools.egg-info/requires.txt +0 -0
  175. {echotools-2.3.60 → echotools-2.3.61}/src/echotools.egg-info/top_level.txt +0 -0
  176. {echotools-2.3.60 → echotools-2.3.61}/src/tests/bench_import.py +0 -0
  177. {echotools-2.3.60 → echotools-2.3.61}/src/tests/conftest.py +0 -0
  178. {echotools-2.3.60 → echotools-2.3.61}/src/tests/fixtures/__init__.py +0 -0
  179. {echotools-2.3.60 → echotools-2.3.61}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
  180. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_adversarial_stream_splits.py +0 -0
  181. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_cache_retry.py +0 -0
  182. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_completion.py +0 -0
  183. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_config.py +0 -0
  184. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_config_extended.py +0 -0
  185. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_console.py +0 -0
  186. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_coverage_core.py +0 -0
  187. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_dispatch.py +0 -0
  188. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_dispatch_extended.py +0 -0
  189. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_dispatch_race.py +0 -0
  190. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_entml_agent_output_formats.py +0 -0
  191. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_entml_bare_invoke_format.py +0 -0
  192. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_entml_parser_comprehensive.py +0 -0
  193. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_entml_protocol.py +0 -0
  194. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_entml_stream_json.py +0 -0
  195. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
  196. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_entml_thinking_history.py +0 -0
  197. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_entml_tool_parse_leak.py +0 -0
  198. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_entml_tool_pipeline.py +0 -0
  199. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_entml_tools.py +0 -0
  200. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_errors.py +0 -0
  201. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_errors_common.py +0 -0
  202. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_events.py +0 -0
  203. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_events_event.py +0 -0
  204. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_facade.py +0 -0
  205. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_fncall.py +0 -0
  206. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_ids.py +0 -0
  207. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_imports.py +0 -0
  208. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_io.py +0 -0
  209. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_keys.py +0 -0
  210. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_lifecycle.py +0 -0
  211. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_logger.py +0 -0
  212. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_merge.py +0 -0
  213. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_model_branch_matrix.py +0 -0
  214. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_plugin.py +0 -0
  215. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_printstream.py +0 -0
  216. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_protocols.py +0 -0
  217. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_proxy.py +0 -0
  218. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_proxy_selector.py +0 -0
  219. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_runtime.py +0 -0
  220. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_scheduler.py +0 -0
  221. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  222. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_stream_partial_json_parity.py +0 -0
  223. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_stream_realtime_parse.py +0 -0
  224. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_tracing.py +0 -0
  225. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_usage.py +0 -0
  226. {echotools-2.3.60 → echotools-2.3.61}/src/tests/test_web.py +0 -0
  227. {echotools-2.3.60 → echotools-2.3.61}/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.61
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.61"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -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,73 @@ 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(text: str, start: int = 0) -> Tuple[int, bool]:
126
+ """返回 ``(开标签下标, 是否 plain)``;无则 ``(-1, False)``。"""
127
+ entml_at = text.find(_THINKING_OPEN_PREFIX, start)
128
+ plain_at = _find_plain_thinking_open(text, start)
129
+ if entml_at < 0 and plain_at < 0:
130
+ return -1, False
131
+ if entml_at < 0:
132
+ return plain_at, True
133
+ if plain_at < 0:
134
+ return entml_at, False
135
+ if plain_at < entml_at:
136
+ return plain_at, True
137
+ return entml_at, False
138
+
139
+
140
+ def _find_thinking_close(
141
+ text: str,
142
+ body_start: int,
143
+ *,
144
+ opened_plain: bool,
145
+ ) -> Tuple[int, int]:
146
+ """返回 ``(闭标签下标, 闭标签长度)``;未找到则 ``(-1, 0)``。"""
147
+ if opened_plain:
148
+ entml_close = text.find(_THINKING_CLOSE, body_start)
149
+ fault_close = text.find(_FAULT_THINKING_CLOSE, body_start)
150
+ candidates: List[Tuple[int, int]] = []
151
+ if entml_close >= 0:
152
+ candidates.append((entml_close, len(_THINKING_CLOSE)))
153
+ if fault_close >= 0:
154
+ candidates.append((fault_close, len(_FAULT_THINKING_CLOSE)))
155
+ if not candidates:
156
+ return -1, 0
157
+ close_at, close_len = min(candidates, key=lambda item: item[0])
158
+ return close_at, close_len
159
+
160
+ entml_close = text.find(_THINKING_CLOSE, body_start)
161
+ fault_close = text.find(_FAULT_THINKING_CLOSE, body_start)
162
+ if entml_close >= 0 and (fault_close < 0 or entml_close <= fault_close):
163
+ return entml_close, len(_THINKING_CLOSE)
164
+ if fault_close < 0:
165
+ return -1, 0
166
+
167
+ after_fault = fault_close + len(_FAULT_THINKING_CLOSE)
168
+ invoke_rel = find_complete_entml_invoke_open(text[after_fault:])
169
+ entml_after = text.find(_THINKING_CLOSE, after_fault)
170
+ if invoke_rel >= 0:
171
+ invoke_abs = after_fault + invoke_rel
172
+ if entml_after < 0 or invoke_abs < entml_after:
173
+ return fault_close, len(_FAULT_THINKING_CLOSE)
174
+ if entml_after >= 0:
175
+ return entml_after, len(_THINKING_CLOSE)
176
+ return -1, 0
177
+
178
+
179
+ def _hold_thinking_open_prefixes(buffer: str) -> Tuple[str, str]:
180
+ """hold ``<entml:thinking`` 或 plain ``<thinking`` 的真前缀。"""
181
+ safe, hold = _hold_prefix(buffer, _THINKING_OPEN_PREFIX)
182
+ if hold:
183
+ return safe, hold
184
+ return _hold_prefix(safe, _PLAIN_THINKING_OPEN_PREFIX)
185
+
186
+
119
187
  def _hold_ambiguous_tool_markup(buffer: str) -> Tuple[str, str]:
120
188
  """thinking 块内:hold 可能长成真实工具调用的 markup(非 prose ``<entml:invoke>`` 提及)。"""
121
189
  if not buffer:
@@ -144,67 +212,94 @@ def _hold_ambiguous_tool_markup(buffer: str) -> Tuple[str, str]:
144
212
 
145
213
 
146
214
  def has_unclosed_entml_thinking(text: str) -> bool:
147
- """buffer 中是否存在尚未闭合的 <entml:thinking> 块(含开标签未收齐)。"""
215
+ """buffer 中是否存在尚未闭合的 thinking 块(含开标签未收齐)。"""
148
216
  if not text:
149
217
  return False
150
218
  i = 0
151
219
  while i < len(text):
152
- open_at = text.find(_THINKING_OPEN_PREFIX, i)
220
+ open_at, opened_plain = _find_earliest_thinking_open(text, i)
153
221
  if open_at < 0:
154
222
  break
155
223
  gt = text.find(">", open_at)
156
224
  if gt < 0:
157
225
  return True
158
- close_at = text.find(_THINKING_CLOSE, gt + 1)
226
+ body_start = gt + 1
227
+ close_at, close_len = _find_thinking_close(
228
+ text, body_start, opened_plain=opened_plain
229
+ )
159
230
  if close_at < 0:
160
231
  return True
161
- i = close_at + len(_THINKING_CLOSE)
162
- max_keep = len(_THINKING_OPEN_PREFIX) - 1
232
+ i = close_at + close_len
233
+ max_keep = max(len(_THINKING_OPEN_PREFIX), len(_PLAIN_THINKING_OPEN_PREFIX)) - 1
163
234
  check_len = min(len(text), max_keep)
164
235
  for length in range(check_len, 0, -1):
165
236
  suffix = text[-length:]
166
- if not (
167
- _THINKING_OPEN_PREFIX.startswith(suffix) and suffix != _THINKING_OPEN_PREFIX
237
+ if (
238
+ _THINKING_OPEN_PREFIX.startswith(suffix)
239
+ and suffix != _THINKING_OPEN_PREFIX
168
240
  ):
169
- continue
170
- # `<e` / `<entml:` 等对 invoke 同样是真前缀 → 不在此抢占
171
- if any(other.startswith(suffix) for other in _AMBIGUOUS_ENTML_PREFIXES):
172
- continue
173
- return True
241
+ if any(other.startswith(suffix) for other in _AMBIGUOUS_ENTML_PREFIXES):
242
+ continue
243
+ return True
244
+ if (
245
+ _PLAIN_THINKING_OPEN_PREFIX.startswith(suffix)
246
+ and suffix != _PLAIN_THINKING_OPEN_PREFIX
247
+ ):
248
+ return True
174
249
  return False
175
250
 
176
251
 
177
252
  def invoke_index_inside_unclosed_thinking(text: str, invoke_at: int) -> bool:
178
- """``invoke_at`` 是否落在尚未正式 ``</entml:thinking>`` 闭合的块内。
179
-
180
- 已出现容错 ``</thinking>`` 且 invoke 在其后的段落,视为 thinking 外。
181
- """
253
+ """``invoke_at`` 是否落在尚未闭合的 thinking 块内。"""
182
254
  if invoke_at < 0:
183
255
  return False
184
- think_open = text.find(_THINKING_OPEN_PREFIX)
256
+ think_open, opened_plain = _find_earliest_thinking_open(text)
185
257
  if think_open < 0 or invoke_at <= think_open:
186
258
  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
259
+ gt = text.find(">", think_open)
260
+ if gt < 0:
261
+ return True
262
+ body_start = gt + 1
263
+ close_at, close_len = _find_thinking_close(
264
+ text, body_start, opened_plain=opened_plain
265
+ )
266
+ if close_at < 0:
267
+ return True
268
+ return invoke_at < close_at + close_len
194
269
 
195
270
 
196
271
  def split_entml_thinking(text: str) -> Tuple[str, str]:
197
- """从文本中剥离 <entml:thinking> 块,返回 (正文, 思考链拼接)。"""
272
+ """从文本中剥离 thinking 块,返回 (正文, 思考链拼接)。"""
198
273
  if not text:
199
274
  return "", ""
200
275
 
201
276
  parts: List[str] = []
277
+ clean_parts: List[str] = []
278
+ i = 0
279
+ while i < len(text):
280
+ open_at, opened_plain = _find_earliest_thinking_open(text, i)
281
+ if open_at < 0:
282
+ clean_parts.append(text[i:])
283
+ break
284
+
285
+ clean_parts.append(text[i:open_at])
286
+ gt = text.find(">", open_at)
287
+ if gt < 0:
288
+ clean_parts.append(text[open_at:])
289
+ break
290
+
291
+ body_start = gt + 1
292
+ close_at, close_len = _find_thinking_close(
293
+ text, body_start, opened_plain=opened_plain
294
+ )
295
+ if close_at < 0:
296
+ clean_parts.append(text[open_at:])
297
+ break
202
298
 
203
- def _collect(match: re.Match[str]) -> str:
204
- parts.append(match.group(1))
205
- return ""
299
+ parts.append(text[body_start:close_at])
300
+ i = close_at + close_len
206
301
 
207
- clean = THINKING_BLOCK_RE.sub(_collect, text)
302
+ clean = "".join(clean_parts)
208
303
  thinking = "\n".join(part.strip() for part in parts if part.strip())
209
304
  return strip_orphan_thinking_close_prefix(clean.strip()), thinking
210
305
 
@@ -244,6 +339,7 @@ class EntmlThinkingStreamFilter:
244
339
  def __init__(self) -> None:
245
340
  self._pending = ""
246
341
  self._in_block = False
342
+ self._opened_plain = False
247
343
  # 当前 thinking 块是否已输出过任何内容(用于首片去前导空白)
248
344
  self._thinking_started = False
249
345
  # 见到 ``</thinking>`` 后暂存,仅当后续出现 invoke 才视为闭合;否则当纯文本
@@ -253,20 +349,21 @@ class EntmlThinkingStreamFilter:
253
349
  def clear_invoke_handoff_state(self) -> None:
254
350
  """invoke 已交给正文/工具解析后,结束 thinking / fault 观察状态。"""
255
351
  self._in_block = False
352
+ self._opened_plain = False
256
353
  self._fault_watch = False
257
354
  self._fault_buffer = ""
258
355
  self._thinking_started = False
259
356
 
260
357
  def in_open_thinking(self) -> bool:
261
- """思考块已开始且尚未收到 ``</entml:thinking>``(含开/闭标签分片 hold)。"""
358
+ """思考块已开始且尚未闭合(含开/闭标签分片 hold)。"""
262
359
  if self._in_block or self._fault_watch:
263
360
  return True
264
- open_at = self._pending.find(_THINKING_OPEN_PREFIX)
361
+ open_at, _ = _find_earliest_thinking_open(self._pending)
265
362
  if open_at >= 0:
266
363
  gt = self._pending.find(">", open_at)
267
364
  if gt < 0:
268
365
  return True
269
- _, hold = _hold_prefix(self._pending, _THINKING_OPEN_PREFIX)
366
+ _, hold = _hold_thinking_open_prefixes(self._pending)
270
367
  if hold:
271
368
  return True
272
369
  return False
@@ -279,6 +376,7 @@ class EntmlThinkingStreamFilter:
279
376
  if invoke_at < len(_FAULT_THINKING_CLOSE):
280
377
  return False
281
378
  self._in_block = False
379
+ self._opened_plain = False
282
380
  self._fault_watch = False
283
381
  self._thinking_started = False
284
382
  self._pending = self._fault_buffer[len(_FAULT_THINKING_CLOSE) :]
@@ -296,6 +394,7 @@ class EntmlThinkingStreamFilter:
296
394
  if emitted:
297
395
  out.append(("thinking", emitted))
298
396
  self._in_block = False
397
+ self._opened_plain = False
299
398
  self._fault_watch = False
300
399
  self._thinking_started = False
301
400
  self._pending = self._fault_buffer[close_at + len(_THINKING_CLOSE) :]
@@ -323,12 +422,15 @@ class EntmlThinkingStreamFilter:
323
422
  out.append(("thinking", emitted))
324
423
  out.append(("content", invoke_block))
325
424
  self._in_block = False
425
+ self._opened_plain = False
326
426
  self._fault_watch = False
327
427
  self._fault_buffer = ""
328
428
  self._thinking_started = False
329
429
  rest = after.lstrip()
330
430
  if rest.startswith(_THINKING_CLOSE):
331
431
  rest = rest[len(_THINKING_CLOSE) :].lstrip()
432
+ if rest.startswith(_FAULT_THINKING_CLOSE):
433
+ rest = rest[len(_FAULT_THINKING_CLOSE) :].lstrip()
332
434
  self._pending = rest
333
435
  return True
334
436
  return False
@@ -361,6 +463,7 @@ class EntmlThinkingStreamFilter:
361
463
  if emitted:
362
464
  out.append(("thinking", emitted))
363
465
  self._in_block = False
466
+ self._opened_plain = False
364
467
  self._fault_watch = False
365
468
  self._fault_buffer = ""
366
469
  self._thinking_started = False
@@ -373,6 +476,17 @@ class EntmlThinkingStreamFilter:
373
476
  fault_at = self._pending.find(_FAULT_THINKING_CLOSE)
374
477
  if fault_at >= 0:
375
478
  before = self._pending[:fault_at]
479
+ if self._opened_plain:
480
+ emitted = self._emit_thinking_piece(before)
481
+ if emitted:
482
+ out.append(("thinking", emitted))
483
+ self._in_block = False
484
+ self._opened_plain = False
485
+ self._fault_watch = False
486
+ self._fault_buffer = ""
487
+ self._thinking_started = False
488
+ self._pending = self._pending[fault_at + len(_FAULT_THINKING_CLOSE) :]
489
+ return True
376
490
  emitted = self._emit_thinking_piece(before)
377
491
  if emitted:
378
492
  out.append(("thinking", emitted))
@@ -402,9 +516,9 @@ class EntmlThinkingStreamFilter:
402
516
 
403
517
  def _feed_before_block(self, out: List[Tuple[str, str]]) -> bool:
404
518
  """开标签之前或尚未进入块;返回 False 表示应退出 feed 主循环。"""
405
- open_at = self._pending.find(_THINKING_OPEN_PREFIX)
519
+ open_at, opened_plain = _find_earliest_thinking_open(self._pending)
406
520
  if open_at < 0:
407
- safe, self._pending = _hold_prefix(self._pending, _THINKING_OPEN_PREFIX)
521
+ safe, self._pending = _hold_thinking_open_prefixes(self._pending)
408
522
  if safe:
409
523
  out.append(("content", safe))
410
524
  return False
@@ -418,6 +532,7 @@ class EntmlThinkingStreamFilter:
418
532
  return False
419
533
 
420
534
  self._in_block = True
535
+ self._opened_plain = opened_plain
421
536
  self._thinking_started = False
422
537
  self._pending = self._pending[gt + 1 :]
423
538
  return True
@@ -471,6 +586,7 @@ class EntmlThinkingStreamFilter:
471
586
  self._fault_buffer = ""
472
587
  self._fault_watch = False
473
588
  self._in_block = False
589
+ self._opened_plain = False
474
590
  self._thinking_started = False
475
591
  if self._in_block:
476
592
  if not self._try_emit_actionable_invoke_from_in_block(out):
@@ -479,6 +595,7 @@ class EntmlThinkingStreamFilter:
479
595
  out.append(("thinking", emitted))
480
596
  self._pending = ""
481
597
  self._in_block = False
598
+ self._opened_plain = False
482
599
  self._thinking_started = False
483
600
  if self._pending:
484
601
  content, thinking = split_entml_thinking(self._pending)
@@ -490,6 +607,7 @@ class EntmlThinkingStreamFilter:
490
607
  out.append(("content", self._pending))
491
608
  self._pending = ""
492
609
  self._in_block = False
610
+ self._opened_plain = False
493
611
  self._fault_watch = False
494
612
  self._fault_buffer = ""
495
613
  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.61
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -92,3 +92,43 @@ 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()
@@ -295,3 +295,36 @@ def test_fault_thinking_close_stays_open_while_waiting_for_invoke() -> None:
295
295
  filt.feed(INVOKE)
296
296
  kinds = [k for k, _ in filt.feed("")]
297
297
  assert "content" in kinds or filt.in_open_thinking() is False
298
+
299
+
300
+ def test_plain_thinking_open_then_invoke_stream() -> None:
301
+ """plain ``<thinking>`` 开标签 + ``</thinking>`` 闭合后应能解析工具。"""
302
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
303
+ text = f"<thinking>\nplan\n</thinking>\n{INVOKE}"
304
+ for i in range(0, len(text), 7):
305
+ parser.feed(text[i : i + 7])
306
+ clean, calls = parser.finalize()
307
+ assert len(calls) == 1
308
+ assert calls[0]["function"]["name"] == "get_weather"
309
+ assert "plan" in parser.partial_thinking
310
+ assert "Hangzhou" in clean or len(calls) == 1
311
+
312
+
313
+ def test_plain_thinking_open_entml_close_then_invoke() -> None:
314
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
315
+ text = f"<thinking>\nplan\n</entml:thinking>\n{INVOKE}"
316
+ parser.feed(text)
317
+ clean, calls = parser.finalize()
318
+ assert len(calls) == 1
319
+ assert "plan" in parser.partial_thinking
320
+
321
+
322
+ def test_entml_open_fault_close_then_invoke_still_works(chunk: int = 3) -> None:
323
+ """``<entml:thinking>`` + ``</thinking>`` + 工具:fault 闭合。"""
324
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
325
+ text = f"<entml:thinking>\nplan\n</thinking>\n{INVOKE}"
326
+ for i in range(0, len(text), chunk):
327
+ parser.feed(text[i : i + chunk])
328
+ clean, calls = parser.finalize()
329
+ assert len(calls) == 1
330
+ assert "plan" in parser.partial_thinking
File without changes
File without changes
File without changes