echotools 2.3.88__tar.gz → 2.3.90__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 (245) hide show
  1. {echotools-2.3.88 → echotools-2.3.90}/PKG-INFO +1 -1
  2. {echotools-2.3.88 → echotools-2.3.90}/pyproject.toml +1 -1
  3. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/stream.py +5 -0
  4. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/stream_delta.py +5 -5
  5. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/stream_feed.py +5 -5
  6. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/stream_final.py +27 -6
  7. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/prompt/templates.py +16 -6
  8. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml.py +47 -29
  9. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_invoke.py +5 -1
  10. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_patterns.py +109 -25
  11. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_stream/body.py +14 -2
  12. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/hold.py +17 -23
  13. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/parse.py +29 -13
  14. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/protocol/base.py +5 -1
  15. {echotools-2.3.88 → echotools-2.3.90}/src/echotools.egg-info/PKG-INFO +1 -1
  16. {echotools-2.3.88 → echotools-2.3.90}/src/echotools.egg-info/SOURCES.txt +1 -0
  17. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_bare_invoke_format.py +30 -9
  18. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_batch_stream_comprehensive.py +2 -1
  19. echotools-2.3.90/src/tests/test_entml_invoke_known_tool_gate.py +220 -0
  20. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_protocol.py +4 -4
  21. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_tool_pipeline.py +3 -3
  22. {echotools-2.3.88 → echotools-2.3.90}/README.md +0 -0
  23. {echotools-2.3.88 → echotools-2.3.90}/setup.cfg +0 -0
  24. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/__init__.py +0 -0
  25. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/cache/__init__.py +0 -0
  26. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/cache/list_cache.py +0 -0
  27. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/cache/memory_cache.py +0 -0
  28. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/__init__.py +0 -0
  29. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/base.py +0 -0
  30. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/center.py +0 -0
  31. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/center_notify.py +0 -0
  32. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/center_template.py +0 -0
  33. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/loader.py +0 -0
  34. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/config/merge.py +0 -0
  35. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/errors/__init__.py +0 -0
  36. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/errors/base.py +0 -0
  37. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/errors/classify.py +0 -0
  38. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/errors/common.py +0 -0
  39. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/errors/http.py +0 -0
  40. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/ids/__init__.py +0 -0
  41. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/ids/generator.py +0 -0
  42. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/io/__init__.py +0 -0
  43. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/io/io_utils.py +0 -0
  44. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/io/printstream.py +0 -0
  45. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/logger/__init__.py +0 -0
  46. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/logger/manager.py +0 -0
  47. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/retry/__init__.py +0 -0
  48. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/base/retry/retry.py +0 -0
  49. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/compat.py +0 -0
  50. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/__init__.py +0 -0
  51. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/candidate.py +0 -0
  52. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  53. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  54. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/race.py +0 -0
  55. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/selector.py +0 -0
  56. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/dispatch/usage.py +0 -0
  57. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/__init__.py +0 -0
  58. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  59. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  60. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  61. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  62. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/prompt/history.py +0 -0
  63. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  64. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  65. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  66. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  67. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_schema.py +0 -0
  68. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_stream/__init__.py +0 -0
  69. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_stream/snap.py +0 -0
  70. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  71. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/blocks.py +0 -0
  72. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
  73. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/filter.py +0 -0
  74. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  75. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/registry.py +0 -0
  76. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  77. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  78. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/history_markup.py +0 -0
  79. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  80. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  81. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  82. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  83. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/keys.py +0 -0
  84. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/lifecycle/__init__.py +0 -0
  85. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/lifecycle/manager.py +0 -0
  86. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/lifecycle/updater.py +0 -0
  87. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  88. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/process/__init__.py +0 -0
  89. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/process/port.py +0 -0
  90. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/protocol/__init__.py +0 -0
  91. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/__init__.py +0 -0
  92. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/conpty.py +0 -0
  93. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/local/__init__.py +0 -0
  94. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/local/core.py +0 -0
  95. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/local/proc.py +0 -0
  96. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/local/unix.py +0 -0
  97. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/local/win.py +0 -0
  98. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/sanitize.py +0 -0
  99. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/session/__init__.py +0 -0
  100. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/session/base.py +0 -0
  101. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/session/client.py +0 -0
  102. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/ssh.py +0 -0
  103. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/exec/terminal/tmux.py +0 -0
  104. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/__init__.py +0 -0
  105. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/charmap.py +0 -0
  106. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/spinner.py +0 -0
  107. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/ui.py +0 -0
  108. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  109. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  110. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  111. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  112. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  113. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  114. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/ui_console.py +0 -0
  115. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/ui_io.py +0 -0
  116. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/ui_log.py +0 -0
  117. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  118. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/ui_text.py +0 -0
  119. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uicore/ui_types.py +0 -0
  120. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  121. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  122. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  123. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  124. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  125. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  126. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  127. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  128. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  129. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  130. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  131. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/events/__init__.py +0 -0
  132. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/events/bus.py +0 -0
  133. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/events/event.py +0 -0
  134. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/spinner/__init__.py +0 -0
  135. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/spinner/spinner.py +0 -0
  136. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/spinner/spinner_color.py +0 -0
  137. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/tracing/__init__.py +0 -0
  138. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/tracing/context.py +0 -0
  139. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/tracing/span.py +0 -0
  140. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/tracing/tracer.py +0 -0
  141. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/translate.py +0 -0
  142. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/watcher/__init__.py +0 -0
  143. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/watcher/file_watcher.py +0 -0
  144. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/__init__.py +0 -0
  145. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/application.py +0 -0
  146. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/broker.py +0 -0
  147. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/app.js +0 -0
  148. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  149. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  150. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  151. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  152. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  153. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  154. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  155. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  156. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  157. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  158. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/inputbox.css +0 -0
  159. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/inputbox.js +0 -0
  160. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/style.css +0 -0
  161. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/input_box/voicegif.js +0 -0
  162. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/middleware.py +0 -0
  163. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/stats.py +0 -0
  164. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/media/web/utils.py +0 -0
  165. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/files/__init__.py +0 -0
  166. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/files/file_util.py +0 -0
  167. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/network/__init__.py +0 -0
  168. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/network/http_utils.py +0 -0
  169. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/plugin/__init__.py +0 -0
  170. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/plugin/base.py +0 -0
  171. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/plugin/discovery.py +0 -0
  172. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/plugin/registry.py +0 -0
  173. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/plugin/registry_ops.py +0 -0
  174. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/proxy/__init__.py +0 -0
  175. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/proxy/manager.py +0 -0
  176. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/runtime/__init__.py +0 -0
  177. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/runtime/collector.py +0 -0
  178. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/scheduler/__init__.py +0 -0
  179. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/scheduler/scheduler.py +0 -0
  180. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/sdk/__init__.py +0 -0
  181. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/plat/sdk/facade.py +0 -0
  182. {echotools-2.3.88 → echotools-2.3.90}/src/echotools/version.py +0 -0
  183. {echotools-2.3.88 → echotools-2.3.90}/src/echotools.egg-info/dependency_links.txt +0 -0
  184. {echotools-2.3.88 → echotools-2.3.90}/src/echotools.egg-info/requires.txt +0 -0
  185. {echotools-2.3.88 → echotools-2.3.90}/src/echotools.egg-info/top_level.txt +0 -0
  186. {echotools-2.3.88 → echotools-2.3.90}/src/tests/bench_import.py +0 -0
  187. {echotools-2.3.88 → echotools-2.3.90}/src/tests/conftest.py +0 -0
  188. {echotools-2.3.88 → echotools-2.3.90}/src/tests/fixtures/__init__.py +0 -0
  189. {echotools-2.3.88 → echotools-2.3.90}/src/tests/fixtures/simulated_fake_history_markup_responses.py +0 -0
  190. {echotools-2.3.88 → echotools-2.3.90}/src/tests/fixtures/simulated_fault_thinking_responses.py +0 -0
  191. {echotools-2.3.88 → echotools-2.3.90}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
  192. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_adversarial_stream_splits.py +0 -0
  193. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_cache_retry.py +0 -0
  194. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_completion.py +0 -0
  195. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_config.py +0 -0
  196. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_config_extended.py +0 -0
  197. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_console.py +0 -0
  198. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_coverage_core.py +0 -0
  199. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_dispatch.py +0 -0
  200. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_dispatch_extended.py +0 -0
  201. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_dispatch_race.py +0 -0
  202. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_agent_output_formats.py +0 -0
  203. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_parser_comprehensive.py +0 -0
  204. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_stream_json.py +0 -0
  205. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
  206. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_thinking_history.py +0 -0
  207. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_thinking_parse.py +0 -0
  208. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_tool_parse_leak.py +0 -0
  209. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_entml_tools.py +0 -0
  210. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_errors.py +0 -0
  211. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_errors_common.py +0 -0
  212. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_events.py +0 -0
  213. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_events_event.py +0 -0
  214. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_facade.py +0 -0
  215. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fake_history_markup_batch.py +0 -0
  216. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
  217. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
  218. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fake_history_markup_stream.py +0 -0
  219. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fault_thinking_close_batch.py +0 -0
  220. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fault_thinking_close_stream.py +0 -0
  221. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fncall.py +0 -0
  222. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_fncall_stream_thinking.py +0 -0
  223. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_history_markup_filter.py +0 -0
  224. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_ids.py +0 -0
  225. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_imports.py +0 -0
  226. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_io.py +0 -0
  227. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_keys.py +0 -0
  228. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_lifecycle.py +0 -0
  229. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_logger.py +0 -0
  230. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_merge.py +0 -0
  231. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_model_branch_matrix.py +0 -0
  232. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_plugin.py +0 -0
  233. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_printstream.py +0 -0
  234. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_protocols.py +0 -0
  235. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_proxy.py +0 -0
  236. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_proxy_selector.py +0 -0
  237. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_runtime.py +0 -0
  238. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_scheduler.py +0 -0
  239. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  240. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_stream_partial_json_parity.py +0 -0
  241. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_stream_realtime_parse.py +0 -0
  242. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_tracing.py +0 -0
  243. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_usage.py +0 -0
  244. {echotools-2.3.88 → echotools-2.3.90}/src/tests/test_web.py +0 -0
  245. {echotools-2.3.88 → echotools-2.3.90}/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.88
3
+ Version: 2.3.90
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.88"
7
+ version = "2.3.90"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -100,6 +100,11 @@ class FncallStreamParser(StreamFeedMixin, StreamDeltaMixin, StreamFinalMixin):
100
100
  self._schema_index = _build_param_schema_index(self._tools)
101
101
  return self._schema_index
102
102
 
103
+ def _get_known_tool_names(self):
104
+ from echotools.exec.fncall.protocols.entml_patterns import resolve_known_tool_names
105
+
106
+ return resolve_known_tool_names(self._tools, self._get_schema_index())
107
+
103
108
  def _emit_text(self, text: str) -> None:
104
109
  """将文本路由给思考过滤器(若启用)或直接追加到 _text_parts。"""
105
110
  if not text:
@@ -26,7 +26,7 @@ class StreamDeltaMixin:
26
26
 
27
27
  complete = list(INVOKE_RE.finditer(self._fncall_buf))
28
28
  complete_n = len(complete)
29
- parsed = split_invoke_open(self._fncall_buf)
29
+ parsed = split_invoke_open(self._fncall_buf, known_names=self._get_known_tool_names())
30
30
  if parsed is None:
31
31
  return max(0, complete_n - 1)
32
32
  _name, body_start = parsed
@@ -77,7 +77,7 @@ class StreamDeltaMixin:
77
77
  build_streaming_json_snapshot,
78
78
  split_invoke_open,
79
79
  )
80
- parsed = split_invoke_open(self._fncall_buf)
80
+ parsed = split_invoke_open(self._fncall_buf, known_names=self._get_known_tool_names())
81
81
  if not parsed:
82
82
  self._json_stream_encoder.reset()
83
83
  return
@@ -125,7 +125,7 @@ class StreamDeltaMixin:
125
125
  split_invoke_open,
126
126
  )
127
127
 
128
- parsed = split_invoke_open(self._fncall_buf)
128
+ parsed = split_invoke_open(self._fncall_buf, known_names=self._get_known_tool_names())
129
129
  if not parsed:
130
130
  self._json_stream_encoder.reset()
131
131
  return
@@ -172,7 +172,7 @@ class StreamDeltaMixin:
172
172
  split_invoke_open,
173
173
  )
174
174
 
175
- parsed = split_invoke_open(self._fncall_buf)
175
+ parsed = split_invoke_open(self._fncall_buf, known_names=self._get_known_tool_names())
176
176
  if not parsed:
177
177
  return None
178
178
  name, body_start = parsed
@@ -241,7 +241,7 @@ class StreamDeltaMixin:
241
241
  split_invoke_open,
242
242
  )
243
243
 
244
- parsed = split_invoke_open(self._fncall_buf)
244
+ parsed = split_invoke_open(self._fncall_buf, known_names=self._get_known_tool_names())
245
245
  if not parsed:
246
246
  return None
247
247
  name, body_start = parsed
@@ -37,7 +37,7 @@ class StreamFeedMixin:
37
37
  """True 表示已消费 text(含 holdback)。"""
38
38
  hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
39
39
  if hold_from_fn is not None:
40
- hold_from = hold_from_fn(text)
40
+ hold_from = hold_from_fn(text, tools=self._tools)
41
41
  if hold_from is not None:
42
42
  if hold_from > 0:
43
43
  self._append_content_text(text[:hold_from])
@@ -52,7 +52,7 @@ class StreamFeedMixin:
52
52
  def _hold_or_split_emit(self, text: str, trigger_tags) -> None:
53
53
  hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
54
54
  if hold_from_fn is not None:
55
- hold_from = hold_from_fn(text)
55
+ hold_from = hold_from_fn(text, tools=self._tools)
56
56
  if hold_from is not None:
57
57
  if hold_from > 0:
58
58
  self._emit_text(text[:hold_from])
@@ -81,7 +81,7 @@ class StreamFeedMixin:
81
81
  self._fncall_buf += text
82
82
  return
83
83
  trigger_tags = self._protocol.get_trigger_tags()
84
- found, pos = self._protocol.detect_start(text)
84
+ found, pos = self._protocol.detect_start(text, tools=self._tools)
85
85
  if found:
86
86
  if pos > 0:
87
87
  self._append_content_text(text[:pos])
@@ -90,7 +90,7 @@ class StreamFeedMixin:
90
90
  self._hold_or_split_content(text, trigger_tags)
91
91
  return
92
92
  trigger_tags = self._protocol.get_trigger_tags()
93
- found, pos = self._protocol.detect_start(text)
93
+ found, pos = self._protocol.detect_start(text, tools=self._tools)
94
94
  if not found:
95
95
  self._hold_or_split_content(text, trigger_tags)
96
96
  return
@@ -138,7 +138,7 @@ class StreamFeedMixin:
138
138
  self._feed_waiting_thinking_plain(combined)
139
139
  return
140
140
  trigger_tags = self._protocol.get_trigger_tags()
141
- found, pos = self._protocol.detect_start(combined)
141
+ found, pos = self._protocol.detect_start(combined, tools=self._tools)
142
142
  if found:
143
143
  if self._thinking_filter is not None:
144
144
  from echotools.exec.fncall.protocols.entml_think.parse import (
@@ -67,10 +67,18 @@ class StreamFinalMixin:
67
67
 
68
68
  lower = raw.lower()
69
69
  cut = len(raw)
70
- for marker in ("<entml:invoke", "<entml:function_calls"):
71
- pos = lower.find(marker)
72
- if pos >= 0:
73
- cut = min(cut, pos)
70
+ from echotools.exec.fncall.protocols.entml_patterns import (
71
+ find_actionable_entml_invoke_open,
72
+ )
73
+
74
+ invoke_pos = find_actionable_entml_invoke_open(
75
+ raw, known_names=self._get_known_tool_names()
76
+ )
77
+ if invoke_pos >= 0:
78
+ cut = min(cut, invoke_pos)
79
+ legacy_pos = lower.find("<entml:function_calls")
80
+ if legacy_pos >= 0:
81
+ cut = min(cut, legacy_pos)
74
82
  segment = raw[:cut]
75
83
 
76
84
  buf = stream_safe_visible_prefix(
@@ -128,12 +136,25 @@ class StreamFinalMixin:
128
136
  )
129
137
  clean_fn = getattr(self._protocol, "clean_tool_tags", None)
130
138
  if callable(clean_fn):
131
- clean_text = clean_fn(clean_text)
139
+ import inspect
140
+
141
+ sig = inspect.signature(clean_fn)
142
+ if "tools" in sig.parameters:
143
+ clean_text = clean_fn(clean_text, tools=self._tools)
144
+ else:
145
+ clean_text = clean_fn(clean_text)
132
146
  elif getattr(self._protocol, "id", None) == "entml":
133
147
  from echotools.exec.fncall.protocols.entml_patterns import (
148
+ resolve_known_tool_names,
134
149
  strip_tool_entml_residue,
135
150
  )
136
- clean_text = strip_tool_entml_residue(clean_text)
151
+
152
+ clean_text = strip_tool_entml_residue(
153
+ clean_text,
154
+ known_names=resolve_known_tool_names(
155
+ self._tools, self._get_schema_index()
156
+ ),
157
+ )
137
158
  display_text = self._finalize_display_text(clean_text)
138
159
  self._text_parts = [display_text] if display_text else []
139
160
  self._finalized_result = (display_text, tool_calls)
@@ -118,14 +118,24 @@ _HISTORY_CLARIFY_EN = (
118
118
  "The user's latest message follows below."
119
119
  )
120
120
 
121
- _HISTORY_TOOL_INVOKE_REMINDER_EN = (
122
- "IMPORTANT: Completed tool turns in conversation history are context only — "
123
- "never re-invoke them and never use the history `<tool>` block notation in "
124
- "your reply. To execute a tool in this turn, you must output a valid "
125
- "`<entml:invoke>` block exactly as specified in the tool instructions at "
126
- "the top of this prompt."
121
+ _TOOL_INVOKE_REMINDER_EN = (
122
+ "IMPORTANT — Tool invocation format for THIS reply (mandatory):\n"
123
+ "1. To execute a tool, output a literal `<entml:invoke name=\"TOOL_NAME\">` block "
124
+ "exactly as shown at the top of this prompt. The opening tag MUST close with `>`; "
125
+ "`TOOL_NAME` MUST be one of the available functions listed above (not a placeholder "
126
+ "or invented name).\n"
127
+ "2. Each argument goes in "
128
+ "`<entml:parameter name=\"PARAMETER_NAME\">VALUE</entml:parameter>` inside the invoke; "
129
+ "close with `</entml:invoke>`. String/scalar values as-is; lists and objects as JSON.\n"
130
+ "3. Do NOT copy `<tool>` blocks or `{ToolName: ...}` lines from conversation history — "
131
+ "those turns are already executed. History is context only; never re-invoke completed tools.\n"
132
+ "4. When mentioning `<entml:invoke>` in prose without executing, use backticks or otherwise "
133
+ "avoid a closed tag with a real tool name."
127
134
  )
128
135
 
136
+ # Back-compat alias
137
+ _HISTORY_TOOL_INVOKE_REMINDER_EN = _TOOL_INVOKE_REMINDER_EN
138
+
129
139
  _HISTORY_CLARIFY_ZH = (
130
140
  "以下是已完成的交互记录。"
131
141
  "此处展示的所有工具调用及其结果均已执行完毕,请勿重复调用。"
@@ -13,7 +13,7 @@ from typing import Any, Dict, List, Optional, Tuple
13
13
 
14
14
  from echotools.exec.fncall.prompt.templates import (
15
15
  _HISTORY_CLARIFY_EN,
16
- _HISTORY_TOOL_INVOKE_REMINDER_EN,
16
+ _TOOL_INVOKE_REMINDER_EN,
17
17
  )
18
18
  from echotools.exec.fncall.protocols.entml_invoke import (
19
19
  parse_entml_tool_calls,
@@ -25,7 +25,9 @@ from echotools.exec.fncall.protocols.entml_think.blocks import (
25
25
  from echotools.exec.fncall.protocols.entml_patterns import (
26
26
  entml_invoke_open_may_be_streaming,
27
27
  extract_attr_value,
28
+ find_actionable_entml_invoke_open,
28
29
  normalize_entml_name,
30
+ resolve_known_tool_names,
29
31
  strip_actionable_entml_invoke_blocks,
30
32
  strip_legacy_function_calls_wrapper,
31
33
  strip_tool_entml_residue,
@@ -192,8 +194,6 @@ class EntmlProtocol(ToolProtocol):
192
194
  sections.append(
193
195
  format_entml_conversation_history(history_text, _HISTORY_CLARIFY_EN)
194
196
  )
195
- if tool_descs and history_has_tool_calls:
196
- sections.append(_HISTORY_TOOL_INVOKE_REMINDER_EN)
197
197
 
198
198
  if loop_warning:
199
199
  sections.append(f"<loop_warning>\n{loop_warning}\n</loop_warning>")
@@ -206,6 +206,9 @@ class EntmlProtocol(ToolProtocol):
206
206
  if current_user_message is not None:
207
207
  sections.append(format_entml_current_user_message(current_user_message))
208
208
 
209
+ if tool_descs:
210
+ sections.append(_TOOL_INVOKE_REMINDER_EN)
211
+
209
212
  # thinking 放在最后,超限截断时优先保留在 send_text 尾部
210
213
  thinking_section = build_entml_thinking_section(
211
214
  protocol_options, has_tools=bool(tool_descs), history_text=history_text
@@ -215,16 +218,28 @@ class EntmlProtocol(ToolProtocol):
215
218
 
216
219
  return "\n\n".join(sections)
217
220
 
218
- def detect_start(self, buffer: str) -> Tuple[bool, int]:
219
- """invoke 开标签(含 name 且闭合 ``>``)稳定后才视为工具流开始。"""
220
- invoke_pos = self._find_complete_invoke_open(buffer)
221
+ def detect_start(
222
+ self,
223
+ buffer: str,
224
+ tools: Optional[List[Dict[str, Any]]] = None,
225
+ ) -> Tuple[bool, int]:
226
+ """invoke 开标签(闭合 ``>`` 且 name 在 tools 内)稳定后才视为工具流开始。"""
227
+ schema_index = _build_param_schema_index(tools) if tools else None
228
+ known = resolve_known_tool_names(tools, schema_index)
229
+ invoke_pos = find_actionable_entml_invoke_open(buffer, known_names=known)
221
230
  if invoke_pos < 0:
222
231
  return (False, -1)
223
232
  return (True, invoke_pos)
224
233
 
225
- def find_fncall_hold_from(self, buffer: str) -> Optional[int]:
234
+ def find_fncall_hold_from(
235
+ self,
236
+ buffer: str,
237
+ tools: Optional[List[Dict[str, Any]]] = None,
238
+ ) -> Optional[int]:
226
239
  """invoke 未稳定前 hold 前缀;legacy 外壳开标签未闭合时同样 hold。"""
227
- if self._find_complete_invoke_open(buffer) >= 0:
240
+ schema_index = _build_param_schema_index(tools) if tools else None
241
+ known = resolve_known_tool_names(tools, schema_index)
242
+ if find_actionable_entml_invoke_open(buffer, known_names=known) >= 0:
228
243
  return None
229
244
  hold: Optional[int] = None
230
245
  legacy_pos = buffer.find(self._LEGACY_WRAPPER_PREFIX)
@@ -235,28 +250,23 @@ class EntmlProtocol(ToolProtocol):
235
250
  invoke_pos = buffer.find(self._TRIGGER_PREFIX)
236
251
  if (
237
252
  invoke_pos >= 0
238
- and entml_invoke_open_may_be_streaming(buffer, invoke_pos)
253
+ and entml_invoke_open_may_be_streaming(
254
+ buffer, invoke_pos, known_names=known
255
+ )
239
256
  and (hold is None or invoke_pos < hold)
240
257
  ):
241
258
  hold = invoke_pos
242
259
  return hold
243
260
 
244
- def _find_complete_invoke_open(self, buffer: str) -> int:
245
- """返回首个含 name 且已闭合 ``>`` 的 ``<entml:invoke`` 起始下标;否则 -1。"""
246
- search_from = 0
247
- prefix_len = len(self._TRIGGER_PREFIX)
248
- while True:
249
- pos = buffer.find(self._TRIGGER_PREFIX, search_from)
250
- if pos < 0:
251
- return -1
252
- close = buffer.find(">", pos + prefix_len)
253
- if close < 0:
254
- return -1
255
- attrs = buffer[pos + prefix_len : close]
256
- name = extract_attr_value(attrs, "name")
257
- if name and normalize_entml_name(name):
258
- return pos
259
- search_from = close + 1
261
+ def _find_complete_invoke_open(
262
+ self,
263
+ buffer: str,
264
+ tools: Optional[List[Dict[str, Any]]] = None,
265
+ ) -> int:
266
+ """返回首个可解析 ``<entml:invoke`` 起始下标;否则 ``-1``。"""
267
+ schema_index = _build_param_schema_index(tools) if tools else None
268
+ known = resolve_known_tool_names(tools, schema_index)
269
+ return find_actionable_entml_invoke_open(buffer, known_names=known)
260
270
 
261
271
  def parse(
262
272
  self,
@@ -282,6 +292,8 @@ class EntmlProtocol(ToolProtocol):
282
292
  )
283
293
  if unclosed_open_at >= 0:
284
294
  parse_text = text[:unclosed_open_at]
295
+ schema_index = _build_param_schema_index(tools) if tools else None
296
+ known = resolve_known_tool_names(tools, schema_index)
285
297
  tool_calls = parse_entml_tool_calls(parse_text, tools, schema_index)
286
298
  tool_block_calls: List[Dict[str, Any]] = []
287
299
  if include_tool_blocks:
@@ -297,8 +309,8 @@ class EntmlProtocol(ToolProtocol):
297
309
  # 须在移除 invoke 之前剥离伪 history,且须晚于已解析的 ``<tool>`` 块剥离。
298
310
  clean, _ = strip_fake_history_markup(clean)
299
311
  if tool_calls:
300
- clean = strip_actionable_entml_invoke_blocks(clean)
301
- clean = strip_tool_entml_residue(clean)
312
+ clean = strip_actionable_entml_invoke_blocks(clean, known_names=known)
313
+ clean = strip_tool_entml_residue(clean, known_names=known)
302
314
  return (clean, normalize_tool_calls(tool_calls, tools))
303
315
 
304
316
  def parse_fragment(
@@ -313,9 +325,15 @@ class EntmlProtocol(ToolProtocol):
313
325
  """user 消息:仅去掉 ``entml:`` 前缀,不做其它剥离。"""
314
326
  return strip_entml_from_content(content)
315
327
 
316
- def clean_tool_tags(self, content: str) -> str:
328
+ def clean_tool_tags(
329
+ self,
330
+ content: str,
331
+ tools: Optional[List[Dict[str, Any]]] = None,
332
+ ) -> str:
317
333
  """仅剥离工具相关标签残留,保留 thinking;并移除伪 history 块。"""
318
- cleaned = strip_tool_entml_residue(content)
334
+ schema_index = _build_param_schema_index(tools) if tools else None
335
+ known = resolve_known_tool_names(tools, schema_index)
336
+ cleaned = strip_tool_entml_residue(content, known_names=known)
319
337
  cleaned, _ = strip_fake_history_markup(cleaned)
320
338
  return cleaned
321
339
 
@@ -15,6 +15,7 @@ from .entml_patterns import (
15
15
  iter_actionable_entml_invoke_blocks,
16
16
  normalize_entml_name,
17
17
  parse_sub_tags,
18
+ resolve_known_tool_names,
18
19
  split_mangled_json_param_tail,
19
20
  synthetic_close_invoke_body,
20
21
  )
@@ -143,7 +144,10 @@ def parse_entml_tool_calls(
143
144
  schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]],
144
145
  ) -> List[Dict[str, Any]]:
145
146
  tool_calls: List[Dict[str, Any]] = []
146
- for _start, _end, attrs, body in iter_actionable_entml_invoke_blocks(text):
147
+ known = resolve_known_tool_names(tools, schema_index)
148
+ for _start, _end, attrs, body in iter_actionable_entml_invoke_blocks(
149
+ text, known_names=known
150
+ ):
147
151
  name = extract_attr_value(attrs, "name")
148
152
  if not name:
149
153
  continue
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import re
5
- from typing import Any, Dict, Iterator, List, Optional, Tuple
5
+ from typing import Any, Dict, Iterator, List, Optional, Set, Tuple
6
6
 
7
7
  from .entml_schema import coerce_entml_parameter_value
8
8
 
@@ -116,8 +116,37 @@ def is_placeholder_invoke_name(name: str) -> bool:
116
116
  return not n or "$" in n
117
117
 
118
118
 
119
- def entml_invoke_open_is_actionable(buffer: str, pos: int) -> bool:
120
- """``pos`` 处 ``<entml:invoke`` 是否为已闭合且含真实 name 的工具开标签。"""
119
+ def resolve_known_tool_names(
120
+ tools: Optional[List[Dict[str, Any]]] = None,
121
+ schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]] = None,
122
+ ) -> Optional[Set[str]]:
123
+ """从 tools / schema_index 解析已知工具名;无约束时返回 ``None``。"""
124
+ if schema_index:
125
+ return {normalize_entml_name(str(k)) for k in schema_index.keys()}
126
+ names: Set[str] = set()
127
+ for tool in tools or []:
128
+ fn = tool.get("function") or {}
129
+ name = fn.get("name")
130
+ if name:
131
+ names.add(normalize_entml_name(str(name)))
132
+ return names if names else None
133
+
134
+
135
+ def _name_is_known(name: str, known_names: Optional[Set[str]]) -> bool:
136
+ if known_names is None:
137
+ return True
138
+ if not known_names:
139
+ return False
140
+ return normalize_entml_name(name) in known_names
141
+
142
+
143
+ def entml_invoke_open_is_actionable(
144
+ buffer: str,
145
+ pos: int,
146
+ *,
147
+ known_names: Optional[Set[str]] = None,
148
+ ) -> bool:
149
+ """``pos`` 处 ``<entml:invoke`` 是否为已闭合、含真实 name 且(若提供)在已知工具表内的开标签。"""
121
150
  if pos < 0 or not buffer.startswith(_INVOKE_OPEN_PREFIX, pos):
122
151
  return False
123
152
  gt = buffer.find(">", pos + len(_INVOKE_OPEN_PREFIX))
@@ -128,29 +157,60 @@ def entml_invoke_open_is_actionable(buffer: str, pos: int) -> bool:
128
157
  if not name:
129
158
  return False
130
159
  name = normalize_entml_name(name)
131
- return bool(name) and not is_placeholder_invoke_name(name)
160
+ if not name or is_placeholder_invoke_name(name):
161
+ return False
162
+ return _name_is_known(name, known_names)
132
163
 
133
164
 
134
- def entml_invoke_open_may_be_streaming(buffer: str, pos: int) -> bool:
135
- """``pos`` 处 ``<entml:invoke`` 是否仍可能长成真实工具开标签(非 prose 提及)。"""
165
+ def entml_invoke_open_may_be_streaming(
166
+ buffer: str,
167
+ pos: int,
168
+ *,
169
+ known_names: Optional[Set[str]] = None,
170
+ ) -> bool:
171
+ """``pos`` 处 ``<entml:invoke`` 是否仍可能长成可解析工具开标签(非 prose 提及)。"""
136
172
  if pos < 0 or not buffer.startswith(_INVOKE_OPEN_PREFIX, pos):
137
173
  return False
138
- if entml_invoke_open_is_actionable(buffer, pos):
139
- return True
140
174
  gt = buffer.find(">", pos + len(_INVOKE_OPEN_PREFIX))
141
175
  if gt < 0:
142
176
  return True
143
- return False
177
+ return entml_invoke_open_is_actionable(buffer, pos, known_names=known_names)
178
+
179
+
180
+ def find_actionable_entml_invoke_open(
181
+ buffer: str,
182
+ *,
183
+ known_names: Optional[Set[str]] = None,
184
+ ) -> int:
185
+ """返回首个可解析 ``<entml:invoke`` 起始下标;否则 ``-1``。"""
186
+ if not buffer:
187
+ return -1
188
+ search_from = 0
189
+ prefix_len = len(_INVOKE_OPEN_PREFIX)
190
+ while search_from < len(buffer):
191
+ pos = buffer.find(_INVOKE_OPEN_PREFIX, search_from)
192
+ if pos < 0:
193
+ return -1
194
+ if entml_invoke_open_is_actionable(buffer, pos, known_names=known_names):
195
+ return pos
196
+ gt = buffer.find(">", pos + prefix_len)
197
+ search_from = (gt + 1) if gt >= 0 else (pos + prefix_len)
198
+ return -1
144
199
 
145
200
 
146
- def _actionable_invoke_open_before_close(content: str, close_pos: int) -> bool:
201
+ def _actionable_invoke_open_before_close(
202
+ content: str,
203
+ close_pos: int,
204
+ *,
205
+ known_names: Optional[Set[str]] = None,
206
+ ) -> bool:
147
207
  """``close_pos`` 处的 ``</entml:invoke>`` 是否闭合带真实 name 的开标签。"""
148
208
  search = close_pos - 1
149
209
  while search >= 0:
150
210
  pos = content.rfind(_INVOKE_OPEN_PREFIX, 0, search + 1)
151
211
  if pos < 0:
152
212
  return False
153
- if entml_invoke_open_is_actionable(content, pos):
213
+ if entml_invoke_open_is_actionable(content, pos, known_names=known_names):
154
214
  gt = content.find(">", pos)
155
215
  if gt < 0 or gt >= close_pos:
156
216
  search = pos - 1
@@ -164,17 +224,26 @@ def _actionable_invoke_open_before_close(content: str, close_pos: int) -> bool:
164
224
  return False
165
225
 
166
226
 
167
- def _invoke_close_should_strip(content: str, close_pos: int) -> bool:
227
+ def _invoke_close_should_strip(
228
+ content: str,
229
+ close_pos: int,
230
+ *,
231
+ known_names: Optional[Set[str]] = None,
232
+ ) -> bool:
168
233
  """是否应剥离 ``</entml:invoke>``(孤儿噪声或真实工具块残留)。"""
169
- if _actionable_invoke_open_before_close(content, close_pos):
234
+ if _actionable_invoke_open_before_close(content, close_pos, known_names=known_names):
170
235
  return True
171
236
  open_pos = content.rfind(_INVOKE_OPEN_PREFIX, 0, close_pos)
172
237
  if open_pos < 0:
173
238
  return True
174
- return entml_invoke_open_is_actionable(content, open_pos)
239
+ return entml_invoke_open_is_actionable(content, open_pos, known_names=known_names)
175
240
 
176
241
 
177
- def iter_actionable_entml_invoke_blocks(text: str) -> Iterator[Tuple[int, int, str, str]]:
242
+ def iter_actionable_entml_invoke_blocks(
243
+ text: str,
244
+ *,
245
+ known_names: Optional[Set[str]] = None,
246
+ ) -> Iterator[Tuple[int, int, str, str]]:
178
247
  """迭代含真实 ``name`` 的完整 ``<entml:invoke>…</entml:invoke>`` 块。"""
179
248
  if not text:
180
249
  return
@@ -185,8 +254,9 @@ def iter_actionable_entml_invoke_blocks(text: str) -> Iterator[Tuple[int, int, s
185
254
  pos = text.find(_INVOKE_OPEN_PREFIX, search_from)
186
255
  if pos < 0:
187
256
  break
188
- if not entml_invoke_open_is_actionable(text, pos):
189
- search_from = pos + prefix_len
257
+ if not entml_invoke_open_is_actionable(text, pos, known_names=known_names):
258
+ gt = text.find(">", pos + prefix_len)
259
+ search_from = (gt + 1) if gt >= 0 else (pos + prefix_len)
190
260
  continue
191
261
  gt = text.find(">", pos + prefix_len)
192
262
  if gt < 0:
@@ -200,30 +270,40 @@ def iter_actionable_entml_invoke_blocks(text: str) -> Iterator[Tuple[int, int, s
200
270
  search_from = close + close_len
201
271
 
202
272
 
203
- def strip_actionable_entml_invoke_blocks(text: str) -> str:
273
+ def strip_actionable_entml_invoke_blocks(
274
+ text: str,
275
+ *,
276
+ known_names: Optional[Set[str]] = None,
277
+ ) -> str:
204
278
  """仅剥离含真实 ``name`` 的 invoke 块;保留 prose ``<entml:invoke>`` 提及。"""
205
279
  if not text:
206
280
  return text
207
281
  parts: List[str] = []
208
282
  last = 0
209
- for start, end, _attrs, _body in iter_actionable_entml_invoke_blocks(text):
283
+ for start, end, _attrs, _body in iter_actionable_entml_invoke_blocks(
284
+ text, known_names=known_names
285
+ ):
210
286
  parts.append(text[last:start])
211
287
  last = end
212
288
  parts.append(text[last:])
213
289
  return "".join(parts)
214
290
 
215
291
 
216
- def _strip_orphan_invoke_tags(content: str) -> str:
292
+ def _strip_orphan_invoke_tags(
293
+ content: str,
294
+ *,
295
+ known_names: Optional[Set[str]] = None,
296
+ ) -> str:
217
297
  """仅剥离带真实 name 的 invoke 孤儿标签;保留 prose 中的 ``<entml:invoke>`` 提及。"""
218
298
  pattern = re.compile(r"</?entml:invoke\b[^>]*/?>", re.DOTALL)
219
299
 
220
300
  def repl(match: re.Match[str]) -> str:
221
301
  tag = match.group(0)
222
302
  if tag.startswith("</"):
223
- if _invoke_close_should_strip(content, match.start()):
303
+ if _invoke_close_should_strip(content, match.start(), known_names=known_names):
224
304
  return ""
225
305
  return tag
226
- if entml_invoke_open_is_actionable(content, match.start()):
306
+ if entml_invoke_open_is_actionable(content, match.start(), known_names=known_names):
227
307
  return ""
228
308
  return tag
229
309
 
@@ -446,13 +526,17 @@ def strip_legacy_function_calls_wrapper(text: str) -> str:
446
526
  return out
447
527
 
448
528
 
449
- def strip_tool_entml_residue(content: str) -> str:
529
+ def strip_tool_entml_residue(
530
+ content: str,
531
+ *,
532
+ known_names: Optional[Set[str]] = None,
533
+ ) -> str:
450
534
  """剥离工具相关 entml 标签残留,保留 thinking 与非工具 prose 提及。"""
451
535
  if not content:
452
536
  return content
453
537
  cleaned = _TOOL_WRAPPER_PAIR_RE.sub("", content)
454
- cleaned = strip_actionable_entml_invoke_blocks(cleaned)
455
- cleaned = _strip_orphan_invoke_tags(cleaned)
538
+ cleaned = strip_actionable_entml_invoke_blocks(cleaned, known_names=known_names)
539
+ cleaned = _strip_orphan_invoke_tags(cleaned, known_names=known_names)
456
540
  cleaned = _strip_orphan_non_invoke_tool_tags(cleaned)
457
541
  cleaned = _EMPTY_FENCE_RE.sub("", cleaned)
458
542
  cleaned = _FENCE_ONLY_LINE_RE.sub("", cleaned)
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import re
5
- from typing import Any, Dict, List, Optional, Tuple
5
+ from typing import Any, Dict, List, Optional, Set, Tuple
6
6
 
7
7
  from echotools.exec.fncall.protocols.entml_patterns import (
8
8
  BARE_INVOKE_CHILD_OPEN_RE,
@@ -59,7 +59,11 @@ def _strip_incomplete_child_close_suffix(value: str, key: str) -> str:
59
59
  return value
60
60
 
61
61
 
62
- def split_invoke_open(buffer: str) -> Optional[Tuple[str, int]]:
62
+ def split_invoke_open(
63
+ buffer: str,
64
+ *,
65
+ known_names: Optional[Set[str]] = None,
66
+ ) -> Optional[Tuple[str, int]]:
63
67
  """返回当前正在流式写入的 invoke(最后一个有效开标签)。"""
64
68
  search_from = 0
65
69
  prefix_len = len(_INVOKE_OPEN_PREFIX)
@@ -68,6 +72,14 @@ def split_invoke_open(buffer: str) -> Optional[Tuple[str, int]]:
68
72
  pos = buffer.find(_INVOKE_OPEN_PREFIX, search_from)
69
73
  if pos < 0:
70
74
  break
75
+ from echotools.exec.fncall.protocols.entml_patterns import (
76
+ entml_invoke_open_is_actionable,
77
+ )
78
+
79
+ if not entml_invoke_open_is_actionable(buffer, pos, known_names=known_names):
80
+ gt = buffer.find(">", pos + prefix_len)
81
+ search_from = (gt + 1) if gt >= 0 else (pos + prefix_len)
82
+ continue
71
83
  gt = buffer.find(">", pos + prefix_len)
72
84
  if gt < 0:
73
85
  break