echotools 2.3.83__tar.gz → 2.3.85__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.83 → echotools-2.3.85}/PKG-INFO +1 -1
  2. {echotools-2.3.83 → echotools-2.3.85}/pyproject.toml +1 -1
  3. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/parsers/stream.py +12 -3
  4. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/protocols/entml.py +7 -17
  5. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/protocols/entml_invoke.py +8 -13
  6. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/protocols/entml_patterns.py +93 -5
  7. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/protocols/entml_stream_json.py +2 -32
  8. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/protocols/entml_think/parse.py +42 -27
  9. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/protocols/entml_values.py +41 -3
  10. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/shared/coercion.py +3 -0
  11. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/shared/history_markup.py +6 -5
  12. {echotools-2.3.83 → echotools-2.3.85}/src/echotools.egg-info/PKG-INFO +1 -1
  13. {echotools-2.3.83 → echotools-2.3.85}/src/tests/fixtures/simulated_fake_history_markup_responses.py +3 -3
  14. {echotools-2.3.83 → echotools-2.3.85}/src/tests/fixtures/simulated_fault_thinking_responses.py +1 -1
  15. {echotools-2.3.83 → echotools-2.3.85}/src/tests/fixtures/simulated_llm_tool_responses.py +2 -2
  16. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_entml_parser_comprehensive.py +1 -1
  17. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_entml_protocol.py +96 -16
  18. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_entml_tool_pipeline.py +24 -9
  19. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_fncall_stream_thinking.py +20 -0
  20. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_history_markup_filter.py +2 -2
  21. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_stream_partial_json_parity.py +67 -0
  22. {echotools-2.3.83 → echotools-2.3.85}/README.md +0 -0
  23. {echotools-2.3.83 → echotools-2.3.85}/docs/README.md +0 -0
  24. {echotools-2.3.83 → echotools-2.3.85}/setup.cfg +0 -0
  25. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/__init__.py +0 -0
  26. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/cache/__init__.py +0 -0
  27. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/cache/list_cache.py +0 -0
  28. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/cache/memory_cache.py +0 -0
  29. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/config/__init__.py +0 -0
  30. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/config/base.py +0 -0
  31. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/config/center.py +0 -0
  32. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/config/center_notify.py +0 -0
  33. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/config/center_template.py +0 -0
  34. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/config/loader.py +0 -0
  35. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/config/merge.py +0 -0
  36. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/errors/__init__.py +0 -0
  37. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/errors/base.py +0 -0
  38. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/errors/classify.py +0 -0
  39. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/errors/common.py +0 -0
  40. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/errors/http.py +0 -0
  41. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/ids/__init__.py +0 -0
  42. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/ids/generator.py +0 -0
  43. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/io/__init__.py +0 -0
  44. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/io/io_utils.py +0 -0
  45. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/io/printstream.py +0 -0
  46. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/logger/__init__.py +0 -0
  47. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/logger/manager.py +0 -0
  48. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/retry/__init__.py +0 -0
  49. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/base/retry/retry.py +0 -0
  50. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/compat.py +0 -0
  51. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/dispatch/__init__.py +0 -0
  52. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/dispatch/candidate.py +0 -0
  53. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  54. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  55. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/dispatch/race.py +0 -0
  56. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/dispatch/selector.py +0 -0
  57. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/dispatch/usage.py +0 -0
  58. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/__init__.py +0 -0
  59. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  60. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  61. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  62. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  63. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/prompt/history.py +0 -0
  64. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  65. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  66. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  67. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  68. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  69. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  70. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
  71. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  72. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/protocols/entml_tool_blocks.py +0 -0
  73. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  74. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/registry.py +0 -0
  75. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  76. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  77. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  78. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  79. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  80. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/keys.py +0 -0
  81. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/lifecycle/__init__.py +0 -0
  82. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/lifecycle/manager.py +0 -0
  83. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/lifecycle/updater.py +0 -0
  84. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  85. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/process/__init__.py +0 -0
  86. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/process/port.py +0 -0
  87. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/protocol/__init__.py +0 -0
  88. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/protocol/base.py +0 -0
  89. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/__init__.py +0 -0
  90. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/conpty.py +0 -0
  91. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/local/__init__.py +0 -0
  92. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/local/core.py +0 -0
  93. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/local/proc.py +0 -0
  94. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/local/unix.py +0 -0
  95. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/local/win.py +0 -0
  96. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/sanitize.py +0 -0
  97. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/session/__init__.py +0 -0
  98. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/session/base.py +0 -0
  99. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/session/client.py +0 -0
  100. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/ssh.py +0 -0
  101. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/exec/terminal/tmux.py +0 -0
  102. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/__init__.py +0 -0
  103. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/charmap.py +0 -0
  104. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/spinner.py +0 -0
  105. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/ui.py +0 -0
  106. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  107. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  108. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  109. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  110. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  111. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  112. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uicore/ui_console.py +0 -0
  113. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uicore/ui_io.py +0 -0
  114. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uicore/ui_log.py +0 -0
  115. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  116. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uicore/ui_text.py +0 -0
  117. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uicore/ui_types.py +0 -0
  118. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  119. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  120. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  121. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  122. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  123. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  124. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  125. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  126. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  127. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  128. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  129. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/events/__init__.py +0 -0
  130. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/events/bus.py +0 -0
  131. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/events/event.py +0 -0
  132. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/spinner/__init__.py +0 -0
  133. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/spinner/spinner.py +0 -0
  134. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/spinner/spinner_color.py +0 -0
  135. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/tracing/__init__.py +0 -0
  136. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/tracing/context.py +0 -0
  137. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/tracing/span.py +0 -0
  138. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/tracing/tracer.py +0 -0
  139. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/translate.py +0 -0
  140. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/watcher/__init__.py +0 -0
  141. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/watcher/file_watcher.py +0 -0
  142. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/__init__.py +0 -0
  143. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/application.py +0 -0
  144. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/broker.py +0 -0
  145. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/app.js +0 -0
  146. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  147. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  148. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  149. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  150. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  151. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  152. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  153. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  154. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  155. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  156. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/inputbox.css +0 -0
  157. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/inputbox.js +0 -0
  158. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/style.css +0 -0
  159. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/input_box/voicegif.js +0 -0
  160. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/middleware.py +0 -0
  161. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/stats.py +0 -0
  162. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/media/web/utils.py +0 -0
  163. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/files/__init__.py +0 -0
  164. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/files/file_util.py +0 -0
  165. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/network/__init__.py +0 -0
  166. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/network/http_utils.py +0 -0
  167. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/plugin/__init__.py +0 -0
  168. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/plugin/base.py +0 -0
  169. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/plugin/discovery.py +0 -0
  170. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/plugin/registry.py +0 -0
  171. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/plugin/registry_ops.py +0 -0
  172. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/proxy/__init__.py +0 -0
  173. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/proxy/manager.py +0 -0
  174. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/runtime/__init__.py +0 -0
  175. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/runtime/collector.py +0 -0
  176. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/scheduler/__init__.py +0 -0
  177. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/scheduler/scheduler.py +0 -0
  178. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/sdk/__init__.py +0 -0
  179. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/plat/sdk/facade.py +0 -0
  180. {echotools-2.3.83 → echotools-2.3.85}/src/echotools/version.py +0 -0
  181. {echotools-2.3.83 → echotools-2.3.85}/src/echotools.egg-info/SOURCES.txt +0 -0
  182. {echotools-2.3.83 → echotools-2.3.85}/src/echotools.egg-info/dependency_links.txt +0 -0
  183. {echotools-2.3.83 → echotools-2.3.85}/src/echotools.egg-info/requires.txt +0 -0
  184. {echotools-2.3.83 → echotools-2.3.85}/src/echotools.egg-info/top_level.txt +0 -0
  185. {echotools-2.3.83 → echotools-2.3.85}/src/tests/_check_parity.py +0 -0
  186. {echotools-2.3.83 → echotools-2.3.85}/src/tests/bench_import.py +0 -0
  187. {echotools-2.3.83 → echotools-2.3.85}/src/tests/conftest.py +0 -0
  188. {echotools-2.3.83 → echotools-2.3.85}/src/tests/fixtures/__init__.py +0 -0
  189. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_adversarial_stream_splits.py +0 -0
  190. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_cache_retry.py +0 -0
  191. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_completion.py +0 -0
  192. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_config.py +0 -0
  193. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_config_extended.py +0 -0
  194. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_console.py +0 -0
  195. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_coverage_core.py +0 -0
  196. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_dispatch.py +0 -0
  197. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_dispatch_extended.py +0 -0
  198. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_dispatch_race.py +0 -0
  199. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_entml_agent_output_formats.py +0 -0
  200. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_entml_bare_invoke_format.py +0 -0
  201. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_entml_batch_stream_comprehensive.py +0 -0
  202. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_entml_stream_json.py +0 -0
  203. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
  204. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_entml_thinking_history.py +0 -0
  205. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_entml_thinking_parse.py +0 -0
  206. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_entml_tool_parse_leak.py +0 -0
  207. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_entml_tools.py +0 -0
  208. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_errors.py +0 -0
  209. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_errors_common.py +0 -0
  210. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_events.py +0 -0
  211. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_events_event.py +0 -0
  212. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_facade.py +0 -0
  213. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_fake_history_markup_batch.py +0 -0
  214. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
  215. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
  216. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_fake_history_markup_stream.py +0 -0
  217. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_fault_thinking_close_batch.py +0 -0
  218. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_fault_thinking_close_stream.py +0 -0
  219. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_fncall.py +0 -0
  220. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_ids.py +0 -0
  221. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_imports.py +0 -0
  222. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_io.py +0 -0
  223. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_keys.py +0 -0
  224. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_lifecycle.py +0 -0
  225. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_logger.py +0 -0
  226. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_merge.py +0 -0
  227. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_model_branch_matrix.py +0 -0
  228. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_plugin.py +0 -0
  229. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_printstream.py +0 -0
  230. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_protocols.py +0 -0
  231. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_proxy.py +0 -0
  232. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_proxy_selector.py +0 -0
  233. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_runtime.py +0 -0
  234. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_scheduler.py +0 -0
  235. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  236. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_stream_realtime_parse.py +0 -0
  237. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_tracing.py +0 -0
  238. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_usage.py +0 -0
  239. {echotools-2.3.83 → echotools-2.3.85}/src/tests/test_web.py +0 -0
  240. {echotools-2.3.83 → echotools-2.3.85}/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.83
3
+ Version: 2.3.85
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.83"
7
+ version = "2.3.85"
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
@@ -23,10 +23,10 @@ from echotools.exec.fncall.protocols.entml_tool_blocks import (
23
23
  strip_tool_block_spans,
24
24
  )
25
25
  from echotools.exec.fncall.protocols.entml_patterns import (
26
- BLOCK_RE,
27
26
  entml_invoke_open_may_be_streaming,
28
27
  extract_attr_value,
29
28
  normalize_entml_name,
29
+ strip_actionable_entml_invoke_blocks,
30
30
  strip_legacy_function_calls_wrapper,
31
31
  strip_tool_entml_residue,
32
32
  )
@@ -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]]:
@@ -308,7 +297,7 @@ class EntmlProtocol(ToolProtocol):
308
297
  # 须在移除 invoke 之前剥离伪 history,且须晚于已解析的 ``<tool>`` 块剥离。
309
298
  clean, _ = strip_fake_history_markup(clean)
310
299
  if tool_calls:
311
- clean = BLOCK_RE.sub("", clean)
300
+ clean = strip_actionable_entml_invoke_blocks(clean)
312
301
  clean = strip_tool_entml_residue(clean)
313
302
  return (clean, normalize_tool_calls(tool_calls, tools))
314
303
 
@@ -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:
@@ -5,20 +5,19 @@ from typing import Any, Dict, List, Optional
5
5
 
6
6
  from .entml_patterns import (
7
7
  BARE_INVOKE_CHILD_RE,
8
- INVOKE_DIRECT_CHILD_OPEN_RE,
9
8
  INVOKE_DIRECT_CHILD_RE,
10
9
  INVOKE_DIRECT_CHILD_SKIP,
11
- INVOKE_RE,
12
10
  PARAM_RE,
13
11
  PARAMETERS_RE,
14
12
  extract_attr_value,
15
13
  extract_parameter_type_attr,
14
+ iter_actionable_entml_invoke_blocks,
16
15
  normalize_entml_name,
17
16
  parse_sub_tags,
18
17
  split_mangled_json_param_tail,
19
18
  synthetic_close_invoke_body,
20
19
  )
21
- from .entml_values import coerce_entml_arguments, coerce_entml_parameter_value
20
+ from .entml_values import coerce_entml_arguments, coerce_entml_parameter_value, _coerce_entml_arg_value
22
21
 
23
22
 
24
23
  def _parse_direct_child_tags(
@@ -98,13 +97,10 @@ def parse_invoke_args(
98
97
  if extra_key in args:
99
98
  continue
100
99
  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
- )
100
+ args[extra_key] = _coerce_entml_arg_value(
101
+ extra_val,
102
+ extra_schema or None,
103
+ )
108
104
 
109
105
  _parse_bare_invoke_children(body, args, func_props)
110
106
  _parse_direct_child_tags(body, args, func_props)
@@ -118,13 +114,12 @@ def parse_entml_tool_calls(
118
114
  schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]],
119
115
  ) -> List[Dict[str, Any]]:
120
116
  tool_calls: List[Dict[str, Any]] = []
121
- for invoke_m in INVOKE_RE.finditer(text):
122
- attrs = invoke_m.group(1) or ""
117
+ for _start, _end, attrs, body in iter_actionable_entml_invoke_blocks(text):
123
118
  name = extract_attr_value(attrs, "name")
124
119
  if not name:
125
120
  continue
126
121
  name = normalize_entml_name(name)
127
- args = parse_invoke_args(invoke_m.group(2), name, schema_index)
122
+ args = parse_invoke_args(body, name, schema_index)
128
123
  arguments = json.dumps(args, ensure_ascii=False)
129
124
  tool_calls.append(
130
125
  {
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import re
4
- from typing import Any, Dict, Optional, Tuple
5
+ from typing import Any, Dict, Iterator, List, Optional, Tuple
5
6
 
6
7
  from .entml_values import coerce_entml_parameter_value
7
8
 
@@ -50,7 +51,7 @@ PARAMETERS_RE = re.compile(
50
51
  r"<entml:parameters>([\s\S]*?)</entml:parameters>",
51
52
  re.DOTALL,
52
53
  )
53
- # invoke 内裸子标签(非 parameter 包裹),如 Claude Code 输出的 description/timeout。
54
+ # invoke 内裸子标签(非 parameter 包裹),如部分 agent 输出的 description/timeout。
54
55
  BARE_INVOKE_CHILD_RE = re.compile(
55
56
  r"<entml:(description|timeout)>([\s\S]*?)</entml:\1>",
56
57
  re.DOTALL | re.IGNORECASE,
@@ -101,6 +102,7 @@ _FENCE_ONLY_LINE_RE = re.compile(
101
102
  re.IGNORECASE | re.MULTILINE,
102
103
  )
103
104
  _INVOKE_OPEN_PREFIX = "<entml:invoke"
105
+ _INVOKE_CLOSE = "</entml:invoke>"
104
106
  _PLACEHOLDER_INVOKE_NAMES = frozenset({"$FUNCTION_NAME", "$FUNCTION_NAME2"})
105
107
 
106
108
 
@@ -137,6 +139,76 @@ def entml_invoke_open_may_be_streaming(buffer: str, pos: int) -> bool:
137
139
  return False
138
140
 
139
141
 
142
+ def _actionable_invoke_open_before_close(content: str, close_pos: int) -> bool:
143
+ """``close_pos`` 处的 ``</entml:invoke>`` 是否闭合带真实 name 的开标签。"""
144
+ search = close_pos - 1
145
+ while search >= 0:
146
+ pos = content.rfind(_INVOKE_OPEN_PREFIX, 0, search + 1)
147
+ if pos < 0:
148
+ return False
149
+ if entml_invoke_open_is_actionable(content, pos):
150
+ gt = content.find(">", pos)
151
+ if gt < 0 or gt >= close_pos:
152
+ search = pos - 1
153
+ continue
154
+ inner_close = content.find(_INVOKE_CLOSE, gt + 1, close_pos)
155
+ if inner_close < 0:
156
+ return True
157
+ search = pos - 1
158
+ continue
159
+ search = pos - 1
160
+ return False
161
+
162
+
163
+ def _invoke_close_should_strip(content: str, close_pos: int) -> bool:
164
+ """是否应剥离 ``</entml:invoke>``(孤儿噪声或真实工具块残留)。"""
165
+ if _actionable_invoke_open_before_close(content, close_pos):
166
+ return True
167
+ open_pos = content.rfind(_INVOKE_OPEN_PREFIX, 0, close_pos)
168
+ if open_pos < 0:
169
+ return True
170
+ return entml_invoke_open_is_actionable(content, open_pos)
171
+
172
+
173
+ def iter_actionable_entml_invoke_blocks(text: str) -> Iterator[Tuple[int, int, str, str]]:
174
+ """迭代含真实 ``name`` 的完整 ``<entml:invoke>…</entml:invoke>`` 块。"""
175
+ if not text:
176
+ return
177
+ search_from = 0
178
+ prefix_len = len(_INVOKE_OPEN_PREFIX)
179
+ close_len = len(_INVOKE_CLOSE)
180
+ while search_from < len(text):
181
+ pos = text.find(_INVOKE_OPEN_PREFIX, search_from)
182
+ if pos < 0:
183
+ break
184
+ if not entml_invoke_open_is_actionable(text, pos):
185
+ search_from = pos + prefix_len
186
+ continue
187
+ gt = text.find(">", pos + prefix_len)
188
+ if gt < 0:
189
+ break
190
+ close = text.find(_INVOKE_CLOSE, gt + 1)
191
+ if close < 0:
192
+ break
193
+ attrs = text[pos + prefix_len : gt]
194
+ body = text[gt + 1 : close]
195
+ yield pos, close + close_len, attrs, body
196
+ search_from = close + close_len
197
+
198
+
199
+ def strip_actionable_entml_invoke_blocks(text: str) -> str:
200
+ """仅剥离含真实 ``name`` 的 invoke 块;保留 prose ``<entml:invoke>`` 提及。"""
201
+ if not text:
202
+ return text
203
+ parts: List[str] = []
204
+ last = 0
205
+ for start, end, _attrs, _body in iter_actionable_entml_invoke_blocks(text):
206
+ parts.append(text[last:start])
207
+ last = end
208
+ parts.append(text[last:])
209
+ return "".join(parts)
210
+
211
+
140
212
  def _strip_orphan_invoke_tags(content: str) -> str:
141
213
  """仅剥离带真实 name 的 invoke 孤儿标签;保留 prose 中的 ``<entml:invoke>`` 提及。"""
142
214
  pattern = re.compile(r"</?entml:invoke\b[^>]*/?>", re.DOTALL)
@@ -144,7 +216,9 @@ def _strip_orphan_invoke_tags(content: str) -> str:
144
216
  def repl(match: re.Match[str]) -> str:
145
217
  tag = match.group(0)
146
218
  if tag.startswith("</"):
147
- return ""
219
+ if _invoke_close_should_strip(content, match.start()):
220
+ return ""
221
+ return tag
148
222
  if entml_invoke_open_is_actionable(content, match.start()):
149
223
  return ""
150
224
  return tag
@@ -224,15 +298,29 @@ _MANGLED_PARAM_JSON_COMMA_AFTER_QUOTE_RE = re.compile(
224
298
  )
225
299
 
226
300
 
301
+ def _param_value_is_json_container(value: str) -> bool:
302
+ stripped = (value or "").lstrip()
303
+ return bool(stripped) and stripped[0] in "{["
304
+
305
+
227
306
  def mangled_json_param_tail_in_progress(value: str) -> bool:
228
307
  """parameter 值中出现误写入 JSON 尾缀但尚未收齐时不应继续增长 partial_json。"""
229
- return bool(value and _MANGLED_PARAM_JSON_TAIL_IN_PROGRESS_RE.search(value))
308
+ if not value or _param_value_is_json_container(value):
309
+ return False
310
+ return bool(_MANGLED_PARAM_JSON_TAIL_IN_PROGRESS_RE.search(value))
230
311
 
231
312
 
232
313
  def split_mangled_json_param_tail(value: str) -> Tuple[str, Dict[str, Any]]:
233
314
  """模型把 ``", "description": ..., "timeout": ...}}`` 误写入 parameter 值时的拆分。"""
234
315
  if not value:
235
316
  return value, {}
317
+ if _param_value_is_json_container(value):
318
+ try:
319
+ json.loads(value)
320
+ return value, {}
321
+ except json.JSONDecodeError:
322
+ # 合法 JSON 数组/对象(含 options.description 等)不得走标量 command 尾缀启发式。
323
+ return value, {}
236
324
  match = _MANGLED_PARAM_JSON_TAIL_RE.search(value)
237
325
  if match:
238
326
  command = value[: match.start() + 1]
@@ -365,7 +453,7 @@ def strip_tool_entml_residue(content: str) -> str:
365
453
  if not content:
366
454
  return content
367
455
  cleaned = _TOOL_WRAPPER_PAIR_RE.sub("", content)
368
- cleaned = BLOCK_RE.sub("", cleaned)
456
+ cleaned = strip_actionable_entml_invoke_blocks(cleaned)
369
457
  cleaned = _strip_orphan_invoke_tags(cleaned)
370
458
  cleaned = _strip_orphan_non_invoke_tool_tags(cleaned)
371
459
  cleaned = _EMPTY_FENCE_RE.sub("", cleaned)
@@ -21,8 +21,7 @@ from .entml_patterns import (
21
21
  split_mangled_json_param_tail,
22
22
  synthetic_close_invoke_body,
23
23
  )
24
- from .entml_values import coerce_entml_parameter_value
25
- from echotools.exec.fncall.shared.coercion import _resolve_effective_type
24
+ from .entml_values import coerce_entml_parameter_value, effective_entml_param_json_type
26
25
 
27
26
  _PARAM_OPEN_RE = re.compile(rf"{PARAM_OPEN_PATTERN}([^>]*)>", re.IGNORECASE)
28
27
  _PARAM_CLOSE = PARAM_CLOSE_ENTML
@@ -31,22 +30,6 @@ _PARAMETERS_CLOSE = "</entml:parameters>"
31
30
  _INVOKE_CLOSE = "</entml:invoke>"
32
31
  _INVOKE_OPEN_PREFIX = "<entml:invoke"
33
32
 
34
- _TYPE_HINT_TO_JSON_TYPE = {
35
- "str": "string",
36
- "string": "string",
37
- "int": "integer",
38
- "integer": "integer",
39
- "float": "number",
40
- "number": "number",
41
- "double": "number",
42
- "bool": "boolean",
43
- "boolean": "boolean",
44
- "array": "array",
45
- "list": "array",
46
- "object": "object",
47
- "dict": "object",
48
- }
49
-
50
33
 
51
34
  def _strip_incomplete_markup_suffix(value: str) -> str:
52
35
  lt = value.rfind("<")
@@ -89,20 +72,7 @@ def _effective_param_type(
89
72
  pschema: Optional[Dict[str, Any]],
90
73
  type_hint: Optional[str],
91
74
  ) -> str:
92
- if type_hint:
93
- mapped = _TYPE_HINT_TO_JSON_TYPE.get(type_hint.strip().lower())
94
- if mapped:
95
- return mapped
96
- if pschema:
97
- resolved = _resolve_effective_type(pschema)
98
- if resolved:
99
- return resolved
100
- stripped = (value or "").lstrip()
101
- if stripped.startswith("["):
102
- return "array"
103
- if stripped.startswith("{"):
104
- return "object"
105
- return "string"
75
+ return effective_entml_param_json_type(value, pschema, type_hint)
106
76
 
107
77
 
108
78
  def _synthetic_close_body(inner: str) -> str:
@@ -353,26 +353,49 @@ def invoke_index_inside_unclosed_thinking(
353
353
  thinking_enabled: bool = True,
354
354
  ) -> bool:
355
355
  """``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
356
+ return invoke_index_inside_any_thinking_block(
357
+ text, invoke_at, thinking_enabled=thinking_enabled, unclosed_only=True,
360
358
  )
361
- if think_open < 0 or invoke_at <= think_open:
359
+
360
+
361
+ def invoke_index_inside_any_thinking_block(
362
+ text: str,
363
+ invoke_at: int,
364
+ *,
365
+ thinking_enabled: bool = True,
366
+ unclosed_only: bool = False,
367
+ ) -> bool:
368
+ """``invoke_at`` 是否落在 thinking 块内(``unclosed_only`` 时仅检查未闭合块)。"""
369
+ if invoke_at < 0:
362
370
  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
371
+ i = 0
372
+ while i < len(text):
373
+ open_at, opened_plain = _find_earliest_thinking_open(
374
+ text, i, thinking_enabled=thinking_enabled,
375
+ )
376
+ if open_at < 0:
377
+ break
378
+ gt = text.find(">", open_at)
379
+ if gt < 0:
380
+ return invoke_at > open_at
381
+ body_start = gt + 1
382
+ close_at, close_len = _find_thinking_close(
383
+ text,
384
+ body_start,
385
+ opened_plain=opened_plain,
386
+ thinking_enabled=thinking_enabled,
387
+ )
388
+ if close_at < 0:
389
+ if open_at < invoke_at:
390
+ return True
391
+ break
392
+ block_end = close_at + close_len
393
+ if open_at < invoke_at < block_end:
394
+ return True
395
+ if unclosed_only:
396
+ return False
397
+ i = block_end
398
+ return False
376
399
 
377
400
 
378
401
  def split_entml_thinking(
@@ -574,15 +597,7 @@ class EntmlThinkingStreamFilter:
574
597
  self._pending = ""
575
598
  return False
576
599
 
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
-
600
+ # thinking 块内 ``<entml:invoke>`` 等仅为 prose/示例时仍计入 thinking,不按工具 hold 截断。
586
601
  safe, hold = _hold_prefix(self._pending, _THINKING_CLOSE)
587
602
  if not hold and self._thinking_enabled:
588
603
  safe, hold = _hold_prefix(safe, _FAULT_THINKING_CLOSE)
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  from typing import Any, Dict, Optional
5
5
 
6
- from echotools.exec.fncall.shared.coercion import _coerce_param_value
6
+ from echotools.exec.fncall.shared.coercion import _coerce_param_value, _resolve_effective_type
7
7
 
8
8
  _TYPE_HINT_TO_JSON_TYPE = {
9
9
  "str": "string",
@@ -71,6 +71,44 @@ def coerce_entml_parameter_value(
71
71
  return stripped
72
72
 
73
73
 
74
+ def effective_entml_param_json_type(
75
+ value: str,
76
+ schema: Optional[Dict[str, Any]] = None,
77
+ type_hint: Optional[str] = None,
78
+ ) -> str:
79
+ """流式 partial_json 与 batch 共用:模型 type 优先,否则 schema,再否则按值推断。"""
80
+ effective = resolve_entml_parameter_schema(schema, type_hint)
81
+ if effective:
82
+ resolved = _resolve_effective_type(effective)
83
+ if resolved:
84
+ return resolved
85
+ stripped = (value or "").lstrip()
86
+ if stripped.startswith("["):
87
+ return "array"
88
+ if stripped.startswith("{"):
89
+ return "object"
90
+ return "string"
91
+
92
+
93
+ def _coerce_entml_arg_value(
94
+ value: Any,
95
+ schema: Optional[Dict[str, Any]] = None,
96
+ type_hint: Optional[str] = None,
97
+ ) -> Any:
98
+ """将已解析的 Python 值再按 type_hint / schema 归一(与 batch 单参路径一致)。"""
99
+ if isinstance(value, str):
100
+ return coerce_entml_parameter_value(value, schema, type_hint=type_hint)
101
+ if isinstance(value, (dict, list)):
102
+ return coerce_entml_parameter_value(
103
+ json.dumps(value, ensure_ascii=False),
104
+ schema,
105
+ type_hint=type_hint,
106
+ )
107
+ if value is None:
108
+ return coerce_entml_parameter_value("", schema, type_hint=type_hint)
109
+ return coerce_entml_parameter_value(str(value), schema, type_hint=type_hint)
110
+
111
+
74
112
  def coerce_entml_arguments(
75
113
  args: Dict[str, Any],
76
114
  func_name: str,
@@ -86,8 +124,8 @@ def coerce_entml_arguments(
86
124
  out: Dict[str, Any] = {}
87
125
  for key, value in args.items():
88
126
  pschema = func_schema.get(key) or {}
89
- if isinstance(value, str) and pschema:
90
- out[key] = coerce_entml_parameter_value(value, pschema)
127
+ if pschema:
128
+ out[key] = _coerce_entml_arg_value(value, pschema)
91
129
  else:
92
130
  out[key] = value
93
131
  return out
@@ -199,6 +199,9 @@ def _coerce_to_array(raw: str, parsed: Any, item_schema: Dict[str, Any]) -> Any:
199
199
  if result_list is None:
200
200
  result_list = _try_parse_json_list(raw)
201
201
 
202
+ if result_list is None and isinstance(parsed, dict):
203
+ result_list = [parsed]
204
+
202
205
  if result_list is None:
203
206
  logger.debug("_coerce_to_array: 无法解析为列表,原样返回 %r", raw[:100])
204
207
  return parsed
@@ -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.83
3
+ Version: 2.3.85
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 分片、