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
|
@@ -0,0 +1,1762 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Provider-neutral planning and live presentation for hosted tools."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import inspect
|
|
8
|
+
import itertools
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from collections import Counter, defaultdict, deque
|
|
12
|
+
from collections.abc import Awaitable, Callable, Collection, Mapping, Sequence
|
|
13
|
+
from copy import copy
|
|
14
|
+
from dataclasses import dataclass, field, replace
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from chrys.foundation.hosted_tools import (
|
|
18
|
+
HOSTED_TOOL_DEFAULT_RETRY_SAFETY_BY_FAMILY,
|
|
19
|
+
HOSTED_TOOL_DEFAULT_TITLE_BY_FAMILY,
|
|
20
|
+
PRESENTATION_TEXT_SEGMENT_ID_KEY,
|
|
21
|
+
HostedRetrySafety,
|
|
22
|
+
HostedToolFamily,
|
|
23
|
+
HostedToolPhase,
|
|
24
|
+
HostedToolStatus,
|
|
25
|
+
normalize_hosted_tool_status,
|
|
26
|
+
)
|
|
27
|
+
from chrys.foundation.io.result_content import extract_result_images, extract_result_text
|
|
28
|
+
from chrys.foundation.models.history_markers import HistoryMarkerKind
|
|
29
|
+
from chrys.foundation.tool_result_metadata import (
|
|
30
|
+
TOOL_ERROR_CODE_METADATA_KEY,
|
|
31
|
+
TOOL_ERROR_MESSAGE_METADATA_KEY,
|
|
32
|
+
TOOL_ERRORED_METADATA_KEY,
|
|
33
|
+
TOOL_FAILURE_TEXT_SYNTHESIZED_METADATA_KEY,
|
|
34
|
+
)
|
|
35
|
+
from chrys.kernel import AgentResponse, Content, Message
|
|
36
|
+
from chrys.kernel.exchanges import (
|
|
37
|
+
TOOL_CALL_CONTENT_TYPES,
|
|
38
|
+
TOOL_RESULT_CONTENT_TYPES,
|
|
39
|
+
EmptyIdPolicy,
|
|
40
|
+
LiveAccessor,
|
|
41
|
+
NoneIdPolicy,
|
|
42
|
+
Occurrence,
|
|
43
|
+
PairingPolicy,
|
|
44
|
+
iter_exchanges,
|
|
45
|
+
pair_results,
|
|
46
|
+
)
|
|
47
|
+
from chrys.kernel.identity import WeakIdentityRegistry
|
|
48
|
+
from chrys.kernel.images import is_image_data_uri, is_image_media_type
|
|
49
|
+
from chrys.service.agent_middleware.events.intermediate_text import intermediate_text_contents
|
|
50
|
+
|
|
51
|
+
type ContentOrder = tuple[int, int, int]
|
|
52
|
+
|
|
53
|
+
_HOSTED_CALL_TYPES = TOOL_CALL_CONTENT_TYPES - {"function_call"}
|
|
54
|
+
_HOSTED_RESULT_TYPES = TOOL_RESULT_CONTENT_TYPES - {"function_result"}
|
|
55
|
+
_TERMINAL_STATUSES = frozenset({HostedToolStatus.COMPLETED, HostedToolStatus.FAILED, HostedToolStatus.INTERRUPTED})
|
|
56
|
+
_PRESENTATION_PAIRING_POLICY = PairingPolicy(
|
|
57
|
+
call_types=TOOL_CALL_CONTENT_TYPES,
|
|
58
|
+
include_informational_calls=True,
|
|
59
|
+
result_types=TOOL_RESULT_CONTENT_TYPES,
|
|
60
|
+
none_id=NoneIdPolicy.POSITIONAL,
|
|
61
|
+
empty_id=EmptyIdPolicy.POSITIONAL,
|
|
62
|
+
malformed_id="stringify",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class HostedToolView:
|
|
68
|
+
"""Safe, provider-neutral presentation fields for one hosted occurrence."""
|
|
69
|
+
|
|
70
|
+
family: str
|
|
71
|
+
provider: str
|
|
72
|
+
provider_item_type: str
|
|
73
|
+
provider_call_id: str
|
|
74
|
+
tool_name: str
|
|
75
|
+
display_title: str
|
|
76
|
+
arguments: dict[str, Any]
|
|
77
|
+
result_text: str
|
|
78
|
+
image_contents: list[Content]
|
|
79
|
+
artifacts: list[Content]
|
|
80
|
+
provider_status: str
|
|
81
|
+
status: str
|
|
82
|
+
phase: str
|
|
83
|
+
retry_safety: str
|
|
84
|
+
metadata: dict[str, Any]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class IntermediateTextOp:
|
|
89
|
+
"""Text that precedes or separates tool activity."""
|
|
90
|
+
|
|
91
|
+
text: str
|
|
92
|
+
order: ContentOrder
|
|
93
|
+
attempt_id: str = ""
|
|
94
|
+
segment_ids: tuple[str, ...] = ()
|
|
95
|
+
provisional: bool = False
|
|
96
|
+
source_contents: tuple[Content, ...] = field(default=(), compare=False, repr=False)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class PresentationAttemptAcceptedOp:
|
|
101
|
+
"""Commit the canonical subset of provisional text from one attempt."""
|
|
102
|
+
|
|
103
|
+
attempt_id: str
|
|
104
|
+
segments: tuple[IntermediateTextOp, ...]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class PresentationAttemptRejectedOp:
|
|
109
|
+
"""Retract every provisional text segment published by one attempt."""
|
|
110
|
+
|
|
111
|
+
attempt_id: str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class HostedToolStartOp:
|
|
116
|
+
"""Start one hosted presentation occurrence."""
|
|
117
|
+
|
|
118
|
+
occurrence_ordinal: int
|
|
119
|
+
presentation_id: str
|
|
120
|
+
view: HostedToolView
|
|
121
|
+
order: ContentOrder
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class HostedToolArgsOp:
|
|
126
|
+
"""Publish a hosted occurrence's current normalized arguments."""
|
|
127
|
+
|
|
128
|
+
occurrence_ordinal: int
|
|
129
|
+
presentation_id: str
|
|
130
|
+
view: HostedToolView
|
|
131
|
+
order: ContentOrder
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class HostedToolProgressOp:
|
|
136
|
+
"""Publish a non-terminal hosted progress snapshot."""
|
|
137
|
+
|
|
138
|
+
occurrence_ordinal: int
|
|
139
|
+
presentation_id: str
|
|
140
|
+
view: HostedToolView
|
|
141
|
+
order: ContentOrder
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True)
|
|
145
|
+
class HostedToolStatusOp:
|
|
146
|
+
"""Publish a hosted lifecycle status without fabricating a result."""
|
|
147
|
+
|
|
148
|
+
occurrence_ordinal: int
|
|
149
|
+
presentation_id: str
|
|
150
|
+
view: HostedToolView
|
|
151
|
+
order: ContentOrder
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class HostedToolResultOp:
|
|
156
|
+
"""Publish a terminal hosted result."""
|
|
157
|
+
|
|
158
|
+
occurrence_ordinal: int
|
|
159
|
+
presentation_id: str
|
|
160
|
+
view: HostedToolView
|
|
161
|
+
order: ContentOrder
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True)
|
|
165
|
+
class FinalTextOp:
|
|
166
|
+
"""Sentinel for the final agent message; empty text remains meaningful."""
|
|
167
|
+
|
|
168
|
+
text: str
|
|
169
|
+
structured_output_completed: bool = False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
type HostedToolOperation = (
|
|
173
|
+
HostedToolStartOp | HostedToolArgsOp | HostedToolProgressOp | HostedToolStatusOp | HostedToolResultOp
|
|
174
|
+
)
|
|
175
|
+
type PresentationOperation = IntermediateTextOp | HostedToolOperation
|
|
176
|
+
type PresentationSinkOperation = (
|
|
177
|
+
PresentationOperation | FinalTextOp | PresentationAttemptAcceptedOp | PresentationAttemptRejectedOp
|
|
178
|
+
)
|
|
179
|
+
type PresentationSink = Callable[[PresentationSinkOperation], Awaitable[None] | None]
|
|
180
|
+
|
|
181
|
+
logger = logging.getLogger(__name__)
|
|
182
|
+
|
|
183
|
+
_CONTEXT_ARGUMENTS_LIMIT = 1_200
|
|
184
|
+
_CONTEXT_RESULT_LIMIT = 2_000
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _family_for(content: Content | None) -> str:
|
|
188
|
+
if content is None:
|
|
189
|
+
return HostedToolFamily.GENERIC
|
|
190
|
+
if content.hosted_family:
|
|
191
|
+
return content.hosted_family
|
|
192
|
+
if content.type.startswith("search_"):
|
|
193
|
+
return HostedToolFamily.SEARCH
|
|
194
|
+
if content.type.startswith("mcp_server_"):
|
|
195
|
+
return HostedToolFamily.MCP
|
|
196
|
+
if content.type.startswith("code_interpreter_"):
|
|
197
|
+
return HostedToolFamily.CODE
|
|
198
|
+
if content.type.startswith("image_generation_"):
|
|
199
|
+
return HostedToolFamily.IMAGE
|
|
200
|
+
if content.type.startswith("shell_"):
|
|
201
|
+
return HostedToolFamily.SHELL
|
|
202
|
+
return HostedToolFamily.GENERIC
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _provider_call_id(content: Content | None) -> str:
|
|
206
|
+
if content is None:
|
|
207
|
+
return ""
|
|
208
|
+
raw_id = content.image_id if content.type.startswith("image_generation_") else content.call_id
|
|
209
|
+
return raw_id if isinstance(raw_id, str) else ""
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _presentation_text_segment_ids(content: Content) -> tuple[str, ...]:
|
|
213
|
+
raw_ids = content.additional_properties.get(PRESENTATION_TEXT_SEGMENT_ID_KEY)
|
|
214
|
+
if isinstance(raw_ids, str):
|
|
215
|
+
return (raw_ids,) if raw_ids else ()
|
|
216
|
+
if isinstance(raw_ids, Sequence):
|
|
217
|
+
return tuple(dict.fromkeys(segment_id for segment_id in raw_ids if isinstance(segment_id, str) and segment_id))
|
|
218
|
+
return ()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _json_safe(value: Any) -> Any:
|
|
222
|
+
if value is None or isinstance(value, str | int | float | bool):
|
|
223
|
+
return value
|
|
224
|
+
if isinstance(value, Mapping):
|
|
225
|
+
return {str(key): _json_safe(item) for key, item in value.items()}
|
|
226
|
+
if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
|
|
227
|
+
return [_json_safe(item) for item in value]
|
|
228
|
+
if isinstance(value, Content):
|
|
229
|
+
return value.to_dict()
|
|
230
|
+
return str(value)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _arguments_dict(value: Any) -> dict[str, Any]:
|
|
234
|
+
if value is None:
|
|
235
|
+
return {}
|
|
236
|
+
if isinstance(value, Mapping):
|
|
237
|
+
return {str(key): _json_safe(item) for key, item in value.items()}
|
|
238
|
+
if isinstance(value, str):
|
|
239
|
+
try:
|
|
240
|
+
decoded = json.loads(value)
|
|
241
|
+
except json.JSONDecodeError:
|
|
242
|
+
return {"input": value}
|
|
243
|
+
if isinstance(decoded, Mapping):
|
|
244
|
+
return {str(key): _json_safe(item) for key, item in decoded.items()}
|
|
245
|
+
return {"input": _json_safe(decoded)}
|
|
246
|
+
return {"input": _json_safe(value)}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _content_values(content: Content | None) -> list[Content]:
|
|
250
|
+
if content is None:
|
|
251
|
+
return []
|
|
252
|
+
candidates: list[Any] = []
|
|
253
|
+
if content.items is not None:
|
|
254
|
+
candidates.extend(content.items)
|
|
255
|
+
if isinstance(content.outputs, Sequence) and not isinstance(content.outputs, str | bytes | bytearray):
|
|
256
|
+
candidates.extend(content.outputs)
|
|
257
|
+
if isinstance(content.output, Sequence) and not isinstance(content.output, str | bytes | bytearray):
|
|
258
|
+
candidates.extend(content.output)
|
|
259
|
+
if isinstance(content.result, Sequence) and not isinstance(content.result, str | bytes | bytearray):
|
|
260
|
+
candidates.extend(content.result)
|
|
261
|
+
return [item for item in candidates if isinstance(item, Content)]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _is_image(content: Content) -> bool:
|
|
265
|
+
# Share the extraction-side predicate: OpenAI code outputs carry the bare
|
|
266
|
+
# "image" media type, which must not double as an artifact.
|
|
267
|
+
return is_image_media_type(content.media_type) or is_image_data_uri(content.uri)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _is_artifact(content: Content) -> bool:
|
|
271
|
+
if content.type in {"hosted_file", "hosted_vector_store"}:
|
|
272
|
+
return True
|
|
273
|
+
return content.type in {"data", "uri"} and not _is_image(content)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _result_payload(content: Content | None) -> Any:
|
|
277
|
+
if content is None:
|
|
278
|
+
return None
|
|
279
|
+
if content.type == "mcp_server_tool_result":
|
|
280
|
+
return content.output
|
|
281
|
+
if content.type in {"code_interpreter_tool_result", "image_generation_tool_result", "shell_tool_result"}:
|
|
282
|
+
return content.outputs
|
|
283
|
+
if content.result is not None:
|
|
284
|
+
return content.result
|
|
285
|
+
if content.items is not None:
|
|
286
|
+
return content.items
|
|
287
|
+
if content.message is not None:
|
|
288
|
+
return content.message
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _error_code(content: Content | None) -> str | int | None:
|
|
293
|
+
if content is None:
|
|
294
|
+
return None
|
|
295
|
+
if content.error_code:
|
|
296
|
+
return content.error_code
|
|
297
|
+
error = content.additional_properties.get("error")
|
|
298
|
+
if isinstance(error, Mapping):
|
|
299
|
+
code = error.get("error_code", error.get("code"))
|
|
300
|
+
if isinstance(code, str | int) and not isinstance(code, bool):
|
|
301
|
+
return code
|
|
302
|
+
code = content.additional_properties.get("error_code")
|
|
303
|
+
if isinstance(code, str | int) and not isinstance(code, bool):
|
|
304
|
+
return code
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _is_failed(content: Content | None, provider_status: str) -> bool:
|
|
309
|
+
if normalize_hosted_tool_status(provider_status) is HostedToolStatus.FAILED:
|
|
310
|
+
return True
|
|
311
|
+
return bool(content is not None and content.additional_properties.get("is_error") is True)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _failure_text(text: str, provider_status: str) -> tuple[str, bool]:
|
|
315
|
+
message = text.strip() or provider_status.strip()
|
|
316
|
+
synthesized = not message
|
|
317
|
+
message = message or "Provider-hosted tool failed."
|
|
318
|
+
return (message if message.startswith("Error: ") else f"Error: {message}", synthesized)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class GenericHostedAdapter:
|
|
322
|
+
"""Adapt any canonical hosted pair without provider-specific branches."""
|
|
323
|
+
|
|
324
|
+
def adapt(self, call: Content | None, result: Content | None = None) -> HostedToolView:
|
|
325
|
+
family = _family_for(call or result)
|
|
326
|
+
tool_name = ""
|
|
327
|
+
if call is not None:
|
|
328
|
+
tool_name = call.tool_name or call.name or ""
|
|
329
|
+
if not tool_name and result is not None:
|
|
330
|
+
tool_name = result.tool_name or result.name or ""
|
|
331
|
+
if not tool_name:
|
|
332
|
+
tool_name = family
|
|
333
|
+
|
|
334
|
+
arguments = self._arguments(call)
|
|
335
|
+
payload = _result_payload(result)
|
|
336
|
+
values = _content_values(result)
|
|
337
|
+
image_contents = extract_result_images(values)
|
|
338
|
+
artifacts = [content for content in values if _is_artifact(content)]
|
|
339
|
+
textual_values = [content for content in values if not _is_image(content) and not _is_artifact(content)]
|
|
340
|
+
if textual_values:
|
|
341
|
+
result_text = extract_result_text(textual_values)
|
|
342
|
+
elif values:
|
|
343
|
+
result_text = ""
|
|
344
|
+
else:
|
|
345
|
+
result_text = extract_result_text(payload) if payload is not None else ""
|
|
346
|
+
provider_status = ""
|
|
347
|
+
for candidate in (result, call):
|
|
348
|
+
if candidate is not None and (candidate.provider_status or candidate.status):
|
|
349
|
+
provider_status = candidate.provider_status or candidate.status or ""
|
|
350
|
+
break
|
|
351
|
+
status = normalize_hosted_tool_status(provider_status)
|
|
352
|
+
failed = _is_failed(result or call, provider_status)
|
|
353
|
+
failure_text_synthesized = False
|
|
354
|
+
if failed:
|
|
355
|
+
status = HostedToolStatus.FAILED
|
|
356
|
+
result_text, failure_text_synthesized = _failure_text(result_text, provider_status)
|
|
357
|
+
|
|
358
|
+
phase = ""
|
|
359
|
+
for candidate in (result, call):
|
|
360
|
+
if candidate is not None and candidate.provider_phase:
|
|
361
|
+
phase = candidate.provider_phase
|
|
362
|
+
break
|
|
363
|
+
if not phase:
|
|
364
|
+
phase = HostedToolPhase.TERMINAL if result is not None else HostedToolPhase.START
|
|
365
|
+
|
|
366
|
+
retry_safety = ""
|
|
367
|
+
for candidate in (result, call):
|
|
368
|
+
if candidate is not None and candidate.retry_safety:
|
|
369
|
+
retry_safety = candidate.retry_safety
|
|
370
|
+
break
|
|
371
|
+
retry_safety = retry_safety or HOSTED_TOOL_DEFAULT_RETRY_SAFETY_BY_FAMILY.get(family, HostedRetrySafety.UNKNOWN)
|
|
372
|
+
|
|
373
|
+
provider = ""
|
|
374
|
+
provider_item_type = ""
|
|
375
|
+
provider_item_id = ""
|
|
376
|
+
for candidate in (call, result):
|
|
377
|
+
if candidate is None:
|
|
378
|
+
continue
|
|
379
|
+
provider = provider or candidate.hosted_provider or ""
|
|
380
|
+
provider_item_type = provider_item_type or candidate.provider_item_type or ""
|
|
381
|
+
provider_item_id = provider_item_id or candidate.provider_item_id or ""
|
|
382
|
+
|
|
383
|
+
metadata: dict[str, Any] = {}
|
|
384
|
+
if provider_item_id:
|
|
385
|
+
metadata["provider_item_id"] = provider_item_id
|
|
386
|
+
if phase:
|
|
387
|
+
metadata["provider_phase"] = phase
|
|
388
|
+
if failed:
|
|
389
|
+
metadata[TOOL_ERRORED_METADATA_KEY] = True
|
|
390
|
+
metadata[TOOL_ERROR_MESSAGE_METADATA_KEY] = result_text.removeprefix("Error: ").strip()
|
|
391
|
+
if failure_text_synthesized:
|
|
392
|
+
metadata[TOOL_FAILURE_TEXT_SYNTHESIZED_METADATA_KEY] = True
|
|
393
|
+
metadata["provider_status"] = provider_status
|
|
394
|
+
error_code = _error_code(result or call)
|
|
395
|
+
if error_code is not None:
|
|
396
|
+
metadata[TOOL_ERROR_CODE_METADATA_KEY] = error_code
|
|
397
|
+
|
|
398
|
+
return HostedToolView(
|
|
399
|
+
family=family,
|
|
400
|
+
provider=provider,
|
|
401
|
+
provider_item_type=provider_item_type,
|
|
402
|
+
provider_call_id=_provider_call_id(call or result),
|
|
403
|
+
tool_name=tool_name,
|
|
404
|
+
display_title=HOSTED_TOOL_DEFAULT_TITLE_BY_FAMILY.get(family, tool_name),
|
|
405
|
+
arguments=arguments,
|
|
406
|
+
result_text=result_text,
|
|
407
|
+
image_contents=image_contents,
|
|
408
|
+
artifacts=artifacts,
|
|
409
|
+
provider_status=provider_status,
|
|
410
|
+
status=status,
|
|
411
|
+
phase=phase,
|
|
412
|
+
retry_safety=retry_safety,
|
|
413
|
+
metadata=metadata,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def _arguments(self, call: Content | None) -> dict[str, Any]:
|
|
417
|
+
if call is None:
|
|
418
|
+
return {}
|
|
419
|
+
return _arguments_dict(call.arguments)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class SearchHostedAdapter(GenericHostedAdapter):
|
|
423
|
+
"""Adapt hosted search and fetch pairs."""
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class McpHostedAdapter(GenericHostedAdapter):
|
|
427
|
+
"""Adapt provider-managed MCP pairs."""
|
|
428
|
+
|
|
429
|
+
def _arguments(self, call: Content | None) -> dict[str, Any]:
|
|
430
|
+
arguments = super()._arguments(call)
|
|
431
|
+
if call is not None and call.server_name:
|
|
432
|
+
return {"server": call.server_name, **arguments}
|
|
433
|
+
return arguments
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class CodeHostedAdapter(GenericHostedAdapter):
|
|
437
|
+
"""Adapt code-interpreter calls, logs, images, and artifacts."""
|
|
438
|
+
|
|
439
|
+
def _arguments(self, call: Content | None) -> dict[str, Any]:
|
|
440
|
+
if call is None:
|
|
441
|
+
return {}
|
|
442
|
+
inputs = call.inputs or []
|
|
443
|
+
code = "".join(content.text or "" for content in inputs if content.type == "text")
|
|
444
|
+
# Anthropic's first canonical representation stored the complete
|
|
445
|
+
# ``{"code": ...}`` input object as text. Unwrap that representation
|
|
446
|
+
# so live and replayed cards expose one provider-neutral code string.
|
|
447
|
+
nested_code = _arguments_dict(code).get("code")
|
|
448
|
+
if isinstance(nested_code, str):
|
|
449
|
+
code = nested_code
|
|
450
|
+
return {"code": code} if code else super()._arguments(call)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class ImageHostedAdapter(GenericHostedAdapter):
|
|
454
|
+
"""Adapt image generation snapshots and terminal outputs."""
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class ShellHostedAdapter(GenericHostedAdapter):
|
|
458
|
+
"""Adapt provider-hosted shell calls and command outputs."""
|
|
459
|
+
|
|
460
|
+
def adapt(self, call: Content | None, result: Content | None = None) -> HostedToolView:
|
|
461
|
+
if result is None or result.type != "shell_tool_result" or result.outputs is None:
|
|
462
|
+
return super().adapt(call, result)
|
|
463
|
+
|
|
464
|
+
normalized_outputs: list[Content] = []
|
|
465
|
+
command_details: list[dict[str, Any]] = []
|
|
466
|
+
for output in result.outputs:
|
|
467
|
+
if output.type != "shell_command_output":
|
|
468
|
+
normalized_outputs.append(output)
|
|
469
|
+
continue
|
|
470
|
+
details = {
|
|
471
|
+
key: value
|
|
472
|
+
for key, value in {
|
|
473
|
+
"stdout": output.stdout,
|
|
474
|
+
"stderr": output.stderr,
|
|
475
|
+
"exit_code": output.exit_code,
|
|
476
|
+
"timed_out": output.timed_out,
|
|
477
|
+
}.items()
|
|
478
|
+
if value is not None
|
|
479
|
+
}
|
|
480
|
+
command_details.append(details)
|
|
481
|
+
text = "\n".join(value for value in (output.stdout, output.stderr) if isinstance(value, str) and value)
|
|
482
|
+
if text:
|
|
483
|
+
normalized_outputs.append(Content.from_text(text))
|
|
484
|
+
|
|
485
|
+
normalized_result = Content.from_shell_tool_result(
|
|
486
|
+
call_id=result.call_id,
|
|
487
|
+
outputs=normalized_outputs,
|
|
488
|
+
max_output_length=result.max_output_length,
|
|
489
|
+
hosted_provider=result.hosted_provider,
|
|
490
|
+
provider_item_type=result.provider_item_type,
|
|
491
|
+
provider_item_id=result.provider_item_id,
|
|
492
|
+
provider_phase=result.provider_phase,
|
|
493
|
+
provider_status=result.provider_status,
|
|
494
|
+
retry_safety=result.retry_safety,
|
|
495
|
+
annotations=result.annotations,
|
|
496
|
+
additional_properties=dict(result.additional_properties),
|
|
497
|
+
raw_representation=result.raw_representation,
|
|
498
|
+
)
|
|
499
|
+
view = super().adapt(call, normalized_result)
|
|
500
|
+
if not command_details:
|
|
501
|
+
return view
|
|
502
|
+
|
|
503
|
+
metadata = dict(view.metadata)
|
|
504
|
+
metadata["outputs"] = command_details
|
|
505
|
+
stdout = "\n".join(
|
|
506
|
+
str(details["stdout"]) for details in command_details if details.get("stdout") not in (None, "")
|
|
507
|
+
)
|
|
508
|
+
stderr = "\n".join(
|
|
509
|
+
str(details["stderr"]) for details in command_details if details.get("stderr") not in (None, "")
|
|
510
|
+
)
|
|
511
|
+
if stdout:
|
|
512
|
+
metadata["stdout"] = stdout
|
|
513
|
+
if stderr:
|
|
514
|
+
metadata["stderr"] = stderr
|
|
515
|
+
exit_codes = [
|
|
516
|
+
details["exit_code"]
|
|
517
|
+
for details in command_details
|
|
518
|
+
if isinstance(details.get("exit_code"), int) and not isinstance(details.get("exit_code"), bool)
|
|
519
|
+
]
|
|
520
|
+
if exit_codes:
|
|
521
|
+
metadata["exit_code"] = next((code for code in exit_codes if code != 0), exit_codes[-1])
|
|
522
|
+
if any(details.get("timed_out") is True for details in command_details):
|
|
523
|
+
metadata["timed_out"] = True
|
|
524
|
+
elif any("timed_out" in details for details in command_details):
|
|
525
|
+
metadata["timed_out"] = False
|
|
526
|
+
return replace(view, metadata=metadata)
|
|
527
|
+
|
|
528
|
+
def _arguments(self, call: Content | None) -> dict[str, Any]:
|
|
529
|
+
if call is None:
|
|
530
|
+
return {}
|
|
531
|
+
arguments: dict[str, Any] = {}
|
|
532
|
+
if call.commands is not None:
|
|
533
|
+
arguments["commands"] = list(call.commands)
|
|
534
|
+
if call.timeout_ms is not None:
|
|
535
|
+
arguments["timeout_ms"] = call.timeout_ms
|
|
536
|
+
if call.max_output_length is not None:
|
|
537
|
+
arguments["max_output_length"] = call.max_output_length
|
|
538
|
+
return arguments or super()._arguments(call)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
_SEARCH_ADAPTER = SearchHostedAdapter()
|
|
542
|
+
_MCP_ADAPTER = McpHostedAdapter()
|
|
543
|
+
_CODE_ADAPTER = CodeHostedAdapter()
|
|
544
|
+
_IMAGE_ADAPTER = ImageHostedAdapter()
|
|
545
|
+
_SHELL_ADAPTER = ShellHostedAdapter()
|
|
546
|
+
_GENERIC_ADAPTER = GenericHostedAdapter()
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def get_hosted_adapter(family: str) -> GenericHostedAdapter:
|
|
550
|
+
"""Return the six-family adapter selected by canonical hosted family."""
|
|
551
|
+
if family in {HostedToolFamily.SEARCH, HostedToolFamily.FETCH}:
|
|
552
|
+
return _SEARCH_ADAPTER
|
|
553
|
+
if family == HostedToolFamily.MCP:
|
|
554
|
+
return _MCP_ADAPTER
|
|
555
|
+
if family == HostedToolFamily.CODE:
|
|
556
|
+
return _CODE_ADAPTER
|
|
557
|
+
if family == HostedToolFamily.IMAGE:
|
|
558
|
+
return _IMAGE_ADAPTER
|
|
559
|
+
if family == HostedToolFamily.SHELL:
|
|
560
|
+
return _SHELL_ADAPTER
|
|
561
|
+
return _GENERIC_ADAPTER
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def adapt_hosted_tool(call: Content | None, result: Content | None = None) -> HostedToolView:
|
|
565
|
+
"""Adapt a canonical hosted call and optional paired result."""
|
|
566
|
+
return get_hosted_adapter(_family_for(call or result)).adapt(call, result)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def hosted_replay_status(view: HostedToolView, *, has_result: bool) -> str:
|
|
570
|
+
"""Return the terminal status used when restoring a persisted occurrence."""
|
|
571
|
+
status = normalize_hosted_tool_status(view.status)
|
|
572
|
+
if status in _TERMINAL_STATUSES:
|
|
573
|
+
return status.value
|
|
574
|
+
if view.phase == HostedToolPhase.TERMINAL:
|
|
575
|
+
return HostedToolStatus.COMPLETED
|
|
576
|
+
# Persisted history cannot keep a live card alive. A pending/running call,
|
|
577
|
+
# or a partial result snapshot, was interrupted when the session stopped.
|
|
578
|
+
del has_result
|
|
579
|
+
return HostedToolStatus.INTERRUPTED
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _is_successful_structured_output(view: HostedToolView, *, has_result: bool) -> bool:
|
|
583
|
+
"""Return whether a hosted occurrence can stand as the turn's visible output."""
|
|
584
|
+
return hosted_replay_status(view, has_result=has_result) == HostedToolStatus.COMPLETED and view.family not in {
|
|
585
|
+
HostedToolFamily.SEARCH,
|
|
586
|
+
HostedToolFamily.FETCH,
|
|
587
|
+
HostedToolFamily.TOOL_DISCOVERY,
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _bounded_context_value(value: Any, limit: int) -> str:
|
|
592
|
+
try:
|
|
593
|
+
rendered = json.dumps(_json_safe(value), ensure_ascii=False, sort_keys=True)
|
|
594
|
+
except TypeError, ValueError:
|
|
595
|
+
rendered = str(value)
|
|
596
|
+
if len(rendered) <= limit:
|
|
597
|
+
return rendered
|
|
598
|
+
return f"{rendered[: limit - 1]}…"
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def hosted_context_summary(call: Content | None, result: Content | None = None) -> str:
|
|
602
|
+
"""Build a bounded provider-neutral assistant summary for one occurrence."""
|
|
603
|
+
view = adapt_hosted_tool(call, result)
|
|
604
|
+
status = hosted_replay_status(view, has_result=result is not None)
|
|
605
|
+
lines = [
|
|
606
|
+
"[Provider-hosted tool context]",
|
|
607
|
+
f"Tool: {view.tool_name or view.display_title}",
|
|
608
|
+
f"Family: {view.family}",
|
|
609
|
+
f"Status: {status}",
|
|
610
|
+
]
|
|
611
|
+
if view.arguments:
|
|
612
|
+
lines.append(f"Arguments: {_bounded_context_value(view.arguments, _CONTEXT_ARGUMENTS_LIMIT)}")
|
|
613
|
+
if view.result_text:
|
|
614
|
+
lines.append(f"Result: {_bounded_context_value(view.result_text, _CONTEXT_RESULT_LIMIT)}")
|
|
615
|
+
if view.image_contents:
|
|
616
|
+
lines.append(f"Images: {len(view.image_contents)}")
|
|
617
|
+
if view.artifacts:
|
|
618
|
+
lines.append(f"Artifacts: {len(view.artifacts)}")
|
|
619
|
+
return "\n".join(lines)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _is_hosted_call(content: Content) -> bool:
|
|
623
|
+
if content.type in _HOSTED_CALL_TYPES:
|
|
624
|
+
return True
|
|
625
|
+
return content.type == "function_call" and content.informational_only and content.provider_hosted
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _is_hosted_result(content: Content) -> bool:
|
|
629
|
+
return content.type in _HOSTED_RESULT_TYPES or (content.type == "function_result" and content.provider_hosted)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def is_legacy_server_executed_informational_call(content: Content | Mapping[str, Any]) -> bool:
|
|
633
|
+
"""Recognize old OpenAI informational calls with explicit server execution proof."""
|
|
634
|
+
if isinstance(content, Content):
|
|
635
|
+
if content.type != "function_call" or not content.informational_only:
|
|
636
|
+
return False
|
|
637
|
+
extra: Mapping[str, Any] = content.additional_properties
|
|
638
|
+
else:
|
|
639
|
+
if content.get("type") != "function_call" or content.get("informational_only") is not True:
|
|
640
|
+
return False
|
|
641
|
+
raw_extra = content.get("additional_properties")
|
|
642
|
+
if not isinstance(raw_extra, Mapping):
|
|
643
|
+
return False
|
|
644
|
+
extra = raw_extra
|
|
645
|
+
item_type = extra.get("item_type")
|
|
646
|
+
if item_type not in {"tool_search_call", "custom_tool_call"}:
|
|
647
|
+
return False
|
|
648
|
+
return extra.get("execution") == "server" or (
|
|
649
|
+
"server_execution" in extra and extra.get("server_execution") is not None
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def normalize_hosted_replay_content(content: Content) -> Content:
|
|
654
|
+
"""Promote supported legacy server-executed calls to canonical hosted content."""
|
|
655
|
+
if not is_legacy_server_executed_informational_call(content):
|
|
656
|
+
return content
|
|
657
|
+
promoted = copy(content)
|
|
658
|
+
extra = promoted.additional_properties
|
|
659
|
+
item_type = str(extra.get("item_type") or "")
|
|
660
|
+
promoted.provider_hosted = True
|
|
661
|
+
promoted.hosted_family = (
|
|
662
|
+
HostedToolFamily.TOOL_DISCOVERY if item_type == "tool_search_call" else HostedToolFamily.GENERIC
|
|
663
|
+
)
|
|
664
|
+
promoted.hosted_provider = "openai"
|
|
665
|
+
promoted.provider_item_type = item_type
|
|
666
|
+
promoted.provider_item_id = str(extra.get("item_id") or "") or None
|
|
667
|
+
promoted.provider_status = str(extra.get("status") or "") or None
|
|
668
|
+
promoted.provider_phase = HostedToolPhase.TERMINAL
|
|
669
|
+
promoted.tool_name = promoted.name or "tool"
|
|
670
|
+
return promoted
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _coord(occurrence: Occurrence) -> tuple[int, int]:
|
|
674
|
+
return occurrence.message_index, occurrence.content_index
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
@dataclass(frozen=True)
|
|
678
|
+
class _Pair:
|
|
679
|
+
call: tuple[int, int] | None
|
|
680
|
+
result: tuple[int, int] | None
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _presentation_pairs(messages: Sequence[Message]) -> dict[tuple[int, int], _Pair]:
|
|
684
|
+
accessor = LiveAccessor()
|
|
685
|
+
pairs: dict[tuple[int, int], _Pair] = {}
|
|
686
|
+
for exchange in iter_exchanges(messages, accessor):
|
|
687
|
+
pairing = pair_results(messages, exchange, accessor, _PRESENTATION_PAIRING_POLICY)
|
|
688
|
+
assignments = [assignment for entries in pairing.truthy_assignments.values() for assignment in entries] + [
|
|
689
|
+
assignment for entries in pairing.falsy_assignments.values() for assignment in entries
|
|
690
|
+
]
|
|
691
|
+
for call_occurrence, result_occurrence in assignments:
|
|
692
|
+
call_coord = _coord(call_occurrence)
|
|
693
|
+
result_coord = _coord(result_occurrence) if result_occurrence is not None else None
|
|
694
|
+
pair = _Pair(call_coord, result_coord)
|
|
695
|
+
pairs[call_coord] = pair
|
|
696
|
+
if result_coord is not None:
|
|
697
|
+
pairs[result_coord] = pair
|
|
698
|
+
unconsumed = [occurrence for entries in pairing.unconsumed_results.values() for occurrence in entries] + [
|
|
699
|
+
occurrence for entries in pairing.falsy_unconsumed_results.values() for occurrence in entries
|
|
700
|
+
]
|
|
701
|
+
for occurrence in [*unconsumed, *pairing.unpairable_results]:
|
|
702
|
+
result_coord = _coord(occurrence)
|
|
703
|
+
pairs.setdefault(result_coord, _Pair(None, result_coord))
|
|
704
|
+
for occurrence in pairing.unpairable_calls:
|
|
705
|
+
call_coord = _coord(occurrence)
|
|
706
|
+
pairs.setdefault(call_coord, _Pair(call_coord, None))
|
|
707
|
+
|
|
708
|
+
# ``iter_exchanges`` starts at a call-bearing assistant run, so a fully
|
|
709
|
+
# standalone hosted result belongs to no exchange. Inventory those
|
|
710
|
+
# occurrences without attempting any global ID matching; each result is
|
|
711
|
+
# its own presentation occurrence and degradation anchor.
|
|
712
|
+
for message_index, message in enumerate(messages):
|
|
713
|
+
if message.role not in {"assistant", "tool"} or HistoryMarkerKind.KEY in message.additional_properties:
|
|
714
|
+
continue
|
|
715
|
+
for content_index, content in enumerate(message.contents):
|
|
716
|
+
coordinate = (message_index, content_index)
|
|
717
|
+
if coordinate not in pairs and _is_hosted_result(content):
|
|
718
|
+
pairs[coordinate] = _Pair(None, coordinate)
|
|
719
|
+
return pairs
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _content_at(messages: Sequence[Message], coordinate: tuple[int, int] | None) -> Content | None:
|
|
723
|
+
if coordinate is None:
|
|
724
|
+
return None
|
|
725
|
+
message_index, content_index = coordinate
|
|
726
|
+
return messages[message_index].contents[content_index]
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def cross_provider_hosted_degradations(
|
|
730
|
+
messages: Sequence[Message],
|
|
731
|
+
*,
|
|
732
|
+
target_provider: str,
|
|
733
|
+
unsafe_same_provider_families: Collection[str] = (),
|
|
734
|
+
) -> dict[int, str | None]:
|
|
735
|
+
"""Map hosted contents that are unsafe to replay to one neutral summary each.
|
|
736
|
+
|
|
737
|
+
A provider switch always makes a hosted item unsafe. Callers may also
|
|
738
|
+
identify same-provider families whose server-issued item identities cannot
|
|
739
|
+
be replayed through a locally managed transcript.
|
|
740
|
+
"""
|
|
741
|
+
pairs = _presentation_pairs(messages)
|
|
742
|
+
degradations: dict[int, str | None] = {}
|
|
743
|
+
handled_pairs: set[_Pair] = set()
|
|
744
|
+
for message_index, message in enumerate(messages):
|
|
745
|
+
for content_index, content in enumerate(message.contents):
|
|
746
|
+
# Hosted-tool persistence shipped together with this marker, so
|
|
747
|
+
# pre-marker histories cannot contain supported hosted records.
|
|
748
|
+
# Current producers must use the canonical factories, which always
|
|
749
|
+
# set it; marker-less hand-built payloads are outside compatibility.
|
|
750
|
+
if not content.provider_hosted:
|
|
751
|
+
continue
|
|
752
|
+
source_provider = content.hosted_provider or ""
|
|
753
|
+
family = _family_for(content)
|
|
754
|
+
# Chat Completions passes an empty target because that wire has no
|
|
755
|
+
# hosted item types: every provider-hosted item is unsafe there,
|
|
756
|
+
# including factory-created contents with unknown provenance. A
|
|
757
|
+
# hosted-capable target may still receive unstamped contents built
|
|
758
|
+
# for its current dialect, so only an explicit mismatch is foreign.
|
|
759
|
+
cross_provider = not target_provider or bool(source_provider and source_provider != target_provider)
|
|
760
|
+
unsafe_same_provider = source_provider == target_provider and family in unsafe_same_provider_families
|
|
761
|
+
if not cross_provider and not unsafe_same_provider:
|
|
762
|
+
continue
|
|
763
|
+
pair = pairs.get((message_index, content_index), _Pair((message_index, content_index), None))
|
|
764
|
+
if pair in handled_pairs:
|
|
765
|
+
continue
|
|
766
|
+
handled_pairs.add(pair)
|
|
767
|
+
call = _content_at(messages, pair.call)
|
|
768
|
+
result = _content_at(messages, pair.result)
|
|
769
|
+
members = [candidate for candidate in (call, result) if candidate is not None and candidate.provider_hosted]
|
|
770
|
+
if not members:
|
|
771
|
+
continue
|
|
772
|
+
anchor = call if call is not None and call.provider_hosted else members[0]
|
|
773
|
+
for member in members:
|
|
774
|
+
degradations[id(member)] = None
|
|
775
|
+
degradations[id(anchor)] = hosted_context_summary(call, result)
|
|
776
|
+
representative = call or result
|
|
777
|
+
if representative is None:
|
|
778
|
+
raise RuntimeError("A hosted exchange requires a call or a result.")
|
|
779
|
+
logger.debug(
|
|
780
|
+
"Degrading provider-hosted history to assistant context: source=%s target=%s family=%s item_type=%s",
|
|
781
|
+
source_provider,
|
|
782
|
+
target_provider,
|
|
783
|
+
_family_for(call or result),
|
|
784
|
+
representative.provider_item_type,
|
|
785
|
+
)
|
|
786
|
+
return degradations
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _hosted_ops(
|
|
790
|
+
*,
|
|
791
|
+
occurrence: int,
|
|
792
|
+
view: HostedToolView,
|
|
793
|
+
order: ContentOrder,
|
|
794
|
+
start: bool,
|
|
795
|
+
result_content: bool,
|
|
796
|
+
) -> list[HostedToolOperation]:
|
|
797
|
+
operations: list[HostedToolOperation] = []
|
|
798
|
+
suborder = order[2]
|
|
799
|
+
if start:
|
|
800
|
+
operations.append(HostedToolStartOp(occurrence, "", view, (order[0], order[1], suborder)))
|
|
801
|
+
suborder += 1
|
|
802
|
+
if view.arguments and (start or result_content):
|
|
803
|
+
# Some providers attach arguments only to the terminal item (OpenAI
|
|
804
|
+
# web_search action), so result observations re-announce them; the
|
|
805
|
+
# publish-side signature dedupes unchanged payloads.
|
|
806
|
+
operations.append(HostedToolArgsOp(occurrence, "", view, (order[0], order[1], suborder)))
|
|
807
|
+
suborder += 1
|
|
808
|
+
status = normalize_hosted_tool_status(view.status)
|
|
809
|
+
nonterminal_snapshot = (
|
|
810
|
+
view.phase in {HostedToolPhase.DELTA, HostedToolPhase.SNAPSHOT} and status not in _TERMINAL_STATUSES
|
|
811
|
+
)
|
|
812
|
+
if result_content and nonterminal_snapshot:
|
|
813
|
+
operations.append(HostedToolProgressOp(occurrence, "", view, (order[0], order[1], suborder)))
|
|
814
|
+
elif result_content:
|
|
815
|
+
operations.append(HostedToolResultOp(occurrence, "", view, (order[0], order[1], suborder)))
|
|
816
|
+
elif view.phase in {HostedToolPhase.DELTA, HostedToolPhase.SNAPSHOT} and (view.result_text or view.image_contents):
|
|
817
|
+
operations.append(HostedToolProgressOp(occurrence, "", view, (order[0], order[1], suborder)))
|
|
818
|
+
elif status is not HostedToolStatus.UNKNOWN:
|
|
819
|
+
operations.append(HostedToolStatusOp(occurrence, "", view, (order[0], order[1], suborder)))
|
|
820
|
+
return operations
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
@dataclass(frozen=True)
|
|
824
|
+
class ResponsePresentationPlan:
|
|
825
|
+
"""Pure, exchange-aware presentation plan for an assembled response."""
|
|
826
|
+
|
|
827
|
+
operations: list[PresentationOperation]
|
|
828
|
+
final_text: str
|
|
829
|
+
structured_output_completed: bool = False
|
|
830
|
+
|
|
831
|
+
@classmethod
|
|
832
|
+
def from_messages(cls, messages: Sequence[Message]) -> ResponsePresentationPlan:
|
|
833
|
+
"""Plan text and hosted operations without rebuilding any content."""
|
|
834
|
+
has_hosted = any(
|
|
835
|
+
_is_hosted_call(content) or _is_hosted_result(content)
|
|
836
|
+
for message in messages
|
|
837
|
+
for content in message.contents
|
|
838
|
+
)
|
|
839
|
+
if not has_hosted:
|
|
840
|
+
final_text = ""
|
|
841
|
+
if messages:
|
|
842
|
+
final_text = "".join(
|
|
843
|
+
content.text or "" for content in messages[-1].contents if content.type == "text" and content.text
|
|
844
|
+
)
|
|
845
|
+
return cls([], final_text)
|
|
846
|
+
|
|
847
|
+
pairs = _presentation_pairs(messages)
|
|
848
|
+
operations: list[PresentationOperation] = []
|
|
849
|
+
occurrence_by_pair: dict[_Pair, int] = {}
|
|
850
|
+
emitted_starts: set[int] = set()
|
|
851
|
+
terminal_occurrences: set[int] = set()
|
|
852
|
+
successful_structured_occurrences: set[int] = set()
|
|
853
|
+
pending_text: list[Content] = []
|
|
854
|
+
trailing_text_final = False
|
|
855
|
+
next_occurrence = 0
|
|
856
|
+
|
|
857
|
+
def flush_intermediate(order: ContentOrder) -> None:
|
|
858
|
+
if not pending_text:
|
|
859
|
+
return
|
|
860
|
+
segment_ids = tuple(
|
|
861
|
+
dict.fromkeys(
|
|
862
|
+
segment_id for content in pending_text for segment_id in _presentation_text_segment_ids(content)
|
|
863
|
+
)
|
|
864
|
+
)
|
|
865
|
+
operations.append(
|
|
866
|
+
IntermediateTextOp(
|
|
867
|
+
"".join(content.text or "" for content in pending_text),
|
|
868
|
+
order,
|
|
869
|
+
segment_ids=segment_ids,
|
|
870
|
+
source_contents=tuple(pending_text),
|
|
871
|
+
)
|
|
872
|
+
)
|
|
873
|
+
pending_text.clear()
|
|
874
|
+
|
|
875
|
+
for message_index, message in enumerate(messages):
|
|
876
|
+
for content_index, content in enumerate(message.contents):
|
|
877
|
+
coordinate = (message_index, content_index)
|
|
878
|
+
order = (message_index, content_index, 0)
|
|
879
|
+
if content.type == "text":
|
|
880
|
+
if content.text:
|
|
881
|
+
pending_text.append(content)
|
|
882
|
+
continue
|
|
883
|
+
if content.type == "function_call" and not content.informational_only:
|
|
884
|
+
flush_intermediate((message_index, content_index, -1))
|
|
885
|
+
trailing_text_final = False
|
|
886
|
+
continue
|
|
887
|
+
if content.type == "function_result" and not _is_hosted_result(content):
|
|
888
|
+
# A full-run transcript interleaves local results between
|
|
889
|
+
# responses; the loop resumed past this call, so text that
|
|
890
|
+
# follows is the model's post-tool answer, not preamble.
|
|
891
|
+
flush_intermediate((message_index, content_index, -1))
|
|
892
|
+
trailing_text_final = True
|
|
893
|
+
continue
|
|
894
|
+
|
|
895
|
+
pair = pairs.get(coordinate)
|
|
896
|
+
if pair is None:
|
|
897
|
+
continue
|
|
898
|
+
call = _content_at(messages, pair.call)
|
|
899
|
+
result = _content_at(messages, pair.result)
|
|
900
|
+
if call is not None and not _is_hosted_call(call):
|
|
901
|
+
continue
|
|
902
|
+
if call is None and (result is None or not _is_hosted_result(result)):
|
|
903
|
+
continue
|
|
904
|
+
occurrence = occurrence_by_pair.get(pair)
|
|
905
|
+
if occurrence is None:
|
|
906
|
+
occurrence = next_occurrence
|
|
907
|
+
next_occurrence += 1
|
|
908
|
+
occurrence_by_pair[pair] = occurrence
|
|
909
|
+
|
|
910
|
+
if _is_hosted_result(content):
|
|
911
|
+
flush_intermediate((message_index, content_index, -1))
|
|
912
|
+
start = occurrence not in emitted_starts
|
|
913
|
+
view = adapt_hosted_tool(call, content)
|
|
914
|
+
operations.extend(
|
|
915
|
+
_hosted_ops(
|
|
916
|
+
occurrence=occurrence,
|
|
917
|
+
view=view,
|
|
918
|
+
order=order,
|
|
919
|
+
start=start,
|
|
920
|
+
result_content=True,
|
|
921
|
+
)
|
|
922
|
+
)
|
|
923
|
+
emitted_starts.add(occurrence)
|
|
924
|
+
status = normalize_hosted_tool_status(view.status)
|
|
925
|
+
if view.phase == HostedToolPhase.TERMINAL or status in _TERMINAL_STATUSES:
|
|
926
|
+
terminal_occurrences.add(occurrence)
|
|
927
|
+
if _is_successful_structured_output(view, has_result=True):
|
|
928
|
+
successful_structured_occurrences.add(occurrence)
|
|
929
|
+
trailing_text_final = True
|
|
930
|
+
else:
|
|
931
|
+
trailing_text_final = False
|
|
932
|
+
continue
|
|
933
|
+
|
|
934
|
+
if _is_hosted_call(content) and occurrence not in emitted_starts:
|
|
935
|
+
flush_intermediate((message_index, content_index, -1))
|
|
936
|
+
view = adapt_hosted_tool(content)
|
|
937
|
+
operations.extend(
|
|
938
|
+
_hosted_ops(
|
|
939
|
+
occurrence=occurrence,
|
|
940
|
+
view=view,
|
|
941
|
+
order=order,
|
|
942
|
+
start=True,
|
|
943
|
+
result_content=False,
|
|
944
|
+
)
|
|
945
|
+
)
|
|
946
|
+
emitted_starts.add(occurrence)
|
|
947
|
+
has_later_result = pair.result is not None and pair.result != coordinate
|
|
948
|
+
status = normalize_hosted_tool_status(view.status)
|
|
949
|
+
terminal_call = not has_later_result and (
|
|
950
|
+
view.phase == HostedToolPhase.TERMINAL or status in _TERMINAL_STATUSES
|
|
951
|
+
)
|
|
952
|
+
if terminal_call:
|
|
953
|
+
terminal_occurrences.add(occurrence)
|
|
954
|
+
if _is_successful_structured_output(view, has_result=False):
|
|
955
|
+
successful_structured_occurrences.add(occurrence)
|
|
956
|
+
trailing_text_final = True
|
|
957
|
+
else:
|
|
958
|
+
trailing_text_final = False
|
|
959
|
+
|
|
960
|
+
if pending_text and trailing_text_final:
|
|
961
|
+
final_text = "".join(content.text or "" for content in pending_text)
|
|
962
|
+
else:
|
|
963
|
+
flush_intermediate((len(messages), 0, 0))
|
|
964
|
+
final_text = ""
|
|
965
|
+
return cls(operations, final_text, bool(successful_structured_occurrences))
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
@dataclass(frozen=True, slots=True)
|
|
969
|
+
class FinalSegment:
|
|
970
|
+
"""A delegated agent's answer: the caller's result and the transcript's last text.
|
|
971
|
+
|
|
972
|
+
The caller receives the response's final segment, as a main turn's answer
|
|
973
|
+
is its last message: text written between tool calls and hosted outputs
|
|
974
|
+
is published live as intermediate transcript text. A response whose final
|
|
975
|
+
segment is empty (it ends on a tool result or hosted output) gives the
|
|
976
|
+
caller everything it said instead; all of that is intermediate text
|
|
977
|
+
already, so ``transcript`` stays empty.
|
|
978
|
+
"""
|
|
979
|
+
|
|
980
|
+
result: str
|
|
981
|
+
transcript: str
|
|
982
|
+
|
|
983
|
+
@classmethod
|
|
984
|
+
def of(cls, response: AgentResponse[Any]) -> FinalSegment:
|
|
985
|
+
final_text = ResponsePresentationPlan.from_messages(response.messages).final_text
|
|
986
|
+
return cls(final_text or response.text, final_text)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
@dataclass
|
|
990
|
+
class _LiveRecord:
|
|
991
|
+
operation: PresentationOperation | None = None
|
|
992
|
+
local_call_id: str = ""
|
|
993
|
+
local_call_content: Content | None = None
|
|
994
|
+
text_segment_id: str = ""
|
|
995
|
+
released: bool = False
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
@dataclass
|
|
999
|
+
class _LiveTextSegment:
|
|
1000
|
+
record: _LiveRecord
|
|
1001
|
+
order: ContentOrder
|
|
1002
|
+
parts: list[str] = field(default_factory=list)
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
@dataclass
|
|
1006
|
+
class _HostedOccurrence:
|
|
1007
|
+
ordinal: int
|
|
1008
|
+
presentation_id: str
|
|
1009
|
+
pairing_key: tuple[str, str]
|
|
1010
|
+
provider_item_id: str
|
|
1011
|
+
call_seen: bool = False
|
|
1012
|
+
result_seen: bool = False
|
|
1013
|
+
start_published: bool = False
|
|
1014
|
+
terminal: bool = False
|
|
1015
|
+
last_view: HostedToolView | None = None
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
@dataclass
|
|
1019
|
+
class _PlanBarrier:
|
|
1020
|
+
call_id: str
|
|
1021
|
+
order: ContentOrder
|
|
1022
|
+
content: Content
|
|
1023
|
+
released: bool = False
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
_hosted_run_generations = itertools.count(1)
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def next_hosted_run_generation() -> int:
|
|
1030
|
+
"""Return a hosted pass generation that no other pass in this process has used.
|
|
1031
|
+
|
|
1032
|
+
Presentation ids embed it, and frontends key tool cards by those ids. A
|
|
1033
|
+
counter per bindings object restarts on every engine rebuild and would
|
|
1034
|
+
hand a new card the id of an earlier one.
|
|
1035
|
+
"""
|
|
1036
|
+
return next(_hosted_run_generations)
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
@dataclass
|
|
1040
|
+
class HostedPresentationBridge:
|
|
1041
|
+
"""Stateful streaming/blocking bridge that emits presentation operations."""
|
|
1042
|
+
|
|
1043
|
+
publish: PresentationSink
|
|
1044
|
+
run_generation: int = 0
|
|
1045
|
+
response_index: int = 0
|
|
1046
|
+
batch_id: int = 0
|
|
1047
|
+
# Awaited as each logical response begins, before anything it produces
|
|
1048
|
+
# is presented; it publishes text an earlier response left buffered.
|
|
1049
|
+
before_response: Callable[[], Awaitable[None]] | None = None
|
|
1050
|
+
attempt_index: int = field(default=-1, init=False)
|
|
1051
|
+
next_occurrence_ordinal: int = field(default=0, init=False)
|
|
1052
|
+
published_terminal_signatures: set[tuple[Any, ...]] = field(default_factory=set, init=False)
|
|
1053
|
+
_published_signatures: set[tuple[Any, ...]] = field(default_factory=set, init=False, repr=False)
|
|
1054
|
+
_live_records: list[_LiveRecord] = field(default_factory=list, init=False, repr=False)
|
|
1055
|
+
_live_cursor: int = field(default=0, init=False, repr=False)
|
|
1056
|
+
_occurrences: list[_HostedOccurrence] = field(default_factory=list, init=False, repr=False)
|
|
1057
|
+
_accepted_occurrences: list[_HostedOccurrence] = field(default_factory=list, init=False, repr=False)
|
|
1058
|
+
_open_occurrences: dict[tuple[str, str], deque[_HostedOccurrence]] = field(
|
|
1059
|
+
default_factory=lambda: defaultdict(deque), init=False, repr=False
|
|
1060
|
+
)
|
|
1061
|
+
_provider_item_candidates: dict[str, list[_HostedOccurrence]] = field(
|
|
1062
|
+
default_factory=lambda: defaultdict(list), init=False, repr=False
|
|
1063
|
+
)
|
|
1064
|
+
_live_text_segments: dict[str, _LiveTextSegment] = field(default_factory=dict, init=False, repr=False)
|
|
1065
|
+
_published_provisional_segments: dict[str, IntermediateTextOp] = field(default_factory=dict, init=False, repr=False)
|
|
1066
|
+
_published_canonical_text_contents: WeakIdentityRegistry = field(
|
|
1067
|
+
default_factory=WeakIdentityRegistry, init=False, repr=False
|
|
1068
|
+
)
|
|
1069
|
+
_started_local_call_contents: WeakIdentityRegistry = field(
|
|
1070
|
+
default_factory=WeakIdentityRegistry, init=False, repr=False
|
|
1071
|
+
)
|
|
1072
|
+
_accepted_barriers: list[_PlanBarrier] = field(default_factory=list, init=False, repr=False)
|
|
1073
|
+
_unbound_local_start_counts: Counter[str] = field(default_factory=Counter, init=False, repr=False)
|
|
1074
|
+
_pending_reconciliation: deque[PresentationSinkOperation] = field(default_factory=deque, init=False, repr=False)
|
|
1075
|
+
_finalized_response_keys: set[tuple[int, int]] = field(default_factory=set, init=False, repr=False)
|
|
1076
|
+
_response_started: bool = field(default=False, init=False, repr=False)
|
|
1077
|
+
_saw_incremental_contents: bool = field(default=False, init=False, repr=False)
|
|
1078
|
+
_saw_hosted_contents: bool = field(default=False, init=False, repr=False)
|
|
1079
|
+
|
|
1080
|
+
async def begin_response(self, *, response_index: int | None = None, batch_id: int | None = None) -> None:
|
|
1081
|
+
"""Begin another logical response without resetting run-monotonic ordinals."""
|
|
1082
|
+
if self.before_response is not None:
|
|
1083
|
+
await self.before_response()
|
|
1084
|
+
if response_index is not None:
|
|
1085
|
+
self.response_index = response_index
|
|
1086
|
+
elif self._response_started:
|
|
1087
|
+
self.response_index += 1
|
|
1088
|
+
self._response_started = True
|
|
1089
|
+
if batch_id is not None:
|
|
1090
|
+
self.batch_id = batch_id
|
|
1091
|
+
# Provider-controlled call ids may repeat in a later response. Only
|
|
1092
|
+
# signals that could not yet bind to a concrete Content are scoped to
|
|
1093
|
+
# the current response; identity-bound starts survive for cumulative
|
|
1094
|
+
# final reconciliation. Continuation attempts do not call this method.
|
|
1095
|
+
self._unbound_local_start_counts.clear()
|
|
1096
|
+
self.attempt_index = -1
|
|
1097
|
+
self._clear_attempt_state()
|
|
1098
|
+
|
|
1099
|
+
async def attempt_started(self, *, continuation: bool = False) -> None:
|
|
1100
|
+
"""Open a validation attempt; continuation resumes the current occurrence set."""
|
|
1101
|
+
self._response_started = True
|
|
1102
|
+
if continuation and self.attempt_index >= 0:
|
|
1103
|
+
return
|
|
1104
|
+
self.attempt_index += 1
|
|
1105
|
+
self._clear_attempt_state()
|
|
1106
|
+
|
|
1107
|
+
async def observe_contents(self, contents: Sequence[Content], *, is_final: bool = False) -> None:
|
|
1108
|
+
"""Observe one raw provider update or final content batch in content order."""
|
|
1109
|
+
if self.attempt_index < 0:
|
|
1110
|
+
await self.attempt_started()
|
|
1111
|
+
if not is_final and any(content.type != "usage" for content in contents):
|
|
1112
|
+
self._saw_incremental_contents = True
|
|
1113
|
+
blocking_final_snapshot = is_final and not self._saw_incremental_contents
|
|
1114
|
+
for content in contents:
|
|
1115
|
+
if content.type == "text":
|
|
1116
|
+
if not content.text:
|
|
1117
|
+
continue
|
|
1118
|
+
if blocking_final_snapshot:
|
|
1119
|
+
self._live_records.append(
|
|
1120
|
+
_LiveRecord(
|
|
1121
|
+
operation=IntermediateTextOp(
|
|
1122
|
+
content.text,
|
|
1123
|
+
(0, len(self._live_records), 0),
|
|
1124
|
+
source_contents=(content,),
|
|
1125
|
+
)
|
|
1126
|
+
)
|
|
1127
|
+
)
|
|
1128
|
+
continue
|
|
1129
|
+
if is_final:
|
|
1130
|
+
continue
|
|
1131
|
+
segment_ids = _presentation_text_segment_ids(content)
|
|
1132
|
+
if len(segment_ids) == 1:
|
|
1133
|
+
segment_id = segment_ids[0]
|
|
1134
|
+
segment = self._live_text_segments.get(segment_id)
|
|
1135
|
+
if segment is None:
|
|
1136
|
+
record = _LiveRecord(text_segment_id=segment_id)
|
|
1137
|
+
self._live_records.append(record)
|
|
1138
|
+
segment = _LiveTextSegment(record, (0, len(self._live_records) - 1, 0))
|
|
1139
|
+
self._live_text_segments[segment_id] = segment
|
|
1140
|
+
segment.parts.append(content.text)
|
|
1141
|
+
else:
|
|
1142
|
+
self._live_records.append(
|
|
1143
|
+
_LiveRecord(operation=IntermediateTextOp(content.text, (0, len(self._live_records), 0)))
|
|
1144
|
+
)
|
|
1145
|
+
continue
|
|
1146
|
+
if content.type == "function_call" and not content.informational_only:
|
|
1147
|
+
if not is_final and self._saw_hosted_contents:
|
|
1148
|
+
await self._seal_live_text_segments()
|
|
1149
|
+
call_id = _provider_call_id(content)
|
|
1150
|
+
record = _LiveRecord(local_call_id=call_id, local_call_content=content)
|
|
1151
|
+
if self._unbound_local_start_counts[call_id] > 0:
|
|
1152
|
+
record.released = True
|
|
1153
|
+
self._started_local_call_contents.register(content)
|
|
1154
|
+
self._unbound_local_start_counts[call_id] -= 1
|
|
1155
|
+
if self._unbound_local_start_counts[call_id] == 0:
|
|
1156
|
+
del self._unbound_local_start_counts[call_id]
|
|
1157
|
+
self._live_records.append(record)
|
|
1158
|
+
await self._drain_live_prefix()
|
|
1159
|
+
continue
|
|
1160
|
+
if _is_hosted_call(content):
|
|
1161
|
+
self._saw_hosted_contents = True
|
|
1162
|
+
if not is_final:
|
|
1163
|
+
await self._seal_live_text_segments()
|
|
1164
|
+
await self._observe_call(content)
|
|
1165
|
+
elif _is_hosted_result(content):
|
|
1166
|
+
self._saw_hosted_contents = True
|
|
1167
|
+
if not is_final:
|
|
1168
|
+
await self._seal_live_text_segments()
|
|
1169
|
+
await self._observe_result(content)
|
|
1170
|
+
|
|
1171
|
+
async def local_call_start_published(self, provider_call_id: str) -> None:
|
|
1172
|
+
"""Release the next local-call ordering barrier for a provider call id."""
|
|
1173
|
+
bound = False
|
|
1174
|
+
for record in self._live_records:
|
|
1175
|
+
if record.local_call_id == provider_call_id and not record.released:
|
|
1176
|
+
record.released = True
|
|
1177
|
+
if record.local_call_content is not None:
|
|
1178
|
+
self._started_local_call_contents.register(record.local_call_content)
|
|
1179
|
+
bound = True
|
|
1180
|
+
break
|
|
1181
|
+
for barrier in self._accepted_barriers:
|
|
1182
|
+
if barrier.call_id == provider_call_id and not barrier.released:
|
|
1183
|
+
barrier.released = True
|
|
1184
|
+
self._started_local_call_contents.register(barrier.content)
|
|
1185
|
+
bound = True
|
|
1186
|
+
break
|
|
1187
|
+
if not bound:
|
|
1188
|
+
self._unbound_local_start_counts[provider_call_id] += 1
|
|
1189
|
+
await self._drain_live_prefix()
|
|
1190
|
+
await self._drain_reconciliation()
|
|
1191
|
+
|
|
1192
|
+
async def attempt_rejected(
|
|
1193
|
+
self,
|
|
1194
|
+
reason: str = "",
|
|
1195
|
+
*,
|
|
1196
|
+
status: str = HostedToolStatus.FAILED,
|
|
1197
|
+
preserve_provisional: bool = False,
|
|
1198
|
+
) -> None:
|
|
1199
|
+
"""Terminalize visible cards and conclude all rejected-attempt buffers.
|
|
1200
|
+
|
|
1201
|
+
Validation failures and retries retract presentation-only text. A
|
|
1202
|
+
user interrupt may instead preserve text the user has already read;
|
|
1203
|
+
publishing it as accepted also commits its reserved persistence batch.
|
|
1204
|
+
"""
|
|
1205
|
+
if self._published_provisional_segments:
|
|
1206
|
+
if preserve_provisional:
|
|
1207
|
+
segments = tuple(dict.fromkeys(self._published_provisional_segments.values()))
|
|
1208
|
+
await self._publish_once(PresentationAttemptAcceptedOp(self._attempt_id(), segments))
|
|
1209
|
+
else:
|
|
1210
|
+
await self._publish_once(PresentationAttemptRejectedOp(self._attempt_id()))
|
|
1211
|
+
terminal_status = (
|
|
1212
|
+
HostedToolStatus.INTERRUPTED if status == HostedToolStatus.INTERRUPTED else HostedToolStatus.FAILED
|
|
1213
|
+
)
|
|
1214
|
+
for occurrence in self._occurrences:
|
|
1215
|
+
if not occurrence.start_published or occurrence.terminal or occurrence.last_view is None:
|
|
1216
|
+
continue
|
|
1217
|
+
metadata = dict(occurrence.last_view.metadata)
|
|
1218
|
+
result_text = occurrence.last_view.result_text
|
|
1219
|
+
if terminal_status is HostedToolStatus.FAILED:
|
|
1220
|
+
metadata[TOOL_ERRORED_METADATA_KEY] = True
|
|
1221
|
+
metadata[TOOL_ERROR_MESSAGE_METADATA_KEY] = reason or "Response attempt rejected."
|
|
1222
|
+
result_text, failure_text_synthesized = _failure_text(reason, terminal_status)
|
|
1223
|
+
if failure_text_synthesized:
|
|
1224
|
+
metadata[TOOL_FAILURE_TEXT_SYNTHESIZED_METADATA_KEY] = True
|
|
1225
|
+
terminal_view = replace(
|
|
1226
|
+
occurrence.last_view,
|
|
1227
|
+
result_text=result_text,
|
|
1228
|
+
status=terminal_status,
|
|
1229
|
+
provider_status=terminal_status,
|
|
1230
|
+
phase=HostedToolPhase.TERMINAL,
|
|
1231
|
+
metadata=metadata,
|
|
1232
|
+
)
|
|
1233
|
+
operation = HostedToolStatusOp(
|
|
1234
|
+
occurrence.ordinal,
|
|
1235
|
+
occurrence.presentation_id,
|
|
1236
|
+
terminal_view,
|
|
1237
|
+
(0, len(self._live_records), 0),
|
|
1238
|
+
)
|
|
1239
|
+
await self._publish_once(operation)
|
|
1240
|
+
occurrence.terminal = True
|
|
1241
|
+
self._clear_attempt_state()
|
|
1242
|
+
|
|
1243
|
+
async def attempt_accepted(self, messages: Sequence[Message] | None = None) -> None:
|
|
1244
|
+
"""Mark validation accepted and reconcile its live-visible operations."""
|
|
1245
|
+
for occurrence in self._occurrences:
|
|
1246
|
+
if occurrence not in self._accepted_occurrences:
|
|
1247
|
+
self._accepted_occurrences.append(occurrence)
|
|
1248
|
+
if messages is not None:
|
|
1249
|
+
# The client's intermediate-text callback already owns this
|
|
1250
|
+
# response's narration, and a final reconcile over a run with
|
|
1251
|
+
# hosted output would publish it a second time.
|
|
1252
|
+
for content in intermediate_text_contents(messages):
|
|
1253
|
+
self._published_canonical_text_contents.register(content)
|
|
1254
|
+
# A logical response inside agent.run() is not necessarily the
|
|
1255
|
+
# turn's final response: local function calls can continue the
|
|
1256
|
+
# loop. Publish its canonical text/tool ordering now, but leave
|
|
1257
|
+
# the terminal FinalTextOp to the run-completion reconciliation.
|
|
1258
|
+
await self._reconcile_accepted(
|
|
1259
|
+
messages,
|
|
1260
|
+
final=False,
|
|
1261
|
+
publish_final=False,
|
|
1262
|
+
occurrence_candidates=tuple(self._occurrences),
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
async def reconcile_accepted(
|
|
1266
|
+
self,
|
|
1267
|
+
messages: Sequence[Message],
|
|
1268
|
+
*,
|
|
1269
|
+
final: bool = False,
|
|
1270
|
+
publish_final: bool = True,
|
|
1271
|
+
) -> None:
|
|
1272
|
+
"""Reconcile live events against assembled content and emit final text last.
|
|
1273
|
+
|
|
1274
|
+
``final`` marks the run-completion reconcile: no further local tool
|
|
1275
|
+
starts can arrive after it. ``publish_final`` is false while accepting
|
|
1276
|
+
one non-terminal response inside the tool loop.
|
|
1277
|
+
"""
|
|
1278
|
+
accepted_ordinals = {occurrence.ordinal for occurrence in self._accepted_occurrences}
|
|
1279
|
+
occurrence_candidates = (
|
|
1280
|
+
*self._accepted_occurrences,
|
|
1281
|
+
*(occurrence for occurrence in self._occurrences if occurrence.ordinal not in accepted_ordinals),
|
|
1282
|
+
)
|
|
1283
|
+
await self._reconcile_accepted(
|
|
1284
|
+
messages,
|
|
1285
|
+
final=final,
|
|
1286
|
+
publish_final=publish_final,
|
|
1287
|
+
occurrence_candidates=occurrence_candidates,
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
async def _reconcile_accepted(
|
|
1291
|
+
self,
|
|
1292
|
+
messages: Sequence[Message],
|
|
1293
|
+
*,
|
|
1294
|
+
final: bool,
|
|
1295
|
+
publish_final: bool,
|
|
1296
|
+
occurrence_candidates: Sequence[_HostedOccurrence],
|
|
1297
|
+
) -> None:
|
|
1298
|
+
response_key = (self.attempt_index, self.response_index)
|
|
1299
|
+
if publish_final and response_key in self._finalized_response_keys:
|
|
1300
|
+
return
|
|
1301
|
+
plan = ResponsePresentationPlan.from_messages(messages)
|
|
1302
|
+
mapped_occurrences: dict[int, _HostedOccurrence] = {}
|
|
1303
|
+
used_ordinals: set[int] = set()
|
|
1304
|
+
reconciled: list[PresentationSinkOperation] = []
|
|
1305
|
+
for operation in plan.operations:
|
|
1306
|
+
if isinstance(operation, IntermediateTextOp):
|
|
1307
|
+
reconciled.append(replace(operation, attempt_id=self._attempt_id()))
|
|
1308
|
+
continue
|
|
1309
|
+
occurrence = mapped_occurrences.get(operation.occurrence_ordinal)
|
|
1310
|
+
if occurrence is None:
|
|
1311
|
+
occurrence = self._match_reconciled_occurrence(
|
|
1312
|
+
operation.view,
|
|
1313
|
+
used_ordinals,
|
|
1314
|
+
occurrence_candidates,
|
|
1315
|
+
)
|
|
1316
|
+
mapped_occurrences[operation.occurrence_ordinal] = occurrence
|
|
1317
|
+
used_ordinals.add(occurrence.ordinal)
|
|
1318
|
+
reconciled.append(
|
|
1319
|
+
replace(
|
|
1320
|
+
operation,
|
|
1321
|
+
occurrence_ordinal=occurrence.ordinal,
|
|
1322
|
+
presentation_id=occurrence.presentation_id,
|
|
1323
|
+
)
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
accepted_segments, reconciled = self._accept_provisional_text(reconciled)
|
|
1327
|
+
reconciled = [
|
|
1328
|
+
operation
|
|
1329
|
+
for operation in reconciled
|
|
1330
|
+
if not (
|
|
1331
|
+
isinstance(operation, IntermediateTextOp)
|
|
1332
|
+
and operation.source_contents
|
|
1333
|
+
and all(content in self._published_canonical_text_contents for content in operation.source_contents)
|
|
1334
|
+
)
|
|
1335
|
+
]
|
|
1336
|
+
if self._published_provisional_segments:
|
|
1337
|
+
await self._publish_once(
|
|
1338
|
+
PresentationAttemptAcceptedOp(
|
|
1339
|
+
self._attempt_id(),
|
|
1340
|
+
tuple(accepted_segments),
|
|
1341
|
+
)
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
self._accepted_barriers = self._plan_barriers(messages)
|
|
1345
|
+
for barrier in self._accepted_barriers:
|
|
1346
|
+
if barrier.content in self._started_local_call_contents:
|
|
1347
|
+
barrier.released = True
|
|
1348
|
+
continue
|
|
1349
|
+
if self._unbound_local_start_counts[barrier.call_id] > 0:
|
|
1350
|
+
barrier.released = True
|
|
1351
|
+
self._started_local_call_contents.register(barrier.content)
|
|
1352
|
+
self._unbound_local_start_counts[barrier.call_id] -= 1
|
|
1353
|
+
if self._unbound_local_start_counts[barrier.call_id] == 0:
|
|
1354
|
+
del self._unbound_local_start_counts[barrier.call_id]
|
|
1355
|
+
if final:
|
|
1356
|
+
# Calls that never entered the tool pipeline (unknown tool,
|
|
1357
|
+
# pre-pipeline argument rejection) publish no start; once the run
|
|
1358
|
+
# is over their barriers can only gate FinalTextOp forever.
|
|
1359
|
+
for barrier in self._accepted_barriers:
|
|
1360
|
+
barrier.released = True
|
|
1361
|
+
pending: list[PresentationSinkOperation] = list(reconciled)
|
|
1362
|
+
if publish_final:
|
|
1363
|
+
pending.append(
|
|
1364
|
+
FinalTextOp(
|
|
1365
|
+
plan.final_text,
|
|
1366
|
+
structured_output_completed=not plan.final_text.strip() and plan.structured_output_completed,
|
|
1367
|
+
)
|
|
1368
|
+
)
|
|
1369
|
+
self._finalized_response_keys.add(response_key)
|
|
1370
|
+
self._pending_reconciliation = deque(pending)
|
|
1371
|
+
await self._drain_reconciliation()
|
|
1372
|
+
|
|
1373
|
+
async def publish_blocking(self, messages: Sequence[Message]) -> None:
|
|
1374
|
+
"""Plan an assembled blocking response and publish its final sentinel last."""
|
|
1375
|
+
if self.attempt_index < 0:
|
|
1376
|
+
await self.attempt_started()
|
|
1377
|
+
plan = ResponsePresentationPlan.from_messages(messages)
|
|
1378
|
+
mapped: dict[int, _HostedOccurrence] = {}
|
|
1379
|
+
for operation in plan.operations:
|
|
1380
|
+
outbound: PresentationOperation = operation
|
|
1381
|
+
if not isinstance(operation, IntermediateTextOp):
|
|
1382
|
+
occurrence = mapped.get(operation.occurrence_ordinal)
|
|
1383
|
+
if occurrence is None:
|
|
1384
|
+
occurrence = self._allocate_occurrence(operation.view)
|
|
1385
|
+
mapped[operation.occurrence_ordinal] = occurrence
|
|
1386
|
+
outbound = replace(
|
|
1387
|
+
operation,
|
|
1388
|
+
occurrence_ordinal=occurrence.ordinal,
|
|
1389
|
+
presentation_id=occurrence.presentation_id,
|
|
1390
|
+
)
|
|
1391
|
+
await self._publish_once(outbound)
|
|
1392
|
+
await self._publish_sink(
|
|
1393
|
+
FinalTextOp(
|
|
1394
|
+
plan.final_text,
|
|
1395
|
+
structured_output_completed=not plan.final_text.strip() and plan.structured_output_completed,
|
|
1396
|
+
)
|
|
1397
|
+
)
|
|
1398
|
+
|
|
1399
|
+
async def _observe_call(self, content: Content) -> None:
|
|
1400
|
+
view = adapt_hosted_tool(content)
|
|
1401
|
+
occurrence = self._call_occurrence(content, view)
|
|
1402
|
+
if occurrence.terminal and not occurrence.call_seen:
|
|
1403
|
+
occurrence.call_seen = True
|
|
1404
|
+
if occurrence.last_view is not None:
|
|
1405
|
+
occurrence.last_view = replace(
|
|
1406
|
+
occurrence.last_view,
|
|
1407
|
+
tool_name=view.tool_name,
|
|
1408
|
+
display_title=view.display_title,
|
|
1409
|
+
arguments=view.arguments,
|
|
1410
|
+
)
|
|
1411
|
+
return
|
|
1412
|
+
occurrence.call_seen = True
|
|
1413
|
+
occurrence.last_view = view
|
|
1414
|
+
operations = _hosted_ops(
|
|
1415
|
+
occurrence=occurrence.ordinal,
|
|
1416
|
+
view=view,
|
|
1417
|
+
order=(0, len(self._live_records), 0),
|
|
1418
|
+
start=not self._has_start_record(occurrence),
|
|
1419
|
+
result_content=False,
|
|
1420
|
+
)
|
|
1421
|
+
for operation in operations:
|
|
1422
|
+
await self._append_live(replace(operation, presentation_id=occurrence.presentation_id))
|
|
1423
|
+
status = normalize_hosted_tool_status(view.status)
|
|
1424
|
+
if view.phase == HostedToolPhase.TERMINAL or status in _TERMINAL_STATUSES:
|
|
1425
|
+
occurrence.terminal = True
|
|
1426
|
+
|
|
1427
|
+
async def _observe_result(self, content: Content) -> None:
|
|
1428
|
+
key = self._pairing_key(content)
|
|
1429
|
+
provider_item_id = content.provider_item_id or ""
|
|
1430
|
+
occurrence = (
|
|
1431
|
+
self._provider_item_candidates[provider_item_id][-1]
|
|
1432
|
+
if provider_item_id and self._provider_item_candidates[provider_item_id]
|
|
1433
|
+
else next(
|
|
1434
|
+
(candidate for candidate in reversed(self._open_occurrences[key]) if not candidate.result_seen),
|
|
1435
|
+
None,
|
|
1436
|
+
)
|
|
1437
|
+
)
|
|
1438
|
+
if occurrence is None:
|
|
1439
|
+
view = adapt_hosted_tool(None, content)
|
|
1440
|
+
occurrence = self._allocate_occurrence(view, pairing_key=key)
|
|
1441
|
+
view = adapt_hosted_tool(None, content)
|
|
1442
|
+
if occurrence.call_seen:
|
|
1443
|
+
view = replace(
|
|
1444
|
+
view,
|
|
1445
|
+
tool_name=occurrence.last_view.tool_name if occurrence.last_view is not None else view.tool_name,
|
|
1446
|
+
display_title=(
|
|
1447
|
+
occurrence.last_view.display_title if occurrence.last_view is not None else view.display_title
|
|
1448
|
+
),
|
|
1449
|
+
arguments=occurrence.last_view.arguments if occurrence.last_view is not None else view.arguments,
|
|
1450
|
+
)
|
|
1451
|
+
occurrence.result_seen = True
|
|
1452
|
+
occurrence.last_view = view
|
|
1453
|
+
operations = _hosted_ops(
|
|
1454
|
+
occurrence=occurrence.ordinal,
|
|
1455
|
+
view=view,
|
|
1456
|
+
order=(0, len(self._live_records), 0),
|
|
1457
|
+
start=not self._has_start_record(occurrence),
|
|
1458
|
+
result_content=True,
|
|
1459
|
+
)
|
|
1460
|
+
for operation in operations:
|
|
1461
|
+
await self._append_live(replace(operation, presentation_id=occurrence.presentation_id))
|
|
1462
|
+
status = normalize_hosted_tool_status(view.status)
|
|
1463
|
+
if view.phase == HostedToolPhase.TERMINAL or status in _TERMINAL_STATUSES:
|
|
1464
|
+
occurrence.terminal = True
|
|
1465
|
+
|
|
1466
|
+
def _call_occurrence(self, content: Content, view: HostedToolView) -> _HostedOccurrence:
|
|
1467
|
+
provider_item_id = content.provider_item_id or ""
|
|
1468
|
+
if provider_item_id and self._provider_item_candidates[provider_item_id]:
|
|
1469
|
+
return self._provider_item_candidates[provider_item_id][-1]
|
|
1470
|
+
key = self._pairing_key(content)
|
|
1471
|
+
for occurrence in reversed(self._open_occurrences[key]):
|
|
1472
|
+
if not occurrence.call_seen or not occurrence.result_seen:
|
|
1473
|
+
return occurrence
|
|
1474
|
+
return self._allocate_occurrence(view, pairing_key=key)
|
|
1475
|
+
|
|
1476
|
+
def _allocate_occurrence(
|
|
1477
|
+
self,
|
|
1478
|
+
view: HostedToolView,
|
|
1479
|
+
*,
|
|
1480
|
+
pairing_key: tuple[str, str] | None = None,
|
|
1481
|
+
) -> _HostedOccurrence:
|
|
1482
|
+
ordinal = self.next_occurrence_ordinal
|
|
1483
|
+
self.next_occurrence_ordinal += 1
|
|
1484
|
+
occurrence = _HostedOccurrence(
|
|
1485
|
+
ordinal=ordinal,
|
|
1486
|
+
presentation_id=(
|
|
1487
|
+
f"hosted:{self.run_generation}:{max(self.attempt_index, 0)}:{self.response_index}:{ordinal}"
|
|
1488
|
+
),
|
|
1489
|
+
pairing_key=pairing_key or ("call_id", view.provider_call_id),
|
|
1490
|
+
provider_item_id=str(view.metadata.get("provider_item_id", "")),
|
|
1491
|
+
last_view=view,
|
|
1492
|
+
)
|
|
1493
|
+
self._occurrences.append(occurrence)
|
|
1494
|
+
self._open_occurrences[occurrence.pairing_key].append(occurrence)
|
|
1495
|
+
if occurrence.provider_item_id:
|
|
1496
|
+
self._provider_item_candidates[occurrence.provider_item_id].append(occurrence)
|
|
1497
|
+
return occurrence
|
|
1498
|
+
|
|
1499
|
+
def _match_reconciled_occurrence(
|
|
1500
|
+
self,
|
|
1501
|
+
view: HostedToolView,
|
|
1502
|
+
used_ordinals: set[int],
|
|
1503
|
+
candidates: Sequence[_HostedOccurrence],
|
|
1504
|
+
) -> _HostedOccurrence:
|
|
1505
|
+
provider_item_id = str(view.metadata.get("provider_item_id", ""))
|
|
1506
|
+
if provider_item_id:
|
|
1507
|
+
for occurrence in candidates:
|
|
1508
|
+
if occurrence.provider_item_id != provider_item_id:
|
|
1509
|
+
continue
|
|
1510
|
+
if occurrence.ordinal not in used_ordinals:
|
|
1511
|
+
return occurrence
|
|
1512
|
+
for occurrence in candidates:
|
|
1513
|
+
if occurrence.ordinal in used_ordinals:
|
|
1514
|
+
continue
|
|
1515
|
+
if occurrence.pairing_key == ("call_id", view.provider_call_id):
|
|
1516
|
+
return occurrence
|
|
1517
|
+
if occurrence.last_view is not None and (
|
|
1518
|
+
occurrence.last_view.family,
|
|
1519
|
+
occurrence.last_view.tool_name,
|
|
1520
|
+
) == (view.family, view.tool_name):
|
|
1521
|
+
return occurrence
|
|
1522
|
+
return self._allocate_occurrence(view)
|
|
1523
|
+
|
|
1524
|
+
def _pairing_key(self, content: Content) -> tuple[str, str]:
|
|
1525
|
+
if content.type.startswith("image_generation_"):
|
|
1526
|
+
return "image_id", content.image_id or ""
|
|
1527
|
+
return "call_id", content.call_id or ""
|
|
1528
|
+
|
|
1529
|
+
def _has_start_record(self, occurrence: _HostedOccurrence) -> bool:
|
|
1530
|
+
return any(
|
|
1531
|
+
isinstance(record.operation, HostedToolStartOp)
|
|
1532
|
+
and record.operation.occurrence_ordinal == occurrence.ordinal
|
|
1533
|
+
for record in self._live_records
|
|
1534
|
+
)
|
|
1535
|
+
|
|
1536
|
+
async def _append_live(self, operation: HostedToolOperation) -> None:
|
|
1537
|
+
self._live_records.append(_LiveRecord(operation=operation))
|
|
1538
|
+
await self._drain_live_prefix()
|
|
1539
|
+
|
|
1540
|
+
async def _seal_live_text_segments(self) -> None:
|
|
1541
|
+
"""Publish completed text only once later output proves it intermediate."""
|
|
1542
|
+
if not self._live_text_segments:
|
|
1543
|
+
return
|
|
1544
|
+
for segment_id, segment in self._live_text_segments.items():
|
|
1545
|
+
segment.record.operation = IntermediateTextOp(
|
|
1546
|
+
"".join(segment.parts),
|
|
1547
|
+
segment.order,
|
|
1548
|
+
attempt_id=self._attempt_id(),
|
|
1549
|
+
segment_ids=(segment_id,),
|
|
1550
|
+
provisional=True,
|
|
1551
|
+
)
|
|
1552
|
+
segment.record.text_segment_id = ""
|
|
1553
|
+
self._live_text_segments.clear()
|
|
1554
|
+
await self._drain_live_prefix()
|
|
1555
|
+
|
|
1556
|
+
async def _drain_live_prefix(self) -> None:
|
|
1557
|
+
while self._live_cursor < len(self._live_records):
|
|
1558
|
+
record = self._live_records[self._live_cursor]
|
|
1559
|
+
if record.local_call_id:
|
|
1560
|
+
if not record.released:
|
|
1561
|
+
return
|
|
1562
|
+
self._live_cursor += 1
|
|
1563
|
+
continue
|
|
1564
|
+
if record.text_segment_id:
|
|
1565
|
+
return
|
|
1566
|
+
if isinstance(record.operation, IntermediateTextOp):
|
|
1567
|
+
if not record.operation.provisional:
|
|
1568
|
+
return
|
|
1569
|
+
await self._publish_once(record.operation)
|
|
1570
|
+
for segment_id in record.operation.segment_ids:
|
|
1571
|
+
self._published_provisional_segments[segment_id] = record.operation
|
|
1572
|
+
self._live_cursor += 1
|
|
1573
|
+
continue
|
|
1574
|
+
if record.operation is None:
|
|
1575
|
+
self._live_cursor += 1
|
|
1576
|
+
continue
|
|
1577
|
+
await self._publish_once(record.operation)
|
|
1578
|
+
if isinstance(record.operation, HostedToolStartOp):
|
|
1579
|
+
occurrence = self._occurrence_by_ordinal(record.operation.occurrence_ordinal)
|
|
1580
|
+
occurrence.start_published = True
|
|
1581
|
+
self._live_cursor += 1
|
|
1582
|
+
|
|
1583
|
+
async def _drain_reconciliation(self) -> None:
|
|
1584
|
+
while self._pending_reconciliation:
|
|
1585
|
+
operation = self._pending_reconciliation[0]
|
|
1586
|
+
if isinstance(operation, PresentationAttemptAcceptedOp | PresentationAttemptRejectedOp):
|
|
1587
|
+
self._pending_reconciliation.popleft()
|
|
1588
|
+
await self._publish_once(operation)
|
|
1589
|
+
continue
|
|
1590
|
+
if isinstance(operation, FinalTextOp):
|
|
1591
|
+
if any(not barrier.released for barrier in self._accepted_barriers):
|
|
1592
|
+
return
|
|
1593
|
+
elif any(not barrier.released and barrier.order < operation.order for barrier in self._accepted_barriers):
|
|
1594
|
+
return
|
|
1595
|
+
self._pending_reconciliation.popleft()
|
|
1596
|
+
await self._publish_once(operation)
|
|
1597
|
+
|
|
1598
|
+
def _plan_barriers(self, messages: Sequence[Message]) -> list[_PlanBarrier]:
|
|
1599
|
+
barriers: list[_PlanBarrier] = []
|
|
1600
|
+
for message_index, message in enumerate(messages):
|
|
1601
|
+
for content_index, content in enumerate(message.contents):
|
|
1602
|
+
if content.type == "function_call" and not content.informational_only:
|
|
1603
|
+
barriers.append(
|
|
1604
|
+
_PlanBarrier(
|
|
1605
|
+
_provider_call_id(content),
|
|
1606
|
+
(message_index, content_index, 0),
|
|
1607
|
+
content,
|
|
1608
|
+
)
|
|
1609
|
+
)
|
|
1610
|
+
return barriers
|
|
1611
|
+
|
|
1612
|
+
def _occurrence_by_ordinal(self, ordinal: int) -> _HostedOccurrence:
|
|
1613
|
+
return next(occurrence for occurrence in self._occurrences if occurrence.ordinal == ordinal)
|
|
1614
|
+
|
|
1615
|
+
async def _publish_once(self, operation: PresentationSinkOperation) -> None:
|
|
1616
|
+
signature = self._signature(operation)
|
|
1617
|
+
if signature in self._published_signatures:
|
|
1618
|
+
return
|
|
1619
|
+
self._published_signatures.add(signature)
|
|
1620
|
+
if isinstance(operation, HostedToolResultOp) or (
|
|
1621
|
+
isinstance(operation, HostedToolStatusOp)
|
|
1622
|
+
and normalize_hosted_tool_status(operation.view.status) in _TERMINAL_STATUSES
|
|
1623
|
+
):
|
|
1624
|
+
self.published_terminal_signatures.add(signature)
|
|
1625
|
+
if isinstance(operation, IntermediateTextOp) and not operation.provisional:
|
|
1626
|
+
for content in operation.source_contents:
|
|
1627
|
+
self._published_canonical_text_contents.register(content)
|
|
1628
|
+
await self._publish_sink(operation)
|
|
1629
|
+
|
|
1630
|
+
async def _publish_sink(self, operation: PresentationSinkOperation) -> None:
|
|
1631
|
+
outcome = self.publish(operation)
|
|
1632
|
+
if inspect.isawaitable(outcome):
|
|
1633
|
+
await outcome
|
|
1634
|
+
|
|
1635
|
+
def _signature(self, operation: PresentationSinkOperation) -> tuple[Any, ...]:
|
|
1636
|
+
if isinstance(operation, FinalTextOp):
|
|
1637
|
+
return (
|
|
1638
|
+
"final",
|
|
1639
|
+
self.attempt_index,
|
|
1640
|
+
self.response_index,
|
|
1641
|
+
operation.text,
|
|
1642
|
+
operation.structured_output_completed,
|
|
1643
|
+
)
|
|
1644
|
+
if isinstance(operation, PresentationAttemptAcceptedOp):
|
|
1645
|
+
return ("presentation_accepted", operation.attempt_id)
|
|
1646
|
+
if isinstance(operation, PresentationAttemptRejectedOp):
|
|
1647
|
+
return ("presentation_rejected", operation.attempt_id)
|
|
1648
|
+
if isinstance(operation, IntermediateTextOp):
|
|
1649
|
+
if operation.segment_ids:
|
|
1650
|
+
return ("text", operation.attempt_id, operation.segment_ids, operation.text)
|
|
1651
|
+
return ("text", self.attempt_index, self.response_index, operation.order, operation.text)
|
|
1652
|
+
view = operation.view
|
|
1653
|
+
base = (type(operation).__name__, operation.presentation_id)
|
|
1654
|
+
if isinstance(operation, HostedToolStartOp):
|
|
1655
|
+
return base
|
|
1656
|
+
if isinstance(operation, HostedToolArgsOp):
|
|
1657
|
+
return (*base, json.dumps(_json_safe(view.arguments), sort_keys=True, ensure_ascii=False))
|
|
1658
|
+
if isinstance(operation, HostedToolProgressOp):
|
|
1659
|
+
images = tuple((content.uri, content.media_type, content.file_id) for content in view.image_contents)
|
|
1660
|
+
return (*base, view.phase, view.provider_status, view.result_text, images)
|
|
1661
|
+
if isinstance(operation, HostedToolStatusOp):
|
|
1662
|
+
return (*base, view.status, view.provider_status)
|
|
1663
|
+
artifacts = tuple((content.file_id, content.uri, content.media_type) for content in view.artifacts)
|
|
1664
|
+
images = tuple((content.uri, content.media_type, content.file_id) for content in view.image_contents)
|
|
1665
|
+
return (*base, view.status, view.provider_status, view.result_text, images, artifacts)
|
|
1666
|
+
|
|
1667
|
+
def _clear_attempt_state(self) -> None:
|
|
1668
|
+
self._live_records.clear()
|
|
1669
|
+
self._live_cursor = 0
|
|
1670
|
+
self._occurrences.clear()
|
|
1671
|
+
self._open_occurrences.clear()
|
|
1672
|
+
self._provider_item_candidates.clear()
|
|
1673
|
+
self._live_text_segments.clear()
|
|
1674
|
+
self._published_provisional_segments.clear()
|
|
1675
|
+
self._accepted_barriers.clear()
|
|
1676
|
+
self._pending_reconciliation.clear()
|
|
1677
|
+
self._saw_incremental_contents = False
|
|
1678
|
+
self._saw_hosted_contents = False
|
|
1679
|
+
|
|
1680
|
+
def _attempt_id(self) -> str:
|
|
1681
|
+
return f"presentation:{self.run_generation}:{self.response_index}:{max(self.attempt_index, 0)}"
|
|
1682
|
+
|
|
1683
|
+
def _accept_provisional_text(
|
|
1684
|
+
self,
|
|
1685
|
+
operations: list[PresentationSinkOperation],
|
|
1686
|
+
) -> tuple[list[IntermediateTextOp], list[PresentationSinkOperation]]:
|
|
1687
|
+
"""Match live provisional text to canonical intermediate operations."""
|
|
1688
|
+
if not self._published_provisional_segments:
|
|
1689
|
+
return [], operations
|
|
1690
|
+
accepted: list[IntermediateTextOp] = []
|
|
1691
|
+
retained: list[PresentationSinkOperation] = []
|
|
1692
|
+
accepted_ids: set[str] = set()
|
|
1693
|
+
published_by_text: dict[str, list[IntermediateTextOp]] = defaultdict(list)
|
|
1694
|
+
for operation in dict.fromkeys(self._published_provisional_segments.values()):
|
|
1695
|
+
published_by_text[operation.text].append(operation)
|
|
1696
|
+
|
|
1697
|
+
for operation in operations:
|
|
1698
|
+
if not isinstance(operation, IntermediateTextOp):
|
|
1699
|
+
retained.append(operation)
|
|
1700
|
+
continue
|
|
1701
|
+
matched: list[IntermediateTextOp] = []
|
|
1702
|
+
if operation.segment_ids and all(
|
|
1703
|
+
segment_id in self._published_provisional_segments for segment_id in operation.segment_ids
|
|
1704
|
+
):
|
|
1705
|
+
matched = list(
|
|
1706
|
+
dict.fromkeys(
|
|
1707
|
+
self._published_provisional_segments[segment_id] for segment_id in operation.segment_ids
|
|
1708
|
+
)
|
|
1709
|
+
)
|
|
1710
|
+
elif candidates := published_by_text.get(operation.text):
|
|
1711
|
+
matched = [
|
|
1712
|
+
candidate for candidate in candidates if not accepted_ids.intersection(candidate.segment_ids)
|
|
1713
|
+
][:1]
|
|
1714
|
+
if not matched:
|
|
1715
|
+
retained.append(operation)
|
|
1716
|
+
continue
|
|
1717
|
+
# The streamed fragments are presentation-only objects, while
|
|
1718
|
+
# ``operation.source_contents`` belongs to the assembled response
|
|
1719
|
+
# that later becomes the final run transcript. Remember that
|
|
1720
|
+
# canonical identity now so run-completion reconciliation cannot
|
|
1721
|
+
# re-publish an already accepted provisional segment.
|
|
1722
|
+
for content in operation.source_contents:
|
|
1723
|
+
self._published_canonical_text_contents.register(content)
|
|
1724
|
+
for candidate in matched:
|
|
1725
|
+
if accepted_ids.intersection(candidate.segment_ids):
|
|
1726
|
+
continue
|
|
1727
|
+
accepted.append(candidate)
|
|
1728
|
+
accepted_ids.update(candidate.segment_ids)
|
|
1729
|
+
return accepted, retained
|
|
1730
|
+
|
|
1731
|
+
|
|
1732
|
+
__all__ = [
|
|
1733
|
+
"CodeHostedAdapter",
|
|
1734
|
+
"FinalSegment",
|
|
1735
|
+
"FinalTextOp",
|
|
1736
|
+
"GenericHostedAdapter",
|
|
1737
|
+
"HostedPresentationBridge",
|
|
1738
|
+
"HostedToolArgsOp",
|
|
1739
|
+
"HostedToolProgressOp",
|
|
1740
|
+
"HostedToolResultOp",
|
|
1741
|
+
"HostedToolStartOp",
|
|
1742
|
+
"HostedToolStatusOp",
|
|
1743
|
+
"HostedToolView",
|
|
1744
|
+
"ImageHostedAdapter",
|
|
1745
|
+
"IntermediateTextOp",
|
|
1746
|
+
"McpHostedAdapter",
|
|
1747
|
+
"PresentationAttemptAcceptedOp",
|
|
1748
|
+
"PresentationAttemptRejectedOp",
|
|
1749
|
+
"PresentationOperation",
|
|
1750
|
+
"PresentationSinkOperation",
|
|
1751
|
+
"ResponsePresentationPlan",
|
|
1752
|
+
"SearchHostedAdapter",
|
|
1753
|
+
"ShellHostedAdapter",
|
|
1754
|
+
"adapt_hosted_tool",
|
|
1755
|
+
"cross_provider_hosted_degradations",
|
|
1756
|
+
"get_hosted_adapter",
|
|
1757
|
+
"hosted_context_summary",
|
|
1758
|
+
"hosted_replay_status",
|
|
1759
|
+
"is_legacy_server_executed_informational_call",
|
|
1760
|
+
"next_hosted_run_generation",
|
|
1761
|
+
"normalize_hosted_replay_content",
|
|
1762
|
+
]
|