echotools 2.3.64__tar.gz → 2.3.66__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 (232) hide show
  1. {echotools-2.3.64 → echotools-2.3.66}/PKG-INFO +1 -1
  2. {echotools-2.3.64 → echotools-2.3.66}/pyproject.toml +1 -1
  3. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/parsers/stream.py +3 -0
  4. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/prompt/inject.py +9 -0
  5. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml.py +22 -8
  6. echotools-2.3.66/src/echotools/exec/fncall/shared/history_markup.py +135 -0
  7. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/protocol/base.py +1 -0
  8. {echotools-2.3.64 → echotools-2.3.66}/src/echotools.egg-info/PKG-INFO +1 -1
  9. {echotools-2.3.64 → echotools-2.3.66}/src/echotools.egg-info/SOURCES.txt +5 -0
  10. echotools-2.3.66/src/tests/fixtures/simulated_fault_thinking_responses.py +220 -0
  11. {echotools-2.3.64 → echotools-2.3.66}/src/tests/fixtures/simulated_llm_tool_responses.py +1 -1
  12. echotools-2.3.66/src/tests/test_fault_thinking_close_batch.py +86 -0
  13. echotools-2.3.66/src/tests/test_fault_thinking_close_stream.py +149 -0
  14. echotools-2.3.66/src/tests/test_history_markup_filter.py +200 -0
  15. {echotools-2.3.64 → echotools-2.3.66}/README.md +0 -0
  16. {echotools-2.3.64 → echotools-2.3.66}/docs/README.md +0 -0
  17. {echotools-2.3.64 → echotools-2.3.66}/setup.cfg +0 -0
  18. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/__init__.py +0 -0
  19. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/cache/__init__.py +0 -0
  20. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/cache/list_cache.py +0 -0
  21. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/cache/memory_cache.py +0 -0
  22. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/__init__.py +0 -0
  23. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/base.py +0 -0
  24. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/center.py +0 -0
  25. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/center_notify.py +0 -0
  26. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/center_template.py +0 -0
  27. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/loader.py +0 -0
  28. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/merge.py +0 -0
  29. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/errors/__init__.py +0 -0
  30. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/errors/base.py +0 -0
  31. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/errors/classify.py +0 -0
  32. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/errors/common.py +0 -0
  33. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/errors/http.py +0 -0
  34. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/ids/__init__.py +0 -0
  35. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/ids/generator.py +0 -0
  36. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/io/__init__.py +0 -0
  37. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/io/io_utils.py +0 -0
  38. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/io/printstream.py +0 -0
  39. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/logger/__init__.py +0 -0
  40. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/logger/manager.py +0 -0
  41. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/retry/__init__.py +0 -0
  42. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/retry/retry.py +0 -0
  43. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/compat.py +0 -0
  44. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/__init__.py +0 -0
  45. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/candidate.py +0 -0
  46. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  47. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  48. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/race.py +0 -0
  49. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/selector.py +0 -0
  50. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/usage.py +0 -0
  51. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/__init__.py +0 -0
  52. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  53. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  54. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  55. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  56. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/prompt/history.py +0 -0
  57. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  58. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  59. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  60. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  61. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
  62. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
  63. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_stream_json.py +0 -0
  64. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  65. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
  66. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  67. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_think/parse.py +0 -0
  68. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  69. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  70. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/registry.py +0 -0
  71. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  72. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  73. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  74. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  75. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  76. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  77. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/keys.py +0 -0
  78. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/lifecycle/__init__.py +0 -0
  79. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/lifecycle/manager.py +0 -0
  80. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/lifecycle/updater.py +0 -0
  81. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  82. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/process/__init__.py +0 -0
  83. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/process/port.py +0 -0
  84. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/protocol/__init__.py +0 -0
  85. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/__init__.py +0 -0
  86. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/conpty.py +0 -0
  87. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/local/__init__.py +0 -0
  88. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/local/core.py +0 -0
  89. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/local/proc.py +0 -0
  90. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/local/unix.py +0 -0
  91. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/local/win.py +0 -0
  92. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/sanitize.py +0 -0
  93. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/session/__init__.py +0 -0
  94. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/session/base.py +0 -0
  95. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/session/client.py +0 -0
  96. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/ssh.py +0 -0
  97. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/tmux.py +0 -0
  98. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/__init__.py +0 -0
  99. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/charmap.py +0 -0
  100. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/spinner.py +0 -0
  101. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/ui.py +0 -0
  102. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  103. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  104. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  105. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  106. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  107. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  108. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/ui_console.py +0 -0
  109. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/ui_io.py +0 -0
  110. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/ui_log.py +0 -0
  111. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  112. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/ui_text.py +0 -0
  113. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/ui_types.py +0 -0
  114. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  115. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  116. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  117. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  118. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  119. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  120. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  121. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  122. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  123. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  124. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  125. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/events/__init__.py +0 -0
  126. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/events/bus.py +0 -0
  127. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/events/event.py +0 -0
  128. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/spinner/__init__.py +0 -0
  129. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/spinner/spinner.py +0 -0
  130. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/spinner/spinner_color.py +0 -0
  131. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/tracing/__init__.py +0 -0
  132. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/tracing/context.py +0 -0
  133. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/tracing/span.py +0 -0
  134. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/tracing/tracer.py +0 -0
  135. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/translate.py +0 -0
  136. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/watcher/__init__.py +0 -0
  137. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/watcher/file_watcher.py +0 -0
  138. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/__init__.py +0 -0
  139. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/application.py +0 -0
  140. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/broker.py +0 -0
  141. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/app.js +0 -0
  142. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  143. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  144. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  145. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  146. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  147. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  148. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  149. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  150. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  151. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  152. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/inputbox.css +0 -0
  153. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/inputbox.js +0 -0
  154. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/style.css +0 -0
  155. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/voicegif.js +0 -0
  156. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/middleware.py +0 -0
  157. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/stats.py +0 -0
  158. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/utils.py +0 -0
  159. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/files/__init__.py +0 -0
  160. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/files/file_util.py +0 -0
  161. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/network/__init__.py +0 -0
  162. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/network/http_utils.py +0 -0
  163. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/plugin/__init__.py +0 -0
  164. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/plugin/base.py +0 -0
  165. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/plugin/discovery.py +0 -0
  166. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/plugin/registry.py +0 -0
  167. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/plugin/registry_ops.py +0 -0
  168. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/proxy/__init__.py +0 -0
  169. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/proxy/manager.py +0 -0
  170. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/runtime/__init__.py +0 -0
  171. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/runtime/collector.py +0 -0
  172. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/scheduler/__init__.py +0 -0
  173. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/scheduler/scheduler.py +0 -0
  174. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/sdk/__init__.py +0 -0
  175. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/sdk/facade.py +0 -0
  176. {echotools-2.3.64 → echotools-2.3.66}/src/echotools/version.py +0 -0
  177. {echotools-2.3.64 → echotools-2.3.66}/src/echotools.egg-info/dependency_links.txt +0 -0
  178. {echotools-2.3.64 → echotools-2.3.66}/src/echotools.egg-info/requires.txt +0 -0
  179. {echotools-2.3.64 → echotools-2.3.66}/src/echotools.egg-info/top_level.txt +0 -0
  180. {echotools-2.3.64 → echotools-2.3.66}/src/tests/bench_import.py +0 -0
  181. {echotools-2.3.64 → echotools-2.3.66}/src/tests/conftest.py +0 -0
  182. {echotools-2.3.64 → echotools-2.3.66}/src/tests/fixtures/__init__.py +0 -0
  183. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_adversarial_stream_splits.py +0 -0
  184. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_cache_retry.py +0 -0
  185. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_completion.py +0 -0
  186. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_config.py +0 -0
  187. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_config_extended.py +0 -0
  188. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_console.py +0 -0
  189. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_coverage_core.py +0 -0
  190. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_dispatch.py +0 -0
  191. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_dispatch_extended.py +0 -0
  192. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_dispatch_race.py +0 -0
  193. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_agent_output_formats.py +0 -0
  194. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_bare_invoke_format.py +0 -0
  195. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_parser_comprehensive.py +0 -0
  196. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_protocol.py +0 -0
  197. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_stream_json.py +0 -0
  198. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
  199. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_thinking_history.py +0 -0
  200. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_thinking_parse.py +0 -0
  201. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_tool_parse_leak.py +0 -0
  202. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_tool_pipeline.py +0 -0
  203. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_tools.py +0 -0
  204. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_errors.py +0 -0
  205. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_errors_common.py +0 -0
  206. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_events.py +0 -0
  207. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_events_event.py +0 -0
  208. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_facade.py +0 -0
  209. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_fncall.py +0 -0
  210. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_fncall_stream_thinking.py +0 -0
  211. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_ids.py +0 -0
  212. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_imports.py +0 -0
  213. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_io.py +0 -0
  214. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_keys.py +0 -0
  215. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_lifecycle.py +0 -0
  216. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_logger.py +0 -0
  217. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_merge.py +0 -0
  218. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_model_branch_matrix.py +0 -0
  219. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_plugin.py +0 -0
  220. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_printstream.py +0 -0
  221. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_protocols.py +0 -0
  222. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_proxy.py +0 -0
  223. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_proxy_selector.py +0 -0
  224. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_runtime.py +0 -0
  225. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_scheduler.py +0 -0
  226. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  227. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_stream_partial_json_parity.py +0 -0
  228. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_stream_realtime_parse.py +0 -0
  229. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_tracing.py +0 -0
  230. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_usage.py +0 -0
  231. {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_web.py +0 -0
  232. {echotools-2.3.64 → echotools-2.3.66}/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.64
3
+ Version: 2.3.66
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.64"
7
+ version = "2.3.66"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -629,6 +629,9 @@ class FncallStreamParser:
629
629
 
630
630
  # 统一从「可见正文 + fncall 缓冲」解析,避免漏检;thinking 已在过滤器中剥离
631
631
  assembled = "".join(self._text_parts) + self._fncall_buf
632
+ from echotools.exec.fncall.shared.history_markup import strip_fake_history_markup
633
+
634
+ assembled, _ = strip_fake_history_markup(assembled)
632
635
  clean_text, tool_calls = self._protocol.parse(assembled, self._tools)
633
636
 
634
637
  clean_fn = getattr(self._protocol, "clean_tool_tags", None)
@@ -29,6 +29,9 @@ from echotools.exec.fncall.protocols.entml_think.hist import (
29
29
  apply_thinking_history_policy,
30
30
  parse_include_thinking_in_history,
31
31
  )
32
+ from echotools.exec.fncall.shared.history_markup import (
33
+ detect_fake_history_markup,
34
+ )
32
35
  from echotools.exec.fncall.shared.loop_detect import detect_tool_loop
33
36
  from echotools.exec.fncall.shared.normalization import (
34
37
  format_tool_descs,
@@ -75,6 +78,11 @@ def build_tools_prompt(
75
78
  if result.is_looping:
76
79
  logger.debug("检测到工具循环(%d 次)", result.repeat_count)
77
80
  loop_warning = result.suggestion
81
+ history_markup_warning = ""
82
+ markup_result = detect_fake_history_markup(normalized)
83
+ if markup_result.detected:
84
+ logger.debug("检测到 assistant/tool 伪 history 标签")
85
+ history_markup_warning = markup_result.suggestion
78
86
  if hasattr(protocol, "format_tool_descs"):
79
87
  tool_descs = protocol.format_tool_descs(tools)
80
88
  else:
@@ -92,6 +100,7 @@ def build_tools_prompt(
92
100
  user_system_prompt=user_system_prompt,
93
101
  history_text=history_text,
94
102
  loop_warning=loop_warning,
103
+ history_markup_warning=history_markup_warning,
95
104
  current_user_message=current_user_message,
96
105
  **extra,
97
106
  )
@@ -30,6 +30,7 @@ from echotools.exec.fncall.protocols.entml_think.core import (
30
30
  build_entml_thinking_section,
31
31
  )
32
32
  from echotools.exec.fncall.protocols.entml_tools import format_entml_tool_descs
33
+ from echotools.exec.fncall.shared.history_markup import strip_fake_history_markup
33
34
  from echotools.exec.fncall.shared.coercion import _build_param_schema_index
34
35
  from echotools.exec.fncall.shared.normalization import (
35
36
  normalize_content,
@@ -176,6 +177,7 @@ class EntmlProtocol(ToolProtocol):
176
177
  user_system_prompt: str = "",
177
178
  history_text: str = "",
178
179
  loop_warning: str = "",
180
+ history_markup_warning: str = "",
179
181
  current_user_message: Optional[str] = None,
180
182
  protocol_options: Optional[Dict[str, Any]] = None,
181
183
  history_has_tool_calls: bool = False,
@@ -202,6 +204,11 @@ class EntmlProtocol(ToolProtocol):
202
204
  if loop_warning:
203
205
  sections.append(f"<loop_warning>\n{loop_warning}\n</loop_warning>")
204
206
 
207
+ if history_markup_warning:
208
+ sections.append(
209
+ f"<history_markup_warning>\n{history_markup_warning}\n</history_markup_warning>"
210
+ )
211
+
205
212
  if current_user_message is not None:
206
213
  sections.append(format_entml_current_user_message(current_user_message))
207
214
 
@@ -270,16 +277,21 @@ class EntmlProtocol(ToolProtocol):
270
277
 
271
278
  schema_index = _build_param_schema_index(tools) if tools else None
272
279
  parse_text, _thinking = split_entml_thinking(text, thinking_enabled=True)
273
- if has_unclosed_entml_thinking(parse_text, thinking_enabled=True):
274
- open_at, _ = _find_earliest_thinking_open(parse_text, thinking_enabled=True)
275
- if open_at >= 0:
276
- parse_text = parse_text[:open_at]
280
+ unclosed_open_at = -1
281
+ if has_unclosed_entml_thinking(text, thinking_enabled=True):
282
+ unclosed_open_at, _ = _find_earliest_thinking_open(
283
+ text, thinking_enabled=True
284
+ )
285
+ if unclosed_open_at >= 0:
286
+ parse_text = text[:unclosed_open_at]
287
+ parse_text, _ = strip_fake_history_markup(parse_text)
277
288
  tool_calls = parse_entml_tool_calls(parse_text, tools, schema_index)
278
- clean = text
289
+ clean = text[:unclosed_open_at] if unclosed_open_at >= 0 else text
279
290
  if tool_calls:
280
- clean = BLOCK_RE.sub("", text)
291
+ clean = BLOCK_RE.sub("", clean)
281
292
  # 无论是否解析成功,都剥离工具相关残留,避免标签泄露;thinking 保留给后续 split。
282
293
  clean = strip_tool_entml_residue(clean)
294
+ clean, _ = strip_fake_history_markup(clean)
283
295
  return (clean, normalize_tool_calls(tool_calls, tools))
284
296
 
285
297
  def parse_fragment(
@@ -294,8 +306,10 @@ class EntmlProtocol(ToolProtocol):
294
306
  return strip_entml_from_content(content)
295
307
 
296
308
  def clean_tool_tags(self, content: str) -> str:
297
- """仅剥离工具相关标签残留,保留 thinking。"""
298
- return strip_tool_entml_residue(content)
309
+ """仅剥离工具相关标签残留,保留 thinking;并移除伪 history 块。"""
310
+ cleaned = strip_tool_entml_residue(content)
311
+ cleaned, _ = strip_fake_history_markup(cleaned)
312
+ return cleaned
299
313
 
300
314
  def format_assistant_tool_calls(
301
315
  self,
@@ -0,0 +1,135 @@
1
+ """检测并剥离模型误输出的 conversation history 伪标签(``<assistant>`` / ``<tool>``)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any, Dict, List, Tuple
7
+
8
+ from echotools.exec.fncall.shared.normalization import normalize_content
9
+
10
+ _FAKE_HISTORY_TAGS = ("assistant", "tool")
11
+ _FAKE_MARKUP_BLOCK_DETECT_RE = re.compile(
12
+ r"(?:^|\n)\s*</?(?:assistant|tool)\s*>\s*(?:\n|$)",
13
+ re.IGNORECASE | re.MULTILINE,
14
+ )
15
+ _ENTML_THINKING_OPEN_RE = re.compile(
16
+ r"<entml:thinking\b[^>]*>",
17
+ re.IGNORECASE,
18
+ )
19
+ _ENTML_THINKING_CLOSE = "</entml:thinking>"
20
+
21
+
22
+ class HistoryMarkupDetectionResult:
23
+ """伪 history 标签检测结果。"""
24
+
25
+ __slots__ = ("detected", "suggestion")
26
+
27
+ def __init__(self, detected: bool, suggestion: str = "") -> None:
28
+ self.detected = detected
29
+ self.suggestion = suggestion
30
+
31
+
32
+ _HISTORY_MARKUP_SUGGESTION = (
33
+ "Do NOT output <assistant> or <tool> blocks in your reply. "
34
+ "Those tags are reserved for injected conversation history only. "
35
+ "Use <entml:thinking> for private reasoning and <entml:invoke> for tool calls. "
36
+ "Write visible replies as plain text without history markup."
37
+ )
38
+
39
+
40
+ def detect_fake_history_markup(
41
+ messages: List[Dict[str, Any]],
42
+ ) -> HistoryMarkupDetectionResult:
43
+ """扫描 assistant 正文是否误含块级 ``<assistant>`` / ``<tool>`` 伪标签。"""
44
+ for msg in messages:
45
+ if (msg.get("role") or "") != "assistant":
46
+ continue
47
+ content = normalize_content(msg.get("content", ""))
48
+ if content and _FAKE_MARKUP_BLOCK_DETECT_RE.search(content):
49
+ return HistoryMarkupDetectionResult(True, _HISTORY_MARKUP_SUGGESTION)
50
+ return HistoryMarkupDetectionResult(False, "")
51
+
52
+
53
+ def _paired_block_re(tag: str) -> re.Pattern[str]:
54
+ return re.compile(
55
+ rf"(?:^|\n)\s*<{tag}\s*>\s*\n[\s\S]*?\n\s*</{tag}\s*>",
56
+ re.IGNORECASE,
57
+ )
58
+
59
+
60
+ def _orphan_close_re(tag: str) -> re.Pattern[str]:
61
+ return re.compile(
62
+ rf"(?:^|\n)([\s\S]*?)\n\s*</{tag}\s*>\s*(?:\n|$)",
63
+ re.IGNORECASE,
64
+ )
65
+
66
+
67
+ def _orphan_open_re(tag: str) -> re.Pattern[str]:
68
+ return re.compile(
69
+ rf"(?:^|\n)\s*<{tag}\s*>\s*\n[\s\S]*$",
70
+ re.IGNORECASE,
71
+ )
72
+
73
+
74
+ def _strip_fake_blocks_in_segment(segment: str) -> Tuple[str, bool]:
75
+ if not segment:
76
+ return segment, False
77
+ found = False
78
+ text = segment
79
+ for tag in _FAKE_HISTORY_TAGS:
80
+ pair_re = _paired_block_re(tag)
81
+ while True:
82
+ new_text, n = pair_re.subn("", text, count=1)
83
+ if n == 0:
84
+ break
85
+ found = True
86
+ text = new_text
87
+ close_re = _orphan_close_re(tag)
88
+ while True:
89
+ new_text, n = close_re.subn("\n", text, count=1)
90
+ if n == 0:
91
+ break
92
+ found = True
93
+ text = new_text
94
+ open_re = _orphan_open_re(tag)
95
+ while True:
96
+ new_text, n = open_re.subn("", text, count=1)
97
+ if n == 0:
98
+ break
99
+ found = True
100
+ text = new_text
101
+ text = re.sub(r"\n{3,}", "\n\n", text)
102
+ return text, found
103
+
104
+
105
+ def strip_fake_history_markup(content: str) -> Tuple[str, bool]:
106
+ """剥离块级伪 ``<assistant>`` / ``<tool>``;``<entml:thinking>`` 区域原样保留。"""
107
+ if not content:
108
+ return content, False
109
+
110
+ parts: List[str] = []
111
+ found = False
112
+ i = 0
113
+ while i < len(content):
114
+ open_m = _ENTML_THINKING_OPEN_RE.search(content, i)
115
+ if not open_m:
116
+ chunk, hit = _strip_fake_blocks_in_segment(content[i:])
117
+ parts.append(chunk)
118
+ found = found or hit
119
+ break
120
+
121
+ if open_m.start() > i:
122
+ chunk, hit = _strip_fake_blocks_in_segment(content[i : open_m.start()])
123
+ parts.append(chunk)
124
+ found = found or hit
125
+
126
+ body_start = open_m.end()
127
+ close_at = content.find(_ENTML_THINKING_CLOSE, body_start)
128
+ if close_at >= 0:
129
+ parts.append(content[open_m.start() : close_at + len(_ENTML_THINKING_CLOSE)])
130
+ i = close_at + len(_ENTML_THINKING_CLOSE)
131
+ else:
132
+ parts.append(content[open_m.start() :])
133
+ break
134
+
135
+ return "".join(parts), found
@@ -49,6 +49,7 @@ class ToolProtocol(ABC):
49
49
  user_system_prompt: str = "",
50
50
  history_text: str = "",
51
51
  loop_warning: str = "",
52
+ history_markup_warning: str = "",
52
53
  current_user_message: Optional[str] = None,
53
54
  protocol_options: Optional[Dict[str, Any]] = None,
54
55
  ) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.64
3
+ Version: 2.3.66
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -67,6 +67,7 @@ src/echotools/exec/fncall/protocols/entml_think/hist.py
67
67
  src/echotools/exec/fncall/protocols/entml_think/parse.py
68
68
  src/echotools/exec/fncall/shared/__init__.py
69
69
  src/echotools/exec/fncall/shared/coercion.py
70
+ src/echotools/exec/fncall/shared/history_markup.py
70
71
  src/echotools/exec/fncall/shared/loop_detect.py
71
72
  src/echotools/exec/fncall/shared/normalization.py
72
73
  src/echotools/exec/fncall/shared/schema_render.py
@@ -198,8 +199,11 @@ src/tests/test_errors_common.py
198
199
  src/tests/test_events.py
199
200
  src/tests/test_events_event.py
200
201
  src/tests/test_facade.py
202
+ src/tests/test_fault_thinking_close_batch.py
203
+ src/tests/test_fault_thinking_close_stream.py
201
204
  src/tests/test_fncall.py
202
205
  src/tests/test_fncall_stream_thinking.py
206
+ src/tests/test_history_markup_filter.py
203
207
  src/tests/test_ids.py
204
208
  src/tests/test_imports.py
205
209
  src/tests/test_io.py
@@ -223,4 +227,5 @@ src/tests/test_usage.py
223
227
  src/tests/test_web.py
224
228
  src/tests/test_web_broker.py
225
229
  src/tests/fixtures/__init__.py
230
+ src/tests/fixtures/simulated_fault_thinking_responses.py
226
231
  src/tests/fixtures/simulated_llm_tool_responses.py
@@ -0,0 +1,220 @@
1
+ """模拟模型 ``<entml:thinking>…</thinking>`` fault 容错输出(rogator / Claude Code 高发)。
2
+
3
+ 非真实模型日志;用于 batch ``protocol.parse`` 与 ``FncallStreamParser`` 分流回归。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+
11
+ from fixtures.simulated_llm_tool_responses import AGENT_TOOLS, TOOLS, _TOOL_BY_NAME
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class FaultThinkingCase:
16
+ """一条 fault ``</thinking>`` 模拟回复及 batch/stream 期望。"""
17
+
18
+ id: str
19
+ description: str
20
+ response: str
21
+ extra_tools: Tuple[str, ...] = ()
22
+ expect_names: Tuple[str, ...] = ()
23
+ expect_args: Tuple[Dict[str, Any], ...] = ()
24
+ expect_call_count: Optional[int] = None
25
+ expect_thinking_contains: Tuple[str, ...] = ()
26
+ expect_clean_contains: Tuple[str, ...] = ()
27
+ expect_clean_absent: Tuple[str, ...] = ("entml:invoke", "entml:parameter")
28
+ expect_clean_excludes: Tuple[str, ...] = ()
29
+ expect_split_thinking_contains: Tuple[str, ...] = ()
30
+ expect_split_thinking_empty: bool = False
31
+ chunk_sizes: Tuple[int, ...] = (1, 3, 5, 8, 17, 64)
32
+ worst_split: bool = False
33
+
34
+
35
+ READ_TOOL: Dict[str, Any] = {
36
+ "type": "function",
37
+ "function": {
38
+ "name": "Read",
39
+ "parameters": {
40
+ "type": "object",
41
+ "properties": {"path": {"type": "string"}},
42
+ "required": ["path"],
43
+ },
44
+ },
45
+ }
46
+
47
+ _EXTRA_BY_NAME: Dict[str, Dict[str, Any]] = {
48
+ **_TOOL_BY_NAME,
49
+ "Read": READ_TOOL,
50
+ }
51
+
52
+
53
+ def tools_for_fault_case(case: FaultThinkingCase) -> List[Dict[str, Any]]:
54
+ names = {t["function"]["name"] for t in TOOLS}
55
+ out = list(TOOLS)
56
+ for name in case.extra_tools:
57
+ if name not in names and name in _EXTRA_BY_NAME:
58
+ out.append(_EXTRA_BY_NAME[name])
59
+ names.add(name)
60
+ return out
61
+
62
+
63
+ def _big_bash_body() -> str:
64
+ script = "python -c \"print('ok')\"" + ";" + "echo " + ("x" * 6800)
65
+ return (
66
+ "<entml:thinking>\n"
67
+ "用户要求执行较长 shell;我先确认不会破坏环境,再用 Bash。\n"
68
+ "</thinking>\n"
69
+ '<entml:invoke name="Bash">\n'
70
+ f'<entml:parameter name="command">{script}</entml:parameter>\n'
71
+ "</entml:invoke>"
72
+ )
73
+
74
+
75
+ FAULT_THINKING_CASES: Tuple[FaultThinkingCase, ...] = (
76
+ FaultThinkingCase(
77
+ id="model_fault_read_after_close",
78
+ description="rogator 高发:中文思考 + </thinking> + 说明 + Read",
79
+ extra_tools=("Read",),
80
+ response=(
81
+ "<entml:thinking>\n"
82
+ "用户希望查看 main.py。我先确认路径,再调用 Read。\n"
83
+ "</thinking>\n"
84
+ "我先读取该文件。\n"
85
+ '<entml:invoke name="Read">\n'
86
+ '<entml:parameter name="path">src/main.py</entml:parameter>\n'
87
+ "</entml:invoke>"
88
+ ),
89
+ expect_names=("Read",),
90
+ expect_args=({"path": "src/main.py"},),
91
+ expect_thinking_contains=("main.py", "Read"),
92
+ expect_clean_contains=("我先读取该文件。",),
93
+ expect_split_thinking_contains=("main.py",),
94
+ worst_split=True,
95
+ ),
96
+ FaultThinkingCase(
97
+ id="model_fault_bash_git_status",
98
+ description="英文思考 + </thinking> 后直接 Bash(无中间正文)",
99
+ extra_tools=("Bash",),
100
+ response=(
101
+ "<entml:thinking>\n"
102
+ "Need to inspect the working tree before answering.\n"
103
+ "</thinking>\n"
104
+ '<entml:invoke name="Bash">\n'
105
+ '<entml:parameter name="command">git status --short</entml:parameter>\n'
106
+ "</entml:invoke>"
107
+ ),
108
+ expect_names=("Bash",),
109
+ expect_args=({"command": "git status --short"},),
110
+ expect_thinking_contains=("working tree",),
111
+ expect_split_thinking_contains=("working tree",),
112
+ ),
113
+ FaultThinkingCase(
114
+ id="model_fault_weather_with_visible",
115
+ description="fault 闭合后先可见说明再 get_weather",
116
+ response=(
117
+ "<entml:thinking>\n"
118
+ "用户问杭州天气,我需要调用 get_weather。\n"
119
+ "</thinking>\n"
120
+ "正在查询杭州天气。\n"
121
+ '<entml:invoke name="get_weather">\n'
122
+ '<entml:parameter name="city">杭州</entml:parameter>\n'
123
+ '<entml:parameter name="unit">c</entml:parameter>\n'
124
+ "</entml:invoke>"
125
+ ),
126
+ expect_names=("get_weather",),
127
+ expect_args=({"city": "杭州", "unit": "c"},),
128
+ expect_thinking_contains=("杭州",),
129
+ expect_clean_contains=("正在查询杭州天气。",),
130
+ expect_split_thinking_contains=("杭州",),
131
+ ),
132
+ FaultThinkingCase(
133
+ id="model_fault_read_then_bash",
134
+ description="</thinking> 后连续 Read + Bash(双工具)",
135
+ extra_tools=("Read", "Bash"),
136
+ response=(
137
+ "<entml:thinking>\n"
138
+ "先读配置,再跑脚本验证。\n"
139
+ "</thinking>\n"
140
+ '<entml:invoke name="Read">\n'
141
+ '<entml:parameter name="path">config.toml</entml:parameter>\n'
142
+ "</entml:invoke>\n"
143
+ '<entml:invoke name="Bash">\n'
144
+ '<entml:parameter name="command">python verify.py</entml:parameter>\n'
145
+ "</entml:invoke>"
146
+ ),
147
+ expect_names=("Read", "Bash"),
148
+ expect_args=({"path": "config.toml"}, {"command": "python verify.py"}),
149
+ expect_thinking_contains=("先读配置",),
150
+ expect_split_thinking_contains=("先读配置",),
151
+ ),
152
+ FaultThinkingCase(
153
+ id="model_fault_close_without_invoke",
154
+ description="</thinking> 后无 invoke,直到标准 </entml:thinking> 才结束",
155
+ response=(
156
+ "<entml:thinking>\n"
157
+ "模型有时会在正文里提到 </thinking> 标签本身。\n"
158
+ "</thinking>\n"
159
+ "继续推理,尚未调用工具。\n"
160
+ "</entml:thinking>\n"
161
+ "这是给用户的最终回答。"
162
+ ),
163
+ expect_call_count=0,
164
+ expect_thinking_contains=("提到", "继续推理"),
165
+ expect_clean_contains=("最终回答",),
166
+ expect_split_thinking_contains=("</thinking>", "继续推理"),
167
+ ),
168
+ FaultThinkingCase(
169
+ id="model_invoke_inside_before_fault_close",
170
+ description="错误:invoke 写在 </thinking> 之前,不得解析",
171
+ extra_tools=("Bash",),
172
+ response=(
173
+ "<entml:thinking>\n"
174
+ "我先尝试调用 Bash:\n"
175
+ '<entml:invoke name="Bash">\n'
176
+ '<entml:parameter name="command">echo oops</entml:parameter>\n'
177
+ "</entml:invoke>\n"
178
+ "</thinking>\n"
179
+ "以上是思考过程,现在开始正式回复。"
180
+ ),
181
+ expect_call_count=0,
182
+ expect_thinking_contains=("echo oops", "正式回复"),
183
+ expect_clean_excludes=("正式回复", "echo oops"),
184
+ expect_split_thinking_empty=True,
185
+ ),
186
+ FaultThinkingCase(
187
+ id="model_fault_large_bash",
188
+ description="rogator 7KB 级 Bash:fault 闭合后长 parameter",
189
+ extra_tools=("Bash",),
190
+ response=_big_bash_body(),
191
+ expect_names=("Bash",),
192
+ expect_thinking_contains=("较长 shell",),
193
+ expect_split_thinking_contains=("较长 shell",),
194
+ chunk_sizes=(1, 8, 17, 64),
195
+ ),
196
+ FaultThinkingCase(
197
+ id="model_fault_multiline_parameter_read",
198
+ description="</thinking> 与 invoke/parameter 之间有额外空行",
199
+ extra_tools=("Read",),
200
+ response=(
201
+ "<entml:thinking>\n"
202
+ "Read C:/tmp/x.py\n"
203
+ "</thinking>\n"
204
+ "\n"
205
+ '<entml:invoke name="Read">\n'
206
+ '\n'
207
+ '<entml:parameter name="path">C:/tmp/x.py</entml:parameter>\n'
208
+ "\n"
209
+ "</entml:invoke>"
210
+ ),
211
+ expect_names=("Read",),
212
+ expect_args=({"path": "C:/tmp/x.py"},),
213
+ expect_thinking_contains=("Read C:/tmp/x.py",),
214
+ worst_split=True,
215
+ ),
216
+ )
217
+
218
+
219
+ def iter_fault_thinking_cases() -> List[FaultThinkingCase]:
220
+ return list(FAULT_THINKING_CASES)
@@ -578,7 +578,7 @@ SIMULATED_LLM_RESPONSES: List[SimulatedCase] = [
578
578
  ),
579
579
  expect_names=["get_weather"],
580
580
  expect_args=[{"city": "杭州", "unit": "c"}],
581
- expect_clean_substrings=["参考历史:", '{get_weather: {"city": "杭州", "unit": "c"}', "我再确认一次实时天气。"],
581
+ expect_clean_substrings=["参考历史:", "我再确认一次实时天气。"],
582
582
  ),
583
583
  # --- 模型分支:Claude Code / rogator 常见 agent 工具 ---
584
584
  SimulatedCase(
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ """非流式:``<entml:thinking>…</thinking>`` fault 容错 — ``protocol.parse`` / ``split_entml_thinking``。"""
4
+
5
+ import json
6
+ from typing import Any, Dict, List
7
+
8
+ import pytest
9
+
10
+ from echotools.exec.fncall import get_protocol
11
+ from echotools.exec.fncall.protocols.entml_think.parse import (
12
+ find_complete_entml_invoke_open,
13
+ has_unclosed_entml_thinking,
14
+ invoke_index_inside_unclosed_thinking,
15
+ split_entml_thinking,
16
+ )
17
+ from fixtures.simulated_fault_thinking_responses import (
18
+ FaultThinkingCase,
19
+ iter_fault_thinking_cases,
20
+ tools_for_fault_case,
21
+ )
22
+
23
+ CASES = iter_fault_thinking_cases()
24
+ CASE_IDS = [c.id for c in CASES]
25
+
26
+
27
+ def _names(calls: List[Dict[str, Any]]) -> List[str]:
28
+ return [c["function"]["name"] for c in calls]
29
+
30
+
31
+ def _args(calls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
32
+ return [json.loads(c["function"]["arguments"]) for c in calls]
33
+
34
+
35
+ def _batch_parse(case: FaultThinkingCase) -> tuple[str, List[Dict[str, Any]]]:
36
+ return get_protocol("entml").parse(case.response, tools_for_fault_case(case))
37
+
38
+
39
+ def _clean_needles(case: FaultThinkingCase) -> tuple[str, ...]:
40
+ return case.expect_clean_contains
41
+
42
+
43
+ def _assert_clean(case: FaultThinkingCase, clean: str, *, label: str) -> None:
44
+ for needle in _clean_needles(case):
45
+ assert needle in clean, f"{case.id}/{label}: clean missing {needle!r}"
46
+ for bad in case.expect_clean_absent:
47
+ assert bad not in clean, f"{case.id}/{label}: clean leaked {bad!r}"
48
+ for bad in case.expect_clean_excludes:
49
+ assert bad not in clean, f"{case.id}/{label}: clean must exclude {bad!r}"
50
+
51
+
52
+ @pytest.mark.parametrize("case", CASES, ids=CASE_IDS)
53
+ def test_batch_tool_calls(case: FaultThinkingCase) -> None:
54
+ clean, calls = _batch_parse(case)
55
+ if case.expect_call_count is not None:
56
+ assert len(calls) == case.expect_call_count, case.id
57
+ if case.expect_names:
58
+ assert _names(calls) == list(case.expect_names), case.id
59
+ if case.expect_args:
60
+ assert _args(calls) == list(case.expect_args), case.id
61
+ _assert_clean(case, clean, label="batch")
62
+
63
+
64
+ @pytest.mark.parametrize("case", CASES, ids=CASE_IDS)
65
+ def test_batch_split_entml_thinking(case: FaultThinkingCase) -> None:
66
+ content, thinking = split_entml_thinking(case.response)
67
+ if case.expect_split_thinking_empty:
68
+ assert thinking == "", case.id
69
+ assert has_unclosed_entml_thinking(case.response), case.id
70
+ return
71
+ for needle in case.expect_split_thinking_contains:
72
+ assert needle in thinking, f"{case.id}: split missing {needle!r}"
73
+ if case.expect_names:
74
+ assert not has_unclosed_entml_thinking(case.response), case.id
75
+ assert not has_unclosed_entml_thinking(content), case.id
76
+
77
+
78
+ @pytest.mark.parametrize("case", CASES, ids=CASE_IDS)
79
+ def test_batch_invoke_not_inside_unclosed_thinking(case: FaultThinkingCase) -> None:
80
+ pos = find_complete_entml_invoke_open(case.response)
81
+ if case.expect_names:
82
+ assert pos >= 0, case.id
83
+ assert not invoke_index_inside_unclosed_thinking(case.response, pos), case.id
84
+ elif case.id == "model_invoke_inside_before_fault_close":
85
+ assert pos >= 0, case.id
86
+ assert invoke_index_inside_unclosed_thinking(case.response, pos), case.id