echotools 2.3.82__tar.gz → 2.3.83__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.
- {echotools-2.3.82 → echotools-2.3.83}/PKG-INFO +1 -1
- {echotools-2.3.82 → echotools-2.3.83}/pyproject.toml +1 -1
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/parsers/stream.py +12 -7
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_invoke.py +17 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_patterns.py +87 -1
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_stream_json.py +18 -21
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_think/parse.py +21 -2
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_tool_blocks.py +37 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_protocol.py +49 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fncall_stream_thinking.py +53 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_stream_partial_json_parity.py +51 -0
- {echotools-2.3.82 → echotools-2.3.83}/README.md +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/docs/README.md +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/setup.cfg +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/compat.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/history_markup.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools/version.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools.egg-info/SOURCES.txt +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/_check_parity.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/bench_import.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/conftest.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/fixtures/simulated_fake_history_markup_responses.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/fixtures/simulated_fault_thinking_responses.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_completion.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_config.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_console.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_agent_output_formats.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_batch_stream_comprehensive.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_parser_comprehensive.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_stream_json.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_tool_pipeline.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_errors.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_events.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_facade.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fake_history_markup_batch.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fake_history_markup_stream.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fault_thinking_close_batch.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fault_thinking_close_stream.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_history_markup_filter.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_ids.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_imports.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_io.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_keys.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_logger.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_merge.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_model_branch_matrix.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_stream_realtime_parse.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_usage.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_web.py +0 -0
- {echotools-2.3.82 → echotools-2.3.83}/src/tests/test_web_broker.py +0 -0
|
@@ -439,7 +439,7 @@ class FncallStreamParser:
|
|
|
439
439
|
else:
|
|
440
440
|
tail = ""
|
|
441
441
|
else:
|
|
442
|
-
tail =
|
|
442
|
+
tail = ""
|
|
443
443
|
if tail:
|
|
444
444
|
name = call["function"]["name"]
|
|
445
445
|
if multi:
|
|
@@ -635,10 +635,16 @@ class FncallStreamParser:
|
|
|
635
635
|
stream_safe_visible_prefix,
|
|
636
636
|
)
|
|
637
637
|
|
|
638
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
639
|
+
split_entml_thinking,
|
|
640
|
+
)
|
|
641
|
+
|
|
638
642
|
buf = stream_safe_visible_prefix(
|
|
639
643
|
self._raw_buf,
|
|
640
644
|
thinking_enabled=self._thinking_enabled,
|
|
641
645
|
)
|
|
646
|
+
if self._thinking_enabled and buf:
|
|
647
|
+
buf, _ = split_entml_thinking(buf, thinking_enabled=True)
|
|
642
648
|
return clean_stream_partial_visible(
|
|
643
649
|
buf,
|
|
644
650
|
has_calls=self.has_calls,
|
|
@@ -690,11 +696,6 @@ class FncallStreamParser:
|
|
|
690
696
|
segment,
|
|
691
697
|
thinking_enabled=self._thinking_enabled,
|
|
692
698
|
)
|
|
693
|
-
buf = clean_stream_partial_visible(
|
|
694
|
-
buf,
|
|
695
|
-
has_calls=self.has_calls,
|
|
696
|
-
thinking_enabled=self._thinking_enabled,
|
|
697
|
-
)
|
|
698
699
|
if not buf:
|
|
699
700
|
return ""
|
|
700
701
|
cleaned, _ = strip_fake_history_markup_for_display(buf)
|
|
@@ -703,7 +704,11 @@ class FncallStreamParser:
|
|
|
703
704
|
thinking_enabled=self._thinking_enabled,
|
|
704
705
|
preserve_visible_whitespace=True,
|
|
705
706
|
)
|
|
706
|
-
return
|
|
707
|
+
return clean_stream_partial_visible(
|
|
708
|
+
visible,
|
|
709
|
+
has_calls=self.has_calls,
|
|
710
|
+
thinking_enabled=self._thinking_enabled,
|
|
711
|
+
)
|
|
707
712
|
|
|
708
713
|
def finalize(self) -> Tuple[str, List[Dict[str, Any]]]:
|
|
709
714
|
"""结束流式解析,返回 (清理后文本, tool_calls 列表)。幂等。"""
|
|
@@ -15,6 +15,8 @@ from .entml_patterns import (
|
|
|
15
15
|
extract_parameter_type_attr,
|
|
16
16
|
normalize_entml_name,
|
|
17
17
|
parse_sub_tags,
|
|
18
|
+
split_mangled_json_param_tail,
|
|
19
|
+
synthetic_close_invoke_body,
|
|
18
20
|
)
|
|
19
21
|
from .entml_values import coerce_entml_arguments, coerce_entml_parameter_value
|
|
20
22
|
|
|
@@ -58,6 +60,9 @@ def parse_invoke_args(
|
|
|
58
60
|
schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]],
|
|
59
61
|
) -> Dict[str, Any]:
|
|
60
62
|
func_props = (schema_index or {}).get(name) or {}
|
|
63
|
+
if "</entml:invoke>" in body:
|
|
64
|
+
body = body[: body.index("</entml:invoke>")]
|
|
65
|
+
body = synthetic_close_invoke_body(body)
|
|
61
66
|
|
|
62
67
|
params_m = PARAMETERS_RE.search(body)
|
|
63
68
|
if params_m:
|
|
@@ -81,6 +86,7 @@ def parse_invoke_args(
|
|
|
81
86
|
continue
|
|
82
87
|
pname = normalize_entml_name(pname)
|
|
83
88
|
pval = (param_m.group(2) or "").strip()
|
|
89
|
+
pval, extra = split_mangled_json_param_tail(pval)
|
|
84
90
|
type_hint = extract_parameter_type_attr(attrs)
|
|
85
91
|
pschema = func_props.get(pname) or {}
|
|
86
92
|
args[pname] = coerce_entml_parameter_value(
|
|
@@ -88,6 +94,17 @@ def parse_invoke_args(
|
|
|
88
94
|
pschema or None,
|
|
89
95
|
type_hint=type_hint,
|
|
90
96
|
)
|
|
97
|
+
for extra_key, extra_val in extra.items():
|
|
98
|
+
if extra_key in args:
|
|
99
|
+
continue
|
|
100
|
+
extra_schema = func_props.get(extra_key) or {}
|
|
101
|
+
if isinstance(extra_val, (int, float, bool)):
|
|
102
|
+
args[extra_key] = extra_val
|
|
103
|
+
else:
|
|
104
|
+
args[extra_key] = coerce_entml_parameter_value(
|
|
105
|
+
str(extra_val),
|
|
106
|
+
extra_schema or None,
|
|
107
|
+
)
|
|
91
108
|
|
|
92
109
|
_parse_bare_invoke_children(body, args, func_props)
|
|
93
110
|
_parse_direct_child_tags(body, args, func_props)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
from typing import Any, Dict, Optional
|
|
4
|
+
from typing import Any, Dict, Optional, Tuple
|
|
5
5
|
|
|
6
6
|
from .entml_values import coerce_entml_parameter_value
|
|
7
7
|
|
|
@@ -195,6 +195,92 @@ def _parameter_close_follower_ok(after: str, *, allow_end: bool) -> bool:
|
|
|
195
195
|
return False
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
_PARAM_OPEN_TAG_RE = re.compile(rf"{PARAM_OPEN_PATTERN}([^>]*)>", re.IGNORECASE)
|
|
199
|
+
_MANGLED_PARAM_JSON_TAIL_RE = re.compile(
|
|
200
|
+
r'"\s*,\s*"description"\s*:\s*"((?:[^"\\]|\\.)*)"\s*,\s*"timeout"\s*:\s*(\d+)\s*\}\}?\s*$',
|
|
201
|
+
re.DOTALL,
|
|
202
|
+
)
|
|
203
|
+
_MANGLED_PARAM_JSON_TAIL_START_RE = re.compile(
|
|
204
|
+
r'"\s*,\s*"description"\s*:\s*"',
|
|
205
|
+
re.DOTALL,
|
|
206
|
+
)
|
|
207
|
+
_MANGLED_PARAM_JSON_TAIL_EARLY_RE = re.compile(
|
|
208
|
+
r'"\s*,\s*"(?:description|timeout)\b',
|
|
209
|
+
re.DOTALL,
|
|
210
|
+
)
|
|
211
|
+
_MANGLED_PARAM_JSON_TAIL_IN_PROGRESS_RE = re.compile(
|
|
212
|
+
r'"\s*,\s*"(?:d|t)',
|
|
213
|
+
re.DOTALL,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
_MANGLED_PARAM_JSON_COMMA_QUOTE_END_RE = re.compile(
|
|
218
|
+
r'"\s*,\s*"$',
|
|
219
|
+
re.DOTALL,
|
|
220
|
+
)
|
|
221
|
+
_MANGLED_PARAM_JSON_COMMA_AFTER_QUOTE_RE = re.compile(
|
|
222
|
+
r'"\s*,\s*$',
|
|
223
|
+
re.DOTALL,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def mangled_json_param_tail_in_progress(value: str) -> bool:
|
|
228
|
+
"""parameter 值中出现误写入 JSON 尾缀但尚未收齐时不应继续增长 partial_json。"""
|
|
229
|
+
return bool(value and _MANGLED_PARAM_JSON_TAIL_IN_PROGRESS_RE.search(value))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def split_mangled_json_param_tail(value: str) -> Tuple[str, Dict[str, Any]]:
|
|
233
|
+
"""模型把 ``", "description": ..., "timeout": ...}}`` 误写入 parameter 值时的拆分。"""
|
|
234
|
+
if not value:
|
|
235
|
+
return value, {}
|
|
236
|
+
match = _MANGLED_PARAM_JSON_TAIL_RE.search(value)
|
|
237
|
+
if match:
|
|
238
|
+
command = value[: match.start() + 1]
|
|
239
|
+
extra: Dict[str, Any] = {
|
|
240
|
+
"description": match.group(1),
|
|
241
|
+
"timeout": int(match.group(2)),
|
|
242
|
+
}
|
|
243
|
+
return command, extra
|
|
244
|
+
partial = _MANGLED_PARAM_JSON_TAIL_START_RE.search(value)
|
|
245
|
+
if partial:
|
|
246
|
+
return value[: partial.start() + 1], {}
|
|
247
|
+
early = _MANGLED_PARAM_JSON_TAIL_EARLY_RE.search(value)
|
|
248
|
+
if early:
|
|
249
|
+
return value[: early.start() + 1], {}
|
|
250
|
+
if _MANGLED_PARAM_JSON_TAIL_IN_PROGRESS_RE.search(value):
|
|
251
|
+
matches = list(re.finditer(r'"\s*,\s*"', value))
|
|
252
|
+
if matches:
|
|
253
|
+
return value[: matches[-1].start() + 1], {}
|
|
254
|
+
comma_quote_end = _MANGLED_PARAM_JSON_COMMA_QUOTE_END_RE.search(value)
|
|
255
|
+
if comma_quote_end:
|
|
256
|
+
return value[: comma_quote_end.start() + 1], {}
|
|
257
|
+
comma_after_quote = _MANGLED_PARAM_JSON_COMMA_AFTER_QUOTE_RE.search(value)
|
|
258
|
+
if comma_after_quote:
|
|
259
|
+
return value[: comma_after_quote.start() + 1], {}
|
|
260
|
+
return value, {}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def synthetic_close_invoke_body(inner: str) -> str:
|
|
264
|
+
"""为 force_close / invoke 已闭合但未闭合的 parameter 补齐结构闭合标签。"""
|
|
265
|
+
if not inner:
|
|
266
|
+
return inner
|
|
267
|
+
closed = inner
|
|
268
|
+
if "<entml:parameters>" in closed and "</entml:parameters>" not in closed:
|
|
269
|
+
closed = closed + "</entml:parameters>"
|
|
270
|
+
matches = list(_PARAM_OPEN_TAG_RE.finditer(closed))
|
|
271
|
+
if matches:
|
|
272
|
+
last = matches[-1]
|
|
273
|
+
after = closed[last.end() :]
|
|
274
|
+
open_snip = closed[last.start() : last.end()].lower()
|
|
275
|
+
if open_snip.startswith("<parameter") and not open_snip.startswith("<entml:"):
|
|
276
|
+
need = PARAM_CLOSE_BARE
|
|
277
|
+
else:
|
|
278
|
+
need = PARAM_CLOSE_ENTML
|
|
279
|
+
if need not in after and find_valid_parameter_close(closed, last.end(), allow_end=True) < 0:
|
|
280
|
+
closed = closed + need
|
|
281
|
+
return closed
|
|
282
|
+
|
|
283
|
+
|
|
198
284
|
def find_valid_parameter_close(body: str, search_from: int = 0, *, allow_end: bool = True) -> int:
|
|
199
285
|
"""返回 parameter 闭合标签在 ``body`` 中的起始下标;忽略参数值内的假闭合。"""
|
|
200
286
|
pos = search_from
|
{echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_stream_json.py
RENAMED
|
@@ -18,6 +18,8 @@ from .entml_patterns import (
|
|
|
18
18
|
find_valid_parameter_close,
|
|
19
19
|
normalize_entml_name,
|
|
20
20
|
parameter_close_at,
|
|
21
|
+
split_mangled_json_param_tail,
|
|
22
|
+
synthetic_close_invoke_body,
|
|
21
23
|
)
|
|
22
24
|
from .entml_values import coerce_entml_parameter_value
|
|
23
25
|
from echotools.exec.fncall.shared.coercion import _resolve_effective_type
|
|
@@ -104,24 +106,8 @@ def _effective_param_type(
|
|
|
104
106
|
|
|
105
107
|
|
|
106
108
|
def _synthetic_close_body(inner: str) -> str:
|
|
107
|
-
"""
|
|
108
|
-
|
|
109
|
-
inner = inner[: inner.index(_INVOKE_CLOSE)]
|
|
110
|
-
closed = inner
|
|
111
|
-
if _PARAMETERS_OPEN in closed and _PARAMETERS_CLOSE not in closed:
|
|
112
|
-
closed = closed + _PARAMETERS_CLOSE
|
|
113
|
-
matches = list(_PARAM_OPEN_RE.finditer(closed))
|
|
114
|
-
if matches:
|
|
115
|
-
last = matches[-1]
|
|
116
|
-
after = closed[last.end() :]
|
|
117
|
-
open_snip = closed[last.start() : last.end()].lower()
|
|
118
|
-
if open_snip.startswith("<parameter") and not open_snip.startswith("<entml:"):
|
|
119
|
-
need = PARAM_CLOSE_BARE
|
|
120
|
-
else:
|
|
121
|
-
need = _PARAM_CLOSE
|
|
122
|
-
if need not in after and find_valid_parameter_close(closed, last.end(), allow_end=True) < 0:
|
|
123
|
-
closed = closed + need
|
|
124
|
-
return closed
|
|
109
|
+
"""兼容旧名;委托 ``synthetic_close_invoke_body``。"""
|
|
110
|
+
return synthetic_close_invoke_body(inner)
|
|
125
111
|
|
|
126
112
|
|
|
127
113
|
def _final_invoke_arguments_json(
|
|
@@ -136,7 +122,7 @@ def _final_invoke_arguments_json(
|
|
|
136
122
|
|
|
137
123
|
invoke_closed = _INVOKE_CLOSE in body
|
|
138
124
|
inner = body[: body.index(_INVOKE_CLOSE)] if invoke_closed else body
|
|
139
|
-
parse_body =
|
|
125
|
+
parse_body = synthetic_close_invoke_body(inner)
|
|
140
126
|
args = parse_invoke_args(parse_body, tool_name, schema_index)
|
|
141
127
|
return json.dumps(args, ensure_ascii=False)
|
|
142
128
|
|
|
@@ -312,16 +298,24 @@ def _parse_parameter_entries(body: str) -> List[Tuple[str, str, bool, Optional[s
|
|
|
312
298
|
val_start = match.end()
|
|
313
299
|
close = _resolve_parameter_close(body, val_start)
|
|
314
300
|
if close < 0:
|
|
301
|
+
raw = _incomplete_parameter_raw(body, val_start)
|
|
302
|
+
raw, extra = split_mangled_json_param_tail(raw)
|
|
315
303
|
entries.append(
|
|
316
304
|
(
|
|
317
305
|
key,
|
|
318
|
-
|
|
306
|
+
raw,
|
|
319
307
|
False,
|
|
320
308
|
type_hint,
|
|
321
309
|
)
|
|
322
310
|
)
|
|
311
|
+
for extra_key, extra_val in extra.items():
|
|
312
|
+
entries.append((extra_key, str(extra_val), True, None))
|
|
323
313
|
break
|
|
324
|
-
|
|
314
|
+
raw = body[val_start:close].strip()
|
|
315
|
+
raw, extra = split_mangled_json_param_tail(raw)
|
|
316
|
+
entries.append((key, raw, True, type_hint))
|
|
317
|
+
for extra_key, extra_val in extra.items():
|
|
318
|
+
entries.append((extra_key, str(extra_val), True, None))
|
|
325
319
|
i = close + parameter_close_at(body, close)
|
|
326
320
|
return entries
|
|
327
321
|
|
|
@@ -467,6 +461,9 @@ class EntmlInvokeJsonStreamEncoder:
|
|
|
467
461
|
if a != b:
|
|
468
462
|
break
|
|
469
463
|
common += 1
|
|
464
|
+
if common < len(self._emitted):
|
|
465
|
+
self._emitted = snapshot
|
|
466
|
+
return ""
|
|
470
467
|
delta = snapshot[common:]
|
|
471
468
|
self._emitted = snapshot
|
|
472
469
|
return delta
|
{echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_think/parse.py
RENAMED
|
@@ -79,6 +79,10 @@ def stream_safe_visible_prefix(
|
|
|
79
79
|
)
|
|
80
80
|
if open_at >= 0:
|
|
81
81
|
return buf[:open_at]
|
|
82
|
+
if thinking_enabled:
|
|
83
|
+
visible, thinking = split_entml_thinking(buf, thinking_enabled=True)
|
|
84
|
+
if thinking:
|
|
85
|
+
return visible
|
|
82
86
|
return buf
|
|
83
87
|
|
|
84
88
|
|
|
@@ -124,8 +128,23 @@ def clean_stream_partial_visible(
|
|
|
124
128
|
if not text.strip():
|
|
125
129
|
return ""
|
|
126
130
|
if re.search(r"</?entml:", text, re.IGNORECASE):
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
if thinking_enabled:
|
|
132
|
+
# 保留 thinking 标签供 split_entml_thinking 剥离;仅去掉工具相关 entml 残留。
|
|
133
|
+
text = re.sub(
|
|
134
|
+
r"</?entml:(?!thinking\b)[^>]*>",
|
|
135
|
+
"",
|
|
136
|
+
text,
|
|
137
|
+
flags=re.IGNORECASE,
|
|
138
|
+
)
|
|
139
|
+
text = re.sub(
|
|
140
|
+
r"</?entml:(?!thinking\b)\w*$",
|
|
141
|
+
"",
|
|
142
|
+
text,
|
|
143
|
+
flags=re.IGNORECASE,
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
text = re.sub(r"</?entml:[^>]*>", "", text, flags=re.IGNORECASE)
|
|
147
|
+
text = re.sub(r"</?entml:\w*$", "", text, flags=re.IGNORECASE)
|
|
129
148
|
text = text.rstrip()
|
|
130
149
|
return text
|
|
131
150
|
|
{echotools-2.3.82 → echotools-2.3.83}/src/echotools/exec/fncall/protocols/entml_tool_blocks.py
RENAMED
|
@@ -24,6 +24,15 @@ _INNER_TOOL_TAG_RE = re.compile(
|
|
|
24
24
|
re.IGNORECASE,
|
|
25
25
|
)
|
|
26
26
|
_BRACE_TOOL_HEAD_RE = re.compile(r"\{([A-Za-z][A-Za-z0-9_]*)\s*:\s*")
|
|
27
|
+
# ``{Bash>`` 后接 ``<entml:parameter>`` 的混合格式(``{Edit: json}`` 仍走 history brace)
|
|
28
|
+
_MANGLED_BRACE_ENTML_HEAD_RE = re.compile(
|
|
29
|
+
r"^\s*\{([A-Za-z][A-Za-z0-9_]*)\s*>\s*",
|
|
30
|
+
re.MULTILINE,
|
|
31
|
+
)
|
|
32
|
+
_PARAM_MARKER_RE = re.compile(
|
|
33
|
+
r"<\s*(?:entml:)?parameter\b",
|
|
34
|
+
re.IGNORECASE,
|
|
35
|
+
)
|
|
27
36
|
# 工具结果常见行号泄漏(Read 伪块)— 仅用于多块 brace 判定
|
|
28
37
|
_OUTPUT_LINE_RE = re.compile(r"^\s*\d+\s+\S", re.MULTILINE)
|
|
29
38
|
_SCALAR_ARG_KEYS = ("path", "command", "pattern", "query", "file_path")
|
|
@@ -220,6 +229,30 @@ def _parse_brace_calls(
|
|
|
220
229
|
return [(name, args)]
|
|
221
230
|
|
|
222
231
|
|
|
232
|
+
def _parse_mangled_brace_entml_params(
|
|
233
|
+
body: str,
|
|
234
|
+
known: Set[str],
|
|
235
|
+
schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]],
|
|
236
|
+
) -> List[Tuple[str, Dict[str, Any]]]:
|
|
237
|
+
"""``<tool>\\n{Bash>\\n<entml:parameter>...`` 混合格式。"""
|
|
238
|
+
match = _MANGLED_BRACE_ENTML_HEAD_RE.search(body)
|
|
239
|
+
if not match:
|
|
240
|
+
return []
|
|
241
|
+
name = normalize_entml_name(match.group(1))
|
|
242
|
+
if known and name not in known:
|
|
243
|
+
return []
|
|
244
|
+
rest = body[match.end() :].lstrip()
|
|
245
|
+
if not _PARAM_MARKER_RE.search(rest):
|
|
246
|
+
return []
|
|
247
|
+
from .entml_invoke import parse_invoke_args
|
|
248
|
+
|
|
249
|
+
args = parse_invoke_args(rest, name, schema_index)
|
|
250
|
+
if not args:
|
|
251
|
+
return []
|
|
252
|
+
args = coerce_entml_arguments(args, name, schema_index)
|
|
253
|
+
return [(name, args)]
|
|
254
|
+
|
|
255
|
+
|
|
223
256
|
def parse_tool_block_body(
|
|
224
257
|
body: str,
|
|
225
258
|
*,
|
|
@@ -237,6 +270,10 @@ def parse_tool_block_body(
|
|
|
237
270
|
if inner:
|
|
238
271
|
return inner
|
|
239
272
|
|
|
273
|
+
mangled = _parse_mangled_brace_entml_params(text, known, schema_index)
|
|
274
|
+
if mangled:
|
|
275
|
+
return mangled
|
|
276
|
+
|
|
240
277
|
if not allow_brace_format:
|
|
241
278
|
return []
|
|
242
279
|
|
|
@@ -1414,3 +1414,52 @@ def test_entml_tool_block_bash_with_output_tail_batch_and_finalize() -> None:
|
|
|
1414
1414
|
stream_args = json.loads(stream_calls[0]["function"]["arguments"])
|
|
1415
1415
|
assert stream_args == batch_args
|
|
1416
1416
|
|
|
1417
|
+
|
|
1418
|
+
def test_entml_tool_block_mangled_brace_entml_params() -> None:
|
|
1419
|
+
"""``<tool>{Bash>\\n<entml:parameter>...`` 混合格式 batch/stream 均解析 Bash。"""
|
|
1420
|
+
from pathlib import Path
|
|
1421
|
+
|
|
1422
|
+
from echotools.exec.fncall.parsers.stream import FncallStreamParser
|
|
1423
|
+
|
|
1424
|
+
sample_path = Path(
|
|
1425
|
+
r"X:/Project/Public/Qwen/logs/responses/req-1785297403-7c656ac5fe40.txt"
|
|
1426
|
+
)
|
|
1427
|
+
if not sample_path.is_file():
|
|
1428
|
+
pytest.skip("corpus file not available")
|
|
1429
|
+
sample = sample_path.read_text(encoding="utf-8")
|
|
1430
|
+
tools = [
|
|
1431
|
+
{
|
|
1432
|
+
"type": "function",
|
|
1433
|
+
"function": {
|
|
1434
|
+
"name": "Bash",
|
|
1435
|
+
"parameters": {
|
|
1436
|
+
"type": "object",
|
|
1437
|
+
"properties": {
|
|
1438
|
+
"command": {"type": "string"},
|
|
1439
|
+
"description": {"type": "string"},
|
|
1440
|
+
"timeout": {"type": "integer"},
|
|
1441
|
+
},
|
|
1442
|
+
"required": ["command"],
|
|
1443
|
+
},
|
|
1444
|
+
},
|
|
1445
|
+
}
|
|
1446
|
+
]
|
|
1447
|
+
clean, batch_calls = get_protocol("entml").parse(sample, tools)
|
|
1448
|
+
assert len(batch_calls) == 1
|
|
1449
|
+
batch_args = json.loads(batch_calls[0]["function"]["arguments"])
|
|
1450
|
+
assert batch_calls[0]["function"]["name"] == "Bash"
|
|
1451
|
+
assert "alicdn-analytics" in batch_args["command"]
|
|
1452
|
+
assert batch_args.get("timeout") == 30
|
|
1453
|
+
assert "RC4" in clean
|
|
1454
|
+
|
|
1455
|
+
for chunk in (1, 17, 64):
|
|
1456
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=tools)
|
|
1457
|
+
for i in range(0, len(sample), chunk):
|
|
1458
|
+
parser.feed(sample[i : i + chunk])
|
|
1459
|
+
stream_clean, stream_calls = parser.finalize()
|
|
1460
|
+
assert len(stream_calls) == 1, f"chunk={chunk}"
|
|
1461
|
+
stream_args = json.loads(stream_calls[0]["function"]["arguments"])
|
|
1462
|
+
assert stream_args == batch_args, f"chunk={chunk}"
|
|
1463
|
+
assert len(parser.partial_thinking) > 100, f"chunk={chunk}"
|
|
1464
|
+
assert "RC4" in stream_clean or "RC4" in parser.partial_text, f"chunk={chunk}"
|
|
1465
|
+
|
|
@@ -90,6 +90,59 @@ def test_thinking_close_and_invoke_in_one_chunk() -> None:
|
|
|
90
90
|
assert "reason" not in clean or clean.strip() == ""
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
def test_thinking_close_does_not_leak_into_partial_text() -> None:
|
|
94
|
+
"""thinking 闭合后 partial_text 不得重复输出思考正文(Rogator text/reasoning 双发)。"""
|
|
95
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
96
|
+
thinking_body = (
|
|
97
|
+
"The concurrent test shows SSE received 0 bytes even though BidiAppend returned 200.\n"
|
|
98
|
+
"Default reply language: Simplified Chinese\n"
|
|
99
|
+
)
|
|
100
|
+
answer = "并发测试显示SSE线程收到状态200但0字节数据。"
|
|
101
|
+
text = (
|
|
102
|
+
f"<entml:thinking>\n{thinking_body}</entml:thinking>\n\n{answer}\n{INVOKE}"
|
|
103
|
+
)
|
|
104
|
+
dup_count = 0
|
|
105
|
+
for i in range(0, len(text), 17):
|
|
106
|
+
parser.feed(text[i : i + 17])
|
|
107
|
+
pt = parser.partial_text
|
|
108
|
+
pth = parser.partial_thinking
|
|
109
|
+
if pth and thinking_body[:40] in pt:
|
|
110
|
+
dup_count += 1
|
|
111
|
+
clean, calls = parser.finalize()
|
|
112
|
+
assert dup_count == 0, "thinking body leaked into partial_text during stream"
|
|
113
|
+
assert thinking_body.strip() in parser.partial_thinking
|
|
114
|
+
assert "The concurrent test shows" not in clean
|
|
115
|
+
assert answer in clean
|
|
116
|
+
assert len(calls) == 1
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@pytest.mark.parametrize("chunk", [1, 17, 64])
|
|
120
|
+
def test_thinking_then_answer_no_partial_text_dup(chunk: int) -> None:
|
|
121
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
122
|
+
text = (
|
|
123
|
+
"<entml:thinking>\nlong reasoning line one\nline two\n</entml:thinking>\n"
|
|
124
|
+
"visible answer\n"
|
|
125
|
+
f"{INVOKE}"
|
|
126
|
+
)
|
|
127
|
+
for i in range(0, len(text), chunk):
|
|
128
|
+
parser.feed(text[i : i + chunk])
|
|
129
|
+
assert "long reasoning" not in parser.partial_text
|
|
130
|
+
clean, calls = parser.finalize()
|
|
131
|
+
assert "long reasoning" in parser.partial_thinking
|
|
132
|
+
assert "visible answer" in clean
|
|
133
|
+
assert "long reasoning" not in clean
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
137
|
+
chunks = ["<entml:thinking>reason</entml:thinking>", INVOKE]
|
|
138
|
+
for chunk in chunks:
|
|
139
|
+
parser.feed(chunk)
|
|
140
|
+
clean, calls = parser.finalize()
|
|
141
|
+
assert parser.partial_thinking.strip() == "reason"
|
|
142
|
+
assert len(calls) == 1
|
|
143
|
+
assert "reason" not in clean or clean.strip() == ""
|
|
144
|
+
|
|
145
|
+
|
|
93
146
|
def test_no_thinking_invoke_still_works() -> None:
|
|
94
147
|
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
95
148
|
parser.feed(f"Sure.\n{INVOKE}")
|
|
@@ -139,6 +139,57 @@ def test_read_anyof_integer_stream_json_buf_matches_batch() -> None:
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
BASH_TOOLS = [
|
|
143
|
+
{
|
|
144
|
+
"type": "function",
|
|
145
|
+
"function": {
|
|
146
|
+
"name": "Bash",
|
|
147
|
+
"parameters": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"properties": {
|
|
150
|
+
"command": {"type": "string"},
|
|
151
|
+
"description": {"type": "string"},
|
|
152
|
+
"timeout": {"type": "integer"},
|
|
153
|
+
},
|
|
154
|
+
"required": ["command"],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_mangled_json_tail_in_command_param_batch_and_stream() -> None:
|
|
162
|
+
"""未闭合 parameter + JSON 尾缀误写入 command:batch/stream json_buf 须可解析且一致。"""
|
|
163
|
+
from pathlib import Path
|
|
164
|
+
|
|
165
|
+
text = Path(
|
|
166
|
+
r"X:/Project/Public/Qwen/logs/responses/req-1785296513-7782eb161d4d.txt"
|
|
167
|
+
).read_text(encoding="utf-8")
|
|
168
|
+
proto = get_protocol("entml")
|
|
169
|
+
batch_args = json.loads(proto.parse(text, BASH_TOOLS)[1][0]["function"]["arguments"])
|
|
170
|
+
assert "command" in batch_args
|
|
171
|
+
assert batch_args["command"].endswith("head -60\"")
|
|
172
|
+
assert "description" not in batch_args["command"]
|
|
173
|
+
assert batch_args.get("description")
|
|
174
|
+
assert batch_args.get("timeout") == 30000
|
|
175
|
+
|
|
176
|
+
for chunk in (1, 17, 64):
|
|
177
|
+
parser = FncallStreamParser(protocol=proto, tools=BASH_TOOLS)
|
|
178
|
+
json_buf = ""
|
|
179
|
+
for i in range(0, len(text), chunk):
|
|
180
|
+
parser.feed(text[i : i + chunk])
|
|
181
|
+
while True:
|
|
182
|
+
delta = parser.consume_stream_delta()
|
|
183
|
+
if not delta:
|
|
184
|
+
break
|
|
185
|
+
json_buf += delta[1]
|
|
186
|
+
comp = parser.complete_stream_delta_if_needed()
|
|
187
|
+
if comp:
|
|
188
|
+
json_buf += comp[1]
|
|
189
|
+
parser.finalize()
|
|
190
|
+
assert json.loads(json_buf) == batch_args, f"chunk={chunk}"
|
|
191
|
+
|
|
192
|
+
|
|
142
193
|
def test_parameters_block_no_stream_until_closed() -> None:
|
|
143
194
|
body = (
|
|
144
195
|
"<entml:parameters>\n"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|