echotools 2.3.82__tar.gz → 2.3.83__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 (240) hide show
  1. {echotools-2.3.82 → echotools-2.3.83}/PKG-INFO +1 -1
  2. {echotools-2.3.82 → echotools-2.3.83}/pyproject.toml +1 -1
  3. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/parsers/stream.py +12 -7
  4. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_invoke.py +17 -0
  5. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_patterns.py +87 -1
  6. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_stream_json.py +18 -21
  7. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_think/parse.py +21 -2
  8. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_tool_blocks.py +37 -0
  9. {echotools-2.3.82 → echotools-2.3.83}/src/echotools.egg-info/PKG-INFO +1 -1
  10. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_protocol.py +49 -0
  11. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fncall_stream_thinking.py +53 -0
  12. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_stream_partial_json_parity.py +51 -0
  13. {echotools-2.3.82 → echotools-2.3.83}/README.md +0 -0
  14. {echotools-2.3.82 → echotools-2.3.83}/docs/README.md +0 -0
  15. {echotools-2.3.82 → echotools-2.3.83}/setup.cfg +0 -0
  16. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/__init__.py +0 -0
  17. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/cache/__init__.py +0 -0
  18. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/cache/list_cache.py +0 -0
  19. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/cache/memory_cache.py +0 -0
  20. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/__init__.py +0 -0
  21. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/base.py +0 -0
  22. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/center.py +0 -0
  23. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/center_notify.py +0 -0
  24. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/center_template.py +0 -0
  25. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/loader.py +0 -0
  26. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/merge.py +0 -0
  27. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/errors/__init__.py +0 -0
  28. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/errors/base.py +0 -0
  29. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/errors/classify.py +0 -0
  30. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/errors/common.py +0 -0
  31. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/errors/http.py +0 -0
  32. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/ids/__init__.py +0 -0
  33. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/ids/generator.py +0 -0
  34. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/io/__init__.py +0 -0
  35. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/io/io_utils.py +0 -0
  36. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/io/printstream.py +0 -0
  37. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/logger/__init__.py +0 -0
  38. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/logger/manager.py +0 -0
  39. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/retry/__init__.py +0 -0
  40. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/retry/retry.py +0 -0
  41. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/compat.py +0 -0
  42. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/__init__.py +0 -0
  43. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/candidate.py +0 -0
  44. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  45. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  46. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/race.py +0 -0
  47. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/selector.py +0 -0
  48. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/usage.py +0 -0
  49. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/__init__.py +0 -0
  50. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  51. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  52. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  53. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  54. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/prompt/history.py +0 -0
  55. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  56. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  57. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  58. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  59. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  60. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml.py +0 -0
  61. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  62. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
  63. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  64. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  65. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  66. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/registry.py +0 -0
  67. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  68. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  69. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/history_markup.py +0 -0
  70. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  71. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  72. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  73. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  74. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/keys.py +0 -0
  75. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/lifecycle/__init__.py +0 -0
  76. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/lifecycle/manager.py +0 -0
  77. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/lifecycle/updater.py +0 -0
  78. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  79. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/process/__init__.py +0 -0
  80. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/process/port.py +0 -0
  81. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/protocol/__init__.py +0 -0
  82. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/protocol/base.py +0 -0
  83. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/__init__.py +0 -0
  84. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/conpty.py +0 -0
  85. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/local/__init__.py +0 -0
  86. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/local/core.py +0 -0
  87. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/local/proc.py +0 -0
  88. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/local/unix.py +0 -0
  89. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/local/win.py +0 -0
  90. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/sanitize.py +0 -0
  91. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/session/__init__.py +0 -0
  92. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/session/base.py +0 -0
  93. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/session/client.py +0 -0
  94. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/ssh.py +0 -0
  95. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/tmux.py +0 -0
  96. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/__init__.py +0 -0
  97. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/charmap.py +0 -0
  98. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/spinner.py +0 -0
  99. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/ui.py +0 -0
  100. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  101. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  102. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  103. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  104. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  105. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  106. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/ui_console.py +0 -0
  107. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/ui_io.py +0 -0
  108. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/ui_log.py +0 -0
  109. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  110. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/ui_text.py +0 -0
  111. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/ui_types.py +0 -0
  112. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  113. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  114. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  115. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  116. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  117. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  118. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  119. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  120. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  121. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  122. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  123. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/events/__init__.py +0 -0
  124. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/events/bus.py +0 -0
  125. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/events/event.py +0 -0
  126. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/spinner/__init__.py +0 -0
  127. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/spinner/spinner.py +0 -0
  128. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/spinner/spinner_color.py +0 -0
  129. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/tracing/__init__.py +0 -0
  130. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/tracing/context.py +0 -0
  131. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/tracing/span.py +0 -0
  132. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/tracing/tracer.py +0 -0
  133. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/translate.py +0 -0
  134. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/watcher/__init__.py +0 -0
  135. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/watcher/file_watcher.py +0 -0
  136. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/__init__.py +0 -0
  137. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/application.py +0 -0
  138. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/broker.py +0 -0
  139. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/app.js +0 -0
  140. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  141. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  142. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  143. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  144. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  145. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  146. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  147. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  148. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  149. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  150. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/inputbox.css +0 -0
  151. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/inputbox.js +0 -0
  152. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/style.css +0 -0
  153. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/voicegif.js +0 -0
  154. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/middleware.py +0 -0
  155. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/stats.py +0 -0
  156. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/utils.py +0 -0
  157. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/files/__init__.py +0 -0
  158. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/files/file_util.py +0 -0
  159. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/network/__init__.py +0 -0
  160. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/network/http_utils.py +0 -0
  161. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/plugin/__init__.py +0 -0
  162. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/plugin/base.py +0 -0
  163. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/plugin/discovery.py +0 -0
  164. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/plugin/registry.py +0 -0
  165. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/plugin/registry_ops.py +0 -0
  166. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/proxy/__init__.py +0 -0
  167. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/proxy/manager.py +0 -0
  168. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/runtime/__init__.py +0 -0
  169. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/runtime/collector.py +0 -0
  170. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/scheduler/__init__.py +0 -0
  171. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/scheduler/scheduler.py +0 -0
  172. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/sdk/__init__.py +0 -0
  173. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/sdk/facade.py +0 -0
  174. {echotools-2.3.82 → echotools-2.3.83}/src/echotools/version.py +0 -0
  175. {echotools-2.3.82 → echotools-2.3.83}/src/echotools.egg-info/SOURCES.txt +0 -0
  176. {echotools-2.3.82 → echotools-2.3.83}/src/echotools.egg-info/dependency_links.txt +0 -0
  177. {echotools-2.3.82 → echotools-2.3.83}/src/echotools.egg-info/requires.txt +0 -0
  178. {echotools-2.3.82 → echotools-2.3.83}/src/echotools.egg-info/top_level.txt +0 -0
  179. {echotools-2.3.82 → echotools-2.3.83}/src/tests/_check_parity.py +0 -0
  180. {echotools-2.3.82 → echotools-2.3.83}/src/tests/bench_import.py +0 -0
  181. {echotools-2.3.82 → echotools-2.3.83}/src/tests/conftest.py +0 -0
  182. {echotools-2.3.82 → echotools-2.3.83}/src/tests/fixtures/__init__.py +0 -0
  183. {echotools-2.3.82 → echotools-2.3.83}/src/tests/fixtures/simulated_fake_history_markup_responses.py +0 -0
  184. {echotools-2.3.82 → echotools-2.3.83}/src/tests/fixtures/simulated_fault_thinking_responses.py +0 -0
  185. {echotools-2.3.82 → echotools-2.3.83}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
  186. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_adversarial_stream_splits.py +0 -0
  187. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_cache_retry.py +0 -0
  188. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_completion.py +0 -0
  189. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_config.py +0 -0
  190. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_config_extended.py +0 -0
  191. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_console.py +0 -0
  192. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_coverage_core.py +0 -0
  193. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_dispatch.py +0 -0
  194. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_dispatch_extended.py +0 -0
  195. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_dispatch_race.py +0 -0
  196. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_agent_output_formats.py +0 -0
  197. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_bare_invoke_format.py +0 -0
  198. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_batch_stream_comprehensive.py +0 -0
  199. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_parser_comprehensive.py +0 -0
  200. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_stream_json.py +0 -0
  201. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
  202. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_thinking_history.py +0 -0
  203. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_thinking_parse.py +0 -0
  204. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_tool_parse_leak.py +0 -0
  205. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_tool_pipeline.py +0 -0
  206. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_tools.py +0 -0
  207. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_errors.py +0 -0
  208. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_errors_common.py +0 -0
  209. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_events.py +0 -0
  210. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_events_event.py +0 -0
  211. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_facade.py +0 -0
  212. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fake_history_markup_batch.py +0 -0
  213. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
  214. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
  215. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fake_history_markup_stream.py +0 -0
  216. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fault_thinking_close_batch.py +0 -0
  217. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fault_thinking_close_stream.py +0 -0
  218. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fncall.py +0 -0
  219. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_history_markup_filter.py +0 -0
  220. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_ids.py +0 -0
  221. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_imports.py +0 -0
  222. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_io.py +0 -0
  223. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_keys.py +0 -0
  224. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_lifecycle.py +0 -0
  225. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_logger.py +0 -0
  226. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_merge.py +0 -0
  227. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_model_branch_matrix.py +0 -0
  228. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_plugin.py +0 -0
  229. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_printstream.py +0 -0
  230. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_protocols.py +0 -0
  231. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_proxy.py +0 -0
  232. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_proxy_selector.py +0 -0
  233. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_runtime.py +0 -0
  234. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_scheduler.py +0 -0
  235. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  236. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_stream_realtime_parse.py +0 -0
  237. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_tracing.py +0 -0
  238. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_usage.py +0 -0
  239. {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_web.py +0 -0
  240. {echotools-2.3.82 → echotools-2.3.83}/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.82
3
+ Version: 2.3.83
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.82"
7
+ version = "2.3.83"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -439,7 +439,7 @@ class FncallStreamParser:
439
439
  else:
440
440
  tail = ""
441
441
  else:
442
- tail = final if multi else ""
442
+ tail = ""
443
443
  if tail:
444
444
  name = call["function"]["name"]
445
445
  if multi:
@@ -635,10 +635,16 @@ class FncallStreamParser:
635
635
  stream_safe_visible_prefix,
636
636
  )
637
637
 
638
+ from echotools.exec.fncall.protocols.entml_think.parse import (
639
+ split_entml_thinking,
640
+ )
641
+
638
642
  buf = stream_safe_visible_prefix(
639
643
  self._raw_buf,
640
644
  thinking_enabled=self._thinking_enabled,
641
645
  )
646
+ if self._thinking_enabled and buf:
647
+ buf, _ = split_entml_thinking(buf, thinking_enabled=True)
642
648
  return clean_stream_partial_visible(
643
649
  buf,
644
650
  has_calls=self.has_calls,
@@ -690,11 +696,6 @@ class FncallStreamParser:
690
696
  segment,
691
697
  thinking_enabled=self._thinking_enabled,
692
698
  )
693
- buf = clean_stream_partial_visible(
694
- buf,
695
- has_calls=self.has_calls,
696
- thinking_enabled=self._thinking_enabled,
697
- )
698
699
  if not buf:
699
700
  return ""
700
701
  cleaned, _ = strip_fake_history_markup_for_display(buf)
@@ -703,7 +704,11 @@ class FncallStreamParser:
703
704
  thinking_enabled=self._thinking_enabled,
704
705
  preserve_visible_whitespace=True,
705
706
  )
706
- return visible
707
+ return clean_stream_partial_visible(
708
+ visible,
709
+ has_calls=self.has_calls,
710
+ thinking_enabled=self._thinking_enabled,
711
+ )
707
712
 
708
713
  def finalize(self) -> Tuple[str, List[Dict[str, Any]]]:
709
714
  """结束流式解析,返回 (清理后文本, tool_calls 列表)。幂等。"""
@@ -15,6 +15,8 @@ from .entml_patterns import (
15
15
  extract_parameter_type_attr,
16
16
  normalize_entml_name,
17
17
  parse_sub_tags,
18
+ split_mangled_json_param_tail,
19
+ synthetic_close_invoke_body,
18
20
  )
19
21
  from .entml_values import coerce_entml_arguments, coerce_entml_parameter_value
20
22
 
@@ -58,6 +60,9 @@ def parse_invoke_args(
58
60
  schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]],
59
61
  ) -> Dict[str, Any]:
60
62
  func_props = (schema_index or {}).get(name) or {}
63
+ if "</entml:invoke>" in body:
64
+ body = body[: body.index("</entml:invoke>")]
65
+ body = synthetic_close_invoke_body(body)
61
66
 
62
67
  params_m = PARAMETERS_RE.search(body)
63
68
  if params_m:
@@ -81,6 +86,7 @@ def parse_invoke_args(
81
86
  continue
82
87
  pname = normalize_entml_name(pname)
83
88
  pval = (param_m.group(2) or "").strip()
89
+ pval, extra = split_mangled_json_param_tail(pval)
84
90
  type_hint = extract_parameter_type_attr(attrs)
85
91
  pschema = func_props.get(pname) or {}
86
92
  args[pname] = coerce_entml_parameter_value(
@@ -88,6 +94,17 @@ def parse_invoke_args(
88
94
  pschema or None,
89
95
  type_hint=type_hint,
90
96
  )
97
+ for extra_key, extra_val in extra.items():
98
+ if extra_key in args:
99
+ continue
100
+ extra_schema = func_props.get(extra_key) or {}
101
+ if isinstance(extra_val, (int, float, bool)):
102
+ args[extra_key] = extra_val
103
+ else:
104
+ args[extra_key] = coerce_entml_parameter_value(
105
+ str(extra_val),
106
+ extra_schema or None,
107
+ )
91
108
 
92
109
  _parse_bare_invoke_children(body, args, func_props)
93
110
  _parse_direct_child_tags(body, args, func_props)
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- from typing import Any, Dict, Optional
4
+ from typing import Any, Dict, Optional, Tuple
5
5
 
6
6
  from .entml_values import coerce_entml_parameter_value
7
7
 
@@ -195,6 +195,92 @@ def _parameter_close_follower_ok(after: str, *, allow_end: bool) -> bool:
195
195
  return False
196
196
 
197
197
 
198
+ _PARAM_OPEN_TAG_RE = re.compile(rf"{PARAM_OPEN_PATTERN}([^>]*)>", re.IGNORECASE)
199
+ _MANGLED_PARAM_JSON_TAIL_RE = re.compile(
200
+ r'"\s*,\s*"description"\s*:\s*"((?:[^"\\]|\\.)*)"\s*,\s*"timeout"\s*:\s*(\d+)\s*\}\}?\s*$',
201
+ re.DOTALL,
202
+ )
203
+ _MANGLED_PARAM_JSON_TAIL_START_RE = re.compile(
204
+ r'"\s*,\s*"description"\s*:\s*"',
205
+ re.DOTALL,
206
+ )
207
+ _MANGLED_PARAM_JSON_TAIL_EARLY_RE = re.compile(
208
+ r'"\s*,\s*"(?:description|timeout)\b',
209
+ re.DOTALL,
210
+ )
211
+ _MANGLED_PARAM_JSON_TAIL_IN_PROGRESS_RE = re.compile(
212
+ r'"\s*,\s*"(?:d|t)',
213
+ re.DOTALL,
214
+ )
215
+
216
+
217
+ _MANGLED_PARAM_JSON_COMMA_QUOTE_END_RE = re.compile(
218
+ r'"\s*,\s*"$',
219
+ re.DOTALL,
220
+ )
221
+ _MANGLED_PARAM_JSON_COMMA_AFTER_QUOTE_RE = re.compile(
222
+ r'"\s*,\s*$',
223
+ re.DOTALL,
224
+ )
225
+
226
+
227
+ def mangled_json_param_tail_in_progress(value: str) -> bool:
228
+ """parameter 值中出现误写入 JSON 尾缀但尚未收齐时不应继续增长 partial_json。"""
229
+ return bool(value and _MANGLED_PARAM_JSON_TAIL_IN_PROGRESS_RE.search(value))
230
+
231
+
232
+ def split_mangled_json_param_tail(value: str) -> Tuple[str, Dict[str, Any]]:
233
+ """模型把 ``", "description": ..., "timeout": ...}}`` 误写入 parameter 值时的拆分。"""
234
+ if not value:
235
+ return value, {}
236
+ match = _MANGLED_PARAM_JSON_TAIL_RE.search(value)
237
+ if match:
238
+ command = value[: match.start() + 1]
239
+ extra: Dict[str, Any] = {
240
+ "description": match.group(1),
241
+ "timeout": int(match.group(2)),
242
+ }
243
+ return command, extra
244
+ partial = _MANGLED_PARAM_JSON_TAIL_START_RE.search(value)
245
+ if partial:
246
+ return value[: partial.start() + 1], {}
247
+ early = _MANGLED_PARAM_JSON_TAIL_EARLY_RE.search(value)
248
+ if early:
249
+ return value[: early.start() + 1], {}
250
+ if _MANGLED_PARAM_JSON_TAIL_IN_PROGRESS_RE.search(value):
251
+ matches = list(re.finditer(r'"\s*,\s*"', value))
252
+ if matches:
253
+ return value[: matches[-1].start() + 1], {}
254
+ comma_quote_end = _MANGLED_PARAM_JSON_COMMA_QUOTE_END_RE.search(value)
255
+ if comma_quote_end:
256
+ return value[: comma_quote_end.start() + 1], {}
257
+ comma_after_quote = _MANGLED_PARAM_JSON_COMMA_AFTER_QUOTE_RE.search(value)
258
+ if comma_after_quote:
259
+ return value[: comma_after_quote.start() + 1], {}
260
+ return value, {}
261
+
262
+
263
+ def synthetic_close_invoke_body(inner: str) -> str:
264
+ """为 force_close / invoke 已闭合但未闭合的 parameter 补齐结构闭合标签。"""
265
+ if not inner:
266
+ return inner
267
+ closed = inner
268
+ if "<entml:parameters>" in closed and "</entml:parameters>" not in closed:
269
+ closed = closed + "</entml:parameters>"
270
+ matches = list(_PARAM_OPEN_TAG_RE.finditer(closed))
271
+ if matches:
272
+ last = matches[-1]
273
+ after = closed[last.end() :]
274
+ open_snip = closed[last.start() : last.end()].lower()
275
+ if open_snip.startswith("<parameter") and not open_snip.startswith("<entml:"):
276
+ need = PARAM_CLOSE_BARE
277
+ else:
278
+ need = PARAM_CLOSE_ENTML
279
+ if need not in after and find_valid_parameter_close(closed, last.end(), allow_end=True) < 0:
280
+ closed = closed + need
281
+ return closed
282
+
283
+
198
284
  def find_valid_parameter_close(body: str, search_from: int = 0, *, allow_end: bool = True) -> int:
199
285
  """返回 parameter 闭合标签在 ``body`` 中的起始下标;忽略参数值内的假闭合。"""
200
286
  pos = search_from
@@ -18,6 +18,8 @@ from .entml_patterns import (
18
18
  find_valid_parameter_close,
19
19
  normalize_entml_name,
20
20
  parameter_close_at,
21
+ split_mangled_json_param_tail,
22
+ synthetic_close_invoke_body,
21
23
  )
22
24
  from .entml_values import coerce_entml_parameter_value
23
25
  from echotools.exec.fncall.shared.coercion import _resolve_effective_type
@@ -104,24 +106,8 @@ def _effective_param_type(
104
106
 
105
107
 
106
108
  def _synthetic_close_body(inner: str) -> str:
107
- """为 force_close 补齐未闭合的 parameter / parameters 标签。"""
108
- if _INVOKE_CLOSE in inner:
109
- inner = inner[: inner.index(_INVOKE_CLOSE)]
110
- closed = inner
111
- if _PARAMETERS_OPEN in closed and _PARAMETERS_CLOSE not in closed:
112
- closed = closed + _PARAMETERS_CLOSE
113
- matches = list(_PARAM_OPEN_RE.finditer(closed))
114
- if matches:
115
- last = matches[-1]
116
- after = closed[last.end() :]
117
- open_snip = closed[last.start() : last.end()].lower()
118
- if open_snip.startswith("<parameter") and not open_snip.startswith("<entml:"):
119
- need = PARAM_CLOSE_BARE
120
- else:
121
- need = _PARAM_CLOSE
122
- if need not in after and find_valid_parameter_close(closed, last.end(), allow_end=True) < 0:
123
- closed = closed + need
124
- return closed
109
+ """兼容旧名;委托 ``synthetic_close_invoke_body``。"""
110
+ return synthetic_close_invoke_body(inner)
125
111
 
126
112
 
127
113
  def _final_invoke_arguments_json(
@@ -136,7 +122,7 @@ def _final_invoke_arguments_json(
136
122
 
137
123
  invoke_closed = _INVOKE_CLOSE in body
138
124
  inner = body[: body.index(_INVOKE_CLOSE)] if invoke_closed else body
139
- parse_body = inner if invoke_closed else _synthetic_close_body(inner)
125
+ parse_body = synthetic_close_invoke_body(inner)
140
126
  args = parse_invoke_args(parse_body, tool_name, schema_index)
141
127
  return json.dumps(args, ensure_ascii=False)
142
128
 
@@ -312,16 +298,24 @@ def _parse_parameter_entries(body: str) -> List[Tuple[str, str, bool, Optional[s
312
298
  val_start = match.end()
313
299
  close = _resolve_parameter_close(body, val_start)
314
300
  if close < 0:
301
+ raw = _incomplete_parameter_raw(body, val_start)
302
+ raw, extra = split_mangled_json_param_tail(raw)
315
303
  entries.append(
316
304
  (
317
305
  key,
318
- _incomplete_parameter_raw(body, val_start),
306
+ raw,
319
307
  False,
320
308
  type_hint,
321
309
  )
322
310
  )
311
+ for extra_key, extra_val in extra.items():
312
+ entries.append((extra_key, str(extra_val), True, None))
323
313
  break
324
- entries.append((key, body[val_start:close].strip(), True, type_hint))
314
+ raw = body[val_start:close].strip()
315
+ raw, extra = split_mangled_json_param_tail(raw)
316
+ entries.append((key, raw, True, type_hint))
317
+ for extra_key, extra_val in extra.items():
318
+ entries.append((extra_key, str(extra_val), True, None))
325
319
  i = close + parameter_close_at(body, close)
326
320
  return entries
327
321
 
@@ -467,6 +461,9 @@ class EntmlInvokeJsonStreamEncoder:
467
461
  if a != b:
468
462
  break
469
463
  common += 1
464
+ if common < len(self._emitted):
465
+ self._emitted = snapshot
466
+ return ""
470
467
  delta = snapshot[common:]
471
468
  self._emitted = snapshot
472
469
  return delta
@@ -79,6 +79,10 @@ def stream_safe_visible_prefix(
79
79
  )
80
80
  if open_at >= 0:
81
81
  return buf[:open_at]
82
+ if thinking_enabled:
83
+ visible, thinking = split_entml_thinking(buf, thinking_enabled=True)
84
+ if thinking:
85
+ return visible
82
86
  return buf
83
87
 
84
88
 
@@ -124,8 +128,23 @@ def clean_stream_partial_visible(
124
128
  if not text.strip():
125
129
  return ""
126
130
  if re.search(r"</?entml:", text, re.IGNORECASE):
127
- text = re.sub(r"</?entml:[^>]*>", "", text, flags=re.IGNORECASE)
128
- text = re.sub(r"</?entml:\w*$", "", text, flags=re.IGNORECASE)
131
+ if thinking_enabled:
132
+ # 保留 thinking 标签供 split_entml_thinking 剥离;仅去掉工具相关 entml 残留。
133
+ text = re.sub(
134
+ r"</?entml:(?!thinking\b)[^>]*>",
135
+ "",
136
+ text,
137
+ flags=re.IGNORECASE,
138
+ )
139
+ text = re.sub(
140
+ r"</?entml:(?!thinking\b)\w*$",
141
+ "",
142
+ text,
143
+ flags=re.IGNORECASE,
144
+ )
145
+ else:
146
+ text = re.sub(r"</?entml:[^>]*>", "", text, flags=re.IGNORECASE)
147
+ text = re.sub(r"</?entml:\w*$", "", text, flags=re.IGNORECASE)
129
148
  text = text.rstrip()
130
149
  return text
131
150
 
@@ -24,6 +24,15 @@ _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)
28
+ _MANGLED_BRACE_ENTML_HEAD_RE = re.compile(
29
+ r"^\s*\{([A-Za-z][A-Za-z0-9_]*)\s*>\s*",
30
+ re.MULTILINE,
31
+ )
32
+ _PARAM_MARKER_RE = re.compile(
33
+ r"<\s*(?:entml:)?parameter\b",
34
+ re.IGNORECASE,
35
+ )
27
36
  # 工具结果常见行号泄漏(Read 伪块)— 仅用于多块 brace 判定
28
37
  _OUTPUT_LINE_RE = re.compile(r"^\s*\d+\s+\S", re.MULTILINE)
29
38
  _SCALAR_ARG_KEYS = ("path", "command", "pattern", "query", "file_path")
@@ -220,6 +229,30 @@ def _parse_brace_calls(
220
229
  return [(name, args)]
221
230
 
222
231
 
232
+ def _parse_mangled_brace_entml_params(
233
+ body: str,
234
+ known: Set[str],
235
+ schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]],
236
+ ) -> List[Tuple[str, Dict[str, Any]]]:
237
+ """``<tool>\\n{Bash>\\n<entml:parameter>...`` 混合格式。"""
238
+ match = _MANGLED_BRACE_ENTML_HEAD_RE.search(body)
239
+ if not match:
240
+ return []
241
+ name = normalize_entml_name(match.group(1))
242
+ if known and name not in known:
243
+ return []
244
+ rest = body[match.end() :].lstrip()
245
+ if not _PARAM_MARKER_RE.search(rest):
246
+ return []
247
+ from .entml_invoke import parse_invoke_args
248
+
249
+ args = parse_invoke_args(rest, name, schema_index)
250
+ if not args:
251
+ return []
252
+ args = coerce_entml_arguments(args, name, schema_index)
253
+ return [(name, args)]
254
+
255
+
223
256
  def parse_tool_block_body(
224
257
  body: str,
225
258
  *,
@@ -237,6 +270,10 @@ def parse_tool_block_body(
237
270
  if inner:
238
271
  return inner
239
272
 
273
+ mangled = _parse_mangled_brace_entml_params(text, known, schema_index)
274
+ if mangled:
275
+ return mangled
276
+
240
277
  if not allow_brace_format:
241
278
  return []
242
279
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.82
3
+ Version: 2.3.83
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -1414,3 +1414,52 @@ def test_entml_tool_block_bash_with_output_tail_batch_and_finalize() -> None:
1414
1414
  stream_args = json.loads(stream_calls[0]["function"]["arguments"])
1415
1415
  assert stream_args == batch_args
1416
1416
 
1417
+
1418
+ def test_entml_tool_block_mangled_brace_entml_params() -> None:
1419
+ """``<tool>{Bash>\\n<entml:parameter>...`` 混合格式 batch/stream 均解析 Bash。"""
1420
+ from pathlib import Path
1421
+
1422
+ from echotools.exec.fncall.parsers.stream import FncallStreamParser
1423
+
1424
+ sample_path = Path(
1425
+ r"X:/Project/Public/Qwen/logs/responses/req-1785297403-7c656ac5fe40.txt"
1426
+ )
1427
+ if not sample_path.is_file():
1428
+ pytest.skip("corpus file not available")
1429
+ sample = sample_path.read_text(encoding="utf-8")
1430
+ tools = [
1431
+ {
1432
+ "type": "function",
1433
+ "function": {
1434
+ "name": "Bash",
1435
+ "parameters": {
1436
+ "type": "object",
1437
+ "properties": {
1438
+ "command": {"type": "string"},
1439
+ "description": {"type": "string"},
1440
+ "timeout": {"type": "integer"},
1441
+ },
1442
+ "required": ["command"],
1443
+ },
1444
+ },
1445
+ }
1446
+ ]
1447
+ clean, batch_calls = get_protocol("entml").parse(sample, tools)
1448
+ assert len(batch_calls) == 1
1449
+ batch_args = json.loads(batch_calls[0]["function"]["arguments"])
1450
+ assert batch_calls[0]["function"]["name"] == "Bash"
1451
+ assert "alicdn-analytics" in batch_args["command"]
1452
+ assert batch_args.get("timeout") == 30
1453
+ assert "RC4" in clean
1454
+
1455
+ for chunk in (1, 17, 64):
1456
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=tools)
1457
+ for i in range(0, len(sample), chunk):
1458
+ parser.feed(sample[i : i + chunk])
1459
+ stream_clean, stream_calls = parser.finalize()
1460
+ assert len(stream_calls) == 1, f"chunk={chunk}"
1461
+ stream_args = json.loads(stream_calls[0]["function"]["arguments"])
1462
+ assert stream_args == batch_args, f"chunk={chunk}"
1463
+ assert len(parser.partial_thinking) > 100, f"chunk={chunk}"
1464
+ assert "RC4" in stream_clean or "RC4" in parser.partial_text, f"chunk={chunk}"
1465
+
@@ -90,6 +90,59 @@ def test_thinking_close_and_invoke_in_one_chunk() -> None:
90
90
  assert "reason" not in clean or clean.strip() == ""
91
91
 
92
92
 
93
+ def test_thinking_close_does_not_leak_into_partial_text() -> None:
94
+ """thinking 闭合后 partial_text 不得重复输出思考正文(Rogator text/reasoning 双发)。"""
95
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
96
+ thinking_body = (
97
+ "The concurrent test shows SSE received 0 bytes even though BidiAppend returned 200.\n"
98
+ "Default reply language: Simplified Chinese\n"
99
+ )
100
+ answer = "并发测试显示SSE线程收到状态200但0字节数据。"
101
+ text = (
102
+ f"<entml:thinking>\n{thinking_body}</entml:thinking>\n\n{answer}\n{INVOKE}"
103
+ )
104
+ dup_count = 0
105
+ for i in range(0, len(text), 17):
106
+ parser.feed(text[i : i + 17])
107
+ pt = parser.partial_text
108
+ pth = parser.partial_thinking
109
+ if pth and thinking_body[:40] in pt:
110
+ dup_count += 1
111
+ clean, calls = parser.finalize()
112
+ assert dup_count == 0, "thinking body leaked into partial_text during stream"
113
+ assert thinking_body.strip() in parser.partial_thinking
114
+ assert "The concurrent test shows" not in clean
115
+ assert answer in clean
116
+ assert len(calls) == 1
117
+
118
+
119
+ @pytest.mark.parametrize("chunk", [1, 17, 64])
120
+ def test_thinking_then_answer_no_partial_text_dup(chunk: int) -> None:
121
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
122
+ text = (
123
+ "<entml:thinking>\nlong reasoning line one\nline two\n</entml:thinking>\n"
124
+ "visible answer\n"
125
+ f"{INVOKE}"
126
+ )
127
+ for i in range(0, len(text), chunk):
128
+ parser.feed(text[i : i + chunk])
129
+ assert "long reasoning" not in parser.partial_text
130
+ clean, calls = parser.finalize()
131
+ assert "long reasoning" in parser.partial_thinking
132
+ assert "visible answer" in clean
133
+ assert "long reasoning" not in clean
134
+
135
+
136
+ parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
137
+ chunks = ["<entml:thinking>reason</entml:thinking>", INVOKE]
138
+ for chunk in chunks:
139
+ parser.feed(chunk)
140
+ clean, calls = parser.finalize()
141
+ assert parser.partial_thinking.strip() == "reason"
142
+ assert len(calls) == 1
143
+ assert "reason" not in clean or clean.strip() == ""
144
+
145
+
93
146
  def test_no_thinking_invoke_still_works() -> None:
94
147
  parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
95
148
  parser.feed(f"Sure.\n{INVOKE}")
@@ -139,6 +139,57 @@ def test_read_anyof_integer_stream_json_buf_matches_batch() -> None:
139
139
  }
140
140
 
141
141
 
142
+ BASH_TOOLS = [
143
+ {
144
+ "type": "function",
145
+ "function": {
146
+ "name": "Bash",
147
+ "parameters": {
148
+ "type": "object",
149
+ "properties": {
150
+ "command": {"type": "string"},
151
+ "description": {"type": "string"},
152
+ "timeout": {"type": "integer"},
153
+ },
154
+ "required": ["command"],
155
+ },
156
+ },
157
+ }
158
+ ]
159
+
160
+
161
+ def test_mangled_json_tail_in_command_param_batch_and_stream() -> None:
162
+ """未闭合 parameter + JSON 尾缀误写入 command:batch/stream json_buf 须可解析且一致。"""
163
+ from pathlib import Path
164
+
165
+ text = Path(
166
+ r"X:/Project/Public/Qwen/logs/responses/req-1785296513-7782eb161d4d.txt"
167
+ ).read_text(encoding="utf-8")
168
+ proto = get_protocol("entml")
169
+ batch_args = json.loads(proto.parse(text, BASH_TOOLS)[1][0]["function"]["arguments"])
170
+ assert "command" in batch_args
171
+ assert batch_args["command"].endswith("head -60\"")
172
+ assert "description" not in batch_args["command"]
173
+ assert batch_args.get("description")
174
+ assert batch_args.get("timeout") == 30000
175
+
176
+ for chunk in (1, 17, 64):
177
+ parser = FncallStreamParser(protocol=proto, tools=BASH_TOOLS)
178
+ json_buf = ""
179
+ for i in range(0, len(text), chunk):
180
+ parser.feed(text[i : i + chunk])
181
+ while True:
182
+ delta = parser.consume_stream_delta()
183
+ if not delta:
184
+ break
185
+ json_buf += delta[1]
186
+ comp = parser.complete_stream_delta_if_needed()
187
+ if comp:
188
+ json_buf += comp[1]
189
+ parser.finalize()
190
+ assert json.loads(json_buf) == batch_args, f"chunk={chunk}"
191
+
192
+
142
193
  def test_parameters_block_no_stream_until_closed() -> None:
143
194
  body = (
144
195
  "<entml:parameters>\n"
File without changes
File without changes
File without changes