echotools 2.3.49__tar.gz → 2.3.50__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.49 → echotools-2.3.50}/PKG-INFO +1 -1
- {echotools-2.3.49 → echotools-2.3.50}/pyproject.toml +1 -1
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/parsers/stream.py +32 -10
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/history.py +0 -19
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/history_format.py +2 -5
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -18
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/parse.py +19 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools.egg-info/SOURCES.txt +2 -0
- echotools-2.3.50/src/tests/test_entml_agent_output_formats.py +143 -0
- echotools-2.3.50/src/tests/test_entml_parser_comprehensive.py +656 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_fncall_stream_thinking.py +38 -3
- {echotools-2.3.49 → echotools-2.3.50}/README.md +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/docs/README.md +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/setup.cfg +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/compat.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_invoke.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_patterns.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_stream_json.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_tools.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_values.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools/version.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/bench_import.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/conftest.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_completion.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_config.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_console.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_protocol.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_stream_json.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_tool_pipeline.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_errors.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_events.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_facade.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_ids.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_imports.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_io.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_keys.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_logger.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_merge.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_stream_realtime_parse.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_usage.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_web.py +0 -0
- {echotools-2.3.49 → echotools-2.3.50}/src/tests/test_web_broker.py +0 -0
|
@@ -148,6 +148,23 @@ class FncallStreamParser:
|
|
|
148
148
|
return fn(text)
|
|
149
149
|
return text
|
|
150
150
|
|
|
151
|
+
def _drain_thinking_holdback_to_fncall(self) -> None:
|
|
152
|
+
"""invoke 已开始后,thinking 过滤器里对工具标签的 holdback 应并入 fncall 缓冲。"""
|
|
153
|
+
if self._thinking_filter is None:
|
|
154
|
+
return
|
|
155
|
+
pending = self._thinking_filter._pending
|
|
156
|
+
if pending:
|
|
157
|
+
self._fncall_buf += pending
|
|
158
|
+
self._thinking_filter._pending = ""
|
|
159
|
+
|
|
160
|
+
def _begin_function_calls(self, buffer_from: str, *, pos: int) -> None:
|
|
161
|
+
self._fncall_buf = buffer_from[pos:]
|
|
162
|
+
self._drain_thinking_holdback_to_fncall()
|
|
163
|
+
self._detected = True
|
|
164
|
+
self._state = self.IN_FUNCTION_CALLS
|
|
165
|
+
if self._is_call_closed():
|
|
166
|
+
self._state = self.DONE
|
|
167
|
+
|
|
151
168
|
def _feed_content_waiting(self, text: str) -> None:
|
|
152
169
|
"""thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
|
|
153
170
|
if not text:
|
|
@@ -173,11 +190,7 @@ class FncallStreamParser:
|
|
|
173
190
|
if pos > 0:
|
|
174
191
|
self._append_content_text(text[:pos])
|
|
175
192
|
|
|
176
|
-
self.
|
|
177
|
-
self._detected = True
|
|
178
|
-
self._state = self.IN_FUNCTION_CALLS
|
|
179
|
-
if self._is_call_closed():
|
|
180
|
-
self._state = self.DONE
|
|
193
|
+
self._begin_function_calls(text, pos=pos)
|
|
181
194
|
|
|
182
195
|
def _feed_waiting_thinking_plain(self, combined: str) -> None:
|
|
183
196
|
"""未闭合 thinking 阶段:块内一律按纯文本进 thinking,不检测 invoke。"""
|
|
@@ -187,6 +200,8 @@ class FncallStreamParser:
|
|
|
187
200
|
self._thinking_parts.append(part)
|
|
188
201
|
elif part:
|
|
189
202
|
self._feed_content_waiting(part)
|
|
203
|
+
if self._state == self.IN_FUNCTION_CALLS:
|
|
204
|
+
self._drain_thinking_holdback_to_fncall()
|
|
190
205
|
|
|
191
206
|
def _feed_waiting(self, chunk: str) -> None:
|
|
192
207
|
"""在 WAITING_FOR_TAG 状态下处理新块。"""
|
|
@@ -202,14 +217,17 @@ class FncallStreamParser:
|
|
|
202
217
|
found, pos = self._protocol.detect_start(combined)
|
|
203
218
|
|
|
204
219
|
if found:
|
|
220
|
+
if self._thinking_filter is not None:
|
|
221
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
222
|
+
invoke_index_inside_unclosed_thinking,
|
|
223
|
+
)
|
|
224
|
+
if invoke_index_inside_unclosed_thinking(combined, pos):
|
|
225
|
+
self._feed_waiting_thinking_plain(combined)
|
|
226
|
+
return
|
|
205
227
|
if pos > 0:
|
|
206
228
|
# 工具标签前的正文仍可能含 thinking,走过滤器。
|
|
207
229
|
self._emit_text(combined[:pos])
|
|
208
|
-
self.
|
|
209
|
-
self._detected = True
|
|
210
|
-
self._state = self.IN_FUNCTION_CALLS
|
|
211
|
-
if self._is_call_closed():
|
|
212
|
-
self._state = self.DONE
|
|
230
|
+
self._begin_function_calls(combined, pos=pos)
|
|
213
231
|
return
|
|
214
232
|
|
|
215
233
|
# 无完整工具开标签时,再处理未闭合 thinking / 思考开标签 holdback。
|
|
@@ -264,6 +282,10 @@ class FncallStreamParser:
|
|
|
264
282
|
if self._state == self.WAITING_FOR_TAG:
|
|
265
283
|
self._feed_waiting(chunk)
|
|
266
284
|
else:
|
|
285
|
+
if self._waiting_tail:
|
|
286
|
+
self._fncall_buf += self._normalize_stream_chunk(self._waiting_tail)
|
|
287
|
+
self._waiting_tail = ""
|
|
288
|
+
self._drain_thinking_holdback_to_fncall()
|
|
267
289
|
self._fncall_buf += self._normalize_stream_chunk(chunk)
|
|
268
290
|
if self._is_call_closed():
|
|
269
291
|
self._state = self.DONE
|
|
@@ -319,22 +319,3 @@ def _normalize_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
|
319
319
|
return inject_orphan_tool_results(step2)
|
|
320
320
|
|
|
321
321
|
|
|
322
|
-
# ---------------------------------------------------------------------------
|
|
323
|
-
# 对话历史格式化
|
|
324
|
-
# ---------------------------------------------------------------------------
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
def _make_assistant_dedup_key(
|
|
328
|
-
content: Optional[str],
|
|
329
|
-
tool_calls: List[Dict[str, Any]],
|
|
330
|
-
) -> Tuple[str, Tuple[Tuple[str, str], ...]]:
|
|
331
|
-
"""生成 assistant 消息的去重键。"""
|
|
332
|
-
safe_content = content or ""
|
|
333
|
-
tc_key: Tuple[Tuple[str, str], ...] = tuple(
|
|
334
|
-
(
|
|
335
|
-
(tc.get("function") or {}).get("name") or "",
|
|
336
|
-
(tc.get("function") or {}).get("arguments") or "",
|
|
337
|
-
)
|
|
338
|
-
for tc in tool_calls
|
|
339
|
-
)
|
|
340
|
-
return (safe_content, tc_key)
|
|
@@ -41,7 +41,6 @@ def _append_assistant_part(
|
|
|
41
41
|
index: int,
|
|
42
42
|
protocol: Optional[Any],
|
|
43
43
|
call_id_to_name: Dict[str, str],
|
|
44
|
-
seen_assistant_keys: set,
|
|
45
44
|
*,
|
|
46
45
|
include_thinking_in_history: bool,
|
|
47
46
|
) -> int:
|
|
@@ -55,7 +54,7 @@ def _append_assistant_part(
|
|
|
55
54
|
tool_msgs.append(messages[j])
|
|
56
55
|
j += 1
|
|
57
56
|
blocks = format_assistant_block_with_results(
|
|
58
|
-
m, tool_msgs, protocol, call_id_to_name,
|
|
57
|
+
m, tool_msgs, protocol, call_id_to_name,
|
|
59
58
|
include_thinking_in_history=include_thinking_in_history,
|
|
60
59
|
)
|
|
61
60
|
for block in blocks:
|
|
@@ -63,7 +62,7 @@ def _append_assistant_part(
|
|
|
63
62
|
return j
|
|
64
63
|
|
|
65
64
|
blocks = format_assistant_block(
|
|
66
|
-
m, content_str, protocol, call_id_to_name,
|
|
65
|
+
m, content_str, protocol, call_id_to_name,
|
|
67
66
|
include_thinking_in_history=include_thinking_in_history,
|
|
68
67
|
)
|
|
69
68
|
for block in blocks:
|
|
@@ -81,7 +80,6 @@ def _format_conversation_history(
|
|
|
81
80
|
return ""
|
|
82
81
|
|
|
83
82
|
call_id_to_name: Dict[str, str] = {}
|
|
84
|
-
seen_assistant_keys = set()
|
|
85
83
|
parts: List[Tuple[str, bool]] = []
|
|
86
84
|
clean_fn = protocol.clean_tags if protocol and hasattr(protocol, "clean_tags") else None
|
|
87
85
|
|
|
@@ -98,7 +96,6 @@ def _format_conversation_history(
|
|
|
98
96
|
if role == "assistant":
|
|
99
97
|
i = _append_assistant_part(
|
|
100
98
|
parts, messages, i, protocol, call_id_to_name,
|
|
101
|
-
seen_assistant_keys,
|
|
102
99
|
include_thinking_in_history=include_thinking_in_history,
|
|
103
100
|
)
|
|
104
101
|
continue
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
4
4
|
|
|
5
|
-
from echotools.base.logger.manager import get_logger
|
|
6
5
|
from echotools.exec.fncall.protocols.entml_think.hist import (
|
|
7
6
|
extract_reasoning_text,
|
|
8
7
|
format_entml_thinking_history_block,
|
|
@@ -12,15 +11,12 @@ from echotools.exec.fncall.shared.normalization import normalize_content
|
|
|
12
11
|
from .history import (
|
|
13
12
|
_TOOL_CALL_MARKER_RE,
|
|
14
13
|
_convert_assistant_pseudo_calls,
|
|
15
|
-
_make_assistant_dedup_key,
|
|
16
14
|
_render_inline_call_result,
|
|
17
15
|
_render_tool_call,
|
|
18
16
|
_render_tool_result,
|
|
19
17
|
_try_convert_user_to_tool,
|
|
20
18
|
)
|
|
21
19
|
|
|
22
|
-
logger = get_logger(__name__)
|
|
23
|
-
|
|
24
20
|
|
|
25
21
|
def _assistant_history_content_blocks(
|
|
26
22
|
m: Dict[str, Any],
|
|
@@ -139,19 +135,12 @@ def format_assistant_block(
|
|
|
139
135
|
content_str: str,
|
|
140
136
|
protocol: Optional[Any],
|
|
141
137
|
call_id_to_name: Dict[str, str],
|
|
142
|
-
seen_assistant_keys: Set[Tuple[str, Tuple[Tuple[str, str], ...]]],
|
|
143
138
|
*,
|
|
144
139
|
include_thinking_in_history: bool = False,
|
|
145
140
|
) -> List[str]:
|
|
146
141
|
tcs: List[Dict[str, Any]] = m.get("tool_calls") or []
|
|
147
142
|
_register_tool_call_names(tcs, call_id_to_name)
|
|
148
143
|
|
|
149
|
-
dedup_key = _make_assistant_dedup_key(content_str, tcs)
|
|
150
|
-
if dedup_key in seen_assistant_keys:
|
|
151
|
-
logger.debug("跳过重复 assistant 消息(dedup_key 已见)")
|
|
152
|
-
return []
|
|
153
|
-
seen_assistant_keys.add(dedup_key)
|
|
154
|
-
|
|
155
144
|
parts: List[str] = []
|
|
156
145
|
assistant = _format_assistant_text_block(
|
|
157
146
|
m, content_str, protocol, include_thinking_in_history=include_thinking_in_history
|
|
@@ -174,7 +163,6 @@ def format_assistant_block_with_results(
|
|
|
174
163
|
tool_msgs: List[Dict[str, Any]],
|
|
175
164
|
protocol: Optional[Any],
|
|
176
165
|
call_id_to_name: Dict[str, str],
|
|
177
|
-
seen_assistant_keys: Set[Tuple[str, Tuple[Tuple[str, str], ...]]],
|
|
178
166
|
*,
|
|
179
167
|
include_thinking_in_history: bool = False,
|
|
180
168
|
) -> List[str]:
|
|
@@ -182,12 +170,6 @@ def format_assistant_block_with_results(
|
|
|
182
170
|
content_str = normalize_content(m.get("content", ""))
|
|
183
171
|
_register_tool_call_names(tcs, call_id_to_name)
|
|
184
172
|
|
|
185
|
-
dedup_key = _make_assistant_dedup_key(content_str, tcs)
|
|
186
|
-
if dedup_key in seen_assistant_keys:
|
|
187
|
-
logger.debug("跳过重复 assistant 消息(dedup_key 已见)")
|
|
188
|
-
return []
|
|
189
|
-
seen_assistant_keys.add(dedup_key)
|
|
190
|
-
|
|
191
173
|
parts: List[str] = []
|
|
192
174
|
assistant = _format_assistant_text_block(
|
|
193
175
|
m, content_str, protocol, include_thinking_in_history=include_thinking_in_history
|
{echotools-2.3.49 → echotools-2.3.50}/src/echotools/exec/fncall/protocols/entml_think/parse.py
RENAMED
|
@@ -103,6 +103,25 @@ def has_unclosed_entml_thinking(text: str) -> bool:
|
|
|
103
103
|
return False
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
def invoke_index_inside_unclosed_thinking(text: str, invoke_at: int) -> bool:
|
|
107
|
+
"""``invoke_at`` 是否落在尚未正式 ``</entml:thinking>`` 闭合的块内。
|
|
108
|
+
|
|
109
|
+
已出现容错 ``</thinking>`` 且 invoke 在其后的段落,视为 thinking 外。
|
|
110
|
+
"""
|
|
111
|
+
if invoke_at < 0:
|
|
112
|
+
return False
|
|
113
|
+
think_open = text.find(_THINKING_OPEN_PREFIX)
|
|
114
|
+
if think_open < 0 or invoke_at <= think_open:
|
|
115
|
+
return False
|
|
116
|
+
entml_close = text.find(_THINKING_CLOSE, think_open)
|
|
117
|
+
if entml_close >= 0:
|
|
118
|
+
return invoke_at < entml_close
|
|
119
|
+
fault_at = text.find(_FAULT_THINKING_CLOSE, think_open)
|
|
120
|
+
if fault_at >= 0:
|
|
121
|
+
return invoke_at < fault_at + len(_FAULT_THINKING_CLOSE)
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
|
|
106
125
|
def split_entml_thinking(text: str) -> Tuple[str, str]:
|
|
107
126
|
"""从文本中剥离 <entml:thinking> 块,返回 (正文, 思考链拼接)。"""
|
|
108
127
|
if not text:
|
|
@@ -182,7 +182,9 @@ src/tests/test_coverage_core.py
|
|
|
182
182
|
src/tests/test_dispatch.py
|
|
183
183
|
src/tests/test_dispatch_extended.py
|
|
184
184
|
src/tests/test_dispatch_race.py
|
|
185
|
+
src/tests/test_entml_agent_output_formats.py
|
|
185
186
|
src/tests/test_entml_bare_invoke_format.py
|
|
187
|
+
src/tests/test_entml_parser_comprehensive.py
|
|
186
188
|
src/tests/test_entml_protocol.py
|
|
187
189
|
src/tests/test_entml_stream_json.py
|
|
188
190
|
src/tests/test_entml_stream_json_exhaustive.py
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""模型/agent 常见输出格式:正确 entml:invoke vs 错误 JSON/旧格式。"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from echotools.exec.fncall import get_protocol
|
|
10
|
+
from echotools.exec.fncall.parsers.stream import FncallStreamParser
|
|
11
|
+
|
|
12
|
+
TOOLS = [
|
|
13
|
+
{
|
|
14
|
+
"type": "function",
|
|
15
|
+
"function": {
|
|
16
|
+
"name": "Read",
|
|
17
|
+
"description": "Read a file",
|
|
18
|
+
"parameters": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {"path": {"type": "string"}},
|
|
21
|
+
"required": ["path"],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"type": "function",
|
|
27
|
+
"function": {
|
|
28
|
+
"name": "get_time",
|
|
29
|
+
"description": "Get current time",
|
|
30
|
+
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
VALID_INVOKE = (
|
|
36
|
+
'<entml:invoke name="Read">\n'
|
|
37
|
+
'<entml:parameter name="path">C:/Users/Administrator/cursor_agent_simple.py</entml:parameter>\n'
|
|
38
|
+
"</entml:invoke>"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
WRONG_JSON_TOOL = (
|
|
42
|
+
'{"name": "Read", "arguments": {"path": "C:/Users/Administrator/cursor_agent_simple.py"}}'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
WRONG_TOOL_BLOCK = (
|
|
46
|
+
"<tool>\n"
|
|
47
|
+
'{"name": "Read", "arguments": {"path": "x.py"}}\n'
|
|
48
|
+
"</tool>"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
FAKE_PARTIAL_JSON = '{"name": "Read", "arguments": {"path":'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parse_batch(text: str):
|
|
55
|
+
proto = get_protocol("entml")
|
|
56
|
+
clean, calls = proto.parse(text, TOOLS)
|
|
57
|
+
return clean, calls
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_stream(text: str, *, chunk: int = 3):
|
|
61
|
+
proto = get_protocol("entml")
|
|
62
|
+
parser = FncallStreamParser(protocol=proto, tools=TOOLS)
|
|
63
|
+
for i in range(0, len(text), chunk):
|
|
64
|
+
parser.feed(text[i : i + chunk])
|
|
65
|
+
return parser.finalize()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize("chunk", [1, 4, 17])
|
|
69
|
+
def test_valid_entml_invoke_batch_and_stream(chunk: int) -> None:
|
|
70
|
+
text = f"我先读文件。\n{VALID_INVOKE}\n完成。"
|
|
71
|
+
clean, calls = _parse_batch(text)
|
|
72
|
+
assert len(calls) == 1
|
|
73
|
+
assert calls[0]["function"]["name"] == "Read"
|
|
74
|
+
assert "cursor_agent_simple.py" in calls[0]["function"]["arguments"]
|
|
75
|
+
assert "entml:invoke" not in clean
|
|
76
|
+
|
|
77
|
+
clean_s, calls_s = _parse_stream(text, chunk=chunk)
|
|
78
|
+
assert len(calls_s) == 1
|
|
79
|
+
assert calls_s[0]["function"]["name"] == "Read"
|
|
80
|
+
assert "entml:invoke" not in clean_s
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_wrong_json_object_not_parsed_as_tool() -> None:
|
|
84
|
+
text = f"调用工具:\n{WRONG_JSON_TOOL}"
|
|
85
|
+
clean, calls = _parse_batch(text)
|
|
86
|
+
assert calls == []
|
|
87
|
+
assert "Read" in clean or "name" in clean
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_wrong_tool_block_not_parsed_as_entml_invoke() -> None:
|
|
91
|
+
text = f"回复\n{WRONG_TOOL_BLOCK}"
|
|
92
|
+
clean, calls = _parse_batch(text)
|
|
93
|
+
assert calls == []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_truncated_json_not_parsed() -> None:
|
|
97
|
+
clean, calls = _parse_batch(FAKE_PARTIAL_JSON)
|
|
98
|
+
assert calls == []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_thinking_then_valid_invoke(chunk: int = 5) -> None:
|
|
102
|
+
text = (
|
|
103
|
+
"<entml:thinking>\n需要先读脚本\n</entml:thinking>\n"
|
|
104
|
+
f"{VALID_INVOKE}"
|
|
105
|
+
)
|
|
106
|
+
clean, calls = _parse_stream(text, chunk=chunk)
|
|
107
|
+
assert len(calls) == 1
|
|
108
|
+
assert calls[0]["function"]["name"] == "Read"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_fault_thinking_close_then_invoke() -> None:
|
|
112
|
+
text = (
|
|
113
|
+
"<entml:thinking>\nplan\n</thinking>\n"
|
|
114
|
+
'<entml:invoke name="get_time">\n'
|
|
115
|
+
"</entml:invoke>"
|
|
116
|
+
)
|
|
117
|
+
clean, calls = _parse_stream(text, chunk=2)
|
|
118
|
+
assert len(calls) == 1
|
|
119
|
+
assert calls[0]["function"]["name"] == "get_time"
|
|
120
|
+
assert json.loads(calls[0]["function"]["arguments"]) == {}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_invoke_inside_unclosed_thinking_not_parsed() -> None:
|
|
124
|
+
text = f"<entml:thinking>\nplan {VALID_INVOKE}\n"
|
|
125
|
+
parser = FncallStreamParser(protocol=get_protocol("entml"), tools=TOOLS)
|
|
126
|
+
for i in range(0, len(text), 4):
|
|
127
|
+
parser.feed(text[i : i + 4])
|
|
128
|
+
assert not parser.has_calls
|
|
129
|
+
assert "cursor_agent_simple.py" in parser.partial_thinking
|
|
130
|
+
clean, calls = parser.finalize()
|
|
131
|
+
assert calls == []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_multiline_parameter_preserved() -> None:
|
|
135
|
+
body = "line1\nline2\nline3"
|
|
136
|
+
text = (
|
|
137
|
+
'<entml:invoke name="Read">\n'
|
|
138
|
+
f'<entml:parameter name="path">{body}</entml:parameter>\n'
|
|
139
|
+
"</entml:invoke>"
|
|
140
|
+
)
|
|
141
|
+
_, calls = _parse_batch(text)
|
|
142
|
+
args = json.loads(calls[0]["function"]["arguments"])
|
|
143
|
+
assert args["path"] == body
|