echotools 2.4.9__tar.gz → 2.4.10__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 (293) hide show
  1. {echotools-2.4.9 → echotools-2.4.10}/PKG-INFO +1 -1
  2. {echotools-2.4.9 → echotools-2.4.10}/pyproject.toml +1 -1
  3. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/io/printstream.py +1 -1
  4. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/parsers/stream_final.py +77 -15
  5. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_patterns/invoke.py +53 -2
  6. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_think/hold.py +100 -39
  7. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_think/parse.py +13 -3
  8. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_tool/invoke.py +7 -1
  9. {echotools-2.4.9 → echotools-2.4.10}/src/echotools.egg-info/PKG-INFO +1 -1
  10. {echotools-2.4.9 → echotools-2.4.10}/src/echotools.egg-info/SOURCES.txt +2 -0
  11. echotools-2.4.10/src/tests/fixtures/interleaved_corpus.py +116 -0
  12. echotools-2.4.10/src/tests/test_entml_interleaved_corpus.py +152 -0
  13. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_tool_pipeline.py +18 -0
  14. {echotools-2.4.9 → echotools-2.4.10}/README.md +0 -0
  15. {echotools-2.4.9 → echotools-2.4.10}/setup.cfg +0 -0
  16. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/__init__.py +0 -0
  17. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/cache/__init__.py +0 -0
  18. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/cache/list_cache.py +0 -0
  19. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/cache/memory_cache.py +0 -0
  20. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/config/__init__.py +0 -0
  21. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/config/base.py +0 -0
  22. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/config/center.py +0 -0
  23. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/config/center_notify.py +0 -0
  24. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/config/center_template.py +0 -0
  25. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/config/loader.py +0 -0
  26. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/config/merge.py +0 -0
  27. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/errors/__init__.py +0 -0
  28. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/errors/base.py +0 -0
  29. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/errors/classify.py +0 -0
  30. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/errors/common.py +0 -0
  31. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/errors/http.py +0 -0
  32. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/ids/__init__.py +0 -0
  33. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/ids/generator.py +0 -0
  34. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/io/__init__.py +0 -0
  35. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/io/io_utils.py +0 -0
  36. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/logger/__init__.py +0 -0
  37. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/logger/manager.py +0 -0
  38. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/retry/__init__.py +0 -0
  39. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/base/retry/retry.py +0 -0
  40. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/dispatch/__init__.py +0 -0
  41. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/dispatch/candidate.py +0 -0
  42. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  43. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  44. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/dispatch/race.py +0 -0
  45. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/dispatch/selector.py +0 -0
  46. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/dispatch/usage.py +0 -0
  47. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/__init__.py +0 -0
  48. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  49. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/parsers/stream.py +0 -0
  50. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/parsers/stream_delta.py +0 -0
  51. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/parsers/stream_feed.py +0 -0
  52. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  53. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/prompt/behavior_blocks.py +0 -0
  54. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/prompt/history.py +0 -0
  55. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  56. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  57. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  58. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  59. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml.py +0 -0
  60. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_patterns/__init__.py +0 -0
  61. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_patterns/params.py +0 -0
  62. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_patterns/regex.py +0 -0
  63. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_patterns/strip.py +0 -0
  64. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_schema/__init__.py +0 -0
  65. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_schema/mangled.py +0 -0
  66. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_schema/validate.py +0 -0
  67. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_stream/__init__.py +0 -0
  68. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_stream/body.py +0 -0
  69. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_stream/snap.py +0 -0
  70. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  71. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
  72. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_think/filter.py +0 -0
  73. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  74. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_tool/__init__.py +0 -0
  75. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_tool/comment.py +0 -0
  76. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_tool/fakemarkup.py +0 -0
  77. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_tool/tools.py +0 -0
  78. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/protocols/entml_tool/values.py +0 -0
  79. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/registry.py +0 -0
  80. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  81. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  82. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/shared/entml_format.py +0 -0
  83. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/shared/history_markup.py +0 -0
  84. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  85. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  86. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  87. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/fncall/tool_id.py +0 -0
  88. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/keys.py +0 -0
  89. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/lifecycle/__init__.py +0 -0
  90. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/lifecycle/manager.py +0 -0
  91. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/lifecycle/updater.py +0 -0
  92. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  93. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/process/__init__.py +0 -0
  94. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/process/port.py +0 -0
  95. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/protocol/__init__.py +0 -0
  96. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/protocol/base.py +0 -0
  97. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/__init__.py +0 -0
  98. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/conpty.py +0 -0
  99. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/local/__init__.py +0 -0
  100. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/local/core.py +0 -0
  101. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/local/proc.py +0 -0
  102. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/local/unix.py +0 -0
  103. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/local/win.py +0 -0
  104. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/sanitize.py +0 -0
  105. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/session/__init__.py +0 -0
  106. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/session/base.py +0 -0
  107. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/session/client.py +0 -0
  108. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/ssh.py +0 -0
  109. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/exec/terminal/tmux.py +0 -0
  110. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/__init__.py +0 -0
  111. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/charmap.py +0 -0
  112. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/spinner.py +0 -0
  113. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/ui.py +0 -0
  114. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  115. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  116. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  117. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  118. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  119. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  120. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uicore/ui_console.py +0 -0
  121. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uicore/ui_io.py +0 -0
  122. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uicore/ui_log.py +0 -0
  123. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  124. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uicore/ui_text.py +0 -0
  125. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uicore/ui_types.py +0 -0
  126. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uilayout/ui_bridge.py +0 -0
  127. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  128. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  129. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  130. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  131. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  132. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  133. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  134. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  135. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  136. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uiwidgets/ui_richcli.py +0 -0
  137. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  138. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  139. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/console/uiwidgets/ui_themes.py +0 -0
  140. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/events/__init__.py +0 -0
  141. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/events/bus.py +0 -0
  142. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/events/event.py +0 -0
  143. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/spinner/__init__.py +0 -0
  144. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/spinner/spinner.py +0 -0
  145. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/spinner/spinner_color.py +0 -0
  146. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/tracing/__init__.py +0 -0
  147. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/tracing/context.py +0 -0
  148. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/tracing/span.py +0 -0
  149. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/tracing/tracer.py +0 -0
  150. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/translate.py +0 -0
  151. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/watcher/__init__.py +0 -0
  152. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/watcher/file_watcher.py +0 -0
  153. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/__init__.py +0 -0
  154. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/application.py +0 -0
  155. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/broker.py +0 -0
  156. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/app.js +0 -0
  157. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  158. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  159. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  160. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  161. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  162. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  163. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  164. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  165. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  166. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  167. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/inputbox.css +0 -0
  168. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/inputbox.js +0 -0
  169. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/style.css +0 -0
  170. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/input_box/voicegif.js +0 -0
  171. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/middleware.py +0 -0
  172. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/stats.py +0 -0
  173. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/media/web/utils.py +0 -0
  174. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/__init__.py +0 -0
  175. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_monitor/api.py +0 -0
  176. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_monitor/config.py +0 -0
  177. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_monitor/linux.py +0 -0
  178. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_monitor/mac.py +0 -0
  179. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_monitor/types.py +0 -0
  180. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_monitor/win.py +0 -0
  181. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_record/api.py +0 -0
  182. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_record/config.py +0 -0
  183. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_record/linux.py +0 -0
  184. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_record/mac.py +0 -0
  185. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_record/win.py +0 -0
  186. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_record/win_wasapi.py +0 -0
  187. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/audio_record/win_winmm.py +0 -0
  188. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/screenshot/api.py +0 -0
  189. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/screenshot/config.py +0 -0
  190. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/screenshot/linux.py +0 -0
  191. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/screenshot/mac.py +0 -0
  192. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/screenshot/win.py +0 -0
  193. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/screenshot/win_dxgi.py +0 -0
  194. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/screenshot/win_gdi.py +0 -0
  195. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/shared/__init__.py +0 -0
  196. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/shared/bmp.py +0 -0
  197. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/shared/pcm.py +0 -0
  198. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/shared/platform.py +0 -0
  199. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/shared/wav.py +0 -0
  200. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/capture/shared/win_com.py +0 -0
  201. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/files/__init__.py +0 -0
  202. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/files/file_util.py +0 -0
  203. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/network/__init__.py +0 -0
  204. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/network/http_utils.py +0 -0
  205. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/plugin/__init__.py +0 -0
  206. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/plugin/base.py +0 -0
  207. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/plugin/discovery.py +0 -0
  208. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/plugin/registry.py +0 -0
  209. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/plugin/registry_ops.py +0 -0
  210. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/proxy/__init__.py +0 -0
  211. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/proxy/manager.py +0 -0
  212. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/sdk/__init__.py +0 -0
  213. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/sdk/facade.py +0 -0
  214. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/sdk/runtime.py +0 -0
  215. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/plat/sdk/scheduler.py +0 -0
  216. {echotools-2.4.9 → echotools-2.4.10}/src/echotools/version.py +0 -0
  217. {echotools-2.4.9 → echotools-2.4.10}/src/echotools.egg-info/dependency_links.txt +0 -0
  218. {echotools-2.4.9 → echotools-2.4.10}/src/echotools.egg-info/requires.txt +0 -0
  219. {echotools-2.4.9 → echotools-2.4.10}/src/echotools.egg-info/top_level.txt +0 -0
  220. {echotools-2.4.9 → echotools-2.4.10}/src/tests/_check_parity.py +0 -0
  221. {echotools-2.4.9 → echotools-2.4.10}/src/tests/bench_import.py +0 -0
  222. {echotools-2.4.9 → echotools-2.4.10}/src/tests/conftest.py +0 -0
  223. {echotools-2.4.9 → echotools-2.4.10}/src/tests/fixtures/__init__.py +0 -0
  224. {echotools-2.4.9 → echotools-2.4.10}/src/tests/fixtures/entml_golden.py +0 -0
  225. {echotools-2.4.9 → echotools-2.4.10}/src/tests/fixtures/entml_golden_update.py +0 -0
  226. {echotools-2.4.9 → echotools-2.4.10}/src/tests/fixtures/simulated_fake_history_markup_responses.py +0 -0
  227. {echotools-2.4.9 → echotools-2.4.10}/src/tests/fixtures/simulated_fault_thinking_responses.py +0 -0
  228. {echotools-2.4.9 → echotools-2.4.10}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
  229. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_adversarial_stream_splits.py +0 -0
  230. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_cache_retry.py +0 -0
  231. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_completion.py +0 -0
  232. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_config.py +0 -0
  233. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_config_extended.py +0 -0
  234. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_console.py +0 -0
  235. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_coverage_core.py +0 -0
  236. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_dispatch.py +0 -0
  237. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_dispatch_extended.py +0 -0
  238. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_dispatch_race.py +0 -0
  239. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_agent_output_formats.py +0 -0
  240. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_bare_invoke_format.py +0 -0
  241. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_batch_stream_comprehensive.py +0 -0
  242. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_fake_structure_markup.py +0 -0
  243. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_golden.py +0 -0
  244. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_invoke_known_tool_gate.py +0 -0
  245. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_parser_comprehensive.py +0 -0
  246. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_protocol.py +0 -0
  247. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_stream_json.py +0 -0
  248. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
  249. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_thinking_history.py +0 -0
  250. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_thinking_parse.py +0 -0
  251. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_tool_parse_leak.py +0 -0
  252. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_entml_tools.py +0 -0
  253. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_errors.py +0 -0
  254. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_errors_common.py +0 -0
  255. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_events.py +0 -0
  256. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_events_event.py +0 -0
  257. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_facade.py +0 -0
  258. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_fake_history_markup_batch.py +0 -0
  259. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
  260. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
  261. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_fake_history_markup_stream.py +0 -0
  262. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_fault_thinking_close_batch.py +0 -0
  263. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_fault_thinking_close_stream.py +0 -0
  264. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_fncall.py +0 -0
  265. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_fncall_stream_thinking.py +0 -0
  266. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_history_markup_filter.py +0 -0
  267. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_ids.py +0 -0
  268. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_imports.py +0 -0
  269. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_io.py +0 -0
  270. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_keys.py +0 -0
  271. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_lifecycle.py +0 -0
  272. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_logger.py +0 -0
  273. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_merge.py +0 -0
  274. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_model_branch_matrix.py +0 -0
  275. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_multiline_parameter_comprehensive.py +0 -0
  276. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_plat_capture.py +0 -0
  277. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_plugin.py +0 -0
  278. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_printstream.py +0 -0
  279. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_protocols.py +0 -0
  280. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_proxy.py +0 -0
  281. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_proxy_selector.py +0 -0
  282. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_runtime.py +0 -0
  283. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_scheduler.py +0 -0
  284. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_schema_validate.py +0 -0
  285. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  286. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_stream_partial_json_parity.py +0 -0
  287. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_stream_realtime_parse.py +0 -0
  288. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_tool_id.py +0 -0
  289. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_tracing.py +0 -0
  290. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_ui_bridge.py +0 -0
  291. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_usage.py +0 -0
  292. {echotools-2.4.9 → echotools-2.4.10}/src/tests/test_web.py +0 -0
  293. {echotools-2.4.9 → echotools-2.4.10}/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.4.9
3
+ Version: 2.4.10
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.4.9"
7
+ version = "2.4.10"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -257,7 +257,7 @@ def _cleanup() -> None:
257
257
  try:
258
258
  _global_print_stream.flush_remaining()
259
259
  _global_print_stream.stop()
260
- except Exception:
260
+ except BaseException:
261
261
  pass
262
262
 
263
263
 
@@ -48,7 +48,10 @@ class StreamFinalMixin:
48
48
  return visible
49
49
 
50
50
  def _stream_partial_display_text(self) -> str:
51
- """流式 ``partial_text``:在 invoke 起点前截断 raw,保留尾部 holdback 空白。"""
51
+ """流式 ``partial_text``:保留 invoke 之间的交错正文,仅截断未闭合 invoke 起点。
52
+
53
+ 在 invoke 起点前截断 raw,保留尾部 holdback 空白。
54
+ """
52
55
  from echotools.exec.fncall.protocols.entml_think.parse import (
53
56
  clean_stream_partial_visible,
54
57
  split_entml_thinking,
@@ -61,17 +64,7 @@ class StreamFinalMixin:
61
64
  raw = self._raw_buf
62
65
  if not raw:
63
66
  return ""
64
-
65
- cut = len(raw)
66
- from echotools.exec.fncall.protocols.entml_patterns import (
67
- find_actionable_entml_invoke_open,
68
- )
69
-
70
- invoke_pos = find_actionable_entml_invoke_open(
71
- raw, known_names=self._get_known_tool_names()
72
- )
73
- if invoke_pos >= 0:
74
- cut = min(cut, invoke_pos)
67
+ cut, saw_invoke = self._invoke_cutoff(raw)
75
68
  segment = raw[:cut]
76
69
 
77
70
  buf = stream_safe_visible_prefix(
@@ -80,18 +73,87 @@ class StreamFinalMixin:
80
73
  )
81
74
  if not buf:
82
75
  return ""
83
- cleaned, _ = strip_fake_history_markup_for_display(buf)
84
76
  visible, _ = split_entml_thinking(
85
- cleaned,
77
+ buf,
86
78
  thinking_enabled=self._thinking_enabled,
87
79
  preserve_visible_whitespace=True,
88
80
  )
81
+ # 与 stream_safe 无未闭合 thinking 的 split 分支对齐:剥前导空白,
82
+ # 避免 thinking 打开/闭合瞬间 partial_text 首部 \n 抖动导致增量错位
83
+ visible = visible.lstrip()
84
+ visible = self._strip_completed_invoke_blocks(visible)
85
+ cleaned, _ = strip_fake_history_markup_for_display(visible)
86
+ if saw_invoke:
87
+ # invoke 前缀(<entml:...)被剥除后可能留下尾部换行;统一剥掉,
88
+ # 否则 invoke 开标签收齐瞬间 rstrip 翻转导致长度回退。空格保留
89
+ cleaned = cleaned.rstrip("\n\r")
89
90
  return clean_stream_partial_visible(
90
- visible,
91
+ cleaned,
91
92
  has_calls=self.has_calls,
92
93
  thinking_enabled=self._thinking_enabled,
93
94
  )
94
95
 
96
+ def _invoke_cutoff(self, raw: str) -> Tuple[int, bool]:
97
+ """返回 ``(截断点, 是否出现 invoke 前缀)``。
98
+
99
+ 截断到最后一个「打开但未闭合」的 invoke 起点:其后内容可能是参数,
100
+ 不能进入 partial_text;已闭合 invoke 之间的正文保留。
101
+ """
102
+ from echotools.exec.fncall.protocols.entml_patterns.invoke import (
103
+ _find_matching_close,
104
+ )
105
+
106
+ cut = len(raw)
107
+ # `<entml:` 前缀即按 invoke 区域处理:前缀被剥除后留下的尾部换行
108
+ # 需统一剥掉,否则 invoke 开标签收齐瞬间 rstrip 翻转导致长度回退
109
+ saw_invoke = "<entml:" in raw
110
+ search_from = 0
111
+ while True:
112
+ open_pos = raw.find("<entml:invoke", search_from)
113
+ if open_pos < 0:
114
+ break
115
+ if not self._is_actionable_invoke_open(raw, open_pos):
116
+ gt = raw.find(">", open_pos + len("<entml:invoke"))
117
+ search_from = (gt + 1) if gt >= 0 else (open_pos + len("<entml:invoke"))
118
+ continue
119
+ gt = raw.find(">", open_pos + len("<entml:invoke"))
120
+ if gt < 0:
121
+ cut = min(cut, open_pos)
122
+ break
123
+ close = _find_matching_close(raw, gt + 1, "</entml:invoke>")
124
+ if close < 0:
125
+ cut = min(cut, open_pos)
126
+ break
127
+ search_from = close + len("</entml:invoke>")
128
+ return cut, saw_invoke
129
+
130
+ def _strip_completed_invoke_blocks(self, visible: str) -> str:
131
+ """已闭合 invoke 块整体移除(含参数),避免标签剥离后参数文本泄漏。"""
132
+ from echotools.exec.fncall.protocols.entml_patterns import (
133
+ iter_actionable_entml_invoke_blocks,
134
+ resolve_known_tool_names,
135
+ )
136
+
137
+ known = resolve_known_tool_names(self._tools, self._get_schema_index())
138
+ removed: List[str] = []
139
+ last_end = 0
140
+ for start, end, _attrs, _body in iter_actionable_entml_invoke_blocks(
141
+ visible, known_names=known
142
+ ):
143
+ removed.append(visible[last_end:start])
144
+ last_end = end
145
+ removed.append(visible[last_end:])
146
+ return "".join(removed)
147
+
148
+ def _is_actionable_invoke_open(self, raw: str, pos: int) -> bool:
149
+ from echotools.exec.fncall.protocols.entml_patterns import (
150
+ entml_invoke_open_is_actionable,
151
+ )
152
+
153
+ return entml_invoke_open_is_actionable(
154
+ raw, pos, known_names=self._get_known_tool_names()
155
+ )
156
+
95
157
  def _finalize_display_text(self, clean_text: str) -> str:
96
158
  if not self._thinking_enabled:
97
159
  return clean_text
@@ -7,6 +7,54 @@ from .params import extract_attr_value, normalize_entml_name
7
7
  from .regex import _INVOKE_CLOSE, _INVOKE_OPEN_PREFIX
8
8
 
9
9
 
10
+ # 参数体是 opaque 文本:内部可能包含字面量 ``</entml:invoke>`` 或
11
+ # ``</entml:thinking>``(如 Python 正则字符串)。找闭合标签时须跳过参数体。
12
+ _SKIP_PARAM_BODIES: List[Tuple[str, str]] = [
13
+ ("<entml:parameter", "</entml:parameter>"),
14
+ ("<entml:parameters>", "</entml:parameters>"),
15
+ ("<entml:parameter>", "</entml:parameter>"),
16
+ ]
17
+
18
+
19
+ def _find_matching_close(
20
+ body: str,
21
+ start: int,
22
+ close_tag: str,
23
+ ) -> int:
24
+ """从 ``start`` 起找 ``close_tag``,跳过 ``<entml:parameter[...]>`` 体的内容。
25
+
26
+ 参数体内部的字面量 ``</entml:invoke>`` / ``</entml:thinking>`` 是内容不是结构,
27
+ 不能当作外层块的闭合。
28
+ """
29
+ i = start
30
+ n = len(body)
31
+ while i < n:
32
+ lt = body.find("<", i)
33
+ if lt < 0:
34
+ return -1
35
+ gt = body.find(">", lt)
36
+ if gt < 0:
37
+ return -1
38
+ tag = body[lt : gt + 1]
39
+ if tag == close_tag:
40
+ return lt
41
+ # 精确匹配 parameter 开标签(含属性的形式);不匹配 siblings
42
+ if tag == "<entml:parameter>" or tag.startswith("<entml:parameter "):
43
+ close_pos = body.find("</entml:parameter>", gt)
44
+ if close_pos < 0:
45
+ return -1
46
+ i = close_pos + len("</entml:parameter>")
47
+ continue
48
+ if tag == "<entml:parameters>":
49
+ close_pos = body.find("</entml:parameters>", gt)
50
+ if close_pos < 0:
51
+ return -1
52
+ i = close_pos + len("</entml:parameters>")
53
+ continue
54
+ i = gt + 1
55
+ return -1
56
+
57
+
10
58
  def is_placeholder_invoke_name(name: str) -> bool:
11
59
  """提示词占位符(如 ``$FUNCTION_NAME``)不算真实工具调用。"""
12
60
  n = (name or "").strip()
@@ -158,7 +206,7 @@ def iter_actionable_entml_invoke_blocks(
158
206
  gt = text.find(">", pos + prefix_len)
159
207
  if gt < 0:
160
208
  break
161
- close = text.find(_INVOKE_CLOSE, gt + 1)
209
+ close = _find_matching_close(text, gt + 1, _INVOKE_CLOSE)
162
210
  if close < 0:
163
211
  break
164
212
  attrs = text[pos + prefix_len : gt]
@@ -189,8 +237,11 @@ def _strip_orphan_invoke_tags(
189
237
 
190
238
 
191
239
  def _strip_orphan_non_invoke_tool_tags(content: str) -> str:
240
+ # 剥离 invoke 块外部的畸形 entml:* 标签(如模型把参数名输出成
241
+ # <entml:old_string">),仅保留 invoke/thinking 相关;prose 提及的
242
+ # <entml:invoke> 与未知工具块不在此列,留给调用方按 known 判定。
192
243
  return re.sub(
193
- r"</?entml:(?:function_calls|parameter|parameters)\b[^>]*/?>",
244
+ r"</?entml:(?!invoke\b|thinking(?:_mode)?\b|max_thinking_length\b)\w+\b[^>]*/?>",
194
245
  "",
195
246
  content,
196
247
  flags=re.DOTALL,
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- from typing import List, Optional, Set, Tuple
4
+ from typing import Callable, List, Optional, Set, Tuple
5
5
 
6
6
  from echotools.exec.fncall.protocols.entml_patterns import (
7
7
  entml_invoke_open_may_be_streaming,
@@ -84,15 +84,19 @@ def _find_thinking_close(
84
84
  thinking_enabled: bool = True,
85
85
  ) -> Tuple[int, int]:
86
86
  """返回 ``(闭标签下标, 闭标签长度)``;未找到则 ``(-1, 0)``。"""
87
+ from echotools.exec.fncall.protocols.entml_patterns.invoke import (
88
+ _find_matching_close,
89
+ )
90
+
87
91
  if not thinking_enabled:
88
- close_at = text.find(_THINKING_CLOSE, body_start)
89
- if close_at >= 0:
90
- return close_at, len(_THINKING_CLOSE)
92
+ entml_close = _find_matching_close(text, body_start, _THINKING_CLOSE)
93
+ if entml_close >= 0:
94
+ return entml_close, len(_THINKING_CLOSE)
91
95
  return -1, 0
92
96
 
97
+ entml_close = _find_matching_close(text, body_start, _THINKING_CLOSE)
98
+ fault_close = _find_matching_close(text, body_start, _FAULT_THINKING_CLOSE)
93
99
  if opened_plain:
94
- entml_close = text.find(_THINKING_CLOSE, body_start)
95
- fault_close = text.find(_FAULT_THINKING_CLOSE, body_start)
96
100
  candidates: List[Tuple[int, int]] = []
97
101
  if entml_close >= 0:
98
102
  candidates.append((entml_close, len(_THINKING_CLOSE)))
@@ -100,11 +104,8 @@ def _find_thinking_close(
100
104
  candidates.append((fault_close, len(_FAULT_THINKING_CLOSE)))
101
105
  if not candidates:
102
106
  return -1, 0
103
- close_at, close_len = min(candidates, key=lambda item: item[0])
104
- return close_at, close_len
107
+ return min(candidates, key=lambda item: item[0])
105
108
 
106
- entml_close = text.find(_THINKING_CLOSE, body_start)
107
- fault_close = text.find(_FAULT_THINKING_CLOSE, body_start)
108
109
  if entml_close >= 0 and (fault_close < 0 or entml_close <= fault_close):
109
110
  return entml_close, len(_THINKING_CLOSE)
110
111
  if fault_close < 0:
@@ -112,7 +113,7 @@ def _find_thinking_close(
112
113
 
113
114
  after_fault = fault_close + len(_FAULT_THINKING_CLOSE)
114
115
  invoke_rel = find_complete_entml_invoke_open(text[after_fault:])
115
- entml_after = text.find(_THINKING_CLOSE, after_fault)
116
+ entml_after = _find_matching_close(text, after_fault, _THINKING_CLOSE)
116
117
  if invoke_rel >= 0:
117
118
  invoke_abs = after_fault + invoke_rel
118
119
  if entml_after < 0 or invoke_abs < entml_after:
@@ -174,41 +175,90 @@ def _hold_ambiguous_tool_markup(
174
175
  return buffer, ""
175
176
 
176
177
 
177
- def _find_plain_thinking_open(text: str, start: int = 0) -> int:
178
- """返回 plain ``<thinking>`` 开标签起始下标(``<entml:thinking>`` 内不含此子串)。"""
179
- return text.find(_PLAIN_THINKING_OPEN_PREFIX, start)
180
-
181
178
  def _find_earliest_thinking_open(
182
179
  text: str,
183
180
  start: int = 0,
184
181
  *,
185
182
  thinking_enabled: bool = True,
186
183
  ) -> Tuple[int, bool]:
187
- """返回 ``(开标签下标, 是否 plain)``;无则 ``(-1, False)``。"""
188
- entml_at = text.find(_THINKING_OPEN_PREFIX, start)
189
- if not thinking_enabled:
190
- if entml_at < 0:
184
+ """返回 ``(开标签下标, 是否 plain)``;无则 ``(-1, False)``。
185
+
186
+ 按标签逐段扫描,跳过 ``<entml:parameter>`` / ``<entml:parameters>`` 体——
187
+ 参数值内可能含 ``<entml:thinking\\b[^>]*>`` 这类字面量(正则字符串),
188
+ 不能当作真实 thinking 块。
189
+ """
190
+ from echotools.exec.fncall.protocols.entml_patterns.invoke import (
191
+ _find_matching_close,
192
+ )
193
+
194
+ def _skip_param_body(close_tag: str) -> Optional[int]:
195
+ pos = _find_matching_close(text, gt + 1, close_tag)
196
+ return pos + len(close_tag) if pos >= 0 else None
197
+
198
+ i = start
199
+ n = len(text)
200
+ while i < n:
201
+ lt = text.find("<", i)
202
+ if lt < 0:
191
203
  return -1, False
192
- return entml_at, False
193
- plain_at = _find_plain_thinking_open(text, start)
194
- if entml_at < 0 and plain_at < 0:
195
- return -1, False
196
- if entml_at < 0:
197
- return plain_at, True
198
- if plain_at < 0:
199
- return entml_at, False
200
- if plain_at < entml_at:
201
- return plain_at, True
202
- return entml_at, False
204
+ gt = text.find(">", lt)
205
+ if gt < 0:
206
+ return _partial_open_check(text, lt, thinking_enabled)
207
+ tag = text[lt : gt + 1]
208
+ i = _advance_past_param_body(tag, gt, _skip_param_body)
209
+ if i is not None:
210
+ continue
211
+ if tag == (_THINKING_OPEN_PREFIX + ">") or tag.startswith(
212
+ _THINKING_OPEN_PREFIX + " "
213
+ ):
214
+ return lt, False
215
+ if thinking_enabled and _is_plain_thinking_open(tag):
216
+ return lt, True
217
+ i = gt + 1
218
+ return -1, False
203
219
 
204
- def has_unclosed_entml_thinking(
220
+
221
+ def _partial_open_check(text: str, lt: int, thinking_enabled: bool) -> Tuple[int, bool]:
222
+ """处理未收齐的 ``<...`` 前缀:若是 thinking 开标签真前缀则视其为未闭合块。"""
223
+ partial = text[lt:]
224
+ if partial.startswith(_THINKING_OPEN_PREFIX):
225
+ return lt, False
226
+ if thinking_enabled and (
227
+ partial.startswith(_PLAIN_THINKING_OPEN_PREFIX)
228
+ and not partial.startswith(_THINKING_OPEN_PREFIX)
229
+ ):
230
+ return lt, True
231
+ return -1, False
232
+
233
+
234
+ def _is_plain_thinking_open(tag: str) -> bool:
235
+ """``tag`` 是否为 plain ``<thinking...>``(不含 ``<entml:thinking`` 前缀)。"""
236
+ return tag.startswith(_PLAIN_THINKING_OPEN_PREFIX) and not tag.startswith(
237
+ _THINKING_OPEN_PREFIX
238
+ )
239
+
240
+
241
+ def _advance_past_param_body(
242
+ tag: str,
243
+ gt: int,
244
+ skip_fn: "Callable[[str], Optional[int]]",
245
+ ) -> Optional[int]:
246
+ """若是 ``<entml:parameter[...]>`` / ``<entml:parameters>``,返回体闭合标签后的下标。"""
247
+ if tag.startswith("<entml:parameter"):
248
+ return skip_fn("</entml:parameter>")
249
+ if tag == "<entml:parameters>":
250
+ return skip_fn("</entml:parameters>")
251
+ return None
252
+
253
+
254
+ def first_unclosed_thinking_open(
205
255
  text: str,
206
256
  *,
207
257
  thinking_enabled: bool = True,
208
- ) -> bool:
209
- """buffer 中是否存在尚未闭合的 thinking 块(含开标签未收齐)。"""
258
+ ) -> int:
259
+ """返回第一个未闭合 thinking 块的开标签下标;无则 ``-1``。"""
210
260
  if not text:
211
- return False
261
+ return -1
212
262
  i = 0
213
263
  while i < len(text):
214
264
  open_at, opened_plain = _find_earliest_thinking_open(
@@ -218,7 +268,7 @@ def has_unclosed_entml_thinking(
218
268
  break
219
269
  gt = text.find(">", open_at)
220
270
  if gt < 0:
221
- return True
271
+ return open_at
222
272
  body_start = gt + 1
223
273
  close_at, close_len = _find_thinking_close(
224
274
  text,
@@ -227,7 +277,7 @@ def has_unclosed_entml_thinking(
227
277
  thinking_enabled=thinking_enabled,
228
278
  )
229
279
  if close_at < 0:
230
- return True
280
+ return open_at
231
281
  i = close_at + close_len
232
282
  max_keep = len(_THINKING_OPEN_PREFIX) - 1
233
283
  if thinking_enabled:
@@ -241,13 +291,24 @@ def has_unclosed_entml_thinking(
241
291
  ):
242
292
  if any(other.startswith(suffix) for other in _AMBIGUOUS_ENTML_PREFIXES):
243
293
  continue
244
- return True
294
+ return len(text) - length
245
295
  if thinking_enabled and (
246
296
  _PLAIN_THINKING_OPEN_PREFIX.startswith(suffix)
247
297
  and suffix != _PLAIN_THINKING_OPEN_PREFIX
248
298
  ):
249
- return True
250
- return False
299
+ return len(text) - length
300
+ return -1
301
+
302
+
303
+ def has_unclosed_entml_thinking(
304
+ text: str,
305
+ *,
306
+ thinking_enabled: bool = True,
307
+ ) -> bool:
308
+ """buffer 中是否存在尚未闭合的 thinking 块(含开标签未收齐)。"""
309
+ return first_unclosed_thinking_open(
310
+ text, thinking_enabled=thinking_enabled
311
+ ) >= 0
251
312
 
252
313
 
253
314
  def invoke_index_inside_unclosed_thinking(
@@ -44,6 +44,7 @@ from echotools.exec.fncall.protocols.entml_think.hold import (
44
44
  _find_earliest_thinking_open,
45
45
  _find_thinking_close,
46
46
  find_complete_entml_invoke_open,
47
+ first_unclosed_thinking_open,
47
48
  has_unclosed_entml_thinking,
48
49
  invoke_index_inside_any_thinking_block,
49
50
  invoke_index_inside_unclosed_thinking,
@@ -71,6 +72,8 @@ def leading_entml_tag_holdback_len(
71
72
  if comment_hold:
72
73
  return comment_hold
73
74
  prefixes: List[str] = list(_LEADING_HOLD_PREFIXES)
75
+ # 流式分片可能把伪 history 闭标签切成真前缀,同样需要 hold
76
+ prefixes.extend(("</assistant", "</tool"))
74
77
  if thinking_enabled:
75
78
  prefixes.append(_PLAIN_THINKING_OPEN_PREFIX)
76
79
  for prefix in prefixes:
@@ -97,7 +100,8 @@ def stream_safe_visible_prefix(
97
100
  return ""
98
101
  buf = text[hold:]
99
102
  if has_unclosed_entml_thinking(buf, thinking_enabled=thinking_enabled):
100
- open_at, _ = _find_earliest_thinking_open(
103
+ # 取第一个未闭合 thinking 块之前的内容;此前已闭合的 thinking 不应截断(交错场景)
104
+ open_at = first_unclosed_thinking_open(
101
105
  buf, thinking_enabled=thinking_enabled
102
106
  )
103
107
  if open_at >= 0:
@@ -128,6 +132,8 @@ def trailing_entml_tag_holdback_len(text: str) -> int:
128
132
  candidates: List[str] = list(_LEADING_HOLD_PREFIXES) + [
129
133
  _THINKING_CLOSE,
130
134
  _FAULT_THINKING_CLOSE,
135
+ "</assistant",
136
+ "</tool",
131
137
  ]
132
138
  for tag in candidates:
133
139
  if tag.startswith(tail) and tail != tag:
@@ -177,6 +183,9 @@ def clean_stream_partial_visible(
177
183
  tail_hold = trailing_entml_tag_holdback_len(text)
178
184
  if tail_hold:
179
185
  text = text[:-tail_hold]
186
+ # 剥掉未收齐标签后统一去尾部换行空白:否则标签补全瞬间长度回退,
187
+ # 流式增量(按长度推进)会错位。不剥空格——词尾空格是正文的一部分
188
+ text = text.rstrip("\n\r")
180
189
  text = strip_fake_entml_structure_markup_for_display(text)[0]
181
190
  if has_calls:
182
191
  text = text.rstrip()
@@ -239,7 +248,7 @@ def split_entml_thinking(
239
248
  clean_parts.append(text[i:open_at])
240
249
  gt = text.find(">", open_at)
241
250
  if gt < 0:
242
- clean_parts.append(text[open_at:])
251
+ # 半开标签(未收齐)——不进 content,只保留其前可见
243
252
  break
244
253
  body_start = gt + 1
245
254
  close_at, close_len = _find_thinking_close(
@@ -249,7 +258,8 @@ def split_entml_thinking(
249
258
  thinking_enabled=thinking_enabled,
250
259
  )
251
260
  if close_at < 0:
252
- clean_parts.append(text[open_at:])
261
+ # thinking 开标签完整但未闭合——其内容保留为可见(内含的 invoke 仍可解析)
262
+ clean_parts.append(text[body_start:])
253
263
  break
254
264
  parts.append(text[body_start:close_at])
255
265
  i = _advance_past_fault_thinking_close(
@@ -120,7 +120,13 @@ def parse_invoke_args(
120
120
  ) -> Dict[str, Any]:
121
121
  func_props = (schema_index or {}).get(name) or {}
122
122
  if "</entml:invoke>" in body:
123
- body = body[: body.index("</entml:invoke>")]
123
+ # 参数体内可能含字面量 ``</entml:invoke>``(如 Python 正则字符串),
124
+ # 不能直接截断;用 tag-aware 查找器定位真实闭合(找不到才截断)。
125
+ from ..entml_patterns.invoke import _find_matching_close
126
+
127
+ real_close = _find_matching_close(body, 0, "</entml:invoke>")
128
+ if real_close >= 0:
129
+ body = body[:real_close]
124
130
  body = synthetic_close_invoke_body(body)
125
131
  params_m = PARAMETERS_RE.search(body)
126
132
  if params_m:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.4.9
3
+ Version: 2.4.10
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -231,6 +231,7 @@ src/tests/test_entml_bare_invoke_format.py
231
231
  src/tests/test_entml_batch_stream_comprehensive.py
232
232
  src/tests/test_entml_fake_structure_markup.py
233
233
  src/tests/test_entml_golden.py
234
+ src/tests/test_entml_interleaved_corpus.py
234
235
  src/tests/test_entml_invoke_known_tool_gate.py
235
236
  src/tests/test_entml_parser_comprehensive.py
236
237
  src/tests/test_entml_protocol.py
@@ -285,6 +286,7 @@ src/tests/test_web_broker.py
285
286
  src/tests/fixtures/__init__.py
286
287
  src/tests/fixtures/entml_golden.py
287
288
  src/tests/fixtures/entml_golden_update.py
289
+ src/tests/fixtures/interleaved_corpus.py
288
290
  src/tests/fixtures/simulated_fake_history_markup_responses.py
289
291
  src/tests/fixtures/simulated_fault_thinking_responses.py
290
292
  src/tests/fixtures/simulated_llm_tool_responses.py
@@ -0,0 +1,116 @@
1
+ """交错 thinking / invoke / 正文 回归语料。
2
+
3
+ 结构提炼自 Rogator 日志 `X:\\Project\\Public\\Rogator\\logs\\responses\\req-1787237525-7f1478678d5b.txt`:
4
+ 多轮 ``<entml:thinking>`` → 正文 → ``<entml:invoke>`` 交错出现,
5
+ invoke 之间及 invoke 之后继续有正文与思考,最后以纯正文结尾。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, List
11
+
12
+ INTERLEAVED_TOOLS: List[Dict[str, Any]] = [
13
+ {
14
+ "type": "function",
15
+ "function": {
16
+ "name": "Edit",
17
+ "description": "编辑文件",
18
+ "parameters": {
19
+ "type": "object",
20
+ "properties": {
21
+ "file_path": {"type": "string"},
22
+ "old_string": {"type": "string"},
23
+ "new_string": {"type": "string"},
24
+ },
25
+ "required": ["file_path", "old_string", "new_string"],
26
+ },
27
+ },
28
+ },
29
+ {
30
+ "type": "function",
31
+ "function": {
32
+ "name": "Read",
33
+ "description": "读取文件",
34
+ "parameters": {
35
+ "type": "object",
36
+ "properties": {
37
+ "file_path": {"type": "string"},
38
+ "offset": {"type": "integer"},
39
+ "limit": {"type": "integer"},
40
+ },
41
+ "required": ["file_path"],
42
+ },
43
+ },
44
+ },
45
+ {
46
+ "type": "function",
47
+ "function": {
48
+ "name": "Bash",
49
+ "description": "运行命令",
50
+ "parameters": {
51
+ "type": "object",
52
+ "properties": {"command": {"type": "string"}},
53
+ "required": ["command"],
54
+ },
55
+ },
56
+ },
57
+ ]
58
+
59
+ INTERLEAVED_CORPUS = """<entml:thinking>
60
+ 第一轮思考:需要先定位现有实现。
61
+ </entml:thinking>
62
+
63
+ 第一轮正文:我先看看当前实现。
64
+
65
+ <entml:invoke name="Edit">
66
+ <entml:parameter name="file_path">src/config.py</entml:parameter>
67
+ <entml:parameter name="old_string">old value</entml:parameter>
68
+ <entml:parameter name="new_string">new value</entml:parameter>
69
+ </entml:invoke>
70
+ </assistant>
71
+
72
+ <entml:thinking>
73
+ 第二轮思考:Edit 完成,继续检查调用点。
74
+ </entml:thinking>
75
+
76
+ 第二轮正文:继续修复第二个位置。
77
+
78
+ <entml:invoke name="Read">
79
+ <entml:parameter name="file_path">src/config.py</entml:parameter>
80
+ <entml:parameter name="offset">10</entml:parameter>
81
+ <entml:parameter name="limit">25</entml:parameter>
82
+ </entml:invoke>
83
+ </assistant>
84
+
85
+ <entml:thinking>
86
+ 第三轮思考:调用点确认,最后运行测试。
87
+ </entml:thinking>
88
+
89
+ 第三轮正文:修改完成,运行验证。
90
+
91
+ <entml:invoke name="Bash">
92
+ <entml:parameter name="command">python -m pytest -q</entml:parameter>
93
+ </entml:invoke>
94
+ <entml:invoke name="Bash">
95
+ <entml:parameter name="command">python achecker.py</entml:parameter>
96
+ </entml:invoke>
97
+ </assistant>
98
+
99
+ 全部通过,修复完成。
100
+
101
+ """
102
+
103
+ EXPECT_NAMES = ["Edit", "Read", "Bash", "Bash"]
104
+
105
+ EXPECT_VISIBLE_SEGMENTS = [
106
+ "第一轮正文:我先看看当前实现。",
107
+ "第二轮正文:继续修复第二个位置。",
108
+ "第三轮正文:修改完成,运行验证。",
109
+ "全部通过,修复完成。",
110
+ ]
111
+
112
+ EXPECT_THINKING_MARKS = [
113
+ "第一轮思考:需要先定位现有实现。",
114
+ "第二轮思考:Edit 完成,继续检查调用点。",
115
+ "第三轮思考:调用点确认,最后运行测试。",
116
+ ]