echotools 2.4.1__tar.gz → 2.4.2__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 (264) hide show
  1. {echotools-2.4.1 → echotools-2.4.2}/PKG-INFO +2 -2
  2. {echotools-2.4.1 → echotools-2.4.2}/README.md +1 -1
  3. {echotools-2.4.1 → echotools-2.4.2}/pyproject.toml +1 -1
  4. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/prompt/prompt_helpers.py +14 -1
  5. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml.py +12 -0
  6. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_patterns/params.py +1 -1
  7. echotools-2.4.2/src/echotools/exec/fncall/protocols/entml_schema/validate.py +218 -0
  8. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_stream/body.py +6 -6
  9. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/fakemarkup.py +30 -2
  10. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/invoke.py +7 -4
  11. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/values.py +17 -8
  12. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/coercion.py +27 -6
  13. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/entml_format.py +16 -1
  14. {echotools-2.4.1 → echotools-2.4.2}/src/echotools.egg-info/PKG-INFO +2 -2
  15. {echotools-2.4.1 → echotools-2.4.2}/src/echotools.egg-info/SOURCES.txt +3 -1
  16. {echotools-2.4.1 → echotools-2.4.2}/src/tests/fixtures/simulated_llm_tool_responses.py +1 -1
  17. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_fake_structure_markup.py +41 -0
  18. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_stream_json_exhaustive.py +1 -1
  19. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_tool_pipeline.py +4 -5
  20. echotools-2.4.2/src/tests/test_multiline_parameter_comprehensive.py +346 -0
  21. echotools-2.4.2/src/tests/test_schema_validate.py +103 -0
  22. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_stream_partial_json_parity.py +2 -2
  23. echotools-2.4.1/src/echotools/exec/fncall/prompt/templates.py +0 -168
  24. {echotools-2.4.1 → echotools-2.4.2}/setup.cfg +0 -0
  25. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/__init__.py +0 -0
  26. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/cache/__init__.py +0 -0
  27. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/cache/list_cache.py +0 -0
  28. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/cache/memory_cache.py +0 -0
  29. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/__init__.py +0 -0
  30. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/base.py +0 -0
  31. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/center.py +0 -0
  32. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/center_notify.py +0 -0
  33. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/center_template.py +0 -0
  34. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/loader.py +0 -0
  35. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/config/merge.py +0 -0
  36. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/errors/__init__.py +0 -0
  37. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/errors/base.py +0 -0
  38. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/errors/classify.py +0 -0
  39. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/errors/common.py +0 -0
  40. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/errors/http.py +0 -0
  41. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/ids/__init__.py +0 -0
  42. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/ids/generator.py +0 -0
  43. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/io/__init__.py +0 -0
  44. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/io/io_utils.py +0 -0
  45. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/io/printstream.py +0 -0
  46. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/logger/__init__.py +0 -0
  47. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/logger/manager.py +0 -0
  48. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/retry/__init__.py +0 -0
  49. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/base/retry/retry.py +0 -0
  50. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/__init__.py +0 -0
  51. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/candidate.py +0 -0
  52. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  53. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  54. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/race.py +0 -0
  55. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/selector.py +0 -0
  56. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/dispatch/usage.py +0 -0
  57. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/__init__.py +0 -0
  58. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  59. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/parsers/stream.py +0 -0
  60. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/parsers/stream_delta.py +0 -0
  61. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/parsers/stream_feed.py +0 -0
  62. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/parsers/stream_final.py +0 -0
  63. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  64. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/prompt/behavior_blocks.py +0 -0
  65. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/prompt/history.py +0 -0
  66. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  67. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  68. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  69. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_patterns/__init__.py +0 -0
  70. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_patterns/invoke.py +0 -0
  71. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_patterns/regex.py +0 -0
  72. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_patterns/strip.py +0 -0
  73. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_schema/__init__.py +0 -0
  74. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_schema/mangled.py +0 -0
  75. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_stream/__init__.py +0 -0
  76. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_stream/snap.py +0 -0
  77. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  78. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
  79. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_think/filter.py +0 -0
  80. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  81. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_think/hold.py +0 -0
  82. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_think/parse.py +0 -0
  83. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/__init__.py +0 -0
  84. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/comment.py +0 -0
  85. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/protocols/entml_tool/tools.py +0 -0
  86. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/registry.py +0 -0
  87. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  88. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/history_markup.py +0 -0
  89. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  90. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  91. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  92. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/keys.py +0 -0
  93. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/lifecycle/__init__.py +0 -0
  94. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/lifecycle/manager.py +0 -0
  95. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/lifecycle/updater.py +0 -0
  96. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  97. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/process/__init__.py +0 -0
  98. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/process/port.py +0 -0
  99. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/protocol/__init__.py +0 -0
  100. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/protocol/base.py +0 -0
  101. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/__init__.py +0 -0
  102. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/conpty.py +0 -0
  103. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/local/__init__.py +0 -0
  104. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/local/core.py +0 -0
  105. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/local/proc.py +0 -0
  106. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/local/unix.py +0 -0
  107. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/local/win.py +0 -0
  108. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/sanitize.py +0 -0
  109. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/session/__init__.py +0 -0
  110. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/session/base.py +0 -0
  111. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/session/client.py +0 -0
  112. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/ssh.py +0 -0
  113. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/exec/terminal/tmux.py +0 -0
  114. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/__init__.py +0 -0
  115. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/charmap.py +0 -0
  116. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/spinner.py +0 -0
  117. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/ui.py +0 -0
  118. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  119. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  120. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  121. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  122. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  123. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  124. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/ui_console.py +0 -0
  125. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/ui_io.py +0 -0
  126. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/ui_log.py +0 -0
  127. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  128. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/ui_text.py +0 -0
  129. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uicore/ui_types.py +0 -0
  130. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_bridge.py +0 -0
  131. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  132. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  133. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  134. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  135. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  136. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  137. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  138. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  139. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  140. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_richcli.py +0 -0
  141. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  142. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  143. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/console/uiwidgets/ui_themes.py +0 -0
  144. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/events/__init__.py +0 -0
  145. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/events/bus.py +0 -0
  146. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/events/event.py +0 -0
  147. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/spinner/__init__.py +0 -0
  148. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/spinner/spinner.py +0 -0
  149. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/spinner/spinner_color.py +0 -0
  150. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/tracing/__init__.py +0 -0
  151. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/tracing/context.py +0 -0
  152. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/tracing/span.py +0 -0
  153. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/tracing/tracer.py +0 -0
  154. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/translate.py +0 -0
  155. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/watcher/__init__.py +0 -0
  156. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/watcher/file_watcher.py +0 -0
  157. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/__init__.py +0 -0
  158. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/application.py +0 -0
  159. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/broker.py +0 -0
  160. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/app.js +0 -0
  161. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  162. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  163. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  164. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  165. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  166. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  167. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  168. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  169. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  170. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  171. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/inputbox.css +0 -0
  172. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/inputbox.js +0 -0
  173. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/style.css +0 -0
  174. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/input_box/voicegif.js +0 -0
  175. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/middleware.py +0 -0
  176. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/stats.py +0 -0
  177. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/media/web/utils.py +0 -0
  178. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/files/__init__.py +0 -0
  179. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/files/file_util.py +0 -0
  180. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/network/__init__.py +0 -0
  181. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/network/http_utils.py +0 -0
  182. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/plugin/__init__.py +0 -0
  183. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/plugin/base.py +0 -0
  184. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/plugin/discovery.py +0 -0
  185. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/plugin/registry.py +0 -0
  186. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/plugin/registry_ops.py +0 -0
  187. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/proxy/__init__.py +0 -0
  188. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/proxy/manager.py +0 -0
  189. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/runtime/__init__.py +0 -0
  190. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/runtime/collector.py +0 -0
  191. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/scheduler/__init__.py +0 -0
  192. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/scheduler/scheduler.py +0 -0
  193. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/sdk/__init__.py +0 -0
  194. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/plat/sdk/facade.py +0 -0
  195. {echotools-2.4.1 → echotools-2.4.2}/src/echotools/version.py +0 -0
  196. {echotools-2.4.1 → echotools-2.4.2}/src/echotools.egg-info/dependency_links.txt +0 -0
  197. {echotools-2.4.1 → echotools-2.4.2}/src/echotools.egg-info/requires.txt +0 -0
  198. {echotools-2.4.1 → echotools-2.4.2}/src/echotools.egg-info/top_level.txt +0 -0
  199. {echotools-2.4.1 → echotools-2.4.2}/src/tests/_check_parity.py +0 -0
  200. {echotools-2.4.1 → echotools-2.4.2}/src/tests/bench_import.py +0 -0
  201. {echotools-2.4.1 → echotools-2.4.2}/src/tests/conftest.py +0 -0
  202. {echotools-2.4.1 → echotools-2.4.2}/src/tests/fixtures/__init__.py +0 -0
  203. {echotools-2.4.1 → echotools-2.4.2}/src/tests/fixtures/entml_golden.py +0 -0
  204. {echotools-2.4.1 → echotools-2.4.2}/src/tests/fixtures/entml_golden_update.py +0 -0
  205. {echotools-2.4.1 → echotools-2.4.2}/src/tests/fixtures/simulated_fake_history_markup_responses.py +0 -0
  206. {echotools-2.4.1 → echotools-2.4.2}/src/tests/fixtures/simulated_fault_thinking_responses.py +0 -0
  207. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_adversarial_stream_splits.py +0 -0
  208. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_cache_retry.py +0 -0
  209. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_completion.py +0 -0
  210. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_config.py +0 -0
  211. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_config_extended.py +0 -0
  212. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_console.py +0 -0
  213. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_coverage_core.py +0 -0
  214. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_dispatch.py +0 -0
  215. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_dispatch_extended.py +0 -0
  216. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_dispatch_race.py +0 -0
  217. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_agent_output_formats.py +0 -0
  218. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_bare_invoke_format.py +0 -0
  219. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_batch_stream_comprehensive.py +0 -0
  220. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_golden.py +0 -0
  221. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_invoke_known_tool_gate.py +0 -0
  222. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_parser_comprehensive.py +0 -0
  223. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_protocol.py +0 -0
  224. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_stream_json.py +0 -0
  225. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_thinking_history.py +0 -0
  226. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_thinking_parse.py +0 -0
  227. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_tool_parse_leak.py +0 -0
  228. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_entml_tools.py +0 -0
  229. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_errors.py +0 -0
  230. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_errors_common.py +0 -0
  231. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_events.py +0 -0
  232. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_events_event.py +0 -0
  233. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_facade.py +0 -0
  234. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fake_history_markup_batch.py +0 -0
  235. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
  236. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
  237. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fake_history_markup_stream.py +0 -0
  238. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fault_thinking_close_batch.py +0 -0
  239. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fault_thinking_close_stream.py +0 -0
  240. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fncall.py +0 -0
  241. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_fncall_stream_thinking.py +0 -0
  242. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_history_markup_filter.py +0 -0
  243. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_ids.py +0 -0
  244. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_imports.py +0 -0
  245. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_io.py +0 -0
  246. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_keys.py +0 -0
  247. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_lifecycle.py +0 -0
  248. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_logger.py +0 -0
  249. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_merge.py +0 -0
  250. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_model_branch_matrix.py +0 -0
  251. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_plugin.py +0 -0
  252. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_printstream.py +0 -0
  253. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_protocols.py +0 -0
  254. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_proxy.py +0 -0
  255. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_proxy_selector.py +0 -0
  256. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_runtime.py +0 -0
  257. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_scheduler.py +0 -0
  258. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  259. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_stream_realtime_parse.py +0 -0
  260. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_tracing.py +0 -0
  261. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_ui_bridge.py +0 -0
  262. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_usage.py +0 -0
  263. {echotools-2.4.1 → echotools-2.4.2}/src/tests/test_web.py +0 -0
  264. {echotools-2.4.1 → echotools-2.4.2}/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.4.1
3
+ Version: 2.4.2
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -58,7 +58,7 @@ Requires-Dist: ruff>=0.4.0; extra == "dev"
58
58
 
59
59
  通用基础设施 SDK:配置、日志、事件、调度、插件、调用链,以及 **entml** LLM 工具调用协议与终端 Console UI。
60
60
 
61
- 完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.1**(详见 [CHANGELOG](docs/CHANGELOG.md))。
61
+ 完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.2**(详见 [CHANGELOG](docs/CHANGELOG.md))。
62
62
 
63
63
  ## 安装
64
64
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  通用基础设施 SDK:配置、日志、事件、调度、插件、调用链,以及 **entml** LLM 工具调用协议与终端 Console UI。
4
4
 
5
- 完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.1**(详见 [CHANGELOG](docs/CHANGELOG.md))。
5
+ 完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.2**(详见 [CHANGELOG](docs/CHANGELOG.md))。
6
6
 
7
7
  ## 安装
8
8
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echotools"
7
- version = "2.4.1"
7
+ version = "2.4.2"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -18,6 +18,18 @@ from .history import (
18
18
  )
19
19
 
20
20
 
21
+ def _clean_assistant_history_content(
22
+ content_str: str,
23
+ protocol: Optional[Any],
24
+ ) -> str:
25
+ if not content_str or protocol is None:
26
+ return content_str
27
+ clean_fn = getattr(protocol, "clean_history_assistant_content", None)
28
+ if clean_fn is None:
29
+ return content_str
30
+ return clean_fn(content_str)
31
+
32
+
21
33
  def _assistant_history_content_blocks(
22
34
  m: Dict[str, Any],
23
35
  content_str: str,
@@ -33,11 +45,12 @@ def _assistant_history_content_blocks(
33
45
  ):
34
46
  reasoning = extract_reasoning_text(m)
35
47
  if reasoning:
48
+ reasoning = _clean_assistant_history_content(reasoning, protocol)
36
49
  thinking_block = format_entml_thinking_history_block(reasoning)
37
50
  if thinking_block:
38
51
  blocks.append(thinking_block)
39
52
  if content_str:
40
- blocks.append(content_str)
53
+ blocks.append(_clean_assistant_history_content(content_str, protocol))
41
54
  return blocks
42
55
 
43
56
 
@@ -18,6 +18,7 @@ from echotools.exec.fncall.protocols.entml_patterns import (
18
18
  )
19
19
  from echotools.exec.fncall.protocols.entml_schema import format_entml_tool_descs
20
20
  from echotools.exec.fncall.protocols.entml_tool.fakemarkup import (
21
+ strip_fake_entml_structure_markup,
21
22
  strip_orphan_entml_close_tags,
22
23
  )
23
24
  from echotools.exec.fncall.protocols.entml_tool.invoke import (
@@ -179,6 +180,7 @@ class EntmlProtocol(ToolProtocol):
179
180
  if tool_calls:
180
181
  clean = strip_actionable_entml_invoke_blocks(clean, known_names=known)
181
182
  clean = strip_tool_entml_residue(clean, known_names=known)
183
+ clean, _ = strip_fake_entml_structure_markup(clean)
182
184
  clean, _ = strip_orphan_entml_close_tags(clean)
183
185
  return (clean, normalize_tool_calls(tool_calls, tools))
184
186
 
@@ -193,6 +195,14 @@ class EntmlProtocol(ToolProtocol):
193
195
  def clean_tags(self, content: str) -> str:
194
196
  return strip_entml_from_content(content)
195
197
 
198
+ def clean_history_assistant_content(self, content: str) -> str:
199
+ """构建 conversation_history 时清理 assistant 正文(伪 history 块 + fakemarkup)。"""
200
+ if not content:
201
+ return content
202
+ cleaned, _ = strip_fake_history_markup(content)
203
+ cleaned, _ = strip_fake_entml_structure_markup(cleaned)
204
+ return cleaned
205
+
196
206
  def clean_tool_tags(
197
207
  self,
198
208
  content: str,
@@ -202,6 +212,8 @@ class EntmlProtocol(ToolProtocol):
202
212
  known = resolve_known_tool_names(tools, schema_index)
203
213
  cleaned = strip_tool_entml_residue(content, known_names=known)
204
214
  cleaned, _ = strip_fake_history_markup(cleaned)
215
+ cleaned, _ = strip_fake_entml_structure_markup(cleaned)
216
+ cleaned, _ = strip_orphan_entml_close_tags(cleaned)
205
217
  return cleaned
206
218
 
207
219
  def format_assistant_tool_calls(
@@ -192,7 +192,7 @@ def parse_sub_tags(
192
192
  args: Dict[str, Any] = {}
193
193
  for m in SUB_TAG_RE.finditer(content):
194
194
  pname = m.group(1).strip()
195
- pval = m.group(2).strip()
195
+ pval = m.group(2) or ""
196
196
  pschema = schema_index.get(func_name, {}).get(pname, {}) if schema_index else {}
197
197
  args[pname] = coerce_entml_parameter_value(pval, pschema or None)
198
198
  return args
@@ -0,0 +1,218 @@
1
+ """JSON Schema 边界校验(coerce 之后):enum、null union、基础类型匹配。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, List, Optional, Sequence
7
+
8
+ _SCALAR_CHECK = {
9
+ "string": lambda v: isinstance(v, str),
10
+ "integer": lambda v: isinstance(v, int) and not isinstance(v, bool),
11
+ "number": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool),
12
+ "boolean": lambda v: isinstance(v, bool),
13
+ "null": lambda v: v is None,
14
+ "array": lambda v: isinstance(v, list),
15
+ "object": lambda v: isinstance(v, dict),
16
+ }
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ParamValidationIssue:
21
+ path: str
22
+ message: str
23
+
24
+ def to_llm_feedback(self) -> str:
25
+ loc = self.path or "(root)"
26
+ return f"Parameter validation failed at {loc}: {self.message}"
27
+
28
+
29
+ class ToolArgValidationError(Exception):
30
+ """工具参数未通过 schema 校验(可回灌模型自修正)。"""
31
+
32
+ def __init__(
33
+ self,
34
+ issues: Sequence[ParamValidationIssue],
35
+ *,
36
+ tool_name: str = "",
37
+ ) -> None:
38
+ self.tool_name = tool_name
39
+ self.issues = list(issues)
40
+ detail = "; ".join(i.message for i in self.issues)
41
+ super().__init__(detail or "tool argument validation failed")
42
+
43
+ def to_llm_feedback(self) -> str:
44
+ lines = [i.to_llm_feedback() for i in self.issues]
45
+ prefix = f"Tool {self.tool_name!r} arguments invalid." if self.tool_name else ""
46
+ body = "\n".join(lines)
47
+ return f"{prefix}\n{body}".strip() if prefix else body
48
+
49
+
50
+ def schema_allows_null(schema: Dict[str, Any]) -> bool:
51
+ if not schema:
52
+ return False
53
+ raw_type = schema.get("type")
54
+ if raw_type == "null":
55
+ return True
56
+ if isinstance(raw_type, list) and "null" in raw_type:
57
+ return True
58
+ for key in ("anyOf", "oneOf"):
59
+ combiner = schema.get(key)
60
+ if not isinstance(combiner, list):
61
+ continue
62
+ for sub in combiner:
63
+ if isinstance(sub, dict) and sub.get("type") == "null":
64
+ return True
65
+ return False
66
+
67
+
68
+ def is_null_literal(raw: str) -> bool:
69
+ if raw is None:
70
+ return True
71
+ return raw.strip().lower() in ("", "null", "none")
72
+
73
+
74
+ def _enum_values(schema: Dict[str, Any]) -> Optional[List[Any]]:
75
+ enum_vals = schema.get("enum")
76
+ if isinstance(enum_vals, list) and enum_vals:
77
+ return list(enum_vals)
78
+ return None
79
+
80
+
81
+ def _type_names(schema: Dict[str, Any]) -> List[str]:
82
+ raw_type = schema.get("type")
83
+ if isinstance(raw_type, str) and raw_type:
84
+ return [raw_type]
85
+ if isinstance(raw_type, list):
86
+ return [t for t in raw_type if isinstance(t, str)]
87
+ enum_vals = schema.get("enum")
88
+ if isinstance(enum_vals, list) and enum_vals:
89
+ first = enum_vals[0]
90
+ if isinstance(first, bool):
91
+ return ["boolean"]
92
+ if isinstance(first, int):
93
+ return ["integer"]
94
+ if isinstance(first, float):
95
+ return ["number"]
96
+ if isinstance(first, str):
97
+ return ["string"]
98
+ if isinstance(first, list):
99
+ return ["array"]
100
+ if isinstance(first, dict):
101
+ return ["object"]
102
+ for combiner_key in ("anyOf", "oneOf"):
103
+ combiner = schema.get(combiner_key)
104
+ if not isinstance(combiner, list):
105
+ continue
106
+ names: List[str] = []
107
+ for sub in combiner:
108
+ if isinstance(sub, dict):
109
+ names.extend(_type_names(sub))
110
+ if names:
111
+ return list(dict.fromkeys(names))
112
+ return []
113
+
114
+
115
+ def _value_matches_types(value: Any, type_names: Sequence[str]) -> bool:
116
+ if not type_names:
117
+ return True
118
+ if value is None:
119
+ return "null" in type_names
120
+ for name in type_names:
121
+ if name == "null":
122
+ continue
123
+ checker = _SCALAR_CHECK.get(name)
124
+ if checker and checker(value):
125
+ return True
126
+ return False
127
+
128
+
129
+ def validate_param_value(
130
+ value: Any,
131
+ schema: Dict[str, Any],
132
+ *,
133
+ path: str = "",
134
+ ) -> List[ParamValidationIssue]:
135
+ if not schema:
136
+ return []
137
+ issues: List[ParamValidationIssue] = []
138
+ enum_vals = _enum_values(schema)
139
+ if enum_vals is not None and value not in enum_vals:
140
+ issues.append(
141
+ ParamValidationIssue(
142
+ path,
143
+ f"value {value!r} is not one of {enum_vals!r}",
144
+ )
145
+ )
146
+ type_names = _type_names(schema)
147
+ if type_names and not _value_matches_types(value, type_names):
148
+ issues.append(
149
+ ParamValidationIssue(
150
+ path,
151
+ f"value {value!r} (type {type(value).__name__}) "
152
+ f"does not match schema types {list(type_names)!r}",
153
+ )
154
+ )
155
+ if isinstance(value, dict):
156
+ props = schema.get("properties") or {}
157
+ if isinstance(props, dict):
158
+ for key, sub_schema in props.items():
159
+ if not isinstance(sub_schema, dict):
160
+ continue
161
+ sub_path = f"{path}.{key}" if path else key
162
+ if key in value:
163
+ issues.extend(
164
+ validate_param_value(value[key], sub_schema, path=sub_path)
165
+ )
166
+ if isinstance(value, list):
167
+ item_schema = schema.get("items")
168
+ if isinstance(item_schema, dict) and item_schema:
169
+ for idx, item in enumerate(value):
170
+ sub_path = f"{path}[{idx}]" if path else f"[{idx}]"
171
+ issues.extend(
172
+ validate_param_value(item, item_schema, path=sub_path)
173
+ )
174
+ return issues
175
+
176
+
177
+ def validate_tool_arguments(
178
+ args: Dict[str, Any],
179
+ func_name: str,
180
+ schema_index: Dict[str, Dict[str, Dict[str, Any]]],
181
+ *,
182
+ required: Optional[Sequence[str]] = None,
183
+ ) -> List[ParamValidationIssue]:
184
+ func_props = schema_index.get(func_name) or {}
185
+ issues: List[ParamValidationIssue] = []
186
+ if required:
187
+ for key in required:
188
+ if key not in args:
189
+ issues.append(
190
+ ParamValidationIssue(key, "required parameter is missing")
191
+ )
192
+ for key, value in args.items():
193
+ pschema = func_props.get(key) or {}
194
+ if pschema:
195
+ issues.extend(validate_param_value(value, pschema, path=key))
196
+ return issues
197
+
198
+
199
+ def format_issues_for_llm(
200
+ issues: Sequence[ParamValidationIssue],
201
+ *,
202
+ tool_name: str = "",
203
+ ) -> str:
204
+ return ToolArgValidationError(list(issues), tool_name=tool_name).to_llm_feedback()
205
+
206
+
207
+ def assert_valid_tool_arguments(
208
+ args: Dict[str, Any],
209
+ func_name: str,
210
+ schema_index: Dict[str, Dict[str, Dict[str, Any]]],
211
+ *,
212
+ required: Optional[Sequence[str]] = None,
213
+ ) -> None:
214
+ issues = validate_tool_arguments(
215
+ args, func_name, schema_index, required=required
216
+ )
217
+ if issues:
218
+ raise ToolArgValidationError(issues, tool_name=func_name)
@@ -127,10 +127,10 @@ def _final_invoke_arguments_json(
127
127
 
128
128
 
129
129
  def _streaming_string_value(raw: str, *, is_complete: bool) -> str:
130
- """与 ``parse_invoke_args`` 的 ``strip()`` 对齐,并避免 XML 换行导致非单调前缀。"""
130
+ """流式 string 参数:保留模型原文;未完成时仅去掉尾部未收齐的标签前缀。"""
131
131
  if is_complete:
132
- return raw.strip()
133
- return raw.lstrip().rstrip("\r\n")
132
+ return raw
133
+ return _strip_incomplete_markup_suffix(raw)
134
134
 
135
135
 
136
136
  def _parameter_json_fragment(
@@ -197,7 +197,7 @@ def _parse_bare_invoke_entries(body: str) -> List[Tuple[str, str, bool, Optional
197
197
  key = normalize_entml_name(match.group(1))
198
198
  if not key:
199
199
  continue
200
- tagged.append((match.start(), key, (match.group(2) or "").strip(), True, None))
200
+ tagged.append((match.start(), key, match.group(2) or "", True, None))
201
201
  for match in BARE_INVOKE_CHILD_OPEN_RE.finditer(body):
202
202
  key = normalize_entml_name(match.group(1))
203
203
  if not key:
@@ -229,7 +229,7 @@ def _parse_direct_child_entries(body: str) -> List[Tuple[str, str, bool, Optiona
229
229
  key = normalize_entml_name(match.group(1))
230
230
  if not key or key.lower() in INVOKE_DIRECT_CHILD_SKIP:
231
231
  continue
232
- tagged.append((match.start(), key, (match.group(2) or "").strip(), True, None))
232
+ tagged.append((match.start(), key, match.group(2) or "", True, None))
233
233
  for match in INVOKE_DIRECT_CHILD_OPEN_RE.finditer(gap_text):
234
234
  key = normalize_entml_name(match.group(1))
235
235
  if not key or key.lower() in INVOKE_DIRECT_CHILD_SKIP:
@@ -284,7 +284,7 @@ def _parse_parameter_entries(body: str) -> List[Tuple[str, str, bool, Optional[s
284
284
  for extra_key, extra_val in extra.items():
285
285
  entries.append((extra_key, str(extra_val), True, None))
286
286
  break
287
- raw = body[val_start:close].strip()
287
+ raw = body[val_start:close]
288
288
  raw, extra = split_mangled_json_param_tail(raw, param_name=key)
289
289
  entries.append((key, raw, True, type_hint))
290
290
  for extra_key, extra_val in extra.items():
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
- from typing import Tuple
6
+ from typing import List, Tuple
7
7
 
8
8
  from .comment import (
9
9
  leading_partial_tool_result_id_comment_len,
@@ -83,6 +83,10 @@ _TRAILING_ORPHAN_ENTML_CLOSE_RE = re.compile(
83
83
  r"(?:\n|\A)\s*</entml:?[a-z0-9_-]*>\s*$",
84
84
  re.IGNORECASE,
85
85
  )
86
+ _ENTML_INVOKE_BLOCK_RE = re.compile(
87
+ r"<entml:invoke\b[^>]*>[\s\S]*?</entml:invoke>",
88
+ re.IGNORECASE,
89
+ )
86
90
 
87
91
 
88
92
  def _strip_fake_entml_result_id_blocks(text: str) -> Tuple[str, bool]:
@@ -184,6 +188,30 @@ def _truncate_unclosed_fake_result_id_tail(text: str) -> Tuple[str, bool]:
184
188
  return text, found
185
189
 
186
190
 
191
+ def _collapse_excess_blank_lines(text: str) -> str:
192
+ """压缩连续空行;``<entml:invoke>`` 块内原样保留(含 parameter 多行值)。"""
193
+ if not text:
194
+ return text
195
+ parts: List[Tuple[str, bool]] = []
196
+ i = 0
197
+ while i < len(text):
198
+ match = _ENTML_INVOKE_BLOCK_RE.search(text, i)
199
+ if not match:
200
+ parts.append((text[i:], False))
201
+ break
202
+ if match.start() > i:
203
+ parts.append((text[i : match.start()], False))
204
+ parts.append((match.group(0), True))
205
+ i = match.end()
206
+ out: List[str] = []
207
+ for chunk, protected in parts:
208
+ if protected:
209
+ out.append(chunk)
210
+ else:
211
+ out.append(re.sub(r"\n{3,}", "\n\n", chunk))
212
+ return "".join(out)
213
+
214
+
187
215
  def strip_fake_entml_structure_markup(content: str) -> Tuple[str, bool]:
188
216
  """batch:id result 整块 → 仅标签 → Tool Result ID 注释。"""
189
217
  if not content:
@@ -200,7 +228,7 @@ def strip_fake_entml_structure_markup(content: str) -> Tuple[str, bool]:
200
228
  found = found or hit
201
229
  text, hit = _strip_orphan_entml_close_leaks(text)
202
230
  found = found or hit
203
- text = re.sub(r"\n{3,}", "\n\n", text)
231
+ text = _collapse_excess_blank_lines(text)
204
232
  return text, found
205
233
 
206
234
 
@@ -23,6 +23,7 @@ from ..entml_patterns.regex import (
23
23
  PARAMETERS_RE,
24
24
  )
25
25
  from ..entml_schema.mangled import split_mangled_json_param_tail
26
+ from .fakemarkup import strip_fake_entml_structure_markup
26
27
  from .values import (
27
28
  _coerce_entml_arg_value,
28
29
  coerce_entml_arguments,
@@ -41,7 +42,7 @@ def _parse_direct_child_tags(
41
42
  key = normalize_entml_name(match.group(1))
42
43
  if not key or key.lower() in INVOKE_DIRECT_CHILD_SKIP or key in args:
43
44
  continue
44
- raw = (match.group(2) or "").strip()
45
+ raw = match.group(2) or ""
45
46
  args[key] = coerce_entml_parameter_value(
46
47
  raw,
47
48
  func_props.get(key) or None,
@@ -58,7 +59,7 @@ def _parse_bare_invoke_children(
58
59
  key = normalize_entml_name(match.group(1))
59
60
  if not key or key in args:
60
61
  continue
61
- raw = (match.group(2) or "").strip()
62
+ raw = match.group(2) or ""
62
63
  args[key] = coerce_entml_parameter_value(
63
64
  raw,
64
65
  func_props.get(key) or None,
@@ -93,7 +94,7 @@ def _fill_parameter_tag_args(
93
94
  if not pname:
94
95
  continue
95
96
  pname = normalize_entml_name(pname)
96
- pval = (param_m.group(2) or "").strip()
97
+ pval = param_m.group(2) or ""
97
98
  pval, extra = split_mangled_json_param_tail(pval, param_name=pname)
98
99
  type_hint = extract_parameter_type_attr(attrs)
99
100
  pschema = func_props.get(pname) or {}
@@ -124,7 +125,7 @@ def parse_invoke_args(
124
125
  params_m = PARAMETERS_RE.search(body)
125
126
  if params_m:
126
127
  return _parse_parameters_block_args(
127
- params_m.group(1).strip(), name, schema_index
128
+ params_m.group(1), name, schema_index
128
129
  )
129
130
  args: Dict[str, Any] = {}
130
131
  _fill_parameter_tag_args(body, args, func_props)
@@ -138,6 +139,8 @@ def parse_entml_tool_calls(
138
139
  tools: Optional[List[Dict[str, Any]]],
139
140
  schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]],
140
141
  ) -> List[Dict[str, Any]]:
142
+ if text:
143
+ text, _ = strip_fake_entml_structure_markup(text)
141
144
  tool_calls: List[Dict[str, Any]] = []
142
145
  known = resolve_known_tool_names(tools, schema_index)
143
146
  for _start, _end, attrs, body in iter_actionable_entml_invoke_blocks(
@@ -1,12 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- from typing import Any, Dict, Optional
4
+ from typing import Any, Dict, Optional, Sequence
5
5
 
6
6
  from echotools.exec.fncall.shared.coercion import (
7
7
  _coerce_param_value,
8
8
  _resolve_effective_type,
9
9
  )
10
+ from echotools.exec.fncall.protocols.entml_schema.validate import (
11
+ assert_valid_tool_arguments,
12
+ )
10
13
 
11
14
  _TYPE_HINT_TO_JSON_TYPE = {
12
15
  "str": "string",
@@ -61,17 +64,16 @@ def coerce_entml_parameter_value(
61
64
  if effective:
62
65
  return _coerce_param_value(raw, effective)
63
66
 
64
- stripped = (raw or "").strip()
65
- if not stripped:
67
+ if raw is None or raw == "":
66
68
  return ""
67
69
 
68
- if stripped[0] in "{[":
70
+ if raw.lstrip().startswith(("{", "[")):
69
71
  try:
70
- return json.loads(stripped)
72
+ return json.loads(raw)
71
73
  except json.JSONDecodeError:
72
- return stripped
74
+ return raw
73
75
 
74
- return stripped
76
+ return raw
75
77
 
76
78
 
77
79
  def effective_entml_param_json_type(
@@ -116,8 +118,11 @@ def coerce_entml_arguments(
116
118
  args: Dict[str, Any],
117
119
  func_name: str,
118
120
  schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]],
121
+ *,
122
+ strict: bool = False,
123
+ required: Optional[Sequence[str]] = None,
119
124
  ) -> Dict[str, Any]:
120
- """对已解析参数字典按工具 schema 做逐项类型转换。"""
125
+ """对已解析参数字典按工具 schema 做逐项类型转换,可选 strict 校验。"""
121
126
  if not schema_index or not func_name:
122
127
  return args
123
128
  func_schema = schema_index.get(func_name) or {}
@@ -131,4 +136,8 @@ def coerce_entml_arguments(
131
136
  out[key] = _coerce_entml_arg_value(value, pschema)
132
137
  else:
133
138
  out[key] = value
139
+ if strict:
140
+ assert_valid_tool_arguments(
141
+ out, func_name, schema_index, required=required
142
+ )
134
143
  return out
@@ -7,9 +7,13 @@
7
7
  from __future__ import annotations
8
8
 
9
9
  import json
10
- from typing import Any, Dict, List, Optional
10
+ from typing import Any, Dict, List, Optional, Sequence
11
11
 
12
12
  from echotools.base.logger.manager import get_logger
13
+ from echotools.exec.fncall.protocols.entml_schema.validate import (
14
+ is_null_literal,
15
+ schema_allows_null,
16
+ )
13
17
 
14
18
  logger = get_logger(__name__)
15
19
 
@@ -51,6 +55,24 @@ def _build_param_schema_index(
51
55
  return index
52
56
 
53
57
 
58
+ def required_params_for_func(
59
+ tools: Optional[List[Dict[str, Any]]],
60
+ func_name: str,
61
+ ) -> List[str]:
62
+ if not tools or not func_name:
63
+ return []
64
+ for tool in tools:
65
+ fn: Dict[str, Any] = tool.get("function", tool) # type: ignore[arg-type]
66
+ if (fn.get("name") or "") != func_name:
67
+ continue
68
+ params = fn.get("parameters") or {}
69
+ required = params.get("required")
70
+ if isinstance(required, list):
71
+ return [str(k) for k in required if k]
72
+ return []
73
+ return []
74
+
75
+
54
76
  def _resolve_effective_type(schema: Dict[str, Any]) -> Optional[str]:
55
77
  """从 JSON Schema 中解析出有效的单一类型字符串。"""
56
78
  if not schema:
@@ -254,17 +276,16 @@ def _coerce_param_value(raw: str, schema: Dict[str, Any]) -> Any:
254
276
  if not schema:
255
277
  return parsed
256
278
 
279
+ if schema_allows_null(schema) and is_null_literal(raw):
280
+ return None
281
+
257
282
  effective_type = _resolve_effective_type(schema)
258
283
 
259
284
  if effective_type is None:
260
285
  return parsed
261
286
 
262
287
  if effective_type == "string":
263
- # string 字段保留原文;仅当整段是 JSON 字符串字面量 ("...") 时才解包
264
- if len(stripped) >= 2 and stripped[0] == '"' and stripped[-1] == '"':
265
- if isinstance(parsed, str):
266
- return parsed
267
- return stripped
288
+ return raw
268
289
 
269
290
  if effective_type == "integer":
270
291
  return _coerce_to_integer(stripped, parsed)
@@ -10,7 +10,6 @@ from echotools.exec.fncall.prompt.behavior_blocks import (
10
10
  format_function_calling_behavior,
11
11
  format_hard_constraint_restatement,
12
12
  )
13
- from echotools.exec.fncall.prompt.templates import _HISTORY_CLARIFY_EN
14
13
  from echotools.exec.fncall.protocols.entml_think.core import (
15
14
  build_entml_thinking_behavior_section,
16
15
  build_entml_thinking_meta_section,
@@ -18,6 +17,22 @@ from echotools.exec.fncall.protocols.entml_think.core import (
18
17
 
19
18
  _ENTML_NAMESPACE_RE = re.compile(r"(</?)entml:", re.IGNORECASE)
20
19
 
20
+ _HISTORY_CLARIFY_EN = (
21
+ "The following is a transcript of completed interactions. Each assistant turn "
22
+ "that called a tool ends at its last <entml:invoke> block; the comment "
23
+ "immediately following it states the environment-generated result id and was "
24
+ "appended by the environment when this turn was logged, not by the assistant "
25
+ "at generation time. The actual result content for every id shown here lives "
26
+ "exclusively in the separate, top-level <entml:funtions_results> block that "
27
+ "follows this history block.\n"
28
+ "IMPORTANT: The agent must never regenerate, continue, or imitate the "
29
+ "id-comment format shown below as part of its own current-turn output.\n"
30
+ "IMPORTANT: The agent must not repeat a tool call using the same tool and "
31
+ "the same parameters as one already recorded below, unless the user "
32
+ "explicitly requests a fresh or updated value.\n"
33
+ "The user's latest message follows below."
34
+ )
35
+
21
36
  _ENTML_INSTRUCTION = """\
22
37
  In this environment you have access to a set of tools you can use to answer the user's question.
23
38
  You can invoke functions by writing a "<entml:invoke>" block like the following as part of your reply to the user:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.4.1
3
+ Version: 2.4.2
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -58,7 +58,7 @@ Requires-Dist: ruff>=0.4.0; extra == "dev"
58
58
 
59
59
  通用基础设施 SDK:配置、日志、事件、调度、插件、调用链,以及 **entml** LLM 工具调用协议与终端 Console UI。
60
60
 
61
- 完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.1**(详见 [CHANGELOG](docs/CHANGELOG.md))。
61
+ 完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.2**(详见 [CHANGELOG](docs/CHANGELOG.md))。
62
62
 
63
63
  ## 安装
64
64