echotools 2.3.86__tar.gz → 2.3.88__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.86 → echotools-2.3.88}/PKG-INFO +1 -1
- {echotools-2.3.86 → echotools-2.3.88}/pyproject.toml +2 -2
- echotools-2.3.88/src/echotools/exec/fncall/parsers/stream.py +196 -0
- echotools-2.3.88/src/echotools/exec/fncall/parsers/stream_delta.py +269 -0
- echotools-2.3.88/src/echotools/exec/fncall/parsers/stream_feed.py +167 -0
- echotools-2.3.88/src/echotools/exec/fncall/parsers/stream_final.py +217 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/prompt/inject.py +30 -19
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/entml.py +2 -2
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/entml_invoke.py +56 -27
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/entml_patterns.py +85 -82
- echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_schema.py +392 -0
- echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_stream/__init__.py +22 -0
- echotools-2.3.86/src/echotools/exec/fncall/protocols/entml_stream_json.py → echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_stream/body.py +12 -191
- echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_stream/snap.py +175 -0
- echotools-2.3.86/src/echotools/exec/fncall/protocols/entml_tool_blocks.py → echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_think/blocks.py +54 -3
- echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_think/filter.py +303 -0
- echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_think/hold.py +321 -0
- echotools-2.3.88/src/echotools/exec/fncall/protocols/entml_think/parse.py +265 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/history_markup.py +76 -78
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools.egg-info/SOURCES.txt +10 -5
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_batch_stream_comprehensive.py +4 -1
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_protocol.py +181 -3
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_stream_json.py +1 -1
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_stream_json_exhaustive.py +1 -1
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_tool_pipeline.py +3 -3
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_tools.py +1 -1
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_model_branch_matrix.py +1 -1
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_stream_partial_json_parity.py +2 -2
- echotools-2.3.86/README.md +0 -3
- echotools-2.3.86/src/echotools/exec/fncall/parsers/stream.py +0 -853
- echotools-2.3.86/src/echotools/exec/fncall/protocols/entml_think/parse.py +0 -755
- echotools-2.3.86/src/echotools/exec/fncall/protocols/entml_tools.py +0 -156
- echotools-2.3.86/src/echotools/exec/fncall/protocols/entml_values.py +0 -131
- {echotools-2.3.86/docs → echotools-2.3.88}/README.md +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/setup.cfg +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/base.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/center.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/compat.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/parsers/xml_params.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/parsers/xml_parser.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/prompt/templates.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/coercion.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/fncall/shared/xml_helpers.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/keys.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/events/event.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/translate.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/application.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/runtime/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/runtime/collector.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/scheduler/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/scheduler/scheduler.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/sdk/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/plat/sdk/facade.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools/version.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/bench_import.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/conftest.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/fixtures/simulated_fake_history_markup_responses.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/fixtures/simulated_fault_thinking_responses.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_completion.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_config.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_config_extended.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_console.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_coverage_core.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_dispatch.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_agent_output_formats.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_parser_comprehensive.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_errors.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_errors_common.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_events.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_events_event.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_facade.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fake_history_markup_batch.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fake_history_markup_stream.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fault_thinking_close_batch.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fault_thinking_close_stream.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fncall.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_fncall_stream_thinking.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_history_markup_filter.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_ids.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_imports.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_io.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_keys.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_logger.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_merge.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_plugin.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_printstream.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_protocols.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_proxy.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_runtime.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_scheduler.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_stream_realtime_parse.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_tracing.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_usage.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_web.py +0 -0
- {echotools-2.3.86 → echotools-2.3.88}/src/tests/test_web_broker.py +0 -0
|
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "echotools"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.88"
|
|
8
8
|
description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
|
|
9
|
-
readme = "
|
|
9
|
+
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
11
11
|
license = { text = "MIT" }
|
|
12
12
|
authors = [{ name = "nichengfuben" }]
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""流式 fncall 检测状态机(协议感知版本)。
|
|
2
|
+
|
|
3
|
+
从 src/core/tools.py 迁移并改造为协议感知。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections import deque
|
|
9
|
+
from typing import Any, Deque, Dict, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from echotools.exec.protocol.base import ToolProtocol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 懒导入:仅 entml 协议需要
|
|
15
|
+
def _make_thinking_filter(protocol: ToolProtocol, *, thinking_enabled: bool = True):
|
|
16
|
+
if not thinking_enabled:
|
|
17
|
+
return None
|
|
18
|
+
if getattr(protocol, "id", None) == "entml":
|
|
19
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
20
|
+
EntmlThinkingStreamFilter,
|
|
21
|
+
)
|
|
22
|
+
return EntmlThinkingStreamFilter(thinking_enabled=thinking_enabled)
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
from echotools.exec.fncall.parsers.stream_feed import StreamFeedMixin
|
|
27
|
+
from echotools.exec.fncall.parsers.stream_delta import StreamDeltaMixin
|
|
28
|
+
from echotools.exec.fncall.parsers.stream_final import StreamFinalMixin
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FncallStreamParser(StreamFeedMixin, StreamDeltaMixin, StreamFinalMixin):
|
|
32
|
+
"""协议感知的流式 fncall 检测与解析状态机。
|
|
33
|
+
|
|
34
|
+
用法::
|
|
35
|
+
|
|
36
|
+
protocol = get_protocol("xml")
|
|
37
|
+
parser = FncallStreamParser(protocol=protocol, tools=tools)
|
|
38
|
+
parser.feed(chunk)
|
|
39
|
+
clean_text, tool_calls = parser.finalize()
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
WAITING_FOR_TAG = "WAITING_FOR_TAG"
|
|
43
|
+
IN_FUNCTION_CALLS = "IN_FUNCTION_CALLS"
|
|
44
|
+
DONE = "DONE"
|
|
45
|
+
|
|
46
|
+
def _init_end_tags(self, protocol: ToolProtocol) -> None:
|
|
47
|
+
# 1 声明非空结束标记 / 2 声明空列表禁自动关闭 / 3 从 trigger 推断
|
|
48
|
+
self._end_tags: List[str] = []
|
|
49
|
+
self._no_auto_close: bool = False
|
|
50
|
+
if hasattr(protocol, "get_stream_end_tags"):
|
|
51
|
+
declared = list(protocol.get_stream_end_tags())
|
|
52
|
+
if declared:
|
|
53
|
+
self._end_tags = declared
|
|
54
|
+
else:
|
|
55
|
+
self._no_auto_close = True
|
|
56
|
+
return
|
|
57
|
+
for tag in protocol.get_trigger_tags():
|
|
58
|
+
if tag.startswith("<") and not tag.startswith("</"):
|
|
59
|
+
tag_name = tag.lstrip("<").split(">")[0].split()[0]
|
|
60
|
+
end = f"</{tag_name}>"
|
|
61
|
+
if end != tag.replace("<", "</"):
|
|
62
|
+
self._end_tags.append(end)
|
|
63
|
+
elif tag.startswith("[") and not tag.startswith("[/"):
|
|
64
|
+
inner = tag.lstrip("[").split("]")[0]
|
|
65
|
+
self._end_tags.append(f"[/{inner}]")
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
protocol: ToolProtocol,
|
|
70
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
71
|
+
protocol_options: Optional[Dict[str, Any]] = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
self._protocol = protocol
|
|
74
|
+
self._tools = tools
|
|
75
|
+
from echotools.exec.fncall.protocols.entml_think.core import is_thinking_enabled
|
|
76
|
+
|
|
77
|
+
self._thinking_enabled = is_thinking_enabled(protocol_options)
|
|
78
|
+
self._raw_buf: str = ""
|
|
79
|
+
self._text_parts: List[str] = []
|
|
80
|
+
self._waiting_tail: str = ""
|
|
81
|
+
self._fncall_buf: str = ""
|
|
82
|
+
self._detected: bool = False
|
|
83
|
+
self._state: str = self.WAITING_FOR_TAG
|
|
84
|
+
self._finalized_result: Optional[Tuple[str, List[Dict[str, Any]]]] = None
|
|
85
|
+
self._thinking_parts: List[str] = []
|
|
86
|
+
self._thinking_filter = _make_thinking_filter(
|
|
87
|
+
protocol, thinking_enabled=self._thinking_enabled
|
|
88
|
+
)
|
|
89
|
+
self._emitted_invoke_count: int = 0
|
|
90
|
+
self._json_stream_encoder = None
|
|
91
|
+
self._pending_stream_deltas: Deque[Tuple[str, str, int]] = deque()
|
|
92
|
+
self._stream_invoke_emitted: List[str] = []
|
|
93
|
+
self._schema_index: Optional[Dict[str, Dict[str, Dict[str, Any]]]] = None
|
|
94
|
+
self._init_end_tags(protocol)
|
|
95
|
+
|
|
96
|
+
def _get_schema_index(self) -> Optional[Dict[str, Dict[str, Dict[str, Any]]]]:
|
|
97
|
+
if self._schema_index is None and self._tools:
|
|
98
|
+
from echotools.exec.fncall.shared.coercion import _build_param_schema_index
|
|
99
|
+
|
|
100
|
+
self._schema_index = _build_param_schema_index(self._tools)
|
|
101
|
+
return self._schema_index
|
|
102
|
+
|
|
103
|
+
def _emit_text(self, text: str) -> None:
|
|
104
|
+
"""将文本路由给思考过滤器(若启用)或直接追加到 _text_parts。"""
|
|
105
|
+
if not text:
|
|
106
|
+
return
|
|
107
|
+
if self._thinking_filter is not None:
|
|
108
|
+
for kind, part in self._thinking_filter.feed(text):
|
|
109
|
+
if kind == "thinking":
|
|
110
|
+
self._thinking_parts.append(part)
|
|
111
|
+
else:
|
|
112
|
+
self._append_content_text(part)
|
|
113
|
+
else:
|
|
114
|
+
self._append_content_text(text)
|
|
115
|
+
|
|
116
|
+
def _trim_trailing_visible_whitespace(self) -> None:
|
|
117
|
+
"""invoke 开始前去掉尾部空白可见段(thinking-only 回复常见 ``\\n\\n``)。"""
|
|
118
|
+
while self._text_parts and not self._text_parts[-1].strip():
|
|
119
|
+
self._text_parts.pop()
|
|
120
|
+
if self._text_parts:
|
|
121
|
+
trimmed = self._text_parts[-1].rstrip()
|
|
122
|
+
if trimmed:
|
|
123
|
+
self._text_parts[-1] = trimmed
|
|
124
|
+
else:
|
|
125
|
+
self._text_parts.pop()
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _split_safe_text(
|
|
129
|
+
buffer: str,
|
|
130
|
+
tags: List[str],
|
|
131
|
+
) -> Tuple[str, str]:
|
|
132
|
+
"""将 buffer 分为「可安全输出的前缀」和「需保留的尾部」。
|
|
133
|
+
|
|
134
|
+
除了「尾部是 trigger 的真前缀」外,还处理:
|
|
135
|
+
- trigger 已完整出现但尚未见到闭合 ``>``(例如 ``<entml:invoke name="x"``)
|
|
136
|
+
- trigger 声明带 ``>`` 时,其去尾 ``>`` 形式同样参与 holdback
|
|
137
|
+
"""
|
|
138
|
+
if not buffer:
|
|
139
|
+
return "", ""
|
|
140
|
+
if not tags:
|
|
141
|
+
return buffer, ""
|
|
142
|
+
|
|
143
|
+
bases: List[str] = []
|
|
144
|
+
for tag in tags:
|
|
145
|
+
if not tag:
|
|
146
|
+
continue
|
|
147
|
+
bases.append(tag)
|
|
148
|
+
if tag.endswith(">") and len(tag) > 1:
|
|
149
|
+
bases.append(tag[:-1])
|
|
150
|
+
|
|
151
|
+
# 已匹配完整 base 但属性/闭合 ``>`` 尚未到齐:从 base 起点整体 hold
|
|
152
|
+
hold_from: Optional[int] = None
|
|
153
|
+
for base in bases:
|
|
154
|
+
pos = buffer.find(base)
|
|
155
|
+
if pos < 0:
|
|
156
|
+
continue
|
|
157
|
+
if ">" not in buffer[pos:]:
|
|
158
|
+
if hold_from is None or pos < hold_from:
|
|
159
|
+
hold_from = pos
|
|
160
|
+
if hold_from is not None:
|
|
161
|
+
return buffer[:hold_from], buffer[hold_from:]
|
|
162
|
+
|
|
163
|
+
max_keep = max(len(t) - 1 for t in bases)
|
|
164
|
+
check_len = min(len(buffer), max_keep)
|
|
165
|
+
|
|
166
|
+
for length in range(check_len, 0, -1):
|
|
167
|
+
suffix = buffer[-length:]
|
|
168
|
+
if any(tag.startswith(suffix) and suffix != tag for tag in bases):
|
|
169
|
+
return buffer[:-length], buffer[-length:]
|
|
170
|
+
|
|
171
|
+
return buffer, ""
|
|
172
|
+
|
|
173
|
+
def _append_content_text(self, text: str) -> None:
|
|
174
|
+
if not text:
|
|
175
|
+
return
|
|
176
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
177
|
+
_THINKING_CLOSE,
|
|
178
|
+
strip_orphan_thinking_close_prefix,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
remainder = strip_orphan_thinking_close_prefix(text)
|
|
182
|
+
while True:
|
|
183
|
+
stripped = remainder.strip()
|
|
184
|
+
if not stripped:
|
|
185
|
+
return
|
|
186
|
+
if stripped == _THINKING_CLOSE:
|
|
187
|
+
return
|
|
188
|
+
if _THINKING_CLOSE in remainder:
|
|
189
|
+
before, _, after = remainder.partition(_THINKING_CLOSE)
|
|
190
|
+
if before.strip():
|
|
191
|
+
self._text_parts.append(before)
|
|
192
|
+
remainder = after
|
|
193
|
+
continue
|
|
194
|
+
self._text_parts.append(remainder)
|
|
195
|
+
return
|
|
196
|
+
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""FncallStreamParser streaming delta helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StreamDeltaMixin:
|
|
8
|
+
def _is_call_closed(self) -> bool:
|
|
9
|
+
"""检测 fncall 缓冲区中是否包含结束标记。"""
|
|
10
|
+
if self._no_auto_close:
|
|
11
|
+
return False
|
|
12
|
+
buf = self._fncall_buf
|
|
13
|
+
for end_tag in self._end_tags:
|
|
14
|
+
if end_tag in buf:
|
|
15
|
+
return True
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
def _stream_active_invoke_slot(self) -> int:
|
|
19
|
+
"""当前正在流式编码的 invoke 序号(0-based)。"""
|
|
20
|
+
from echotools.exec.fncall.protocols.entml_patterns import INVOKE_RE
|
|
21
|
+
from echotools.exec.fncall.protocols.entml_stream import (
|
|
22
|
+
_INVOKE_CLOSE,
|
|
23
|
+
_INVOKE_OPEN_PREFIX,
|
|
24
|
+
split_invoke_open,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
complete = list(INVOKE_RE.finditer(self._fncall_buf))
|
|
28
|
+
complete_n = len(complete)
|
|
29
|
+
parsed = split_invoke_open(self._fncall_buf)
|
|
30
|
+
if parsed is None:
|
|
31
|
+
return max(0, complete_n - 1)
|
|
32
|
+
_name, body_start = parsed
|
|
33
|
+
body = self._fncall_buf[body_start:]
|
|
34
|
+
if _INVOKE_CLOSE in body:
|
|
35
|
+
return max(0, complete_n - 1)
|
|
36
|
+
open_pos = self._fncall_buf.rfind(_INVOKE_OPEN_PREFIX)
|
|
37
|
+
if complete and open_pos >= complete[-1].end():
|
|
38
|
+
return complete_n
|
|
39
|
+
return max(0, complete_n - 1)
|
|
40
|
+
|
|
41
|
+
def _merge_or_append_pending_delta(self, name: str, piece: str, slot: int) -> None:
|
|
42
|
+
if not piece:
|
|
43
|
+
return
|
|
44
|
+
if self._pending_stream_deltas and self._pending_stream_deltas[-1][2] == slot:
|
|
45
|
+
prev_name, prev_piece, prev_slot = self._pending_stream_deltas[-1]
|
|
46
|
+
self._pending_stream_deltas[-1] = (prev_name, prev_piece + piece, prev_slot)
|
|
47
|
+
else:
|
|
48
|
+
self._pending_stream_deltas.append((name, piece, slot))
|
|
49
|
+
|
|
50
|
+
def _pending_body_for_name(self, name: str) -> str:
|
|
51
|
+
return "".join(p for n, p in self._pending_stream_deltas if n == name)
|
|
52
|
+
|
|
53
|
+
def _track_stream_delta(self, name: str, piece: str) -> None:
|
|
54
|
+
slot = self._stream_active_invoke_slot()
|
|
55
|
+
while len(self._stream_invoke_emitted) <= slot:
|
|
56
|
+
self._stream_invoke_emitted.append("")
|
|
57
|
+
self._stream_invoke_emitted[slot] += piece
|
|
58
|
+
self._merge_or_append_pending_delta(name, piece, slot)
|
|
59
|
+
|
|
60
|
+
def _ready_call_tail(self, call, prev: str, *, multi: bool) -> str:
|
|
61
|
+
final = call["function"]["arguments"]
|
|
62
|
+
if final.startswith(prev):
|
|
63
|
+
return final[len(prev) :]
|
|
64
|
+
if prev:
|
|
65
|
+
return ""
|
|
66
|
+
pname = call["function"]["name"]
|
|
67
|
+
pending_body = self._pending_body_for_name(pname) if not multi else ""
|
|
68
|
+
if final.startswith(pending_body):
|
|
69
|
+
return final[len(pending_body) :]
|
|
70
|
+
return final if not pending_body else ""
|
|
71
|
+
|
|
72
|
+
def _align_json_encoder_after_ready(self) -> None:
|
|
73
|
+
if self._json_stream_encoder is None:
|
|
74
|
+
return
|
|
75
|
+
from echotools.exec.fncall.protocols.entml_stream import (
|
|
76
|
+
_INVOKE_CLOSE,
|
|
77
|
+
build_streaming_json_snapshot,
|
|
78
|
+
split_invoke_open,
|
|
79
|
+
)
|
|
80
|
+
parsed = split_invoke_open(self._fncall_buf)
|
|
81
|
+
if not parsed:
|
|
82
|
+
self._json_stream_encoder.reset()
|
|
83
|
+
return
|
|
84
|
+
name, body_start = parsed
|
|
85
|
+
body = self._fncall_buf[body_start:]
|
|
86
|
+
if _INVOKE_CLOSE in body:
|
|
87
|
+
self._json_stream_encoder.reset()
|
|
88
|
+
return
|
|
89
|
+
snap = build_streaming_json_snapshot(
|
|
90
|
+
body, tool_name=name, schema_index=self._get_schema_index()
|
|
91
|
+
)
|
|
92
|
+
self._json_stream_encoder.set_tool_context(name, self._get_schema_index())
|
|
93
|
+
self._json_stream_encoder._emitted = snap
|
|
94
|
+
|
|
95
|
+
def _ensure_ready_invoke_stream_tails(
|
|
96
|
+
self, ready: List[Dict[str, Any]]
|
|
97
|
+
) -> None:
|
|
98
|
+
"""invoke ready 时补齐 partial_json 尾部,避免大 chunk 下缺 ``}``。"""
|
|
99
|
+
if not ready:
|
|
100
|
+
return
|
|
101
|
+
base = self._emitted_invoke_count - len(ready)
|
|
102
|
+
multi = len(ready) > 1
|
|
103
|
+
if multi:
|
|
104
|
+
self._pending_stream_deltas.clear()
|
|
105
|
+
for offset, call in enumerate(ready):
|
|
106
|
+
idx = base + offset
|
|
107
|
+
while len(self._stream_invoke_emitted) <= idx:
|
|
108
|
+
self._stream_invoke_emitted.append("")
|
|
109
|
+
prev = self._stream_invoke_emitted[idx]
|
|
110
|
+
tail = self._ready_call_tail(call, prev, multi=multi)
|
|
111
|
+
if tail:
|
|
112
|
+
name = call["function"]["name"]
|
|
113
|
+
if multi:
|
|
114
|
+
self._pending_stream_deltas.append((name, tail, idx))
|
|
115
|
+
else:
|
|
116
|
+
self._merge_or_append_pending_delta(name, tail, idx)
|
|
117
|
+
self._stream_invoke_emitted[idx] = call["function"]["arguments"]
|
|
118
|
+
self._align_json_encoder_after_ready()
|
|
119
|
+
|
|
120
|
+
def _sync_json_stream_encoder_emitted(self) -> None:
|
|
121
|
+
"""invoke 已 ready 后对齐 encoder,避免下一 chunk 重发整段 partial_json。"""
|
|
122
|
+
if self._json_stream_encoder is None:
|
|
123
|
+
return
|
|
124
|
+
from echotools.exec.fncall.protocols.entml_stream import (
|
|
125
|
+
split_invoke_open,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
parsed = split_invoke_open(self._fncall_buf)
|
|
129
|
+
if not parsed:
|
|
130
|
+
self._json_stream_encoder.reset()
|
|
131
|
+
return
|
|
132
|
+
name, body_start = parsed
|
|
133
|
+
body = self._fncall_buf[body_start:]
|
|
134
|
+
self._json_stream_encoder.poll(body)
|
|
135
|
+
|
|
136
|
+
def feed(self, chunk: str) -> List[Dict[str, Any]]:
|
|
137
|
+
"""喂入新的流式文本块。
|
|
138
|
+
|
|
139
|
+
返回本轮新完成的 tool_calls(与 ``get_ready_tool_calls()`` 相同语义:
|
|
140
|
+
调用后计数前进,勿再对本轮结果重复调用 ``get_ready_tool_calls``)。
|
|
141
|
+
DONE 或 finalize 后调用返回 ``[]``。
|
|
142
|
+
"""
|
|
143
|
+
if not chunk or self._state == self.DONE:
|
|
144
|
+
return []
|
|
145
|
+
if self._finalized_result is not None:
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
self._raw_buf += chunk
|
|
149
|
+
|
|
150
|
+
if self._state != self.DONE:
|
|
151
|
+
self._feed_waiting(chunk)
|
|
152
|
+
|
|
153
|
+
polled = self._poll_streaming_tool_input_delta()
|
|
154
|
+
if polled:
|
|
155
|
+
self._track_stream_delta(polled[0], polled[1])
|
|
156
|
+
ready = self.get_ready_tool_calls()
|
|
157
|
+
if ready:
|
|
158
|
+
self._ensure_ready_invoke_stream_tails(ready)
|
|
159
|
+
return ready
|
|
160
|
+
|
|
161
|
+
def _poll_streaming_tool_input_delta(self) -> Optional[Tuple[str, str]]:
|
|
162
|
+
"""invoke 开标签完整匹配后,返回 (tool_name, partial_json_delta)。"""
|
|
163
|
+
if not self._detected or getattr(self._protocol, "id", None) != "entml":
|
|
164
|
+
return None
|
|
165
|
+
if self._state not in (self.IN_FUNCTION_CALLS,):
|
|
166
|
+
return None
|
|
167
|
+
from echotools.exec.fncall.protocols.entml_patterns import INVOKE_RE
|
|
168
|
+
from echotools.exec.fncall.protocols.entml_stream import (
|
|
169
|
+
_INVOKE_CLOSE,
|
|
170
|
+
EntmlInvokeJsonStreamEncoder,
|
|
171
|
+
build_streaming_json_snapshot,
|
|
172
|
+
split_invoke_open,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
parsed = split_invoke_open(self._fncall_buf)
|
|
176
|
+
if not parsed:
|
|
177
|
+
return None
|
|
178
|
+
name, body_start = parsed
|
|
179
|
+
body = self._fncall_buf[body_start:]
|
|
180
|
+
complete = list(INVOKE_RE.finditer(self._fncall_buf))
|
|
181
|
+
complete_n = len(complete)
|
|
182
|
+
if _INVOKE_CLOSE in body:
|
|
183
|
+
if complete_n <= self._emitted_invoke_count:
|
|
184
|
+
return None
|
|
185
|
+
# 同一 chunk 内多个 invoke 同时闭合时,由 ensure 按序入队,避免只 poll 最后一个
|
|
186
|
+
if complete_n - self._emitted_invoke_count > 1:
|
|
187
|
+
return None
|
|
188
|
+
slot = max(0, complete_n - 1)
|
|
189
|
+
schema_index = self._get_schema_index()
|
|
190
|
+
final = build_streaming_json_snapshot(
|
|
191
|
+
body,
|
|
192
|
+
tool_name=name,
|
|
193
|
+
schema_index=schema_index,
|
|
194
|
+
)
|
|
195
|
+
while len(self._stream_invoke_emitted) <= slot:
|
|
196
|
+
self._stream_invoke_emitted.append("")
|
|
197
|
+
if self._stream_invoke_emitted[slot] == final:
|
|
198
|
+
return None
|
|
199
|
+
schema_index = self._get_schema_index()
|
|
200
|
+
if self._json_stream_encoder is None:
|
|
201
|
+
self._json_stream_encoder = EntmlInvokeJsonStreamEncoder(
|
|
202
|
+
tool_name=name,
|
|
203
|
+
schema_index=schema_index,
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
self._json_stream_encoder.set_tool_context(name, schema_index)
|
|
207
|
+
delta = self._json_stream_encoder.poll(body)
|
|
208
|
+
if not delta:
|
|
209
|
+
return None
|
|
210
|
+
return (name, delta)
|
|
211
|
+
|
|
212
|
+
def consume_stream_delta(self) -> Optional[Tuple[str, str]]:
|
|
213
|
+
"""取出本轮 ``feed`` 产生的 streaming partial_json 增量(FIFO,可多段)。"""
|
|
214
|
+
if not self._pending_stream_deltas:
|
|
215
|
+
return None
|
|
216
|
+
name, piece, _slot = self._pending_stream_deltas.popleft()
|
|
217
|
+
return (name, piece)
|
|
218
|
+
|
|
219
|
+
def stream_invoke_argument_snapshots(self) -> List[str]:
|
|
220
|
+
"""各 invoke slot 已流式发出的 arguments JSON 累积(与 batch 对齐)。"""
|
|
221
|
+
return list(self._stream_invoke_emitted)
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def streaming_invoke_closed(self) -> bool:
|
|
225
|
+
"""流式 fncall 缓冲中是否已出现 ``</entml:invoke>``。"""
|
|
226
|
+
if getattr(self._protocol, "id", None) != "entml":
|
|
227
|
+
return False
|
|
228
|
+
from echotools.exec.fncall.protocols.entml_stream import _INVOKE_CLOSE
|
|
229
|
+
|
|
230
|
+
return _INVOKE_CLOSE in self._fncall_buf
|
|
231
|
+
|
|
232
|
+
def complete_stream_delta_if_needed(self) -> Optional[Tuple[str, str]]:
|
|
233
|
+
"""invoke 已开标签但未闭合时,补齐 partial_json 尾部的合法 JSON 后缀。"""
|
|
234
|
+
if not self._detected or getattr(self._protocol, "id", None) != "entml":
|
|
235
|
+
return None
|
|
236
|
+
if self.streaming_invoke_closed:
|
|
237
|
+
return None
|
|
238
|
+
from echotools.exec.fncall.protocols.entml_stream import (
|
|
239
|
+
EntmlInvokeJsonStreamEncoder,
|
|
240
|
+
build_streaming_json_snapshot,
|
|
241
|
+
split_invoke_open,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
parsed = split_invoke_open(self._fncall_buf)
|
|
245
|
+
if not parsed:
|
|
246
|
+
return None
|
|
247
|
+
name, body_start = parsed
|
|
248
|
+
body = self._fncall_buf[body_start:]
|
|
249
|
+
schema_index = self._get_schema_index()
|
|
250
|
+
snapshot = build_streaming_json_snapshot(
|
|
251
|
+
body,
|
|
252
|
+
tool_name=name,
|
|
253
|
+
schema_index=schema_index,
|
|
254
|
+
force_close=True,
|
|
255
|
+
)
|
|
256
|
+
if not snapshot:
|
|
257
|
+
return None
|
|
258
|
+
if self._json_stream_encoder is None:
|
|
259
|
+
self._json_stream_encoder = EntmlInvokeJsonStreamEncoder(
|
|
260
|
+
tool_name=name,
|
|
261
|
+
schema_index=schema_index,
|
|
262
|
+
)
|
|
263
|
+
else:
|
|
264
|
+
self._json_stream_encoder.set_tool_context(name, schema_index)
|
|
265
|
+
delta = self._json_stream_encoder.poll(body, force_close=True)
|
|
266
|
+
if not delta:
|
|
267
|
+
return None
|
|
268
|
+
return (name, delta)
|
|
269
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""FncallStreamParser waiting/feed helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StreamFeedMixin:
|
|
8
|
+
def _normalize_stream_chunk(self, text: str) -> str:
|
|
9
|
+
fn = getattr(self._protocol, "normalize_stream_buffer", None)
|
|
10
|
+
if callable(fn):
|
|
11
|
+
return fn(text)
|
|
12
|
+
return text
|
|
13
|
+
|
|
14
|
+
def _invoke_close_tag(self) -> str:
|
|
15
|
+
return self._end_tags[0] if self._end_tags else "</entml:invoke>"
|
|
16
|
+
|
|
17
|
+
def _drain_thinking_holdback_to_fncall(self) -> None:
|
|
18
|
+
"""invoke 已开始后,thinking 过滤器里对工具标签的 holdback 应并入 fncall 缓冲。"""
|
|
19
|
+
if self._thinking_filter is None:
|
|
20
|
+
return
|
|
21
|
+
pending = self._thinking_filter._pending
|
|
22
|
+
if pending:
|
|
23
|
+
self._fncall_buf += pending
|
|
24
|
+
self._thinking_filter._pending = ""
|
|
25
|
+
|
|
26
|
+
def _begin_function_calls(self, buffer_from: str, *, pos: int) -> None:
|
|
27
|
+
self._trim_trailing_visible_whitespace()
|
|
28
|
+
self._fncall_buf = buffer_from[pos:]
|
|
29
|
+
self._drain_thinking_holdback_to_fncall()
|
|
30
|
+
self._detected = True
|
|
31
|
+
self._state = self.IN_FUNCTION_CALLS
|
|
32
|
+
self._json_stream_encoder = None
|
|
33
|
+
if self._thinking_filter is not None:
|
|
34
|
+
self._thinking_filter.clear_invoke_handoff_state()
|
|
35
|
+
|
|
36
|
+
def _hold_or_split_content(self, text: str, trigger_tags) -> bool:
|
|
37
|
+
"""True 表示已消费 text(含 holdback)。"""
|
|
38
|
+
hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
|
|
39
|
+
if hold_from_fn is not None:
|
|
40
|
+
hold_from = hold_from_fn(text)
|
|
41
|
+
if hold_from is not None:
|
|
42
|
+
if hold_from > 0:
|
|
43
|
+
self._append_content_text(text[:hold_from])
|
|
44
|
+
self._waiting_tail = text[hold_from:]
|
|
45
|
+
return True
|
|
46
|
+
safe, remain = self._split_safe_text(text, trigger_tags)
|
|
47
|
+
if safe:
|
|
48
|
+
self._append_content_text(safe)
|
|
49
|
+
self._waiting_tail = remain
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
def _hold_or_split_emit(self, text: str, trigger_tags) -> None:
|
|
53
|
+
hold_from_fn = getattr(self._protocol, "find_fncall_hold_from", None)
|
|
54
|
+
if hold_from_fn is not None:
|
|
55
|
+
hold_from = hold_from_fn(text)
|
|
56
|
+
if hold_from is not None:
|
|
57
|
+
if hold_from > 0:
|
|
58
|
+
self._emit_text(text[:hold_from])
|
|
59
|
+
self._waiting_tail = text[hold_from:]
|
|
60
|
+
return
|
|
61
|
+
safe, remain = self._split_safe_text(text, trigger_tags)
|
|
62
|
+
if safe:
|
|
63
|
+
self._emit_text(safe)
|
|
64
|
+
self._waiting_tail = remain
|
|
65
|
+
|
|
66
|
+
def _feed_content_waiting(self, text: str) -> None:
|
|
67
|
+
"""thinking 已闭合后,对可见正文做 invoke 检测(不再经过 thinking 过滤器)。"""
|
|
68
|
+
if not text:
|
|
69
|
+
return
|
|
70
|
+
text = self._normalize_stream_chunk(text)
|
|
71
|
+
if self._state == self.IN_FUNCTION_CALLS:
|
|
72
|
+
close_tag = self._invoke_close_tag()
|
|
73
|
+
if not self._is_call_closed():
|
|
74
|
+
if close_tag in text:
|
|
75
|
+
idx = text.find(close_tag)
|
|
76
|
+
self._fncall_buf += text[: idx + len(close_tag)]
|
|
77
|
+
tail = text[idx + len(close_tag) :]
|
|
78
|
+
if tail:
|
|
79
|
+
self._feed_content_waiting(tail)
|
|
80
|
+
return
|
|
81
|
+
self._fncall_buf += text
|
|
82
|
+
return
|
|
83
|
+
trigger_tags = self._protocol.get_trigger_tags()
|
|
84
|
+
found, pos = self._protocol.detect_start(text)
|
|
85
|
+
if found:
|
|
86
|
+
if pos > 0:
|
|
87
|
+
self._append_content_text(text[:pos])
|
|
88
|
+
self._fncall_buf += text[pos:]
|
|
89
|
+
return
|
|
90
|
+
self._hold_or_split_content(text, trigger_tags)
|
|
91
|
+
return
|
|
92
|
+
trigger_tags = self._protocol.get_trigger_tags()
|
|
93
|
+
found, pos = self._protocol.detect_start(text)
|
|
94
|
+
if not found:
|
|
95
|
+
self._hold_or_split_content(text, trigger_tags)
|
|
96
|
+
return
|
|
97
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
98
|
+
invoke_index_inside_any_thinking_block,
|
|
99
|
+
)
|
|
100
|
+
if invoke_index_inside_any_thinking_block(
|
|
101
|
+
text, pos, thinking_enabled=self._thinking_enabled,
|
|
102
|
+
):
|
|
103
|
+
self._append_content_text(text)
|
|
104
|
+
return
|
|
105
|
+
if pos > 0:
|
|
106
|
+
self._append_content_text(text[:pos])
|
|
107
|
+
self._begin_function_calls(text, pos=pos)
|
|
108
|
+
|
|
109
|
+
def _feed_waiting_thinking_plain(self, combined: str) -> None:
|
|
110
|
+
"""未闭合 thinking 阶段:块内一律按纯文本进 thinking,不检测 invoke。"""
|
|
111
|
+
assert self._thinking_filter is not None
|
|
112
|
+
for kind, part in self._thinking_filter.feed(combined):
|
|
113
|
+
if kind == "thinking":
|
|
114
|
+
self._thinking_parts.append(part)
|
|
115
|
+
elif part:
|
|
116
|
+
self._feed_content_waiting(part)
|
|
117
|
+
if self._state == self.IN_FUNCTION_CALLS:
|
|
118
|
+
self._drain_thinking_holdback_to_fncall()
|
|
119
|
+
|
|
120
|
+
def _feed_waiting(self, chunk: str) -> None:
|
|
121
|
+
"""在 WAITING_FOR_TAG / IN_FUNCTION_CALLS 状态下处理新块。"""
|
|
122
|
+
combined = self._normalize_stream_chunk(self._waiting_tail + chunk)
|
|
123
|
+
self._waiting_tail = ""
|
|
124
|
+
if self._state == self.IN_FUNCTION_CALLS:
|
|
125
|
+
if self._thinking_filter is not None and self._thinking_filter.in_open_thinking():
|
|
126
|
+
self._feed_waiting_thinking_plain(combined)
|
|
127
|
+
return
|
|
128
|
+
if self._waiting_tail:
|
|
129
|
+
self._fncall_buf += self._normalize_stream_chunk(self._waiting_tail)
|
|
130
|
+
self._waiting_tail = ""
|
|
131
|
+
self._drain_thinking_holdback_to_fncall()
|
|
132
|
+
if not self._is_call_closed():
|
|
133
|
+
self._fncall_buf += combined
|
|
134
|
+
return
|
|
135
|
+
self._feed_content_waiting(combined)
|
|
136
|
+
return
|
|
137
|
+
if self._thinking_filter is not None and self._thinking_filter.in_open_thinking():
|
|
138
|
+
self._feed_waiting_thinking_plain(combined)
|
|
139
|
+
return
|
|
140
|
+
trigger_tags = self._protocol.get_trigger_tags()
|
|
141
|
+
found, pos = self._protocol.detect_start(combined)
|
|
142
|
+
if found:
|
|
143
|
+
if self._thinking_filter is not None:
|
|
144
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
145
|
+
invoke_index_inside_any_thinking_block,
|
|
146
|
+
)
|
|
147
|
+
if invoke_index_inside_any_thinking_block(
|
|
148
|
+
combined, pos, thinking_enabled=self._thinking_enabled,
|
|
149
|
+
):
|
|
150
|
+
self._feed_waiting_thinking_plain(combined)
|
|
151
|
+
return
|
|
152
|
+
if pos > 0:
|
|
153
|
+
self._emit_text(combined[:pos])
|
|
154
|
+
self._begin_function_calls(combined, pos=pos)
|
|
155
|
+
return
|
|
156
|
+
# detect_start 之后再处理未闭合 thinking,避免 legacy 外壳后 `<en` 误 hold
|
|
157
|
+
if self._thinking_filter is not None:
|
|
158
|
+
from echotools.exec.fncall.protocols.entml_think.parse import (
|
|
159
|
+
has_unclosed_entml_thinking,
|
|
160
|
+
)
|
|
161
|
+
if has_unclosed_entml_thinking(
|
|
162
|
+
combined, thinking_enabled=self._thinking_enabled
|
|
163
|
+
):
|
|
164
|
+
self._feed_waiting_thinking_plain(combined)
|
|
165
|
+
return
|
|
166
|
+
self._hold_or_split_emit(combined, trigger_tags)
|
|
167
|
+
|