echotools 2.3.86__tar.gz → 2.3.88__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 (249) hide show
  1. {echotools-2.3.86 → echotools-2.3.88}/PKG-INFO +1 -1
  2. {echotools-2.3.86 → echotools-2.3.88}/pyproject.toml +2 -2
  3. echotools-2.3.88/src/echotools/exec/fncall/parsers/stream.py +196 -0
  4. echotools-2.3.88/src/echotools/exec/fncall/parsers/stream_delta.py +269 -0
  5. echotools-2.3.88/src/echotools/exec/fncall/parsers/stream_feed.py +167 -0
  6. echotools-2.3.88/src/echotools/exec/fncall/parsers/stream_final.py +217 -0
  7. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/prompt/inject.py +30 -19
  8. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/entml.py +2 -2
  9. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/entml_invoke.py +56 -27
  10. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/entml_patterns.py +85 -82
  11. echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_schema.py +392 -0
  12. echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_stream/__init__.py +22 -0
  13. echotools-2.3.86/src/echotools/exec/fncall/protocols/entml_stream_json.py → echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_stream/body.py +12 -191
  14. echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_stream/snap.py +175 -0
  15. echotools-2.3.86/src/echotools/exec/fncall/protocols/entml_tool_blocks.py → echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_think/blocks.py +54 -3
  16. echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_think/filter.py +303 -0
  17. echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_think/hold.py +321 -0
  18. echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_think/parse.py +265 -0
  19. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/history_markup.py +76 -78
  20. {echotools-2.3.86 → echotools-2.3.88}/src/echotools.egg-info/PKG-INFO +1 -1
  21. {echotools-2.3.86 → echotools-2.3.88}/src/echotools.egg-info/SOURCES.txt +10 -5
  22. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_batch_stream_comprehensive.py +4 -1
  23. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_protocol.py +181 -3
  24. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_stream_json.py +1 -1
  25. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_stream_json_exhaustive.py +1 -1
  26. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_tool_pipeline.py +3 -3
  27. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_tools.py +1 -1
  28. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_model_branch_matrix.py +1 -1
  29. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_stream_partial_json_parity.py +2 -2
  30. echotools-2.3.86/README.md +0 -3
  31. echotools-2.3.86/src/echotools/exec/fncall/parsers/stream.py +0 -853
  32. echotools-2.3.86/src/echotools/exec/fncall/protocols/entml_think/parse.py +0 -755
  33. echotools-2.3.86/src/echotools/exec/fncall/protocols/entml_tools.py +0 -156
  34. echotools-2.3.86/src/echotools/exec/fncall/protocols/entml_values.py +0 -131
  35. {echotools-2.3.86/docs → echotools-2.3.88}/README.md +0 -0
  36. {echotools-2.3.86 → echotools-2.3.88}/setup.cfg +0 -0
  37. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/__init__.py +0 -0
  38. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/cache/__init__.py +0 -0
  39. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/cache/list_cache.py +0 -0
  40. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/cache/memory_cache.py +0 -0
  41. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/__init__.py +0 -0
  42. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/base.py +0 -0
  43. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/center.py +0 -0
  44. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/center_notify.py +0 -0
  45. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/center_template.py +0 -0
  46. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/loader.py +0 -0
  47. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/merge.py +0 -0
  48. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/errors/__init__.py +0 -0
  49. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/errors/base.py +0 -0
  50. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/errors/classify.py +0 -0
  51. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/errors/common.py +0 -0
  52. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/errors/http.py +0 -0
  53. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/ids/__init__.py +0 -0
  54. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/ids/generator.py +0 -0
  55. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/io/__init__.py +0 -0
  56. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/io/io_utils.py +0 -0
  57. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/io/printstream.py +0 -0
  58. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/logger/__init__.py +0 -0
  59. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/logger/manager.py +0 -0
  60. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/retry/__init__.py +0 -0
  61. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/retry/retry.py +0 -0
  62. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/compat.py +0 -0
  63. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/__init__.py +0 -0
  64. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/candidate.py +0 -0
  65. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  66. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  67. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/race.py +0 -0
  68. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/selector.py +0 -0
  69. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/usage.py +0 -0
  70. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/__init__.py +0 -0
  71. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  72. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  73. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  74. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  75. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/prompt/history.py +0 -0
  76. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  77. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  78. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  79. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  80. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  81. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
  82. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  83. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/registry.py +0 -0
  84. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  85. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  86. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  87. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  88. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  89. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  90. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/keys.py +0 -0
  91. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/lifecycle/__init__.py +0 -0
  92. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/lifecycle/manager.py +0 -0
  93. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/lifecycle/updater.py +0 -0
  94. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  95. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/process/__init__.py +0 -0
  96. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/process/port.py +0 -0
  97. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/protocol/__init__.py +0 -0
  98. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/protocol/base.py +0 -0
  99. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/__init__.py +0 -0
  100. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/conpty.py +0 -0
  101. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/local/__init__.py +0 -0
  102. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/local/core.py +0 -0
  103. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/local/proc.py +0 -0
  104. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/local/unix.py +0 -0
  105. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/local/win.py +0 -0
  106. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/sanitize.py +0 -0
  107. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/session/__init__.py +0 -0
  108. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/session/base.py +0 -0
  109. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/session/client.py +0 -0
  110. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/ssh.py +0 -0
  111. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/tmux.py +0 -0
  112. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/__init__.py +0 -0
  113. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/charmap.py +0 -0
  114. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/spinner.py +0 -0
  115. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/ui.py +0 -0
  116. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  117. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  118. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  119. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  120. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  121. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  122. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/ui_console.py +0 -0
  123. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/ui_io.py +0 -0
  124. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/ui_log.py +0 -0
  125. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  126. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/ui_text.py +0 -0
  127. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/ui_types.py +0 -0
  128. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  129. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  130. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  131. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  132. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  133. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  134. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  135. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  136. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  137. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  138. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  139. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/events/__init__.py +0 -0
  140. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/events/bus.py +0 -0
  141. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/events/event.py +0 -0
  142. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/spinner/__init__.py +0 -0
  143. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/spinner/spinner.py +0 -0
  144. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/spinner/spinner_color.py +0 -0
  145. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/tracing/__init__.py +0 -0
  146. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/tracing/context.py +0 -0
  147. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/tracing/span.py +0 -0
  148. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/tracing/tracer.py +0 -0
  149. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/translate.py +0 -0
  150. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/watcher/__init__.py +0 -0
  151. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/watcher/file_watcher.py +0 -0
  152. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/__init__.py +0 -0
  153. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/application.py +0 -0
  154. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/broker.py +0 -0
  155. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/app.js +0 -0
  156. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  157. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  158. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  159. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  160. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  161. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  162. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  163. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  164. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  165. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  166. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/inputbox.css +0 -0
  167. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/inputbox.js +0 -0
  168. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/style.css +0 -0
  169. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/voicegif.js +0 -0
  170. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/middleware.py +0 -0
  171. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/stats.py +0 -0
  172. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/utils.py +0 -0
  173. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/files/__init__.py +0 -0
  174. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/files/file_util.py +0 -0
  175. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/network/__init__.py +0 -0
  176. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/network/http_utils.py +0 -0
  177. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/plugin/__init__.py +0 -0
  178. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/plugin/base.py +0 -0
  179. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/plugin/discovery.py +0 -0
  180. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/plugin/registry.py +0 -0
  181. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/plugin/registry_ops.py +0 -0
  182. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/proxy/__init__.py +0 -0
  183. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/proxy/manager.py +0 -0
  184. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/runtime/__init__.py +0 -0
  185. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/runtime/collector.py +0 -0
  186. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/scheduler/__init__.py +0 -0
  187. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/scheduler/scheduler.py +0 -0
  188. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/sdk/__init__.py +0 -0
  189. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/sdk/facade.py +0 -0
  190. {echotools-2.3.86 → echotools-2.3.88}/src/echotools/version.py +0 -0
  191. {echotools-2.3.86 → echotools-2.3.88}/src/echotools.egg-info/dependency_links.txt +0 -0
  192. {echotools-2.3.86 → echotools-2.3.88}/src/echotools.egg-info/requires.txt +0 -0
  193. {echotools-2.3.86 → echotools-2.3.88}/src/echotools.egg-info/top_level.txt +0 -0
  194. {echotools-2.3.86 → echotools-2.3.88}/src/tests/bench_import.py +0 -0
  195. {echotools-2.3.86 → echotools-2.3.88}/src/tests/conftest.py +0 -0
  196. {echotools-2.3.86 → echotools-2.3.88}/src/tests/fixtures/__init__.py +0 -0
  197. {echotools-2.3.86 → echotools-2.3.88}/src/tests/fixtures/simulated_fake_history_markup_responses.py +0 -0
  198. {echotools-2.3.86 → echotools-2.3.88}/src/tests/fixtures/simulated_fault_thinking_responses.py +0 -0
  199. {echotools-2.3.86 → echotools-2.3.88}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
  200. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_adversarial_stream_splits.py +0 -0
  201. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_cache_retry.py +0 -0
  202. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_completion.py +0 -0
  203. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_config.py +0 -0
  204. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_config_extended.py +0 -0
  205. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_console.py +0 -0
  206. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_coverage_core.py +0 -0
  207. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_dispatch.py +0 -0
  208. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_dispatch_extended.py +0 -0
  209. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_dispatch_race.py +0 -0
  210. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_agent_output_formats.py +0 -0
  211. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_bare_invoke_format.py +0 -0
  212. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_parser_comprehensive.py +0 -0
  213. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_thinking_history.py +0 -0
  214. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_thinking_parse.py +0 -0
  215. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_tool_parse_leak.py +0 -0
  216. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_errors.py +0 -0
  217. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_errors_common.py +0 -0
  218. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_events.py +0 -0
  219. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_events_event.py +0 -0
  220. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_facade.py +0 -0
  221. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fake_history_markup_batch.py +0 -0
  222. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
  223. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
  224. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fake_history_markup_stream.py +0 -0
  225. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fault_thinking_close_batch.py +0 -0
  226. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fault_thinking_close_stream.py +0 -0
  227. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fncall.py +0 -0
  228. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fncall_stream_thinking.py +0 -0
  229. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_history_markup_filter.py +0 -0
  230. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_ids.py +0 -0
  231. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_imports.py +0 -0
  232. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_io.py +0 -0
  233. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_keys.py +0 -0
  234. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_lifecycle.py +0 -0
  235. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_logger.py +0 -0
  236. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_merge.py +0 -0
  237. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_plugin.py +0 -0
  238. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_printstream.py +0 -0
  239. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_protocols.py +0 -0
  240. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_proxy.py +0 -0
  241. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_proxy_selector.py +0 -0
  242. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_runtime.py +0 -0
  243. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_scheduler.py +0 -0
  244. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  245. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_stream_realtime_parse.py +0 -0
  246. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_tracing.py +0 -0
  247. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_usage.py +0 -0
  248. {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_web.py +0 -0
  249. {echotools-2.3.86 → echotools-2.3.88}/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.86
3
+ Version: 2.3.88
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echotools"
7
- version = "2.3.86"
7
+ version = "2.3.88"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
- readme = "docs/README.md"
9
+ readme = "README.md"
10
10
  requires-python = ">=3.8"
11
11
  license = { text = "MIT" }
12
12
  authors = [{ name = "nichengfuben" }]
@@ -0,0 +1,196 @@
1
+ """流式 fncall 检测状态机(协议感知版本)。
2
+
3
+ 从 src/core/tools.py 迁移并改造为协议感知。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections import deque
9
+ from typing import Any, Deque, Dict, List, Optional, Tuple
10
+
11
+ from echotools.exec.protocol.base import ToolProtocol
12
+
13
+
14
+ # 懒导入:仅 entml 协议需要
15
+ def _make_thinking_filter(protocol: ToolProtocol, *, thinking_enabled: bool = True):
16
+ if not thinking_enabled:
17
+ return None
18
+ if getattr(protocol, "id", None) == "entml":
19
+ from echotools.exec.fncall.protocols.entml_think.parse import (
20
+ EntmlThinkingStreamFilter,
21
+ )
22
+ return EntmlThinkingStreamFilter(thinking_enabled=thinking_enabled)
23
+ return None
24
+
25
+
26
+ from echotools.exec.fncall.parsers.stream_feed import StreamFeedMixin
27
+ from echotools.exec.fncall.parsers.stream_delta import StreamDeltaMixin
28
+ from echotools.exec.fncall.parsers.stream_final import StreamFinalMixin
29
+
30
+
31
+ class FncallStreamParser(StreamFeedMixin, StreamDeltaMixin, StreamFinalMixin):
32
+ """协议感知的流式 fncall 检测与解析状态机。
33
+
34
+ 用法::
35
+
36
+ protocol = get_protocol("xml")
37
+ parser = FncallStreamParser(protocol=protocol, tools=tools)
38
+ parser.feed(chunk)
39
+ clean_text, tool_calls = parser.finalize()
40
+ """
41
+
42
+ WAITING_FOR_TAG = "WAITING_FOR_TAG"
43
+ IN_FUNCTION_CALLS = "IN_FUNCTION_CALLS"
44
+ DONE = "DONE"
45
+
46
+ def _init_end_tags(self, protocol: ToolProtocol) -> None:
47
+ # 1 声明非空结束标记 / 2 声明空列表禁自动关闭 / 3 从 trigger 推断
48
+ self._end_tags: List[str] = []
49
+ self._no_auto_close: bool = False
50
+ if hasattr(protocol, "get_stream_end_tags"):
51
+ declared = list(protocol.get_stream_end_tags())
52
+ if declared:
53
+ self._end_tags = declared
54
+ else:
55
+ self._no_auto_close = True
56
+ return
57
+ for tag in protocol.get_trigger_tags():
58
+ if tag.startswith("<") and not tag.startswith("</"):
59
+ tag_name = tag.lstrip("<").split(">")[0].split()[0]
60
+ end = f"</{tag_name}>"
61
+ if end != tag.replace("<", "</"):
62
+ self._end_tags.append(end)
63
+ elif tag.startswith("[") and not tag.startswith("[/"):
64
+ inner = tag.lstrip("[").split("]")[0]
65
+ self._end_tags.append(f"[/{inner}]")
66
+
67
+ def __init__(
68
+ self,
69
+ protocol: ToolProtocol,
70
+ tools: Optional[List[Dict[str, Any]]] = None,
71
+ protocol_options: Optional[Dict[str, Any]] = None,
72
+ ) -> None:
73
+ self._protocol = protocol
74
+ self._tools = tools
75
+ from echotools.exec.fncall.protocols.entml_think.core import is_thinking_enabled
76
+
77
+ self._thinking_enabled = is_thinking_enabled(protocol_options)
78
+ self._raw_buf: str = ""
79
+ self._text_parts: List[str] = []
80
+ self._waiting_tail: str = ""
81
+ self._fncall_buf: str = ""
82
+ self._detected: bool = False
83
+ self._state: str = self.WAITING_FOR_TAG
84
+ self._finalized_result: Optional[Tuple[str, List[Dict[str, Any]]]] = None
85
+ self._thinking_parts: List[str] = []
86
+ self._thinking_filter = _make_thinking_filter(
87
+ protocol, thinking_enabled=self._thinking_enabled
88
+ )
89
+ self._emitted_invoke_count: int = 0
90
+ self._json_stream_encoder = None
91
+ self._pending_stream_deltas: Deque[Tuple[str, str, int]] = deque()
92
+ self._stream_invoke_emitted: List[str] = []
93
+ self._schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]] = None
94
+ self._init_end_tags(protocol)
95
+
96
+ def _get_schema_index(self) -> Optional[Dict[str, Dict[str, Dict[str, Any]]]]:
97
+ if self._schema_index is None and self._tools:
98
+ from echotools.exec.fncall.shared.coercion import _build_param_schema_index
99
+
100
+ self._schema_index = _build_param_schema_index(self._tools)
101
+ return self._schema_index
102
+
103
+ def _emit_text(self, text: str) -> None:
104
+ """将文本路由给思考过滤器(若启用)或直接追加到 _text_parts。"""
105
+ if not text:
106
+ return
107
+ if self._thinking_filter is not None:
108
+ for kind, part in self._thinking_filter.feed(text):
109
+ if kind == "thinking":
110
+ self._thinking_parts.append(part)
111
+ else:
112
+ self._append_content_text(part)
113
+ else:
114
+ self._append_content_text(text)
115
+
116
+ def _trim_trailing_visible_whitespace(self) -> None:
117
+ """invoke 开始前去掉尾部空白可见段(thinking-only 回复常见 ``\\n\\n``)。"""
118
+ while self._text_parts and not self._text_parts[-1].strip():
119
+ self._text_parts.pop()
120
+ if self._text_parts:
121
+ trimmed = self._text_parts[-1].rstrip()
122
+ if trimmed:
123
+ self._text_parts[-1] = trimmed
124
+ else:
125
+ self._text_parts.pop()
126
+
127
+ @staticmethod
128
+ def _split_safe_text(
129
+ buffer: str,
130
+ tags: List[str],
131
+ ) -> Tuple[str, str]:
132
+ """将 buffer 分为「可安全输出的前缀」和「需保留的尾部」。
133
+
134
+ 除了「尾部是 trigger 的真前缀」外,还处理:
135
+ - trigger 已完整出现但尚未见到闭合 ``>``(例如 ``<entml:invoke name="x"``)
136
+ - trigger 声明带 ``>`` 时,其去尾 ``>`` 形式同样参与 holdback
137
+ """
138
+ if not buffer:
139
+ return "", ""
140
+ if not tags:
141
+ return buffer, ""
142
+
143
+ bases: List[str] = []
144
+ for tag in tags:
145
+ if not tag:
146
+ continue
147
+ bases.append(tag)
148
+ if tag.endswith(">") and len(tag) > 1:
149
+ bases.append(tag[:-1])
150
+
151
+ # 已匹配完整 base 但属性/闭合 ``>`` 尚未到齐:从 base 起点整体 hold
152
+ hold_from: Optional[int] = None
153
+ for base in bases:
154
+ pos = buffer.find(base)
155
+ if pos < 0:
156
+ continue
157
+ if ">" not in buffer[pos:]:
158
+ if hold_from is None or pos < hold_from:
159
+ hold_from = pos
160
+ if hold_from is not None:
161
+ return buffer[:hold_from], buffer[hold_from:]
162
+
163
+ max_keep = max(len(t) - 1 for t in bases)
164
+ check_len = min(len(buffer), max_keep)
165
+
166
+ for length in range(check_len, 0, -1):
167
+ suffix = buffer[-length:]
168
+ if any(tag.startswith(suffix) and suffix != tag for tag in bases):
169
+ return buffer[:-length], buffer[-length:]
170
+
171
+ return buffer, ""
172
+
173
+ def _append_content_text(self, text: str) -> None:
174
+ if not text:
175
+ return
176
+ from echotools.exec.fncall.protocols.entml_think.parse import (
177
+ _THINKING_CLOSE,
178
+ strip_orphan_thinking_close_prefix,
179
+ )
180
+
181
+ remainder = strip_orphan_thinking_close_prefix(text)
182
+ while True:
183
+ stripped = remainder.strip()
184
+ if not stripped:
185
+ return
186
+ if stripped == _THINKING_CLOSE:
187
+ return
188
+ if _THINKING_CLOSE in remainder:
189
+ before, _, after = remainder.partition(_THINKING_CLOSE)
190
+ if before.strip():
191
+ self._text_parts.append(before)
192
+ remainder = after
193
+ continue
194
+ self._text_parts.append(remainder)
195
+ return
196
+
@@ -0,0 +1,269 @@
1
+ """FncallStreamParser streaming delta helpers."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Dict, List, Optional, Tuple
5
+
6
+
7
+ class StreamDeltaMixin:
8
+ def _is_call_closed(self) -> bool:
9
+ """检测 fncall 缓冲区中是否包含结束标记。"""
10
+ if self._no_auto_close:
11
+ return False
12
+ buf = self._fncall_buf
13
+ for end_tag in self._end_tags:
14
+ if end_tag in buf:
15
+ return True
16
+ return False
17
+
18
+ def _stream_active_invoke_slot(self) -> int:
19
+ """当前正在流式编码的 invoke 序号(0-based)。"""
20
+ from echotools.exec.fncall.protocols.entml_patterns import INVOKE_RE
21
+ from echotools.exec.fncall.protocols.entml_stream import (
22
+ _INVOKE_CLOSE,
23
+ _INVOKE_OPEN_PREFIX,
24
+ split_invoke_open,
25
+ )
26
+
27
+ complete = list(INVOKE_RE.finditer(self._fncall_buf))
28
+ complete_n = len(complete)
29
+ parsed = split_invoke_open(self._fncall_buf)
30
+ if parsed is None:
31
+ return max(0, complete_n - 1)
32
+ _name, body_start = parsed
33
+ body = self._fncall_buf[body_start:]
34
+ if _INVOKE_CLOSE in body:
35
+ return max(0, complete_n - 1)
36
+ open_pos = self._fncall_buf.rfind(_INVOKE_OPEN_PREFIX)
37
+ if complete and open_pos >= complete[-1].end():
38
+ return complete_n
39
+ return max(0, complete_n - 1)
40
+
41
+ def _merge_or_append_pending_delta(self, name: str, piece: str, slot: int) -> None:
42
+ if not piece:
43
+ return
44
+ if self._pending_stream_deltas and self._pending_stream_deltas[-1][2] == slot:
45
+ prev_name, prev_piece, prev_slot = self._pending_stream_deltas[-1]
46
+ self._pending_stream_deltas[-1] = (prev_name, prev_piece + piece, prev_slot)
47
+ else:
48
+ self._pending_stream_deltas.append((name, piece, slot))
49
+
50
+ def _pending_body_for_name(self, name: str) -> str:
51
+ return "".join(p for n, p in self._pending_stream_deltas if n == name)
52
+
53
+ def _track_stream_delta(self, name: str, piece: str) -> None:
54
+ slot = self._stream_active_invoke_slot()
55
+ while len(self._stream_invoke_emitted) <= slot:
56
+ self._stream_invoke_emitted.append("")
57
+ self._stream_invoke_emitted[slot] += piece
58
+ self._merge_or_append_pending_delta(name, piece, slot)
59
+
60
+ def _ready_call_tail(self, call, prev: str, *, multi: bool) -> str:
61
+ final = call["function"]["arguments"]
62
+ if final.startswith(prev):
63
+ return final[len(prev) :]
64
+ if prev:
65
+ return ""
66
+ pname = call["function"]["name"]
67
+ pending_body = self._pending_body_for_name(pname) if not multi else ""
68
+ if final.startswith(pending_body):
69
+ return final[len(pending_body) :]
70
+ return final if not pending_body else ""
71
+
72
+ def _align_json_encoder_after_ready(self) -> None:
73
+ if self._json_stream_encoder is None:
74
+ return
75
+ from echotools.exec.fncall.protocols.entml_stream import (
76
+ _INVOKE_CLOSE,
77
+ build_streaming_json_snapshot,
78
+ split_invoke_open,
79
+ )
80
+ parsed = split_invoke_open(self._fncall_buf)
81
+ if not parsed:
82
+ self._json_stream_encoder.reset()
83
+ return
84
+ name, body_start = parsed
85
+ body = self._fncall_buf[body_start:]
86
+ if _INVOKE_CLOSE in body:
87
+ self._json_stream_encoder.reset()
88
+ return
89
+ snap = build_streaming_json_snapshot(
90
+ body, tool_name=name, schema_index=self._get_schema_index()
91
+ )
92
+ self._json_stream_encoder.set_tool_context(name, self._get_schema_index())
93
+ self._json_stream_encoder._emitted = snap
94
+
95
+ def _ensure_ready_invoke_stream_tails(
96
+ self, ready: List[Dict[str, Any]]
97
+ ) -> None:
98
+ """invoke ready 时补齐 partial_json 尾部,避免大 chunk 下缺 ``}``。"""
99
+ if not ready:
100
+ return
101
+ base = self._emitted_invoke_count - len(ready)
102
+ multi = len(ready) > 1
103
+ if multi:
104
+ self._pending_stream_deltas.clear()
105
+ for offset, call in enumerate(ready):
106
+ idx = base + offset
107
+ while len(self._stream_invoke_emitted) <= idx:
108
+ self._stream_invoke_emitted.append("")
109
+ prev = self._stream_invoke_emitted[idx]
110
+ tail = self._ready_call_tail(call, prev, multi=multi)
111
+ if tail:
112
+ name = call["function"]["name"]
113
+ if multi:
114
+ self._pending_stream_deltas.append((name, tail, idx))
115
+ else:
116
+ self._merge_or_append_pending_delta(name, tail, idx)
117
+ self._stream_invoke_emitted[idx] = call["function"]["arguments"]
118
+ self._align_json_encoder_after_ready()
119
+
120
+ def _sync_json_stream_encoder_emitted(self) -> None:
121
+ """invoke 已 ready 后对齐 encoder,避免下一 chunk 重发整段 partial_json。"""
122
+ if self._json_stream_encoder is None:
123
+ return
124
+ from echotools.exec.fncall.protocols.entml_stream import (
125
+ split_invoke_open,
126
+ )
127
+
128
+ parsed = split_invoke_open(self._fncall_buf)
129
+ if not parsed:
130
+ self._json_stream_encoder.reset()
131
+ return
132
+ name, body_start = parsed
133
+ body = self._fncall_buf[body_start:]
134
+ self._json_stream_encoder.poll(body)
135
+
136
+ def feed(self, chunk: str) -> List[Dict[str, Any]]:
137
+ """喂入新的流式文本块。
138
+
139
+ 返回本轮新完成的 tool_calls(与 ``get_ready_tool_calls()`` 相同语义:
140
+ 调用后计数前进,勿再对本轮结果重复调用 ``get_ready_tool_calls``)。
141
+ DONE 或 finalize 后调用返回 ``[]``。
142
+ """
143
+ if not chunk or self._state == self.DONE:
144
+ return []
145
+ if self._finalized_result is not None:
146
+ return []
147
+
148
+ self._raw_buf += chunk
149
+
150
+ if self._state != self.DONE:
151
+ self._feed_waiting(chunk)
152
+
153
+ polled = self._poll_streaming_tool_input_delta()
154
+ if polled:
155
+ self._track_stream_delta(polled[0], polled[1])
156
+ ready = self.get_ready_tool_calls()
157
+ if ready:
158
+ self._ensure_ready_invoke_stream_tails(ready)
159
+ return ready
160
+
161
+ def _poll_streaming_tool_input_delta(self) -> Optional[Tuple[str, str]]:
162
+ """invoke 开标签完整匹配后,返回 (tool_name, partial_json_delta)。"""
163
+ if not self._detected or getattr(self._protocol, "id", None) != "entml":
164
+ return None
165
+ if self._state not in (self.IN_FUNCTION_CALLS,):
166
+ return None
167
+ from echotools.exec.fncall.protocols.entml_patterns import INVOKE_RE
168
+ from echotools.exec.fncall.protocols.entml_stream import (
169
+ _INVOKE_CLOSE,
170
+ EntmlInvokeJsonStreamEncoder,
171
+ build_streaming_json_snapshot,
172
+ split_invoke_open,
173
+ )
174
+
175
+ parsed = split_invoke_open(self._fncall_buf)
176
+ if not parsed:
177
+ return None
178
+ name, body_start = parsed
179
+ body = self._fncall_buf[body_start:]
180
+ complete = list(INVOKE_RE.finditer(self._fncall_buf))
181
+ complete_n = len(complete)
182
+ if _INVOKE_CLOSE in body:
183
+ if complete_n <= self._emitted_invoke_count:
184
+ return None
185
+ # 同一 chunk 内多个 invoke 同时闭合时,由 ensure 按序入队,避免只 poll 最后一个
186
+ if complete_n - self._emitted_invoke_count > 1:
187
+ return None
188
+ slot = max(0, complete_n - 1)
189
+ schema_index = self._get_schema_index()
190
+ final = build_streaming_json_snapshot(
191
+ body,
192
+ tool_name=name,
193
+ schema_index=schema_index,
194
+ )
195
+ while len(self._stream_invoke_emitted) <= slot:
196
+ self._stream_invoke_emitted.append("")
197
+ if self._stream_invoke_emitted[slot] == final:
198
+ return None
199
+ schema_index = self._get_schema_index()
200
+ if self._json_stream_encoder is None:
201
+ self._json_stream_encoder = EntmlInvokeJsonStreamEncoder(
202
+ tool_name=name,
203
+ schema_index=schema_index,
204
+ )
205
+ else:
206
+ self._json_stream_encoder.set_tool_context(name, schema_index)
207
+ delta = self._json_stream_encoder.poll(body)
208
+ if not delta:
209
+ return None
210
+ return (name, delta)
211
+
212
+ def consume_stream_delta(self) -> Optional[Tuple[str, str]]:
213
+ """取出本轮 ``feed`` 产生的 streaming partial_json 增量(FIFO,可多段)。"""
214
+ if not self._pending_stream_deltas:
215
+ return None
216
+ name, piece, _slot = self._pending_stream_deltas.popleft()
217
+ return (name, piece)
218
+
219
+ def stream_invoke_argument_snapshots(self) -> List[str]:
220
+ """各 invoke slot 已流式发出的 arguments JSON 累积(与 batch 对齐)。"""
221
+ return list(self._stream_invoke_emitted)
222
+
223
+ @property
224
+ def streaming_invoke_closed(self) -> bool:
225
+ """流式 fncall 缓冲中是否已出现 ``</entml:invoke>``。"""
226
+ if getattr(self._protocol, "id", None) != "entml":
227
+ return False
228
+ from echotools.exec.fncall.protocols.entml_stream import _INVOKE_CLOSE
229
+
230
+ return _INVOKE_CLOSE in self._fncall_buf
231
+
232
+ def complete_stream_delta_if_needed(self) -> Optional[Tuple[str, str]]:
233
+ """invoke 已开标签但未闭合时,补齐 partial_json 尾部的合法 JSON 后缀。"""
234
+ if not self._detected or getattr(self._protocol, "id", None) != "entml":
235
+ return None
236
+ if self.streaming_invoke_closed:
237
+ return None
238
+ from echotools.exec.fncall.protocols.entml_stream import (
239
+ EntmlInvokeJsonStreamEncoder,
240
+ build_streaming_json_snapshot,
241
+ split_invoke_open,
242
+ )
243
+
244
+ parsed = split_invoke_open(self._fncall_buf)
245
+ if not parsed:
246
+ return None
247
+ name, body_start = parsed
248
+ body = self._fncall_buf[body_start:]
249
+ schema_index = self._get_schema_index()
250
+ snapshot = build_streaming_json_snapshot(
251
+ body,
252
+ tool_name=name,
253
+ schema_index=schema_index,
254
+ force_close=True,
255
+ )
256
+ if not snapshot:
257
+ return None
258
+ if self._json_stream_encoder is None:
259
+ self._json_stream_encoder = EntmlInvokeJsonStreamEncoder(
260
+ tool_name=name,
261
+ schema_index=schema_index,
262
+ )
263
+ else:
264
+ self._json_stream_encoder.set_tool_context(name, schema_index)
265
+ delta = self._json_stream_encoder.poll(body, force_close=True)
266
+ if not delta:
267
+ return None
268
+ return (name, delta)
269
+
@@ -0,0 +1,167 @@
1
+ """FncallStreamParser waiting/feed helpers."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Dict, List, Optional, Tuple
5
+
6
+
7
+ class StreamFeedMixin:
8
+ def _normalize_stream_chunk(self, text: str) -> str:
9
+ fn = getattr(self._protocol, "normalize_stream_buffer", None)
10
+ if callable(fn):
11
+ return fn(text)
12
+ return text
13
+
14
+ def _invoke_close_tag(self) -> str:
15
+ return self._end_tags[0] if self._end_tags else "</entml:invoke>"
16
+
17
+ def _drain_thinking_holdback_to_fncall(self) -> None:
18
+ """invoke 已开始后,thinking 过滤器里对工具标签的 holdback 应并入 fncall 缓冲。"""
19
+ if self._thinking_filter is None:
20
+ return
21
+ pending = self._thinking_filter._pending
22
+ if pending:
23
+ self._fncall_buf += pending
24
+ self._thinking_filter._pending = ""
25
+
26
+ def _begin_function_calls(self, buffer_from: str, *, pos: int) -> None:
27
+ self._trim_trailing_visible_whitespace()
28
+ self._fncall_buf = buffer_from[pos:]
29
+ self._drain_thinking_holdback_to_fncall()
30
+ self._detected = True
31
+ self._state = self.IN_FUNCTION_CALLS
32
+ self._json_stream_encoder = None
33
+ if self._thinking_filter is not None:
34
+ self._thinking_filter.clear_invoke_handoff_state()
35
+
36
+ def _hold_or_split_content(self, text: str, trigger_tags) -> bool:
37
+ """True 表示已消费 text(含 holdback)。"""
38
+ hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
39
+ if hold_from_fn is not None:
40
+ hold_from = hold_from_fn(text)
41
+ if hold_from is not None:
42
+ if hold_from > 0:
43
+ self._append_content_text(text[:hold_from])
44
+ self._waiting_tail = text[hold_from:]
45
+ return True
46
+ safe, remain = self._split_safe_text(text, trigger_tags)
47
+ if safe:
48
+ self._append_content_text(safe)
49
+ self._waiting_tail = remain
50
+ return True
51
+
52
+ def _hold_or_split_emit(self, text: str, trigger_tags) -> None:
53
+ hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
54
+ if hold_from_fn is not None:
55
+ hold_from = hold_from_fn(text)
56
+ if hold_from is not None:
57
+ if hold_from > 0:
58
+ self._emit_text(text[:hold_from])
59
+ self._waiting_tail = text[hold_from:]
60
+ return
61
+ safe, remain = self._split_safe_text(text, trigger_tags)
62
+ if safe:
63
+ self._emit_text(safe)
64
+ self._waiting_tail = remain
65
+
66
+ def _feed_content_waiting(self, text: str) -> None:
67
+ """thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
68
+ if not text:
69
+ return
70
+ text = self._normalize_stream_chunk(text)
71
+ if self._state == self.IN_FUNCTION_CALLS:
72
+ close_tag = self._invoke_close_tag()
73
+ if not self._is_call_closed():
74
+ if close_tag in text:
75
+ idx = text.find(close_tag)
76
+ self._fncall_buf += text[: idx + len(close_tag)]
77
+ tail = text[idx + len(close_tag) :]
78
+ if tail:
79
+ self._feed_content_waiting(tail)
80
+ return
81
+ self._fncall_buf += text
82
+ return
83
+ trigger_tags = self._protocol.get_trigger_tags()
84
+ found, pos = self._protocol.detect_start(text)
85
+ if found:
86
+ if pos > 0:
87
+ self._append_content_text(text[:pos])
88
+ self._fncall_buf += text[pos:]
89
+ return
90
+ self._hold_or_split_content(text, trigger_tags)
91
+ return
92
+ trigger_tags = self._protocol.get_trigger_tags()
93
+ found, pos = self._protocol.detect_start(text)
94
+ if not found:
95
+ self._hold_or_split_content(text, trigger_tags)
96
+ return
97
+ from echotools.exec.fncall.protocols.entml_think.parse import (
98
+ invoke_index_inside_any_thinking_block,
99
+ )
100
+ if invoke_index_inside_any_thinking_block(
101
+ text, pos, thinking_enabled=self._thinking_enabled,
102
+ ):
103
+ self._append_content_text(text)
104
+ return
105
+ if pos > 0:
106
+ self._append_content_text(text[:pos])
107
+ self._begin_function_calls(text, pos=pos)
108
+
109
+ def _feed_waiting_thinking_plain(self, combined: str) -> None:
110
+ """未闭合 thinking 阶段:块内一律按纯文本进 thinking,不检测 invoke。"""
111
+ assert self._thinking_filter is not None
112
+ for kind, part in self._thinking_filter.feed(combined):
113
+ if kind == "thinking":
114
+ self._thinking_parts.append(part)
115
+ elif part:
116
+ self._feed_content_waiting(part)
117
+ if self._state == self.IN_FUNCTION_CALLS:
118
+ self._drain_thinking_holdback_to_fncall()
119
+
120
+ def _feed_waiting(self, chunk: str) -> None:
121
+ """在 WAITING_FOR_TAG / IN_FUNCTION_CALLS 状态下处理新块。"""
122
+ combined = self._normalize_stream_chunk(self._waiting_tail + chunk)
123
+ self._waiting_tail = ""
124
+ if self._state == self.IN_FUNCTION_CALLS:
125
+ if self._thinking_filter is not None and self._thinking_filter.in_open_thinking():
126
+ self._feed_waiting_thinking_plain(combined)
127
+ return
128
+ if self._waiting_tail:
129
+ self._fncall_buf += self._normalize_stream_chunk(self._waiting_tail)
130
+ self._waiting_tail = ""
131
+ self._drain_thinking_holdback_to_fncall()
132
+ if not self._is_call_closed():
133
+ self._fncall_buf += combined
134
+ return
135
+ self._feed_content_waiting(combined)
136
+ return
137
+ if self._thinking_filter is not None and self._thinking_filter.in_open_thinking():
138
+ self._feed_waiting_thinking_plain(combined)
139
+ return
140
+ trigger_tags = self._protocol.get_trigger_tags()
141
+ found, pos = self._protocol.detect_start(combined)
142
+ if found:
143
+ if self._thinking_filter is not None:
144
+ from echotools.exec.fncall.protocols.entml_think.parse import (
145
+ invoke_index_inside_any_thinking_block,
146
+ )
147
+ if invoke_index_inside_any_thinking_block(
148
+ combined, pos, thinking_enabled=self._thinking_enabled,
149
+ ):
150
+ self._feed_waiting_thinking_plain(combined)
151
+ return
152
+ if pos > 0:
153
+ self._emit_text(combined[:pos])
154
+ self._begin_function_calls(combined, pos=pos)
155
+ return
156
+ # detect_start 之后再处理未闭合 thinking,避免 legacy 外壳后 `<en` 误 hold
157
+ if self._thinking_filter is not None:
158
+ from echotools.exec.fncall.protocols.entml_think.parse import (
159
+ has_unclosed_entml_thinking,
160
+ )
161
+ if has_unclosed_entml_thinking(
162
+ combined, thinking_enabled=self._thinking_enabled
163
+ ):
164
+ self._feed_waiting_thinking_plain(combined)
165
+ return
166
+ self._hold_or_split_emit(combined, trigger_tags)
167
+