echotools 2.3.84__tar.gz → 2.3.86__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.84 → echotools-2.3.86}/PKG-INFO +1 -1
- {echotools-2.3.84 → echotools-2.3.86}/pyproject.toml +1 -1
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/parsers/stream.py +17 -3
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml.py +5 -15
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_patterns.py +1 -1
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_stream_json.py +15 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_think/parse.py +91 -34
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_tool_blocks.py +2 -2
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/history_markup.py +6 -5
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/fixtures/simulated_fake_history_markup_responses.py +3 -3
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/fixtures/simulated_fault_thinking_responses.py +1 -1
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/fixtures/simulated_llm_tool_responses.py +2 -2
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_parser_comprehensive.py +1 -1
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_protocol.py +111 -16
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_tool_pipeline.py +11 -9
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fncall_stream_thinking.py +20 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_history_markup_filter.py +2 -2
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_stream_partial_json_parity.py +37 -0
- {echotools-2.3.84 → echotools-2.3.86}/README.md +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/docs/README.md +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/setup.cfg +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/compat.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools/version.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools.egg-info/SOURCES.txt +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/bench_import.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/conftest.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_completion.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_config.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_console.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_agent_output_formats.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_batch_stream_comprehensive.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_stream_json.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_errors.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_events.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_facade.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fake_history_markup_batch.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fake_history_markup_stream.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fault_thinking_close_batch.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fault_thinking_close_stream.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_ids.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_imports.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_io.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_keys.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_logger.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_merge.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_model_branch_matrix.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_stream_realtime_parse.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_usage.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_web.py +0 -0
- {echotools-2.3.84 → echotools-2.3.86}/src/tests/test_web_broker.py +0 -0
|
@@ -273,6 +273,15 @@ class FncallStreamParser:
|
|
|
273
273
|
self._waiting_tail = remain
|
|
274
274
|
return
|
|
275
275
|
|
|
276
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
277
|
+
invoke_index_inside_any_thinking_block,
|
|
278
|
+
)
|
|
279
|
+
if invoke_index_inside_any_thinking_block(
|
|
280
|
+
text, pos, thinking_enabled=self._thinking_enabled,
|
|
281
|
+
):
|
|
282
|
+
self._append_content_text(text)
|
|
283
|
+
return
|
|
284
|
+
|
|
276
285
|
if pos > 0:
|
|
277
286
|
self._append_content_text(text[:pos])
|
|
278
287
|
|
|
@@ -319,10 +328,10 @@ class FncallStreamParser:
|
|
|
319
328
|
if found:
|
|
320
329
|
if self._thinking_filter is not None:
|
|
321
330
|
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
322
|
-
|
|
331
|
+
invoke_index_inside_any_thinking_block,
|
|
323
332
|
)
|
|
324
|
-
if
|
|
325
|
-
combined, pos, thinking_enabled=self._thinking_enabled
|
|
333
|
+
if invoke_index_inside_any_thinking_block(
|
|
334
|
+
combined, pos, thinking_enabled=self._thinking_enabled,
|
|
326
335
|
):
|
|
327
336
|
self._feed_waiting_thinking_plain(combined)
|
|
328
337
|
return
|
|
@@ -750,6 +759,11 @@ class FncallStreamParser:
|
|
|
750
759
|
split_entml_thinking,
|
|
751
760
|
)
|
|
752
761
|
|
|
762
|
+
_, batch_thinking = split_entml_thinking(
|
|
763
|
+
self._raw_buf, thinking_enabled=self._thinking_enabled
|
|
764
|
+
)
|
|
765
|
+
if batch_thinking:
|
|
766
|
+
self._thinking_parts = [batch_thinking]
|
|
753
767
|
display_text, _ = split_entml_thinking(
|
|
754
768
|
clean_text, thinking_enabled=self._thinking_enabled
|
|
755
769
|
)
|
|
@@ -42,26 +42,15 @@ from echotools.exec.fncall.shared.normalization import (
|
|
|
42
42
|
)
|
|
43
43
|
from echotools.exec.protocol.base import ToolProtocol
|
|
44
44
|
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
r"<entml:[a-zA-Z_][\w]*\b[^>]*>.*?</entml:[a-zA-Z_][\w]*>",
|
|
48
|
-
re.DOTALL,
|
|
49
|
-
)
|
|
50
|
-
_ENTML_SELF_CLOSING_RE = re.compile(r"<entml:[a-zA-Z_][\w]*\b[^>]*/>", re.DOTALL)
|
|
51
|
-
_ENTML_ORPHAN_CLOSE_RE = re.compile(r"</entml:[a-zA-Z_][\w]*>", re.DOTALL)
|
|
52
|
-
_ENTML_ORPHAN_OPEN_RE = re.compile(r"<entml:[a-zA-Z_][\w]*\b[^>]*>", re.DOTALL)
|
|
45
|
+
# user 消息:仅去掉标签名中的 entml: 命名空间,保留标签与正文。
|
|
46
|
+
_ENTML_NAMESPACE_RE = re.compile(r"(</?)entml:", re.IGNORECASE)
|
|
53
47
|
|
|
54
48
|
|
|
55
49
|
def strip_entml_from_content(content: str) -> str:
|
|
56
|
-
"""
|
|
50
|
+
"""user 消息:去掉 ``entml:`` 标签前缀,保留标签结构与正文(含 ``//``)。"""
|
|
57
51
|
if not content:
|
|
58
52
|
return content
|
|
59
|
-
|
|
60
|
-
cleaned = _ENTML_PAIR_RE.sub("", cleaned)
|
|
61
|
-
cleaned = _ENTML_SELF_CLOSING_RE.sub("", cleaned)
|
|
62
|
-
cleaned = _ENTML_ORPHAN_CLOSE_RE.sub("", cleaned)
|
|
63
|
-
cleaned = _ENTML_ORPHAN_OPEN_RE.sub("", cleaned)
|
|
64
|
-
return cleaned.strip()
|
|
53
|
+
return _ENTML_NAMESPACE_RE.sub(r"\1", content)
|
|
65
54
|
|
|
66
55
|
|
|
67
56
|
def _parse_tool_call_args(tc: Dict[str, Any]) -> tuple[str, Dict[str, Any]]:
|
|
@@ -321,6 +310,7 @@ class EntmlProtocol(ToolProtocol):
|
|
|
321
310
|
return tool_calls
|
|
322
311
|
|
|
323
312
|
def clean_tags(self, content: str) -> str:
|
|
313
|
+
"""user 消息:仅去掉 ``entml:`` 前缀,不做其它剥离。"""
|
|
324
314
|
return strip_entml_from_content(content)
|
|
325
315
|
|
|
326
316
|
def clean_tool_tags(self, content: str) -> str:
|
|
@@ -51,7 +51,7 @@ PARAMETERS_RE = re.compile(
|
|
|
51
51
|
r"<entml:parameters>([\s\S]*?)</entml:parameters>",
|
|
52
52
|
re.DOTALL,
|
|
53
53
|
)
|
|
54
|
-
# invoke 内裸子标签(非 parameter
|
|
54
|
+
# invoke 内裸子标签(非 parameter 包裹),如部分 agent 输出的 description/timeout。
|
|
55
55
|
BARE_INVOKE_CHILD_RE = re.compile(
|
|
56
56
|
r"<entml:(description|timeout)>([\s\S]*?)</entml:\1>",
|
|
57
57
|
re.DOTALL | re.IGNORECASE,
|
{echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_stream_json.py
RENAMED
|
@@ -42,6 +42,19 @@ def _strip_incomplete_markup_suffix(value: str) -> str:
|
|
|
42
42
|
return value
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _strip_incomplete_child_close_suffix(value: str, key: str) -> str:
|
|
46
|
+
"""流式未闭合 bare/direct 子标签:去掉尾部尚未收齐的 ``</...>``。"""
|
|
47
|
+
value = _strip_incomplete_markup_suffix(value)
|
|
48
|
+
lt = value.rfind("<")
|
|
49
|
+
if lt < 0:
|
|
50
|
+
return value
|
|
51
|
+
tail = value[lt:]
|
|
52
|
+
for close in (f"</entml:{key}>", f"</{key}>"):
|
|
53
|
+
if close.startswith(tail) and tail != close:
|
|
54
|
+
return value[:lt]
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
45
58
|
def split_invoke_open(buffer: str) -> Optional[Tuple[str, int]]:
|
|
46
59
|
"""返回当前正在流式写入的 invoke(最后一个有效开标签)。"""
|
|
47
60
|
search_from = 0
|
|
@@ -177,6 +190,7 @@ def _parse_bare_invoke_entries(body: str) -> List[Tuple[str, str, bool, Optional
|
|
|
177
190
|
tail = match.group(2) or ""
|
|
178
191
|
if close in tail:
|
|
179
192
|
continue
|
|
193
|
+
tail = _strip_incomplete_child_close_suffix(tail, key)
|
|
180
194
|
tagged.append((match.start(), key, tail, False, None))
|
|
181
195
|
tagged.sort(key=lambda item: item[0])
|
|
182
196
|
seen: set[str] = set()
|
|
@@ -236,6 +250,7 @@ def _parse_direct_child_entries(body: str) -> List[Tuple[str, str, bool, Optiona
|
|
|
236
250
|
tail = match.group(2) or ""
|
|
237
251
|
if close in tail:
|
|
238
252
|
continue
|
|
253
|
+
tail = _strip_incomplete_child_close_suffix(tail, key)
|
|
239
254
|
tagged.append((match.start(), key, tail, False, None))
|
|
240
255
|
tagged.sort(key=lambda item: item[0])
|
|
241
256
|
seen: set[str] = set()
|
{echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_think/parse.py
RENAMED
|
@@ -210,6 +210,19 @@ def _find_earliest_thinking_open(
|
|
|
210
210
|
return entml_at, False
|
|
211
211
|
|
|
212
212
|
|
|
213
|
+
def tool_markup_follows_entml_thinking_close(text: str) -> bool:
|
|
214
|
+
"""``</entml:thinking>`` 后若紧跟真实工具 markup,则 fault ``</thinking>`` 与 entml 闭标签间为可见正文。"""
|
|
215
|
+
if not text:
|
|
216
|
+
return False
|
|
217
|
+
return bool(
|
|
218
|
+
re.search(
|
|
219
|
+
r"^\s*(?:<tool\s*>|<entml:invoke\b)",
|
|
220
|
+
text,
|
|
221
|
+
re.IGNORECASE | re.DOTALL,
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
213
226
|
def _find_thinking_close(
|
|
214
227
|
text: str,
|
|
215
228
|
body_start: int,
|
|
@@ -252,6 +265,9 @@ def _find_thinking_close(
|
|
|
252
265
|
if entml_after < 0 or invoke_abs < entml_after:
|
|
253
266
|
return fault_close, len(_FAULT_THINKING_CLOSE)
|
|
254
267
|
if entml_after >= 0:
|
|
268
|
+
after_entml = text[entml_after + len(_THINKING_CLOSE) :]
|
|
269
|
+
if tool_markup_follows_entml_thinking_close(after_entml):
|
|
270
|
+
return fault_close, len(_FAULT_THINKING_CLOSE)
|
|
255
271
|
return entml_after, len(_THINKING_CLOSE)
|
|
256
272
|
return -1, 0
|
|
257
273
|
|
|
@@ -353,26 +369,49 @@ def invoke_index_inside_unclosed_thinking(
|
|
|
353
369
|
thinking_enabled: bool = True,
|
|
354
370
|
) -> bool:
|
|
355
371
|
"""``invoke_at`` 是否落在尚未闭合的 thinking 块内。"""
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
think_open, opened_plain = _find_earliest_thinking_open(
|
|
359
|
-
text, thinking_enabled=thinking_enabled
|
|
372
|
+
return invoke_index_inside_any_thinking_block(
|
|
373
|
+
text, invoke_at, thinking_enabled=thinking_enabled, unclosed_only=True,
|
|
360
374
|
)
|
|
361
|
-
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def invoke_index_inside_any_thinking_block(
|
|
378
|
+
text: str,
|
|
379
|
+
invoke_at: int,
|
|
380
|
+
*,
|
|
381
|
+
thinking_enabled: bool = True,
|
|
382
|
+
unclosed_only: bool = False,
|
|
383
|
+
) -> bool:
|
|
384
|
+
"""``invoke_at`` 是否落在 thinking 块内(``unclosed_only`` 时仅检查未闭合块)。"""
|
|
385
|
+
if invoke_at < 0:
|
|
362
386
|
return False
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
387
|
+
i = 0
|
|
388
|
+
while i < len(text):
|
|
389
|
+
open_at, opened_plain = _find_earliest_thinking_open(
|
|
390
|
+
text, i, thinking_enabled=thinking_enabled,
|
|
391
|
+
)
|
|
392
|
+
if open_at < 0:
|
|
393
|
+
break
|
|
394
|
+
gt = text.find(">", open_at)
|
|
395
|
+
if gt < 0:
|
|
396
|
+
return invoke_at > open_at
|
|
397
|
+
body_start = gt + 1
|
|
398
|
+
close_at, close_len = _find_thinking_close(
|
|
399
|
+
text,
|
|
400
|
+
body_start,
|
|
401
|
+
opened_plain=opened_plain,
|
|
402
|
+
thinking_enabled=thinking_enabled,
|
|
403
|
+
)
|
|
404
|
+
if close_at < 0:
|
|
405
|
+
if open_at < invoke_at:
|
|
406
|
+
return True
|
|
407
|
+
break
|
|
408
|
+
block_end = close_at + close_len
|
|
409
|
+
if open_at < invoke_at < block_end:
|
|
410
|
+
return True
|
|
411
|
+
if unclosed_only:
|
|
412
|
+
return False
|
|
413
|
+
i = block_end
|
|
414
|
+
return False
|
|
376
415
|
|
|
377
416
|
|
|
378
417
|
def split_entml_thinking(
|
|
@@ -415,6 +454,18 @@ def split_entml_thinking(
|
|
|
415
454
|
|
|
416
455
|
parts.append(text[body_start:close_at])
|
|
417
456
|
i = close_at + close_len
|
|
457
|
+
if (
|
|
458
|
+
close_len == len(_FAULT_THINKING_CLOSE)
|
|
459
|
+
and not opened_plain
|
|
460
|
+
and thinking_enabled
|
|
461
|
+
):
|
|
462
|
+
entml_after = text.find(_THINKING_CLOSE, i)
|
|
463
|
+
if entml_after >= 0:
|
|
464
|
+
after_entml = text[entml_after + len(_THINKING_CLOSE) :]
|
|
465
|
+
if tool_markup_follows_entml_thinking_close(after_entml):
|
|
466
|
+
clean_parts.append(text[i:entml_after])
|
|
467
|
+
i = entml_after + len(_THINKING_CLOSE)
|
|
468
|
+
continue
|
|
418
469
|
|
|
419
470
|
clean = "".join(clean_parts)
|
|
420
471
|
thinking = "\n".join(part.strip() for part in parts if part.strip())
|
|
@@ -426,16 +477,23 @@ def split_entml_thinking(
|
|
|
426
477
|
|
|
427
478
|
|
|
428
479
|
def strip_orphan_thinking_close_prefix(text: str) -> str:
|
|
429
|
-
"""
|
|
480
|
+
"""去掉可见正文开头/末尾的 orphan ``</entml:thinking>``(流式分片边界)。"""
|
|
430
481
|
if not text:
|
|
431
482
|
return text
|
|
432
|
-
|
|
483
|
+
text = re.sub(
|
|
433
484
|
r"^\s*</entml:thinking>\s*",
|
|
434
485
|
"",
|
|
435
486
|
text,
|
|
436
487
|
count=1,
|
|
437
488
|
flags=re.IGNORECASE,
|
|
438
489
|
)
|
|
490
|
+
return re.sub(
|
|
491
|
+
r"\s*</entml:thinking>\s*$",
|
|
492
|
+
"",
|
|
493
|
+
text,
|
|
494
|
+
count=1,
|
|
495
|
+
flags=re.IGNORECASE,
|
|
496
|
+
)
|
|
439
497
|
|
|
440
498
|
|
|
441
499
|
def _hold_prefix(buffer: str, tag: str) -> Tuple[str, str]:
|
|
@@ -510,20 +568,27 @@ class EntmlThinkingStreamFilter:
|
|
|
510
568
|
return True
|
|
511
569
|
|
|
512
570
|
def _try_resolve_fault_watch_on_entml_close(self, out: List[Tuple[str, str]]) -> bool:
|
|
513
|
-
"""未见 invoke 时出现 ``</entml:thinking>`` →
|
|
571
|
+
"""未见 invoke 时出现 ``</entml:thinking>`` → 若其后为 tool/invoke 则中间段为可见正文。"""
|
|
514
572
|
if not self._fault_watch:
|
|
515
573
|
return False
|
|
516
574
|
close_at = self._fault_buffer.find(_THINKING_CLOSE)
|
|
517
575
|
if close_at < 0:
|
|
518
576
|
return False
|
|
519
|
-
|
|
520
|
-
if
|
|
521
|
-
|
|
577
|
+
after_entml = self._fault_buffer[close_at + len(_THINKING_CLOSE) :]
|
|
578
|
+
if tool_markup_follows_entml_thinking_close(after_entml):
|
|
579
|
+
middle = self._fault_buffer[len(_FAULT_THINKING_CLOSE) : close_at]
|
|
580
|
+
visible = middle.strip()
|
|
581
|
+
if visible:
|
|
582
|
+
out.append(("content", visible))
|
|
583
|
+
else:
|
|
584
|
+
emitted = self._emit_thinking_piece(self._fault_buffer[:close_at])
|
|
585
|
+
if emitted:
|
|
586
|
+
out.append(("thinking", emitted))
|
|
522
587
|
self._in_block = False
|
|
523
588
|
self._opened_plain = False
|
|
524
589
|
self._fault_watch = False
|
|
525
590
|
self._thinking_started = False
|
|
526
|
-
self._pending =
|
|
591
|
+
self._pending = after_entml
|
|
527
592
|
self._fault_buffer = ""
|
|
528
593
|
return True
|
|
529
594
|
|
|
@@ -574,15 +639,7 @@ class EntmlThinkingStreamFilter:
|
|
|
574
639
|
self._pending = ""
|
|
575
640
|
return False
|
|
576
641
|
|
|
577
|
-
|
|
578
|
-
if tool_hold:
|
|
579
|
-
if safe:
|
|
580
|
-
emitted = self._emit_thinking_piece(safe)
|
|
581
|
-
if emitted:
|
|
582
|
-
out.append(("thinking", emitted))
|
|
583
|
-
self._pending = tool_hold
|
|
584
|
-
return False
|
|
585
|
-
|
|
642
|
+
# thinking 块内 ``<entml:invoke>`` 等仅为 prose/示例时仍计入 thinking,不按工具 hold 截断。
|
|
586
643
|
safe, hold = _hold_prefix(self._pending, _THINKING_CLOSE)
|
|
587
644
|
if not hold and self._thinking_enabled:
|
|
588
645
|
safe, hold = _hold_prefix(safe, _FAULT_THINKING_CLOSE)
|
{echotools-2.3.84 → echotools-2.3.86}/src/echotools/exec/fncall/protocols/entml_tool_blocks.py
RENAMED
|
@@ -24,9 +24,9 @@ _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)
|
|
27
|
+
# ``{Bash>`` / ``{Read}`` 后接 ``<entml:parameter>`` 的混合格式(``{Edit: json}`` 仍走 history brace)
|
|
28
28
|
_MANGLED_BRACE_ENTML_HEAD_RE = re.compile(
|
|
29
|
-
r"^\s*\{([A-Za-z][A-Za-z0-9_]*)\s
|
|
29
|
+
r"^\s*\{([A-Za-z][A-Za-z0-9_]*)\s*(?:>|\})\s*",
|
|
30
30
|
re.MULTILINE,
|
|
31
31
|
)
|
|
32
32
|
_PARAM_MARKER_RE = re.compile(
|
|
@@ -98,10 +98,11 @@ def _paired_block_glued_brace_re(tag: str) -> re.Pattern[str]:
|
|
|
98
98
|
)
|
|
99
99
|
|
|
100
100
|
|
|
101
|
-
def
|
|
101
|
+
def _orphan_close_line_re(tag: str) -> re.Pattern[str]:
|
|
102
|
+
"""无配对开标签的 ``</tag>`` 行:仅删该行,不删其前的合法可见正文。"""
|
|
102
103
|
return re.compile(
|
|
103
|
-
rf"
|
|
104
|
-
re.IGNORECASE,
|
|
104
|
+
rf"^\s*</{tag}\s*>\s*$",
|
|
105
|
+
re.IGNORECASE | re.MULTILINE,
|
|
105
106
|
)
|
|
106
107
|
|
|
107
108
|
|
|
@@ -193,9 +194,9 @@ def _strip_fake_blocks_in_unprotected(
|
|
|
193
194
|
break
|
|
194
195
|
found = True
|
|
195
196
|
text = new_text
|
|
196
|
-
close_re =
|
|
197
|
+
close_re = _orphan_close_line_re(tag)
|
|
197
198
|
while True:
|
|
198
|
-
new_text, n = close_re.subn("
|
|
199
|
+
new_text, n = close_re.subn("", text, count=1)
|
|
199
200
|
if n == 0:
|
|
200
201
|
break
|
|
201
202
|
found = True
|
{echotools-2.3.84 → echotools-2.3.86}/src/tests/fixtures/simulated_fake_history_markup_responses.py
RENAMED
|
@@ -110,7 +110,7 @@ def tools_for_markup_case(case: HistoryMarkupCase) -> List[Dict[str, Any]]:
|
|
|
110
110
|
FAKE_HISTORY_MARKUP_CASES: Tuple[HistoryMarkupCase, ...] = (
|
|
111
111
|
HistoryMarkupCase(
|
|
112
112
|
id="model_edit_mimic_orphan_thinking",
|
|
113
|
-
description="
|
|
113
|
+
description="高发语料:正文 + orphan </thinking> + 多块 Edit 伪 tool",
|
|
114
114
|
extra_tools=("Read", "Edit"),
|
|
115
115
|
response=(
|
|
116
116
|
"● 验证通过,无外部依赖残留。现在清理几个文件中遗留的旧路径注释。\n\n"
|
|
@@ -166,8 +166,8 @@ FAKE_HISTORY_MARKUP_CASES: Tuple[HistoryMarkupCase, ...] = (
|
|
|
166
166
|
id="orphan_assistant_close",
|
|
167
167
|
description="无开标签的 </assistant> 反向闭合",
|
|
168
168
|
response="分析中...\n</assistant>\n后续仍可见",
|
|
169
|
-
expect_clean_contains=("后续仍可见",),
|
|
170
|
-
expect_clean_excludes=("
|
|
169
|
+
expect_clean_contains=("后续仍可见", "分析中"),
|
|
170
|
+
expect_clean_excludes=("</assistant>",),
|
|
171
171
|
),
|
|
172
172
|
HistoryMarkupCase(
|
|
173
173
|
id="fake_tool_bash_scalar",
|
|
@@ -99,7 +99,7 @@ TOOLS: List[Dict[str, Any]] = [
|
|
|
99
99
|
},
|
|
100
100
|
]
|
|
101
101
|
|
|
102
|
-
#
|
|
102
|
+
# Anthropic 兼容客户端 / rogator 链路常见工具(不在默认 TOOLS 内)
|
|
103
103
|
AGENT_TOOLS: List[Dict[str, Any]] = [
|
|
104
104
|
{
|
|
105
105
|
"type": "function",
|
|
@@ -580,7 +580,7 @@ SIMULATED_LLM_RESPONSES: List[SimulatedCase] = [
|
|
|
580
580
|
expect_args=[{"city": "杭州", "unit": "c"}],
|
|
581
581
|
expect_clean_substrings=["参考历史:", "我再确认一次实时天气。"],
|
|
582
582
|
),
|
|
583
|
-
# --- 模型分支:
|
|
583
|
+
# --- 模型分支:Anthropic 兼容客户端 / rogator 常见 agent 工具 ---
|
|
584
584
|
SimulatedCase(
|
|
585
585
|
id="agent_write_windows_path",
|
|
586
586
|
description="Write:Windows 绝对路径 + 含引号 contents",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
"""entml 流式/批量解析器全方位测试:普通路径 + 边界 + rogator/
|
|
3
|
+
"""entml 流式/批量解析器全方位测试:普通路径 + 边界 + rogator/agent 客户端高发场景。
|
|
4
4
|
|
|
5
5
|
与 test_simulated_llm_tool_parse / test_adversarial_stream_splits 互补:
|
|
6
6
|
- 本文件聚焦 thinking/invoke 边界、fault ``</thinking>``、holdback 分片、
|
|
@@ -518,16 +518,30 @@ def test_strip_entml_from_user_content() -> None:
|
|
|
518
518
|
|
|
519
519
|
raw = (
|
|
520
520
|
"请查天气\n"
|
|
521
|
-
"<entml:thinking
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
"</entml:invoke
|
|
521
|
+
"<entml:thinking>thinking body</entml:thinking>\n"
|
|
522
|
+
'<entml:invoke name="get_weather">'
|
|
523
|
+
'<entml:parameter name="city">杭州</entml:parameter>'
|
|
524
|
+
"</entml:invoke>"
|
|
525
525
|
)
|
|
526
526
|
cleaned = strip_entml_from_content(raw)
|
|
527
|
-
assert cleaned ==
|
|
527
|
+
assert cleaned == (
|
|
528
|
+
"请查天气\n"
|
|
529
|
+
"<thinking>thinking body</thinking>\n"
|
|
530
|
+
'<invoke name="get_weather">'
|
|
531
|
+
'<parameter name="city">杭州</parameter>'
|
|
532
|
+
"</invoke>"
|
|
533
|
+
)
|
|
528
534
|
assert "entml:" not in cleaned
|
|
529
|
-
assert "
|
|
530
|
-
assert "杭州"
|
|
535
|
+
assert "thinking body" in cleaned
|
|
536
|
+
assert "杭州" in cleaned
|
|
537
|
+
assert cleaned.count("//") == raw.count("//")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def test_strip_entml_from_user_preserves_double_slash() -> None:
|
|
541
|
+
from echotools.exec.fncall.protocols.entml import strip_entml_from_content
|
|
542
|
+
|
|
543
|
+
raw = '<entml:invoke name="x">//path//</entml:invoke>'
|
|
544
|
+
assert strip_entml_from_content(raw) == '<invoke name="x">//path//</invoke>'
|
|
531
545
|
|
|
532
546
|
|
|
533
547
|
def test_inject_strips_entml_from_all_user_messages() -> None:
|
|
@@ -550,14 +564,12 @@ def test_inject_strips_entml_from_all_user_messages() -> None:
|
|
|
550
564
|
},
|
|
551
565
|
]
|
|
552
566
|
content = inject_fncall(msgs, [], proto)[0]["content"]
|
|
553
|
-
assert "secret"
|
|
567
|
+
assert "secret" in content or "<thinking>secret</thinking>" in content
|
|
554
568
|
assert "entml:invoke" not in content
|
|
555
|
-
assert "
|
|
556
|
-
assert "历史问题
|
|
557
|
-
assert "当前问题
|
|
558
|
-
assert "<current_user_message
|
|
559
|
-
"<current_user_message>\n当前问题 结束\n</current_user_message>" in content
|
|
560
|
-
)
|
|
569
|
+
assert '<invoke name="search">' in content or "search" in content
|
|
570
|
+
assert "历史问题" in content and "继续" in content
|
|
571
|
+
assert "当前问题" in content and "结束" in content
|
|
572
|
+
assert "<current_user_message>" in content
|
|
561
573
|
|
|
562
574
|
|
|
563
575
|
def test_entml_multi_tool_history_blocks() -> None:
|
|
@@ -1025,7 +1037,7 @@ def test_inject_include_thinking_in_history() -> None:
|
|
|
1025
1037
|
|
|
1026
1038
|
|
|
1027
1039
|
def test_entml_parse_bare_description_timeout_tags() -> None:
|
|
1028
|
-
"""
|
|
1040
|
+
"""常见 agent 风格:invoke 内裸 <entml:description>/<entml:timeout>。"""
|
|
1029
1041
|
tools = [
|
|
1030
1042
|
{
|
|
1031
1043
|
"type": "function",
|
|
@@ -1369,7 +1381,7 @@ def test_entml_stream_finalize_strips_thinking_when_tool_calls() -> None:
|
|
|
1369
1381
|
assert len(calls) == 1
|
|
1370
1382
|
assert calls[0]["function"]["name"] == "Bash"
|
|
1371
1383
|
assert "<entml:thinking>" not in clean
|
|
1372
|
-
assert "StreamAiPreviews"
|
|
1384
|
+
assert "StreamAiPreviews" in parser.partial_thinking
|
|
1373
1385
|
assert len(parser.partial_thinking) > 500
|
|
1374
1386
|
assert "260727" in json.loads(calls[0]["function"]["arguments"])["command"]
|
|
1375
1387
|
|
|
@@ -1464,6 +1476,89 @@ def test_entml_tool_block_mangled_brace_entml_params() -> None:
|
|
|
1464
1476
|
assert "RC4" in stream_clean or "RC4" in parser.partial_text, f"chunk={chunk}"
|
|
1465
1477
|
|
|
1466
1478
|
|
|
1479
|
+
def test_entml_tool_block_brace_read_entml_params() -> None:
|
|
1480
|
+
"""``<tool>{Read}\\n<entml:parameter>...`` + fault thinking(req-1785310901)。"""
|
|
1481
|
+
from pathlib import Path
|
|
1482
|
+
|
|
1483
|
+
from echotools.exec.fncall.parsers.stream import FncallStreamParser
|
|
1484
|
+
|
|
1485
|
+
sample_path = Path(
|
|
1486
|
+
r"X:/Project/Public/Qwen/logs/responses/req-1785310901-34c502c9e8a9.txt"
|
|
1487
|
+
)
|
|
1488
|
+
if not sample_path.is_file():
|
|
1489
|
+
pytest.skip("corpus file not available")
|
|
1490
|
+
sample = sample_path.read_text(encoding="utf-8")
|
|
1491
|
+
tools = [
|
|
1492
|
+
{
|
|
1493
|
+
"type": "function",
|
|
1494
|
+
"function": {
|
|
1495
|
+
"name": "Read",
|
|
1496
|
+
"parameters": {
|
|
1497
|
+
"type": "object",
|
|
1498
|
+
"properties": {
|
|
1499
|
+
"file_path": {"type": "string"},
|
|
1500
|
+
"offset": {"type": "integer"},
|
|
1501
|
+
"limit": {"type": "integer"},
|
|
1502
|
+
},
|
|
1503
|
+
"required": ["file_path"],
|
|
1504
|
+
},
|
|
1505
|
+
},
|
|
1506
|
+
}
|
|
1507
|
+
]
|
|
1508
|
+
clean, batch_calls = get_protocol("entml").parse(sample, tools)
|
|
1509
|
+
assert len(batch_calls) == 1
|
|
1510
|
+
batch_args = json.loads(batch_calls[0]["function"]["arguments"])
|
|
1511
|
+
assert batch_calls[0]["function"]["name"] == "Read"
|
|
1512
|
+
assert batch_args["offset"] == 0
|
|
1513
|
+
assert batch_args["limit"] == 200
|
|
1514
|
+
assert "b01pggtpe.txt" in batch_args["file_path"]
|
|
1515
|
+
assert "流发送逻辑" in clean
|
|
1516
|
+
assert "entml:" not in clean
|
|
1517
|
+
|
|
1518
|
+
for chunk in (1, 17, 64):
|
|
1519
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=tools)
|
|
1520
|
+
for i in range(0, len(sample), chunk):
|
|
1521
|
+
parser.feed(sample[i : i + chunk])
|
|
1522
|
+
stream_clean, stream_calls = parser.finalize()
|
|
1523
|
+
assert len(stream_calls) == 1, f"chunk={chunk}"
|
|
1524
|
+
stream_args = json.loads(stream_calls[0]["function"]["arguments"])
|
|
1525
|
+
assert stream_args == batch_args, f"chunk={chunk}"
|
|
1526
|
+
assert "流发送逻辑" in stream_clean, f"chunk={chunk}"
|
|
1527
|
+
assert "流发送逻辑" not in parser.partial_thinking, f"chunk={chunk}"
|
|
1528
|
+
assert len(parser.partial_thinking) > 500, f"chunk={chunk}"
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
def test_entml_tool_block_mangled_brace_entml_params_brace_close() -> None:
|
|
1532
|
+
"""``{Read}``(非 ``{Read>``)后接 entml parameter 须解析。"""
|
|
1533
|
+
from echotools.exec.fncall.protocols.entml_tool_blocks import parse_tool_block_body
|
|
1534
|
+
from echotools.exec.fncall.shared.coercion import _build_param_schema_index
|
|
1535
|
+
|
|
1536
|
+
tools = [
|
|
1537
|
+
{
|
|
1538
|
+
"type": "function",
|
|
1539
|
+
"function": {
|
|
1540
|
+
"name": "Read",
|
|
1541
|
+
"parameters": {
|
|
1542
|
+
"type": "object",
|
|
1543
|
+
"properties": {
|
|
1544
|
+
"file_path": {"type": "string"},
|
|
1545
|
+
"offset": {"type": "integer"},
|
|
1546
|
+
},
|
|
1547
|
+
"required": ["file_path"],
|
|
1548
|
+
},
|
|
1549
|
+
},
|
|
1550
|
+
}
|
|
1551
|
+
]
|
|
1552
|
+
body = (
|
|
1553
|
+
"{Read}\n"
|
|
1554
|
+
'<entml:parameter name="file_path">src/a.py</entml:parameter>\n'
|
|
1555
|
+
'<entml:parameter name="offset">10</entml:parameter>'
|
|
1556
|
+
)
|
|
1557
|
+
schema = _build_param_schema_index(tools)
|
|
1558
|
+
parsed = parse_tool_block_body(body, tools=tools, schema_index=schema)
|
|
1559
|
+
assert parsed == [("Read", {"file_path": "src/a.py", "offset": 10})]
|
|
1560
|
+
|
|
1561
|
+
|
|
1467
1562
|
def test_prose_entml_invoke_mention_does_not_swallow_real_invoke() -> None:
|
|
1468
1563
|
"""正文提及 ``<entml:invoke>``(无 name)不得吞掉后续真实工具块(req-1785299710)。"""
|
|
1469
1564
|
from pathlib import Path
|