echotools 2.3.84__tar.gz → 2.3.86__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 (239) hide show
  1. {echotools-2.3.84 → echotools-2.3.86}/PKG-INFO +1 -1
  2. {echotools-2.3.84 → echotools-2.3.86}/pyproject.toml +1 -1
  3. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/parsers/stream.py +17 -3
  4. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml.py +5 -15
  5. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_patterns.py +1 -1
  6. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_stream_json.py +15 -0
  7. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_think/parse.py +91 -34
  8. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_tool_blocks.py +2 -2
  9. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/history_markup.py +6 -5
  10. {echotools-2.3.84 → echotools-2.3.86}/src/echotools.egg-info/PKG-INFO +1 -1
  11. {echotools-2.3.84 → echotools-2.3.86}/src/tests/fixtures/simulated_fake_history_markup_responses.py +3 -3
  12. {echotools-2.3.84 → echotools-2.3.86}/src/tests/fixtures/simulated_fault_thinking_responses.py +1 -1
  13. {echotools-2.3.84 → echotools-2.3.86}/src/tests/fixtures/simulated_llm_tool_responses.py +2 -2
  14. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_parser_comprehensive.py +1 -1
  15. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_protocol.py +111 -16
  16. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_tool_pipeline.py +11 -9
  17. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fncall_stream_thinking.py +20 -0
  18. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_history_markup_filter.py +2 -2
  19. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_stream_partial_json_parity.py +37 -0
  20. {echotools-2.3.84 → echotools-2.3.86}/README.md +0 -0
  21. {echotools-2.3.84 → echotools-2.3.86}/docs/README.md +0 -0
  22. {echotools-2.3.84 → echotools-2.3.86}/setup.cfg +0 -0
  23. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/__init__.py +0 -0
  24. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/cache/__init__.py +0 -0
  25. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/cache/list_cache.py +0 -0
  26. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/cache/memory_cache.py +0 -0
  27. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/__init__.py +0 -0
  28. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/base.py +0 -0
  29. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/center.py +0 -0
  30. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/center_notify.py +0 -0
  31. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/center_template.py +0 -0
  32. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/loader.py +0 -0
  33. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/merge.py +0 -0
  34. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/errors/__init__.py +0 -0
  35. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/errors/base.py +0 -0
  36. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/errors/classify.py +0 -0
  37. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/errors/common.py +0 -0
  38. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/errors/http.py +0 -0
  39. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/ids/__init__.py +0 -0
  40. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/ids/generator.py +0 -0
  41. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/io/__init__.py +0 -0
  42. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/io/io_utils.py +0 -0
  43. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/io/printstream.py +0 -0
  44. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/logger/__init__.py +0 -0
  45. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/logger/manager.py +0 -0
  46. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/retry/__init__.py +0 -0
  47. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/retry/retry.py +0 -0
  48. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/compat.py +0 -0
  49. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/__init__.py +0 -0
  50. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/candidate.py +0 -0
  51. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  52. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  53. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/race.py +0 -0
  54. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/selector.py +0 -0
  55. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/usage.py +0 -0
  56. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/__init__.py +0 -0
  57. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  58. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  59. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  60. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  61. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/prompt/history.py +0 -0
  62. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  63. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  64. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  65. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  66. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  67. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
  68. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  69. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
  70. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  71. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  72. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  73. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/registry.py +0 -0
  74. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  75. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  76. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  77. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  78. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  79. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  80. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/keys.py +0 -0
  81. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/lifecycle/__init__.py +0 -0
  82. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/lifecycle/manager.py +0 -0
  83. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/lifecycle/updater.py +0 -0
  84. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  85. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/process/__init__.py +0 -0
  86. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/process/port.py +0 -0
  87. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/protocol/__init__.py +0 -0
  88. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/protocol/base.py +0 -0
  89. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/__init__.py +0 -0
  90. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/conpty.py +0 -0
  91. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/local/__init__.py +0 -0
  92. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/local/core.py +0 -0
  93. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/local/proc.py +0 -0
  94. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/local/unix.py +0 -0
  95. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/local/win.py +0 -0
  96. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/sanitize.py +0 -0
  97. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/session/__init__.py +0 -0
  98. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/session/base.py +0 -0
  99. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/session/client.py +0 -0
  100. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/ssh.py +0 -0
  101. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/tmux.py +0 -0
  102. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/__init__.py +0 -0
  103. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/charmap.py +0 -0
  104. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/spinner.py +0 -0
  105. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/ui.py +0 -0
  106. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  107. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  108. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  109. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  110. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  111. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  112. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/ui_console.py +0 -0
  113. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/ui_io.py +0 -0
  114. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/ui_log.py +0 -0
  115. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  116. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/ui_text.py +0 -0
  117. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/ui_types.py +0 -0
  118. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  119. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  120. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  121. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  122. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  123. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  124. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  125. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  126. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  127. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  128. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  129. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/events/__init__.py +0 -0
  130. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/events/bus.py +0 -0
  131. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/events/event.py +0 -0
  132. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/spinner/__init__.py +0 -0
  133. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/spinner/spinner.py +0 -0
  134. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/spinner/spinner_color.py +0 -0
  135. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/tracing/__init__.py +0 -0
  136. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/tracing/context.py +0 -0
  137. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/tracing/span.py +0 -0
  138. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/tracing/tracer.py +0 -0
  139. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/translate.py +0 -0
  140. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/watcher/__init__.py +0 -0
  141. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/watcher/file_watcher.py +0 -0
  142. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/__init__.py +0 -0
  143. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/application.py +0 -0
  144. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/broker.py +0 -0
  145. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/app.js +0 -0
  146. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  147. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  148. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  149. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  150. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  151. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  152. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  153. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  154. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  155. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  156. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/inputbox.css +0 -0
  157. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/inputbox.js +0 -0
  158. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/style.css +0 -0
  159. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/voicegif.js +0 -0
  160. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/middleware.py +0 -0
  161. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/stats.py +0 -0
  162. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/utils.py +0 -0
  163. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/files/__init__.py +0 -0
  164. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/files/file_util.py +0 -0
  165. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/network/__init__.py +0 -0
  166. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/network/http_utils.py +0 -0
  167. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/plugin/__init__.py +0 -0
  168. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/plugin/base.py +0 -0
  169. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/plugin/discovery.py +0 -0
  170. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/plugin/registry.py +0 -0
  171. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/plugin/registry_ops.py +0 -0
  172. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/proxy/__init__.py +0 -0
  173. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/proxy/manager.py +0 -0
  174. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/runtime/__init__.py +0 -0
  175. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/runtime/collector.py +0 -0
  176. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/scheduler/__init__.py +0 -0
  177. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/scheduler/scheduler.py +0 -0
  178. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/sdk/__init__.py +0 -0
  179. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/sdk/facade.py +0 -0
  180. {echotools-2.3.84 → echotools-2.3.86}/src/echotools/version.py +0 -0
  181. {echotools-2.3.84 → echotools-2.3.86}/src/echotools.egg-info/SOURCES.txt +0 -0
  182. {echotools-2.3.84 → echotools-2.3.86}/src/echotools.egg-info/dependency_links.txt +0 -0
  183. {echotools-2.3.84 → echotools-2.3.86}/src/echotools.egg-info/requires.txt +0 -0
  184. {echotools-2.3.84 → echotools-2.3.86}/src/echotools.egg-info/top_level.txt +0 -0
  185. {echotools-2.3.84 → echotools-2.3.86}/src/tests/bench_import.py +0 -0
  186. {echotools-2.3.84 → echotools-2.3.86}/src/tests/conftest.py +0 -0
  187. {echotools-2.3.84 → echotools-2.3.86}/src/tests/fixtures/__init__.py +0 -0
  188. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_adversarial_stream_splits.py +0 -0
  189. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_cache_retry.py +0 -0
  190. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_completion.py +0 -0
  191. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_config.py +0 -0
  192. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_config_extended.py +0 -0
  193. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_console.py +0 -0
  194. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_coverage_core.py +0 -0
  195. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_dispatch.py +0 -0
  196. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_dispatch_extended.py +0 -0
  197. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_dispatch_race.py +0 -0
  198. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_agent_output_formats.py +0 -0
  199. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_bare_invoke_format.py +0 -0
  200. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_batch_stream_comprehensive.py +0 -0
  201. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_stream_json.py +0 -0
  202. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
  203. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_thinking_history.py +0 -0
  204. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_thinking_parse.py +0 -0
  205. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_tool_parse_leak.py +0 -0
  206. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_tools.py +0 -0
  207. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_errors.py +0 -0
  208. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_errors_common.py +0 -0
  209. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_events.py +0 -0
  210. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_events_event.py +0 -0
  211. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_facade.py +0 -0
  212. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fake_history_markup_batch.py +0 -0
  213. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
  214. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
  215. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fake_history_markup_stream.py +0 -0
  216. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fault_thinking_close_batch.py +0 -0
  217. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fault_thinking_close_stream.py +0 -0
  218. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fncall.py +0 -0
  219. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_ids.py +0 -0
  220. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_imports.py +0 -0
  221. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_io.py +0 -0
  222. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_keys.py +0 -0
  223. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_lifecycle.py +0 -0
  224. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_logger.py +0 -0
  225. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_merge.py +0 -0
  226. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_model_branch_matrix.py +0 -0
  227. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_plugin.py +0 -0
  228. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_printstream.py +0 -0
  229. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_protocols.py +0 -0
  230. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_proxy.py +0 -0
  231. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_proxy_selector.py +0 -0
  232. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_runtime.py +0 -0
  233. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_scheduler.py +0 -0
  234. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  235. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_stream_realtime_parse.py +0 -0
  236. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_tracing.py +0 -0
  237. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_usage.py +0 -0
  238. {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_web.py +0 -0
  239. {echotools-2.3.84 → echotools-2.3.86}/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.84
3
+ Version: 2.3.86
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.84"
7
+ version = "2.3.86"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -273,6 +273,15 @@ class FncallStreamParser:
273
273
  self._waiting_tail = remain
274
274
  return
275
275
 
276
+ from echotools.exec.fncall.protocols.entml_think.parse import (
277
+ invoke_index_inside_any_thinking_block,
278
+ )
279
+ if invoke_index_inside_any_thinking_block(
280
+ text, pos, thinking_enabled=self._thinking_enabled,
281
+ ):
282
+ self._append_content_text(text)
283
+ return
284
+
276
285
  if pos > 0:
277
286
  self._append_content_text(text[:pos])
278
287
 
@@ -319,10 +328,10 @@ class FncallStreamParser:
319
328
  if found:
320
329
  if self._thinking_filter is not None:
321
330
  from echotools.exec.fncall.protocols.entml_think.parse import (
322
- invoke_index_inside_unclosed_thinking,
331
+ invoke_index_inside_any_thinking_block,
323
332
  )
324
- if invoke_index_inside_unclosed_thinking(
325
- combined, pos, thinking_enabled=self._thinking_enabled
333
+ if invoke_index_inside_any_thinking_block(
334
+ combined, pos, thinking_enabled=self._thinking_enabled,
326
335
  ):
327
336
  self._feed_waiting_thinking_plain(combined)
328
337
  return
@@ -750,6 +759,11 @@ class FncallStreamParser:
750
759
  split_entml_thinking,
751
760
  )
752
761
 
762
+ _, batch_thinking = split_entml_thinking(
763
+ self._raw_buf, thinking_enabled=self._thinking_enabled
764
+ )
765
+ if batch_thinking:
766
+ self._thinking_parts = [batch_thinking]
753
767
  display_text, _ = split_entml_thinking(
754
768
  clean_text, thinking_enabled=self._thinking_enabled
755
769
  )
@@ -42,26 +42,15 @@ from echotools.exec.fncall.shared.normalization import (
42
42
  )
43
43
  from echotools.exec.protocol.base import ToolProtocol
44
44
 
45
- # 仅按 entml: 前缀剥离标签,不区分具体标签名。
46
- _ENTML_PAIR_RE = re.compile(
47
- r"<entml:[a-zA-Z_][\w]*\b[^>]*>.*?</entml:[a-zA-Z_][\w]*>",
48
- re.DOTALL,
49
- )
50
- _ENTML_SELF_CLOSING_RE = re.compile(r"<entml:[a-zA-Z_][\w]*\b[^>]*/>", re.DOTALL)
51
- _ENTML_ORPHAN_CLOSE_RE = re.compile(r"</entml:[a-zA-Z_][\w]*>", re.DOTALL)
52
- _ENTML_ORPHAN_OPEN_RE = re.compile(r"<entml:[a-zA-Z_][\w]*\b[^>]*>", re.DOTALL)
45
+ # user 消息:仅去掉标签名中的 entml: 命名空间,保留标签与正文。
46
+ _ENTML_NAMESPACE_RE = re.compile(r"(</?)entml:", re.IGNORECASE)
53
47
 
54
48
 
55
49
  def strip_entml_from_content(content: str) -> str:
56
- """从 user 消息正文剥离所有 entml:* 标签及残留开闭标签。"""
50
+ """user 消息:去掉 ``entml:`` 标签前缀,保留标签结构与正文(含 ``//``)。"""
57
51
  if not content:
58
52
  return content
59
- cleaned = content
60
- cleaned = _ENTML_PAIR_RE.sub("", cleaned)
61
- cleaned = _ENTML_SELF_CLOSING_RE.sub("", cleaned)
62
- cleaned = _ENTML_ORPHAN_CLOSE_RE.sub("", cleaned)
63
- cleaned = _ENTML_ORPHAN_OPEN_RE.sub("", cleaned)
64
- return cleaned.strip()
53
+ return _ENTML_NAMESPACE_RE.sub(r"\1", content)
65
54
 
66
55
 
67
56
  def _parse_tool_call_args(tc: Dict[str, Any]) -> tuple[str, Dict[str, Any]]:
@@ -321,6 +310,7 @@ class EntmlProtocol(ToolProtocol):
321
310
  return tool_calls
322
311
 
323
312
  def clean_tags(self, content: str) -> str:
313
+ """user 消息:仅去掉 ``entml:`` 前缀,不做其它剥离。"""
324
314
  return strip_entml_from_content(content)
325
315
 
326
316
  def clean_tool_tags(self, content: str) -> str:
@@ -51,7 +51,7 @@ PARAMETERS_RE = re.compile(
51
51
  r"<entml:parameters>([\s\S]*?)</entml:parameters>",
52
52
  re.DOTALL,
53
53
  )
54
- # invoke 内裸子标签(非 parameter 包裹),如 Claude Code 输出的 description/timeout。
54
+ # invoke 内裸子标签(非 parameter 包裹),如部分 agent 输出的 description/timeout。
55
55
  BARE_INVOKE_CHILD_RE = re.compile(
56
56
  r"<entml:(description|timeout)>([\s\S]*?)</entml:\1>",
57
57
  re.DOTALL | re.IGNORECASE,
@@ -42,6 +42,19 @@ def _strip_incomplete_markup_suffix(value: str) -> str:
42
42
  return value
43
43
 
44
44
 
45
+ def _strip_incomplete_child_close_suffix(value: str, key: str) -> str:
46
+ """流式未闭合 bare/direct 子标签:去掉尾部尚未收齐的 ``</...>``。"""
47
+ value = _strip_incomplete_markup_suffix(value)
48
+ lt = value.rfind("<")
49
+ if lt < 0:
50
+ return value
51
+ tail = value[lt:]
52
+ for close in (f"</entml:{key}>", f"</{key}>"):
53
+ if close.startswith(tail) and tail != close:
54
+ return value[:lt]
55
+ return value
56
+
57
+
45
58
  def split_invoke_open(buffer: str) -> Optional[Tuple[str, int]]:
46
59
  """返回当前正在流式写入的 invoke(最后一个有效开标签)。"""
47
60
  search_from = 0
@@ -177,6 +190,7 @@ def _parse_bare_invoke_entries(body: str) -> List[Tuple[str, str, bool, Optional
177
190
  tail = match.group(2) or ""
178
191
  if close in tail:
179
192
  continue
193
+ tail = _strip_incomplete_child_close_suffix(tail, key)
180
194
  tagged.append((match.start(), key, tail, False, None))
181
195
  tagged.sort(key=lambda item: item[0])
182
196
  seen: set[str] = set()
@@ -236,6 +250,7 @@ def _parse_direct_child_entries(body: str) -> List[Tuple[str, str, bool, Optiona
236
250
  tail = match.group(2) or ""
237
251
  if close in tail:
238
252
  continue
253
+ tail = _strip_incomplete_child_close_suffix(tail, key)
239
254
  tagged.append((match.start(), key, tail, False, None))
240
255
  tagged.sort(key=lambda item: item[0])
241
256
  seen: set[str] = set()
@@ -210,6 +210,19 @@ def _find_earliest_thinking_open(
210
210
  return entml_at, False
211
211
 
212
212
 
213
+ def tool_markup_follows_entml_thinking_close(text: str) -> bool:
214
+ """``</entml:thinking>`` 后若紧跟真实工具 markup,则 fault ``</thinking>`` 与 entml 闭标签间为可见正文。"""
215
+ if not text:
216
+ return False
217
+ return bool(
218
+ re.search(
219
+ r"^\s*(?:<tool\s*>|<entml:invoke\b)",
220
+ text,
221
+ re.IGNORECASE | re.DOTALL,
222
+ )
223
+ )
224
+
225
+
213
226
  def _find_thinking_close(
214
227
  text: str,
215
228
  body_start: int,
@@ -252,6 +265,9 @@ def _find_thinking_close(
252
265
  if entml_after < 0 or invoke_abs < entml_after:
253
266
  return fault_close, len(_FAULT_THINKING_CLOSE)
254
267
  if entml_after >= 0:
268
+ after_entml = text[entml_after + len(_THINKING_CLOSE) :]
269
+ if tool_markup_follows_entml_thinking_close(after_entml):
270
+ return fault_close, len(_FAULT_THINKING_CLOSE)
255
271
  return entml_after, len(_THINKING_CLOSE)
256
272
  return -1, 0
257
273
 
@@ -353,26 +369,49 @@ def invoke_index_inside_unclosed_thinking(
353
369
  thinking_enabled: bool = True,
354
370
  ) -> bool:
355
371
  """``invoke_at`` 是否落在尚未闭合的 thinking 块内。"""
356
- if invoke_at < 0:
357
- return False
358
- think_open, opened_plain = _find_earliest_thinking_open(
359
- text, thinking_enabled=thinking_enabled
372
+ return invoke_index_inside_any_thinking_block(
373
+ text, invoke_at, thinking_enabled=thinking_enabled, unclosed_only=True,
360
374
  )
361
- if think_open < 0 or invoke_at <= think_open:
375
+
376
+
377
+ def invoke_index_inside_any_thinking_block(
378
+ text: str,
379
+ invoke_at: int,
380
+ *,
381
+ thinking_enabled: bool = True,
382
+ unclosed_only: bool = False,
383
+ ) -> bool:
384
+ """``invoke_at`` 是否落在 thinking 块内(``unclosed_only`` 时仅检查未闭合块)。"""
385
+ if invoke_at < 0:
362
386
  return False
363
- gt = text.find(">", think_open)
364
- if gt < 0:
365
- return True
366
- body_start = gt + 1
367
- close_at, close_len = _find_thinking_close(
368
- text,
369
- body_start,
370
- opened_plain=opened_plain,
371
- thinking_enabled=thinking_enabled,
372
- )
373
- if close_at < 0:
374
- return True
375
- return invoke_at < close_at + close_len
387
+ i = 0
388
+ while i < len(text):
389
+ open_at, opened_plain = _find_earliest_thinking_open(
390
+ text, i, thinking_enabled=thinking_enabled,
391
+ )
392
+ if open_at < 0:
393
+ break
394
+ gt = text.find(">", open_at)
395
+ if gt < 0:
396
+ return invoke_at > open_at
397
+ body_start = gt + 1
398
+ close_at, close_len = _find_thinking_close(
399
+ text,
400
+ body_start,
401
+ opened_plain=opened_plain,
402
+ thinking_enabled=thinking_enabled,
403
+ )
404
+ if close_at < 0:
405
+ if open_at < invoke_at:
406
+ return True
407
+ break
408
+ block_end = close_at + close_len
409
+ if open_at < invoke_at < block_end:
410
+ return True
411
+ if unclosed_only:
412
+ return False
413
+ i = block_end
414
+ return False
376
415
 
377
416
 
378
417
  def split_entml_thinking(
@@ -415,6 +454,18 @@ def split_entml_thinking(
415
454
 
416
455
  parts.append(text[body_start:close_at])
417
456
  i = close_at + close_len
457
+ if (
458
+ close_len == len(_FAULT_THINKING_CLOSE)
459
+ and not opened_plain
460
+ and thinking_enabled
461
+ ):
462
+ entml_after = text.find(_THINKING_CLOSE, i)
463
+ if entml_after >= 0:
464
+ after_entml = text[entml_after + len(_THINKING_CLOSE) :]
465
+ if tool_markup_follows_entml_thinking_close(after_entml):
466
+ clean_parts.append(text[i:entml_after])
467
+ i = entml_after + len(_THINKING_CLOSE)
468
+ continue
418
469
 
419
470
  clean = "".join(clean_parts)
420
471
  thinking = "\n".join(part.strip() for part in parts if part.strip())
@@ -426,16 +477,23 @@ def split_entml_thinking(
426
477
 
427
478
 
428
479
  def strip_orphan_thinking_close_prefix(text: str) -> str:
429
- """去掉可见正文开头的 orphan ``</entml:thinking>``(流式分片边界)。"""
480
+ """去掉可见正文开头/末尾的 orphan ``</entml:thinking>``(流式分片边界)。"""
430
481
  if not text:
431
482
  return text
432
- return re.sub(
483
+ text = re.sub(
433
484
  r"^\s*</entml:thinking>\s*",
434
485
  "",
435
486
  text,
436
487
  count=1,
437
488
  flags=re.IGNORECASE,
438
489
  )
490
+ return re.sub(
491
+ r"\s*</entml:thinking>\s*$",
492
+ "",
493
+ text,
494
+ count=1,
495
+ flags=re.IGNORECASE,
496
+ )
439
497
 
440
498
 
441
499
  def _hold_prefix(buffer: str, tag: str) -> Tuple[str, str]:
@@ -510,20 +568,27 @@ class EntmlThinkingStreamFilter:
510
568
  return True
511
569
 
512
570
  def _try_resolve_fault_watch_on_entml_close(self, out: List[Tuple[str, str]]) -> bool:
513
- """未见 invoke 时出现 ``</entml:thinking>`` → ``</thinking>`` 仅为思考内纯文本。"""
571
+ """未见 invoke 时出现 ``</entml:thinking>`` → 若其后为 tool/invoke 则中间段为可见正文。"""
514
572
  if not self._fault_watch:
515
573
  return False
516
574
  close_at = self._fault_buffer.find(_THINKING_CLOSE)
517
575
  if close_at < 0:
518
576
  return False
519
- emitted = self._emit_thinking_piece(self._fault_buffer[:close_at])
520
- if emitted:
521
- out.append(("thinking", emitted))
577
+ after_entml = self._fault_buffer[close_at + len(_THINKING_CLOSE) :]
578
+ if tool_markup_follows_entml_thinking_close(after_entml):
579
+ middle = self._fault_buffer[len(_FAULT_THINKING_CLOSE) : close_at]
580
+ visible = middle.strip()
581
+ if visible:
582
+ out.append(("content", visible))
583
+ else:
584
+ emitted = self._emit_thinking_piece(self._fault_buffer[:close_at])
585
+ if emitted:
586
+ out.append(("thinking", emitted))
522
587
  self._in_block = False
523
588
  self._opened_plain = False
524
589
  self._fault_watch = False
525
590
  self._thinking_started = False
526
- self._pending = self._fault_buffer[close_at + len(_THINKING_CLOSE) :]
591
+ self._pending = after_entml
527
592
  self._fault_buffer = ""
528
593
  return True
529
594
 
@@ -574,15 +639,7 @@ class EntmlThinkingStreamFilter:
574
639
  self._pending = ""
575
640
  return False
576
641
 
577
- safe, tool_hold = _hold_ambiguous_tool_markup(self._pending)
578
- if tool_hold:
579
- if safe:
580
- emitted = self._emit_thinking_piece(safe)
581
- if emitted:
582
- out.append(("thinking", emitted))
583
- self._pending = tool_hold
584
- return False
585
-
642
+ # thinking 块内 ``<entml:invoke>`` 等仅为 prose/示例时仍计入 thinking,不按工具 hold 截断。
586
643
  safe, hold = _hold_prefix(self._pending, _THINKING_CLOSE)
587
644
  if not hold and self._thinking_enabled:
588
645
  safe, hold = _hold_prefix(safe, _FAULT_THINKING_CLOSE)
@@ -24,9 +24,9 @@ _INNER_TOOL_TAG_RE = re.compile(
24
24
  re.IGNORECASE,
25
25
  )
26
26
  _BRACE_TOOL_HEAD_RE = re.compile(r"\{([A-Za-z][A-Za-z0-9_]*)\s*:\s*")
27
- # ``{Bash>`` 后接 ``<entml:parameter>`` 的混合格式(``{Edit: json}`` 仍走 history brace)
27
+ # ``{Bash>`` / ``{Read}`` 后接 ``<entml:parameter>`` 的混合格式(``{Edit: json}`` 仍走 history brace)
28
28
  _MANGLED_BRACE_ENTML_HEAD_RE = re.compile(
29
- r"^\s*\{([A-Za-z][A-Za-z0-9_]*)\s*>\s*",
29
+ r"^\s*\{([A-Za-z][A-Za-z0-9_]*)\s*(?:>|\})\s*",
30
30
  re.MULTILINE,
31
31
  )
32
32
  _PARAM_MARKER_RE = re.compile(
@@ -98,10 +98,11 @@ def _paired_block_glued_brace_re(tag: str) -> re.Pattern[str]:
98
98
  )
99
99
 
100
100
 
101
- def _orphan_close_re(tag: str) -> re.Pattern[str]:
101
+ def _orphan_close_line_re(tag: str) -> re.Pattern[str]:
102
+ """无配对开标签的 ``</tag>`` 行:仅删该行,不删其前的合法可见正文。"""
102
103
  return re.compile(
103
- rf"(?:^|\n)([\s\S]*?)\n\s*</{tag}\s*>\s*(?:\n|$)",
104
- re.IGNORECASE,
104
+ rf"^\s*</{tag}\s*>\s*$",
105
+ re.IGNORECASE | re.MULTILINE,
105
106
  )
106
107
 
107
108
 
@@ -193,9 +194,9 @@ def _strip_fake_blocks_in_unprotected(
193
194
  break
194
195
  found = True
195
196
  text = new_text
196
- close_re = _orphan_close_re(tag)
197
+ close_re = _orphan_close_line_re(tag)
197
198
  while True:
198
- new_text, n = close_re.subn("\n", text, count=1)
199
+ new_text, n = close_re.subn("", text, count=1)
199
200
  if n == 0:
200
201
  break
201
202
  found = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.84
3
+ Version: 2.3.86
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -110,7 +110,7 @@ def tools_for_markup_case(case: HistoryMarkupCase) -> List[Dict[str, Any]]:
110
110
  FAKE_HISTORY_MARKUP_CASES: Tuple[HistoryMarkupCase, ...] = (
111
111
  HistoryMarkupCase(
112
112
  id="model_edit_mimic_orphan_thinking",
113
- description="Claude Code 高发:正文 + orphan </thinking> + 多块 Edit 伪 tool",
113
+ description="高发语料:正文 + orphan </thinking> + 多块 Edit 伪 tool",
114
114
  extra_tools=("Read", "Edit"),
115
115
  response=(
116
116
  "● 验证通过,无外部依赖残留。现在清理几个文件中遗留的旧路径注释。\n\n"
@@ -166,8 +166,8 @@ FAKE_HISTORY_MARKUP_CASES: Tuple[HistoryMarkupCase, ...] = (
166
166
  id="orphan_assistant_close",
167
167
  description="无开标签的 </assistant> 反向闭合",
168
168
  response="分析中...\n</assistant>\n后续仍可见",
169
- expect_clean_contains=("后续仍可见",),
170
- expect_clean_excludes=("分析中", "</assistant>"),
169
+ expect_clean_contains=("后续仍可见", "分析中"),
170
+ expect_clean_excludes=("</assistant>",),
171
171
  ),
172
172
  HistoryMarkupCase(
173
173
  id="fake_tool_bash_scalar",
@@ -1,4 +1,4 @@
1
- """模拟模型 ``<entml:thinking>…</thinking>`` fault 容错输出(rogator / Claude Code 高发)。
1
+ """模拟模型 ``<entml:thinking>…</thinking>`` fault 容错输出(rogator / agent 客户端高发)。
2
2
 
3
3
  非真实模型日志;用于 batch ``protocol.parse`` 与 ``FncallStreamParser`` 分流回归。
4
4
  """
@@ -99,7 +99,7 @@ TOOLS: List[Dict[str, Any]] = [
99
99
  },
100
100
  ]
101
101
 
102
- # Claude Code / rogator 链路常见工具(不在默认 TOOLS 内)
102
+ # Anthropic 兼容客户端 / rogator 链路常见工具(不在默认 TOOLS 内)
103
103
  AGENT_TOOLS: List[Dict[str, Any]] = [
104
104
  {
105
105
  "type": "function",
@@ -580,7 +580,7 @@ SIMULATED_LLM_RESPONSES: List[SimulatedCase] = [
580
580
  expect_args=[{"city": "杭州", "unit": "c"}],
581
581
  expect_clean_substrings=["参考历史:", "我再确认一次实时天气。"],
582
582
  ),
583
- # --- 模型分支:Claude Code / rogator 常见 agent 工具 ---
583
+ # --- 模型分支:Anthropic 兼容客户端 / rogator 常见 agent 工具 ---
584
584
  SimulatedCase(
585
585
  id="agent_write_windows_path",
586
586
  description="Write:Windows 绝对路径 + 含引号 contents",
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- """entml 流式/批量解析器全方位测试:普通路径 + 边界 + rogator/Claude Code 高发场景。
3
+ """entml 流式/批量解析器全方位测试:普通路径 + 边界 + rogator/agent 客户端高发场景。
4
4
 
5
5
  与 test_simulated_llm_tool_parse / test_adversarial_stream_splits 互补:
6
6
  - 本文件聚焦 thinking/invoke 边界、fault ``</thinking>``、holdback 分片、
@@ -518,16 +518,30 @@ def test_strip_entml_from_user_content() -> None:
518
518
 
519
519
  raw = (
520
520
  "请查天气\n"
521
- "<entml:thinking>不应出现</entml:thinking>\n"
522
- "<entml:function_calls><entml:invoke name=\"get_weather\">"
523
- "<entml:parameter name=\"city\">杭州</entml:parameter>"
524
- "</entml:invoke></entml:function_calls>"
521
+ "<entml:thinking>thinking body</entml:thinking>\n"
522
+ '<entml:invoke name="get_weather">'
523
+ '<entml:parameter name="city">杭州</entml:parameter>'
524
+ "</entml:invoke>"
525
525
  )
526
526
  cleaned = strip_entml_from_content(raw)
527
- assert cleaned == "请查天气"
527
+ assert cleaned == (
528
+ "请查天气\n"
529
+ "<thinking>thinking body</thinking>\n"
530
+ '<invoke name="get_weather">'
531
+ '<parameter name="city">杭州</parameter>'
532
+ "</invoke>"
533
+ )
528
534
  assert "entml:" not in cleaned
529
- assert "不应出现" not in cleaned
530
- assert "杭州" not in cleaned
535
+ assert "thinking body" in cleaned
536
+ assert "杭州" in cleaned
537
+ assert cleaned.count("//") == raw.count("//")
538
+
539
+
540
+ def test_strip_entml_from_user_preserves_double_slash() -> None:
541
+ from echotools.exec.fncall.protocols.entml import strip_entml_from_content
542
+
543
+ raw = '<entml:invoke name="x">//path//</entml:invoke>'
544
+ assert strip_entml_from_content(raw) == '<invoke name="x">//path//</invoke>'
531
545
 
532
546
 
533
547
  def test_inject_strips_entml_from_all_user_messages() -> None:
@@ -550,14 +564,12 @@ def test_inject_strips_entml_from_all_user_messages() -> None:
550
564
  },
551
565
  ]
552
566
  content = inject_fncall(msgs, [], proto)[0]["content"]
553
- assert "secret" not in content
567
+ assert "secret" in content or "<thinking>secret</thinking>" in content
554
568
  assert "entml:invoke" not in content
555
- assert "entml:thinking" not in content
556
- assert "历史问题 继续" in content or "历史问题 继续" in content
557
- assert "当前问题 结束" in content or "当前问题 结束" in content
558
- assert "<current_user_message>\n当前问题 结束\n</current_user_message>" in content or (
559
- "<current_user_message>\n当前问题 结束\n</current_user_message>" in content
560
- )
569
+ assert '<invoke name="search">' in content or "search" in content
570
+ assert "历史问题" in content and "继续" in content
571
+ assert "当前问题" in content and "结束" in content
572
+ assert "<current_user_message>" in content
561
573
 
562
574
 
563
575
  def test_entml_multi_tool_history_blocks() -> None:
@@ -1025,7 +1037,7 @@ def test_inject_include_thinking_in_history() -> None:
1025
1037
 
1026
1038
 
1027
1039
  def test_entml_parse_bare_description_timeout_tags() -> None:
1028
- """Claude Code 风格:invoke 内裸 <entml:description>/<entml:timeout>。"""
1040
+ """常见 agent 风格:invoke 内裸 <entml:description>/<entml:timeout>。"""
1029
1041
  tools = [
1030
1042
  {
1031
1043
  "type": "function",
@@ -1369,7 +1381,7 @@ def test_entml_stream_finalize_strips_thinking_when_tool_calls() -> None:
1369
1381
  assert len(calls) == 1
1370
1382
  assert calls[0]["function"]["name"] == "Bash"
1371
1383
  assert "<entml:thinking>" not in clean
1372
- assert "StreamAiPreviews" not in clean
1384
+ assert "StreamAiPreviews" in parser.partial_thinking
1373
1385
  assert len(parser.partial_thinking) > 500
1374
1386
  assert "260727" in json.loads(calls[0]["function"]["arguments"])["command"]
1375
1387
 
@@ -1464,6 +1476,89 @@ def test_entml_tool_block_mangled_brace_entml_params() -> None:
1464
1476
  assert "RC4" in stream_clean or "RC4" in parser.partial_text, f"chunk={chunk}"
1465
1477
 
1466
1478
 
1479
+ def test_entml_tool_block_brace_read_entml_params() -> None:
1480
+ """``<tool>{Read}\\n<entml:parameter>...`` + fault thinking(req-1785310901)。"""
1481
+ from pathlib import Path
1482
+
1483
+ from echotools.exec.fncall.parsers.stream import FncallStreamParser
1484
+
1485
+ sample_path = Path(
1486
+ r"X:/Project/Public/Qwen/logs/responses/req-1785310901-34c502c9e8a9.txt"
1487
+ )
1488
+ if not sample_path.is_file():
1489
+ pytest.skip("corpus file not available")
1490
+ sample = sample_path.read_text(encoding="utf-8")
1491
+ tools = [
1492
+ {
1493
+ "type": "function",
1494
+ "function": {
1495
+ "name": "Read",
1496
+ "parameters": {
1497
+ "type": "object",
1498
+ "properties": {
1499
+ "file_path": {"type": "string"},
1500
+ "offset": {"type": "integer"},
1501
+ "limit": {"type": "integer"},
1502
+ },
1503
+ "required": ["file_path"],
1504
+ },
1505
+ },
1506
+ }
1507
+ ]
1508
+ clean, batch_calls = get_protocol("entml").parse(sample, tools)
1509
+ assert len(batch_calls) == 1
1510
+ batch_args = json.loads(batch_calls[0]["function"]["arguments"])
1511
+ assert batch_calls[0]["function"]["name"] == "Read"
1512
+ assert batch_args["offset"] == 0
1513
+ assert batch_args["limit"] == 200
1514
+ assert "b01pggtpe.txt" in batch_args["file_path"]
1515
+ assert "流发送逻辑" in clean
1516
+ assert "entml:" not in clean
1517
+
1518
+ for chunk in (1, 17, 64):
1519
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=tools)
1520
+ for i in range(0, len(sample), chunk):
1521
+ parser.feed(sample[i : i + chunk])
1522
+ stream_clean, stream_calls = parser.finalize()
1523
+ assert len(stream_calls) == 1, f"chunk={chunk}"
1524
+ stream_args = json.loads(stream_calls[0]["function"]["arguments"])
1525
+ assert stream_args == batch_args, f"chunk={chunk}"
1526
+ assert "流发送逻辑" in stream_clean, f"chunk={chunk}"
1527
+ assert "流发送逻辑" not in parser.partial_thinking, f"chunk={chunk}"
1528
+ assert len(parser.partial_thinking) > 500, f"chunk={chunk}"
1529
+
1530
+
1531
+ def test_entml_tool_block_mangled_brace_entml_params_brace_close() -> None:
1532
+ """``{Read}``(非 ``{Read>``)后接 entml parameter 须解析。"""
1533
+ from echotools.exec.fncall.protocols.entml_tool_blocks import parse_tool_block_body
1534
+ from echotools.exec.fncall.shared.coercion import _build_param_schema_index
1535
+
1536
+ tools = [
1537
+ {
1538
+ "type": "function",
1539
+ "function": {
1540
+ "name": "Read",
1541
+ "parameters": {
1542
+ "type": "object",
1543
+ "properties": {
1544
+ "file_path": {"type": "string"},
1545
+ "offset": {"type": "integer"},
1546
+ },
1547
+ "required": ["file_path"],
1548
+ },
1549
+ },
1550
+ }
1551
+ ]
1552
+ body = (
1553
+ "{Read}\n"
1554
+ '<entml:parameter name="file_path">src/a.py</entml:parameter>\n'
1555
+ '<entml:parameter name="offset">10</entml:parameter>'
1556
+ )
1557
+ schema = _build_param_schema_index(tools)
1558
+ parsed = parse_tool_block_body(body, tools=tools, schema_index=schema)
1559
+ assert parsed == [("Read", {"file_path": "src/a.py", "offset": 10})]
1560
+
1561
+
1467
1562
  def test_prose_entml_invoke_mention_does_not_swallow_real_invoke() -> None:
1468
1563
  """正文提及 ``<entml:invoke>``(无 name)不得吞掉后续真实工具块(req-1785299710)。"""
1469
1564
  from pathlib import Path