echotools 2.3.64__tar.gz → 2.3.66__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.64 → echotools-2.3.66}/PKG-INFO +1 -1
- {echotools-2.3.64 → echotools-2.3.66}/pyproject.toml +1 -1
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/parsers/stream.py +3 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/prompt/inject.py +9 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml.py +22 -8
- echotools-2.3.66/src/echotools/exec/fncall/shared/history_markup.py +135 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/protocol/base.py +1 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools.egg-info/SOURCES.txt +5 -0
- echotools-2.3.66/src/tests/fixtures/simulated_fault_thinking_responses.py +220 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/fixtures/simulated_llm_tool_responses.py +1 -1
- echotools-2.3.66/src/tests/test_fault_thinking_close_batch.py +86 -0
- echotools-2.3.66/src/tests/test_fault_thinking_close_stream.py +149 -0
- echotools-2.3.66/src/tests/test_history_markup_filter.py +200 -0
- {echotools-2.3.64 → echotools-2.3.66}/README.md +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/docs/README.md +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/setup.cfg +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/compat.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_stream_json.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_think/parse.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools/version.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/bench_import.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/conftest.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_completion.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_config.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_console.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_agent_output_formats.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_parser_comprehensive.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_protocol.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_stream_json.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_tool_pipeline.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_errors.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_events.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_facade.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_fncall_stream_thinking.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_ids.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_imports.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_io.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_keys.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_logger.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_merge.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_model_branch_matrix.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_stream_partial_json_parity.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_stream_realtime_parse.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_usage.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_web.py +0 -0
- {echotools-2.3.64 → echotools-2.3.66}/src/tests/test_web_broker.py +0 -0
|
@@ -629,6 +629,9 @@ class FncallStreamParser:
|
|
|
629
629
|
|
|
630
630
|
# 统一从「可见正文 + fncall 缓冲」解析,避免漏检;thinking 已在过滤器中剥离
|
|
631
631
|
assembled = "".join(self._text_parts) + self._fncall_buf
|
|
632
|
+
from echotools.exec.fncall.shared.history_markup import strip_fake_history_markup
|
|
633
|
+
|
|
634
|
+
assembled, _ = strip_fake_history_markup(assembled)
|
|
632
635
|
clean_text, tool_calls = self._protocol.parse(assembled, self._tools)
|
|
633
636
|
|
|
634
637
|
clean_fn = getattr(self._protocol, "clean_tool_tags", None)
|
|
@@ -29,6 +29,9 @@ from echotools.exec.fncall.protocols.entml_think.hist import (
|
|
|
29
29
|
apply_thinking_history_policy,
|
|
30
30
|
parse_include_thinking_in_history,
|
|
31
31
|
)
|
|
32
|
+
from echotools.exec.fncall.shared.history_markup import (
|
|
33
|
+
detect_fake_history_markup,
|
|
34
|
+
)
|
|
32
35
|
from echotools.exec.fncall.shared.loop_detect import detect_tool_loop
|
|
33
36
|
from echotools.exec.fncall.shared.normalization import (
|
|
34
37
|
format_tool_descs,
|
|
@@ -75,6 +78,11 @@ def build_tools_prompt(
|
|
|
75
78
|
if result.is_looping:
|
|
76
79
|
logger.debug("检测到工具循环(%d 次)", result.repeat_count)
|
|
77
80
|
loop_warning = result.suggestion
|
|
81
|
+
history_markup_warning = ""
|
|
82
|
+
markup_result = detect_fake_history_markup(normalized)
|
|
83
|
+
if markup_result.detected:
|
|
84
|
+
logger.debug("检测到 assistant/tool 伪 history 标签")
|
|
85
|
+
history_markup_warning = markup_result.suggestion
|
|
78
86
|
if hasattr(protocol, "format_tool_descs"):
|
|
79
87
|
tool_descs = protocol.format_tool_descs(tools)
|
|
80
88
|
else:
|
|
@@ -92,6 +100,7 @@ def build_tools_prompt(
|
|
|
92
100
|
user_system_prompt=user_system_prompt,
|
|
93
101
|
history_text=history_text,
|
|
94
102
|
loop_warning=loop_warning,
|
|
103
|
+
history_markup_warning=history_markup_warning,
|
|
95
104
|
current_user_message=current_user_message,
|
|
96
105
|
**extra,
|
|
97
106
|
)
|
|
@@ -30,6 +30,7 @@ from echotools.exec.fncall.protocols.entml_think.core import (
|
|
|
30
30
|
build_entml_thinking_section,
|
|
31
31
|
)
|
|
32
32
|
from echotools.exec.fncall.protocols.entml_tools import format_entml_tool_descs
|
|
33
|
+
from echotools.exec.fncall.shared.history_markup import strip_fake_history_markup
|
|
33
34
|
from echotools.exec.fncall.shared.coercion import _build_param_schema_index
|
|
34
35
|
from echotools.exec.fncall.shared.normalization import (
|
|
35
36
|
normalize_content,
|
|
@@ -176,6 +177,7 @@ class EntmlProtocol(ToolProtocol):
|
|
|
176
177
|
user_system_prompt: str = "",
|
|
177
178
|
history_text: str = "",
|
|
178
179
|
loop_warning: str = "",
|
|
180
|
+
history_markup_warning: str = "",
|
|
179
181
|
current_user_message: Optional[str] = None,
|
|
180
182
|
protocol_options: Optional[Dict[str, Any]] = None,
|
|
181
183
|
history_has_tool_calls: bool = False,
|
|
@@ -202,6 +204,11 @@ class EntmlProtocol(ToolProtocol):
|
|
|
202
204
|
if loop_warning:
|
|
203
205
|
sections.append(f"<loop_warning>\n{loop_warning}\n</loop_warning>")
|
|
204
206
|
|
|
207
|
+
if history_markup_warning:
|
|
208
|
+
sections.append(
|
|
209
|
+
f"<history_markup_warning>\n{history_markup_warning}\n</history_markup_warning>"
|
|
210
|
+
)
|
|
211
|
+
|
|
205
212
|
if current_user_message is not None:
|
|
206
213
|
sections.append(format_entml_current_user_message(current_user_message))
|
|
207
214
|
|
|
@@ -270,16 +277,21 @@ class EntmlProtocol(ToolProtocol):
|
|
|
270
277
|
|
|
271
278
|
schema_index = _build_param_schema_index(tools) if tools else None
|
|
272
279
|
parse_text, _thinking = split_entml_thinking(text, thinking_enabled=True)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
280
|
+
unclosed_open_at = -1
|
|
281
|
+
if has_unclosed_entml_thinking(text, thinking_enabled=True):
|
|
282
|
+
unclosed_open_at, _ = _find_earliest_thinking_open(
|
|
283
|
+
text, thinking_enabled=True
|
|
284
|
+
)
|
|
285
|
+
if unclosed_open_at >= 0:
|
|
286
|
+
parse_text = text[:unclosed_open_at]
|
|
287
|
+
parse_text, _ = strip_fake_history_markup(parse_text)
|
|
277
288
|
tool_calls = parse_entml_tool_calls(parse_text, tools, schema_index)
|
|
278
|
-
clean = text
|
|
289
|
+
clean = text[:unclosed_open_at] if unclosed_open_at >= 0 else text
|
|
279
290
|
if tool_calls:
|
|
280
|
-
clean = BLOCK_RE.sub("",
|
|
291
|
+
clean = BLOCK_RE.sub("", clean)
|
|
281
292
|
# 无论是否解析成功,都剥离工具相关残留,避免标签泄露;thinking 保留给后续 split。
|
|
282
293
|
clean = strip_tool_entml_residue(clean)
|
|
294
|
+
clean, _ = strip_fake_history_markup(clean)
|
|
283
295
|
return (clean, normalize_tool_calls(tool_calls, tools))
|
|
284
296
|
|
|
285
297
|
def parse_fragment(
|
|
@@ -294,8 +306,10 @@ class EntmlProtocol(ToolProtocol):
|
|
|
294
306
|
return strip_entml_from_content(content)
|
|
295
307
|
|
|
296
308
|
def clean_tool_tags(self, content: str) -> str:
|
|
297
|
-
"""仅剥离工具相关标签残留,保留 thinking
|
|
298
|
-
|
|
309
|
+
"""仅剥离工具相关标签残留,保留 thinking;并移除伪 history 块。"""
|
|
310
|
+
cleaned = strip_tool_entml_residue(content)
|
|
311
|
+
cleaned, _ = strip_fake_history_markup(cleaned)
|
|
312
|
+
return cleaned
|
|
299
313
|
|
|
300
314
|
def format_assistant_tool_calls(
|
|
301
315
|
self,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""检测并剥离模型误输出的 conversation history 伪标签(``<assistant>`` / ``<tool>``)。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Dict, List, Tuple
|
|
7
|
+
|
|
8
|
+
from echotools.exec.fncall.shared.normalization import normalize_content
|
|
9
|
+
|
|
10
|
+
_FAKE_HISTORY_TAGS = ("assistant", "tool")
|
|
11
|
+
_FAKE_MARKUP_BLOCK_DETECT_RE = re.compile(
|
|
12
|
+
r"(?:^|\n)\s*</?(?:assistant|tool)\s*>\s*(?:\n|$)",
|
|
13
|
+
re.IGNORECASE | re.MULTILINE,
|
|
14
|
+
)
|
|
15
|
+
_ENTML_THINKING_OPEN_RE = re.compile(
|
|
16
|
+
r"<entml:thinking\b[^>]*>",
|
|
17
|
+
re.IGNORECASE,
|
|
18
|
+
)
|
|
19
|
+
_ENTML_THINKING_CLOSE = "</entml:thinking>"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HistoryMarkupDetectionResult:
|
|
23
|
+
"""伪 history 标签检测结果。"""
|
|
24
|
+
|
|
25
|
+
__slots__ = ("detected", "suggestion")
|
|
26
|
+
|
|
27
|
+
def __init__(self, detected: bool, suggestion: str = "") -> None:
|
|
28
|
+
self.detected = detected
|
|
29
|
+
self.suggestion = suggestion
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_HISTORY_MARKUP_SUGGESTION = (
|
|
33
|
+
"Do NOT output <assistant> or <tool> blocks in your reply. "
|
|
34
|
+
"Those tags are reserved for injected conversation history only. "
|
|
35
|
+
"Use <entml:thinking> for private reasoning and <entml:invoke> for tool calls. "
|
|
36
|
+
"Write visible replies as plain text without history markup."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def detect_fake_history_markup(
|
|
41
|
+
messages: List[Dict[str, Any]],
|
|
42
|
+
) -> HistoryMarkupDetectionResult:
|
|
43
|
+
"""扫描 assistant 正文是否误含块级 ``<assistant>`` / ``<tool>`` 伪标签。"""
|
|
44
|
+
for msg in messages:
|
|
45
|
+
if (msg.get("role") or "") != "assistant":
|
|
46
|
+
continue
|
|
47
|
+
content = normalize_content(msg.get("content", ""))
|
|
48
|
+
if content and _FAKE_MARKUP_BLOCK_DETECT_RE.search(content):
|
|
49
|
+
return HistoryMarkupDetectionResult(True, _HISTORY_MARKUP_SUGGESTION)
|
|
50
|
+
return HistoryMarkupDetectionResult(False, "")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _paired_block_re(tag: str) -> re.Pattern[str]:
|
|
54
|
+
return re.compile(
|
|
55
|
+
rf"(?:^|\n)\s*<{tag}\s*>\s*\n[\s\S]*?\n\s*</{tag}\s*>",
|
|
56
|
+
re.IGNORECASE,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _orphan_close_re(tag: str) -> re.Pattern[str]:
|
|
61
|
+
return re.compile(
|
|
62
|
+
rf"(?:^|\n)([\s\S]*?)\n\s*</{tag}\s*>\s*(?:\n|$)",
|
|
63
|
+
re.IGNORECASE,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _orphan_open_re(tag: str) -> re.Pattern[str]:
|
|
68
|
+
return re.compile(
|
|
69
|
+
rf"(?:^|\n)\s*<{tag}\s*>\s*\n[\s\S]*$",
|
|
70
|
+
re.IGNORECASE,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _strip_fake_blocks_in_segment(segment: str) -> Tuple[str, bool]:
|
|
75
|
+
if not segment:
|
|
76
|
+
return segment, False
|
|
77
|
+
found = False
|
|
78
|
+
text = segment
|
|
79
|
+
for tag in _FAKE_HISTORY_TAGS:
|
|
80
|
+
pair_re = _paired_block_re(tag)
|
|
81
|
+
while True:
|
|
82
|
+
new_text, n = pair_re.subn("", text, count=1)
|
|
83
|
+
if n == 0:
|
|
84
|
+
break
|
|
85
|
+
found = True
|
|
86
|
+
text = new_text
|
|
87
|
+
close_re = _orphan_close_re(tag)
|
|
88
|
+
while True:
|
|
89
|
+
new_text, n = close_re.subn("\n", text, count=1)
|
|
90
|
+
if n == 0:
|
|
91
|
+
break
|
|
92
|
+
found = True
|
|
93
|
+
text = new_text
|
|
94
|
+
open_re = _orphan_open_re(tag)
|
|
95
|
+
while True:
|
|
96
|
+
new_text, n = open_re.subn("", text, count=1)
|
|
97
|
+
if n == 0:
|
|
98
|
+
break
|
|
99
|
+
found = True
|
|
100
|
+
text = new_text
|
|
101
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
102
|
+
return text, found
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def strip_fake_history_markup(content: str) -> Tuple[str, bool]:
|
|
106
|
+
"""剥离块级伪 ``<assistant>`` / ``<tool>``;``<entml:thinking>`` 区域原样保留。"""
|
|
107
|
+
if not content:
|
|
108
|
+
return content, False
|
|
109
|
+
|
|
110
|
+
parts: List[str] = []
|
|
111
|
+
found = False
|
|
112
|
+
i = 0
|
|
113
|
+
while i < len(content):
|
|
114
|
+
open_m = _ENTML_THINKING_OPEN_RE.search(content, i)
|
|
115
|
+
if not open_m:
|
|
116
|
+
chunk, hit = _strip_fake_blocks_in_segment(content[i:])
|
|
117
|
+
parts.append(chunk)
|
|
118
|
+
found = found or hit
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
if open_m.start() > i:
|
|
122
|
+
chunk, hit = _strip_fake_blocks_in_segment(content[i : open_m.start()])
|
|
123
|
+
parts.append(chunk)
|
|
124
|
+
found = found or hit
|
|
125
|
+
|
|
126
|
+
body_start = open_m.end()
|
|
127
|
+
close_at = content.find(_ENTML_THINKING_CLOSE, body_start)
|
|
128
|
+
if close_at >= 0:
|
|
129
|
+
parts.append(content[open_m.start() : close_at + len(_ENTML_THINKING_CLOSE)])
|
|
130
|
+
i = close_at + len(_ENTML_THINKING_CLOSE)
|
|
131
|
+
else:
|
|
132
|
+
parts.append(content[open_m.start() :])
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
return "".join(parts), found
|
|
@@ -49,6 +49,7 @@ class ToolProtocol(ABC):
|
|
|
49
49
|
user_system_prompt: str = "",
|
|
50
50
|
history_text: str = "",
|
|
51
51
|
loop_warning: str = "",
|
|
52
|
+
history_markup_warning: str = "",
|
|
52
53
|
current_user_message: Optional[str] = None,
|
|
53
54
|
protocol_options: Optional[Dict[str, Any]] = None,
|
|
54
55
|
) -> str:
|
|
@@ -67,6 +67,7 @@ src/echotools/exec/fncall/protocols/entml_think/hist.py
|
|
|
67
67
|
src/echotools/exec/fncall/protocols/entml_think/parse.py
|
|
68
68
|
src/echotools/exec/fncall/shared/__init__.py
|
|
69
69
|
src/echotools/exec/fncall/shared/coercion.py
|
|
70
|
+
src/echotools/exec/fncall/shared/history_markup.py
|
|
70
71
|
src/echotools/exec/fncall/shared/loop_detect.py
|
|
71
72
|
src/echotools/exec/fncall/shared/normalization.py
|
|
72
73
|
src/echotools/exec/fncall/shared/schema_render.py
|
|
@@ -198,8 +199,11 @@ src/tests/test_errors_common.py
|
|
|
198
199
|
src/tests/test_events.py
|
|
199
200
|
src/tests/test_events_event.py
|
|
200
201
|
src/tests/test_facade.py
|
|
202
|
+
src/tests/test_fault_thinking_close_batch.py
|
|
203
|
+
src/tests/test_fault_thinking_close_stream.py
|
|
201
204
|
src/tests/test_fncall.py
|
|
202
205
|
src/tests/test_fncall_stream_thinking.py
|
|
206
|
+
src/tests/test_history_markup_filter.py
|
|
203
207
|
src/tests/test_ids.py
|
|
204
208
|
src/tests/test_imports.py
|
|
205
209
|
src/tests/test_io.py
|
|
@@ -223,4 +227,5 @@ src/tests/test_usage.py
|
|
|
223
227
|
src/tests/test_web.py
|
|
224
228
|
src/tests/test_web_broker.py
|
|
225
229
|
src/tests/fixtures/__init__.py
|
|
230
|
+
src/tests/fixtures/simulated_fault_thinking_responses.py
|
|
226
231
|
src/tests/fixtures/simulated_llm_tool_responses.py
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""模拟模型 ``<entml:thinking>…</thinking>`` fault 容错输出(rogator / Claude Code 高发)。
|
|
2
|
+
|
|
3
|
+
非真实模型日志;用于 batch ``protocol.parse`` 与 ``FncallStreamParser`` 分流回归。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from fixtures.simulated_llm_tool_responses import AGENT_TOOLS, TOOLS, _TOOL_BY_NAME
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class FaultThinkingCase:
|
|
16
|
+
"""一条 fault ``</thinking>`` 模拟回复及 batch/stream 期望。"""
|
|
17
|
+
|
|
18
|
+
id: str
|
|
19
|
+
description: str
|
|
20
|
+
response: str
|
|
21
|
+
extra_tools: Tuple[str, ...] = ()
|
|
22
|
+
expect_names: Tuple[str, ...] = ()
|
|
23
|
+
expect_args: Tuple[Dict[str, Any], ...] = ()
|
|
24
|
+
expect_call_count: Optional[int] = None
|
|
25
|
+
expect_thinking_contains: Tuple[str, ...] = ()
|
|
26
|
+
expect_clean_contains: Tuple[str, ...] = ()
|
|
27
|
+
expect_clean_absent: Tuple[str, ...] = ("entml:invoke", "entml:parameter")
|
|
28
|
+
expect_clean_excludes: Tuple[str, ...] = ()
|
|
29
|
+
expect_split_thinking_contains: Tuple[str, ...] = ()
|
|
30
|
+
expect_split_thinking_empty: bool = False
|
|
31
|
+
chunk_sizes: Tuple[int, ...] = (1, 3, 5, 8, 17, 64)
|
|
32
|
+
worst_split: bool = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
READ_TOOL: Dict[str, Any] = {
|
|
36
|
+
"type": "function",
|
|
37
|
+
"function": {
|
|
38
|
+
"name": "Read",
|
|
39
|
+
"parameters": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {"path": {"type": "string"}},
|
|
42
|
+
"required": ["path"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_EXTRA_BY_NAME: Dict[str, Dict[str, Any]] = {
|
|
48
|
+
**_TOOL_BY_NAME,
|
|
49
|
+
"Read": READ_TOOL,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def tools_for_fault_case(case: FaultThinkingCase) -> List[Dict[str, Any]]:
|
|
54
|
+
names = {t["function"]["name"] for t in TOOLS}
|
|
55
|
+
out = list(TOOLS)
|
|
56
|
+
for name in case.extra_tools:
|
|
57
|
+
if name not in names and name in _EXTRA_BY_NAME:
|
|
58
|
+
out.append(_EXTRA_BY_NAME[name])
|
|
59
|
+
names.add(name)
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _big_bash_body() -> str:
|
|
64
|
+
script = "python -c \"print('ok')\"" + ";" + "echo " + ("x" * 6800)
|
|
65
|
+
return (
|
|
66
|
+
"<entml:thinking>\n"
|
|
67
|
+
"用户要求执行较长 shell;我先确认不会破坏环境,再用 Bash。\n"
|
|
68
|
+
"</thinking>\n"
|
|
69
|
+
'<entml:invoke name="Bash">\n'
|
|
70
|
+
f'<entml:parameter name="command">{script}</entml:parameter>\n'
|
|
71
|
+
"</entml:invoke>"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
FAULT_THINKING_CASES: Tuple[FaultThinkingCase, ...] = (
|
|
76
|
+
FaultThinkingCase(
|
|
77
|
+
id="model_fault_read_after_close",
|
|
78
|
+
description="rogator 高发:中文思考 + </thinking> + 说明 + Read",
|
|
79
|
+
extra_tools=("Read",),
|
|
80
|
+
response=(
|
|
81
|
+
"<entml:thinking>\n"
|
|
82
|
+
"用户希望查看 main.py。我先确认路径,再调用 Read。\n"
|
|
83
|
+
"</thinking>\n"
|
|
84
|
+
"我先读取该文件。\n"
|
|
85
|
+
'<entml:invoke name="Read">\n'
|
|
86
|
+
'<entml:parameter name="path">src/main.py</entml:parameter>\n'
|
|
87
|
+
"</entml:invoke>"
|
|
88
|
+
),
|
|
89
|
+
expect_names=("Read",),
|
|
90
|
+
expect_args=({"path": "src/main.py"},),
|
|
91
|
+
expect_thinking_contains=("main.py", "Read"),
|
|
92
|
+
expect_clean_contains=("我先读取该文件。",),
|
|
93
|
+
expect_split_thinking_contains=("main.py",),
|
|
94
|
+
worst_split=True,
|
|
95
|
+
),
|
|
96
|
+
FaultThinkingCase(
|
|
97
|
+
id="model_fault_bash_git_status",
|
|
98
|
+
description="英文思考 + </thinking> 后直接 Bash(无中间正文)",
|
|
99
|
+
extra_tools=("Bash",),
|
|
100
|
+
response=(
|
|
101
|
+
"<entml:thinking>\n"
|
|
102
|
+
"Need to inspect the working tree before answering.\n"
|
|
103
|
+
"</thinking>\n"
|
|
104
|
+
'<entml:invoke name="Bash">\n'
|
|
105
|
+
'<entml:parameter name="command">git status --short</entml:parameter>\n'
|
|
106
|
+
"</entml:invoke>"
|
|
107
|
+
),
|
|
108
|
+
expect_names=("Bash",),
|
|
109
|
+
expect_args=({"command": "git status --short"},),
|
|
110
|
+
expect_thinking_contains=("working tree",),
|
|
111
|
+
expect_split_thinking_contains=("working tree",),
|
|
112
|
+
),
|
|
113
|
+
FaultThinkingCase(
|
|
114
|
+
id="model_fault_weather_with_visible",
|
|
115
|
+
description="fault 闭合后先可见说明再 get_weather",
|
|
116
|
+
response=(
|
|
117
|
+
"<entml:thinking>\n"
|
|
118
|
+
"用户问杭州天气,我需要调用 get_weather。\n"
|
|
119
|
+
"</thinking>\n"
|
|
120
|
+
"正在查询杭州天气。\n"
|
|
121
|
+
'<entml:invoke name="get_weather">\n'
|
|
122
|
+
'<entml:parameter name="city">杭州</entml:parameter>\n'
|
|
123
|
+
'<entml:parameter name="unit">c</entml:parameter>\n'
|
|
124
|
+
"</entml:invoke>"
|
|
125
|
+
),
|
|
126
|
+
expect_names=("get_weather",),
|
|
127
|
+
expect_args=({"city": "杭州", "unit": "c"},),
|
|
128
|
+
expect_thinking_contains=("杭州",),
|
|
129
|
+
expect_clean_contains=("正在查询杭州天气。",),
|
|
130
|
+
expect_split_thinking_contains=("杭州",),
|
|
131
|
+
),
|
|
132
|
+
FaultThinkingCase(
|
|
133
|
+
id="model_fault_read_then_bash",
|
|
134
|
+
description="</thinking> 后连续 Read + Bash(双工具)",
|
|
135
|
+
extra_tools=("Read", "Bash"),
|
|
136
|
+
response=(
|
|
137
|
+
"<entml:thinking>\n"
|
|
138
|
+
"先读配置,再跑脚本验证。\n"
|
|
139
|
+
"</thinking>\n"
|
|
140
|
+
'<entml:invoke name="Read">\n'
|
|
141
|
+
'<entml:parameter name="path">config.toml</entml:parameter>\n'
|
|
142
|
+
"</entml:invoke>\n"
|
|
143
|
+
'<entml:invoke name="Bash">\n'
|
|
144
|
+
'<entml:parameter name="command">python verify.py</entml:parameter>\n'
|
|
145
|
+
"</entml:invoke>"
|
|
146
|
+
),
|
|
147
|
+
expect_names=("Read", "Bash"),
|
|
148
|
+
expect_args=({"path": "config.toml"}, {"command": "python verify.py"}),
|
|
149
|
+
expect_thinking_contains=("先读配置",),
|
|
150
|
+
expect_split_thinking_contains=("先读配置",),
|
|
151
|
+
),
|
|
152
|
+
FaultThinkingCase(
|
|
153
|
+
id="model_fault_close_without_invoke",
|
|
154
|
+
description="</thinking> 后无 invoke,直到标准 </entml:thinking> 才结束",
|
|
155
|
+
response=(
|
|
156
|
+
"<entml:thinking>\n"
|
|
157
|
+
"模型有时会在正文里提到 </thinking> 标签本身。\n"
|
|
158
|
+
"</thinking>\n"
|
|
159
|
+
"继续推理,尚未调用工具。\n"
|
|
160
|
+
"</entml:thinking>\n"
|
|
161
|
+
"这是给用户的最终回答。"
|
|
162
|
+
),
|
|
163
|
+
expect_call_count=0,
|
|
164
|
+
expect_thinking_contains=("提到", "继续推理"),
|
|
165
|
+
expect_clean_contains=("最终回答",),
|
|
166
|
+
expect_split_thinking_contains=("</thinking>", "继续推理"),
|
|
167
|
+
),
|
|
168
|
+
FaultThinkingCase(
|
|
169
|
+
id="model_invoke_inside_before_fault_close",
|
|
170
|
+
description="错误:invoke 写在 </thinking> 之前,不得解析",
|
|
171
|
+
extra_tools=("Bash",),
|
|
172
|
+
response=(
|
|
173
|
+
"<entml:thinking>\n"
|
|
174
|
+
"我先尝试调用 Bash:\n"
|
|
175
|
+
'<entml:invoke name="Bash">\n'
|
|
176
|
+
'<entml:parameter name="command">echo oops</entml:parameter>\n'
|
|
177
|
+
"</entml:invoke>\n"
|
|
178
|
+
"</thinking>\n"
|
|
179
|
+
"以上是思考过程,现在开始正式回复。"
|
|
180
|
+
),
|
|
181
|
+
expect_call_count=0,
|
|
182
|
+
expect_thinking_contains=("echo oops", "正式回复"),
|
|
183
|
+
expect_clean_excludes=("正式回复", "echo oops"),
|
|
184
|
+
expect_split_thinking_empty=True,
|
|
185
|
+
),
|
|
186
|
+
FaultThinkingCase(
|
|
187
|
+
id="model_fault_large_bash",
|
|
188
|
+
description="rogator 7KB 级 Bash:fault 闭合后长 parameter",
|
|
189
|
+
extra_tools=("Bash",),
|
|
190
|
+
response=_big_bash_body(),
|
|
191
|
+
expect_names=("Bash",),
|
|
192
|
+
expect_thinking_contains=("较长 shell",),
|
|
193
|
+
expect_split_thinking_contains=("较长 shell",),
|
|
194
|
+
chunk_sizes=(1, 8, 17, 64),
|
|
195
|
+
),
|
|
196
|
+
FaultThinkingCase(
|
|
197
|
+
id="model_fault_multiline_parameter_read",
|
|
198
|
+
description="</thinking> 与 invoke/parameter 之间有额外空行",
|
|
199
|
+
extra_tools=("Read",),
|
|
200
|
+
response=(
|
|
201
|
+
"<entml:thinking>\n"
|
|
202
|
+
"Read C:/tmp/x.py\n"
|
|
203
|
+
"</thinking>\n"
|
|
204
|
+
"\n"
|
|
205
|
+
'<entml:invoke name="Read">\n'
|
|
206
|
+
'\n'
|
|
207
|
+
'<entml:parameter name="path">C:/tmp/x.py</entml:parameter>\n'
|
|
208
|
+
"\n"
|
|
209
|
+
"</entml:invoke>"
|
|
210
|
+
),
|
|
211
|
+
expect_names=("Read",),
|
|
212
|
+
expect_args=({"path": "C:/tmp/x.py"},),
|
|
213
|
+
expect_thinking_contains=("Read C:/tmp/x.py",),
|
|
214
|
+
worst_split=True,
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def iter_fault_thinking_cases() -> List[FaultThinkingCase]:
|
|
220
|
+
return list(FAULT_THINKING_CASES)
|
|
@@ -578,7 +578,7 @@ SIMULATED_LLM_RESPONSES: List[SimulatedCase] = [
|
|
|
578
578
|
),
|
|
579
579
|
expect_names=["get_weather"],
|
|
580
580
|
expect_args=[{"city": "杭州", "unit": "c"}],
|
|
581
|
-
expect_clean_substrings=["参考历史:",
|
|
581
|
+
expect_clean_substrings=["参考历史:", "我再确认一次实时天气。"],
|
|
582
582
|
),
|
|
583
583
|
# --- 模型分支:Claude Code / rogator 常见 agent 工具 ---
|
|
584
584
|
SimulatedCase(
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""非流式:``<entml:thinking>…</thinking>`` fault 容错 — ``protocol.parse`` / ``split_entml_thinking``。"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from echotools.exec.fncall import get_protocol
|
|
11
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
12
|
+
find_complete_entml_invoke_open,
|
|
13
|
+
has_unclosed_entml_thinking,
|
|
14
|
+
invoke_index_inside_unclosed_thinking,
|
|
15
|
+
split_entml_thinking,
|
|
16
|
+
)
|
|
17
|
+
from fixtures.simulated_fault_thinking_responses import (
|
|
18
|
+
FaultThinkingCase,
|
|
19
|
+
iter_fault_thinking_cases,
|
|
20
|
+
tools_for_fault_case,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
CASES = iter_fault_thinking_cases()
|
|
24
|
+
CASE_IDS = [c.id for c in CASES]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _names(calls: List[Dict[str, Any]]) -> List[str]:
|
|
28
|
+
return [c["function"]["name"] for c in calls]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _args(calls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
32
|
+
return [json.loads(c["function"]["arguments"]) for c in calls]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _batch_parse(case: FaultThinkingCase) -> tuple[str, List[Dict[str, Any]]]:
|
|
36
|
+
return get_protocol("entml").parse(case.response, tools_for_fault_case(case))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _clean_needles(case: FaultThinkingCase) -> tuple[str, ...]:
|
|
40
|
+
return case.expect_clean_contains
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _assert_clean(case: FaultThinkingCase, clean: str, *, label: str) -> None:
|
|
44
|
+
for needle in _clean_needles(case):
|
|
45
|
+
assert needle in clean, f"{case.id}/{label}: clean missing {needle!r}"
|
|
46
|
+
for bad in case.expect_clean_absent:
|
|
47
|
+
assert bad not in clean, f"{case.id}/{label}: clean leaked {bad!r}"
|
|
48
|
+
for bad in case.expect_clean_excludes:
|
|
49
|
+
assert bad not in clean, f"{case.id}/{label}: clean must exclude {bad!r}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.parametrize("case", CASES, ids=CASE_IDS)
|
|
53
|
+
def test_batch_tool_calls(case: FaultThinkingCase) -> None:
|
|
54
|
+
clean, calls = _batch_parse(case)
|
|
55
|
+
if case.expect_call_count is not None:
|
|
56
|
+
assert len(calls) == case.expect_call_count, case.id
|
|
57
|
+
if case.expect_names:
|
|
58
|
+
assert _names(calls) == list(case.expect_names), case.id
|
|
59
|
+
if case.expect_args:
|
|
60
|
+
assert _args(calls) == list(case.expect_args), case.id
|
|
61
|
+
_assert_clean(case, clean, label="batch")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize("case", CASES, ids=CASE_IDS)
|
|
65
|
+
def test_batch_split_entml_thinking(case: FaultThinkingCase) -> None:
|
|
66
|
+
content, thinking = split_entml_thinking(case.response)
|
|
67
|
+
if case.expect_split_thinking_empty:
|
|
68
|
+
assert thinking == "", case.id
|
|
69
|
+
assert has_unclosed_entml_thinking(case.response), case.id
|
|
70
|
+
return
|
|
71
|
+
for needle in case.expect_split_thinking_contains:
|
|
72
|
+
assert needle in thinking, f"{case.id}: split missing {needle!r}"
|
|
73
|
+
if case.expect_names:
|
|
74
|
+
assert not has_unclosed_entml_thinking(case.response), case.id
|
|
75
|
+
assert not has_unclosed_entml_thinking(content), case.id
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.parametrize("case", CASES, ids=CASE_IDS)
|
|
79
|
+
def test_batch_invoke_not_inside_unclosed_thinking(case: FaultThinkingCase) -> None:
|
|
80
|
+
pos = find_complete_entml_invoke_open(case.response)
|
|
81
|
+
if case.expect_names:
|
|
82
|
+
assert pos >= 0, case.id
|
|
83
|
+
assert not invoke_index_inside_unclosed_thinking(case.response, pos), case.id
|
|
84
|
+
elif case.id == "model_invoke_inside_before_fault_close":
|
|
85
|
+
assert pos >= 0, case.id
|
|
86
|
+
assert invoke_index_inside_unclosed_thinking(case.response, pos), case.id
|