echotools 2.3.45__tar.gz → 2.3.47__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 (228) hide show
  1. {echotools-2.3.45 → echotools-2.3.47}/PKG-INFO +2 -1
  2. {echotools-2.3.45 → echotools-2.3.47}/pyproject.toml +2 -2
  3. echotools-2.3.47/setup.cfg +8 -0
  4. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/parsers/stream.py +69 -7
  5. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/prompt/inject.py +7 -1
  6. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml.py +48 -16
  7. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_invoke.py +1 -1
  8. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_patterns.py +18 -0
  9. echotools-2.3.47/src/echotools/exec/fncall/protocols/entml_stream_json.py +141 -0
  10. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_think/core.py +18 -5
  11. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_think/parse.py +122 -4
  12. echotools-2.3.47/src/echotools/media/console/ui.py +128 -0
  13. echotools-2.3.47/src/echotools/media/console/uicore/mixins/__init__.py +15 -0
  14. echotools-2.3.47/src/echotools/media/console/uicore/mixins/uibase.py +195 -0
  15. echotools-2.3.47/src/echotools/media/console/uicore/mixins/uicmds.py +224 -0
  16. echotools-2.3.47/src/echotools/media/console/uicore/mixins/uiinteract.py +199 -0
  17. echotools-2.3.47/src/echotools/media/console/uicore/mixins/uioutput.py +304 -0
  18. echotools-2.3.47/src/echotools/media/console/uicore/mixins/uistream.py +144 -0
  19. echotools-2.3.47/src/echotools/media/console/uicore/ui_console.py +20 -0
  20. echotools-2.3.47/src/echotools/media/console/uicore/ui_io.py +27 -0
  21. echotools-2.3.47/src/echotools/media/console/uicore/ui_log.py +67 -0
  22. echotools-2.3.47/src/echotools/media/console/uicore/ui_platform.py +389 -0
  23. echotools-2.3.47/src/echotools/media/console/uicore/ui_text.py +368 -0
  24. echotools-2.3.47/src/echotools/media/console/uicore/ui_types.py +246 -0
  25. echotools-2.3.47/src/echotools/media/console/uilayout/ui_countdown.py +62 -0
  26. echotools-2.3.47/src/echotools/media/console/uilayout/ui_layout.py +50 -0
  27. echotools-2.3.47/src/echotools/media/console/uilayout/ui_misc.py +336 -0
  28. echotools-2.3.47/src/echotools/media/console/uilayout/ui_panel.py +137 -0
  29. echotools-2.3.47/src/echotools/media/console/uilayout/ui_table.py +205 -0
  30. echotools-2.3.47/src/echotools/media/console/uilayout/ui_tree.py +146 -0
  31. echotools-2.3.47/src/echotools/media/console/uiwidgets/ui_box.py +197 -0
  32. echotools-2.3.47/src/echotools/media/console/uiwidgets/ui_editor.py +360 -0
  33. echotools-2.3.47/src/echotools/media/console/uiwidgets/ui_progress.py +296 -0
  34. echotools-2.3.47/src/echotools/media/console/uiwidgets/ui_select.py +199 -0
  35. echotools-2.3.47/src/echotools/media/console/uiwidgets/ui_stream.py +85 -0
  36. echotools-2.3.47/src/echotools/media/web/input_box/components/sortablelist.js +104 -0
  37. echotools-2.3.47/src/echotools/media/web/input_box/components/textinput.js +185 -0
  38. echotools-2.3.47/src/echotools/media/web/input_box/inputbox.js +277 -0
  39. {echotools-2.3.45 → echotools-2.3.47}/src/echotools.egg-info/PKG-INFO +2 -1
  40. {echotools-2.3.45 → echotools-2.3.47}/src/echotools.egg-info/SOURCES.txt +27 -1
  41. {echotools-2.3.45 → echotools-2.3.47}/src/tests/fixtures/simulated_llm_tool_responses.py +44 -0
  42. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_adversarial_stream_splits.py +4 -5
  43. echotools-2.3.47/src/tests/test_entml_bare_invoke_format.py +375 -0
  44. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_entml_protocol.py +56 -0
  45. echotools-2.3.47/src/tests/test_entml_stream_json.py +76 -0
  46. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_entml_tool_parse_leak.py +0 -1
  47. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_entml_tool_pipeline.py +30 -2
  48. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_fncall_stream_thinking.py +11 -8
  49. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_simulated_llm_tool_parse.py +7 -5
  50. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_stream_realtime_parse.py +12 -15
  51. echotools-2.3.45/setup.cfg +0 -4
  52. echotools-2.3.45/src/echotools/media/console/ui.py +0 -4242
  53. echotools-2.3.45/src/echotools/media/web/input_box/components/sortablelist.js +0 -104
  54. echotools-2.3.45/src/echotools/media/web/input_box/components/textinput.js +0 -186
  55. echotools-2.3.45/src/echotools/media/web/input_box/fonts/tailwind.css +0 -1
  56. echotools-2.3.45/src/echotools/media/web/input_box/inputbox.js +0 -278
  57. {echotools-2.3.45 → echotools-2.3.47}/README.md +0 -0
  58. {echotools-2.3.45 → echotools-2.3.47}/docs/README.md +0 -0
  59. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/__init__.py +0 -0
  60. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/cache/__init__.py +0 -0
  61. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/cache/list_cache.py +0 -0
  62. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/cache/memory_cache.py +0 -0
  63. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/__init__.py +0 -0
  64. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/base.py +0 -0
  65. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/center.py +0 -0
  66. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/center_notify.py +0 -0
  67. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/center_template.py +0 -0
  68. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/loader.py +0 -0
  69. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/config/merge.py +0 -0
  70. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/errors/__init__.py +0 -0
  71. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/errors/base.py +0 -0
  72. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/errors/classify.py +0 -0
  73. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/errors/common.py +0 -0
  74. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/errors/http.py +0 -0
  75. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/ids/__init__.py +0 -0
  76. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/ids/generator.py +0 -0
  77. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/io/__init__.py +0 -0
  78. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/io/io_utils.py +0 -0
  79. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/io/printstream.py +0 -0
  80. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/logger/__init__.py +0 -0
  81. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/logger/manager.py +0 -0
  82. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/retry/__init__.py +0 -0
  83. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/base/retry/retry.py +0 -0
  84. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/compat.py +0 -0
  85. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/__init__.py +0 -0
  86. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/candidate.py +0 -0
  87. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  88. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  89. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/race.py +0 -0
  90. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/selector.py +0 -0
  91. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/dispatch/usage.py +0 -0
  92. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/__init__.py +0 -0
  93. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  94. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
  95. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
  96. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  97. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/prompt/history.py +0 -0
  98. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  99. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  100. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/prompt/templates.py +0 -0
  101. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  102. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  103. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  104. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
  105. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
  106. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/registry.py +0 -0
  107. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  108. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/shared/coercion.py +0 -0
  109. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  110. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  111. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  112. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
  113. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/keys.py +0 -0
  114. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/lifecycle/__init__.py +0 -0
  115. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/lifecycle/manager.py +0 -0
  116. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/lifecycle/updater.py +0 -0
  117. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  118. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/process/__init__.py +0 -0
  119. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/process/port.py +0 -0
  120. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/protocol/__init__.py +0 -0
  121. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/protocol/base.py +0 -0
  122. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/__init__.py +0 -0
  123. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/conpty.py +0 -0
  124. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/local/__init__.py +0 -0
  125. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/local/core.py +0 -0
  126. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/local/proc.py +0 -0
  127. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/local/unix.py +0 -0
  128. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/local/win.py +0 -0
  129. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/sanitize.py +0 -0
  130. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/session/__init__.py +0 -0
  131. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/session/base.py +0 -0
  132. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/session/client.py +0 -0
  133. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/ssh.py +0 -0
  134. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/exec/terminal/tmux.py +0 -0
  135. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/console/__init__.py +0 -0
  136. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/console/charmap.py +0 -0
  137. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/console/spinner.py +0 -0
  138. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/events/__init__.py +0 -0
  139. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/events/bus.py +0 -0
  140. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/events/event.py +0 -0
  141. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/spinner/__init__.py +0 -0
  142. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/spinner/spinner.py +0 -0
  143. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/spinner/spinner_color.py +0 -0
  144. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/tracing/__init__.py +0 -0
  145. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/tracing/context.py +0 -0
  146. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/tracing/span.py +0 -0
  147. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/tracing/tracer.py +0 -0
  148. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/translate.py +0 -0
  149. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/watcher/__init__.py +0 -0
  150. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/watcher/file_watcher.py +0 -0
  151. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/__init__.py +0 -0
  152. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/application.py +0 -0
  153. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/broker.py +0 -0
  154. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/app.js +0 -0
  155. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  156. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  157. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  158. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  159. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  160. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  161. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  162. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  163. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/inputbox.css +0 -0
  164. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/style.css +0 -0
  165. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/input_box/voicegif.js +0 -0
  166. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/middleware.py +0 -0
  167. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/stats.py +0 -0
  168. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/media/web/utils.py +0 -0
  169. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/files/__init__.py +0 -0
  170. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/files/file_util.py +0 -0
  171. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/network/__init__.py +0 -0
  172. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/network/http_utils.py +0 -0
  173. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/plugin/__init__.py +0 -0
  174. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/plugin/base.py +0 -0
  175. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/plugin/discovery.py +0 -0
  176. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/plugin/registry.py +0 -0
  177. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/plugin/registry_ops.py +0 -0
  178. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/proxy/__init__.py +0 -0
  179. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/proxy/manager.py +0 -0
  180. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/runtime/__init__.py +0 -0
  181. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/runtime/collector.py +0 -0
  182. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/scheduler/__init__.py +0 -0
  183. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/scheduler/scheduler.py +0 -0
  184. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/sdk/__init__.py +0 -0
  185. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/plat/sdk/facade.py +0 -0
  186. {echotools-2.3.45 → echotools-2.3.47}/src/echotools/version.py +0 -0
  187. {echotools-2.3.45 → echotools-2.3.47}/src/echotools.egg-info/dependency_links.txt +0 -0
  188. {echotools-2.3.45 → echotools-2.3.47}/src/echotools.egg-info/requires.txt +0 -0
  189. {echotools-2.3.45 → echotools-2.3.47}/src/echotools.egg-info/top_level.txt +0 -0
  190. {echotools-2.3.45 → echotools-2.3.47}/src/tests/bench_import.py +0 -0
  191. {echotools-2.3.45 → echotools-2.3.47}/src/tests/conftest.py +0 -0
  192. {echotools-2.3.45 → echotools-2.3.47}/src/tests/fixtures/__init__.py +0 -0
  193. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_cache_retry.py +0 -0
  194. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_completion.py +0 -0
  195. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_config.py +0 -0
  196. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_config_extended.py +0 -0
  197. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_console.py +0 -0
  198. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_coverage_core.py +0 -0
  199. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_dispatch.py +0 -0
  200. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_dispatch_extended.py +0 -0
  201. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_dispatch_race.py +0 -0
  202. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_entml_thinking_history.py +0 -0
  203. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_entml_thinking_parse.py +0 -0
  204. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_entml_tools.py +0 -0
  205. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_errors.py +0 -0
  206. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_errors_common.py +0 -0
  207. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_events.py +0 -0
  208. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_events_event.py +0 -0
  209. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_facade.py +0 -0
  210. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_fncall.py +0 -0
  211. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_ids.py +0 -0
  212. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_imports.py +0 -0
  213. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_io.py +0 -0
  214. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_keys.py +0 -0
  215. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_lifecycle.py +0 -0
  216. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_logger.py +0 -0
  217. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_merge.py +0 -0
  218. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_plugin.py +0 -0
  219. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_printstream.py +0 -0
  220. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_protocols.py +0 -0
  221. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_proxy.py +0 -0
  222. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_proxy_selector.py +0 -0
  223. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_runtime.py +0 -0
  224. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_scheduler.py +0 -0
  225. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_tracing.py +0 -0
  226. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_usage.py +0 -0
  227. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_web.py +0 -0
  228. {echotools-2.3.45 → echotools-2.3.47}/src/tests/test_web_broker.py +0 -0
@@ -1,7 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.3.45
3
+ Version: 2.3.47
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
+ Home-page: https://github.com/nichengfuben/echotools
5
6
  Author: nichengfuben
6
7
  License: MIT
7
8
  Project-URL: Homepage, https://github.com/nichengfuben/echotools
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echotools"
7
- version = "2.3.45"
7
+ version = "2.3.47"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.8"
@@ -98,7 +98,7 @@ module = "echotools.exec.terminal.*"
98
98
  ignore_errors = true
99
99
 
100
100
  [[tool.mypy.overrides]]
101
- module = "echotools.media.console.ui"
101
+ module = "echotools.media.console.*"
102
102
  ignore_errors = true
103
103
 
104
104
  [tool.pytest.ini_options]
@@ -0,0 +1,8 @@
1
+ [metadata]
2
+ url = https://github.com/nichengfuben/echotools
3
+ home_page = https://github.com/nichengfuben/echotools
4
+
5
+ [egg_info]
6
+ tag_build =
7
+ tag_date = 0
8
+
@@ -52,6 +52,8 @@ class FncallStreamParser:
52
52
  self._thinking_parts: List[str] = []
53
53
  self._thinking_filter = _make_thinking_filter(protocol)
54
54
  self._emitted_invoke_count: int = 0
55
+ self._json_stream_encoder = None
56
+ self._pending_stream_delta: Optional[Tuple[str, str]] = None
55
57
 
56
58
  # 三种情况:
57
59
  # 1. 协议实现了 get_stream_end_tags() 且返回非空列表 → 用声明的结束标记
@@ -140,13 +142,28 @@ class FncallStreamParser:
140
142
  if text:
141
143
  self._text_parts.append(text)
142
144
 
145
+ def _normalize_stream_chunk(self, text: str) -> str:
146
+ fn = getattr(self._protocol, "normalize_stream_buffer", None)
147
+ if callable(fn):
148
+ return fn(text)
149
+ return text
150
+
143
151
  def _feed_content_waiting(self, text: str) -> None:
144
152
  """thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
145
153
  if not text:
146
154
  return
155
+ text = self._normalize_stream_chunk(text)
147
156
  trigger_tags = self._protocol.get_trigger_tags()
148
157
  found, pos = self._protocol.detect_start(text)
149
158
  if not found:
159
+ hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
160
+ if hold_from_fn is not None:
161
+ hold_from = hold_from_fn(text)
162
+ if hold_from is not None:
163
+ if hold_from > 0:
164
+ self._append_content_text(text[:hold_from])
165
+ self._waiting_tail = text[hold_from:]
166
+ return
150
167
  safe, remain = self._split_safe_text(text, trigger_tags)
151
168
  if safe:
152
169
  self._append_content_text(safe)
@@ -173,7 +190,7 @@ class FncallStreamParser:
173
190
 
174
191
  def _feed_waiting(self, chunk: str) -> None:
175
192
  """在 WAITING_FOR_TAG 状态下处理新块。"""
176
- combined = self._waiting_tail + chunk
193
+ combined = self._normalize_stream_chunk(self._waiting_tail + chunk)
177
194
  self._waiting_tail = ""
178
195
 
179
196
  # 已进入 thinking 块时,块内不做 invoke 检测。
@@ -196,8 +213,8 @@ class FncallStreamParser:
196
213
  return
197
214
 
198
215
  # 无完整工具开标签时,再处理未闭合 thinking / 思考开标签 holdback。
199
- # 必须放在 detect_start 之后:否则 `<entml:function_calls>\n<en` 会被
200
- # 尾部 `<en` 误判为 thinking 前缀,导致 invoke 内容被截断。
216
+ # 必须放在 detect_start 之后:否则 legacy 外壳后的 `<en` 会被
217
+ # 尾部误判为 thinking 前缀,导致 invoke 内容被截断。
201
218
  if self._thinking_filter is not None:
202
219
  from echotools.exec.fncall.protocols.entml_think.parse import (
203
220
  has_unclosed_entml_thinking,
@@ -206,6 +223,15 @@ class FncallStreamParser:
206
223
  self._feed_waiting_thinking_plain(combined)
207
224
  return
208
225
 
226
+ hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
227
+ if hold_from_fn is not None:
228
+ hold_from = hold_from_fn(combined)
229
+ if hold_from is not None:
230
+ if hold_from > 0:
231
+ self._emit_text(combined[:hold_from])
232
+ self._waiting_tail = combined[hold_from:]
233
+ return
234
+
209
235
  safe, remain = self._split_safe_text(combined, trigger_tags)
210
236
  if safe:
211
237
  self._emit_text(safe)
@@ -222,9 +248,11 @@ class FncallStreamParser:
222
248
  return False
223
249
 
224
250
  def feed(self, chunk: str) -> List[Dict[str, Any]]:
225
- """喂入新的流式文本块,返回本轮新完成的 tool_calls(可空列表)。
251
+ """喂入新的流式文本块。
226
252
 
227
- DONE 或 finalize 后调用静默忽略并返回 ``[]``。
253
+ 返回本轮新完成的 tool_calls(与 ``get_ready_tool_calls()`` 相同语义:
254
+ 调用后计数前进,勿再对本轮结果重复调用 ``get_ready_tool_calls``)。
255
+ DONE 或 finalize 后调用返回 ``[]``。
228
256
  """
229
257
  if not chunk or self._state == self.DONE:
230
258
  return []
@@ -236,11 +264,45 @@ class FncallStreamParser:
236
264
  if self._state == self.WAITING_FOR_TAG:
237
265
  self._feed_waiting(chunk)
238
266
  else:
239
- self._fncall_buf += chunk
267
+ self._fncall_buf += self._normalize_stream_chunk(chunk)
240
268
  if self._is_call_closed():
241
269
  self._state = self.DONE
242
270
 
243
- return self.get_ready_tool_calls()
271
+ self._pending_stream_delta = self._poll_streaming_tool_input_delta()
272
+ ready = self.get_ready_tool_calls()
273
+ if ready:
274
+ if self._json_stream_encoder is not None:
275
+ self._json_stream_encoder.reset()
276
+ return ready
277
+
278
+ def _poll_streaming_tool_input_delta(self) -> Optional[Tuple[str, str]]:
279
+ """invoke 开标签完整匹配后,返回 (tool_name, partial_json_delta)。"""
280
+ if not self._detected or getattr(self._protocol, "id", None) != "entml":
281
+ return None
282
+ if self._state not in (self.IN_FUNCTION_CALLS, self.DONE):
283
+ return None
284
+ from echotools.exec.fncall.protocols.entml_stream_json import (
285
+ EntmlInvokeJsonStreamEncoder,
286
+ split_invoke_open,
287
+ )
288
+
289
+ parsed = split_invoke_open(self._fncall_buf)
290
+ if not parsed:
291
+ return None
292
+ name, body_start = parsed
293
+ body = self._fncall_buf[body_start:]
294
+ if self._json_stream_encoder is None:
295
+ self._json_stream_encoder = EntmlInvokeJsonStreamEncoder()
296
+ delta = self._json_stream_encoder.poll(body)
297
+ if not delta:
298
+ return None
299
+ return (name, delta)
300
+
301
+ def consume_stream_delta(self) -> Optional[Tuple[str, str]]:
302
+ """取出本轮 ``feed`` 产生的 streaming partial_json 增量。"""
303
+ delta = self._pending_stream_delta
304
+ self._pending_stream_delta = None
305
+ return delta
244
306
 
245
307
  def _assembly_for_tool_parse(self) -> str:
246
308
  """可供工具解析的已缓冲文本(不含 thinking 过滤器内部 pending)。"""
@@ -147,11 +147,17 @@ def inject_fncall(
147
147
  if not tools:
148
148
  if protocol.id == "entml":
149
149
  sections: List[str] = []
150
+ if user_system_prompt and user_system_prompt.strip():
151
+ sections.append(
152
+ f"<user_system_prompt>\n{user_system_prompt.strip()}\n</user_system_prompt>"
153
+ )
150
154
  if history_text.strip():
151
155
  sections.append(format_entml_conversation_history(history_text))
152
156
  sections.append(format_entml_current_user_message(current_user_message))
153
157
  # thinking 放在最后,超限截断时优先保留在 send_text 尾部
154
- thinking_section = build_entml_thinking_section(protocol_options)
158
+ thinking_section = build_entml_thinking_section(
159
+ protocol_options, has_tools=False
160
+ )
155
161
  if thinking_section:
156
162
  sections.append(thinking_section)
157
163
  prompt = "\n\n".join(sections)
@@ -1,6 +1,6 @@
1
1
  """Entropy ML (entml) 协议实现。
2
2
 
3
- - 工具调用:<entml:function_calls> / <entml:invoke> / <entml:parameter>
3
+ - 工具调用:<entml:invoke> / <entml:parameter>(legacy ``function_calls`` 外壳仅解析兼容)
4
4
  - 对话历史:<entml:conversation_history>
5
5
  - 当前用户消息:<current_user_message>
6
6
  """
@@ -20,6 +20,9 @@ from echotools.exec.fncall.protocols.entml_invoke import (
20
20
  )
21
21
  from echotools.exec.fncall.protocols.entml_patterns import (
22
22
  BLOCK_RE,
23
+ extract_attr_value,
24
+ normalize_entml_name,
25
+ strip_legacy_function_calls_wrapper,
23
26
  strip_tool_entml_residue,
24
27
  )
25
28
  from echotools.exec.fncall.protocols.entml_think.core import (
@@ -136,20 +139,22 @@ class EntmlProtocol(ToolProtocol):
136
139
 
137
140
  _TRIGGER = "<entml:invoke>"
138
141
  _TRIGGER_PREFIX = "<entml:invoke"
139
- _WRAPPER_PREFIX = "<entml:function_calls"
142
+ _LEGACY_WRAPPER_PREFIX = "<entml:function_calls"
140
143
  _THINKING_PREFIX = "<entml:thinking"
141
144
 
142
145
  def get_trigger_tags(self) -> List[str]:
143
- # invoke / function_calls / thinking 前缀一并 holdback,避免 `<e` 歧义被提前吐出
146
+ # invoke / thinking 前缀 holdback,避免 `<e` 歧义被提前吐出
144
147
  return [
145
148
  self._TRIGGER,
146
149
  self._TRIGGER_PREFIX,
147
- self._WRAPPER_PREFIX,
148
- f"{self._WRAPPER_PREFIX}>",
149
150
  self._THINKING_PREFIX,
150
151
  f"{self._THINKING_PREFIX}>",
151
152
  ]
152
153
 
154
+ def normalize_stream_buffer(self, buffer: str) -> str:
155
+ """流式处理前剥离 legacy function_calls 外壳(提示词已不再要求)。"""
156
+ return strip_legacy_function_calls_wrapper(buffer)
157
+
153
158
  def get_stream_end_tags(self) -> List[str]:
154
159
  """新格式无外层 wrapper,不自动关闭流,由 finalize() 统一解析。"""
155
160
  return []
@@ -194,25 +199,52 @@ class EntmlProtocol(ToolProtocol):
194
199
  sections.append(format_entml_current_user_message(current_user_message))
195
200
 
196
201
  # thinking 放在最后,超限截断时优先保留在 send_text 尾部
197
- thinking_section = build_entml_thinking_section(protocol_options)
202
+ thinking_section = build_entml_thinking_section(
203
+ protocol_options, has_tools=bool(tool_descs)
204
+ )
198
205
  if thinking_section:
199
206
  sections.append(thinking_section)
200
207
 
201
208
  return "\n\n".join(sections)
202
209
 
203
210
  def detect_start(self, buffer: str) -> Tuple[bool, int]:
204
- candidates: List[int] = []
205
- for prefix in (self._WRAPPER_PREFIX, self._TRIGGER_PREFIX):
206
- pos = buffer.find(prefix)
211
+ """invoke 开标签(含 name 且闭合 ``>``)稳定后才视为工具流开始。"""
212
+ invoke_pos = self._find_complete_invoke_open(buffer)
213
+ if invoke_pos < 0:
214
+ return (False, -1)
215
+ return (True, invoke_pos)
216
+
217
+ def find_fncall_hold_from(self, buffer: str) -> Optional[int]:
218
+ """invoke 未稳定前 hold 前缀;legacy 外壳开标签未闭合时同样 hold。"""
219
+ if self._find_complete_invoke_open(buffer) >= 0:
220
+ return None
221
+ hold: Optional[int] = None
222
+ legacy_pos = buffer.find(self._LEGACY_WRAPPER_PREFIX)
223
+ if legacy_pos >= 0:
224
+ close = buffer.find(">", legacy_pos)
225
+ if close < 0 and (hold is None or legacy_pos < hold):
226
+ hold = legacy_pos
227
+ invoke_pos = buffer.find(self._TRIGGER_PREFIX)
228
+ if invoke_pos >= 0 and (hold is None or invoke_pos < hold):
229
+ hold = invoke_pos
230
+ return hold
231
+
232
+ def _find_complete_invoke_open(self, buffer: str) -> int:
233
+ """返回首个含 name 且已闭合 ``>`` 的 ``<entml:invoke`` 起始下标;否则 -1。"""
234
+ search_from = 0
235
+ prefix_len = len(self._TRIGGER_PREFIX)
236
+ while True:
237
+ pos = buffer.find(self._TRIGGER_PREFIX, search_from)
207
238
  if pos < 0:
208
- continue
209
- close = buffer.find(">", pos + len(prefix))
239
+ return -1
240
+ close = buffer.find(">", pos + prefix_len)
210
241
  if close < 0:
211
- continue
212
- candidates.append(pos)
213
- if not candidates:
214
- return (False, -1)
215
- return (True, min(candidates))
242
+ return -1
243
+ attrs = buffer[pos + prefix_len : close]
244
+ name = extract_attr_value(attrs, "name")
245
+ if name and normalize_entml_name(name):
246
+ return pos
247
+ search_from = close + 1
216
248
 
217
249
  def parse(
218
250
  self,
@@ -93,7 +93,7 @@ def format_entml_parameter_value(value: Any) -> str:
93
93
 
94
94
 
95
95
  def format_entml_tool_calls(tool_calls: List[Dict[str, Any]]) -> str:
96
- """将 tool_call 列表渲染为 entml function_calls 块。"""
96
+ """将 tool_call 列表渲染为裸 ``<entml:invoke>`` 块(无 function_calls 外壳)。"""
97
97
  if not tool_calls:
98
98
  return ""
99
99
 
@@ -39,6 +39,15 @@ _TOOL_WRAPPER_PAIR_RE = re.compile(
39
39
  r"<entml:function_calls\b[^>]*>[\s\S]*?</entml:function_calls>",
40
40
  re.DOTALL,
41
41
  )
42
+ # 旧版 function_calls 外壳(提示词已不再要求;流式/解析前静默剥离完整开闭标签)。
43
+ _LEGACY_WRAPPER_OPEN_RE = re.compile(
44
+ r"<entml:function_calls\b[^>]*>\s*",
45
+ re.IGNORECASE,
46
+ )
47
+ _LEGACY_WRAPPER_CLOSE_RE = re.compile(
48
+ r"\s*</entml:function_calls\s*>",
49
+ re.IGNORECASE,
50
+ )
42
51
  _TOOL_ORPHAN_TAG_RE = re.compile(
43
52
  r"</?entml:(?:function_calls|invoke|parameter|parameters)\b[^>]*/?>",
44
53
  re.DOTALL,
@@ -98,6 +107,15 @@ def parse_sub_tags(
98
107
  return args
99
108
 
100
109
 
110
+ def strip_legacy_function_calls_wrapper(text: str) -> str:
111
+ """移除完整 legacy ``<entml:function_calls>`` 开/闭标签(裸 invoke 为一等格式)。"""
112
+ if not text:
113
+ return text
114
+ out = _LEGACY_WRAPPER_OPEN_RE.sub("", text)
115
+ out = _LEGACY_WRAPPER_CLOSE_RE.sub("", out)
116
+ return out
117
+
118
+
101
119
  def strip_tool_entml_residue(content: str) -> str:
102
120
  """剥离工具相关 entml 标签残留,保留 thinking 等非工具标签。"""
103
121
  if not content:
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Any, Dict, List, Optional, Tuple
6
+
7
+ from .entml_patterns import extract_attr_value, normalize_entml_name
8
+
9
+ _PARAM_OPEN_RE = re.compile(r"<entml:parameter\b([^>]*)>", re.IGNORECASE)
10
+ _PARAM_CLOSE = "</entml:parameter>"
11
+ _INVOKE_CLOSE = "</entml:invoke>"
12
+ _INVOKE_OPEN_PREFIX = "<entml:invoke"
13
+
14
+
15
+ def _strip_incomplete_markup_suffix(value: str) -> str:
16
+ lt = value.rfind("<")
17
+ if lt < 0:
18
+ return value
19
+ tail = value[lt:]
20
+ for tag in (_PARAM_CLOSE, _INVOKE_CLOSE):
21
+ if tag.startswith(tail) and tail != tag:
22
+ return value[:lt]
23
+ return value
24
+
25
+
26
+ def split_invoke_open(buffer: str) -> Optional[Tuple[str, int]]:
27
+ search_from = 0
28
+ prefix_len = len(_INVOKE_OPEN_PREFIX)
29
+ while True:
30
+ pos = buffer.find(_INVOKE_OPEN_PREFIX, search_from)
31
+ if pos < 0:
32
+ return None
33
+ gt = buffer.find(">", pos + prefix_len)
34
+ if gt < 0:
35
+ return None
36
+ attrs = buffer[pos + prefix_len : gt]
37
+ name = extract_attr_value(attrs, "name")
38
+ if name and normalize_entml_name(name):
39
+ return normalize_entml_name(name), gt + 1
40
+ search_from = gt + 1
41
+
42
+
43
+ def _json_string_body(value: str) -> str:
44
+ """JSON 字符串内部片段(不含外围引号)。"""
45
+ return json.dumps(value, ensure_ascii=False)[1:-1]
46
+
47
+
48
+ def _parse_parameter_entries(body: str) -> List[Tuple[str, str, bool]]:
49
+ """返回 [(key, value, is_complete), ...]。"""
50
+ if not body:
51
+ return []
52
+ entries: List[Tuple[str, str, bool]] = []
53
+ i = 0
54
+ while i < len(body):
55
+ match = _PARAM_OPEN_RE.search(body, i)
56
+ if not match:
57
+ break
58
+ attrs = match.group(1) or ""
59
+ pname = extract_attr_value(attrs, "name")
60
+ if not pname:
61
+ i = match.end()
62
+ continue
63
+ key = normalize_entml_name(pname)
64
+ val_start = match.end()
65
+ close = body.find(_PARAM_CLOSE, val_start)
66
+ if close < 0:
67
+ entries.append((key, _strip_incomplete_markup_suffix(body[val_start:]), False))
68
+ break
69
+ entries.append((key, body[val_start:close], True))
70
+ i = close + len(_PARAM_CLOSE)
71
+ return entries
72
+
73
+
74
+ def build_streaming_json_snapshot(body: str) -> str:
75
+ """构造当前应已发出的 partial_json 累积串(可未完成)。"""
76
+ invoke_closed = _INVOKE_CLOSE in body
77
+ inner = body[: body.index(_INVOKE_CLOSE)] if invoke_closed else body
78
+ entries = _parse_parameter_entries(inner)
79
+ if not entries:
80
+ return ""
81
+
82
+ parts: List[str] = ["{"]
83
+ for idx, (key, value, is_complete) in enumerate(entries):
84
+ if idx > 0:
85
+ parts.append(", ")
86
+ parts.append(json.dumps(key, ensure_ascii=False))
87
+ parts.append(": ")
88
+ parts.append('"')
89
+ parts.append(_json_string_body(value))
90
+ if is_complete:
91
+ parts.append('"')
92
+ else:
93
+ return "".join(parts)
94
+
95
+ if entries and all(entry[2] for entry in entries):
96
+ parts.append("}")
97
+ elif invoke_closed:
98
+ parts.append("}")
99
+ return "".join(parts)
100
+
101
+
102
+ def _parse_partial_parameter_body(body: str) -> Dict[str, Any]:
103
+ obj: Dict[str, Any] = {}
104
+ for key, value, is_complete in _parse_parameter_entries(
105
+ body[: body.index(_INVOKE_CLOSE)] if _INVOKE_CLOSE in body else body
106
+ ):
107
+ if is_complete or value:
108
+ obj[key] = value
109
+ return obj
110
+
111
+
112
+ def encode_streaming_invoke_json(body: str) -> str:
113
+ obj = _parse_partial_parameter_body(body)
114
+ if not obj:
115
+ return ""
116
+ return json.dumps(obj, ensure_ascii=False)
117
+
118
+
119
+ class EntmlInvokeJsonStreamEncoder:
120
+ """invoke 开标签之后,将 growing body 编码为可拼接的 partial_json 增量。"""
121
+
122
+ def __init__(self) -> None:
123
+ self._emitted = ""
124
+
125
+ def poll(self, body: str) -> str:
126
+ snapshot = build_streaming_json_snapshot(body)
127
+ if not snapshot.startswith(self._emitted):
128
+ common = 0
129
+ for a, b in zip(self._emitted, snapshot):
130
+ if a != b:
131
+ break
132
+ common += 1
133
+ delta = snapshot[common:]
134
+ self._emitted = snapshot
135
+ return delta
136
+ delta = snapshot[len(self._emitted) :]
137
+ self._emitted = snapshot
138
+ return delta
139
+
140
+ def reset(self) -> None:
141
+ self._emitted = ""
@@ -62,16 +62,24 @@ _DEFAULT_MAX_BY_LEVEL: Dict[str, int] = {
62
62
 
63
63
  _LEGACY_ON_DEFAULT_MAX = _DEFAULT_MAX_BY_LEVEL["medium"]
64
64
 
65
- _THINKING_BEHAVIOR_ON = """\
65
+ _THINKING_BEHAVIOR_ON_WITH_TOOLS = """\
66
66
  Your default is to think before it answers to give the person the best possible answer. Even for questions that might seem obvious, if there are any signs of lurking complexity, You takes the time to open up an extended thinking block and dig in to make sure it's got the details figured out and isn't just pattern-matching to the familiar. At the end of its thinking, You restates which language it should respond in.
67
67
 
68
68
  You MUST output a <entml:thinking> block before any other content in every reply. Only after that block, output your visible reply and/or <entml:invoke> tool call(s). Never skip the thinking block."""
69
69
 
70
- _THINKING_BEHAVIOR_AUTO = """\
70
+ _THINKING_BEHAVIOR_ON_NO_TOOLS = """\
71
+ Your default is to think before it answers to give the person the best possible answer. Even for questions that might seem obvious, if there are any signs of lurking complexity, You takes the time to open up an extended thinking block and dig in to make sure it's got the details figured out and isn't just pattern-matching to the familiar. At the end of its thinking, You restates which language it should respond in.
72
+
73
+ You MUST output a <entml:thinking> block before any other content in every reply. Only after that block, output your visible reply. Never skip the thinking block."""
74
+
75
+ _THINKING_BEHAVIOR_AUTO_WITH_TOOLS = """\
71
76
  You decide whether extended thinking helps for each reply. When the question has hidden complexity, when tool results need interpretation, or when you are uncertain, open a <entml:thinking> block before continuing and strongly prefer to do so rather than guessing.
72
77
 
73
78
  After completed tool turns appear in conversation history inside <tool> blocks (for example a line like [tool_name: value] followed by its result), strongly consider outputting a <entml:thinking> block before your next visible reply or tool call."""
74
79
 
80
+ _THINKING_BEHAVIOR_AUTO_NO_TOOLS = """\
81
+ You decide whether extended thinking helps for each reply. When the question has hidden complexity or when you are uncertain, open a <entml:thinking> block before continuing and strongly prefer to do so rather than guessing."""
82
+
75
83
 
76
84
  def normalize_thinking_level(level: Any) -> Optional[str]:
77
85
  """将 echotools 思考挡位归一化为 none | low | medium | high | xhigh | max | auto。"""
@@ -155,13 +163,18 @@ def _uses_forced_thinking_behavior(injection_mode: str) -> bool:
155
163
  return injection_mode != "auto"
156
164
 
157
165
 
158
- def _format_thinking_behavior(injection_mode: str) -> str:
159
- body = _THINKING_BEHAVIOR_ON if _uses_forced_thinking_behavior(injection_mode) else _THINKING_BEHAVIOR_AUTO
166
+ def _format_thinking_behavior(injection_mode: str, *, has_tools: bool) -> str:
167
+ if _uses_forced_thinking_behavior(injection_mode):
168
+ body = _THINKING_BEHAVIOR_ON_WITH_TOOLS if has_tools else _THINKING_BEHAVIOR_ON_NO_TOOLS
169
+ else:
170
+ body = _THINKING_BEHAVIOR_AUTO_WITH_TOOLS if has_tools else _THINKING_BEHAVIOR_AUTO_NO_TOOLS
160
171
  return f"<thinking_behavior>\n{body}\n</thinking_behavior>"
161
172
 
162
173
 
163
174
  def build_entml_thinking_section(
164
175
  protocol_options: Optional[Dict[str, Any]] = None,
176
+ *,
177
+ has_tools: bool = True,
165
178
  ) -> str:
166
179
  """按思考挡位构建注入块:thinking_mode + max_thinking_length + thinking_behavior。"""
167
180
  resolved = resolve_thinking_injection(protocol_options)
@@ -173,5 +186,5 @@ def build_entml_thinking_section(
173
186
  if max_length is not None:
174
187
  lines.append(f"<entml:max_thinking_length>{max_length}</entml:max_thinking_length>")
175
188
  lines.append("")
176
- lines.append(_format_thinking_behavior(injection_mode))
189
+ lines.append(_format_thinking_behavior(injection_mode, has_tools=has_tools))
177
190
  return "\n".join(lines)