echotools 2.3.43__tar.gz → 2.3.45__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 (198) hide show
  1. {echotools-2.3.43 → echotools-2.3.45}/PKG-INFO +1 -1
  2. {echotools-2.3.43 → echotools-2.3.45}/pyproject.toml +2 -1
  3. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/parsers/stream.py +64 -42
  4. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/prompt/inject.py +3 -1
  5. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/protocols/entml.py +36 -12
  6. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/protocols/entml_invoke.py +13 -4
  7. echotools-2.3.45/src/echotools/exec/fncall/protocols/entml_patterns.py +113 -0
  8. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/protocols/entml_think/parse.py +15 -2
  9. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/shared/coercion.py +5 -3
  10. {echotools-2.3.43 → echotools-2.3.45}/src/echotools.egg-info/PKG-INFO +1 -1
  11. {echotools-2.3.43 → echotools-2.3.45}/src/echotools.egg-info/SOURCES.txt +8 -1
  12. echotools-2.3.45/src/tests/fixtures/__init__.py +0 -0
  13. echotools-2.3.45/src/tests/fixtures/simulated_llm_tool_responses.py +450 -0
  14. echotools-2.3.45/src/tests/test_adversarial_stream_splits.py +145 -0
  15. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_entml_protocol.py +3 -1
  16. echotools-2.3.45/src/tests/test_entml_tool_parse_leak.py +256 -0
  17. echotools-2.3.45/src/tests/test_entml_tool_pipeline.py +813 -0
  18. echotools-2.3.45/src/tests/test_simulated_llm_tool_parse.py +166 -0
  19. echotools-2.3.45/src/tests/test_stream_realtime_parse.py +137 -0
  20. echotools-2.3.43/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -51
  21. {echotools-2.3.43 → echotools-2.3.45}/README.md +0 -0
  22. {echotools-2.3.43 → echotools-2.3.45}/docs/README.md +0 -0
  23. {echotools-2.3.43 → echotools-2.3.45}/setup.cfg +0 -0
  24. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/__init__.py +0 -0
  25. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/cache/__init__.py +0 -0
  26. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/cache/list_cache.py +0 -0
  27. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/cache/memory_cache.py +0 -0
  28. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/config/__init__.py +0 -0
  29. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/config/base.py +0 -0
  30. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/config/center.py +0 -0
  31. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/config/center_notify.py +0 -0
  32. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/config/center_template.py +0 -0
  33. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/config/loader.py +0 -0
  34. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/config/merge.py +0 -0
  35. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/errors/__init__.py +0 -0
  36. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/errors/base.py +0 -0
  37. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/errors/classify.py +0 -0
  38. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/errors/common.py +0 -0
  39. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/errors/http.py +0 -0
  40. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/ids/__init__.py +0 -0
  41. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/ids/generator.py +0 -0
  42. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/io/__init__.py +0 -0
  43. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/io/io_utils.py +0 -0
  44. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/io/printstream.py +0 -0
  45. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/logger/__init__.py +0 -0
  46. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/logger/manager.py +0 -0
  47. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/retry/__init__.py +0 -0
  48. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/base/retry/retry.py +0 -0
  49. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/compat.py +0 -0
  50. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/dispatch/__init__.py +0 -0
  51. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/dispatch/candidate.py +0 -0
  52. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  53. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  54. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/dispatch/race.py +0 -0
  55. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/dispatch/selector.py +0 -0
  56. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/dispatch/usage.py +0 -0
  57. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/__init__.py +0 -0
  58. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  59. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  60. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  61. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  62. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/prompt/history.py +0 -0
  63. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  64. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  65. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  66. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  67. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  68. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
  69. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  70. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  71. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  72. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/registry.py +0 -0
  73. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  74. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  75. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  76. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  77. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  78. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/keys.py +0 -0
  79. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/lifecycle/__init__.py +0 -0
  80. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/lifecycle/manager.py +0 -0
  81. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/lifecycle/updater.py +0 -0
  82. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  83. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/process/__init__.py +0 -0
  84. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/process/port.py +0 -0
  85. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/protocol/__init__.py +0 -0
  86. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/protocol/base.py +0 -0
  87. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/__init__.py +0 -0
  88. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/conpty.py +0 -0
  89. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/local/__init__.py +0 -0
  90. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/local/core.py +0 -0
  91. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/local/proc.py +0 -0
  92. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/local/unix.py +0 -0
  93. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/local/win.py +0 -0
  94. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/sanitize.py +0 -0
  95. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/session/__init__.py +0 -0
  96. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/session/base.py +0 -0
  97. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/session/client.py +0 -0
  98. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/ssh.py +0 -0
  99. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/exec/terminal/tmux.py +0 -0
  100. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/console/__init__.py +0 -0
  101. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/console/charmap.py +0 -0
  102. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/console/spinner.py +0 -0
  103. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/console/ui.py +0 -0
  104. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/events/__init__.py +0 -0
  105. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/events/bus.py +0 -0
  106. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/events/event.py +0 -0
  107. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/spinner/__init__.py +0 -0
  108. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/spinner/spinner.py +0 -0
  109. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/spinner/spinner_color.py +0 -0
  110. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/tracing/__init__.py +0 -0
  111. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/tracing/context.py +0 -0
  112. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/tracing/span.py +0 -0
  113. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/tracing/tracer.py +0 -0
  114. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/translate.py +0 -0
  115. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/watcher/__init__.py +0 -0
  116. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/watcher/file_watcher.py +0 -0
  117. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/__init__.py +0 -0
  118. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/application.py +0 -0
  119. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/broker.py +0 -0
  120. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/app.js +0 -0
  121. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  122. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  123. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  124. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  125. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  126. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  127. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  128. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  129. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  130. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  131. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/fonts/tailwind.css +0 -0
  132. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/inputbox.css +0 -0
  133. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/inputbox.js +0 -0
  134. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/style.css +0 -0
  135. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/input_box/voicegif.js +0 -0
  136. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/middleware.py +0 -0
  137. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/stats.py +0 -0
  138. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/media/web/utils.py +0 -0
  139. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/files/__init__.py +0 -0
  140. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/files/file_util.py +0 -0
  141. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/network/__init__.py +0 -0
  142. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/network/http_utils.py +0 -0
  143. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/plugin/__init__.py +0 -0
  144. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/plugin/base.py +0 -0
  145. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/plugin/discovery.py +0 -0
  146. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/plugin/registry.py +0 -0
  147. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/plugin/registry_ops.py +0 -0
  148. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/proxy/__init__.py +0 -0
  149. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/proxy/manager.py +0 -0
  150. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/runtime/__init__.py +0 -0
  151. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/runtime/collector.py +0 -0
  152. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/scheduler/__init__.py +0 -0
  153. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/scheduler/scheduler.py +0 -0
  154. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/sdk/__init__.py +0 -0
  155. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/plat/sdk/facade.py +0 -0
  156. {echotools-2.3.43 → echotools-2.3.45}/src/echotools/version.py +0 -0
  157. {echotools-2.3.43 → echotools-2.3.45}/src/echotools.egg-info/dependency_links.txt +0 -0
  158. {echotools-2.3.43 → echotools-2.3.45}/src/echotools.egg-info/requires.txt +0 -0
  159. {echotools-2.3.43 → echotools-2.3.45}/src/echotools.egg-info/top_level.txt +0 -0
  160. {echotools-2.3.43 → echotools-2.3.45}/src/tests/bench_import.py +0 -0
  161. {echotools-2.3.43 → echotools-2.3.45}/src/tests/conftest.py +0 -0
  162. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_cache_retry.py +0 -0
  163. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_completion.py +0 -0
  164. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_config.py +0 -0
  165. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_config_extended.py +0 -0
  166. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_console.py +0 -0
  167. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_coverage_core.py +0 -0
  168. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_dispatch.py +0 -0
  169. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_dispatch_extended.py +0 -0
  170. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_dispatch_race.py +0 -0
  171. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_entml_thinking_history.py +0 -0
  172. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_entml_thinking_parse.py +0 -0
  173. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_entml_tools.py +0 -0
  174. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_errors.py +0 -0
  175. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_errors_common.py +0 -0
  176. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_events.py +0 -0
  177. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_events_event.py +0 -0
  178. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_facade.py +0 -0
  179. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_fncall.py +0 -0
  180. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_fncall_stream_thinking.py +0 -0
  181. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_ids.py +0 -0
  182. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_imports.py +0 -0
  183. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_io.py +0 -0
  184. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_keys.py +0 -0
  185. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_lifecycle.py +0 -0
  186. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_logger.py +0 -0
  187. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_merge.py +0 -0
  188. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_plugin.py +0 -0
  189. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_printstream.py +0 -0
  190. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_protocols.py +0 -0
  191. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_proxy.py +0 -0
  192. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_proxy_selector.py +0 -0
  193. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_runtime.py +0 -0
  194. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_scheduler.py +0 -0
  195. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_tracing.py +0 -0
  196. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_usage.py +0 -0
  197. {echotools-2.3.43 → echotools-2.3.45}/src/tests/test_web.py +0 -0
  198. {echotools-2.3.43 → echotools-2.3.45}/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.43
3
+ Version: 2.3.45
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echotools"
7
- version = "2.3.43"
7
+ version = "2.3.45"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -103,6 +103,7 @@ ignore_errors = true
103
103
 
104
104
  [tool.pytest.ini_options]
105
105
  testpaths = ["src/tests"]
106
+ pythonpath = ["src", "src/tests"]
106
107
  asyncio_mode = "auto"
107
108
  addopts = ["--strict-markers", "--tb=short"]
108
109
  markers = [
@@ -176,37 +176,40 @@ class FncallStreamParser:
176
176
  combined = self._waiting_tail + chunk
177
177
  self._waiting_tail = ""
178
178
 
179
- if self._thinking_filter is not None:
180
- from echotools.exec.fncall.protocols.entml_think.parse import (
181
- has_unclosed_entml_thinking,
182
- )
183
- if (
184
- self._thinking_filter.in_open_thinking()
185
- or has_unclosed_entml_thinking(combined)
186
- ):
187
- self._feed_waiting_thinking_plain(combined)
188
- return
179
+ # 已进入 thinking 块时,块内不做 invoke 检测。
180
+ if self._thinking_filter is not None and self._thinking_filter.in_open_thinking():
181
+ self._feed_waiting_thinking_plain(combined)
182
+ return
189
183
 
190
184
  trigger_tags = self._protocol.get_trigger_tags()
191
185
  found, pos = self._protocol.detect_start(combined)
192
186
 
193
- if not found:
194
- safe, remain = self._split_safe_text(combined, trigger_tags)
195
- if safe:
196
- self._emit_text(safe)
197
- self._waiting_tail = remain
187
+ if found:
188
+ if pos > 0:
189
+ # 工具标签前的正文仍可能含 thinking,走过滤器。
190
+ self._emit_text(combined[:pos])
191
+ self._fncall_buf = combined[pos:]
192
+ self._detected = True
193
+ self._state = self.IN_FUNCTION_CALLS
194
+ if self._is_call_closed():
195
+ self._state = self.DONE
198
196
  return
199
197
 
200
- if pos > 0:
201
- self._emit_text(combined[:pos])
202
-
203
- self._fncall_buf = combined[pos:]
204
- self._detected = True
205
- self._state = self.IN_FUNCTION_CALLS
198
+ # 无完整工具开标签时,再处理未闭合 thinking / 思考开标签 holdback。
199
+ # 必须放在 detect_start 之后:否则 `<entml:function_calls>\n<en` 会被
200
+ # 尾部 `<en` 误判为 thinking 前缀,导致 invoke 内容被截断。
201
+ if self._thinking_filter is not None:
202
+ from echotools.exec.fncall.protocols.entml_think.parse import (
203
+ has_unclosed_entml_thinking,
204
+ )
205
+ if has_unclosed_entml_thinking(combined):
206
+ self._feed_waiting_thinking_plain(combined)
207
+ return
206
208
 
207
- # 检查是否已闭合(单块内完成)
208
- if self._is_call_closed():
209
- self._state = self.DONE
209
+ safe, remain = self._split_safe_text(combined, trigger_tags)
210
+ if safe:
211
+ self._emit_text(safe)
212
+ self._waiting_tail = remain
210
213
 
211
214
  def _is_call_closed(self) -> bool:
212
215
  """检测 fncall 缓冲区中是否包含结束标记。"""
@@ -218,12 +221,15 @@ class FncallStreamParser:
218
221
  return True
219
222
  return False
220
223
 
221
- def feed(self, chunk: str) -> None:
222
- """喂入新的流式文本块。DONE 或 finalize 后调用静默忽略。"""
224
+ def feed(self, chunk: str) -> List[Dict[str, Any]]:
225
+ """喂入新的流式文本块,返回本轮新完成的 tool_calls(可空列表)。
226
+
227
+ DONE 或 finalize 后调用静默忽略并返回 ``[]``。
228
+ """
223
229
  if not chunk or self._state == self.DONE:
224
- return
230
+ return []
225
231
  if self._finalized_result is not None:
226
- return
232
+ return []
227
233
 
228
234
  self._raw_buf += chunk
229
235
 
@@ -234,6 +240,12 @@ class FncallStreamParser:
234
240
  if self._is_call_closed():
235
241
  self._state = self.DONE
236
242
 
243
+ return self.get_ready_tool_calls()
244
+
245
+ def _assembly_for_tool_parse(self) -> str:
246
+ """可供工具解析的已缓冲文本(不含 thinking 过滤器内部 pending)。"""
247
+ return "".join(self._text_parts) + self._waiting_tail + self._fncall_buf
248
+
237
249
  def finalize(self) -> Tuple[str, List[Dict[str, Any]]]:
238
250
  """结束流式解析,返回 (清理后文本, tool_calls 列表)。幂等。"""
239
251
  if self._finalized_result is not None:
@@ -253,12 +265,18 @@ class FncallStreamParser:
253
265
  else:
254
266
  self._text_parts.append(part)
255
267
 
256
- if not self._detected:
257
- full_text = "".join(self._text_parts)
258
- clean_text, tool_calls = self._protocol.parse(full_text, self._tools)
259
- else:
260
- clean_text = "".join(self._text_parts).strip()
261
- _, tool_calls = self._protocol.parse(self._fncall_buf, self._tools)
268
+ # 统一从「可见正文 + fncall 缓冲」解析,避免漏检;thinking 已在过滤器中剥离
269
+ assembled = "".join(self._text_parts) + self._fncall_buf
270
+ clean_text, tool_calls = self._protocol.parse(assembled, self._tools)
271
+
272
+ clean_fn = getattr(self._protocol, "clean_tool_tags", None)
273
+ if callable(clean_fn):
274
+ clean_text = clean_fn(clean_text)
275
+ elif getattr(self._protocol, "id", None) == "entml":
276
+ from echotools.exec.fncall.protocols.entml_patterns import (
277
+ strip_tool_entml_residue,
278
+ )
279
+ clean_text = strip_tool_entml_residue(clean_text)
262
280
 
263
281
  # 兜底:剥离残留 <entml:thinking>(holdback 边界/分片异常时)
264
282
  if self._thinking_filter is not None and clean_text:
@@ -298,16 +316,20 @@ class FncallStreamParser:
298
316
  def get_ready_tool_calls(self) -> List[Dict[str, Any]]:
299
317
  """返回新完成的 tool_calls(增量,每次调用只返回上次调用之后新增的)。
300
318
 
301
- 适用于在流式阶段实时发送已解析完毕的 tool call,无需等待整个 buffer 就绪。
319
+ 流式阶段可在每次 ``feed`` 后调用,无需等待整个 buffer 结束。
302
320
  """
303
- if self._state not in (self.IN_FUNCTION_CALLS, self.DONE):
304
- return []
305
- try:
306
- _, all_calls = self._protocol.parse(self._fncall_buf, self._tools)
307
- except Exception:
308
- return []
321
+ if self._finalized_result is not None:
322
+ _, all_calls = self._finalized_result
323
+ else:
324
+ try:
325
+ _, all_calls = self._protocol.parse(
326
+ self._assembly_for_tool_parse(),
327
+ self._tools,
328
+ )
329
+ except Exception:
330
+ return []
309
331
  if len(all_calls) <= self._emitted_invoke_count:
310
332
  return []
311
- new_calls = all_calls[self._emitted_invoke_count:]
333
+ new_calls = all_calls[self._emitted_invoke_count :]
312
334
  self._emitted_invoke_count = len(all_calls)
313
335
  return new_calls
@@ -22,7 +22,9 @@ from echotools.exec.fncall.protocols.entml import (
22
22
  format_entml_conversation_history,
23
23
  format_entml_current_user_message,
24
24
  )
25
- from echotools.exec.fncall.protocols.entml_think.core import build_entml_thinking_section
25
+ from echotools.exec.fncall.protocols.entml_think.core import (
26
+ build_entml_thinking_section,
27
+ )
26
28
  from echotools.exec.fncall.protocols.entml_think.hist import (
27
29
  apply_thinking_history_policy,
28
30
  parse_include_thinking_in_history,
@@ -18,8 +18,13 @@ from echotools.exec.fncall.prompt.templates import (
18
18
  from echotools.exec.fncall.protocols.entml_invoke import (
19
19
  parse_entml_tool_calls,
20
20
  )
21
- from echotools.exec.fncall.protocols.entml_patterns import BLOCK_RE
22
- from echotools.exec.fncall.protocols.entml_think.core import build_entml_thinking_section
21
+ from echotools.exec.fncall.protocols.entml_patterns import (
22
+ BLOCK_RE,
23
+ strip_tool_entml_residue,
24
+ )
25
+ from echotools.exec.fncall.protocols.entml_think.core import (
26
+ build_entml_thinking_section,
27
+ )
23
28
  from echotools.exec.fncall.protocols.entml_tools import format_entml_tool_descs
24
29
  from echotools.exec.fncall.shared.coercion import _build_param_schema_index
25
30
  from echotools.exec.fncall.shared.normalization import (
@@ -131,10 +136,19 @@ class EntmlProtocol(ToolProtocol):
131
136
 
132
137
  _TRIGGER = "<entml:invoke>"
133
138
  _TRIGGER_PREFIX = "<entml:invoke"
139
+ _WRAPPER_PREFIX = "<entml:function_calls"
140
+ _THINKING_PREFIX = "<entml:thinking"
134
141
 
135
142
  def get_trigger_tags(self) -> List[str]:
136
- # 同时声明带/不带 ``>`` 的形式,便于流式 holdback 覆盖属性段
137
- return [self._TRIGGER, self._TRIGGER_PREFIX]
143
+ # invoke / function_calls / thinking 前缀一并 holdback,避免 `<e` 歧义被提前吐出
144
+ return [
145
+ self._TRIGGER,
146
+ self._TRIGGER_PREFIX,
147
+ self._WRAPPER_PREFIX,
148
+ f"{self._WRAPPER_PREFIX}>",
149
+ self._THINKING_PREFIX,
150
+ f"{self._THINKING_PREFIX}>",
151
+ ]
138
152
 
139
153
  def get_stream_end_tags(self) -> List[str]:
140
154
  """新格式无外层 wrapper,不自动关闭流,由 finalize() 统一解析。"""
@@ -187,14 +201,18 @@ class EntmlProtocol(ToolProtocol):
187
201
  return "\n\n".join(sections)
188
202
 
189
203
  def detect_start(self, buffer: str) -> Tuple[bool, int]:
190
- pos = buffer.find(self._TRIGGER_PREFIX)
191
- if pos < 0:
204
+ candidates: List[int] = []
205
+ for prefix in (self._WRAPPER_PREFIX, self._TRIGGER_PREFIX):
206
+ pos = buffer.find(prefix)
207
+ if pos < 0:
208
+ continue
209
+ close = buffer.find(">", pos + len(prefix))
210
+ if close < 0:
211
+ continue
212
+ candidates.append(pos)
213
+ if not candidates:
192
214
  return (False, -1)
193
- # 确保是完整的开标签(name=" 属性存在)
194
- close = buffer.find(">", pos + len(self._TRIGGER_PREFIX))
195
- if close < 0:
196
- return (False, -1)
197
- return (True, pos)
215
+ return (True, min(candidates))
198
216
 
199
217
  def parse(
200
218
  self,
@@ -205,7 +223,9 @@ class EntmlProtocol(ToolProtocol):
205
223
  tool_calls = parse_entml_tool_calls(text, tools, schema_index)
206
224
  clean = text
207
225
  if tool_calls:
208
- clean = BLOCK_RE.sub("", text).strip()
226
+ clean = BLOCK_RE.sub("", text)
227
+ # 无论是否解析成功,都剥离工具相关残留,避免标签泄露;thinking 保留给后续 split。
228
+ clean = strip_tool_entml_residue(clean)
209
229
  return (clean, normalize_tool_calls(tool_calls, tools))
210
230
 
211
231
  def parse_fragment(
@@ -219,6 +239,10 @@ class EntmlProtocol(ToolProtocol):
219
239
  def clean_tags(self, content: str) -> str:
220
240
  return strip_entml_from_content(content)
221
241
 
242
+ def clean_tool_tags(self, content: str) -> str:
243
+ """仅剥离工具相关标签残留,保留 thinking。"""
244
+ return strip_tool_entml_residue(content)
245
+
222
246
  def format_assistant_tool_calls(
223
247
  self,
224
248
  tool_calls: List[Dict[str, Any]],
@@ -7,7 +7,9 @@ from .entml_patterns import (
7
7
  INVOKE_RE,
8
8
  PARAM_RE,
9
9
  PARAMETERS_RE,
10
+ extract_attr_value,
10
11
  extract_parameter_type_attr,
12
+ normalize_entml_name,
11
13
  parse_sub_tags,
12
14
  )
13
15
  from .entml_values import coerce_entml_arguments, coerce_entml_parameter_value
@@ -36,9 +38,12 @@ def parse_invoke_args(
36
38
 
37
39
  args: Dict[str, Any] = {}
38
40
  for param_m in PARAM_RE.finditer(body):
39
- pname = param_m.group(1).strip()
40
- attrs = param_m.group(2) or ""
41
- pval = param_m.group(3)
41
+ attrs = param_m.group(1) or ""
42
+ pname = extract_attr_value(attrs, "name")
43
+ if not pname:
44
+ continue
45
+ pname = normalize_entml_name(pname)
46
+ pval = (param_m.group(2) or "").strip()
42
47
  type_hint = extract_parameter_type_attr(attrs)
43
48
  pschema = func_props.get(pname) or {}
44
49
  args[pname] = coerce_entml_parameter_value(
@@ -57,7 +62,11 @@ def parse_entml_tool_calls(
57
62
  ) -> List[Dict[str, Any]]:
58
63
  tool_calls: List[Dict[str, Any]] = []
59
64
  for invoke_m in INVOKE_RE.finditer(text):
60
- name = invoke_m.group(1).strip()
65
+ attrs = invoke_m.group(1) or ""
66
+ name = extract_attr_value(attrs, "name")
67
+ if not name:
68
+ continue
69
+ name = normalize_entml_name(name)
61
70
  args = parse_invoke_args(invoke_m.group(2), name, schema_index)
62
71
  arguments = json.dumps(args, ensure_ascii=False)
63
72
  tool_calls.append(
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Dict, Optional
5
+
6
+ from .entml_values import coerce_entml_parameter_value
7
+
8
+ BLOCK_RE = re.compile(
9
+ r"<entml:invoke\b[^>]*>[\s\S]*?</entml:invoke>",
10
+ re.DOTALL,
11
+ )
12
+ # 允许 name 前后有其它属性;单/双引号均可。
13
+ INVOKE_RE = re.compile(
14
+ r"<entml:invoke\b([^>]*)>([\s\S]*?)</entml:invoke>",
15
+ re.DOTALL,
16
+ )
17
+ # 允许 name / type 等属性任意顺序;单/双引号均可。
18
+ PARAM_RE = re.compile(
19
+ r"<entml:parameter\b([^>]*)>([\s\S]*?)</entml:parameter>",
20
+ re.DOTALL,
21
+ )
22
+ _ATTR_NAME_RE = re.compile(
23
+ r"""\bname\s*=\s*(?P<q>["'])(?P<v>.*?)(?P=q)""",
24
+ re.DOTALL,
25
+ )
26
+ _PARAM_TYPE_ATTR_RE = re.compile(
27
+ r"""\btype\s*=\s*(?P<q>["'])(?P<v>.*?)(?P=q)"""
28
+ )
29
+ PARAMETERS_RE = re.compile(
30
+ r"<entml:parameters>([\s\S]*?)</entml:parameters>",
31
+ re.DOTALL,
32
+ )
33
+ SUB_TAG_RE = re.compile(
34
+ r"<([^>]+)>([\s\S]*?)</\1>",
35
+ re.DOTALL,
36
+ )
37
+ # 工具相关外壳 / 残留(不含 thinking,留给 split_entml_thinking)。
38
+ _TOOL_WRAPPER_PAIR_RE = re.compile(
39
+ r"<entml:function_calls\b[^>]*>[\s\S]*?</entml:function_calls>",
40
+ re.DOTALL,
41
+ )
42
+ _TOOL_ORPHAN_TAG_RE = re.compile(
43
+ r"</?entml:(?:function_calls|invoke|parameter|parameters)\b[^>]*/?>",
44
+ re.DOTALL,
45
+ )
46
+ _EMPTY_FENCE_RE = re.compile(
47
+ r"```(?:xml|entml|text)?\s*```",
48
+ re.IGNORECASE,
49
+ )
50
+ _FENCE_ONLY_LINE_RE = re.compile(
51
+ r"^\s*```(?:xml|entml|text)?\s*$",
52
+ re.IGNORECASE | re.MULTILINE,
53
+ )
54
+
55
+
56
+ def extract_attr_value(attrs: str, attr_name: str = "name") -> Optional[str]:
57
+ """从标签属性串中提取 name/type 等(支持单双引号)。"""
58
+ if not attrs:
59
+ return None
60
+ if attr_name == "name":
61
+ match = _ATTR_NAME_RE.search(attrs)
62
+ elif attr_name == "type":
63
+ match = _PARAM_TYPE_ATTR_RE.search(attrs)
64
+ else:
65
+ pattern = re.compile(
66
+ rf"""\b{re.escape(attr_name)}\s*=\s*(?P<q>["'])(?P<v>.*?)(?P=q)"""
67
+ )
68
+ match = pattern.search(attrs)
69
+ if not match:
70
+ return None
71
+ return match.group("v").strip()
72
+
73
+
74
+ def normalize_entml_name(name: str) -> str:
75
+ """还原 markdown 转义下划线等常见名称噪声。"""
76
+ if not name:
77
+ return ""
78
+ return name.replace("\\_", "_").replace("\\-", "-").strip()
79
+
80
+
81
+ def extract_parameter_type_attr(attrs: str) -> Optional[str]:
82
+ """从 parameter 开标签属性中提取 type=\"...\"。"""
83
+ return extract_attr_value(attrs or "", "type")
84
+
85
+
86
+ def parse_sub_tags(
87
+ content: str,
88
+ schema_index: Optional[Dict[str, Any]] = None,
89
+ func_name: str = "",
90
+ ) -> Dict[str, Any]:
91
+ """解析 <entml:parameters> 内的子标签,返回参数字典。"""
92
+ args: Dict[str, Any] = {}
93
+ for m in SUB_TAG_RE.finditer(content):
94
+ pname = m.group(1).strip()
95
+ pval = m.group(2).strip()
96
+ pschema = schema_index.get(func_name, {}).get(pname, {}) if schema_index else {}
97
+ args[pname] = coerce_entml_parameter_value(pval, pschema or None)
98
+ return args
99
+
100
+
101
+ def strip_tool_entml_residue(content: str) -> str:
102
+ """剥离工具相关 entml 标签残留,保留 thinking 等非工具标签。"""
103
+ if not content:
104
+ return content
105
+ cleaned = _TOOL_WRAPPER_PAIR_RE.sub("", content)
106
+ cleaned = BLOCK_RE.sub("", cleaned)
107
+ cleaned = _TOOL_ORPHAN_TAG_RE.sub("", cleaned)
108
+ cleaned = _EMPTY_FENCE_RE.sub("", cleaned)
109
+ cleaned = _FENCE_ONLY_LINE_RE.sub("", cleaned)
110
+ # 折叠因剥离产生的多余空行
111
+ cleaned = re.sub(r"[ \t]+\n", "\n", cleaned)
112
+ cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
113
+ return cleaned.strip()
@@ -10,6 +10,13 @@ THINKING_BLOCK_RE = re.compile(
10
10
 
11
11
  _THINKING_OPEN_PREFIX = "<entml:thinking"
12
12
  _THINKING_CLOSE = "</entml:thinking>"
13
+ # 与 thinking 共享 `<entml:` 前缀的其它标签:歧义 holdback 应交由工具流式状态机处理
14
+ _AMBIGUOUS_ENTML_PREFIXES = (
15
+ "<entml:invoke",
16
+ "<entml:function_calls",
17
+ "<entml:parameter",
18
+ "<entml:parameters",
19
+ )
13
20
 
14
21
 
15
22
  def has_unclosed_entml_thinking(text: str) -> bool:
@@ -32,8 +39,14 @@ def has_unclosed_entml_thinking(text: str) -> bool:
32
39
  check_len = min(len(text), max_keep)
33
40
  for length in range(check_len, 0, -1):
34
41
  suffix = text[-length:]
35
- if _THINKING_OPEN_PREFIX.startswith(suffix) and suffix != _THINKING_OPEN_PREFIX:
36
- return True
42
+ if not (
43
+ _THINKING_OPEN_PREFIX.startswith(suffix) and suffix != _THINKING_OPEN_PREFIX
44
+ ):
45
+ continue
46
+ # `<e` / `<entml:` 等对 invoke 同样是真前缀 → 不在此抢占
47
+ if any(other.startswith(suffix) for other in _AMBIGUOUS_ENTML_PREFIXES):
48
+ continue
49
+ return True
37
50
  return False
38
51
 
39
52
 
@@ -257,9 +257,11 @@ def _coerce_param_value(raw: str, schema: Dict[str, Any]) -> Any:
257
257
  return parsed
258
258
 
259
259
  if effective_type == "string":
260
- if isinstance(parsed, str):
261
- return parsed
262
- return _coerce_to_string(parsed)
260
+ # string 字段保留原文;仅当整段是 JSON 字符串字面量 ("...") 时才解包
261
+ if len(stripped) >= 2 and stripped[0] == '"' and stripped[-1] == '"':
262
+ if isinstance(parsed, str):
263
+ return parsed
264
+ return stripped
263
265
 
264
266
  if effective_type == "integer":
265
267
  return _coerce_to_integer(stripped, parsed)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.43
3
+ Version: 2.3.45
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Author: nichengfuben
6
6
  License: MIT
@@ -148,6 +148,7 @@ src/echotools/plat/sdk/__init__.py
148
148
  src/echotools/plat/sdk/facade.py
149
149
  src/tests/bench_import.py
150
150
  src/tests/conftest.py
151
+ src/tests/test_adversarial_stream_splits.py
151
152
  src/tests/test_cache_retry.py
152
153
  src/tests/test_completion.py
153
154
  src/tests/test_config.py
@@ -160,6 +161,8 @@ src/tests/test_dispatch_race.py
160
161
  src/tests/test_entml_protocol.py
161
162
  src/tests/test_entml_thinking_history.py
162
163
  src/tests/test_entml_thinking_parse.py
164
+ src/tests/test_entml_tool_parse_leak.py
165
+ src/tests/test_entml_tool_pipeline.py
163
166
  src/tests/test_entml_tools.py
164
167
  src/tests/test_errors.py
165
168
  src/tests/test_errors_common.py
@@ -182,7 +185,11 @@ src/tests/test_proxy.py
182
185
  src/tests/test_proxy_selector.py
183
186
  src/tests/test_runtime.py
184
187
  src/tests/test_scheduler.py
188
+ src/tests/test_simulated_llm_tool_parse.py
189
+ src/tests/test_stream_realtime_parse.py
185
190
  src/tests/test_tracing.py
186
191
  src/tests/test_usage.py
187
192
  src/tests/test_web.py
188
- src/tests/test_web_broker.py
193
+ src/tests/test_web_broker.py
194
+ src/tests/fixtures/__init__.py
195
+ src/tests/fixtures/simulated_llm_tool_responses.py
File without changes