iCode-TUI 0.29.1__py3-none-any.whl
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.
- chrys/__init__.py +16 -0
- chrys/app/__init__.py +3 -0
- chrys/app/acp/__init__.py +3 -0
- chrys/app/acp/bridge.py +664 -0
- chrys/app/acp/content.py +132 -0
- chrys/app/acp/doc/frontend-api.md +425 -0
- chrys/app/acp/history.py +585 -0
- chrys/app/acp/server.py +1796 -0
- chrys/app/acp/session_manager.py +1815 -0
- chrys/app/acp/tool_status.py +45 -0
- chrys/app/cli/__init__.py +3 -0
- chrys/app/cli/acp.py +178 -0
- chrys/app/cli/app.py +165 -0
- chrys/app/cli/approvals.py +101 -0
- chrys/app/cli/headless.py +223 -0
- chrys/app/cli/launch_cwd.py +42 -0
- chrys/app/cli/profiles.py +298 -0
- chrys/app/cli/progress.py +857 -0
- chrys/app/cli/run.py +425 -0
- chrys/app/cli/serve.py +1593 -0
- chrys/app/cli/tool_summary.py +225 -0
- chrys/app/cli/trajectory.py +201 -0
- chrys/app/cli/workflow.py +601 -0
- chrys/app/features/__init__.py +3 -0
- chrys/app/features/buddy/__init__.py +3 -0
- chrys/app/features/buddy/actions.py +68 -0
- chrys/app/features/buddy/animation.py +76 -0
- chrys/app/features/buddy/commands.py +196 -0
- chrys/app/features/buddy/docs/design.md +187 -0
- chrys/app/features/buddy/hatchery.py +80 -0
- chrys/app/features/buddy/lifecycle.py +41 -0
- chrys/app/features/buddy/model.py +265 -0
- chrys/app/features/buddy/pixel_renderer.py +113 -0
- chrys/app/features/buddy/pixel_sprites.py +279 -0
- chrys/app/features/buddy/portrait.py +76 -0
- chrys/app/features/buddy/progression.py +58 -0
- chrys/app/features/buddy/replies.py +191 -0
- chrys/app/features/buddy/sprites/alien.toml +70 -0
- chrys/app/features/buddy/sprites/axolotl.toml +71 -0
- chrys/app/features/buddy/sprites/bat.toml +69 -0
- chrys/app/features/buddy/sprites/bee.toml +69 -0
- chrys/app/features/buddy/sprites/cactus.toml +73 -0
- chrys/app/features/buddy/sprites/capybara.toml +70 -0
- chrys/app/features/buddy/sprites/cat.toml +69 -0
- chrys/app/features/buddy/sprites/crab.toml +70 -0
- chrys/app/features/buddy/sprites/dragon.toml +71 -0
- chrys/app/features/buddy/sprites/duck.toml +69 -0
- chrys/app/features/buddy/sprites/fox.toml +69 -0
- chrys/app/features/buddy/sprites/frog.toml +70 -0
- chrys/app/features/buddy/sprites/ghost.toml +70 -0
- chrys/app/features/buddy/sprites/goose.toml +69 -0
- chrys/app/features/buddy/sprites/jelly.toml +70 -0
- chrys/app/features/buddy/sprites/llama.toml +70 -0
- chrys/app/features/buddy/sprites/mushroom.toml +69 -0
- chrys/app/features/buddy/sprites/octopus.toml +70 -0
- chrys/app/features/buddy/sprites/owl.toml +70 -0
- chrys/app/features/buddy/sprites/panda.toml +71 -0
- chrys/app/features/buddy/sprites/penguin.toml +69 -0
- chrys/app/features/buddy/sprites/rabbit.toml +70 -0
- chrys/app/features/buddy/sprites/robot.toml +72 -0
- chrys/app/features/buddy/sprites/shark.toml +70 -0
- chrys/app/features/buddy/sprites/snail.toml +69 -0
- chrys/app/features/buddy/sprites/snake.toml +69 -0
- chrys/app/features/buddy/sprites/tabby.toml +69 -0
- chrys/app/features/buddy/sprites/turtle.toml +69 -0
- chrys/app/features/buddy/store.py +143 -0
- chrys/app/features/session_title/__init__.py +7 -0
- chrys/app/features/session_title/updater.py +582 -0
- chrys/app/installer.py +651 -0
- chrys/app/parsing.py +17 -0
- chrys/app/tui/__init__.py +3 -0
- chrys/app/tui/app.py +1287 -0
- chrys/app/tui/behaviors/__init__.py +3 -0
- chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
- chrys/app/tui/behaviors/insert_clipboard.py +73 -0
- chrys/app/tui/behaviors/right_click_copy.py +146 -0
- chrys/app/tui/binding_display.py +139 -0
- chrys/app/tui/buddy_messages.py +17 -0
- chrys/app/tui/buddy_reply.py +86 -0
- chrys/app/tui/chrys.tcss +549 -0
- chrys/app/tui/clipboard.py +109 -0
- chrys/app/tui/copy_messages.py +10 -0
- chrys/app/tui/i18n.py +230 -0
- chrys/app/tui/language.py +58 -0
- chrys/app/tui/notifications/__init__.py +12 -0
- chrys/app/tui/notifications/drivers.py +180 -0
- chrys/app/tui/notifications/service.py +126 -0
- chrys/app/tui/notifications/settings.py +141 -0
- chrys/app/tui/screens/__init__.py +3 -0
- chrys/app/tui/screens/agents/__init__.py +8 -0
- chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
- chrys/app/tui/screens/agents/config.py +2142 -0
- chrys/app/tui/screens/agents/config.tcss +169 -0
- chrys/app/tui/screens/agents/panels/__init__.py +3 -0
- chrys/app/tui/screens/agents/panels/acp.py +1176 -0
- chrys/app/tui/screens/agents/panels/basic.py +497 -0
- chrys/app/tui/screens/agents/panels/compaction.py +231 -0
- chrys/app/tui/screens/agents/panels/config_card.py +218 -0
- chrys/app/tui/screens/agents/panels/instructions.py +91 -0
- chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
- chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
- chrys/app/tui/screens/agents/panels/memory.py +684 -0
- chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
- chrys/app/tui/screens/agents/panels/skills.py +806 -0
- chrys/app/tui/screens/agents/panels/subagents.py +652 -0
- chrys/app/tui/screens/agents/panels/tools.py +205 -0
- chrys/app/tui/screens/agents/picker.py +103 -0
- chrys/app/tui/screens/agents/skill_paths.py +38 -0
- chrys/app/tui/screens/agents/validation_messages.py +452 -0
- chrys/app/tui/screens/dialogs/__init__.py +35 -0
- chrys/app/tui/screens/dialogs/agent_load.py +834 -0
- chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
- chrys/app/tui/screens/dialogs/app_mode.py +55 -0
- chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
- chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
- chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
- chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
- chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
- chrys/app/tui/screens/dialogs/approval/body.py +110 -0
- chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
- chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
- chrys/app/tui/screens/dialogs/ask_user.py +148 -0
- chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
- chrys/app/tui/screens/dialogs/base.py +84 -0
- chrys/app/tui/screens/dialogs/confirm.py +184 -0
- chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
- chrys/app/tui/screens/dialogs/connection_test.py +183 -0
- chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
- chrys/app/tui/screens/dialogs/diagram.py +102 -0
- chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
- chrys/app/tui/screens/dialogs/editor.py +378 -0
- chrys/app/tui/screens/dialogs/editor.tcss +132 -0
- chrys/app/tui/screens/dialogs/file_picker.py +582 -0
- chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
- chrys/app/tui/screens/dialogs/fork_session.py +219 -0
- chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
- chrys/app/tui/screens/dialogs/image_compression.py +102 -0
- chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
- chrys/app/tui/screens/dialogs/man_page.py +172 -0
- chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
- chrys/app/tui/screens/dialogs/new_folder.py +277 -0
- chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
- chrys/app/tui/screens/dialogs/notice.tcss +14 -0
- chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
- chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
- chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
- chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
- chrys/app/tui/screens/dialogs/session_title.py +110 -0
- chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
- chrys/app/tui/screens/dialogs/tool_view.py +207 -0
- chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
- chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
- chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
- chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
- chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
- chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
- chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
- chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
- chrys/app/tui/screens/diff/__init__.py +22 -0
- chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
- chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
- chrys/app/tui/screens/diff/screen.py +1532 -0
- chrys/app/tui/screens/diff/screen.tcss +104 -0
- chrys/app/tui/screens/guides/__init__.py +7 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
- chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/index.py +215 -0
- chrys/app/tui/screens/guides/reader.py +173 -0
- chrys/app/tui/screens/guides/screen.py +442 -0
- chrys/app/tui/screens/guides/screen.tcss +52 -0
- chrys/app/tui/screens/logs/__init__.py +7 -0
- chrys/app/tui/screens/logs/screen.py +504 -0
- chrys/app/tui/screens/logs/screen.tcss +45 -0
- chrys/app/tui/screens/main/__init__.py +7 -0
- chrys/app/tui/screens/main/action_availability.py +30 -0
- chrys/app/tui/screens/main/buddy_command.py +143 -0
- chrys/app/tui/screens/main/commands.py +1073 -0
- chrys/app/tui/screens/main/config_actions.py +464 -0
- chrys/app/tui/screens/main/copy_actions.py +117 -0
- chrys/app/tui/screens/main/dialog_controllers.py +826 -0
- chrys/app/tui/screens/main/dialog_gateway.py +166 -0
- chrys/app/tui/screens/main/diff_controller.py +478 -0
- chrys/app/tui/screens/main/engine_read_model.py +184 -0
- chrys/app/tui/screens/main/event_handlers.py +2014 -0
- chrys/app/tui/screens/main/input_flow.py +241 -0
- chrys/app/tui/screens/main/live_diff.py +244 -0
- chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
- chrys/app/tui/screens/main/model_indicator.py +217 -0
- chrys/app/tui/screens/main/navigation.py +343 -0
- chrys/app/tui/screens/main/ports.py +571 -0
- chrys/app/tui/screens/main/recent_dirs.py +123 -0
- chrys/app/tui/screens/main/rollback_controller.py +586 -0
- chrys/app/tui/screens/main/runtime_info.py +71 -0
- chrys/app/tui/screens/main/screen.py +2265 -0
- chrys/app/tui/screens/main/screen.tcss +21 -0
- chrys/app/tui/screens/main/session_handlers.py +1127 -0
- chrys/app/tui/screens/main/session_title.py +239 -0
- chrys/app/tui/screens/main/settings_coordinator.py +365 -0
- chrys/app/tui/screens/main/settings_persistence.py +184 -0
- chrys/app/tui/screens/main/shell_mode.py +112 -0
- chrys/app/tui/screens/main/state.py +289 -0
- chrys/app/tui/screens/main/submit_state.py +11 -0
- chrys/app/tui/screens/main/subscriptions.py +177 -0
- chrys/app/tui/screens/main/suggestions.py +982 -0
- chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
- chrys/app/tui/screens/main/view_adapter.py +1567 -0
- chrys/app/tui/screens/main/workflow_browser.py +747 -0
- chrys/app/tui/screens/main/workflow_content.py +248 -0
- chrys/app/tui/screens/main/workflow_controller.py +240 -0
- chrys/app/tui/screens/main/workflow_feedback.py +137 -0
- chrys/app/tui/screens/main/workflow_flow.py +49 -0
- chrys/app/tui/screens/main/workflow_run_control.py +456 -0
- chrys/app/tui/screens/main/workflow_session_view.py +316 -0
- chrys/app/tui/screens/main/workspace_actions.py +272 -0
- chrys/app/tui/screens/main/workspace_branch.py +194 -0
- chrys/app/tui/screens/menus/__init__.py +8 -0
- chrys/app/tui/screens/menus/languages.py +114 -0
- chrys/app/tui/screens/models/__init__.py +7 -0
- chrys/app/tui/screens/models/picker.py +147 -0
- chrys/app/tui/screens/models/screen.py +2043 -0
- chrys/app/tui/screens/models/screen.tcss +255 -0
- chrys/app/tui/screens/sessions/__init__.py +8 -0
- chrys/app/tui/screens/sessions/presenter.py +586 -0
- chrys/app/tui/screens/sessions/screen.py +779 -0
- chrys/app/tui/screens/sessions/screen.tcss +127 -0
- chrys/app/tui/screens/settings/__init__.py +29 -0
- chrys/app/tui/screens/settings/danger.py +26 -0
- chrys/app/tui/screens/settings/dialog.py +281 -0
- chrys/app/tui/screens/settings/layout.py +519 -0
- chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
- chrys/app/tui/screens/settings/panes/__init__.py +3 -0
- chrys/app/tui/screens/settings/panes/notifications.py +248 -0
- chrys/app/tui/screens/settings/panes/sessions.py +148 -0
- chrys/app/tui/screens/settings/ports.py +55 -0
- chrys/app/tui/screens/settings/provenance.py +102 -0
- chrys/app/tui/screens/settings/rows.py +546 -0
- chrys/app/tui/screens/settings/settings.tcss +406 -0
- chrys/app/tui/screens/themes/__init__.py +8 -0
- chrys/app/tui/screens/themes/dialogs.py +330 -0
- chrys/app/tui/screens/themes/editor.py +632 -0
- chrys/app/tui/screens/themes/editor.tcss +268 -0
- chrys/app/tui/screens/themes/messages.py +95 -0
- chrys/app/tui/screens/themes/palette.py +694 -0
- chrys/app/tui/screens/themes/panel.py +113 -0
- chrys/app/tui/screens/themes/picker.py +203 -0
- chrys/app/tui/screens/themes/save.py +191 -0
- chrys/app/tui/support/__init__.py +3 -0
- chrys/app/tui/support/file_manager.py +64 -0
- chrys/app/tui/support/gc_freeze.py +1114 -0
- chrys/app/tui/support/path_shortcuts.py +50 -0
- chrys/app/tui/support/prompt_history.py +298 -0
- chrys/app/tui/support/workspace_mru.py +476 -0
- chrys/app/tui/terminal/__init__.py +6 -0
- chrys/app/tui/terminal/_pty_backend.py +114 -0
- chrys/app/tui/terminal/emulator/__init__.py +24 -0
- chrys/app/tui/terminal/emulator/buffer.py +456 -0
- chrys/app/tui/terminal/emulator/charsets.py +113 -0
- chrys/app/tui/terminal/emulator/core.py +963 -0
- chrys/app/tui/terminal/emulator/input.py +342 -0
- chrys/app/tui/terminal/emulator/parser.py +227 -0
- chrys/app/tui/terminal/emulator/pen.py +158 -0
- chrys/app/tui/terminal/launcher.py +455 -0
- chrys/app/tui/terminal/leaked_reports.py +44 -0
- chrys/app/tui/terminal/panel.py +323 -0
- chrys/app/tui/terminal/pty_process.py +405 -0
- chrys/app/tui/terminal/rendering.py +151 -0
- chrys/app/tui/terminal/shell.py +273 -0
- chrys/app/tui/terminal/shell_integration.py +199 -0
- chrys/app/tui/terminal/title.py +224 -0
- chrys/app/tui/terminal/widget.py +654 -0
- chrys/app/tui/theme.py +422 -0
- chrys/app/tui/theme_loader.py +201 -0
- chrys/app/tui/theme_stylesheet.py +49 -0
- chrys/app/tui/themes/__init__.py +3 -0
- chrys/app/tui/themes/document.py +120 -0
- chrys/app/tui/themes/preview.py +170 -0
- chrys/app/tui/themes/store.py +176 -0
- chrys/app/tui/toasts.py +69 -0
- chrys/app/tui/util/__init__.py +3 -0
- chrys/app/tui/util/context_pressure.py +146 -0
- chrys/app/tui/util/debug_log.py +292 -0
- chrys/app/tui/util/diff_entries.py +64 -0
- chrys/app/tui/util/formatting.py +53 -0
- chrys/app/tui/util/git_branch.py +326 -0
- chrys/app/tui/util/invocation_progress.py +57 -0
- chrys/app/tui/util/logo.py +32 -0
- chrys/app/tui/util/message_gate.py +35 -0
- chrys/app/tui/util/mutation_sources.py +14 -0
- chrys/app/tui/util/picker_text.py +27 -0
- chrys/app/tui/util/removal.py +47 -0
- chrys/app/tui/util/rich_style.py +36 -0
- chrys/app/tui/util/source_text.py +12 -0
- chrys/app/tui/util/static_update.py +88 -0
- chrys/app/tui/util/todo_format.py +51 -0
- chrys/app/tui/util/visibility.py +214 -0
- chrys/app/tui/widgets/__init__.py +60 -0
- chrys/app/tui/widgets/ask_user_controls.py +652 -0
- chrys/app/tui/widgets/ask_user_markdown.py +17 -0
- chrys/app/tui/widgets/ask_user_prompt.py +606 -0
- chrys/app/tui/widgets/buttons.py +52 -0
- chrys/app/tui/widgets/chat/__init__.py +3 -0
- chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
- chrys/app/tui/widgets/chat/chrome.py +296 -0
- chrys/app/tui/widgets/chat/compaction_card.py +353 -0
- chrys/app/tui/widgets/chat/context_fold.py +77 -0
- chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
- chrys/app/tui/widgets/chat/image_preview.py +214 -0
- chrys/app/tui/widgets/chat/live_renderer.py +383 -0
- chrys/app/tui/widgets/chat/messages.py +1039 -0
- chrys/app/tui/widgets/chat/panel.py +1769 -0
- chrys/app/tui/widgets/chat/ports.py +245 -0
- chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
- chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
- chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
- chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
- chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
- chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
- chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
- chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
- chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
- chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
- chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
- chrys/app/tui/widgets/chat/renderers/search.py +348 -0
- chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
- chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
- chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
- chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
- chrys/app/tui/widgets/chat/renderers/web.py +69 -0
- chrys/app/tui/widgets/chat/replay.py +1605 -0
- chrys/app/tui/widgets/chat/replay_mount.py +322 -0
- chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
- chrys/app/tui/widgets/chat/selection_controller.py +435 -0
- chrys/app/tui/widgets/chat/session_json.py +508 -0
- chrys/app/tui/widgets/chat/toc_model.py +156 -0
- chrys/app/tui/widgets/chat/tool_call.py +2478 -0
- chrys/app/tui/widgets/chat/tool_registry.py +753 -0
- chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
- chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
- chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
- chrys/app/tui/widgets/checkbox.py +24 -0
- chrys/app/tui/widgets/chrome/__init__.py +3 -0
- chrys/app/tui/widgets/chrome/app_header.py +283 -0
- chrys/app/tui/widgets/chrome/commands.py +95 -0
- chrys/app/tui/widgets/chrome/file_index.py +68 -0
- chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
- chrys/app/tui/widgets/chrome/footer.py +284 -0
- chrys/app/tui/widgets/chrome/image_paste.py +351 -0
- chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
- chrys/app/tui/widgets/chrome/status_bar.py +911 -0
- chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
- chrys/app/tui/widgets/click_affordance.py +24 -0
- chrys/app/tui/widgets/color_picker/__init__.py +254 -0
- chrys/app/tui/widgets/color_picker/controls.py +171 -0
- chrys/app/tui/widgets/color_picker/model.py +89 -0
- chrys/app/tui/widgets/dialog_buttons.py +58 -0
- chrys/app/tui/widgets/dialog_scroll.py +24 -0
- chrys/app/tui/widgets/diff_view/__init__.py +20 -0
- chrys/app/tui/widgets/diff_view/cells.py +59 -0
- chrys/app/tui/widgets/diff_view/code.py +280 -0
- chrys/app/tui/widgets/diff_view/compute.py +286 -0
- chrys/app/tui/widgets/diff_view/gutter.py +161 -0
- chrys/app/tui/widgets/diff_view/inline.py +77 -0
- chrys/app/tui/widgets/diff_view/palette.py +87 -0
- chrys/app/tui/widgets/diff_view/protocols.py +24 -0
- chrys/app/tui/widgets/diff_view/rows.py +138 -0
- chrys/app/tui/widgets/diff_view/unified.py +267 -0
- chrys/app/tui/widgets/diff_view/widget.py +209 -0
- chrys/app/tui/widgets/editor/__init__.py +79 -0
- chrys/app/tui/widgets/editor/coordinates.py +30 -0
- chrys/app/tui/widgets/editor/highlighter.py +139 -0
- chrys/app/tui/widgets/editor/keymaps.py +357 -0
- chrys/app/tui/widgets/editor/types.py +216 -0
- chrys/app/tui/widgets/editor/vim.py +1195 -0
- chrys/app/tui/widgets/editor/widget.py +401 -0
- chrys/app/tui/widgets/file_preview.py +58 -0
- chrys/app/tui/widgets/hatch.py +138 -0
- chrys/app/tui/widgets/input.py +66 -0
- chrys/app/tui/widgets/loading.py +125 -0
- chrys/app/tui/widgets/markdown/__init__.py +25 -0
- chrys/app/tui/widgets/markdown/_utils.py +21 -0
- chrys/app/tui/widgets/markdown/anchors.py +71 -0
- chrys/app/tui/widgets/markdown/blocks.py +115 -0
- chrys/app/tui/widgets/markdown/copyable.py +163 -0
- chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
- chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
- chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
- chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
- chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
- chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
- chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
- chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
- chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
- chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
- chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
- chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
- chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
- chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
- chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
- chrys/app/tui/widgets/markdown/links.py +47 -0
- chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
- chrys/app/tui/widgets/markdown/math/layout.py +739 -0
- chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
- chrys/app/tui/widgets/markdown/math/parser.py +325 -0
- chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
- chrys/app/tui/widgets/markdown/navigator.py +80 -0
- chrys/app/tui/widgets/markdown/parser.py +1057 -0
- chrys/app/tui/widgets/markdown/stream.py +73 -0
- chrys/app/tui/widgets/markdown/toc.py +98 -0
- chrys/app/tui/widgets/markdown/viewer.py +146 -0
- chrys/app/tui/widgets/markdown/widget.py +2046 -0
- chrys/app/tui/widgets/marquee.py +101 -0
- chrys/app/tui/widgets/option_menu.py +82 -0
- chrys/app/tui/widgets/page_navigator.py +118 -0
- chrys/app/tui/widgets/select.py +196 -0
- chrys/app/tui/widgets/selection.py +59 -0
- chrys/app/tui/widgets/sidebar/__init__.py +3 -0
- chrys/app/tui/widgets/sidebar/buddy.py +367 -0
- chrys/app/tui/widgets/sidebar/context.py +482 -0
- chrys/app/tui/widgets/sidebar/debug.py +195 -0
- chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
- chrys/app/tui/widgets/sidebar/panel.py +198 -0
- chrys/app/tui/widgets/sidebar/tasks.py +198 -0
- chrys/app/tui/widgets/sidebar/toc.py +166 -0
- chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
- chrys/app/tui/widgets/syntax_theme.py +85 -0
- chrys/app/tui/widgets/text_area.py +258 -0
- chrys/app/tui/widgets/trajectory/__init__.py +8 -0
- chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
- chrys/app/tui/widgets/trajectory/insights.py +1266 -0
- chrys/app/tui/widgets/trajectory/overview.py +1101 -0
- chrys/app/tui/widgets/trajectory/panel.py +983 -0
- chrys/app/tui/widgets/trajectory/presentation.py +311 -0
- chrys/app/tui/widgets/trajectory/session_info.py +124 -0
- chrys/app/tui/widgets/trajectory/text_view.py +124 -0
- chrys/app/tui/widgets/trajectory/timeline.py +380 -0
- chrys/app/tui/widgets/welcome.py +126 -0
- chrys/app/tui/widgets/workflow/__init__.py +5 -0
- chrys/app/tui/widgets/workflow/edges.py +89 -0
- chrys/app/tui/widgets/workflow/graph.py +869 -0
- chrys/app/tui/widgets/workflow/info.py +310 -0
- chrys/app/tui/widgets/workflow/node_view.py +27 -0
- chrys/app/tui/widgets/workflow/output.py +225 -0
- chrys/app/tui/widgets/workflow/panel.py +658 -0
- chrys/app/tui/widgets/workflow/projector.py +324 -0
- chrys/app/tui/widgets/workflow/records.py +69 -0
- chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
- chrys/app/tui/widgets/workflow/selection.py +151 -0
- chrys/app/tui/widgets/workflow/source.py +65 -0
- chrys/app/tui/widgets/workflow/text.py +257 -0
- chrys/app/tui/widgets/workflow/values.py +317 -0
- chrys/foundation/__init__.py +0 -0
- chrys/foundation/branding.py +15 -0
- chrys/foundation/config/__init__.py +7 -0
- chrys/foundation/config/coercion.py +374 -0
- chrys/foundation/config/context.py +34 -0
- chrys/foundation/config/env_file.py +141 -0
- chrys/foundation/config/env_layers.py +370 -0
- chrys/foundation/config/migrations.py +521 -0
- chrys/foundation/config/notification_events.py +22 -0
- chrys/foundation/config/process_settings.py +332 -0
- chrys/foundation/config/runtime_pointer.py +133 -0
- chrys/foundation/config/settings.py +1952 -0
- chrys/foundation/config/settings_store.py +996 -0
- chrys/foundation/config/spec.py +396 -0
- chrys/foundation/config/user_settings.py +140 -0
- chrys/foundation/config/warnings.py +262 -0
- chrys/foundation/config/web_values.py +135 -0
- chrys/foundation/config/yaml_store.py +487 -0
- chrys/foundation/errors/__init__.py +68 -0
- chrys/foundation/errors/_legacy.py +117 -0
- chrys/foundation/errors/_text.py +13 -0
- chrys/foundation/errors/_walk.py +85 -0
- chrys/foundation/errors/classify.py +278 -0
- chrys/foundation/errors/display.py +274 -0
- chrys/foundation/errors/formatting.py +283 -0
- chrys/foundation/errors/kinds.py +67 -0
- chrys/foundation/errors/network.py +497 -0
- chrys/foundation/errors/provider.py +445 -0
- chrys/foundation/errors/route.py +85 -0
- chrys/foundation/events/__init__.py +8 -0
- chrys/foundation/events/bus.py +143 -0
- chrys/foundation/events/types.py +1629 -0
- chrys/foundation/events/workflow.py +38 -0
- chrys/foundation/hosted_tools.py +142 -0
- chrys/foundation/i18n/__init__.py +32 -0
- chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
- chrys/foundation/i18n/catalogs.py +42 -0
- chrys/foundation/i18n/formatting.py +202 -0
- chrys/foundation/i18n/locale.py +92 -0
- chrys/foundation/i18n/localizer.py +187 -0
- chrys/foundation/i18n/messages.py +268 -0
- chrys/foundation/io/__init__.py +3 -0
- chrys/foundation/io/result_content.py +237 -0
- chrys/foundation/models/__init__.py +3 -0
- chrys/foundation/models/approval_reuse.py +23 -0
- chrys/foundation/models/ask_user.py +468 -0
- chrys/foundation/models/execution.py +18 -0
- chrys/foundation/models/history_markers.py +115 -0
- chrys/foundation/models/invocations.py +63 -0
- chrys/foundation/models/mutation_scope.py +28 -0
- chrys/foundation/models/session_env.py +87 -0
- chrys/foundation/models/session_surface.py +29 -0
- chrys/foundation/models/todos.py +74 -0
- chrys/foundation/models/turns.py +125 -0
- chrys/foundation/models/workflow_session.py +89 -0
- chrys/foundation/models/workspace.py +59 -0
- chrys/foundation/net/__init__.py +3 -0
- chrys/foundation/net/address_class.py +57 -0
- chrys/foundation/net/route_probe.py +94 -0
- chrys/foundation/net/url.py +107 -0
- chrys/foundation/observability/__init__.py +3 -0
- chrys/foundation/observability/exporters.py +174 -0
- chrys/foundation/observability/gate.py +31 -0
- chrys/foundation/observability/setup.py +497 -0
- chrys/foundation/observability/sink.py +228 -0
- chrys/foundation/patches/__init__.py +19 -0
- chrys/foundation/patches/mdurl_cache.py +59 -0
- chrys/foundation/patches/patcher.py +287 -0
- chrys/foundation/patches/staged_members.py +205 -0
- chrys/foundation/patches/textual_block_border.py +105 -0
- chrys/foundation/patches/textual_button_ansi.py +65 -0
- chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
- chrys/foundation/patches/textual_callback_cache.py +248 -0
- chrys/foundation/patches/textual_callback_dispatch.py +109 -0
- chrys/foundation/patches/textual_compositor_cjk.py +566 -0
- chrys/foundation/patches/textual_dispatch_cache.py +150 -0
- chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
- chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
- chrys/foundation/patches/textual_lru_acyclic.py +241 -0
- chrys/foundation/patches/textual_message_pump.py +136 -0
- chrys/foundation/patches/textual_node_diet.py +875 -0
- chrys/foundation/patches/textual_one_shot_timer.py +48 -0
- chrys/foundation/patches/textual_option_list.py +90 -0
- chrys/foundation/patches/textual_precompose.py +89 -0
- chrys/foundation/patches/textual_pruned_tabs.py +69 -0
- chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
- chrys/foundation/patches/textual_removed_node_caches.py +86 -0
- chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
- chrys/foundation/patches/textual_selection_extract.py +48 -0
- chrys/foundation/patches/textual_strip_cycles.py +119 -0
- chrys/foundation/patches/textual_tab_selection.py +1611 -0
- chrys/foundation/patches/textual_timer_skip.py +70 -0
- chrys/foundation/patches/textual_utf8_decoder.py +106 -0
- chrys/foundation/patches/textual_win_sleep.py +167 -0
- chrys/foundation/patches/watchdog_fsevents.py +70 -0
- chrys/foundation/patches/watchdog_windows.py +52 -0
- chrys/foundation/platform/__init__.py +556 -0
- chrys/foundation/platform/c_api.py +32 -0
- chrys/foundation/platform/child_reap.py +107 -0
- chrys/foundation/platform/command_line.py +95 -0
- chrys/foundation/platform/files.py +1470 -0
- chrys/foundation/platform/output_capture.py +113 -0
- chrys/foundation/platform/paths.py +193 -0
- chrys/foundation/platform/process.py +1468 -0
- chrys/foundation/platform/pty_output.py +19 -0
- chrys/foundation/platform/runtime_paths.py +281 -0
- chrys/foundation/platform/win_console.py +129 -0
- chrys/foundation/reasoning_origin.py +67 -0
- chrys/foundation/recovery.py +16 -0
- chrys/foundation/retry.py +278 -0
- chrys/foundation/text/__init__.py +3 -0
- chrys/foundation/text/encoding.py +603 -0
- chrys/foundation/text/images.py +304 -0
- chrys/foundation/text/lines.py +24 -0
- chrys/foundation/text/mentions.py +90 -0
- chrys/foundation/text/model_json.py +25 -0
- chrys/foundation/text/reading_order.py +20 -0
- chrys/foundation/text/tokenizer.py +25 -0
- chrys/foundation/text/tool_output.py +287 -0
- chrys/foundation/text/yaml_io.py +62 -0
- chrys/foundation/tool_call_context.py +202 -0
- chrys/foundation/tool_execution_stamp.py +184 -0
- chrys/foundation/tool_invocation_order.py +48 -0
- chrys/foundation/tool_kinds.py +102 -0
- chrys/foundation/tool_result_metadata.py +287 -0
- chrys/foundation/trajectory/__init__.py +10 -0
- chrys/foundation/trajectory/context.py +481 -0
- chrys/foundation/trajectory/envelope.py +571 -0
- chrys/foundation/trajectory/event_types.py +294 -0
- chrys/foundation/trajectory/finalizers.py +46 -0
- chrys/foundation/trajectory/fingerprint.py +62 -0
- chrys/foundation/trajectory/ids.py +155 -0
- chrys/foundation/trajectory/keys.py +99 -0
- chrys/foundation/trajectory/lease.py +173 -0
- chrys/foundation/trajectory/metadata.py +72 -0
- chrys/foundation/trajectory/reader.py +271 -0
- chrys/foundation/trajectory/recovery.py +225 -0
- chrys/foundation/trajectory/revisions.py +152 -0
- chrys/foundation/trajectory/segments.py +225 -0
- chrys/foundation/trajectory/tools.py +117 -0
- chrys/foundation/trajectory/usage.py +151 -0
- chrys/foundation/trajectory/writer.py +972 -0
- chrys/foundation/trajectory_timing.py +97 -0
- chrys/foundation/util/__init__.py +3 -0
- chrys/foundation/util/async_tasks.py +29 -0
- chrys/foundation/util/chrys_headers.py +23 -0
- chrys/foundation/util/env_templates.py +104 -0
- chrys/foundation/util/filenames.py +96 -0
- chrys/foundation/util/header_charset.py +138 -0
- chrys/foundation/util/httpx_helpers.py +12 -0
- chrys/foundation/util/lock.py +236 -0
- chrys/foundation/util/once_close.py +46 -0
- chrys/foundation/util/session_ids.py +29 -0
- chrys/foundation/util/shell_tokens.py +88 -0
- chrys/foundation/util/sub_agent_context.py +38 -0
- chrys/foundation/util/time.py +35 -0
- chrys/foundation/util/unicode_scalars.py +36 -0
- chrys/foundation/vendor/__init__.py +43 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
- chrys/kernel/__init__.py +256 -0
- chrys/kernel/_content.py +1866 -0
- chrys/kernel/_loop_recorder.py +631 -0
- chrys/kernel/_null_overlay.py +1026 -0
- chrys/kernel/_result_ceiling.py +216 -0
- chrys/kernel/_serialization.py +431 -0
- chrys/kernel/_tool_arg_errors.py +1629 -0
- chrys/kernel/_tool_execution.py +1172 -0
- chrys/kernel/_tool_expansion.py +26 -0
- chrys/kernel/_types.py +2598 -0
- chrys/kernel/agent.py +994 -0
- chrys/kernel/client.py +1084 -0
- chrys/kernel/compaction.py +1315 -0
- chrys/kernel/exceptions.py +211 -0
- chrys/kernel/exchanges.py +683 -0
- chrys/kernel/identity.py +95 -0
- chrys/kernel/images.py +36 -0
- chrys/kernel/instrumentation.py +1618 -0
- chrys/kernel/loop.py +2308 -0
- chrys/kernel/middleware.py +616 -0
- chrys/kernel/sessions.py +604 -0
- chrys/kernel/tools.py +1061 -0
- chrys/kernel/types.py +126 -0
- chrys/orchestration/__init__.py +3 -0
- chrys/orchestration/engine/__init__.py +3 -0
- chrys/orchestration/engine/assembly.py +236 -0
- chrys/orchestration/engine/build/__init__.py +3 -0
- chrys/orchestration/engine/build/builder.py +1061 -0
- chrys/orchestration/engine/build/construction.py +408 -0
- chrys/orchestration/engine/build/loaded.py +139 -0
- chrys/orchestration/engine/engine.py +789 -0
- chrys/orchestration/engine/execution.py +771 -0
- chrys/orchestration/engine/loader.py +865 -0
- chrys/orchestration/engine/rollback.py +918 -0
- chrys/orchestration/engine/run/__init__.py +3 -0
- chrys/orchestration/engine/run/active_injection.py +419 -0
- chrys/orchestration/engine/run/attachments.py +519 -0
- chrys/orchestration/engine/run/bindings.py +1095 -0
- chrys/orchestration/engine/run/coordinator.py +1062 -0
- chrys/orchestration/engine/run/finalizer.py +417 -0
- chrys/orchestration/engine/run/input_refs.py +60 -0
- chrys/orchestration/engine/run/prompt_content.py +457 -0
- chrys/orchestration/engine/run/resume.py +299 -0
- chrys/orchestration/engine/run/retry.py +948 -0
- chrys/orchestration/engine/run/runner.py +625 -0
- chrys/orchestration/engine/run/runtime_skills.py +146 -0
- chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
- chrys/orchestration/engine/run/turn_hooks.py +258 -0
- chrys/orchestration/engine/run/turn_state.py +149 -0
- chrys/orchestration/engine/run/working_dir.py +50 -0
- chrys/orchestration/engine/session_lifecycle.py +2013 -0
- chrys/orchestration/engine/state/__init__.py +3 -0
- chrys/orchestration/engine/state/active_session.py +183 -0
- chrys/orchestration/engine/state/controls.py +639 -0
- chrys/orchestration/engine/state/current_agent.py +26 -0
- chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
- chrys/orchestration/engine/state/machine.py +193 -0
- chrys/orchestration/engine/state/session_writer.py +434 -0
- chrys/orchestration/engine/trajectory.py +714 -0
- chrys/orchestration/engine/usage.py +118 -0
- chrys/orchestration/invoker/__init__.py +3 -0
- chrys/orchestration/invoker/acp.py +727 -0
- chrys/orchestration/invoker/acp_protocol.py +1115 -0
- chrys/orchestration/invoker/acp_spec.py +79 -0
- chrys/orchestration/invoker/attempts.py +994 -0
- chrys/orchestration/invoker/child_compaction.py +131 -0
- chrys/orchestration/invoker/child_history.py +99 -0
- chrys/orchestration/invoker/contracts.py +253 -0
- chrys/orchestration/invoker/evidence.py +94 -0
- chrys/orchestration/invoker/kernel.py +343 -0
- chrys/orchestration/invoker/origin.py +79 -0
- chrys/orchestration/invoker/resources.py +343 -0
- chrys/orchestration/invoker/runtime.py +299 -0
- chrys/orchestration/invoker/usage.py +80 -0
- chrys/orchestration/session_hooks.py +117 -0
- chrys/orchestration/session_host.py +1023 -0
- chrys/orchestration/session_resources.py +95 -0
- chrys/orchestration/session_usage.py +219 -0
- chrys/orchestration/startup.py +255 -0
- chrys/orchestration/sub_agents/__init__.py +3 -0
- chrys/orchestration/sub_agents/acp_policy.py +269 -0
- chrys/orchestration/sub_agents/kernel_policy.py +848 -0
- chrys/orchestration/sub_agents/registration.py +44 -0
- chrys/orchestration/sub_agents/shell.py +337 -0
- chrys/orchestration/sub_agents/tools.py +2228 -0
- chrys/orchestration/workflows/__init__.py +3 -0
- chrys/orchestration/workflows/agent_archive.py +156 -0
- chrys/orchestration/workflows/agent_node.py +894 -0
- chrys/orchestration/workflows/agent_node_build.py +687 -0
- chrys/orchestration/workflows/catalog.py +332 -0
- chrys/orchestration/workflows/coordinator.py +771 -0
- chrys/orchestration/workflows/hooks.py +176 -0
- chrys/orchestration/workflows/preview.py +322 -0
- chrys/orchestration/workflows/rollback.py +151 -0
- chrys/orchestration/workflows/runner.py +986 -0
- chrys/orchestration/workflows/session.py +390 -0
- chrys/orchestration/workflows/settings.py +97 -0
- chrys/orchestration/workflows/validation.py +674 -0
- chrys/orchestration/workflows/worker_client.py +1002 -0
- chrys/service/__init__.py +3 -0
- chrys/service/acp_client/__init__.py +65 -0
- chrys/service/acp_client/_transport.py +922 -0
- chrys/service/acp_client/client.py +1311 -0
- chrys/service/acp_client/errors.py +224 -0
- chrys/service/acp_client/protocol.py +220 -0
- chrys/service/acp_client/spawn.py +239 -0
- chrys/service/acp_client/spec.py +100 -0
- chrys/service/agent_middleware/__init__.py +60 -0
- chrys/service/agent_middleware/_metadata_keys.py +52 -0
- chrys/service/agent_middleware/control/__init__.py +3 -0
- chrys/service/agent_middleware/control/approval.py +769 -0
- chrys/service/agent_middleware/control/approval_hooks.py +62 -0
- chrys/service/agent_middleware/control/ask_user.py +207 -0
- chrys/service/agent_middleware/control/interrupt.py +50 -0
- chrys/service/agent_middleware/control/sleep.py +118 -0
- chrys/service/agent_middleware/control/todo.py +117 -0
- chrys/service/agent_middleware/events/__init__.py +3 -0
- chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
- chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
- chrys/service/agent_middleware/events/intermediate_text.py +128 -0
- chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
- chrys/service/agent_middleware/events/result_persistence.py +98 -0
- chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
- chrys/service/agent_middleware/events/tool_error_text.py +22 -0
- chrys/service/agent_middleware/events/tool_events.py +779 -0
- chrys/service/agent_middleware/injection.py +510 -0
- chrys/service/agent_middleware/reminders/__init__.py +91 -0
- chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
- chrys/service/agent_middleware/reminders/base.py +25 -0
- chrys/service/agent_middleware/reminders/context_usage.py +74 -0
- chrys/service/agent_middleware/reminders/file_change.py +63 -0
- chrys/service/agent_middleware/reminders/mcp.py +39 -0
- chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
- chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
- chrys/service/agent_middleware/reminders/skills.py +44 -0
- chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
- chrys/service/agent_middleware/reminders/todo.py +35 -0
- chrys/service/agent_middleware/reminders/turn_line.py +48 -0
- chrys/service/agent_middleware/response_validation.py +1380 -0
- chrys/service/agent_middleware/system_reminder.py +1107 -0
- chrys/service/agent_middleware/validators.py +396 -0
- chrys/service/analytics/__init__.py +124 -0
- chrys/service/analytics/_actions.py +896 -0
- chrys/service/analytics/_context_evidence.py +357 -0
- chrys/service/analytics/_critical_path.py +179 -0
- chrys/service/analytics/_facts.py +919 -0
- chrys/service/analytics/_insights.py +769 -0
- chrys/service/analytics/_metric_ops.py +83 -0
- chrys/service/analytics/_session_projection.py +460 -0
- chrys/service/analytics/_timeline.py +434 -0
- chrys/service/analytics/_turn_graph.py +1237 -0
- chrys/service/analytics/_turns.py +1377 -0
- chrys/service/analytics/_workflows.py +171 -0
- chrys/service/analytics/aggregation.py +1474 -0
- chrys/service/analytics/classification.py +128 -0
- chrys/service/analytics/export.py +803 -0
- chrys/service/analytics/findings.py +282 -0
- chrys/service/analytics/math.py +79 -0
- chrys/service/analytics/model.py +886 -0
- chrys/service/analytics/reader.py +321 -0
- chrys/service/approval/__init__.py +3 -0
- chrys/service/approval/arbitration.py +102 -0
- chrys/service/approval/command_identity.py +109 -0
- chrys/service/approval/correlation.py +57 -0
- chrys/service/approval/grant_store.py +108 -0
- chrys/service/approval/judge.py +589 -0
- chrys/service/approval/level.py +15 -0
- chrys/service/approval/policy.py +209 -0
- chrys/service/approval/reuse.py +223 -0
- chrys/service/approval/reuse_binding.py +133 -0
- chrys/service/approval/safety_classifier.py +536 -0
- chrys/service/approval/turn_context.py +49 -0
- chrys/service/context/__init__.py +3 -0
- chrys/service/context/compaction/__init__.py +38 -0
- chrys/service/context/compaction/breaker.py +156 -0
- chrys/service/context/compaction/budgets.py +93 -0
- chrys/service/context/compaction/calibration.py +77 -0
- chrys/service/context/compaction/compression.py +690 -0
- chrys/service/context/compaction/current_turn_drop.py +458 -0
- chrys/service/context/compaction/events.py +201 -0
- chrys/service/context/compaction/exclusions.py +261 -0
- chrys/service/context/compaction/groups.py +361 -0
- chrys/service/context/compaction/last_words.py +1996 -0
- chrys/service/context/compaction/last_words_state.py +752 -0
- chrys/service/context/compaction/scoped.py +472 -0
- chrys/service/context/compaction/spill.py +1947 -0
- chrys/service/context/compaction/strategy.py +1116 -0
- chrys/service/context/compaction/summaries.py +319 -0
- chrys/service/context/compaction/turns.py +375 -0
- chrys/service/context/manager.py +174 -0
- chrys/service/context/memory_loader.py +792 -0
- chrys/service/context/middleware/__init__.py +3 -0
- chrys/service/context/middleware/usage.py +332 -0
- chrys/service/context/providers/__init__.py +3 -0
- chrys/service/context/providers/context_management.py +432 -0
- chrys/service/context/providers/history.py +647 -0
- chrys/service/context/providers/memory.py +50 -0
- chrys/service/context/tool_content.py +22 -0
- chrys/service/hooks/__init__.py +37 -0
- chrys/service/hooks/detached_worker.py +118 -0
- chrys/service/hooks/docs/README.md +14 -0
- chrys/service/hooks/docs/authoring.md +684 -0
- chrys/service/hooks/docs/configuration.md +499 -0
- chrys/service/hooks/docs/design.md +359 -0
- chrys/service/hooks/docs/project-hooks.md +348 -0
- chrys/service/hooks/events.py +157 -0
- chrys/service/hooks/loader.py +665 -0
- chrys/service/hooks/manager.py +807 -0
- chrys/service/hooks/matcher.py +111 -0
- chrys/service/hooks/outbox.py +294 -0
- chrys/service/hooks/runner.py +572 -0
- chrys/service/hooks/schema.py +407 -0
- chrys/service/llm/__init__.py +7 -0
- chrys/service/llm/_structured_outputs.py +192 -0
- chrys/service/llm/anthropic_messages/__init__.py +9 -0
- chrys/service/llm/anthropic_messages/client.py +254 -0
- chrys/service/llm/anthropic_messages/decode.py +331 -0
- chrys/service/llm/anthropic_messages/history.py +279 -0
- chrys/service/llm/anthropic_messages/request.py +323 -0
- chrys/service/llm/anthropic_messages/server_tools.py +319 -0
- chrys/service/llm/anthropic_messages/stream.py +297 -0
- chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
- chrys/service/llm/chat_completions/__init__.py +9 -0
- chrys/service/llm/chat_completions/client.py +369 -0
- chrys/service/llm/chat_completions/decode.py +338 -0
- chrys/service/llm/chat_completions/history.py +657 -0
- chrys/service/llm/chat_completions/reasoning.py +244 -0
- chrys/service/llm/chat_completions/request.py +192 -0
- chrys/service/llm/chat_completions/stream.py +546 -0
- chrys/service/llm/chat_completions/validation.py +308 -0
- chrys/service/llm/clients.py +613 -0
- chrys/service/llm/images.py +65 -0
- chrys/service/llm/mock.py +413 -0
- chrys/service/llm/observer.py +446 -0
- chrys/service/llm/one_shot.py +106 -0
- chrys/service/llm/openai_exceptions.py +93 -0
- chrys/service/llm/openai_responses/__init__.py +10 -0
- chrys/service/llm/openai_responses/client.py +323 -0
- chrys/service/llm/openai_responses/decode.py +432 -0
- chrys/service/llm/openai_responses/history.py +519 -0
- chrys/service/llm/openai_responses/hosted.py +625 -0
- chrys/service/llm/openai_responses/replay.py +629 -0
- chrys/service/llm/openai_responses/request.py +342 -0
- chrys/service/llm/openai_responses/stream.py +847 -0
- chrys/service/llm/openai_timestamps.py +57 -0
- chrys/service/llm/providers.py +107 -0
- chrys/service/llm/proxy_route.py +54 -0
- chrys/service/llm/raw_http_log.py +309 -0
- chrys/service/llm/route_facts.py +63 -0
- chrys/service/llm/route_sessions.py +49 -0
- chrys/service/llm/sdk_anthropic.py +18 -0
- chrys/service/llm/sdk_headers.py +47 -0
- chrys/service/llm/sdk_openai.py +18 -0
- chrys/service/llm/sdk_retry.py +59 -0
- chrys/service/llm/wire_client.py +246 -0
- chrys/service/mcp/__init__.py +3 -0
- chrys/service/mcp/_connection.py +235 -0
- chrys/service/mcp/_http_transport.py +350 -0
- chrys/service/mcp/_progressive.py +459 -0
- chrys/service/mcp/_stdio_transport.py +614 -0
- chrys/service/mcp/_tool_mixins.py +96 -0
- chrys/service/mcp/adapter.py +1031 -0
- chrys/service/mcp/cache.py +476 -0
- chrys/service/mcp/content.py +32 -0
- chrys/service/mcp/errors.py +169 -0
- chrys/service/mcp/owned.py +1330 -0
- chrys/service/mcp/result_limits.py +168 -0
- chrys/service/mcp/thinking_warning.py +95 -0
- chrys/service/mcp/validation.py +74 -0
- chrys/service/mutations/__init__.py +84 -0
- chrys/service/mutations/coordination.py +894 -0
- chrys/service/mutations/detector.py +458 -0
- chrys/service/mutations/git_calibrator.py +397 -0
- chrys/service/mutations/git_state.py +647 -0
- chrys/service/mutations/pipeline.py +779 -0
- chrys/service/mutations/scanner.py +466 -0
- chrys/service/mutations/snapshot_files.py +233 -0
- chrys/service/mutations/store.py +625 -0
- chrys/service/mutations/tool_names.py +15 -0
- chrys/service/mutations/trace.py +246 -0
- chrys/service/mutations/tracker.py +1289 -0
- chrys/service/mutations/types.py +559 -0
- chrys/service/mutations/workspace_changes.py +1583 -0
- chrys/service/profiles/__init__.py +2 -0
- chrys/service/profiles/agents/__init__.py +3 -0
- chrys/service/profiles/agents/builtins/Code.yaml +199 -0
- chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
- chrys/service/profiles/agents/builtins/General.yaml +114 -0
- chrys/service/profiles/agents/builtins/QA.yaml +128 -0
- chrys/service/profiles/agents/loader.py +808 -0
- chrys/service/profiles/agents/registry.py +727 -0
- chrys/service/profiles/agents/schema.py +371 -0
- chrys/service/profiles/agents/serializer.py +196 -0
- chrys/service/profiles/coercion.py +22 -0
- chrys/service/profiles/models/__init__.py +3 -0
- chrys/service/profiles/models/env_bridge.py +157 -0
- chrys/service/profiles/models/loader.py +282 -0
- chrys/service/profiles/models/options.py +438 -0
- chrys/service/profiles/models/registry.py +104 -0
- chrys/service/profiles/models/resolver.py +286 -0
- chrys/service/profiles/models/schema.py +91 -0
- chrys/service/profiles/models/serializer.py +76 -0
- chrys/service/session/__init__.py +3 -0
- chrys/service/session/checkpoint.py +156 -0
- chrys/service/session/history.py +1616 -0
- chrys/service/session/message_metadata.py +151 -0
- chrys/service/session/persistence.py +435 -0
- chrys/service/session/runtime_metadata.py +210 -0
- chrys/service/session/sub_agent_logs.py +1069 -0
- chrys/service/session/sub_agent_transcript.py +654 -0
- chrys/service/skills/__init__.py +3 -0
- chrys/service/skills/adapter.py +408 -0
- chrys/service/skills/constants.py +8 -0
- chrys/service/skills/loader.py +575 -0
- chrys/service/skills/model.py +130 -0
- chrys/service/skills/provider.py +841 -0
- chrys/service/skills/runner.py +390 -0
- chrys/service/state/__init__.py +3 -0
- chrys/service/state/_fork.py +710 -0
- chrys/service/state/_session_files.py +175 -0
- chrys/service/state/_session_meta.py +552 -0
- chrys/service/state/locks.py +154 -0
- chrys/service/state/serializers.py +156 -0
- chrys/service/state/session_catalog.py +361 -0
- chrys/service/state/session_listing.py +84 -0
- chrys/service/state/session_migration.py +446 -0
- chrys/service/state/session_mru.py +315 -0
- chrys/service/state/store.py +2132 -0
- chrys/service/state/workflow.py +158 -0
- chrys/service/text_blocks.py +47 -0
- chrys/service/todos/__init__.py +3 -0
- chrys/service/todos/reminder.py +31 -0
- chrys/service/todos/tracker.py +58 -0
- chrys/service/tools/__init__.py +3 -0
- chrys/service/tools/approval_targets.py +92 -0
- chrys/service/tools/builtins/__init__.py +3 -0
- chrys/service/tools/builtins/ask_user.py +326 -0
- chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
- chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
- chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
- chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
- chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
- chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
- chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
- chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
- chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
- chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
- chrys/service/tools/builtins/doc_converter/registry.py +55 -0
- chrys/service/tools/builtins/filesystem.py +1071 -0
- chrys/service/tools/builtins/search.py +1265 -0
- chrys/service/tools/builtins/shell.py +967 -0
- chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
- chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
- chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
- chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
- chrys/service/tools/builtins/shell_filter.py +24 -0
- chrys/service/tools/builtins/sleep.py +91 -0
- chrys/service/tools/builtins/todo.py +64 -0
- chrys/service/tools/builtins/web/__init__.py +6 -0
- chrys/service/tools/builtins/web/build.py +268 -0
- chrys/service/tools/builtins/web/config.py +338 -0
- chrys/service/tools/builtins/web/credentials.py +37 -0
- chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
- chrys/service/tools/builtins/web/fetch/cache.py +68 -0
- chrys/service/tools/builtins/web/fetch/convert.py +56 -0
- chrys/service/tools/builtins/web/fetch/tool.py +223 -0
- chrys/service/tools/builtins/web/hosted.py +61 -0
- chrys/service/tools/builtins/web/http.py +330 -0
- chrys/service/tools/builtins/web/progress.py +18 -0
- chrys/service/tools/builtins/web/search/__init__.py +3 -0
- chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
- chrys/service/tools/builtins/web/search/html.py +152 -0
- chrys/service/tools/builtins/web/search/json_api.py +227 -0
- chrys/service/tools/builtins/web/search/providers.py +77 -0
- chrys/service/tools/builtins/web/search/tool.py +218 -0
- chrys/service/tools/builtins/web/search/types.py +28 -0
- chrys/service/tools/builtins/web/tls_identity.py +66 -0
- chrys/service/tools/kinds.py +115 -0
- chrys/service/tools/names.py +47 -0
- chrys/service/tools/registry.py +242 -0
- chrys/service/tools/result_metadata.py +103 -0
- chrys/service/tools/session_artifacts.py +196 -0
- chrys/service/tools/spill.py +218 -0
- chrys/service/tools/workspace_paths.py +81 -0
- chrys/service/trajectory/__init__.py +11 -0
- chrys/service/trajectory/approvals.py +201 -0
- chrys/service/trajectory/compaction.py +420 -0
- chrys/service/trajectory/fork.py +75 -0
- chrys/service/trajectory/hooks.py +217 -0
- chrys/service/trajectory/items.py +41 -0
- chrys/service/trajectory/preparation.py +374 -0
- chrys/service/trajectory/retries.py +95 -0
- chrys/service/trajectory/revisions.py +129 -0
- chrys/service/trajectory/segmented.py +163 -0
- chrys/service/trajectory/session.py +762 -0
- chrys/service/trajectory/state.py +83 -0
- chrys/service/trajectory/sub_agents.py +169 -0
- chrys/service/trajectory/tombstone.py +262 -0
- chrys/service/trajectory/tools.py +323 -0
- chrys/service/trajectory/validation.py +130 -0
- chrys/service/trajectory/waits.py +161 -0
- chrys/service/trajectory/workflow.py +148 -0
- chrys/service/vision.py +128 -0
- chrys/service/workflows/__init__.py +3 -0
- chrys/service/workflows/admission.py +151 -0
- chrys/service/workflows/artifacts.py +109 -0
- chrys/service/workflows/asks.py +130 -0
- chrys/service/workflows/builtins/__init__.py +12 -0
- chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
- chrys/service/workflows/builtins/demo-workflow.py +756 -0
- chrys/service/workflows/discovery.py +554 -0
- chrys/service/workflows/environment.py +284 -0
- chrys/service/workflows/graph.py +430 -0
- chrys/service/workflows/history.py +82 -0
- chrys/service/workflows/interpreter.py +107 -0
- chrys/service/workflows/journal.py +334 -0
- chrys/service/workflows/layout.py +56 -0
- chrys/service/workflows/ledger.py +149 -0
- chrys/service/workflows/model_selection.py +18 -0
- chrys/service/workflows/orphans.py +209 -0
- chrys/service/workflows/outcomes.py +46 -0
- chrys/service/workflows/protocol.py +210 -0
- chrys/service/workflows/records.py +142 -0
- chrys/service/workflows/scheduler.py +770 -0
- chrys/service/workflows/sdk/__init__.py +51 -0
- chrys/service/workflows/sdk/_ask.py +167 -0
- chrys/service/workflows/sdk/_builder.py +781 -0
- chrys/service/workflows/sdk/_values.py +92 -0
- chrys/service/workflows/sdk_artifact.py +74 -0
- chrys/service/workflows/store.py +494 -0
- chrys/service/workflows/transcript.py +102 -0
- chrys/service/workflows/values.py +73 -0
- chrys/service/workflows/worker_host.py +1820 -0
- chrys/workflows.py +38 -0
- icode_tui-0.29.1.dist-info/METADATA +104 -0
- icode_tui-0.29.1.dist-info/RECORD +1138 -0
- icode_tui-0.29.1.dist-info/WHEEL +4 -0
- icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
- icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
- icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
chrys/kernel/loop.py
ADDED
|
@@ -0,0 +1,2308 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
3
|
+
# Contains code adapted from Microsoft Agent Framework (MIT License; see NOTICE).
|
|
4
|
+
|
|
5
|
+
"""Model/tool loop over a chat middleware layer and a provider wire client.
|
|
6
|
+
|
|
7
|
+
``ToolLoopLayer`` owns per-run middleware pipelines, dispatch, result assembly,
|
|
8
|
+
continuation state and interrupt-recovery recording. Each ``get_response``
|
|
9
|
+
builds one ``_LoopRun`` (the blocking and streaming drivers) over a
|
|
10
|
+
``_WireCaller`` (logical model calls and their wire retry lanes) and a
|
|
11
|
+
``_LoopTrajectory`` (cycle, exchange and retry events, and the landed tool
|
|
12
|
+
operations not yet dispatched). Tool calls execute in ``_tool_execution``;
|
|
13
|
+
the interrupt-recovery journal is ``_loop_recorder``.
|
|
14
|
+
|
|
15
|
+
Tool containers become fresh run-local lists, since direct callers and
|
|
16
|
+
progressive exposure can add tools after agent preparation. Inner stream
|
|
17
|
+
result hooks run once; final assembly must not replay them.
|
|
18
|
+
|
|
19
|
+
Kernel dependencies stay within this package, allowed third-party packages
|
|
20
|
+
and foundation. Intra-package imports are relative.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import functools
|
|
27
|
+
import inspect
|
|
28
|
+
import logging
|
|
29
|
+
from collections.abc import Mapping, Sequence
|
|
30
|
+
from copy import copy
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from enum import Enum
|
|
33
|
+
from time import monotonic_ns
|
|
34
|
+
from typing import TYPE_CHECKING, Any, Protocol, cast
|
|
35
|
+
|
|
36
|
+
from chrys.foundation.errors import (
|
|
37
|
+
clean_error_message,
|
|
38
|
+
invalidates_continuation_token,
|
|
39
|
+
is_context_overflow,
|
|
40
|
+
is_thinking_binding_rejection,
|
|
41
|
+
)
|
|
42
|
+
from chrys.foundation.retry import StreamStall
|
|
43
|
+
from chrys.foundation.tool_invocation_order import TOOL_INVOCATION_ORDER_KEY
|
|
44
|
+
from chrys.foundation.trajectory.context import (
|
|
45
|
+
TRAJECTORY_CONTEXT_KWARG,
|
|
46
|
+
TRAJECTORY_EXCHANGE_KWARG,
|
|
47
|
+
ExchangeTrace,
|
|
48
|
+
TrajectoryContext,
|
|
49
|
+
current_trajectory,
|
|
50
|
+
trajectory_scope,
|
|
51
|
+
)
|
|
52
|
+
from chrys.foundation.trajectory.envelope import MeasurementSource, measurement
|
|
53
|
+
from chrys.foundation.trajectory.event_types import (
|
|
54
|
+
EventType as TrajectoryEventType,
|
|
55
|
+
)
|
|
56
|
+
from chrys.foundation.trajectory.event_types import (
|
|
57
|
+
ExchangeOutcome,
|
|
58
|
+
RetryMode,
|
|
59
|
+
RetryReason,
|
|
60
|
+
ToolOutcome,
|
|
61
|
+
)
|
|
62
|
+
from chrys.foundation.trajectory.ids import new_analytics_id
|
|
63
|
+
from chrys.foundation.trajectory.metadata import (
|
|
64
|
+
ANALYTICS_ITEM_ID_KEY,
|
|
65
|
+
OPERATION_ID_KEY,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
from ._content import Content, add_usage_details, normalize_stream_usage
|
|
69
|
+
from ._loop_recorder import LoopRecorder, _message_snapshot
|
|
70
|
+
from ._tool_execution import (
|
|
71
|
+
_execute_function_calls,
|
|
72
|
+
_is_actionable_function_call,
|
|
73
|
+
_record_unexecuted_tool_operation,
|
|
74
|
+
)
|
|
75
|
+
from ._types import (
|
|
76
|
+
ChatResponse,
|
|
77
|
+
ChatResponseUpdate,
|
|
78
|
+
Message,
|
|
79
|
+
ResponseStream,
|
|
80
|
+
)
|
|
81
|
+
from .client import _wire_message_view, resolve_storage_mode_and_handles, start_with_wire_progress
|
|
82
|
+
from .compaction import ContextOverflowSink
|
|
83
|
+
from .exchanges import TOOL_CALL_CONTENT_TYPES
|
|
84
|
+
from .identity import WeakIdentityRegistry
|
|
85
|
+
from .middleware import FunctionMiddlewarePipeline, _as_middleware_list, split_middleware
|
|
86
|
+
from .sessions import AgentSession, is_local_history_conversation_id
|
|
87
|
+
from .tools import (
|
|
88
|
+
normalize_tools,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if TYPE_CHECKING:
|
|
92
|
+
from collections.abc import AsyncIterable, Awaitable, Callable
|
|
93
|
+
|
|
94
|
+
from chrys.foundation.trajectory.envelope import EventDraft
|
|
95
|
+
|
|
96
|
+
from ._content import UsageDetails
|
|
97
|
+
from .middleware import ChatMiddleware, ChatMiddlewareLayer, FunctionMiddleware
|
|
98
|
+
|
|
99
|
+
logger = logging.getLogger(__name__)
|
|
100
|
+
|
|
101
|
+
# Default bounds on model/tool iterations and consecutive failed tool batches.
|
|
102
|
+
DEFAULT_MAX_ITERATIONS = 40
|
|
103
|
+
DEFAULT_MAX_CONSECUTIVE_ERRORS = 3
|
|
104
|
+
|
|
105
|
+
# Pause between continuation polls of a still-running background response.
|
|
106
|
+
# Retrieval returns immediately while the response is queued/in_progress, so
|
|
107
|
+
# an unpaced loop would hammer the provider straight into rate limits.
|
|
108
|
+
CONTINUATION_POLL_INTERVAL_SECONDS: float = 2.0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class StallExhaustedAction(Enum):
|
|
112
|
+
"""Action taken when the stream-idle retry budget is exhausted."""
|
|
113
|
+
|
|
114
|
+
BLOCKING_FALLBACK = "blocking_fallback"
|
|
115
|
+
RAISE = "raise"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class WireRetryPolicy(Protocol):
|
|
119
|
+
"""Per-run policy for retrying one logical provider call."""
|
|
120
|
+
|
|
121
|
+
max_retries: int
|
|
122
|
+
stall_timeout_seconds: float | None
|
|
123
|
+
stall_max_retries: int
|
|
124
|
+
stall_exhausted_action: StallExhaustedAction
|
|
125
|
+
# Probe for the hosted tool calls a replay must not re-run (side-effectful
|
|
126
|
+
# or of unknown safety) that the current wire attempt already ran
|
|
127
|
+
# server-side; None when the caller has no such probe.
|
|
128
|
+
hosted_commits_in_flight: Callable[[], tuple[str, ...]] | None
|
|
129
|
+
|
|
130
|
+
def backoff_seconds(self, attempt: int) -> int: ...
|
|
131
|
+
|
|
132
|
+
def is_retryable(self, exc: BaseException) -> bool: ...
|
|
133
|
+
|
|
134
|
+
def is_interrupted(self) -> bool: ...
|
|
135
|
+
|
|
136
|
+
async def sleep(self, seconds: int) -> bool: ...
|
|
137
|
+
|
|
138
|
+
async def on_retry(
|
|
139
|
+
self,
|
|
140
|
+
message: str,
|
|
141
|
+
attempt: int,
|
|
142
|
+
max_attempts: int,
|
|
143
|
+
delay_seconds: int,
|
|
144
|
+
exc: BaseException,
|
|
145
|
+
) -> None: ...
|
|
146
|
+
|
|
147
|
+
def before_retry(self) -> None: ...
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True, slots=True)
|
|
151
|
+
class ConsumedInjectionMessageProbe:
|
|
152
|
+
"""Downward-safe seam for clean injections consumed on a wire call.
|
|
153
|
+
|
|
154
|
+
Chat middleware may append transient reminders after injection. The
|
|
155
|
+
service-owned probe exposes only the clean user messages so a later tool
|
|
156
|
+
iteration can retain the user's instruction without persisting wire-only
|
|
157
|
+
enrichment.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
drain_consumed_injection_messages: Callable[[], list[Message]]
|
|
161
|
+
commit_consumed_injections: Callable[[], None] | None = None
|
|
162
|
+
|
|
163
|
+
def take_consumed_messages(self) -> list[Message]:
|
|
164
|
+
"""Take clean user messages consumed by the latest wire call."""
|
|
165
|
+
return self.drain_consumed_injection_messages()
|
|
166
|
+
|
|
167
|
+
def commit(self) -> None:
|
|
168
|
+
"""Commit the latest logical call's injection transaction."""
|
|
169
|
+
if self.commit_consumed_injections is not None:
|
|
170
|
+
self.commit_consumed_injections()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
_MAX_ITERATIONS_FALLBACK_TEXT = "Maximum iterations reached before a final answer could be produced."
|
|
174
|
+
_CONSECUTIVE_ERRORS_FALLBACK_TEXT = (
|
|
175
|
+
"Tool calls kept failing, so they were stopped before a final answer could be produced."
|
|
176
|
+
)
|
|
177
|
+
_MAX_FUNCTION_CALLS_FALLBACK_TEXT = "Maximum function calls reached before a final answer could be produced."
|
|
178
|
+
|
|
179
|
+
_USER_VISIBLE_CONTENT_TYPES = frozenset({"data", "uri", "error", "hosted_file", "hosted_vector_store"})
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _strip_unexecutable_calls_from_response(response: ChatResponse) -> bool:
|
|
183
|
+
"""Drop actionable function calls from an exhaustion-tail final response.
|
|
184
|
+
|
|
185
|
+
The tail's request carries ``tool_choice="none"``; a provider that ignores
|
|
186
|
+
it returns calls the loop will never execute, and a dangling call recorded
|
|
187
|
+
on this success path has no later repair. Messages the strip empties are
|
|
188
|
+
dropped, and a ``"tool_calls"`` finish reason is normalized to ``"stop"``
|
|
189
|
+
— the stripped response holds no remaining tool work, so advertising it
|
|
190
|
+
would route a plain-text final as unfinished. Returns whether anything
|
|
191
|
+
was removed.
|
|
192
|
+
"""
|
|
193
|
+
removed = False
|
|
194
|
+
kept_messages: list[Message] = []
|
|
195
|
+
for message in response.messages:
|
|
196
|
+
if any(_is_actionable_function_call(content) for content in message.contents):
|
|
197
|
+
removed = True
|
|
198
|
+
remaining = [content for content in message.contents if not _is_actionable_function_call(content)]
|
|
199
|
+
if not remaining:
|
|
200
|
+
continue
|
|
201
|
+
message.contents = remaining
|
|
202
|
+
kept_messages.append(message)
|
|
203
|
+
if removed:
|
|
204
|
+
response.messages = kept_messages
|
|
205
|
+
if response.finish_reason == "tool_calls":
|
|
206
|
+
response.finish_reason = "stop"
|
|
207
|
+
return removed
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _strip_unexecutable_calls_from_update(
|
|
211
|
+
update: ChatResponseUpdate, *, suppress_finish_reason: bool = False
|
|
212
|
+
) -> ChatResponseUpdate | None:
|
|
213
|
+
"""Copy-on-write strip of actionable calls from an exhaustion-tail update.
|
|
214
|
+
|
|
215
|
+
Runs in the tail's own yield loop — above the client filter seam, so the
|
|
216
|
+
ResponseStream filter contract ("must return an update") is not involved
|
|
217
|
+
and a fully emptied update can be dropped by returning ``None``. The
|
|
218
|
+
incoming update is never mutated: the retry stream's recorded updates
|
|
219
|
+
must keep the provider's original contents so the post-assembly response
|
|
220
|
+
strip still observes what came back (that observation drives the
|
|
221
|
+
service-storage desync guard). An emptied update still passes through
|
|
222
|
+
when it carries meaningful metadata — any semantic field ``from_updates``
|
|
223
|
+
or a streaming consumer reads (continuation handles, model/timestamps/
|
|
224
|
+
ids, provider metadata); usage survives as a retained content.
|
|
225
|
+
``raw_representation`` deliberately does NOT exempt: every wire-parsed
|
|
226
|
+
update carries its SDK event, so exempting it would make the drop
|
|
227
|
+
unreachable.
|
|
228
|
+
|
|
229
|
+
``finish_reason`` never exempts and is cleared from every stripped copy:
|
|
230
|
+
the provider concluded the turn on calls this strip is withholding, and
|
|
231
|
+
a consumer keying on the first terminal reason would treat the run as
|
|
232
|
+
complete before the tail's corrective final update re-emits the real
|
|
233
|
+
one. ``suppress_finish_reason`` extends that verdict to updates with no
|
|
234
|
+
call of their own — providers commonly emit the call delta and the
|
|
235
|
+
finish-reason chunk separately, so once the tail has stripped a call the
|
|
236
|
+
caller must suppress every later provider reason too, or the reason-only
|
|
237
|
+
chunk crosses untouched ahead of the corrective update.
|
|
238
|
+
"""
|
|
239
|
+
has_actionable_call = any(_is_actionable_function_call(content) for content in update.contents)
|
|
240
|
+
if not has_actionable_call and not (suppress_finish_reason and update.finish_reason is not None):
|
|
241
|
+
return update
|
|
242
|
+
stripped = copy(update)
|
|
243
|
+
stripped.contents = [content for content in update.contents if not _is_actionable_function_call(content)]
|
|
244
|
+
stripped.finish_reason = None
|
|
245
|
+
if (
|
|
246
|
+
stripped.contents
|
|
247
|
+
or any(
|
|
248
|
+
value is not None
|
|
249
|
+
for value in (
|
|
250
|
+
stripped.response_id,
|
|
251
|
+
stripped.conversation_id,
|
|
252
|
+
stripped.continuation_token,
|
|
253
|
+
stripped.model,
|
|
254
|
+
stripped.created_at,
|
|
255
|
+
stripped.message_id,
|
|
256
|
+
stripped.author_name,
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
or stripped.additional_properties
|
|
260
|
+
):
|
|
261
|
+
return stripped
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _response_has_visible_content(response: ChatResponse) -> bool:
|
|
266
|
+
for message in response.messages:
|
|
267
|
+
for content in message.contents:
|
|
268
|
+
if content.type == "text":
|
|
269
|
+
if content.text and content.text.strip():
|
|
270
|
+
return True
|
|
271
|
+
elif content.type in _USER_VISIBLE_CONTENT_TYPES:
|
|
272
|
+
return True
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _ensure_exhaustion_fallback_response(response: ChatResponse, fallback_text: str) -> bool:
|
|
277
|
+
"""Synthesize *fallback_text* when the final response has nothing visible.
|
|
278
|
+
|
|
279
|
+
Runs unconditionally after the tail strip (and, in streaming, after echo
|
|
280
|
+
shell cleanup): a blank or reasoning-only final response would otherwise
|
|
281
|
+
return as an empty success — the exact shape response validation exists to
|
|
282
|
+
prevent — regardless of whether the strip removed anything.
|
|
283
|
+
"""
|
|
284
|
+
if _response_has_visible_content(response):
|
|
285
|
+
return False
|
|
286
|
+
fallback_content = Content.from_text(fallback_text)
|
|
287
|
+
if response.messages and not response.messages[-1].contents:
|
|
288
|
+
response.messages[-1].role = "assistant"
|
|
289
|
+
response.messages[-1].contents = [fallback_content]
|
|
290
|
+
else:
|
|
291
|
+
response.messages.append(Message(role="assistant", contents=[fallback_content]))
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _invalidate_service_continuation_state(
|
|
296
|
+
response: ChatResponse,
|
|
297
|
+
session: AgentSession | None,
|
|
298
|
+
) -> None:
|
|
299
|
+
"""Withhold a stripped, service-stored response's continuation handles.
|
|
300
|
+
|
|
301
|
+
The service retains the unstripped transcript (calls with no outputs)
|
|
302
|
+
behind them; propagating a handle would reproduce next turn the 400 the
|
|
303
|
+
strip prevents. Clearing forces authoritative local-history replay. The
|
|
304
|
+
withheld handles — the response's own id included, since a stateful
|
|
305
|
+
provider accepts it back as ``previous_response_id`` — are recorded on
|
|
306
|
+
the session so a caller repeating one under any handle spelling gets
|
|
307
|
+
suppressed instead of re-sent alongside that replay.
|
|
308
|
+
"""
|
|
309
|
+
if session is not None:
|
|
310
|
+
for handle in (response.conversation_id, response.response_id, session.service_session_id):
|
|
311
|
+
if isinstance(handle, str) and handle and not is_local_history_conversation_id(handle):
|
|
312
|
+
session.invalidated_service_session_ids.add(handle)
|
|
313
|
+
response.conversation_id = None
|
|
314
|
+
response.response_id = None
|
|
315
|
+
response._chrys_service_state_invalidated = True
|
|
316
|
+
if session is not None and session.service_session_id is not None:
|
|
317
|
+
session.service_session_id = None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# --------------------------------------------------------------------------- #
|
|
321
|
+
# Loop helpers (response shaping)
|
|
322
|
+
# --------------------------------------------------------------------------- #
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _extract_function_calls(response: ChatResponse) -> list[Content]:
|
|
326
|
+
"""Collect unresolved, deduplicated function calls from a response.
|
|
327
|
+
|
|
328
|
+
Skips calls that already carry a result
|
|
329
|
+
in the same response and duplicate ``call_id``\\ s. Echoed calls never
|
|
330
|
+
reach this filter — ``_land_response_contents`` removes them from
|
|
331
|
+
the response before anything else observes it.
|
|
332
|
+
"""
|
|
333
|
+
function_results = {
|
|
334
|
+
item.call_id
|
|
335
|
+
for message in response.messages
|
|
336
|
+
for item in message.contents
|
|
337
|
+
if item.type == "function_result" and item.call_id
|
|
338
|
+
}
|
|
339
|
+
seen_call_ids: set[str] = set()
|
|
340
|
+
function_calls: list[Content] = []
|
|
341
|
+
for message in response.messages:
|
|
342
|
+
for item in message.contents:
|
|
343
|
+
if not _is_actionable_function_call(item):
|
|
344
|
+
continue
|
|
345
|
+
if item.call_id and item.call_id in function_results:
|
|
346
|
+
continue
|
|
347
|
+
if item.call_id and item.call_id in seen_call_ids:
|
|
348
|
+
continue
|
|
349
|
+
if item.call_id:
|
|
350
|
+
seen_call_ids.add(item.call_id)
|
|
351
|
+
function_calls.append(item)
|
|
352
|
+
return function_calls
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
_HOSTED_CALL_CONTENT_TYPES = frozenset(TOOL_CALL_CONTENT_TYPES - {"function_call"})
|
|
356
|
+
"""Provider-hosted tool invocations a landed response may carry (observed, never executed here)."""
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _hosted_call_kinds(response: ChatResponse) -> list[str]:
|
|
360
|
+
"""Content types of the hosted calls in *response*, in emission order."""
|
|
361
|
+
return [
|
|
362
|
+
item.type
|
|
363
|
+
for message in response.messages
|
|
364
|
+
for item in message.contents
|
|
365
|
+
if item.type in _HOSTED_CALL_CONTENT_TYPES
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _land_response_contents(
|
|
370
|
+
response: ChatResponse,
|
|
371
|
+
next_ordinal: int,
|
|
372
|
+
echo_registry: WeakIdentityRegistry,
|
|
373
|
+
*,
|
|
374
|
+
exchange_operation_id: str | None = None,
|
|
375
|
+
) -> int:
|
|
376
|
+
"""Normalize a landed response's contents and stamp fresh function calls.
|
|
377
|
+
|
|
378
|
+
This loop is the single authoritative producer of ``_chrys_tool_invocation_order``.
|
|
379
|
+
Landing happens at response arrival — before recording, dispatch
|
|
380
|
+
extraction, tool lookup, and argument validation — over every
|
|
381
|
+
non-informational ``function_call`` content in model emission order.
|
|
382
|
+
Calls the dispatch filter drops (already-answered, duplicate ids,
|
|
383
|
+
no-tools batches) and calls that fail pre-pipeline validation still
|
|
384
|
+
consume ordinals, so persisted numbering can never diverge from pipeline
|
|
385
|
+
numbering. Tool-operation ids are assigned later, once response-scoped
|
|
386
|
+
pairing has distinguished work this loop owns from calls the provider
|
|
387
|
+
already answered. Returns the next unassigned ordinal.
|
|
388
|
+
|
|
389
|
+
Sole-authority consequences: a pre-existing stamp on a content this run
|
|
390
|
+
has NOT numbered is foreign data (a bridged or cached response replaying
|
|
391
|
+
another run's history) and is overwritten — preserving it while the
|
|
392
|
+
counter advances would let a stale value collide with a fresh assignment.
|
|
393
|
+
|
|
394
|
+
``echo_registry`` holds the identity of every content the conversation
|
|
395
|
+
already holds — the loop seeds it from caller history and response
|
|
396
|
+
contents land here, batch results register in
|
|
397
|
+
``_handle_function_call_results``; usage stays out via the registry's
|
|
398
|
+
``ignore_usage`` construction policy. A content OBJECT reappearing
|
|
399
|
+
later (a client echoing prior messages back, or the same object
|
|
400
|
+
repeated within one response) is an echo of history, not new work, and
|
|
401
|
+
is removed before the recorder, the dispatch filter, and final
|
|
402
|
+
transcript assembly observe it: an echoed call would otherwise
|
|
403
|
+
re-execute or persist as a dangling duplicate, and an echoed result
|
|
404
|
+
would duplicate an answer the transcript already holds. Removal is by
|
|
405
|
+
object identity only — a DIFFERENT content reusing an echoed call id
|
|
406
|
+
is fresh work (providers mint per-response counter ids) and lands
|
|
407
|
+
normally; a dead member's recycled address can never match, because
|
|
408
|
+
membership requires the registered object itself to still be alive.
|
|
409
|
+
Identity removal presumes assembly cannot launder an echoed object into
|
|
410
|
+
a new one: ``ChatResponseUpdate`` assembly MERGES consecutive same-call
|
|
411
|
+
function-call fragments (and coalesces adjacent text) into NEW objects,
|
|
412
|
+
so the streaming loop registers ``_strip_echoed_update`` as a per-turn
|
|
413
|
+
update filter on the wire stream — conversation-held objects are
|
|
414
|
+
removed at the innermost stream BEFORE accumulation, so a merge copy of
|
|
415
|
+
an echo never exists, a fresh call reusing an echoed id assembles alone
|
|
416
|
+
with its own arguments, and the provider finalizer plus every result
|
|
417
|
+
hook observe the same echo-free sequence (no re-assembly ever
|
|
418
|
+
overwrites a hook's rewrite of the response). Call-id-level provenance
|
|
419
|
+
cannot do this job: an id carrying both a laundered echo merge and a
|
|
420
|
+
fresh call is ambiguous per id, and either verdict misfires
|
|
421
|
+
(re-executing the echo or swallowing the fresh call via duplicate-id
|
|
422
|
+
dispatch). The reverse direction of the same laundering is covered by
|
|
423
|
+
``_record_stream_fragment_identities`` after landing: merges mint NEW
|
|
424
|
+
assembled objects, so the raw fragments a client just streamed would
|
|
425
|
+
otherwise stay unknown to the memo and could be replayed next turn.
|
|
426
|
+
|
|
427
|
+
Message wrappers are never shared with the client, in either direction.
|
|
428
|
+
Every retained message is REPLACED in ``response.messages`` by a
|
|
429
|
+
loop-owned shallow copy (own contents list, copied
|
|
430
|
+
``additional_properties``) — or dropped when nothing fresh remains.
|
|
431
|
+
That isolation cuts both ways: echo removal never mutates the incoming
|
|
432
|
+
message (the same object may sit in the accumulated transcript, and a
|
|
433
|
+
client-held alias must keep its view), and a stateful client that later
|
|
434
|
+
mutates a message object it returned — appending a fresh call between
|
|
435
|
+
iterations — cannot rewrite the history this run already landed.
|
|
436
|
+
The request direction is covered too: ``_WireCaller._call`` sends EVERY
|
|
437
|
+
outgoing message — caller history included — as a per-call view
|
|
438
|
+
(``_wire_message_view``), so mutating an INPUT message can corrupt
|
|
439
|
+
neither landed history nor the caller's session-state objects; message
|
|
440
|
+
metadata still writes through because views share the wrapper's
|
|
441
|
+
``additional_properties`` dict. ``messages`` is rebound, not mutated,
|
|
442
|
+
so caller-held aliases of the original list keep their view. The
|
|
443
|
+
CONTENT objects stay the originals: the registry references them
|
|
444
|
+
weakly, so an entry lives exactly as long as its object and a
|
|
445
|
+
collected content simply stops being a member.
|
|
446
|
+
|
|
447
|
+
Landing is also where the trajectory identities are minted, under the
|
|
448
|
+
same sole-authority rule as the ordinal: every retained message gets a
|
|
449
|
+
fresh analytics item id and is bound to ``exchange_operation_id`` (the
|
|
450
|
+
wire exchange that produced it); every fresh function call gets its own
|
|
451
|
+
item id. A pre-existing operation stamp is foreign data (a replayed or
|
|
452
|
+
cached response) and is removed, never trusted. The dispatch collector
|
|
453
|
+
later assigns one operation id per unresolved call id; duplicate call
|
|
454
|
+
contents share it, while a provider-answered call has no Chrys tool
|
|
455
|
+
operation to name.
|
|
456
|
+
"""
|
|
457
|
+
normalized_messages: list[Message] = []
|
|
458
|
+
for message in response.messages:
|
|
459
|
+
fresh_contents: list[Content] = []
|
|
460
|
+
dropped_echo = False
|
|
461
|
+
for item in message.contents:
|
|
462
|
+
if item in echo_registry:
|
|
463
|
+
dropped_echo = True
|
|
464
|
+
continue
|
|
465
|
+
echo_registry.register(item)
|
|
466
|
+
if _is_actionable_function_call(item):
|
|
467
|
+
item.additional_properties[TOOL_INVOCATION_ORDER_KEY] = next_ordinal
|
|
468
|
+
next_ordinal += 1
|
|
469
|
+
item.additional_properties.pop(OPERATION_ID_KEY, None)
|
|
470
|
+
if item.type == "function_call":
|
|
471
|
+
item.additional_properties[ANALYTICS_ITEM_ID_KEY] = new_analytics_id()
|
|
472
|
+
fresh_contents.append(item)
|
|
473
|
+
if dropped_echo and not fresh_contents:
|
|
474
|
+
continue
|
|
475
|
+
# Snapshot the wrapper, keep the contents: the client holds its own
|
|
476
|
+
# reference to this message and may mutate it between iterations.
|
|
477
|
+
snapshot = _message_snapshot(message, fresh_contents)
|
|
478
|
+
snapshot.additional_properties[ANALYTICS_ITEM_ID_KEY] = new_analytics_id()
|
|
479
|
+
if exchange_operation_id is not None:
|
|
480
|
+
snapshot.additional_properties[OPERATION_ID_KEY] = exchange_operation_id
|
|
481
|
+
normalized_messages.append(snapshot)
|
|
482
|
+
response.messages = normalized_messages
|
|
483
|
+
return next_ordinal
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _stamp_function_call_operations(response: ChatResponse, function_calls: Sequence[Content]) -> None:
|
|
487
|
+
"""Assign one tool operation to each unresolved call the loop may dispatch.
|
|
488
|
+
|
|
489
|
+
``_extract_function_calls`` is the response-scoped pairing authority: a
|
|
490
|
+
call already answered in the same response is provider-owned history, not
|
|
491
|
+
a Chrys tool operation. Repeated unresolved ``call_id`` contents describe
|
|
492
|
+
the same dispatch (the extractor keeps the first), so they share that
|
|
493
|
+
first call's operation id rather than opening operations nobody executes.
|
|
494
|
+
Empty ids are not deduplicated and therefore keep one operation per call.
|
|
495
|
+
"""
|
|
496
|
+
operation_by_identity: dict[int, str] = {}
|
|
497
|
+
operation_by_call_id: dict[str, str] = {}
|
|
498
|
+
for function_call in function_calls:
|
|
499
|
+
operation_id = new_analytics_id()
|
|
500
|
+
function_call.additional_properties[OPERATION_ID_KEY] = operation_id
|
|
501
|
+
operation_by_identity[id(function_call)] = operation_id
|
|
502
|
+
if function_call.call_id:
|
|
503
|
+
operation_by_call_id[function_call.call_id] = operation_id
|
|
504
|
+
|
|
505
|
+
for message in response.messages:
|
|
506
|
+
for item in message.contents:
|
|
507
|
+
if not _is_actionable_function_call(item) or id(item) in operation_by_identity:
|
|
508
|
+
continue
|
|
509
|
+
operation_id = operation_by_call_id.get(item.call_id or "")
|
|
510
|
+
if operation_id is not None:
|
|
511
|
+
item.additional_properties[OPERATION_ID_KEY] = operation_id
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _strip_echoed_update(
|
|
515
|
+
update: ChatResponseUpdate,
|
|
516
|
+
echo_registry: WeakIdentityRegistry,
|
|
517
|
+
) -> ChatResponseUpdate:
|
|
518
|
+
"""The update with conversation-held content objects removed.
|
|
519
|
+
|
|
520
|
+
Registered per turn as an update filter on the wire stream
|
|
521
|
+
(:meth:`ResponseStream.with_update_filter`), so it runs at the innermost
|
|
522
|
+
stream BEFORE accumulation: update assembly merges consecutive same-call
|
|
523
|
+
function-call fragments (and coalesces adjacent text) into NEW objects,
|
|
524
|
+
so an echoed historical object that participates in a merge would
|
|
525
|
+
launder its identity past the echo memo. Filtering ahead of assembly
|
|
526
|
+
means a laundered merge copy never exists, a fresh call reusing an
|
|
527
|
+
echoed call id assembles alone with its own arguments, and — because
|
|
528
|
+
the provider finalizer and every result hook see the already-filtered
|
|
529
|
+
updates — no re-assembly ever overwrites a hook's rewrite of the
|
|
530
|
+
response ("hooks run before tool extraction" stays true). The incoming
|
|
531
|
+
update is never mutated: one carrying an echo is shallow-copied with a
|
|
532
|
+
filtered contents list, so a producer-held alias keeps its shape.
|
|
533
|
+
The copy carries private, non-serialized provenance through stream
|
|
534
|
+
proxies and assembly, letting the loop drop only the logical message
|
|
535
|
+
shell this update actually emptied. The marker is attached to the
|
|
536
|
+
accepted update object rather than keyed by provider message ID, which
|
|
537
|
+
can collide across validation retries.
|
|
538
|
+
"""
|
|
539
|
+
if any(item in echo_registry for item in update.contents):
|
|
540
|
+
update = copy(update)
|
|
541
|
+
update._chrys_echo_content_stripped = True
|
|
542
|
+
update.contents = [item for item in update.contents if item not in echo_registry]
|
|
543
|
+
return update
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _remove_echo_emptied_message_shells(response: ChatResponse) -> None:
|
|
547
|
+
"""Remove only accepted-stream messages left empty by echo stripping.
|
|
548
|
+
|
|
549
|
+
A call-wide marker cannot distinguish an unrelated legitimate empty
|
|
550
|
+
message from an echo-only one. Per-update provenance is folded into the
|
|
551
|
+
assembled Message, so this cleanup remains per message and per accepted
|
|
552
|
+
validation attempt without trusting non-unique provider message IDs.
|
|
553
|
+
"""
|
|
554
|
+
response.messages = [
|
|
555
|
+
message for message in response.messages if message.contents or not message._chrys_echo_content_stripped
|
|
556
|
+
]
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _record_stream_fragment_identities(
|
|
560
|
+
stream: ResponseStream[ChatResponseUpdate, ChatResponse],
|
|
561
|
+
echo_registry: WeakIdentityRegistry,
|
|
562
|
+
) -> None:
|
|
563
|
+
"""Record the identity of every accepted stream content.
|
|
564
|
+
|
|
565
|
+
Landing only sees the ASSEMBLED response, and assembly merges
|
|
566
|
+
multi-fragment calls (and coalesces adjacent text) into NEW objects —
|
|
567
|
+
so the raw fragment objects the client actually yielded would never
|
|
568
|
+
enter the echo registry, and a stateful client replaying a buffered
|
|
569
|
+
fragment on a later turn would sail past ``_strip_echoed_update`` and
|
|
570
|
+
re-execute the call. The consumed stream's accumulated updates are the
|
|
571
|
+
accepted (post-strip) sequence; under the response-validation proxy
|
|
572
|
+
they are the SELECTED attempt's replay, so failed attempts' fragments
|
|
573
|
+
are not recorded and can be legitimately re-sent on a retry.
|
|
574
|
+
|
|
575
|
+
Must run AFTER ``_land_response_contents``: a single-fragment call's
|
|
576
|
+
assembled content IS the fragment object, and pre-registering it
|
|
577
|
+
would make landing drop the fresh call as its own echo.
|
|
578
|
+
|
|
579
|
+
No retention side: a replay requires the client to still HOLD the
|
|
580
|
+
fragment, which keeps the weak entry alive; a collected fragment's
|
|
581
|
+
entry evicts, so its recycled id can never strip a genuinely fresh
|
|
582
|
+
content. Usage is request-scoped accounting rather than conversation
|
|
583
|
+
content (a client may reuse a mutable usage object across model
|
|
584
|
+
calls), so the registry's ``ignore_usage`` policy keeps it out.
|
|
585
|
+
"""
|
|
586
|
+
for update in stream.updates:
|
|
587
|
+
for item in update.contents:
|
|
588
|
+
echo_registry.register(item)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _record_wire_request_content_identities(
|
|
592
|
+
messages: Sequence[Message],
|
|
593
|
+
echo_registry: WeakIdentityRegistry,
|
|
594
|
+
) -> None:
|
|
595
|
+
"""Register the post-middleware contents of one actual wire request.
|
|
596
|
+
|
|
597
|
+
Called at the chat pipeline's final-handler boundary, after every
|
|
598
|
+
middleware has transformed the request and immediately before the inner
|
|
599
|
+
client sees it. This closes the identity-registry gap for wire-only
|
|
600
|
+
messages such as injections and reminders, including repeated
|
|
601
|
+
``call_next`` retries.
|
|
602
|
+
|
|
603
|
+
No retention side: a weak entry lives exactly as long as its object,
|
|
604
|
+
so a collected wire-only enrichment's entry evicts and its recycled id
|
|
605
|
+
can never strip a genuinely fresh content. Usage is request-scoped
|
|
606
|
+
accounting; the registry's ``ignore_usage`` policy keeps it out.
|
|
607
|
+
"""
|
|
608
|
+
for message in messages:
|
|
609
|
+
for item in message.contents:
|
|
610
|
+
echo_registry.register(item)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _stream_usage_chunks(update: ChatResponseUpdate) -> list[Mapping[str, Any]]:
|
|
614
|
+
"""Return usage snapshots carried by one streaming update."""
|
|
615
|
+
return [
|
|
616
|
+
content.usage_details
|
|
617
|
+
for content in update.contents
|
|
618
|
+
if content.type == "usage" and content.usage_details is not None
|
|
619
|
+
]
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _truncated_final_function_call_ids(response: ChatResponse, function_calls: Sequence[Content]) -> set[int]:
|
|
623
|
+
"""Return the object id for the final content block when it is a function call."""
|
|
624
|
+
if response.finish_reason != "length" or not function_calls:
|
|
625
|
+
return set()
|
|
626
|
+
final_content = next(
|
|
627
|
+
(item for message in reversed(response.messages) for item in reversed(message.contents)),
|
|
628
|
+
None,
|
|
629
|
+
)
|
|
630
|
+
if final_content is None or not _is_actionable_function_call(final_content):
|
|
631
|
+
return set()
|
|
632
|
+
function_call_ids = {id(item) for item in function_calls}
|
|
633
|
+
final_id = id(final_content)
|
|
634
|
+
return {final_id} if final_id in function_call_ids else set()
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _prepend_fcc_messages(response: ChatResponse, fcc_messages: list[Message]) -> None:
|
|
638
|
+
"""Insert accumulated loop messages before the final response."""
|
|
639
|
+
if not fcc_messages:
|
|
640
|
+
return
|
|
641
|
+
for msg in reversed(fcc_messages):
|
|
642
|
+
response.messages.insert(0, msg)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _update_conversation_id(
|
|
646
|
+
kwargs: dict[str, Any],
|
|
647
|
+
conversation_id: str | None,
|
|
648
|
+
options: dict[str, Any] | None = None,
|
|
649
|
+
) -> None:
|
|
650
|
+
"""Write a service conversation id back into kwargs/options."""
|
|
651
|
+
if conversation_id is None:
|
|
652
|
+
return
|
|
653
|
+
if "chat_options" in kwargs:
|
|
654
|
+
kwargs["chat_options"]["conversation_id"] = conversation_id
|
|
655
|
+
else:
|
|
656
|
+
kwargs["conversation_id"] = conversation_id
|
|
657
|
+
if options is not None:
|
|
658
|
+
options["conversation_id"] = conversation_id
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _update_continuation_state(
|
|
662
|
+
kwargs: dict[str, Any],
|
|
663
|
+
response: ChatResponse,
|
|
664
|
+
*,
|
|
665
|
+
session: AgentSession | None,
|
|
666
|
+
options: dict[str, Any] | None = None,
|
|
667
|
+
) -> None:
|
|
668
|
+
"""Update in-flight and persisted continuation state from a response."""
|
|
669
|
+
conversation_id = response.conversation_id
|
|
670
|
+
if conversation_id is None:
|
|
671
|
+
return
|
|
672
|
+
|
|
673
|
+
_update_conversation_id(kwargs, conversation_id, options)
|
|
674
|
+
if (
|
|
675
|
+
session is not None
|
|
676
|
+
and not response.has_internal_conversation_id()
|
|
677
|
+
and session.service_session_id != conversation_id
|
|
678
|
+
):
|
|
679
|
+
session.service_session_id = conversation_id
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _clear_internal_conversation_id(response: ChatResponse) -> ChatResponse:
|
|
683
|
+
"""Strip a client-internal conversation id before returning."""
|
|
684
|
+
if response.has_internal_conversation_id():
|
|
685
|
+
response.conversation_id = None
|
|
686
|
+
response.clear_internal_conversation_id()
|
|
687
|
+
return response
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _handle_function_call_results(
|
|
691
|
+
*,
|
|
692
|
+
response: ChatResponse,
|
|
693
|
+
function_call_results: list[Content],
|
|
694
|
+
fcc_messages: list[Message],
|
|
695
|
+
echo_registry: WeakIdentityRegistry,
|
|
696
|
+
errors_in_a_row: int,
|
|
697
|
+
had_errors: bool,
|
|
698
|
+
max_errors: int,
|
|
699
|
+
result_carrier_item_id: str,
|
|
700
|
+
recorder: LoopRecorder | None = None,
|
|
701
|
+
) -> tuple[str, int]:
|
|
702
|
+
"""Fold a batch of tool results back into the response.
|
|
703
|
+
|
|
704
|
+
Results are ``function_result`` contents. Returns
|
|
705
|
+
``(action, errors_in_a_row)`` where action is
|
|
706
|
+
``"continue"`` or ``"stop"`` (consecutive-error cap reached: submit the
|
|
707
|
+
collected results once more with tools disabled).
|
|
708
|
+
|
|
709
|
+
New results register in the echo registry —
|
|
710
|
+
they are part of the run's transcript from here on, so a client echoing
|
|
711
|
+
them back in a later response must see them removed as history rather
|
|
712
|
+
than landing a duplicate answer (``_land_response_contents``).
|
|
713
|
+
"""
|
|
714
|
+
for result in function_call_results:
|
|
715
|
+
echo_registry.register(result)
|
|
716
|
+
if had_errors:
|
|
717
|
+
errors_in_a_row += 1
|
|
718
|
+
reached_error_limit = errors_in_a_row >= max_errors
|
|
719
|
+
if reached_error_limit:
|
|
720
|
+
logger.warning(
|
|
721
|
+
"Maximum consecutive function call errors reached (%d). "
|
|
722
|
+
"Stopping further function calls for this request.",
|
|
723
|
+
max_errors,
|
|
724
|
+
)
|
|
725
|
+
else:
|
|
726
|
+
errors_in_a_row = 0
|
|
727
|
+
reached_error_limit = False
|
|
728
|
+
|
|
729
|
+
result_message = Message(role="tool", contents=function_call_results)
|
|
730
|
+
# The carrier is a persisted item of its own. Its batch-wide id was minted
|
|
731
|
+
# at dispatch, before any tool terminal named it, so the next context
|
|
732
|
+
# revision and every tool.operation.finished agree on this same item.
|
|
733
|
+
result_message.additional_properties[ANALYTICS_ITEM_ID_KEY] = result_carrier_item_id
|
|
734
|
+
response.messages.append(result_message)
|
|
735
|
+
if recorder is not None:
|
|
736
|
+
recorder.seal_exchange(result_message)
|
|
737
|
+
fcc_messages.extend(response.messages)
|
|
738
|
+
return ("stop" if reached_error_limit else "continue", errors_in_a_row)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
# --------------------------------------------------------------------------- #
|
|
742
|
+
# Run building blocks
|
|
743
|
+
# --------------------------------------------------------------------------- #
|
|
744
|
+
|
|
745
|
+
# The first tracked continuation token is always applied: nothing equals it.
|
|
746
|
+
_UNTRACKED = object()
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _wire_request_observer(
|
|
750
|
+
echo_registry: WeakIdentityRegistry,
|
|
751
|
+
caller_observer: Callable[[Sequence[Message]], None] | None,
|
|
752
|
+
) -> Callable[[Sequence[Message]], None]:
|
|
753
|
+
internal_observer = functools.partial(
|
|
754
|
+
_record_wire_request_content_identities,
|
|
755
|
+
echo_registry=echo_registry,
|
|
756
|
+
)
|
|
757
|
+
if caller_observer is None:
|
|
758
|
+
return internal_observer
|
|
759
|
+
|
|
760
|
+
def observe(messages: Sequence[Message]) -> None:
|
|
761
|
+
# Echo tracking remains the loop's invariant even if a caller
|
|
762
|
+
# observer raises; callers then see the exact same provider
|
|
763
|
+
# views immediately after the internal identity recorder.
|
|
764
|
+
internal_observer(messages)
|
|
765
|
+
caller_observer(messages)
|
|
766
|
+
|
|
767
|
+
return observe
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
async def _watchdog_await(awaitable: Awaitable[Any], timeout: float | None, label: str) -> Any:
|
|
771
|
+
# Idle timing: a pull whose first byte waits on compaction (and
|
|
772
|
+
# its LAST_WORDS side call) stays alive while that work reports
|
|
773
|
+
# progress, and stalls after *timeout* without any.
|
|
774
|
+
if timeout is None:
|
|
775
|
+
return await awaitable
|
|
776
|
+
event_loop = asyncio.get_running_loop()
|
|
777
|
+
last_progress = event_loop.time()
|
|
778
|
+
|
|
779
|
+
def _on_progress() -> None:
|
|
780
|
+
nonlocal last_progress
|
|
781
|
+
last_progress = event_loop.time()
|
|
782
|
+
|
|
783
|
+
task = start_with_wire_progress(awaitable, _on_progress)
|
|
784
|
+
try:
|
|
785
|
+
while not task.done():
|
|
786
|
+
idle_budget = last_progress + timeout - event_loop.time()
|
|
787
|
+
if idle_budget <= 0:
|
|
788
|
+
break
|
|
789
|
+
await asyncio.wait((task,), timeout=idle_budget)
|
|
790
|
+
except asyncio.CancelledError:
|
|
791
|
+
# The pull task may still be running INSIDE the stream's
|
|
792
|
+
# generator; closing that stream before the task settles
|
|
793
|
+
# would raise "asynchronous generator is already running".
|
|
794
|
+
task.cancel()
|
|
795
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
796
|
+
raise
|
|
797
|
+
if task.done():
|
|
798
|
+
return task.result()
|
|
799
|
+
task.cancel()
|
|
800
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
801
|
+
raise StreamStall(f"{label} produced no progress for {timeout:g}s")
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
class _LoopTrajectory:
|
|
805
|
+
"""Trajectory bookkeeping for one loop run.
|
|
806
|
+
|
|
807
|
+
One ``model.cycle`` per loop acquisition, one ``model.exchange`` per wire
|
|
808
|
+
attempt, ``retry.*`` around every wire retry; all of them are no-ops
|
|
809
|
+
without a bound context. The exchange trace rides to the wire client in
|
|
810
|
+
``client_kwargs`` — the loop cannot bind it ambiently because the provider
|
|
811
|
+
stream is lazy and resolves outside this call's context.
|
|
812
|
+
|
|
813
|
+
State, by lifetime:
|
|
814
|
+
|
|
815
|
+
- cycle, from ``cycle_started`` to ``cycle_finished`` or ``abort``: the
|
|
816
|
+
cycle context, its start time, exchange count and last exchange id;
|
|
817
|
+
- wire attempt, from ``begin_exchange`` to ``close_exchange``: the open
|
|
818
|
+
exchange trace;
|
|
819
|
+
- retry, from ``retry_scheduled`` to the next ``begin_exchange``: the id
|
|
820
|
+
minted for the next exchange;
|
|
821
|
+
- landing, from one ``cycle_finished`` to the next: the exchange context
|
|
822
|
+
the landed tool operations and hosted calls hang under;
|
|
823
|
+
- landing to dispatch: the tool operations minted for landed calls. This
|
|
824
|
+
ledger runs with or without a bound context; each entry settles under
|
|
825
|
+
the context it landed in.
|
|
826
|
+
|
|
827
|
+
``retry_policy`` is read only to classify a failed exchange as retryable.
|
|
828
|
+
"""
|
|
829
|
+
|
|
830
|
+
def __init__(self, root: TrajectoryContext | None, *, retry_policy: WireRetryPolicy | None) -> None:
|
|
831
|
+
self._root = root
|
|
832
|
+
self._retry_policy = retry_policy
|
|
833
|
+
self._cycle: TrajectoryContext | None = None
|
|
834
|
+
self._cycle_started_ns = 0
|
|
835
|
+
self._cycle_exchange_count = 0
|
|
836
|
+
self._active_exchange: ExchangeTrace | None = None
|
|
837
|
+
self._pending_exchange_id: str | None = None
|
|
838
|
+
self._last_exchange_id: str | None = None
|
|
839
|
+
self._landed_exchange: TrajectoryContext | None = None
|
|
840
|
+
self._undispatched_tool_operations: dict[int, tuple[Content, TrajectoryContext | None]] = {}
|
|
841
|
+
|
|
842
|
+
@property
|
|
843
|
+
def last_exchange_id(self) -> str | None:
|
|
844
|
+
return self._last_exchange_id
|
|
845
|
+
|
|
846
|
+
async def _emit(self, draft: EventDraft) -> None:
|
|
847
|
+
if self._root is None:
|
|
848
|
+
return
|
|
849
|
+
try:
|
|
850
|
+
await self._root.sink.emit(draft)
|
|
851
|
+
except Exception:
|
|
852
|
+
logger.debug("Trajectory emit failed", exc_info=True)
|
|
853
|
+
|
|
854
|
+
async def cycle_started(self, cycle_index: int, *, tools_offered: bool) -> None:
|
|
855
|
+
root = self._root
|
|
856
|
+
if root is None:
|
|
857
|
+
return
|
|
858
|
+
cycle_id = new_analytics_id()
|
|
859
|
+
cycle = root.with_cycle(cycle_id)
|
|
860
|
+
self._cycle = cycle
|
|
861
|
+
self._cycle_started_ns = monotonic_ns()
|
|
862
|
+
self._cycle_exchange_count = 0
|
|
863
|
+
self._last_exchange_id = None
|
|
864
|
+
await self._emit(
|
|
865
|
+
cycle.draft(
|
|
866
|
+
TrajectoryEventType.MODEL_CYCLE_STARTED,
|
|
867
|
+
operation_id=cycle_id,
|
|
868
|
+
parent_operation_id=root.run_operation_id,
|
|
869
|
+
payload={"cycle_index": cycle_index, "tools_offered": tools_offered},
|
|
870
|
+
)
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
def _cycle_finished_draft(self, *, outcome: str, function_call_count: int = 0) -> EventDraft | None:
|
|
874
|
+
cycle = self._cycle
|
|
875
|
+
if cycle is None or cycle.cycle_operation_id is None:
|
|
876
|
+
return None
|
|
877
|
+
self._landed_exchange = cycle.with_exchange(self._last_exchange_id)
|
|
878
|
+
return cycle.draft(
|
|
879
|
+
TrajectoryEventType.MODEL_CYCLE_FINISHED,
|
|
880
|
+
operation_id=cycle.cycle_operation_id,
|
|
881
|
+
parent_operation_id=cycle.run_operation_id,
|
|
882
|
+
payload={
|
|
883
|
+
"outcome": outcome,
|
|
884
|
+
"exchange_count": self._cycle_exchange_count,
|
|
885
|
+
"function_call_count": function_call_count,
|
|
886
|
+
# Monotonic at both ends: a wall clock that steps mid-cycle
|
|
887
|
+
# would otherwise stretch or flatten the span it measures.
|
|
888
|
+
"duration_ms": max(0, (monotonic_ns() - self._cycle_started_ns) // 1_000_000),
|
|
889
|
+
"final_exchange_operation_id": self._last_exchange_id,
|
|
890
|
+
},
|
|
891
|
+
measurements={"/payload/duration_ms": measurement(MeasurementSource.MONOTONIC_CLOCK, method_version=1)},
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
async def cycle_finished(self, response: ChatResponse, function_calls: Sequence[Content]) -> None:
|
|
895
|
+
"""Close the cycle *response* landed in and hand its calls to the operation ledger."""
|
|
896
|
+
draft = self._cycle_finished_draft(outcome=ExchangeOutcome.SUCCESS, function_call_count=len(function_calls))
|
|
897
|
+
self._register_landed_tool_operations(response, function_calls)
|
|
898
|
+
if draft is None:
|
|
899
|
+
return
|
|
900
|
+
await self._hosted_calls(response)
|
|
901
|
+
# Given up only here: an interrupt during the hosted-call records
|
|
902
|
+
# above still leaves the cycle for the abort path to close.
|
|
903
|
+
self._cycle = None
|
|
904
|
+
await self._emit(draft)
|
|
905
|
+
|
|
906
|
+
async def _hosted_calls(self, response: ChatResponse) -> None:
|
|
907
|
+
# Hosted calls never become tool operations (the provider ran
|
|
908
|
+
# them); one fact per call keeps the exchange's fan-out visible
|
|
909
|
+
# without an unbounded array on the exchange event.
|
|
910
|
+
exchange = self._landed_exchange
|
|
911
|
+
if exchange is None or exchange.exchange_operation_id is None:
|
|
912
|
+
return
|
|
913
|
+
for ordinal, hosted_kind in enumerate(_hosted_call_kinds(response)):
|
|
914
|
+
await self._emit(
|
|
915
|
+
exchange.draft(
|
|
916
|
+
TrajectoryEventType.HOSTED_CALL_OBSERVED,
|
|
917
|
+
operation_id=new_analytics_id(),
|
|
918
|
+
parent_operation_id=exchange.exchange_operation_id,
|
|
919
|
+
payload={
|
|
920
|
+
"parent_exchange_operation_id": exchange.exchange_operation_id,
|
|
921
|
+
"hosted_kind": hosted_kind,
|
|
922
|
+
"ordinal": ordinal,
|
|
923
|
+
},
|
|
924
|
+
)
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
def abort(self, outcome: str) -> None:
|
|
928
|
+
# Synchronous close for exits that cannot await (cancellation,
|
|
929
|
+
# generator close): the open exchange and cycle are queued in
|
|
930
|
+
# order without waiting for the ack.
|
|
931
|
+
self.close_exchange(outcome)
|
|
932
|
+
draft = self._cycle_finished_draft(outcome=outcome)
|
|
933
|
+
self._cycle = None
|
|
934
|
+
if draft is not None and self._root is not None:
|
|
935
|
+
try:
|
|
936
|
+
self._root.sink.emit_soon(draft)
|
|
937
|
+
except Exception:
|
|
938
|
+
logger.debug("Trajectory cycle close failed", exc_info=True)
|
|
939
|
+
|
|
940
|
+
def exchange_scope(self) -> trajectory_scope:
|
|
941
|
+
# Ambient context for the tool batch a landed response spawns:
|
|
942
|
+
# tool middleware hangs its operations under the producing
|
|
943
|
+
# exchange. Without a loop-owned context the ambient one (if any)
|
|
944
|
+
# is simply re-bound.
|
|
945
|
+
if self._landed_exchange is None:
|
|
946
|
+
return trajectory_scope(current_trajectory())
|
|
947
|
+
return trajectory_scope(self._landed_exchange)
|
|
948
|
+
|
|
949
|
+
def begin_exchange(self, client_kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
950
|
+
"""Open the exchange for the next wire attempt; returns the kwargs that carry it."""
|
|
951
|
+
self._active_exchange = None
|
|
952
|
+
cycle = self._cycle
|
|
953
|
+
if cycle is None:
|
|
954
|
+
return client_kwargs
|
|
955
|
+
exchange_id = self._pending_exchange_id or new_analytics_id()
|
|
956
|
+
self._pending_exchange_id = None
|
|
957
|
+
self._last_exchange_id = exchange_id
|
|
958
|
+
self._cycle_exchange_count += 1
|
|
959
|
+
self._active_exchange = ExchangeTrace(cycle.with_exchange(exchange_id))
|
|
960
|
+
return {**client_kwargs, TRAJECTORY_EXCHANGE_KWARG: self._active_exchange}
|
|
961
|
+
|
|
962
|
+
def stall_observed(self) -> None:
|
|
963
|
+
if self._active_exchange is not None:
|
|
964
|
+
self._active_exchange.stall_observed()
|
|
965
|
+
|
|
966
|
+
def close_exchange(self, outcome: str, *, exc: BaseException | None = None) -> None:
|
|
967
|
+
# Idempotent: a wire client that already reported its own terminal
|
|
968
|
+
# marker wins; this closes the abandoned/failed remainder.
|
|
969
|
+
trace = self._active_exchange
|
|
970
|
+
if trace is None:
|
|
971
|
+
return
|
|
972
|
+
self._active_exchange = None
|
|
973
|
+
# A middleware beneath the loop may have re-issued the request in
|
|
974
|
+
# place (validation retry): the handle then names the final
|
|
975
|
+
# exchange and every re-issue was one more acquisition.
|
|
976
|
+
self._last_exchange_id = trace.operation_id
|
|
977
|
+
self._cycle_exchange_count += trace.generation
|
|
978
|
+
payload: dict[str, Any] = {}
|
|
979
|
+
if exc is not None:
|
|
980
|
+
payload["error_code"] = type(exc).__name__
|
|
981
|
+
policy = self._retry_policy
|
|
982
|
+
payload["retryable"] = bool(policy is not None and policy.is_retryable(exc))
|
|
983
|
+
try:
|
|
984
|
+
trace.finished(outcome=outcome, payload=payload)
|
|
985
|
+
except Exception:
|
|
986
|
+
logger.debug("Trajectory exchange close failed", exc_info=True)
|
|
987
|
+
|
|
988
|
+
async def retry_scheduled(
|
|
989
|
+
self,
|
|
990
|
+
*,
|
|
991
|
+
reason_code: str,
|
|
992
|
+
retry_mode: str,
|
|
993
|
+
delay_seconds: int,
|
|
994
|
+
fallback_to_blocking: bool,
|
|
995
|
+
committed_work_present: bool,
|
|
996
|
+
) -> None:
|
|
997
|
+
cycle = self._cycle
|
|
998
|
+
if cycle is None:
|
|
999
|
+
return
|
|
1000
|
+
previous = self._last_exchange_id
|
|
1001
|
+
next_id = new_analytics_id()
|
|
1002
|
+
self._pending_exchange_id = next_id
|
|
1003
|
+
await self._emit(
|
|
1004
|
+
cycle.draft(
|
|
1005
|
+
TrajectoryEventType.RETRY_SCHEDULED,
|
|
1006
|
+
operation_id=next_id,
|
|
1007
|
+
parent_operation_id=cycle.cycle_operation_id,
|
|
1008
|
+
payload={
|
|
1009
|
+
"reason_code": reason_code,
|
|
1010
|
+
"delay_ms": max(0, int(delay_seconds * 1000)),
|
|
1011
|
+
"retry_mode": retry_mode,
|
|
1012
|
+
"previous_operation_id": previous,
|
|
1013
|
+
"committed_work_present": committed_work_present,
|
|
1014
|
+
"fallback_to_blocking": fallback_to_blocking,
|
|
1015
|
+
},
|
|
1016
|
+
)
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
async def retry_started(self, *, retry_mode: str) -> None:
|
|
1020
|
+
cycle = self._cycle
|
|
1021
|
+
next_id = self._pending_exchange_id
|
|
1022
|
+
if cycle is None or next_id is None:
|
|
1023
|
+
return
|
|
1024
|
+
await self._emit(
|
|
1025
|
+
cycle.draft(
|
|
1026
|
+
TrajectoryEventType.RETRY_STARTED,
|
|
1027
|
+
operation_id=next_id,
|
|
1028
|
+
parent_operation_id=cycle.cycle_operation_id,
|
|
1029
|
+
payload={
|
|
1030
|
+
"retry_mode": retry_mode,
|
|
1031
|
+
"next_operation_id": next_id,
|
|
1032
|
+
"previous_operation_id": self._last_exchange_id,
|
|
1033
|
+
},
|
|
1034
|
+
)
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
def _register_landed_tool_operations(
|
|
1038
|
+
self,
|
|
1039
|
+
response: ChatResponse,
|
|
1040
|
+
function_calls: Sequence[Content],
|
|
1041
|
+
) -> None:
|
|
1042
|
+
"""Mint and remember every operation until the one dispatch point takes it."""
|
|
1043
|
+
_stamp_function_call_operations(response, function_calls)
|
|
1044
|
+
operation_context = self._landed_exchange or current_trajectory()
|
|
1045
|
+
for function_call in function_calls:
|
|
1046
|
+
self._undispatched_tool_operations[id(function_call)] = (function_call, operation_context)
|
|
1047
|
+
|
|
1048
|
+
def mark_tool_operations_dispatched(self, function_calls: Sequence[Content]) -> None:
|
|
1049
|
+
for function_call in function_calls:
|
|
1050
|
+
self._undispatched_tool_operations.pop(id(function_call), None)
|
|
1051
|
+
|
|
1052
|
+
async def settle_undispatched_tool_operations(self, *, queued: bool = False) -> None:
|
|
1053
|
+
"""Close every operation minted but never handed to the execution batch.
|
|
1054
|
+
|
|
1055
|
+
This is the single reconciliation point for no-tools responses,
|
|
1056
|
+
exhaustion tails, future early returns, and failures between
|
|
1057
|
+
landing and dispatch. Every entry keeps the exchange context it
|
|
1058
|
+
landed under rather than consulting the loop's later current one.
|
|
1059
|
+
"""
|
|
1060
|
+
pending = list(self._undispatched_tool_operations.values())
|
|
1061
|
+
self._undispatched_tool_operations.clear()
|
|
1062
|
+
first_failure: BaseException | None = None
|
|
1063
|
+
for function_call, operation_context in pending:
|
|
1064
|
+
try:
|
|
1065
|
+
with trajectory_scope(operation_context):
|
|
1066
|
+
await _record_unexecuted_tool_operation(
|
|
1067
|
+
function_call,
|
|
1068
|
+
outcome=ToolOutcome.FILTERED,
|
|
1069
|
+
queued=queued,
|
|
1070
|
+
)
|
|
1071
|
+
except BaseException as exc:
|
|
1072
|
+
# The helper settles the current pair before propagating a
|
|
1073
|
+
# cancelled ack. Continue so one cancellation cannot strand
|
|
1074
|
+
# the rest of a parallel batch, then preserve the caller's
|
|
1075
|
+
# original control-flow signal.
|
|
1076
|
+
if first_failure is None:
|
|
1077
|
+
first_failure = exc
|
|
1078
|
+
if first_failure is not None:
|
|
1079
|
+
raise first_failure
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
class _WireCaller:
|
|
1083
|
+
"""The loop's logical model calls and their wire retry lanes.
|
|
1084
|
+
|
|
1085
|
+
A logical call is one ``blocking_response`` or ``streaming_response``:
|
|
1086
|
+
every request it sends until a response lands — continuation polls,
|
|
1087
|
+
transient and stall retries, the stall fallback to a blocking request
|
|
1088
|
+
and one context-overflow resend. Retries need a wire policy, which a
|
|
1089
|
+
service-side run never gets.
|
|
1090
|
+
|
|
1091
|
+
State, by lifetime: the continuation token last mirrored to the retry
|
|
1092
|
+
owner lives for the run; the overflow-resend flag for one logical call
|
|
1093
|
+
(the stall fallback inherits it); the transient and stall counters for
|
|
1094
|
+
one ``blocking_response`` call or the streaming attempts of one
|
|
1095
|
+
``streaming_response``, so the stall fallback's blocking request starts
|
|
1096
|
+
a fresh transient budget; the provider stream and its usage chunks for
|
|
1097
|
+
one request. Of the run's options, only ``continuation_token`` is
|
|
1098
|
+
written here.
|
|
1099
|
+
"""
|
|
1100
|
+
|
|
1101
|
+
def __init__(
|
|
1102
|
+
self,
|
|
1103
|
+
inner: ChatMiddlewareLayer,
|
|
1104
|
+
*,
|
|
1105
|
+
options: dict[str, Any],
|
|
1106
|
+
client_kwargs: dict[str, Any],
|
|
1107
|
+
chat_middleware: list[ChatMiddleware],
|
|
1108
|
+
compaction_strategy: Any,
|
|
1109
|
+
tokenizer: Any,
|
|
1110
|
+
policy: WireRetryPolicy | None,
|
|
1111
|
+
recorder: LoopRecorder | None,
|
|
1112
|
+
injection_probe: ConsumedInjectionMessageProbe | None,
|
|
1113
|
+
continuation_token_observer: Callable[[Any], None] | None,
|
|
1114
|
+
trajectory: _LoopTrajectory,
|
|
1115
|
+
) -> None:
|
|
1116
|
+
self._inner = inner
|
|
1117
|
+
self._options = options
|
|
1118
|
+
self._client_kwargs = client_kwargs
|
|
1119
|
+
self._chat_middleware = chat_middleware
|
|
1120
|
+
self._compaction_strategy = compaction_strategy
|
|
1121
|
+
self._tokenizer = tokenizer
|
|
1122
|
+
self._policy = policy
|
|
1123
|
+
self._recorder = recorder
|
|
1124
|
+
self._injection_probe = injection_probe
|
|
1125
|
+
self._continuation_token_observer = continuation_token_observer
|
|
1126
|
+
self._trajectory = trajectory
|
|
1127
|
+
# Sentinel start: the logical call may begin with a retry-owned token
|
|
1128
|
+
# already in its options (whole-run retry resuming a background
|
|
1129
|
+
# response), so the first tracked value — including a terminal
|
|
1130
|
+
# ``None`` — must always be applied, never swallowed by dedupe.
|
|
1131
|
+
self._last_tracked_continuation_token: Any = _UNTRACKED
|
|
1132
|
+
|
|
1133
|
+
def cancel_outcome(self) -> str:
|
|
1134
|
+
policy = self._policy
|
|
1135
|
+
return (
|
|
1136
|
+
ExchangeOutcome.INTERRUPTED if policy is not None and policy.is_interrupted() else ExchangeOutcome.CANCELLED
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
def _track_continuation_token(self, token: Any) -> None:
|
|
1140
|
+
# Mirror the live token into the retry owner's request state: a
|
|
1141
|
+
# transient poll failure must resume THIS response on the next
|
|
1142
|
+
# attempt (outer whole-run retry included), never re-issue the
|
|
1143
|
+
# original create request. Providers re-announce the id on every
|
|
1144
|
+
# progress event, so identical repeats are dropped.
|
|
1145
|
+
if token == self._last_tracked_continuation_token:
|
|
1146
|
+
return
|
|
1147
|
+
self._last_tracked_continuation_token = token
|
|
1148
|
+
if token is None:
|
|
1149
|
+
self._options.pop("continuation_token", None)
|
|
1150
|
+
else:
|
|
1151
|
+
self._options["continuation_token"] = token
|
|
1152
|
+
if self._continuation_token_observer is not None:
|
|
1153
|
+
self._continuation_token_observer(token)
|
|
1154
|
+
|
|
1155
|
+
async def _call(
|
|
1156
|
+
self,
|
|
1157
|
+
prepped: list[Message],
|
|
1158
|
+
*,
|
|
1159
|
+
as_stream: bool,
|
|
1160
|
+
stream_update_filter: Callable[[ChatResponseUpdate], ChatResponseUpdate] | None = None,
|
|
1161
|
+
request_message_observer: Callable[[Sequence[Message]], None],
|
|
1162
|
+
) -> Any:
|
|
1163
|
+
# The recorder keeps the loop's canonical objects (its identity
|
|
1164
|
+
# dedup depends on re-recorded history being the SAME objects).
|
|
1165
|
+
if self._recorder is not None:
|
|
1166
|
+
await self._recorder.record_pre_call(prepped)
|
|
1167
|
+
# EVERY outgoing message is a per-call view — caller history
|
|
1168
|
+
# included — so a client mutating a received message in place can
|
|
1169
|
+
# corrupt neither the loop's transcript nor the caller's
|
|
1170
|
+
# session-state objects. Message-metadata write-through survives:
|
|
1171
|
+
# views share the wrapper's additional_properties dict, which is
|
|
1172
|
+
# how compaction exclusion flags reach stored history.
|
|
1173
|
+
wire_view = [_wire_message_view(m) for m in prepped]
|
|
1174
|
+
return self._inner.get_response(
|
|
1175
|
+
wire_view,
|
|
1176
|
+
stream=as_stream,
|
|
1177
|
+
stream_update_filter=stream_update_filter,
|
|
1178
|
+
request_message_observer=request_message_observer,
|
|
1179
|
+
options=self._options,
|
|
1180
|
+
middleware=self._chat_middleware,
|
|
1181
|
+
compaction_strategy=self._compaction_strategy,
|
|
1182
|
+
tokenizer=self._tokenizer,
|
|
1183
|
+
client_kwargs=self._trajectory.begin_exchange(self._client_kwargs),
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
async def _schedule_retry(
|
|
1187
|
+
self,
|
|
1188
|
+
policy: WireRetryPolicy,
|
|
1189
|
+
exc: BaseException,
|
|
1190
|
+
*,
|
|
1191
|
+
message: str,
|
|
1192
|
+
attempt: int,
|
|
1193
|
+
max_attempts: int,
|
|
1194
|
+
delay_seconds: int | None = None,
|
|
1195
|
+
fallback_to_blocking: bool = False,
|
|
1196
|
+
retry_mode: str | None = None,
|
|
1197
|
+
) -> None:
|
|
1198
|
+
if self._options.get("continuation_token") is None:
|
|
1199
|
+
# A retry that resumes an already-created response via its
|
|
1200
|
+
# continuation token must NOT replay consumed injections: the
|
|
1201
|
+
# create that consumed them succeeded (the provider holds
|
|
1202
|
+
# them), and a poll never consumes a replay — the batch would
|
|
1203
|
+
# sit stranded until commit destroys it.
|
|
1204
|
+
policy.before_retry()
|
|
1205
|
+
delay = policy.backoff_seconds(attempt - 1) if delay_seconds is None else delay_seconds
|
|
1206
|
+
stalled = isinstance(exc, StreamStall)
|
|
1207
|
+
if retry_mode is None:
|
|
1208
|
+
retry_mode = RetryMode.STALL_FALLBACK if fallback_to_blocking else RetryMode.WIRE
|
|
1209
|
+
recorder = self._recorder
|
|
1210
|
+
await self._trajectory.retry_scheduled(
|
|
1211
|
+
reason_code=RetryReason.STREAM_STALL if stalled else RetryReason.TRANSIENT_ERROR,
|
|
1212
|
+
retry_mode=retry_mode,
|
|
1213
|
+
delay_seconds=delay,
|
|
1214
|
+
fallback_to_blocking=fallback_to_blocking,
|
|
1215
|
+
committed_work_present=recorder is not None and recorder.committed_count > 0,
|
|
1216
|
+
)
|
|
1217
|
+
await policy.on_retry(message, attempt, max_attempts, delay, exc)
|
|
1218
|
+
if policy.is_interrupted() or await policy.sleep(delay):
|
|
1219
|
+
raise asyncio.CancelledError
|
|
1220
|
+
await self._trajectory.retry_started(retry_mode=retry_mode)
|
|
1221
|
+
|
|
1222
|
+
async def _pause_between_continuation_polls(self) -> None:
|
|
1223
|
+
# Task cancellation is the interrupt channel for both service and
|
|
1224
|
+
# local runs; the policy flag check covers soft interrupts that
|
|
1225
|
+
# only set state.
|
|
1226
|
+
policy = self._policy
|
|
1227
|
+
if policy is not None and policy.is_interrupted():
|
|
1228
|
+
raise asyncio.CancelledError
|
|
1229
|
+
if CONTINUATION_POLL_INTERVAL_SECONDS > 0:
|
|
1230
|
+
await asyncio.sleep(CONTINUATION_POLL_INTERVAL_SECONDS)
|
|
1231
|
+
|
|
1232
|
+
def _hosted_commits_vetoing_replay(self, policy: WireRetryPolicy) -> tuple[str, ...]:
|
|
1233
|
+
# Without a live continuation token a retry re-creates the request
|
|
1234
|
+
# and re-runs the hosted tool calls the failed attempt already
|
|
1235
|
+
# executed server-side; with one it merely resumes the same
|
|
1236
|
+
# response, which is safe.
|
|
1237
|
+
hosted_probe = policy.hosted_commits_in_flight
|
|
1238
|
+
hosted_commits = tuple(hosted_probe()) if hosted_probe is not None else ()
|
|
1239
|
+
if hosted_commits and self._options.get("continuation_token") is None:
|
|
1240
|
+
return hosted_commits
|
|
1241
|
+
return ()
|
|
1242
|
+
|
|
1243
|
+
def _note_context_overflow(self, exc: BaseException) -> bool:
|
|
1244
|
+
# The provider measured the real input and found the window full:
|
|
1245
|
+
# the strategy compacts before the next request instead of letting
|
|
1246
|
+
# it resend the rejected input. Runs before any retry decision, so
|
|
1247
|
+
# service-side runs (no wire policy) are noted too. Returns whether
|
|
1248
|
+
# compacting and resending can help. Thinking the service refused
|
|
1249
|
+
# as bound to another conversation is no full window, even when the
|
|
1250
|
+
# refusal names it: the client resends without that thinking, or
|
|
1251
|
+
# the profile asked for the refusal.
|
|
1252
|
+
strategy = self._compaction_strategy
|
|
1253
|
+
if (
|
|
1254
|
+
not isinstance(strategy, ContextOverflowSink)
|
|
1255
|
+
or not is_context_overflow(exc)
|
|
1256
|
+
or is_thinking_binding_rejection(exc)
|
|
1257
|
+
):
|
|
1258
|
+
return False
|
|
1259
|
+
return strategy.note_context_overflow(exc)
|
|
1260
|
+
|
|
1261
|
+
def _resends_after_overflow(self, policy: WireRetryPolicy, *, noted: bool, recovered: bool) -> bool:
|
|
1262
|
+
# One in-place resend per logical call, outside the transient and
|
|
1263
|
+
# stall budgets. Service-side runs have no wire policy and only
|
|
1264
|
+
# keep the note; a live continuation token would poll the rejected
|
|
1265
|
+
# response, and hosted work an attempt already ran must not rerun.
|
|
1266
|
+
return (
|
|
1267
|
+
noted
|
|
1268
|
+
and not recovered
|
|
1269
|
+
and self._options.get("continuation_token") is None
|
|
1270
|
+
and not self._hosted_commits_vetoing_replay(policy)
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
async def _schedule_overflow_resend(self, policy: WireRetryPolicy, exc: BaseException) -> None:
|
|
1274
|
+
# The strategy holds the note, so the resend's client preparation
|
|
1275
|
+
# compacts before the request goes out.
|
|
1276
|
+
await self._schedule_retry(
|
|
1277
|
+
policy,
|
|
1278
|
+
exc,
|
|
1279
|
+
message=clean_error_message(exc),
|
|
1280
|
+
attempt=1,
|
|
1281
|
+
max_attempts=1,
|
|
1282
|
+
delay_seconds=0,
|
|
1283
|
+
retry_mode=RetryMode.CONTEXT_OVERFLOW,
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
async def blocking_response(
|
|
1287
|
+
self,
|
|
1288
|
+
prepped: list[Message],
|
|
1289
|
+
*,
|
|
1290
|
+
request_message_observer: Callable[[Sequence[Message]], None],
|
|
1291
|
+
overflow_recovered: bool = False,
|
|
1292
|
+
) -> ChatResponse:
|
|
1293
|
+
retry_attempt = 0
|
|
1294
|
+
while True:
|
|
1295
|
+
policy = self._policy
|
|
1296
|
+
if policy is not None and policy.is_interrupted():
|
|
1297
|
+
raise asyncio.CancelledError
|
|
1298
|
+
try:
|
|
1299
|
+
while True:
|
|
1300
|
+
response = await _resolve_response(
|
|
1301
|
+
await self._call(
|
|
1302
|
+
prepped,
|
|
1303
|
+
as_stream=False,
|
|
1304
|
+
request_message_observer=request_message_observer,
|
|
1305
|
+
)
|
|
1306
|
+
)
|
|
1307
|
+
self._trajectory.close_exchange(ExchangeOutcome.SUCCESS)
|
|
1308
|
+
if response.continuation_token is None:
|
|
1309
|
+
self._track_continuation_token(None)
|
|
1310
|
+
if self._injection_probe is not None:
|
|
1311
|
+
self._injection_probe.commit()
|
|
1312
|
+
return response
|
|
1313
|
+
self._track_continuation_token(response.continuation_token)
|
|
1314
|
+
await self._pause_between_continuation_polls()
|
|
1315
|
+
except asyncio.CancelledError:
|
|
1316
|
+
self._trajectory.close_exchange(self.cancel_outcome())
|
|
1317
|
+
raise
|
|
1318
|
+
except Exception as exc:
|
|
1319
|
+
self._trajectory.close_exchange(ExchangeOutcome.ERROR, exc=exc)
|
|
1320
|
+
if invalidates_continuation_token(exc):
|
|
1321
|
+
# The failure judged a terminal response: a retry must
|
|
1322
|
+
# issue a fresh request, never re-poll the completed
|
|
1323
|
+
# (immutable) one.
|
|
1324
|
+
self._track_continuation_token(None)
|
|
1325
|
+
noted = self._note_context_overflow(exc)
|
|
1326
|
+
if policy is not None and self._resends_after_overflow(
|
|
1327
|
+
policy, noted=noted, recovered=overflow_recovered
|
|
1328
|
+
):
|
|
1329
|
+
overflow_recovered = True
|
|
1330
|
+
await self._schedule_overflow_resend(policy, exc)
|
|
1331
|
+
continue
|
|
1332
|
+
if policy is None or not policy.is_retryable(exc) or retry_attempt >= policy.max_retries:
|
|
1333
|
+
raise
|
|
1334
|
+
hosted_commits = self._hosted_commits_vetoing_replay(policy)
|
|
1335
|
+
if hosted_commits:
|
|
1336
|
+
logger.warning(
|
|
1337
|
+
"Not retrying wire call: provider-hosted tool call(s) already "
|
|
1338
|
+
"executed in the failed attempt (%s)",
|
|
1339
|
+
", ".join(str(label) for label in hosted_commits),
|
|
1340
|
+
)
|
|
1341
|
+
raise
|
|
1342
|
+
retry_attempt += 1
|
|
1343
|
+
await self._schedule_retry(
|
|
1344
|
+
policy,
|
|
1345
|
+
exc,
|
|
1346
|
+
message=clean_error_message(exc),
|
|
1347
|
+
attempt=retry_attempt,
|
|
1348
|
+
max_attempts=policy.max_retries,
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
def streaming_response(
|
|
1352
|
+
self,
|
|
1353
|
+
prepped: list[Message],
|
|
1354
|
+
*,
|
|
1355
|
+
stream_update_filter: Callable[[ChatResponseUpdate], ChatResponseUpdate],
|
|
1356
|
+
request_message_observer: Callable[[Sequence[Message]], None],
|
|
1357
|
+
) -> ResponseStream[ChatResponseUpdate, ChatResponse]:
|
|
1358
|
+
final_response: ChatResponse | None = None
|
|
1359
|
+
# The provider stream of the attempt being read; None once it was
|
|
1360
|
+
# read to its end.
|
|
1361
|
+
inner_stream: ResponseStream[ChatResponseUpdate, ChatResponse] | None = None
|
|
1362
|
+
logical_stream: ResponseStream[ChatResponseUpdate, ChatResponse]
|
|
1363
|
+
|
|
1364
|
+
async def _updates() -> AsyncIterable[ChatResponseUpdate]:
|
|
1365
|
+
nonlocal final_response, inner_stream
|
|
1366
|
+
retry_attempt = 0
|
|
1367
|
+
stall_retry_attempt = 0
|
|
1368
|
+
overflow_recovered = False
|
|
1369
|
+
while True:
|
|
1370
|
+
policy = self._policy
|
|
1371
|
+
if policy is not None and policy.is_interrupted():
|
|
1372
|
+
raise asyncio.CancelledError
|
|
1373
|
+
inner_stream = None
|
|
1374
|
+
try:
|
|
1375
|
+
while True:
|
|
1376
|
+
raw_stream = await self._call(
|
|
1377
|
+
prepped,
|
|
1378
|
+
as_stream=True,
|
|
1379
|
+
stream_update_filter=stream_update_filter,
|
|
1380
|
+
request_message_observer=request_message_observer,
|
|
1381
|
+
)
|
|
1382
|
+
if not isinstance(raw_stream, ResponseStream):
|
|
1383
|
+
raise TypeError("Streaming wire call did not return a ResponseStream.")
|
|
1384
|
+
inner_stream = raw_stream
|
|
1385
|
+
inner_stream.with_update_filter(stream_update_filter)
|
|
1386
|
+
stream_usage_chunks: list[Mapping[str, Any]] = []
|
|
1387
|
+
iterator = inner_stream.__aiter__()
|
|
1388
|
+
while True:
|
|
1389
|
+
try:
|
|
1390
|
+
update = await _watchdog_await(
|
|
1391
|
+
iterator.__anext__(),
|
|
1392
|
+
policy.stall_timeout_seconds if policy is not None else None,
|
|
1393
|
+
"Streaming response",
|
|
1394
|
+
)
|
|
1395
|
+
except StopAsyncIteration:
|
|
1396
|
+
break
|
|
1397
|
+
if update.continuation_token is not None:
|
|
1398
|
+
# Providers announce the background
|
|
1399
|
+
# response id mid-stream; mirror it
|
|
1400
|
+
# immediately so a disconnect before
|
|
1401
|
+
# finalization retries by retrieval, not
|
|
1402
|
+
# by a duplicate create.
|
|
1403
|
+
self._track_continuation_token(update.continuation_token)
|
|
1404
|
+
stream_usage_chunks.extend(_stream_usage_chunks(update))
|
|
1405
|
+
yield update
|
|
1406
|
+
response = await _watchdog_await(
|
|
1407
|
+
inner_stream.get_final_response(),
|
|
1408
|
+
policy.stall_timeout_seconds if policy is not None else None,
|
|
1409
|
+
"Streaming response finalization",
|
|
1410
|
+
)
|
|
1411
|
+
inner_stream = None
|
|
1412
|
+
self._trajectory.close_exchange(ExchangeOutcome.SUCCESS)
|
|
1413
|
+
response.latest_usage_details = normalize_stream_usage(stream_usage_chunks)
|
|
1414
|
+
if response.latest_usage_details is None:
|
|
1415
|
+
response.latest_usage_details = response.usage_details
|
|
1416
|
+
if response.continuation_token is None:
|
|
1417
|
+
self._track_continuation_token(None)
|
|
1418
|
+
final_response = response
|
|
1419
|
+
if self._injection_probe is not None:
|
|
1420
|
+
self._injection_probe.commit()
|
|
1421
|
+
return
|
|
1422
|
+
self._track_continuation_token(response.continuation_token)
|
|
1423
|
+
await self._pause_between_continuation_polls()
|
|
1424
|
+
except asyncio.CancelledError:
|
|
1425
|
+
self._trajectory.close_exchange(self.cancel_outcome())
|
|
1426
|
+
if inner_stream is not None:
|
|
1427
|
+
await inner_stream.aclose()
|
|
1428
|
+
raise
|
|
1429
|
+
except Exception as exc:
|
|
1430
|
+
if isinstance(exc, StreamStall):
|
|
1431
|
+
self._trajectory.stall_observed()
|
|
1432
|
+
self._trajectory.close_exchange(ExchangeOutcome.STALLED, exc=exc)
|
|
1433
|
+
else:
|
|
1434
|
+
self._trajectory.close_exchange(ExchangeOutcome.ERROR, exc=exc)
|
|
1435
|
+
if inner_stream is not None:
|
|
1436
|
+
try:
|
|
1437
|
+
await inner_stream.aclose()
|
|
1438
|
+
except Exception:
|
|
1439
|
+
logger.debug("Failed to close abandoned provider stream", exc_info=True)
|
|
1440
|
+
if invalidates_continuation_token(exc):
|
|
1441
|
+
# The failure judged a terminal response: a retry
|
|
1442
|
+
# must issue a fresh request, never re-poll the
|
|
1443
|
+
# completed (immutable) one.
|
|
1444
|
+
self._track_continuation_token(None)
|
|
1445
|
+
noted = self._note_context_overflow(exc)
|
|
1446
|
+
if policy is None:
|
|
1447
|
+
raise
|
|
1448
|
+
hosted_commits = self._hosted_commits_vetoing_replay(policy)
|
|
1449
|
+
if hosted_commits:
|
|
1450
|
+
logger.warning(
|
|
1451
|
+
"Not replaying wire call: provider-hosted tool call(s) already "
|
|
1452
|
+
"executed in the aborted stream (%s)",
|
|
1453
|
+
", ".join(str(label) for label in hosted_commits),
|
|
1454
|
+
)
|
|
1455
|
+
raise
|
|
1456
|
+
if self._resends_after_overflow(policy, noted=noted, recovered=overflow_recovered):
|
|
1457
|
+
overflow_recovered = True
|
|
1458
|
+
await self._schedule_overflow_resend(policy, exc)
|
|
1459
|
+
logical_stream._updates.clear()
|
|
1460
|
+
yield ChatResponseUpdate.retry_boundary()
|
|
1461
|
+
continue
|
|
1462
|
+
if isinstance(exc, StreamStall):
|
|
1463
|
+
if stall_retry_attempt >= policy.stall_max_retries:
|
|
1464
|
+
if policy.stall_exhausted_action is StallExhaustedAction.RAISE:
|
|
1465
|
+
raise
|
|
1466
|
+
await self._schedule_retry(
|
|
1467
|
+
policy,
|
|
1468
|
+
exc,
|
|
1469
|
+
message="Stream stalled; retrying with a blocking response",
|
|
1470
|
+
attempt=stall_retry_attempt + 1,
|
|
1471
|
+
max_attempts=policy.stall_max_retries + 1,
|
|
1472
|
+
delay_seconds=0,
|
|
1473
|
+
fallback_to_blocking=True,
|
|
1474
|
+
)
|
|
1475
|
+
logical_stream._updates.clear()
|
|
1476
|
+
yield ChatResponseUpdate.retry_boundary()
|
|
1477
|
+
final_response = await self.blocking_response(
|
|
1478
|
+
prepped,
|
|
1479
|
+
request_message_observer=request_message_observer,
|
|
1480
|
+
overflow_recovered=overflow_recovered,
|
|
1481
|
+
)
|
|
1482
|
+
return
|
|
1483
|
+
stall_retry_attempt += 1
|
|
1484
|
+
await self._schedule_retry(
|
|
1485
|
+
policy,
|
|
1486
|
+
exc,
|
|
1487
|
+
message="Stream stalled",
|
|
1488
|
+
attempt=stall_retry_attempt,
|
|
1489
|
+
max_attempts=policy.stall_max_retries,
|
|
1490
|
+
)
|
|
1491
|
+
logical_stream._updates.clear()
|
|
1492
|
+
yield ChatResponseUpdate.retry_boundary()
|
|
1493
|
+
continue
|
|
1494
|
+
if not policy.is_retryable(exc) or retry_attempt >= policy.max_retries:
|
|
1495
|
+
raise
|
|
1496
|
+
retry_attempt += 1
|
|
1497
|
+
await self._schedule_retry(
|
|
1498
|
+
policy,
|
|
1499
|
+
exc,
|
|
1500
|
+
message=clean_error_message(exc),
|
|
1501
|
+
attempt=retry_attempt,
|
|
1502
|
+
max_attempts=policy.max_retries,
|
|
1503
|
+
)
|
|
1504
|
+
logical_stream._updates.clear()
|
|
1505
|
+
yield ChatResponseUpdate.retry_boundary()
|
|
1506
|
+
|
|
1507
|
+
def _finalizer(_updates: Sequence[ChatResponseUpdate]) -> ChatResponse:
|
|
1508
|
+
if final_response is None:
|
|
1509
|
+
raise RuntimeError("Logical streaming call ended without a final response.")
|
|
1510
|
+
return final_response
|
|
1511
|
+
|
|
1512
|
+
async def _close_abandoned_attempt() -> None:
|
|
1513
|
+
# A close of this logical call mid-response closes the provider
|
|
1514
|
+
# stream it was reading, as ``_LoopRun.close_abandoned_read`` does
|
|
1515
|
+
# one level up; one already closed or read to its end is a no-op.
|
|
1516
|
+
if inner_stream is not None:
|
|
1517
|
+
try:
|
|
1518
|
+
await inner_stream.aclose()
|
|
1519
|
+
except Exception:
|
|
1520
|
+
logger.debug("Failed to close abandoned provider stream", exc_info=True)
|
|
1521
|
+
|
|
1522
|
+
logical_stream = ResponseStream(_updates(), finalizer=_finalizer).with_cleanup_hook(_close_abandoned_attempt)
|
|
1523
|
+
return logical_stream
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
class _LoopRun:
|
|
1527
|
+
"""One ``ToolLoopLayer.get_response`` run: the two loop drivers and the steps they share.
|
|
1528
|
+
|
|
1529
|
+
``run_blocking`` and ``stream`` (assembled by ``finalize_stream``) drive
|
|
1530
|
+
the same iterations: send the prepped messages, land the response, close
|
|
1531
|
+
its cycle, run the landed calls as one batch and queue the next request —
|
|
1532
|
+
until no local call is left, or the iteration budget runs out and a final
|
|
1533
|
+
request without tools ends the run. They differ in how usage is counted
|
|
1534
|
+
and how a terminating batch ends the run; the streaming driver also
|
|
1535
|
+
yields between steps and withholds service-side handles ahead of the
|
|
1536
|
+
yields a consumer may stop at. Every step the two drive identically is a
|
|
1537
|
+
method here.
|
|
1538
|
+
|
|
1539
|
+
The run's transcript state — echo registry, prepped and accumulated loop
|
|
1540
|
+
messages, tool ordinal, error and call counters, aggregated usage — lives
|
|
1541
|
+
for the run; the caller's history is read when a driver starts. Only the
|
|
1542
|
+
streaming driver writes ``_latest_usage``, ``_final_messages`` and
|
|
1543
|
+
``_service_state_invalidated``, which ``finalize_stream`` reads, and
|
|
1544
|
+
``_reading``, which ``close_abandoned_read`` reads and clears.
|
|
1545
|
+
|
|
1546
|
+
Writes outside the run: ``tool_choice`` in the run's options, and
|
|
1547
|
+
``tools`` through each tool batch (normalized per batch, so the next
|
|
1548
|
+
request offers the live list); the recorder's journal (landed responses,
|
|
1549
|
+
staged exchanges and their sealed results); the injection probe's drain;
|
|
1550
|
+
continuation state in the client kwargs, options and session through
|
|
1551
|
+
``_update_continuation_state`` and ``_invalidate_service_continuation_state``;
|
|
1552
|
+
and the streaming driver's two session pre-clears. ``_WireCaller`` writes
|
|
1553
|
+
the options' ``continuation_token``.
|
|
1554
|
+
"""
|
|
1555
|
+
|
|
1556
|
+
def __init__(
|
|
1557
|
+
self,
|
|
1558
|
+
messages: Sequence[Message],
|
|
1559
|
+
*,
|
|
1560
|
+
options: dict[str, Any],
|
|
1561
|
+
client_kwargs: dict[str, Any],
|
|
1562
|
+
service_side: bool,
|
|
1563
|
+
session: AgentSession | None,
|
|
1564
|
+
recorder: LoopRecorder | None,
|
|
1565
|
+
injection_probe: ConsumedInjectionMessageProbe | None,
|
|
1566
|
+
pipeline: FunctionMiddlewarePipeline,
|
|
1567
|
+
additional_function_arguments: dict[str, Any],
|
|
1568
|
+
request_message_observer: Callable[[Sequence[Message]], None] | None,
|
|
1569
|
+
max_iterations: int,
|
|
1570
|
+
max_consecutive_errors: int,
|
|
1571
|
+
max_function_calls: int | None,
|
|
1572
|
+
tool_result_ceiling_tokens: int | None,
|
|
1573
|
+
trajectory: _LoopTrajectory,
|
|
1574
|
+
wire: _WireCaller,
|
|
1575
|
+
) -> None:
|
|
1576
|
+
self._messages = messages
|
|
1577
|
+
self._options = options
|
|
1578
|
+
self._client_kwargs = client_kwargs
|
|
1579
|
+
self._service_side = service_side
|
|
1580
|
+
self._session = session
|
|
1581
|
+
self._recorder = recorder
|
|
1582
|
+
self._injection_probe = injection_probe
|
|
1583
|
+
self._pipeline = pipeline
|
|
1584
|
+
self._additional_function_arguments = additional_function_arguments
|
|
1585
|
+
self._max_iterations = max_iterations
|
|
1586
|
+
self._max_consecutive_errors = max_consecutive_errors
|
|
1587
|
+
self._max_function_calls = max_function_calls
|
|
1588
|
+
self._tool_result_ceiling_tokens = tool_result_ceiling_tokens
|
|
1589
|
+
self._trajectory = trajectory
|
|
1590
|
+
self._wire = wire
|
|
1591
|
+
self._response_format = options.get("response_format")
|
|
1592
|
+
# Per-run identity registry of every content the conversation
|
|
1593
|
+
# holds — seeded from caller history so a client echoing a
|
|
1594
|
+
# historical object cannot re-execute a past side-effecting
|
|
1595
|
+
# call; _land_response_contents removes reappearances as
|
|
1596
|
+
# echoes. ignore_usage: echo filtering never strips usage.
|
|
1597
|
+
self._echo_registry = WeakIdentityRegistry(ignore_usage=True)
|
|
1598
|
+
self._wire_request_observer = _wire_request_observer(self._echo_registry, request_message_observer)
|
|
1599
|
+
self._prepped_messages: list[Message] = []
|
|
1600
|
+
self._fcc_messages: list[Message] = []
|
|
1601
|
+
self._next_tool_ordinal = 0
|
|
1602
|
+
self._errors_in_a_row = 0
|
|
1603
|
+
self._total_function_calls = 0
|
|
1604
|
+
self._aggregated_usage: UsageDetails | None = None
|
|
1605
|
+
self._latest_usage: UsageDetails | None = None
|
|
1606
|
+
# Authoritative streamed transcript, recorded by each loop exit: the final response
|
|
1607
|
+
# reuses these already-assembled Message objects instead of re-merging
|
|
1608
|
+
# raw updates, so kernel-stamped call provenance survives streaming.
|
|
1609
|
+
# ``None`` means no loop exit completed — finalize_stream keeps the
|
|
1610
|
+
# ``from_updates`` merge as a defensive fallback.
|
|
1611
|
+
self._final_messages: list[Message] | None = None
|
|
1612
|
+
# Set by the streaming exits that withhold a service-stored response's
|
|
1613
|
+
# handles (termination, exhaustion strip): finalize_stream rebuilds
|
|
1614
|
+
# the response from raw updates, which still carry those continuation
|
|
1615
|
+
# ids, so it withholds them again.
|
|
1616
|
+
self._service_state_invalidated = False
|
|
1617
|
+
# The logical call the streaming driver is reading; None once it was
|
|
1618
|
+
# read to its end.
|
|
1619
|
+
self._reading: ResponseStream[ChatResponseUpdate, ChatResponse] | None = None
|
|
1620
|
+
|
|
1621
|
+
# -- steps both drivers share ------------------------------------------
|
|
1622
|
+
|
|
1623
|
+
def _start(self) -> None:
|
|
1624
|
+
for message in self._messages:
|
|
1625
|
+
for content in message.contents:
|
|
1626
|
+
self._echo_registry.register(content)
|
|
1627
|
+
self._prepped_messages = list(self._messages)
|
|
1628
|
+
|
|
1629
|
+
def _land(self, response: ChatResponse) -> None:
|
|
1630
|
+
self._next_tool_ordinal = _land_response_contents(
|
|
1631
|
+
response,
|
|
1632
|
+
self._next_tool_ordinal,
|
|
1633
|
+
self._echo_registry,
|
|
1634
|
+
exchange_operation_id=self._trajectory.last_exchange_id,
|
|
1635
|
+
)
|
|
1636
|
+
|
|
1637
|
+
def _take_consumed_injections(self) -> list[Message]:
|
|
1638
|
+
return self._injection_probe.take_consumed_messages() if self._injection_probe else []
|
|
1639
|
+
|
|
1640
|
+
def _record_response_state(self, response: ChatResponse) -> None:
|
|
1641
|
+
_update_continuation_state(
|
|
1642
|
+
self._client_kwargs,
|
|
1643
|
+
response,
|
|
1644
|
+
session=self._session,
|
|
1645
|
+
options=self._options,
|
|
1646
|
+
)
|
|
1647
|
+
if self._recorder is not None:
|
|
1648
|
+
self._recorder.record_response(response)
|
|
1649
|
+
|
|
1650
|
+
def _queue_consumed_injections(self, response: ChatResponse, consumed_injection_messages: list[Message]) -> None:
|
|
1651
|
+
if response.conversation_id is not None:
|
|
1652
|
+
self._prepped_messages = []
|
|
1653
|
+
else:
|
|
1654
|
+
self._prepped_messages.extend(consumed_injection_messages)
|
|
1655
|
+
|
|
1656
|
+
async def _finish_cycle(self, response: ChatResponse) -> list[Content]:
|
|
1657
|
+
function_calls = _extract_function_calls(response)
|
|
1658
|
+
await self._trajectory.cycle_finished(response, function_calls)
|
|
1659
|
+
return function_calls
|
|
1660
|
+
|
|
1661
|
+
async def _run_tool_batch(
|
|
1662
|
+
self, response: ChatResponse, function_calls: list[Content]
|
|
1663
|
+
) -> tuple[list[Content], bool, str]:
|
|
1664
|
+
"""Run the landed calls as one batch and fold their results into the loop messages.
|
|
1665
|
+
|
|
1666
|
+
Returns the results, whether middleware terminated the loop, and
|
|
1667
|
+
``"stop"`` when the error streak asks for a final turn without tools.
|
|
1668
|
+
"""
|
|
1669
|
+
result_carrier_item_id = new_analytics_id()
|
|
1670
|
+
result_commits = (
|
|
1671
|
+
self._recorder.stage_exchange(
|
|
1672
|
+
response.messages,
|
|
1673
|
+
function_calls,
|
|
1674
|
+
result_carrier_item_id=result_carrier_item_id,
|
|
1675
|
+
)
|
|
1676
|
+
if self._recorder is not None
|
|
1677
|
+
else None
|
|
1678
|
+
)
|
|
1679
|
+
response_truncated = response.finish_reason == "length"
|
|
1680
|
+
with self._trajectory.exchange_scope():
|
|
1681
|
+
results, should_terminate, had_errors = await _execute_function_calls(
|
|
1682
|
+
function_calls=function_calls,
|
|
1683
|
+
result_commits=result_commits,
|
|
1684
|
+
tool_options=self._options,
|
|
1685
|
+
custom_args=self._additional_function_arguments,
|
|
1686
|
+
invocation_session=self._session,
|
|
1687
|
+
pipeline=self._pipeline,
|
|
1688
|
+
result_carrier_item_id=result_carrier_item_id,
|
|
1689
|
+
response_truncated=response_truncated,
|
|
1690
|
+
truncated_final_function_call_ids=(
|
|
1691
|
+
_truncated_final_function_call_ids(response, function_calls) if response_truncated else None
|
|
1692
|
+
),
|
|
1693
|
+
tool_result_ceiling_tokens=self._tool_result_ceiling_tokens,
|
|
1694
|
+
dispatch_observer=self._trajectory.mark_tool_operations_dispatched,
|
|
1695
|
+
)
|
|
1696
|
+
action, self._errors_in_a_row = _handle_function_call_results(
|
|
1697
|
+
response=response,
|
|
1698
|
+
function_call_results=results,
|
|
1699
|
+
fcc_messages=self._fcc_messages,
|
|
1700
|
+
echo_registry=self._echo_registry,
|
|
1701
|
+
errors_in_a_row=self._errors_in_a_row,
|
|
1702
|
+
had_errors=had_errors,
|
|
1703
|
+
max_errors=self._max_consecutive_errors,
|
|
1704
|
+
result_carrier_item_id=result_carrier_item_id,
|
|
1705
|
+
recorder=self._recorder,
|
|
1706
|
+
)
|
|
1707
|
+
self._total_function_calls += sum(1 for r in results if r.type == "function_result")
|
|
1708
|
+
return results, should_terminate, action
|
|
1709
|
+
|
|
1710
|
+
def _tool_limit_fallback_text(self, action: str) -> str | None:
|
|
1711
|
+
"""Return the final turn's fallback text when the batch just run hit a tool limit, else None.
|
|
1712
|
+
|
|
1713
|
+
A hit limit ends the loop like iteration exhaustion: the collected
|
|
1714
|
+
results go out once more in the final turn without tools, whose calls
|
|
1715
|
+
are stripped, never run — a provider ignoring ``tool_choice="none"``
|
|
1716
|
+
cannot get another batch executed.
|
|
1717
|
+
"""
|
|
1718
|
+
if action == "stop":
|
|
1719
|
+
return _CONSECUTIVE_ERRORS_FALLBACK_TEXT
|
|
1720
|
+
if self._max_function_calls is not None and self._total_function_calls >= self._max_function_calls:
|
|
1721
|
+
# Best-effort limit, checked after each parallel batch.
|
|
1722
|
+
logger.info(
|
|
1723
|
+
"Maximum function calls reached (%d/%d). Stopping further function calls for this request.",
|
|
1724
|
+
self._total_function_calls,
|
|
1725
|
+
self._max_function_calls,
|
|
1726
|
+
)
|
|
1727
|
+
return _MAX_FUNCTION_CALLS_FALLBACK_TEXT
|
|
1728
|
+
return None
|
|
1729
|
+
|
|
1730
|
+
def _reset_required_tool_choice(self) -> None:
|
|
1731
|
+
# 'required' tool_choice resets after one iteration.
|
|
1732
|
+
if self._options.get("tool_choice") == "required" or (
|
|
1733
|
+
isinstance(self._options.get("tool_choice"), dict)
|
|
1734
|
+
and self._options.get("tool_choice", {}).get("mode") == "required"
|
|
1735
|
+
):
|
|
1736
|
+
self._options["tool_choice"] = None
|
|
1737
|
+
|
|
1738
|
+
def _queue_batch(self, response: ChatResponse) -> None:
|
|
1739
|
+
if response.conversation_id is not None:
|
|
1740
|
+
# Conversation APIs already hold the function-call message;
|
|
1741
|
+
# send only the new result message.
|
|
1742
|
+
self._prepped_messages.clear()
|
|
1743
|
+
if response.messages:
|
|
1744
|
+
self._prepped_messages.append(response.messages[-1])
|
|
1745
|
+
else:
|
|
1746
|
+
self._prepped_messages.extend(response.messages)
|
|
1747
|
+
|
|
1748
|
+
def _log_iterations_exhausted(self, response: ChatResponse | None) -> None:
|
|
1749
|
+
if response is not None:
|
|
1750
|
+
logger.info(
|
|
1751
|
+
"Maximum iterations reached (%d). Requesting final response without tools.",
|
|
1752
|
+
self._max_iterations,
|
|
1753
|
+
)
|
|
1754
|
+
|
|
1755
|
+
# -- blocking driver ----------------------------------------------------
|
|
1756
|
+
|
|
1757
|
+
async def run_blocking(self) -> ChatResponse:
|
|
1758
|
+
interrupted = False
|
|
1759
|
+
try:
|
|
1760
|
+
return await self._blocking_iterations()
|
|
1761
|
+
except asyncio.CancelledError:
|
|
1762
|
+
# Nothing below may wait on the writer once the consumer
|
|
1763
|
+
# is gone: the settlement queues its lines instead.
|
|
1764
|
+
interrupted = True
|
|
1765
|
+
self._trajectory.abort(self._wire.cancel_outcome())
|
|
1766
|
+
raise
|
|
1767
|
+
except Exception:
|
|
1768
|
+
self._trajectory.abort(ExchangeOutcome.ERROR)
|
|
1769
|
+
raise
|
|
1770
|
+
finally:
|
|
1771
|
+
await self._trajectory.settle_undispatched_tool_operations(queued=interrupted)
|
|
1772
|
+
|
|
1773
|
+
async def _blocking_iterations(self) -> ChatResponse:
|
|
1774
|
+
self._start()
|
|
1775
|
+
response: ChatResponse | None = None
|
|
1776
|
+
tail_cycle_index = self._max_iterations
|
|
1777
|
+
fallback_text = _MAX_ITERATIONS_FALLBACK_TEXT
|
|
1778
|
+
|
|
1779
|
+
for cycle_index in range(self._max_iterations):
|
|
1780
|
+
await self._trajectory.cycle_started(cycle_index, tools_offered=bool(self._options.get("tools")))
|
|
1781
|
+
response = await self._wire.blocking_response(
|
|
1782
|
+
self._prepped_messages,
|
|
1783
|
+
request_message_observer=self._wire_request_observer,
|
|
1784
|
+
)
|
|
1785
|
+
self._land(response)
|
|
1786
|
+
consumed_injection_messages = self._take_consumed_injections()
|
|
1787
|
+
response.latest_usage_details = response.usage_details
|
|
1788
|
+
self._aggregated_usage = add_usage_details(self._aggregated_usage, response.usage_details)
|
|
1789
|
+
self._record_response_state(response)
|
|
1790
|
+
self._queue_consumed_injections(response, consumed_injection_messages)
|
|
1791
|
+
|
|
1792
|
+
function_calls = await self._finish_cycle(response)
|
|
1793
|
+
if not (function_calls and self._options.get("tools")):
|
|
1794
|
+
_prepend_fcc_messages(response, self._fcc_messages)
|
|
1795
|
+
response.usage_details = self._aggregated_usage
|
|
1796
|
+
return _clear_internal_conversation_id(response)
|
|
1797
|
+
|
|
1798
|
+
_results, should_terminate, action = await self._run_tool_batch(response, function_calls)
|
|
1799
|
+
if should_terminate:
|
|
1800
|
+
# Middleware termination: return the current response
|
|
1801
|
+
# (tool results already appended) without an fcc
|
|
1802
|
+
# prepend. Streaming termination instead returns the
|
|
1803
|
+
# accumulated transcript because earlier updates were delivered.
|
|
1804
|
+
if self._service_side:
|
|
1805
|
+
# No further request ever posts this batch's
|
|
1806
|
+
# results, so the service transcript behind the
|
|
1807
|
+
# mirrored handle keeps its calls unanswered.
|
|
1808
|
+
# Record and withhold the handles like the
|
|
1809
|
+
# exhaustion strip does; the marker lets the
|
|
1810
|
+
# agent post-hook install the local fallback.
|
|
1811
|
+
_invalidate_service_continuation_state(response, self._session)
|
|
1812
|
+
response.usage_details = self._aggregated_usage
|
|
1813
|
+
return _clear_internal_conversation_id(response)
|
|
1814
|
+
self._queue_batch(response)
|
|
1815
|
+
limit_text = self._tool_limit_fallback_text(action)
|
|
1816
|
+
if limit_text is not None:
|
|
1817
|
+
tail_cycle_index, fallback_text = cycle_index + 1, limit_text
|
|
1818
|
+
break
|
|
1819
|
+
self._reset_required_tool_choice()
|
|
1820
|
+
else:
|
|
1821
|
+
self._log_iterations_exhausted(response)
|
|
1822
|
+
|
|
1823
|
+
# Loop exhausted or a tool limit hit: final model call with
|
|
1824
|
+
# tool_choice="none" so the model produces plain text instead of
|
|
1825
|
+
# orphaned function calls.
|
|
1826
|
+
self._options["tool_choice"] = "none"
|
|
1827
|
+
await self._trajectory.cycle_started(tail_cycle_index, tools_offered=bool(self._options.get("tools")))
|
|
1828
|
+
response = await self._wire.blocking_response(
|
|
1829
|
+
self._prepped_messages,
|
|
1830
|
+
request_message_observer=self._wire_request_observer,
|
|
1831
|
+
)
|
|
1832
|
+
self._land(response)
|
|
1833
|
+
# Counted before the strip below: a provider that ignored
|
|
1834
|
+
# ``tool_choice="none"`` did ask for calls, and a tail that
|
|
1835
|
+
# reported none would read as a compliant one.
|
|
1836
|
+
await self._finish_cycle(response)
|
|
1837
|
+
stripped = _strip_unexecutable_calls_from_response(response)
|
|
1838
|
+
_ensure_exhaustion_fallback_response(response, fallback_text)
|
|
1839
|
+
# Gate on the resolved storage mode, not response metadata: under
|
|
1840
|
+
# conversation storage the service holds the stripped calls even
|
|
1841
|
+
# when the parsed response failed to carry the handle, and a
|
|
1842
|
+
# client-side-storage response's metadata must survive untouched.
|
|
1843
|
+
if stripped and self._service_side:
|
|
1844
|
+
_invalidate_service_continuation_state(response, self._session)
|
|
1845
|
+
self._take_consumed_injections()
|
|
1846
|
+
response.latest_usage_details = response.usage_details
|
|
1847
|
+
self._aggregated_usage = add_usage_details(self._aggregated_usage, response.usage_details)
|
|
1848
|
+
self._record_response_state(response)
|
|
1849
|
+
response.usage_details = self._aggregated_usage
|
|
1850
|
+
_prepend_fcc_messages(response, self._fcc_messages)
|
|
1851
|
+
return _clear_internal_conversation_id(response)
|
|
1852
|
+
|
|
1853
|
+
# -- streaming driver ---------------------------------------------------
|
|
1854
|
+
|
|
1855
|
+
def _count_streamed_usage(self, response: ChatResponse) -> None:
|
|
1856
|
+
latest_usage = response.latest_usage_details
|
|
1857
|
+
if latest_usage is None:
|
|
1858
|
+
latest_usage = response.usage_details
|
|
1859
|
+
response.latest_usage_details = latest_usage
|
|
1860
|
+
self._latest_usage = latest_usage
|
|
1861
|
+
if latest_usage is not None:
|
|
1862
|
+
self._aggregated_usage = add_usage_details(self._aggregated_usage, latest_usage)
|
|
1863
|
+
|
|
1864
|
+
async def stream(self) -> AsyncIterable[ChatResponseUpdate]:
|
|
1865
|
+
# One generator end to end: every exit but the normal return closes
|
|
1866
|
+
# the trajectory bookkeeping it opened. A consumer abandoning the
|
|
1867
|
+
# stream lands here as GeneratorExit, an interrupt as CancelledError.
|
|
1868
|
+
# Delegating the body to an inner generator would lose the
|
|
1869
|
+
# GeneratorExit: closing this generator does not close one it is
|
|
1870
|
+
# iterating, so the inner body would see it only at garbage collection.
|
|
1871
|
+
# For the same reason the logical call it is reading is closed apart,
|
|
1872
|
+
# by ``close_abandoned_read``.
|
|
1873
|
+
interrupted = False
|
|
1874
|
+
try:
|
|
1875
|
+
self._start()
|
|
1876
|
+
response: ChatResponse | None = None
|
|
1877
|
+
tail_cycle_index = self._max_iterations
|
|
1878
|
+
fallback_text = _MAX_ITERATIONS_FALLBACK_TEXT
|
|
1879
|
+
|
|
1880
|
+
for cycle_index in range(self._max_iterations):
|
|
1881
|
+
await self._trajectory.cycle_started(cycle_index, tools_offered=bool(self._options.get("tools")))
|
|
1882
|
+
# Echoed conversation-held objects must never reach update
|
|
1883
|
+
# assembly (merges would launder their identity past the
|
|
1884
|
+
# memo). Delivered on the REQUEST path so the middleware
|
|
1885
|
+
# pipeline attaches it to the stream its final handler
|
|
1886
|
+
# resolves — beneath every middleware, which is the only
|
|
1887
|
+
# placement that crosses semantic stream proxies (response
|
|
1888
|
+
# validation drains the inner stream and replays it) and
|
|
1889
|
+
# re-attaches on every validation retry. The provider
|
|
1890
|
+
# finalizer, every result hook, and the yielded updates all
|
|
1891
|
+
# observe one echo-free sequence, so nothing is re-assembled
|
|
1892
|
+
# behind a hook's back.
|
|
1893
|
+
echo_filter = functools.partial(
|
|
1894
|
+
_strip_echoed_update,
|
|
1895
|
+
echo_registry=self._echo_registry,
|
|
1896
|
+
)
|
|
1897
|
+
logical_stream = self._wire.streaming_response(
|
|
1898
|
+
self._prepped_messages,
|
|
1899
|
+
stream_update_filter=echo_filter,
|
|
1900
|
+
request_message_observer=self._wire_request_observer,
|
|
1901
|
+
)
|
|
1902
|
+
self._reading = logical_stream
|
|
1903
|
+
async for update in logical_stream:
|
|
1904
|
+
yield update
|
|
1905
|
+
self._reading = None
|
|
1906
|
+
# Triggers the inner stream's finalizer and result hooks (the
|
|
1907
|
+
# wire client's intermediate-text hook runs before tool
|
|
1908
|
+
# extraction below — "hook before tool detection" ordering).
|
|
1909
|
+
response = await logical_stream.get_final_response()
|
|
1910
|
+
_remove_echo_emptied_message_shells(response)
|
|
1911
|
+
self._land(response)
|
|
1912
|
+
_record_stream_fragment_identities(logical_stream, self._echo_registry)
|
|
1913
|
+
self._count_streamed_usage(response)
|
|
1914
|
+
consumed_injection_messages = self._take_consumed_injections()
|
|
1915
|
+
self._record_response_state(response)
|
|
1916
|
+
self._queue_consumed_injections(response, consumed_injection_messages)
|
|
1917
|
+
|
|
1918
|
+
# Continue only for unresolved local function calls.
|
|
1919
|
+
function_calls = await self._finish_cycle(response)
|
|
1920
|
+
if not (function_calls and self._options.get("tools")):
|
|
1921
|
+
self._final_messages = [*self._fcc_messages, *response.messages]
|
|
1922
|
+
return
|
|
1923
|
+
|
|
1924
|
+
results, should_terminate, action = await self._run_tool_batch(response, function_calls)
|
|
1925
|
+
limit_text = None if should_terminate else self._tool_limit_fallback_text(action)
|
|
1926
|
+
if should_terminate and self._service_side:
|
|
1927
|
+
# No further request ever posts this batch's results, so
|
|
1928
|
+
# the service transcript behind the mirrored handle keeps
|
|
1929
|
+
# its calls unanswered. Record and withhold the handles
|
|
1930
|
+
# like the exhaustion strip does — ahead of the
|
|
1931
|
+
# last-iteration clear below, which would drop the
|
|
1932
|
+
# session's copy unrecorded — and raise the run flag
|
|
1933
|
+
# so ``finalize_stream`` stamps the verdict on the
|
|
1934
|
+
# assembled response for the agent post-hook.
|
|
1935
|
+
self._service_state_invalidated = True
|
|
1936
|
+
_invalidate_service_continuation_state(response, self._session)
|
|
1937
|
+
elif (
|
|
1938
|
+
(limit_text is not None or cycle_index + 1 == self._max_iterations)
|
|
1939
|
+
and self._service_side
|
|
1940
|
+
and self._session is not None
|
|
1941
|
+
):
|
|
1942
|
+
# Last batch (iterations exhausted or a tool limit hit):
|
|
1943
|
+
# the yield below is the final suspension point before
|
|
1944
|
+
# the exhaustion tail, and these results are not posted
|
|
1945
|
+
# to the service until the tail request lands.
|
|
1946
|
+
# A consumer closing at that yield must not inherit the
|
|
1947
|
+
# mirrored handle — the service transcript behind it still
|
|
1948
|
+
# holds this batch's unanswered calls. The finalized tail
|
|
1949
|
+
# restores the fresh handle via
|
|
1950
|
+
# ``_update_continuation_state``.
|
|
1951
|
+
self._session.service_session_id = None
|
|
1952
|
+
# Synthesized tool-result update so from_updates can rebuild the
|
|
1953
|
+
# full transcript.
|
|
1954
|
+
yield ChatResponseUpdate(contents=results, role="tool")
|
|
1955
|
+
if should_terminate:
|
|
1956
|
+
# This batch (calls + results) was already folded into
|
|
1957
|
+
# _fcc_messages by _handle_function_call_results, so the
|
|
1958
|
+
# assembly is the accumulated transcript with no tail.
|
|
1959
|
+
# Deliberate asymmetry with the non-streaming termination
|
|
1960
|
+
# return (only the terminating response, no prepend):
|
|
1961
|
+
# each path keeps its existing transcript shape.
|
|
1962
|
+
self._final_messages = list(self._fcc_messages)
|
|
1963
|
+
return
|
|
1964
|
+
self._queue_batch(response)
|
|
1965
|
+
if limit_text is not None:
|
|
1966
|
+
tail_cycle_index, fallback_text = cycle_index + 1, limit_text
|
|
1967
|
+
break
|
|
1968
|
+
self._reset_required_tool_choice()
|
|
1969
|
+
else:
|
|
1970
|
+
self._log_iterations_exhausted(response)
|
|
1971
|
+
|
|
1972
|
+
# Loop exhausted or a tool limit hit: final non-tool streaming turn.
|
|
1973
|
+
self._options["tool_choice"] = "none"
|
|
1974
|
+
if self._service_side and self._session is not None:
|
|
1975
|
+
# The tail request consumes the mirrored handle: the moment it
|
|
1976
|
+
# lands, the service transcript behind that handle holds this
|
|
1977
|
+
# run's still-unanswered calls, so a consumer abandoning the
|
|
1978
|
+
# tail must not inherit it (the tail updates below are marked
|
|
1979
|
+
# against eager re-mirroring for the same reason). Successful
|
|
1980
|
+
# finalization restores the fresh handle via
|
|
1981
|
+
# ``_update_continuation_state`` — or withholds it when the
|
|
1982
|
+
# strip invalidates the service state.
|
|
1983
|
+
self._session.service_session_id = None
|
|
1984
|
+
echo_filter = functools.partial(
|
|
1985
|
+
_strip_echoed_update,
|
|
1986
|
+
echo_registry=self._echo_registry,
|
|
1987
|
+
)
|
|
1988
|
+
await self._trajectory.cycle_started(tail_cycle_index, tools_offered=bool(self._options.get("tools")))
|
|
1989
|
+
final_logical_stream = self._wire.streaming_response(
|
|
1990
|
+
self._prepped_messages,
|
|
1991
|
+
stream_update_filter=echo_filter,
|
|
1992
|
+
request_message_observer=self._wire_request_observer,
|
|
1993
|
+
)
|
|
1994
|
+
tail_stripped_call = False
|
|
1995
|
+
self._reading = final_logical_stream
|
|
1996
|
+
async for update in final_logical_stream:
|
|
1997
|
+
kept = _strip_unexecutable_calls_from_update(update, suppress_finish_reason=tail_stripped_call)
|
|
1998
|
+
if kept is not update:
|
|
1999
|
+
# A call was stripped: from here on no provider
|
|
2000
|
+
# finish_reason may cross the stream — providers commonly
|
|
2001
|
+
# emit the call delta and the finish-reason chunk
|
|
2002
|
+
# separately, and a first-terminal consumer would stop at
|
|
2003
|
+
# that later chunk before the corrective final update
|
|
2004
|
+
# re-emits the corrected reason.
|
|
2005
|
+
tail_stripped_call = True
|
|
2006
|
+
if kept is not None:
|
|
2007
|
+
# Ephemeral marker (never serialized): eager continuation
|
|
2008
|
+
# mirrors must skip tail updates. The run is ending, so a
|
|
2009
|
+
# mid-stream mirror has no disconnect-recovery value left,
|
|
2010
|
+
# and a consumer abandoning the stream mid-tail would
|
|
2011
|
+
# otherwise keep the session pointed at a service
|
|
2012
|
+
# transcript whose stripped calls will never be answered;
|
|
2013
|
+
# the tail's own verdict (post-hook restore or
|
|
2014
|
+
# invalidation) is the sole writer once the stream ends.
|
|
2015
|
+
kept.__dict__["_chrys_exhaustion_tail_update"] = True
|
|
2016
|
+
yield kept
|
|
2017
|
+
self._reading = None
|
|
2018
|
+
final_response = await final_logical_stream.get_final_response()
|
|
2019
|
+
_remove_echo_emptied_message_shells(final_response)
|
|
2020
|
+
self._land(final_response)
|
|
2021
|
+
await self._finish_cycle(final_response)
|
|
2022
|
+
_record_stream_fragment_identities(final_logical_stream, self._echo_registry)
|
|
2023
|
+
stripped = _strip_unexecutable_calls_from_response(final_response)
|
|
2024
|
+
fallback_added = _ensure_exhaustion_fallback_response(final_response, fallback_text)
|
|
2025
|
+
# Invalidate BEFORE the fallback yield: a yield suspends the
|
|
2026
|
+
# generator, and a consumer that stops right after the fallback
|
|
2027
|
+
# would otherwise leave the session pointing at the stale service
|
|
2028
|
+
# transcript (the eager transform already wrote it mid-stream).
|
|
2029
|
+
# Gate on the resolved storage mode, not response metadata: under
|
|
2030
|
+
# conversation storage the service holds the stripped calls even
|
|
2031
|
+
# when the parsed response failed to carry the handle, and a
|
|
2032
|
+
# client-side-storage response's metadata must survive untouched.
|
|
2033
|
+
if stripped and self._service_side:
|
|
2034
|
+
self._service_state_invalidated = True
|
|
2035
|
+
_invalidate_service_continuation_state(final_response, self._session)
|
|
2036
|
+
if fallback_added:
|
|
2037
|
+
# Streamed consumers saw no visible content either (the yield
|
|
2038
|
+
# loop strips the same calls); surface the fallback text there
|
|
2039
|
+
# too, not only on the assembled response. The stripped tail
|
|
2040
|
+
# updates carry no terminal reason (the update strip clears
|
|
2041
|
+
# it), so this update supplies the stream's sole terminal
|
|
2042
|
+
# "stop" — the fallback ends the run, there is no tool work
|
|
2043
|
+
# left to route.
|
|
2044
|
+
yield ChatResponseUpdate(
|
|
2045
|
+
role="assistant",
|
|
2046
|
+
contents=[Content.from_text(fallback_text)],
|
|
2047
|
+
finish_reason="stop",
|
|
2048
|
+
)
|
|
2049
|
+
elif stripped and final_response.finish_reason:
|
|
2050
|
+
# No fallback (the final kept visible text), but the strip
|
|
2051
|
+
# suppressed every provider reason after the first stripped
|
|
2052
|
+
# call and normalized the wire's "tool_calls" to "stop":
|
|
2053
|
+
# streamed consumers need the corrected signal — whatever
|
|
2054
|
+
# reason the assembled response settled on, "stop" or a
|
|
2055
|
+
# non-stop reason like "length" — and it must arrive only
|
|
2056
|
+
# after every stripped update.
|
|
2057
|
+
yield ChatResponseUpdate(role="assistant", contents=[], finish_reason=final_response.finish_reason)
|
|
2058
|
+
self._count_streamed_usage(final_response)
|
|
2059
|
+
self._take_consumed_injections()
|
|
2060
|
+
self._record_response_state(final_response)
|
|
2061
|
+
self._final_messages = [*self._fcc_messages, *final_response.messages]
|
|
2062
|
+
except asyncio.CancelledError:
|
|
2063
|
+
# Nothing below may wait on the writer once the consumer is
|
|
2064
|
+
# gone: the settlement queues its lines instead.
|
|
2065
|
+
interrupted = True
|
|
2066
|
+
self._trajectory.abort(self._wire.cancel_outcome())
|
|
2067
|
+
raise
|
|
2068
|
+
except GeneratorExit:
|
|
2069
|
+
interrupted = True
|
|
2070
|
+
self._trajectory.abort(ExchangeOutcome.ABANDONED)
|
|
2071
|
+
raise
|
|
2072
|
+
except Exception:
|
|
2073
|
+
self._trajectory.abort(ExchangeOutcome.ERROR)
|
|
2074
|
+
raise
|
|
2075
|
+
finally:
|
|
2076
|
+
await self._trajectory.settle_undispatched_tool_operations(queued=interrupted)
|
|
2077
|
+
|
|
2078
|
+
async def close_abandoned_read(self) -> None:
|
|
2079
|
+
"""Cleanup hook of the run's stream: close the logical call it was reading.
|
|
2080
|
+
|
|
2081
|
+
A logical call is left unread when the consumer closes the run's
|
|
2082
|
+
stream mid-response, or when the run fails between two pulls of it.
|
|
2083
|
+
On every other exit it was read to its end, or it ended by itself
|
|
2084
|
+
(error, cancellation) and closing it again has no effect. Closing it
|
|
2085
|
+
closes the provider stream under it, so that stream's own cleanup
|
|
2086
|
+
hooks (usage, telemetry) run before the close or the failure reaches
|
|
2087
|
+
the consumer. The hook runs after ``stream`` exited and aborted its
|
|
2088
|
+
trajectory, never from a ``GeneratorExit`` handler: finalization
|
|
2089
|
+
closes each abandoned generator in a task of its own, and a handler
|
|
2090
|
+
closing another generator would collide with that generator's own
|
|
2091
|
+
close.
|
|
2092
|
+
"""
|
|
2093
|
+
reading, self._reading = self._reading, None
|
|
2094
|
+
if reading is not None:
|
|
2095
|
+
try:
|
|
2096
|
+
await reading.aclose()
|
|
2097
|
+
except Exception:
|
|
2098
|
+
logger.debug("Failed to close an abandoned logical stream", exc_info=True)
|
|
2099
|
+
|
|
2100
|
+
def finalize_stream(self, updates: Sequence[ChatResponseUpdate]) -> ChatResponse:
|
|
2101
|
+
# Inner result hooks already ran via get_final_response; do not run them again.
|
|
2102
|
+
response = ChatResponse.from_updates(updates, output_format_type=self._response_format)
|
|
2103
|
+
if self._final_messages is not None:
|
|
2104
|
+
# The loop is the sole reconstruction authority: keep
|
|
2105
|
+
# ``from_updates`` for response-level fields, but reuse the
|
|
2106
|
+
# loop's assembled Message objects. Re-merging raw fragments
|
|
2107
|
+
# here would rebuild multi-fragment function calls as new
|
|
2108
|
+
# Content objects, dropping the kernel-stamped invocation
|
|
2109
|
+
# ordinal, tool kind/context, and folded result metadata —
|
|
2110
|
+
# and could glue a boundary-shaped delta onto the synthesized
|
|
2111
|
+
# tool message. Structured-output ``.value`` parses lazily
|
|
2112
|
+
# from these swapped messages; their text is identical.
|
|
2113
|
+
response.messages = list(self._final_messages)
|
|
2114
|
+
response.usage_details = self._aggregated_usage
|
|
2115
|
+
response.latest_usage_details = self._latest_usage
|
|
2116
|
+
if self._service_state_invalidated:
|
|
2117
|
+
# ``from_updates`` restored the handles from raw updates; the
|
|
2118
|
+
# run withheld them (exhaustion strip or middleware termination)
|
|
2119
|
+
# because the service-side transcript holds calls it never sees
|
|
2120
|
+
# answered.
|
|
2121
|
+
response.conversation_id = None
|
|
2122
|
+
response.response_id = None
|
|
2123
|
+
response._chrys_service_state_invalidated = True
|
|
2124
|
+
return response
|
|
2125
|
+
|
|
2126
|
+
|
|
2127
|
+
# --------------------------------------------------------------------------- #
|
|
2128
|
+
# ToolLoopLayer
|
|
2129
|
+
# --------------------------------------------------------------------------- #
|
|
2130
|
+
|
|
2131
|
+
|
|
2132
|
+
class ToolLoopLayer:
|
|
2133
|
+
"""Composition-style tool-invocation loop over an inner chat layer.
|
|
2134
|
+
|
|
2135
|
+
Constructor settings bound iteration and tool execution. Clients that
|
|
2136
|
+
need a single model call can use the inner chat layer directly.
|
|
2137
|
+
|
|
2138
|
+
Per-run state arrives via ``client_kwargs``: ``session`` (continuation
|
|
2139
|
+
bookkeeping target) and ``loop_recorder`` (chrys interrupt-recovery
|
|
2140
|
+
recorder) are popped here; everything else flows to the inner layer
|
|
2141
|
+
untouched, with continuation ids written back into the same dict between
|
|
2142
|
+
iterations.
|
|
2143
|
+
"""
|
|
2144
|
+
|
|
2145
|
+
def __init__(
|
|
2146
|
+
self,
|
|
2147
|
+
inner: ChatMiddlewareLayer,
|
|
2148
|
+
*,
|
|
2149
|
+
middleware: FunctionMiddleware | Sequence[FunctionMiddleware] | None = None,
|
|
2150
|
+
max_iterations: int = DEFAULT_MAX_ITERATIONS,
|
|
2151
|
+
max_consecutive_errors: int = DEFAULT_MAX_CONSECUTIVE_ERRORS,
|
|
2152
|
+
max_function_calls: int | None = None,
|
|
2153
|
+
tool_result_ceiling_tokens: int | None = None,
|
|
2154
|
+
) -> None:
|
|
2155
|
+
self.inner = inner
|
|
2156
|
+
split = split_middleware(middleware)
|
|
2157
|
+
if split.chat:
|
|
2158
|
+
raise TypeError(
|
|
2159
|
+
"ToolLoopLayer constructor middleware must be FunctionMiddleware; "
|
|
2160
|
+
f"chat middleware belongs to the inner ChatMiddlewareLayer, got {split.chat!r}"
|
|
2161
|
+
)
|
|
2162
|
+
self.function_middleware: list[FunctionMiddleware] = split.function
|
|
2163
|
+
self.max_iterations = max_iterations
|
|
2164
|
+
self.max_consecutive_errors = max_consecutive_errors
|
|
2165
|
+
self.max_function_calls = max_function_calls
|
|
2166
|
+
self.tool_result_ceiling_tokens = tool_result_ceiling_tokens
|
|
2167
|
+
|
|
2168
|
+
def __getattr__(self, name: str) -> Any:
|
|
2169
|
+
# Delegate unknown attributes to the inner layer so agent-facing client
|
|
2170
|
+
# surface (model, STORES_BY_DEFAULT, ...) resolves through the stack.
|
|
2171
|
+
if name == "inner":
|
|
2172
|
+
raise AttributeError(name)
|
|
2173
|
+
return getattr(self.inner, name)
|
|
2174
|
+
|
|
2175
|
+
async def aclose(self) -> None:
|
|
2176
|
+
await self.inner.aclose()
|
|
2177
|
+
|
|
2178
|
+
def get_response(
|
|
2179
|
+
self,
|
|
2180
|
+
messages: Sequence[Message],
|
|
2181
|
+
*,
|
|
2182
|
+
stream: bool = False,
|
|
2183
|
+
options: Mapping[str, Any] | None = None,
|
|
2184
|
+
middleware: ChatMiddleware | FunctionMiddleware | Sequence[ChatMiddleware | FunctionMiddleware] | None = None,
|
|
2185
|
+
compaction_strategy: Any = None,
|
|
2186
|
+
tokenizer: Any = None,
|
|
2187
|
+
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
|
2188
|
+
client_kwargs: Mapping[str, Any] | None = None,
|
|
2189
|
+
request_message_observer: Callable[[Sequence[Message]], None] | None = None,
|
|
2190
|
+
) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
|
|
2191
|
+
"""Build a lazy model/tool run.
|
|
2192
|
+
|
|
2193
|
+
``stream=False`` returns an un-awaited coroutine; ``stream=True`` returns
|
|
2194
|
+
a ``ResponseStream`` synchronously. Execution begins when the result is driven.
|
|
2195
|
+
The run's settings are resolved here; the caller's messages are read
|
|
2196
|
+
when the run starts.
|
|
2197
|
+
"""
|
|
2198
|
+
# Merge the middleware kwarg with the
|
|
2199
|
+
# client_kwargs channel, split chat vs function, pop per-run state.
|
|
2200
|
+
effective_client_kwargs = dict(client_kwargs) if client_kwargs is not None else {}
|
|
2201
|
+
if middleware is not None:
|
|
2202
|
+
existing = effective_client_kwargs.get("middleware")
|
|
2203
|
+
effective_client_kwargs["middleware"] = [
|
|
2204
|
+
*_as_middleware_list(existing),
|
|
2205
|
+
*_as_middleware_list(middleware),
|
|
2206
|
+
]
|
|
2207
|
+
runtime_split = split_middleware(effective_client_kwargs.pop("middleware", None))
|
|
2208
|
+
pipeline = FunctionMiddlewarePipeline(*self.function_middleware, *runtime_split.function)
|
|
2209
|
+
per_call_chat: list[ChatMiddleware] = runtime_split.chat
|
|
2210
|
+
|
|
2211
|
+
raw_session = effective_client_kwargs.pop("session", None)
|
|
2212
|
+
invocation_session: AgentSession | None = raw_session if isinstance(raw_session, AgentSession) else None
|
|
2213
|
+
raw_recorder = effective_client_kwargs.pop("loop_recorder", None)
|
|
2214
|
+
recorder: LoopRecorder | None = raw_recorder if isinstance(raw_recorder, LoopRecorder) else None
|
|
2215
|
+
raw_injection_probe = effective_client_kwargs.pop("consumed_injection_message_probe", None)
|
|
2216
|
+
injection_probe = (
|
|
2217
|
+
raw_injection_probe if isinstance(raw_injection_probe, ConsumedInjectionMessageProbe) else None
|
|
2218
|
+
)
|
|
2219
|
+
raw_wire_retry_policy = effective_client_kwargs.pop("wire_retry_policy", None)
|
|
2220
|
+
raw_token_observer = effective_client_kwargs.pop("continuation_token_observer", None)
|
|
2221
|
+
continuation_token_observer: Callable[[Any], None] | None = (
|
|
2222
|
+
raw_token_observer if callable(raw_token_observer) else None
|
|
2223
|
+
)
|
|
2224
|
+
raw_trajectory = effective_client_kwargs.pop(TRAJECTORY_CONTEXT_KWARG, None)
|
|
2225
|
+
trajectory: TrajectoryContext | None = raw_trajectory if isinstance(raw_trajectory, TrajectoryContext) else None
|
|
2226
|
+
|
|
2227
|
+
filtered_kwargs = effective_client_kwargs
|
|
2228
|
+
|
|
2229
|
+
additional_function_arguments = dict(function_invocation_kwargs) if function_invocation_kwargs else {}
|
|
2230
|
+
if options and (additional_opts := options.get("additional_function_arguments")):
|
|
2231
|
+
additional_function_arguments.update(additional_opts)
|
|
2232
|
+
|
|
2233
|
+
mutable_options: dict[str, Any] = dict(options) if options else {}
|
|
2234
|
+
# Tool-invocation-only key; not recognized by chat service APIs.
|
|
2235
|
+
mutable_options.pop("additional_function_arguments", None)
|
|
2236
|
+
# Run-local mutable tools list (progressive tool exposure): fresh list so
|
|
2237
|
+
# the caller's container is never mutated, same object shared with the
|
|
2238
|
+
# model and the per-batch tool map.
|
|
2239
|
+
if mutable_options.get("tools"):
|
|
2240
|
+
mutable_options["tools"] = normalize_tools(mutable_options["tools"])
|
|
2241
|
+
|
|
2242
|
+
try:
|
|
2243
|
+
stores_by_default = bool(self.STORES_BY_DEFAULT)
|
|
2244
|
+
except AttributeError:
|
|
2245
|
+
stores_by_default = False
|
|
2246
|
+
try:
|
|
2247
|
+
force_stateless = bool(self.FORCES_STATELESS)
|
|
2248
|
+
except AttributeError:
|
|
2249
|
+
force_stateless = False
|
|
2250
|
+
storage = resolve_storage_mode_and_handles(
|
|
2251
|
+
mutable_options,
|
|
2252
|
+
stores_by_default=stores_by_default,
|
|
2253
|
+
client_kwargs=filtered_kwargs,
|
|
2254
|
+
force_stateless=force_stateless,
|
|
2255
|
+
)
|
|
2256
|
+
wire_retry_policy = (
|
|
2257
|
+
cast(WireRetryPolicy, raw_wire_retry_policy)
|
|
2258
|
+
if raw_wire_retry_policy is not None and not storage.service_side
|
|
2259
|
+
else None
|
|
2260
|
+
)
|
|
2261
|
+
|
|
2262
|
+
run_trajectory = _LoopTrajectory(trajectory, retry_policy=wire_retry_policy)
|
|
2263
|
+
wire = _WireCaller(
|
|
2264
|
+
self.inner,
|
|
2265
|
+
options=mutable_options,
|
|
2266
|
+
client_kwargs=filtered_kwargs,
|
|
2267
|
+
chat_middleware=per_call_chat,
|
|
2268
|
+
compaction_strategy=compaction_strategy,
|
|
2269
|
+
tokenizer=tokenizer,
|
|
2270
|
+
policy=wire_retry_policy,
|
|
2271
|
+
recorder=recorder,
|
|
2272
|
+
injection_probe=injection_probe,
|
|
2273
|
+
continuation_token_observer=continuation_token_observer,
|
|
2274
|
+
trajectory=run_trajectory,
|
|
2275
|
+
)
|
|
2276
|
+
run = _LoopRun(
|
|
2277
|
+
messages,
|
|
2278
|
+
options=mutable_options,
|
|
2279
|
+
client_kwargs=filtered_kwargs,
|
|
2280
|
+
service_side=storage.service_side,
|
|
2281
|
+
session=invocation_session,
|
|
2282
|
+
recorder=recorder,
|
|
2283
|
+
injection_probe=injection_probe,
|
|
2284
|
+
pipeline=pipeline,
|
|
2285
|
+
additional_function_arguments=additional_function_arguments,
|
|
2286
|
+
request_message_observer=request_message_observer,
|
|
2287
|
+
max_iterations=self.max_iterations,
|
|
2288
|
+
max_consecutive_errors=self.max_consecutive_errors,
|
|
2289
|
+
max_function_calls=self.max_function_calls,
|
|
2290
|
+
tool_result_ceiling_tokens=self.tool_result_ceiling_tokens,
|
|
2291
|
+
trajectory=run_trajectory,
|
|
2292
|
+
wire=wire,
|
|
2293
|
+
)
|
|
2294
|
+
if not stream:
|
|
2295
|
+
return run.run_blocking()
|
|
2296
|
+
return ResponseStream(run.stream(), finalizer=run.finalize_stream).with_cleanup_hook(run.close_abandoned_read)
|
|
2297
|
+
|
|
2298
|
+
|
|
2299
|
+
async def _resolve_response(value: Any) -> ChatResponse:
|
|
2300
|
+
"""Resolve the inner layer's non-streaming return to a ``ChatResponse``.
|
|
2301
|
+
|
|
2302
|
+
The inner :class:`~.middleware.ChatMiddlewareLayer` returns an un-awaited
|
|
2303
|
+
coroutine for ``stream=False``; a plain client returning a response object
|
|
2304
|
+
directly is accepted too.
|
|
2305
|
+
"""
|
|
2306
|
+
if inspect.isawaitable(value):
|
|
2307
|
+
return await value
|
|
2308
|
+
return value
|