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,1330 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
3
|
+
# Contains code adapted from Microsoft Agent Framework (MIT License; see NOTICE).
|
|
4
|
+
|
|
5
|
+
"""Chrys-owned MCP client tool engine.
|
|
6
|
+
|
|
7
|
+
This module owns the MCP surface Chrys consumes:
|
|
8
|
+
stdio and streamable-HTTP transports, connection lifecycle, tool/prompt
|
|
9
|
+
loading, tool/prompt calls, result parsing, and MCP client spans. WebSocket
|
|
10
|
+
and SEP-2663 long-running task driving are intentionally out of scope.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import contextlib
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
from abc import abstractmethod
|
|
21
|
+
from collections.abc import Awaitable, Callable, Collection, Mapping, Sequence
|
|
22
|
+
from contextlib import AsyncExitStack
|
|
23
|
+
from datetime import timedelta
|
|
24
|
+
from functools import partial
|
|
25
|
+
from typing import TYPE_CHECKING, Any, Self, cast
|
|
26
|
+
|
|
27
|
+
from opentelemetry import propagate
|
|
28
|
+
from opentelemetry import trace as otel_trace
|
|
29
|
+
|
|
30
|
+
from chrys.foundation.branding import APP_DISPLAY_NAME
|
|
31
|
+
from chrys.foundation.text.model_json import model_json
|
|
32
|
+
from chrys.foundation.tool_call_context import set_tool_context
|
|
33
|
+
from chrys.foundation.trajectory.context import current_tool_operation_id
|
|
34
|
+
from chrys.foundation.trajectory.event_types import WaitCategory
|
|
35
|
+
from chrys.kernel import Content, normalize_tools
|
|
36
|
+
from chrys.kernel._tool_expansion import _set_tool_expander
|
|
37
|
+
from chrys.kernel.exceptions import ModelVisibleToolError, ToolException, ToolExecutionException
|
|
38
|
+
from chrys.kernel.instrumentation import OtelAttr, create_mcp_client_span, set_mcp_span_error
|
|
39
|
+
from chrys.kernel.middleware import FunctionInvocationContext
|
|
40
|
+
from chrys.kernel.tools import FunctionTool
|
|
41
|
+
from chrys.service.mcp.content import INVALID_AUDIO_TEXT, INVALID_IMAGE_TEXT, INVALID_RESOURCE_TEXT, decode_media_base64
|
|
42
|
+
from chrys.service.mcp.result_limits import (
|
|
43
|
+
_MCP_PROMPT_TOOL_KEY,
|
|
44
|
+
DEFAULT_MCP_TOOL_RESULT_MAX_TOKENS,
|
|
45
|
+
sanitize_mcp_result_binary,
|
|
46
|
+
truncate_mcp_error,
|
|
47
|
+
)
|
|
48
|
+
from chrys.service.trajectory.waits import WaitOutcome, WaitTrace
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from contextlib import _AsyncGeneratorContextManager # type: ignore[attr-defined]
|
|
52
|
+
|
|
53
|
+
from httpx import AsyncClient
|
|
54
|
+
from mcp import types
|
|
55
|
+
from mcp.client.session import ClientSession
|
|
56
|
+
from mcp.shared.session import RequestResponder
|
|
57
|
+
|
|
58
|
+
from chrys.kernel.types import ToolTypes
|
|
59
|
+
|
|
60
|
+
logger = logging.getLogger(__name__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_MCP_REMOTE_NAME_KEY = "_mcp_remote_name"
|
|
64
|
+
_MCP_NORMALIZED_NAME_KEY = "_mcp_normalized_name"
|
|
65
|
+
_MCP_GLOBAL_EXTRA_ARGS_KEY = "*"
|
|
66
|
+
# Runtime argument names retained for diagnostics and regression coverage.
|
|
67
|
+
# Never use this catalog to reject an argument declared by the remote tool:
|
|
68
|
+
# provenance, not spelling, is the security boundary.
|
|
69
|
+
_MCP_FRAMEWORK_DENYLIST: frozenset[str] = frozenset(
|
|
70
|
+
{
|
|
71
|
+
"chat_options",
|
|
72
|
+
"tools",
|
|
73
|
+
"tool_choice",
|
|
74
|
+
"session",
|
|
75
|
+
"thread",
|
|
76
|
+
"conversation_id",
|
|
77
|
+
"options",
|
|
78
|
+
"response_format",
|
|
79
|
+
"_meta",
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
# ``ErrorData.data`` of the JSON-RPC errors the HTTP transport makes up when its
|
|
83
|
+
# own request fails. Their message is local httpx detail — the configured URL,
|
|
84
|
+
# which can carry credentials, included — not an answer from the server.
|
|
85
|
+
LOCAL_HTTP_FAILURE_ERROR_DATA = "chrys.local_http_failure"
|
|
86
|
+
|
|
87
|
+
LOG_LEVEL_MAPPING: dict[types.LoggingLevel, int] = {
|
|
88
|
+
"debug": logging.DEBUG,
|
|
89
|
+
"info": logging.INFO,
|
|
90
|
+
"notice": logging.INFO,
|
|
91
|
+
"warning": logging.WARNING,
|
|
92
|
+
"error": logging.ERROR,
|
|
93
|
+
"critical": logging.CRITICAL,
|
|
94
|
+
"alert": logging.CRITICAL,
|
|
95
|
+
"emergency": logging.CRITICAL,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _get_input_model_from_mcp_prompt(prompt: types.Prompt) -> dict[str, Any]:
|
|
100
|
+
if not prompt.arguments:
|
|
101
|
+
return {"type": "object", "properties": {}}
|
|
102
|
+
|
|
103
|
+
properties: dict[str, Any] = {}
|
|
104
|
+
required: list[str] = []
|
|
105
|
+
for prompt_argument in prompt.arguments:
|
|
106
|
+
properties[prompt_argument.name] = {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"description": prompt_argument.description if hasattr(prompt_argument, "description") else "",
|
|
109
|
+
}
|
|
110
|
+
if prompt_argument.required:
|
|
111
|
+
required.append(prompt_argument.name)
|
|
112
|
+
|
|
113
|
+
schema: dict[str, Any] = {"type": "object", "properties": properties}
|
|
114
|
+
if required:
|
|
115
|
+
schema["required"] = required
|
|
116
|
+
return schema
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _normalize_mcp_name(name: str) -> str:
|
|
120
|
+
# Keep the exposed name portable across model providers. MCP servers may
|
|
121
|
+
# advertise a broader character set, but OpenAI-compatible function tools
|
|
122
|
+
# reject periods and other punctuation before the first model response.
|
|
123
|
+
return re.sub(r"[^A-Za-z0-9_-]", "-", name)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _build_prefixed_mcp_name(normalized_name: str, tool_name_prefix: str | None) -> str:
|
|
127
|
+
if not tool_name_prefix:
|
|
128
|
+
return normalized_name
|
|
129
|
+
normalized_prefix = _normalize_mcp_name(tool_name_prefix).rstrip("_.-")
|
|
130
|
+
if not normalized_prefix:
|
|
131
|
+
return normalized_name
|
|
132
|
+
trimmed_name = normalized_name.lstrip("_.-")
|
|
133
|
+
return f"{normalized_prefix}_{trimmed_name}" if trimmed_name else normalized_prefix
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _normalize_additional_tool_argument_names(
|
|
137
|
+
additional_tool_argument_names: Sequence[str] | Mapping[str, str | Sequence[str]] | None,
|
|
138
|
+
) -> tuple[set[str], dict[str, set[str]]]:
|
|
139
|
+
if additional_tool_argument_names is None:
|
|
140
|
+
return set(), {}
|
|
141
|
+
if isinstance(additional_tool_argument_names, str):
|
|
142
|
+
return {additional_tool_argument_names}, {}
|
|
143
|
+
if isinstance(additional_tool_argument_names, Mapping):
|
|
144
|
+
global_extras: set[str] = set()
|
|
145
|
+
per_tool_extras: dict[str, set[str]] = {}
|
|
146
|
+
for tool_name, names in additional_tool_argument_names.items():
|
|
147
|
+
# The input contract leaves only a string sequence after the string case.
|
|
148
|
+
names_set = {names} if isinstance(names, str) else set(cast(Sequence[str], names))
|
|
149
|
+
if tool_name == _MCP_GLOBAL_EXTRA_ARGS_KEY:
|
|
150
|
+
global_extras.update(names_set)
|
|
151
|
+
else:
|
|
152
|
+
# Mapping keys are declared strings; the literal comparison only refines their value.
|
|
153
|
+
per_tool_extras[cast(str, tool_name)] = names_set
|
|
154
|
+
return global_extras, per_tool_extras
|
|
155
|
+
return set(additional_tool_argument_names), {}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _mcp_config_candidate_names(*, local_name: str, normalized_name: str, remote_name: str) -> tuple[str, ...]:
|
|
159
|
+
"""Return safe configuration names for MCP allow/approval matching."""
|
|
160
|
+
names = [remote_name]
|
|
161
|
+
if normalized_name == remote_name and local_name != remote_name:
|
|
162
|
+
names.append(local_name)
|
|
163
|
+
return tuple(names)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _mcp_config_names_for(function: FunctionTool) -> tuple[str, ...]:
|
|
167
|
+
"""Return the ``allowed_tools``/``always_load`` names that select *function*; none without MCP name stamps."""
|
|
168
|
+
additional = function.additional_properties or {}
|
|
169
|
+
normalized_name = additional.get(_MCP_NORMALIZED_NAME_KEY)
|
|
170
|
+
remote_name = additional.get(_MCP_REMOTE_NAME_KEY)
|
|
171
|
+
if not isinstance(normalized_name, str) or not isinstance(remote_name, str):
|
|
172
|
+
return ()
|
|
173
|
+
return _mcp_config_candidate_names(
|
|
174
|
+
local_name=function.name,
|
|
175
|
+
normalized_name=normalized_name,
|
|
176
|
+
remote_name=remote_name,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _inject_otel_into_mcp_meta(
|
|
181
|
+
meta: dict[str, Any] | None = None,
|
|
182
|
+
*,
|
|
183
|
+
overwrite: bool = False,
|
|
184
|
+
) -> dict[str, Any] | None:
|
|
185
|
+
carrier: dict[str, str] = {}
|
|
186
|
+
propagate.inject(carrier)
|
|
187
|
+
if not carrier:
|
|
188
|
+
return meta
|
|
189
|
+
|
|
190
|
+
if meta is None:
|
|
191
|
+
meta = {}
|
|
192
|
+
for key, value in carrier.items():
|
|
193
|
+
if overwrite or key not in meta:
|
|
194
|
+
meta[key] = value
|
|
195
|
+
return meta
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _server_error(message: str, subject: str, *, inner_exception: Exception | None = None) -> ModelVisibleToolError:
|
|
199
|
+
"""Return the error the model reads for the server's answer to a call: *message*, already bounded.
|
|
200
|
+
|
|
201
|
+
A blank message (or one that is only ``Error:``) says so, naming *subject*
|
|
202
|
+
(``MCP tool 'x'``), rather than leave the model the generic fixed line.
|
|
203
|
+
"""
|
|
204
|
+
error = ModelVisibleToolError(message, inner_exception=inner_exception)
|
|
205
|
+
if error.result_text is None:
|
|
206
|
+
error = ModelVisibleToolError(
|
|
207
|
+
f"{subject} reported an error without an error message.", inner_exception=inner_exception
|
|
208
|
+
)
|
|
209
|
+
return error
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _mcp_error_exception(error: Exception, message: str, subject: str) -> ToolExecutionException:
|
|
213
|
+
"""Choose what to raise for a request that ended in *error*, whose bounded text is *message*.
|
|
214
|
+
|
|
215
|
+
A JSON-RPC error the server sent back (``-32602 Invalid params``, say) is
|
|
216
|
+
its answer to this call, and the MCP SDK's own errors (timeout, connection
|
|
217
|
+
closed) are fixed text, so the model reads either one. The HTTP
|
|
218
|
+
transport's made-up errors carry local detail and reach only the user.
|
|
219
|
+
"""
|
|
220
|
+
from mcp.shared.exceptions import McpError
|
|
221
|
+
|
|
222
|
+
if isinstance(error, McpError) and error.error.data != LOCAL_HTTP_FAILURE_ERROR_DATA:
|
|
223
|
+
return _server_error(message, subject, inner_exception=error)
|
|
224
|
+
return ToolExecutionException(message, inner_exception=error)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _should_propagate_cancelled_error(ex: BaseException) -> bool:
|
|
228
|
+
if not isinstance(ex, asyncio.CancelledError):
|
|
229
|
+
return False
|
|
230
|
+
task = asyncio.current_task()
|
|
231
|
+
return task is not None and task.cancelling() > 0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _describe_error(ex: BaseException) -> str:
|
|
235
|
+
"""Unwrap task-group and cancellation wrappers to name the actual failure."""
|
|
236
|
+
current = ex
|
|
237
|
+
seen: set[int] = set()
|
|
238
|
+
for _ in range(20):
|
|
239
|
+
seen.add(id(current))
|
|
240
|
+
if isinstance(current, BaseExceptionGroup) and len(current.exceptions) == 1:
|
|
241
|
+
cause = current.exceptions[0]
|
|
242
|
+
else:
|
|
243
|
+
cause = current.__cause__ or current.__context__
|
|
244
|
+
if cause is None or id(cause) in seen:
|
|
245
|
+
break
|
|
246
|
+
if isinstance(current, asyncio.CancelledError) and isinstance(cause, asyncio.CancelledError):
|
|
247
|
+
break
|
|
248
|
+
current = cause
|
|
249
|
+
return str(current) or repr(current)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _describe_with_cleanup(ex: BaseException, cleanup_error: BaseException | None) -> str:
|
|
253
|
+
"""A cancelled initialize may reveal its real error only during teardown."""
|
|
254
|
+
if isinstance(ex, asyncio.CancelledError) and cleanup_error is not None:
|
|
255
|
+
return _describe_error(cleanup_error)
|
|
256
|
+
return _describe_error(ex)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _media_content(item: types.ImageContent | types.AudioContent) -> Content:
|
|
260
|
+
"""An image or audio item as data; a placeholder text when its data is not non-empty base64."""
|
|
261
|
+
if data := decode_media_base64(item.data):
|
|
262
|
+
return Content.from_data(data=data, media_type=item.mimeType)
|
|
263
|
+
return Content.from_text(INVALID_IMAGE_TEXT if item.type == "image" else INVALID_AUDIO_TEXT)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _blob_content(resource: types.BlobResourceContents) -> Content:
|
|
267
|
+
"""A blob resource as data, empty allowed; a placeholder text when its blob is not base64."""
|
|
268
|
+
if (data := decode_media_base64(resource.blob)) is None:
|
|
269
|
+
return Content.from_text(INVALID_RESOURCE_TEXT)
|
|
270
|
+
return Content.from_data(data=data, media_type=resource.mimeType or "application/octet-stream")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _resource_link_text(item: types.ResourceLink) -> Content:
|
|
274
|
+
"""A link as text the model reads, never as the linked item itself.
|
|
275
|
+
|
|
276
|
+
Model APIs would fetch a linked item on every request, and one they can't
|
|
277
|
+
reach (a local file, a localhost or expired URL) would fail every later
|
|
278
|
+
request of the session.
|
|
279
|
+
"""
|
|
280
|
+
lines = [f"Resource link: {item.name}", f"URI: {item.uri}"]
|
|
281
|
+
if item.mimeType:
|
|
282
|
+
lines.append(f"MIME type: {item.mimeType}")
|
|
283
|
+
if item.description:
|
|
284
|
+
lines.append(f"Description: {item.description}")
|
|
285
|
+
return Content.from_text("\n".join(lines))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class MCPTool:
|
|
289
|
+
"""Base class for Chrys-owned MCP stdio and streamable-HTTP tools."""
|
|
290
|
+
|
|
291
|
+
def __init__(
|
|
292
|
+
self,
|
|
293
|
+
name: str,
|
|
294
|
+
description: str | None = None,
|
|
295
|
+
allowed_tools: Collection[str] | None = None,
|
|
296
|
+
tool_name_prefix: str | None = None,
|
|
297
|
+
load_tools: bool = True,
|
|
298
|
+
parse_tool_results: Callable[[types.CallToolResult], str | list[Content]] | None = None,
|
|
299
|
+
load_prompts: bool = True,
|
|
300
|
+
parse_prompt_results: Callable[[types.GetPromptResult], str] | None = None,
|
|
301
|
+
session: ClientSession | None = None,
|
|
302
|
+
request_timeout: int | None = None,
|
|
303
|
+
additional_properties: dict[str, Any] | None = None,
|
|
304
|
+
additional_tool_argument_names: Sequence[str] | Mapping[str, str | Sequence[str]] | None = None,
|
|
305
|
+
) -> None:
|
|
306
|
+
self.name = name
|
|
307
|
+
self.description = description or ""
|
|
308
|
+
self.allowed_tools = allowed_tools
|
|
309
|
+
self.tool_name_prefix = _normalize_mcp_name(tool_name_prefix).rstrip("_.-") if tool_name_prefix else None
|
|
310
|
+
self.additional_properties = additional_properties
|
|
311
|
+
self.load_tools_flag = load_tools
|
|
312
|
+
self.parse_tool_results = parse_tool_results
|
|
313
|
+
self.load_prompts_flag = load_prompts
|
|
314
|
+
self.parse_prompt_results = parse_prompt_results
|
|
315
|
+
self._exit_stack = AsyncExitStack()
|
|
316
|
+
self._lifecycle_lock = asyncio.Lock()
|
|
317
|
+
self._lifecycle_request_lock = asyncio.Lock()
|
|
318
|
+
self._function_load_lock = asyncio.Lock()
|
|
319
|
+
self._lifecycle_queue: asyncio.Queue[tuple[str, bool, bool, asyncio.Future[None]]] | None = None
|
|
320
|
+
self._lifecycle_owner_task: asyncio.Task[None] | None = None
|
|
321
|
+
self.session = session
|
|
322
|
+
self.request_timeout = request_timeout
|
|
323
|
+
self._functions: list[FunctionTool] = []
|
|
324
|
+
# Reloads must skip an already wrapped remote declaration without
|
|
325
|
+
# collapsing *different* remote names that normalize to the same local
|
|
326
|
+
# name. The adapter validates those duplicates before exposure.
|
|
327
|
+
self._loaded_tool_remote_names: set[str] = set()
|
|
328
|
+
self._loaded_prompt_remote_names: set[str] = set()
|
|
329
|
+
self._tool_call_meta_by_name: dict[str, dict[str, Any]] = {}
|
|
330
|
+
self._tool_task_support_by_name: dict[str, str] = {}
|
|
331
|
+
self._tool_param_names_by_name: dict[str, set[str]] = {}
|
|
332
|
+
self._global_extra_arg_names, self._tool_extra_arg_names = _normalize_additional_tool_argument_names(
|
|
333
|
+
additional_tool_argument_names
|
|
334
|
+
)
|
|
335
|
+
self.is_connected = False
|
|
336
|
+
self._tools_loaded = False
|
|
337
|
+
self._prompts_loaded = False
|
|
338
|
+
self._server_capabilities: types.ServerCapabilities | None = None
|
|
339
|
+
self._server_instructions: str | None = None
|
|
340
|
+
self._server_info: types.Implementation | None = None
|
|
341
|
+
self._protocol_version: str | None = None
|
|
342
|
+
self._supports_tools = True
|
|
343
|
+
self._supports_prompts = True
|
|
344
|
+
self._supports_logging: bool | None = None
|
|
345
|
+
self._ping_available = True
|
|
346
|
+
self._pending_reload_tasks: set[asyncio.Task[None]] = set()
|
|
347
|
+
|
|
348
|
+
def __str__(self) -> str:
|
|
349
|
+
return f"MCPTool(name={self.name}, description={self.description})"
|
|
350
|
+
|
|
351
|
+
def _mcp_base_span_attributes(self) -> dict[str, Any]:
|
|
352
|
+
return {}
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def functions(self) -> list[FunctionTool]:
|
|
356
|
+
"""Return exposed functions; ``None`` allows all tools, while ``[]`` allows none."""
|
|
357
|
+
if self.allowed_tools is None:
|
|
358
|
+
return self._functions
|
|
359
|
+
allowed_names = set(self.allowed_tools)
|
|
360
|
+
return [func for func in self._functions if not allowed_names.isdisjoint(_mcp_config_names_for(func))]
|
|
361
|
+
|
|
362
|
+
async def _safe_close_exit_stack(self) -> BaseException | None:
|
|
363
|
+
try:
|
|
364
|
+
await self._exit_stack.aclose()
|
|
365
|
+
except RuntimeError as err:
|
|
366
|
+
if "cancel scope" in str(err).lower():
|
|
367
|
+
logger.warning("Could not cleanly close MCP exit stack due to cancel scope error: %s", err)
|
|
368
|
+
return err
|
|
369
|
+
raise
|
|
370
|
+
except asyncio.CancelledError as err:
|
|
371
|
+
logger.warning("Could not cleanly close MCP exit stack because the task was cancelled.")
|
|
372
|
+
return err
|
|
373
|
+
except Exception as err:
|
|
374
|
+
if type(err).__name__ == "ExceptionGroup":
|
|
375
|
+
logger.warning("Could not cleanly close MCP exit stack due to cleanup error group: %s", err)
|
|
376
|
+
return err
|
|
377
|
+
raise
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
async def _close_and_check_cancelled(self, ex: BaseException) -> tuple[bool, BaseException | None]:
|
|
381
|
+
cleanup_error = await self._safe_close_exit_stack()
|
|
382
|
+
return _should_propagate_cancelled_error(ex), cleanup_error
|
|
383
|
+
|
|
384
|
+
def _reset_session_state(self) -> None:
|
|
385
|
+
self._server_capabilities = None
|
|
386
|
+
self._server_instructions = None
|
|
387
|
+
self._server_info = None
|
|
388
|
+
self._protocol_version = None
|
|
389
|
+
self._supports_tools = True
|
|
390
|
+
self._supports_prompts = True
|
|
391
|
+
self._supports_logging = None
|
|
392
|
+
self._ping_available = True
|
|
393
|
+
|
|
394
|
+
def _set_server_capabilities(self, capabilities: types.ServerCapabilities | None) -> None:
|
|
395
|
+
self._server_capabilities = capabilities
|
|
396
|
+
if capabilities is None:
|
|
397
|
+
self._supports_tools = False
|
|
398
|
+
self._supports_prompts = False
|
|
399
|
+
self._supports_logging = False
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
self._supports_tools = getattr(capabilities, "tools", None) is not None
|
|
403
|
+
self._supports_prompts = getattr(capabilities, "prompts", None) is not None
|
|
404
|
+
self._supports_logging = getattr(capabilities, "logging", None) is not None
|
|
405
|
+
|
|
406
|
+
def _set_server_instructions(self, instructions: object) -> None:
|
|
407
|
+
self._server_instructions = (
|
|
408
|
+
instructions.strip() if isinstance(instructions, str) and instructions.strip() else None
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def _set_server_identity(self, initialize_result: object) -> None:
|
|
412
|
+
# Duck-typed: ``mcp.types`` is a deferred import, so don't isinstance
|
|
413
|
+
# against Implementation here — a serverInfo with a name is enough.
|
|
414
|
+
server_info = getattr(initialize_result, "serverInfo", None)
|
|
415
|
+
self._server_info = server_info if getattr(server_info, "name", None) else None
|
|
416
|
+
protocol_version = getattr(initialize_result, "protocolVersion", None)
|
|
417
|
+
self._protocol_version = str(protocol_version) if protocol_version is not None else None
|
|
418
|
+
|
|
419
|
+
async def _ensure_lifecycle_owner(self) -> None:
|
|
420
|
+
async with self._lifecycle_lock:
|
|
421
|
+
if self._lifecycle_owner_task is not None and not self._lifecycle_owner_task.done():
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
self._lifecycle_queue = asyncio.Queue()
|
|
425
|
+
self._lifecycle_owner_task = asyncio.create_task(
|
|
426
|
+
self._run_lifecycle_owner(),
|
|
427
|
+
name=f"mcp-lifecycle:{self.name}",
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
async def _run_lifecycle_owner(self) -> None:
|
|
431
|
+
queue = self._lifecycle_queue
|
|
432
|
+
if queue is None:
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
stop_error: BaseException | None = None
|
|
436
|
+
try:
|
|
437
|
+
while True:
|
|
438
|
+
action, reset, load_configured, future = await queue.get()
|
|
439
|
+
try:
|
|
440
|
+
if action == "connect":
|
|
441
|
+
await self._connect_on_owner(reset=reset, load_configured=load_configured)
|
|
442
|
+
elif action == "close":
|
|
443
|
+
await self._close_on_owner()
|
|
444
|
+
else:
|
|
445
|
+
raise RuntimeError(f"Unknown MCP lifecycle action: {action}")
|
|
446
|
+
except asyncio.CancelledError as ex:
|
|
447
|
+
stop_error = ex
|
|
448
|
+
if not future.done():
|
|
449
|
+
future.set_exception(ex)
|
|
450
|
+
raise
|
|
451
|
+
except Exception as ex:
|
|
452
|
+
if not future.done():
|
|
453
|
+
future.set_exception(ex)
|
|
454
|
+
else:
|
|
455
|
+
if not future.done():
|
|
456
|
+
future.set_result(None)
|
|
457
|
+
|
|
458
|
+
if action == "close":
|
|
459
|
+
return
|
|
460
|
+
except asyncio.CancelledError as ex:
|
|
461
|
+
stop_error = ex
|
|
462
|
+
raise
|
|
463
|
+
finally:
|
|
464
|
+
while True:
|
|
465
|
+
try:
|
|
466
|
+
_, _, _, future = queue.get_nowait()
|
|
467
|
+
except asyncio.QueueEmpty:
|
|
468
|
+
break
|
|
469
|
+
if not future.done():
|
|
470
|
+
future.set_exception(stop_error or RuntimeError("MCP lifecycle owner stopped unexpectedly."))
|
|
471
|
+
self._lifecycle_queue = None
|
|
472
|
+
self._lifecycle_owner_task = None
|
|
473
|
+
|
|
474
|
+
def _is_lifecycle_owner_task(self) -> bool:
|
|
475
|
+
owner_task = self._lifecycle_owner_task
|
|
476
|
+
return owner_task is not None and asyncio.current_task() is owner_task
|
|
477
|
+
|
|
478
|
+
async def _run_on_lifecycle_owner(
|
|
479
|
+
self,
|
|
480
|
+
action: str,
|
|
481
|
+
*,
|
|
482
|
+
reset: bool = False,
|
|
483
|
+
load_configured: bool = True,
|
|
484
|
+
) -> None:
|
|
485
|
+
await self._ensure_lifecycle_owner()
|
|
486
|
+
|
|
487
|
+
if self._is_lifecycle_owner_task():
|
|
488
|
+
if action == "connect":
|
|
489
|
+
await self._connect_on_owner(reset=reset, load_configured=load_configured)
|
|
490
|
+
elif action == "close":
|
|
491
|
+
await self._close_on_owner()
|
|
492
|
+
else:
|
|
493
|
+
raise RuntimeError(f"Unknown MCP lifecycle action: {action}")
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
queue = self._lifecycle_queue
|
|
497
|
+
if queue is None:
|
|
498
|
+
raise RuntimeError("MCP lifecycle owner is not available.")
|
|
499
|
+
|
|
500
|
+
future = asyncio.get_running_loop().create_future()
|
|
501
|
+
await queue.put((action, reset, load_configured, future))
|
|
502
|
+
await future
|
|
503
|
+
|
|
504
|
+
async def connect(self, *, reset: bool = False) -> None:
|
|
505
|
+
# A connect inside a model run (lazy first use, reconnect after a
|
|
506
|
+
# dropped session) is a wait the turn timeline must account for.
|
|
507
|
+
wait = WaitTrace.open(
|
|
508
|
+
WaitCategory.MCP_CONNECT,
|
|
509
|
+
target_operation_id=current_tool_operation_id(),
|
|
510
|
+
server_name=self.name,
|
|
511
|
+
)
|
|
512
|
+
try:
|
|
513
|
+
# Inside the block that closes the wait: the start marker awaits
|
|
514
|
+
# its write ack, and an interrupt landing there would otherwise
|
|
515
|
+
# leave the wait open forever.
|
|
516
|
+
if wait is not None:
|
|
517
|
+
await wait.started()
|
|
518
|
+
await self._connect_unrecorded(reset=reset)
|
|
519
|
+
except asyncio.CancelledError:
|
|
520
|
+
if wait is not None:
|
|
521
|
+
wait.finished_soon(outcome=WaitOutcome.CANCELLED)
|
|
522
|
+
raise
|
|
523
|
+
except BaseException:
|
|
524
|
+
if wait is not None:
|
|
525
|
+
await wait.finished(outcome=WaitOutcome.FAILED)
|
|
526
|
+
raise
|
|
527
|
+
if wait is not None:
|
|
528
|
+
await wait.finished()
|
|
529
|
+
|
|
530
|
+
async def _connect_unrecorded(self, *, reset: bool) -> None:
|
|
531
|
+
if self._is_lifecycle_owner_task():
|
|
532
|
+
await self._connect_on_owner(reset=reset)
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
async with self._lifecycle_request_lock:
|
|
536
|
+
await self._run_on_lifecycle_owner("connect", reset=reset)
|
|
537
|
+
|
|
538
|
+
async def _connect_on_owner(self, *, reset: bool = False, load_configured: bool = True) -> None:
|
|
539
|
+
"""Connect, initialize, and load configured MCP functions."""
|
|
540
|
+
if reset:
|
|
541
|
+
await self._safe_close_exit_stack()
|
|
542
|
+
self.session = None
|
|
543
|
+
self.is_connected = False
|
|
544
|
+
self._reset_session_state()
|
|
545
|
+
self._exit_stack = AsyncExitStack()
|
|
546
|
+
|
|
547
|
+
if not self.session:
|
|
548
|
+
try:
|
|
549
|
+
transport = await self._exit_stack.enter_async_context(self.get_mcp_client())
|
|
550
|
+
except (Exception, asyncio.CancelledError) as ex:
|
|
551
|
+
cancelled, cleanup_error = await self._close_and_check_cancelled(ex)
|
|
552
|
+
if cancelled:
|
|
553
|
+
raise
|
|
554
|
+
command = getattr(self, "command", None)
|
|
555
|
+
if command:
|
|
556
|
+
message = f"Failed to start MCP server '{command}': {_describe_with_cleanup(ex, cleanup_error)}"
|
|
557
|
+
else:
|
|
558
|
+
message = f"Failed to connect to MCP server: {_describe_with_cleanup(ex, cleanup_error)}"
|
|
559
|
+
raise ToolException(message, inner_exception=ex if isinstance(ex, Exception) else None) from ex
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
from mcp.client.session import ClientSession
|
|
563
|
+
except ModuleNotFoundError as ex:
|
|
564
|
+
await self._safe_close_exit_stack()
|
|
565
|
+
raise ToolException("MCP support requires `mcp`. Please install `mcp`.", inner_exception=ex) from ex
|
|
566
|
+
|
|
567
|
+
try:
|
|
568
|
+
session = await self._exit_stack.enter_async_context(
|
|
569
|
+
ClientSession(
|
|
570
|
+
read_stream=transport[0],
|
|
571
|
+
write_stream=transport[1],
|
|
572
|
+
read_timeout_seconds=timedelta(seconds=self.request_timeout) if self.request_timeout else None,
|
|
573
|
+
message_handler=self.message_handler,
|
|
574
|
+
logging_callback=self.logging_callback,
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
except (Exception, asyncio.CancelledError) as ex:
|
|
578
|
+
cancelled, cleanup_error = await self._close_and_check_cancelled(ex)
|
|
579
|
+
if cancelled:
|
|
580
|
+
raise
|
|
581
|
+
message = f"Failed to create MCP session: {_describe_with_cleanup(ex, cleanup_error)}"
|
|
582
|
+
raise ToolException(message, inner_exception=ex if isinstance(ex, Exception) else None) from ex
|
|
583
|
+
|
|
584
|
+
try:
|
|
585
|
+
with create_mcp_client_span("initialize", attributes=self._mcp_base_span_attributes()) as init_span:
|
|
586
|
+
initialize_result = await session.initialize()
|
|
587
|
+
init_span.set_attribute(OtelAttr.MCP_PROTOCOL_VERSION, initialize_result.protocolVersion)
|
|
588
|
+
self._set_server_capabilities(getattr(initialize_result, "capabilities", None))
|
|
589
|
+
self._set_server_instructions(getattr(initialize_result, "instructions", None))
|
|
590
|
+
self._set_server_identity(initialize_result)
|
|
591
|
+
except (Exception, asyncio.CancelledError) as ex:
|
|
592
|
+
cancelled, cleanup_error = await self._close_and_check_cancelled(ex)
|
|
593
|
+
if cancelled:
|
|
594
|
+
raise
|
|
595
|
+
command = getattr(self, "command", None)
|
|
596
|
+
if command:
|
|
597
|
+
args_str = " ".join(getattr(self, "args", []))
|
|
598
|
+
full_command = f"{command} {args_str}".strip()
|
|
599
|
+
message = (
|
|
600
|
+
f"MCP server '{full_command}' failed to initialize: {_describe_with_cleanup(ex, cleanup_error)}"
|
|
601
|
+
)
|
|
602
|
+
else:
|
|
603
|
+
message = f"MCP server failed to initialize: {_describe_with_cleanup(ex, cleanup_error)}"
|
|
604
|
+
raise ToolException(message, inner_exception=ex if isinstance(ex, Exception) else None) from ex
|
|
605
|
+
self.session = session
|
|
606
|
+
|
|
607
|
+
elif getattr(self.session, "_request_id", 0) == 0:
|
|
608
|
+
with create_mcp_client_span("initialize", attributes=self._mcp_base_span_attributes()) as init_span:
|
|
609
|
+
initialize_result = await self.session.initialize()
|
|
610
|
+
init_span.set_attribute(OtelAttr.MCP_PROTOCOL_VERSION, initialize_result.protocolVersion)
|
|
611
|
+
self._set_server_capabilities(getattr(initialize_result, "capabilities", None))
|
|
612
|
+
self._set_server_instructions(getattr(initialize_result, "instructions", None))
|
|
613
|
+
self._set_server_identity(initialize_result)
|
|
614
|
+
elif self._server_capabilities is None:
|
|
615
|
+
self._set_server_capabilities(getattr(self.session, "_server_capabilities", None))
|
|
616
|
+
|
|
617
|
+
self.is_connected = True
|
|
618
|
+
if load_configured and self.load_tools_flag:
|
|
619
|
+
if self._supports_tools:
|
|
620
|
+
await self.load_tools()
|
|
621
|
+
self._tools_loaded = True
|
|
622
|
+
if load_configured and self.load_prompts_flag:
|
|
623
|
+
if self._supports_prompts:
|
|
624
|
+
await self.load_prompts()
|
|
625
|
+
self._prompts_loaded = True
|
|
626
|
+
|
|
627
|
+
if logger.level != logging.NOTSET and self._supports_logging is not False:
|
|
628
|
+
with contextlib.suppress(Exception):
|
|
629
|
+
level_name = next(level for level, value in LOG_LEVEL_MAPPING.items() if value == logger.level)
|
|
630
|
+
await self.session.set_logging_level(level_name)
|
|
631
|
+
|
|
632
|
+
async def _reconnect_without_loading(self) -> None:
|
|
633
|
+
if self._is_lifecycle_owner_task():
|
|
634
|
+
await self._connect_on_owner(reset=True, load_configured=False)
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
await self._run_on_lifecycle_owner("connect", reset=True, load_configured=False)
|
|
638
|
+
|
|
639
|
+
async def _close_on_owner(self) -> None:
|
|
640
|
+
tasks = list(self._pending_reload_tasks)
|
|
641
|
+
for task in tasks:
|
|
642
|
+
task.cancel()
|
|
643
|
+
self._pending_reload_tasks.clear()
|
|
644
|
+
if tasks:
|
|
645
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
646
|
+
|
|
647
|
+
await self._safe_close_exit_stack()
|
|
648
|
+
self._exit_stack = AsyncExitStack()
|
|
649
|
+
self.session = None
|
|
650
|
+
self.is_connected = False
|
|
651
|
+
self._reset_session_state()
|
|
652
|
+
|
|
653
|
+
async def close(self) -> None:
|
|
654
|
+
if self._is_lifecycle_owner_task():
|
|
655
|
+
await self._close_on_owner()
|
|
656
|
+
return
|
|
657
|
+
|
|
658
|
+
async with self._lifecycle_request_lock:
|
|
659
|
+
await self._run_on_lifecycle_owner("close")
|
|
660
|
+
|
|
661
|
+
@abstractmethod
|
|
662
|
+
def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
|
|
663
|
+
"""Return the MCP SDK transport context manager."""
|
|
664
|
+
|
|
665
|
+
async def _ensure_connected(self) -> None:
|
|
666
|
+
from mcp.shared.exceptions import McpError
|
|
667
|
+
|
|
668
|
+
if not self._ping_available:
|
|
669
|
+
return
|
|
670
|
+
# Connected operations run only after the lifecycle installs a session.
|
|
671
|
+
session = cast("ClientSession", self.session)
|
|
672
|
+
try:
|
|
673
|
+
await session.send_ping()
|
|
674
|
+
except McpError as mcp_exc:
|
|
675
|
+
if mcp_exc.error.code == -32601:
|
|
676
|
+
self._ping_available = False
|
|
677
|
+
logger.debug("Skipping future MCP pings because the server does not support ping.")
|
|
678
|
+
return
|
|
679
|
+
await self._reconnect_or_raise(mcp_exc)
|
|
680
|
+
except Exception as ex:
|
|
681
|
+
await self._reconnect_or_raise(ex)
|
|
682
|
+
|
|
683
|
+
async def _reconnect_or_raise(self, ex: BaseException) -> None:
|
|
684
|
+
logger.info("MCP connection invalid or closed. Reconnecting...")
|
|
685
|
+
try:
|
|
686
|
+
await self._reconnect_without_loading()
|
|
687
|
+
except Exception as reconn_ex:
|
|
688
|
+
raise ToolExecutionException("Failed to establish MCP connection.", inner_exception=reconn_ex) from ex
|
|
689
|
+
|
|
690
|
+
async def logging_callback(self, params: types.LoggingMessageNotificationParams) -> None:
|
|
691
|
+
logger.log(LOG_LEVEL_MAPPING[params.level], params.data)
|
|
692
|
+
|
|
693
|
+
async def message_handler(
|
|
694
|
+
self,
|
|
695
|
+
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
|
|
696
|
+
) -> None:
|
|
697
|
+
from mcp import types
|
|
698
|
+
|
|
699
|
+
if isinstance(message, Exception):
|
|
700
|
+
logger.error("Error from MCP server: %s", message, exc_info=message)
|
|
701
|
+
return
|
|
702
|
+
if isinstance(message, types.ServerNotification):
|
|
703
|
+
match message.root.method:
|
|
704
|
+
case "notifications/tools/list_changed":
|
|
705
|
+
self._schedule_reload(self.load_tools())
|
|
706
|
+
case "notifications/prompts/list_changed":
|
|
707
|
+
self._schedule_reload(self.load_prompts())
|
|
708
|
+
case _:
|
|
709
|
+
logger.debug("Unhandled notification: %s", message.root.method)
|
|
710
|
+
|
|
711
|
+
def _schedule_reload(self, coro: Any) -> None:
|
|
712
|
+
reload_name = f"mcp-reload:{self.name}:{coro.__qualname__}"
|
|
713
|
+
for existing in list(self._pending_reload_tasks):
|
|
714
|
+
if existing.get_name() == reload_name and not existing.done():
|
|
715
|
+
existing.cancel()
|
|
716
|
+
|
|
717
|
+
async def _safe_reload() -> None:
|
|
718
|
+
try:
|
|
719
|
+
await coro
|
|
720
|
+
except asyncio.CancelledError:
|
|
721
|
+
raise
|
|
722
|
+
except Exception:
|
|
723
|
+
logger.warning("Background MCP reload failed", exc_info=True)
|
|
724
|
+
|
|
725
|
+
task = asyncio.create_task(_safe_reload(), name=reload_name)
|
|
726
|
+
self._pending_reload_tasks.add(task)
|
|
727
|
+
task.add_done_callback(self._pending_reload_tasks.discard)
|
|
728
|
+
|
|
729
|
+
async def load_prompts(self) -> None:
|
|
730
|
+
async with self._function_load_lock:
|
|
731
|
+
await self._load_prompts_locked()
|
|
732
|
+
|
|
733
|
+
async def _load_prompts_locked(self) -> None:
|
|
734
|
+
from anyio import ClosedResourceError
|
|
735
|
+
from mcp import types
|
|
736
|
+
|
|
737
|
+
if not self._supports_prompts:
|
|
738
|
+
return
|
|
739
|
+
|
|
740
|
+
params: types.PaginatedRequestParams | None = None
|
|
741
|
+
while True:
|
|
742
|
+
prompt_list: types.ListPromptsResult | None = None
|
|
743
|
+
for attempt in range(2):
|
|
744
|
+
try:
|
|
745
|
+
await self._ensure_connected()
|
|
746
|
+
# Prompt loading is entered only for a connected lifecycle session.
|
|
747
|
+
session = cast("ClientSession", self.session)
|
|
748
|
+
if not self._supports_prompts:
|
|
749
|
+
return
|
|
750
|
+
with create_mcp_client_span("prompts/list", attributes=self._mcp_base_span_attributes()):
|
|
751
|
+
prompt_list = await session.list_prompts(params=params)
|
|
752
|
+
break
|
|
753
|
+
except ClosedResourceError as cl_ex:
|
|
754
|
+
if attempt == 0:
|
|
755
|
+
try:
|
|
756
|
+
await self._reconnect_without_loading()
|
|
757
|
+
except Exception as reconn_ex:
|
|
758
|
+
raise ToolExecutionException(
|
|
759
|
+
"Failed to reconnect to MCP server.",
|
|
760
|
+
inner_exception=reconn_ex,
|
|
761
|
+
) from reconn_ex
|
|
762
|
+
continue
|
|
763
|
+
raise ToolExecutionException(
|
|
764
|
+
"Failed to load prompts - connection lost.", inner_exception=cl_ex
|
|
765
|
+
) from cl_ex
|
|
766
|
+
if prompt_list is None:
|
|
767
|
+
raise ToolExecutionException("Failed to load prompts.")
|
|
768
|
+
|
|
769
|
+
for prompt in prompt_list.prompts:
|
|
770
|
+
normalized_name = _normalize_mcp_name(prompt.name)
|
|
771
|
+
local_name = _build_prefixed_mcp_name(normalized_name, self.tool_name_prefix)
|
|
772
|
+
if prompt.name in self._loaded_prompt_remote_names:
|
|
773
|
+
continue
|
|
774
|
+
func = FunctionTool(
|
|
775
|
+
func=partial(self.get_prompt, prompt.name),
|
|
776
|
+
name=local_name,
|
|
777
|
+
description=prompt.description or "",
|
|
778
|
+
input_model=_get_input_model_from_mcp_prompt(prompt),
|
|
779
|
+
additional_properties={
|
|
780
|
+
_MCP_REMOTE_NAME_KEY: prompt.name,
|
|
781
|
+
_MCP_NORMALIZED_NAME_KEY: normalized_name,
|
|
782
|
+
_MCP_PROMPT_TOOL_KEY: True,
|
|
783
|
+
},
|
|
784
|
+
)
|
|
785
|
+
# Static provenance persisted on the function_call content;
|
|
786
|
+
# identity only — never URLs, headers, env, or command lines.
|
|
787
|
+
set_tool_context(
|
|
788
|
+
func,
|
|
789
|
+
{
|
|
790
|
+
"server_name": self.name,
|
|
791
|
+
"remote_name": prompt.name,
|
|
792
|
+
"normalized_name": normalized_name,
|
|
793
|
+
},
|
|
794
|
+
)
|
|
795
|
+
self._functions.append(func)
|
|
796
|
+
self._loaded_prompt_remote_names.add(prompt.name)
|
|
797
|
+
|
|
798
|
+
if not prompt_list.nextCursor:
|
|
799
|
+
break
|
|
800
|
+
params = types.PaginatedRequestParams(cursor=prompt_list.nextCursor)
|
|
801
|
+
|
|
802
|
+
async def load_tools(self) -> None:
|
|
803
|
+
async with self._function_load_lock:
|
|
804
|
+
await self._load_tools_locked()
|
|
805
|
+
|
|
806
|
+
def _make_remote_tool_call(self, remote_tool_name: str) -> Callable[..., Awaitable[str | list[Content]]]:
|
|
807
|
+
"""Bind one trusted remote name outside the model-callable signature."""
|
|
808
|
+
|
|
809
|
+
async def _call_tool_with_runtime_kwargs(
|
|
810
|
+
ctx: FunctionInvocationContext,
|
|
811
|
+
**kwargs: Any,
|
|
812
|
+
) -> str | list[Content]:
|
|
813
|
+
trusted_meta = ctx.kwargs.get("_meta")
|
|
814
|
+
extras = self._resolved_extra_args(remote_tool_name)
|
|
815
|
+
runtime_arguments = {key: value for key, value in ctx.kwargs.items() if key != "_meta" and key in extras}
|
|
816
|
+
tool_arguments = dict(ctx.arguments)
|
|
817
|
+
|
|
818
|
+
# ``_meta`` is an MCP request-metadata channel, not a tool
|
|
819
|
+
# argument. Model arguments are untrusted even when the
|
|
820
|
+
# remote schema accepts additional properties; only the
|
|
821
|
+
# runtime context may populate request metadata.
|
|
822
|
+
tool_arguments.pop("_meta", None)
|
|
823
|
+
|
|
824
|
+
# Explicit runtime extras are trusted injections and therefore
|
|
825
|
+
# win over a same-named value supplied by the model.
|
|
826
|
+
call_kwargs = {**tool_arguments, **runtime_arguments}
|
|
827
|
+
if trusted_meta is not None:
|
|
828
|
+
call_kwargs["_meta"] = trusted_meta
|
|
829
|
+
return await self.call_tool(remote_tool_name, **call_kwargs)
|
|
830
|
+
|
|
831
|
+
return _call_tool_with_runtime_kwargs
|
|
832
|
+
|
|
833
|
+
async def _load_tools_locked(self) -> None:
|
|
834
|
+
from anyio import ClosedResourceError
|
|
835
|
+
from mcp import types
|
|
836
|
+
|
|
837
|
+
if not self._supports_tools:
|
|
838
|
+
return
|
|
839
|
+
|
|
840
|
+
tool_call_meta_by_name: dict[str, dict[str, Any]] = {}
|
|
841
|
+
tool_task_support_by_name: dict[str, str] = {}
|
|
842
|
+
tool_param_names_by_name: dict[str, set[str]] = {}
|
|
843
|
+
|
|
844
|
+
params: types.PaginatedRequestParams | None = None
|
|
845
|
+
while True:
|
|
846
|
+
tool_list: types.ListToolsResult | None = None
|
|
847
|
+
for attempt in range(2):
|
|
848
|
+
try:
|
|
849
|
+
await self._ensure_connected()
|
|
850
|
+
# Tool loading is entered only for a connected lifecycle session.
|
|
851
|
+
session = cast("ClientSession", self.session)
|
|
852
|
+
if not self._supports_tools:
|
|
853
|
+
return
|
|
854
|
+
with create_mcp_client_span("tools/list", attributes=self._mcp_base_span_attributes()):
|
|
855
|
+
tool_list = await session.list_tools(params=params)
|
|
856
|
+
break
|
|
857
|
+
except ClosedResourceError as cl_ex:
|
|
858
|
+
if attempt == 0:
|
|
859
|
+
try:
|
|
860
|
+
await self._reconnect_without_loading()
|
|
861
|
+
except Exception as reconn_ex:
|
|
862
|
+
raise ToolExecutionException(
|
|
863
|
+
"Failed to reconnect to MCP server.",
|
|
864
|
+
inner_exception=reconn_ex,
|
|
865
|
+
) from reconn_ex
|
|
866
|
+
continue
|
|
867
|
+
raise ToolExecutionException(
|
|
868
|
+
"Failed to load tools - connection lost.", inner_exception=cl_ex
|
|
869
|
+
) from cl_ex
|
|
870
|
+
if tool_list is None:
|
|
871
|
+
raise ToolExecutionException("Failed to load tools.")
|
|
872
|
+
|
|
873
|
+
for tool in tool_list.tools:
|
|
874
|
+
if tool.meta is not None:
|
|
875
|
+
tool_call_meta_by_name[tool.name] = dict(tool.meta)
|
|
876
|
+
task_support = getattr(getattr(tool, "execution", None), "taskSupport", None)
|
|
877
|
+
if task_support is not None:
|
|
878
|
+
tool_task_support_by_name[tool.name] = task_support
|
|
879
|
+
|
|
880
|
+
input_schema = dict(tool.inputSchema or {})
|
|
881
|
+
if input_schema.get("type") == "object" and "properties" not in input_schema:
|
|
882
|
+
input_schema["properties"] = {}
|
|
883
|
+
properties = input_schema.get("properties")
|
|
884
|
+
tool_param_names_by_name[tool.name] = (
|
|
885
|
+
set(cast(dict[str, Any], properties)) if isinstance(properties, dict) else set()
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
normalized_name = _normalize_mcp_name(tool.name)
|
|
889
|
+
local_name = _build_prefixed_mcp_name(normalized_name, self.tool_name_prefix)
|
|
890
|
+
if tool.name in self._loaded_tool_remote_names:
|
|
891
|
+
continue
|
|
892
|
+
|
|
893
|
+
func = FunctionTool(
|
|
894
|
+
func=self._make_remote_tool_call(tool.name),
|
|
895
|
+
name=local_name,
|
|
896
|
+
description=tool.description or "",
|
|
897
|
+
input_model=input_schema,
|
|
898
|
+
additional_properties={
|
|
899
|
+
_MCP_REMOTE_NAME_KEY: tool.name,
|
|
900
|
+
_MCP_NORMALIZED_NAME_KEY: normalized_name,
|
|
901
|
+
},
|
|
902
|
+
)
|
|
903
|
+
# Static provenance persisted on the function_call content;
|
|
904
|
+
# identity only — never URLs, headers, env, or command lines.
|
|
905
|
+
set_tool_context(
|
|
906
|
+
func,
|
|
907
|
+
{
|
|
908
|
+
"server_name": self.name,
|
|
909
|
+
"remote_name": tool.name,
|
|
910
|
+
"normalized_name": normalized_name,
|
|
911
|
+
},
|
|
912
|
+
)
|
|
913
|
+
self._functions.append(func)
|
|
914
|
+
self._loaded_tool_remote_names.add(tool.name)
|
|
915
|
+
|
|
916
|
+
if not tool_list.nextCursor:
|
|
917
|
+
break
|
|
918
|
+
params = types.PaginatedRequestParams(cursor=tool_list.nextCursor)
|
|
919
|
+
|
|
920
|
+
self._tool_call_meta_by_name = tool_call_meta_by_name
|
|
921
|
+
self._tool_task_support_by_name = tool_task_support_by_name
|
|
922
|
+
self._tool_param_names_by_name = tool_param_names_by_name
|
|
923
|
+
|
|
924
|
+
def _parse_prompt_result_from_mcp(self, mcp_type: types.GetPromptResult) -> str:
|
|
925
|
+
from mcp import types
|
|
926
|
+
|
|
927
|
+
parts: list[str] = []
|
|
928
|
+
for message in mcp_type.messages:
|
|
929
|
+
content = message.content
|
|
930
|
+
if isinstance(content, types.TextContent):
|
|
931
|
+
parts.append(content.text)
|
|
932
|
+
elif isinstance(content, (types.ImageContent, types.AudioContent)):
|
|
933
|
+
parts.append(
|
|
934
|
+
json.dumps(
|
|
935
|
+
{
|
|
936
|
+
"type": "image" if isinstance(content, types.ImageContent) else "audio",
|
|
937
|
+
"data": content.data,
|
|
938
|
+
"mimeType": content.mimeType,
|
|
939
|
+
},
|
|
940
|
+
default=str,
|
|
941
|
+
)
|
|
942
|
+
)
|
|
943
|
+
elif isinstance(content, types.EmbeddedResource):
|
|
944
|
+
match content.resource:
|
|
945
|
+
case types.TextResourceContents():
|
|
946
|
+
parts.append(content.resource.text)
|
|
947
|
+
case types.BlobResourceContents():
|
|
948
|
+
parts.append(
|
|
949
|
+
json.dumps(
|
|
950
|
+
{"type": "blob", "data": content.resource.blob, "mimeType": content.resource.mimeType},
|
|
951
|
+
default=str,
|
|
952
|
+
)
|
|
953
|
+
)
|
|
954
|
+
else:
|
|
955
|
+
parts.append(str(content))
|
|
956
|
+
if not parts:
|
|
957
|
+
return ""
|
|
958
|
+
if len(parts) == 1:
|
|
959
|
+
return parts[0]
|
|
960
|
+
return model_json(parts, default=str)
|
|
961
|
+
|
|
962
|
+
def _parse_tool_result_from_mcp(self, mcp_type: types.CallToolResult) -> list[Content]:
|
|
963
|
+
from mcp import types
|
|
964
|
+
|
|
965
|
+
result: list[Content] = []
|
|
966
|
+
for item in mcp_type.content:
|
|
967
|
+
match item:
|
|
968
|
+
case types.TextContent():
|
|
969
|
+
result.append(Content.from_text(item.text))
|
|
970
|
+
case types.ImageContent() | types.AudioContent():
|
|
971
|
+
result.append(_media_content(item))
|
|
972
|
+
case types.ResourceLink():
|
|
973
|
+
result.append(_resource_link_text(item))
|
|
974
|
+
case types.EmbeddedResource():
|
|
975
|
+
match item.resource:
|
|
976
|
+
case types.TextResourceContents():
|
|
977
|
+
result.append(Content.from_text(item.resource.text))
|
|
978
|
+
case types.BlobResourceContents():
|
|
979
|
+
result.append(_blob_content(item.resource))
|
|
980
|
+
case _:
|
|
981
|
+
result.append(Content.from_text(str(item)))
|
|
982
|
+
if not result:
|
|
983
|
+
result.append(Content.from_text("null"))
|
|
984
|
+
return result
|
|
985
|
+
|
|
986
|
+
def _resolved_extra_args(self, tool_name: str) -> set[str]:
|
|
987
|
+
return self._global_extra_arg_names | self._tool_extra_arg_names.get(tool_name, set())
|
|
988
|
+
|
|
989
|
+
def _prepare_call_kwargs(
|
|
990
|
+
self, tool_name: str, kwargs: dict[str, Any]
|
|
991
|
+
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
992
|
+
"""Build wire arguments from source-separated or directly trusted kwargs.
|
|
993
|
+
|
|
994
|
+
Generated MCP functions isolate Chrys runtime kwargs before this
|
|
995
|
+
method. Direct Python callers of :meth:`call_tool` are trusted, while
|
|
996
|
+
undeclared names still require an explicit extra-argument opt-in.
|
|
997
|
+
"""
|
|
998
|
+
raw_user_meta = kwargs.get("_meta")
|
|
999
|
+
user_meta: dict[str, Any] | None = None
|
|
1000
|
+
if raw_user_meta is not None and not isinstance(raw_user_meta, dict):
|
|
1001
|
+
raise ToolExecutionException("MCP tool metadata provided via _meta must be a dict.")
|
|
1002
|
+
if isinstance(raw_user_meta, dict):
|
|
1003
|
+
user_meta = {}
|
|
1004
|
+
for key, value in raw_user_meta.items():
|
|
1005
|
+
if not isinstance(key, str):
|
|
1006
|
+
raise ToolExecutionException("MCP tool metadata provided via _meta must use string keys.")
|
|
1007
|
+
user_meta[key] = value
|
|
1008
|
+
|
|
1009
|
+
declared = self._tool_param_names_by_name.get(tool_name, set())
|
|
1010
|
+
extras = self._resolved_extra_args(tool_name)
|
|
1011
|
+
filtered_kwargs = {k: v for k, v in kwargs.items() if k != "_meta" and (k in declared or k in extras)}
|
|
1012
|
+
request_meta = dict(user_meta) if user_meta is not None else None
|
|
1013
|
+
request_meta = _inject_otel_into_mcp_meta(request_meta, overwrite=True)
|
|
1014
|
+
tool_meta = self._tool_call_meta_by_name.get(tool_name)
|
|
1015
|
+
if tool_meta is not None:
|
|
1016
|
+
request_meta = {**(request_meta or {}), **tool_meta}
|
|
1017
|
+
return filtered_kwargs, request_meta
|
|
1018
|
+
|
|
1019
|
+
async def call_tool(self, tool_name: str, **kwargs: Any) -> str | list[Content]:
|
|
1020
|
+
if not self.load_tools_flag:
|
|
1021
|
+
raise ToolExecutionException(
|
|
1022
|
+
"Tools are not loaded for this server, please set load_tools=True in the constructor."
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
if self._tool_task_support_by_name.get(tool_name) == "required":
|
|
1026
|
+
raise ModelVisibleToolError(
|
|
1027
|
+
f"MCP tool '{tool_name}' requires long-running task support, which {APP_DISPLAY_NAME} does not implement yet."
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
filtered_kwargs, meta = self._prepare_call_kwargs(tool_name, kwargs)
|
|
1031
|
+
parser = self.parse_tool_results or self._parse_tool_result_from_mcp
|
|
1032
|
+
attrs = self._mcp_base_span_attributes()
|
|
1033
|
+
attrs.update({OtelAttr.TOOL_NAME: tool_name, OtelAttr.OPERATION: OtelAttr.TOOL_EXECUTION_OPERATION})
|
|
1034
|
+
with create_mcp_client_span("tools/call", target=tool_name, attributes=attrs) as span:
|
|
1035
|
+
return await self._call_tool_with_retries(tool_name, filtered_kwargs, meta, parser, span)
|
|
1036
|
+
|
|
1037
|
+
async def _call_tool_with_retries(
|
|
1038
|
+
self,
|
|
1039
|
+
tool_name: str,
|
|
1040
|
+
filtered_kwargs: dict[str, Any],
|
|
1041
|
+
meta: dict[str, Any] | None,
|
|
1042
|
+
parser: Callable[..., str | list[Content]],
|
|
1043
|
+
span: otel_trace.Span,
|
|
1044
|
+
) -> str | list[Content]:
|
|
1045
|
+
from anyio import ClosedResourceError
|
|
1046
|
+
from mcp.shared.exceptions import McpError
|
|
1047
|
+
|
|
1048
|
+
for attempt in range(2):
|
|
1049
|
+
# Tool calls are exposed only after the lifecycle installs a session.
|
|
1050
|
+
session = cast("ClientSession", self.session)
|
|
1051
|
+
try:
|
|
1052
|
+
result = sanitize_mcp_result_binary(
|
|
1053
|
+
await session.call_tool(tool_name, arguments=filtered_kwargs, meta=meta)
|
|
1054
|
+
)
|
|
1055
|
+
if result.isError:
|
|
1056
|
+
# With neither content nor structuredContent, skip the parser and its "null"
|
|
1057
|
+
# placeholder. A structured-only error still parses: the transports' parsers
|
|
1058
|
+
# turn structuredContent into text when content is empty.
|
|
1059
|
+
has_payload = bool(result.content) or result.structuredContent is not None
|
|
1060
|
+
parsed = parser(result) if has_payload else ""
|
|
1061
|
+
text = (
|
|
1062
|
+
"\n".join(c.text for c in parsed if c.type == "text" and c.text)
|
|
1063
|
+
if isinstance(parsed, list)
|
|
1064
|
+
else parsed
|
|
1065
|
+
)
|
|
1066
|
+
# The server's execution error is written for the model to correct its call. With no
|
|
1067
|
+
# text (only media, links and binary resources, or nothing at all) it says so.
|
|
1068
|
+
error = _server_error(truncate_mcp_error(text), f"MCP tool '{tool_name}'")
|
|
1069
|
+
if span.is_recording():
|
|
1070
|
+
set_mcp_span_error(span, "tool_error", error.model_message)
|
|
1071
|
+
raise error
|
|
1072
|
+
return parser(result)
|
|
1073
|
+
except ToolExecutionException:
|
|
1074
|
+
raise
|
|
1075
|
+
except (ClosedResourceError, McpError) as call_ex:
|
|
1076
|
+
is_session_terminated = (
|
|
1077
|
+
isinstance(call_ex, McpError) and "session terminated" in call_ex.error.message.lower()
|
|
1078
|
+
)
|
|
1079
|
+
is_connection_lost = isinstance(call_ex, ClosedResourceError) or is_session_terminated
|
|
1080
|
+
if not is_connection_lost:
|
|
1081
|
+
message = truncate_mcp_error(
|
|
1082
|
+
call_ex.error.message if isinstance(call_ex, McpError) else str(call_ex)
|
|
1083
|
+
)
|
|
1084
|
+
if span.is_recording():
|
|
1085
|
+
set_mcp_span_error(span, type(call_ex).__name__, message)
|
|
1086
|
+
raise _mcp_error_exception(call_ex, message, f"MCP tool '{tool_name}'") from call_ex
|
|
1087
|
+
|
|
1088
|
+
if attempt == 0:
|
|
1089
|
+
try:
|
|
1090
|
+
await self.connect(reset=True)
|
|
1091
|
+
continue
|
|
1092
|
+
except Exception as reconn_ex:
|
|
1093
|
+
raise ToolExecutionException(
|
|
1094
|
+
"Failed to reconnect to MCP server.", inner_exception=reconn_ex
|
|
1095
|
+
) from reconn_ex
|
|
1096
|
+
if span.is_recording():
|
|
1097
|
+
set_mcp_span_error(span, type(call_ex).__name__, truncate_mcp_error(str(call_ex)))
|
|
1098
|
+
raise ToolExecutionException(
|
|
1099
|
+
f"Failed to call tool '{tool_name}' - connection lost.",
|
|
1100
|
+
inner_exception=call_ex,
|
|
1101
|
+
) from call_ex
|
|
1102
|
+
except Exception as ex:
|
|
1103
|
+
if span.is_recording():
|
|
1104
|
+
set_mcp_span_error(span, type(ex).__name__, str(ex))
|
|
1105
|
+
raise ToolExecutionException(f"Failed to call tool '{tool_name}'.", inner_exception=ex) from ex
|
|
1106
|
+
raise ToolExecutionException(f"Failed to call tool '{tool_name}' after retries.")
|
|
1107
|
+
|
|
1108
|
+
async def get_prompt(self, prompt_name: str, **kwargs: Any) -> str:
|
|
1109
|
+
from anyio import ClosedResourceError
|
|
1110
|
+
from mcp.shared.exceptions import McpError
|
|
1111
|
+
|
|
1112
|
+
if not self.load_prompts_flag:
|
|
1113
|
+
raise ToolExecutionException(
|
|
1114
|
+
"Prompts are not loaded for this server, please set load_prompts=True in the constructor."
|
|
1115
|
+
)
|
|
1116
|
+
parser = self.parse_prompt_results or self._parse_prompt_result_from_mcp
|
|
1117
|
+
attrs = self._mcp_base_span_attributes()
|
|
1118
|
+
attrs.update({OtelAttr.PROMPT_NAME: prompt_name})
|
|
1119
|
+
with create_mcp_client_span("prompts/get", target=prompt_name, attributes=attrs) as span:
|
|
1120
|
+
for attempt in range(2):
|
|
1121
|
+
# Prompt functions are exposed only after the lifecycle installs a session.
|
|
1122
|
+
session = cast("ClientSession", self.session)
|
|
1123
|
+
try:
|
|
1124
|
+
return parser(await session.get_prompt(prompt_name, arguments=kwargs))
|
|
1125
|
+
except ClosedResourceError as cl_ex:
|
|
1126
|
+
if attempt == 0:
|
|
1127
|
+
try:
|
|
1128
|
+
await self.connect(reset=True)
|
|
1129
|
+
except Exception as reconn_ex:
|
|
1130
|
+
raise ToolExecutionException(
|
|
1131
|
+
"Failed to reconnect to MCP server.",
|
|
1132
|
+
inner_exception=reconn_ex,
|
|
1133
|
+
) from reconn_ex
|
|
1134
|
+
continue
|
|
1135
|
+
set_mcp_span_error(span, type(cl_ex).__name__, truncate_mcp_error(str(cl_ex)))
|
|
1136
|
+
raise ToolExecutionException(
|
|
1137
|
+
f"Failed to call prompt '{prompt_name}' - connection lost.", inner_exception=cl_ex
|
|
1138
|
+
) from cl_ex
|
|
1139
|
+
except McpError as mcp_exc:
|
|
1140
|
+
message = truncate_mcp_error(mcp_exc.error.message)
|
|
1141
|
+
set_mcp_span_error(span, type(mcp_exc).__name__, message)
|
|
1142
|
+
raise _mcp_error_exception(mcp_exc, message, f"MCP prompt '{prompt_name}'") from mcp_exc
|
|
1143
|
+
except Exception as ex:
|
|
1144
|
+
set_mcp_span_error(span, type(ex).__name__, str(ex))
|
|
1145
|
+
raise ToolExecutionException(f"Failed to call prompt '{prompt_name}'.", inner_exception=ex) from ex
|
|
1146
|
+
raise ToolExecutionException(f"Failed to get prompt '{prompt_name}' after retries.")
|
|
1147
|
+
|
|
1148
|
+
async def __aenter__(self) -> Self:
|
|
1149
|
+
try:
|
|
1150
|
+
await self.connect()
|
|
1151
|
+
return self
|
|
1152
|
+
except ToolException:
|
|
1153
|
+
raise
|
|
1154
|
+
except Exception as ex:
|
|
1155
|
+
await self.close()
|
|
1156
|
+
raise ToolExecutionException("Failed to enter context manager.", inner_exception=ex) from ex
|
|
1157
|
+
|
|
1158
|
+
async def __aexit__(
|
|
1159
|
+
self,
|
|
1160
|
+
exc_type: type[BaseException] | None,
|
|
1161
|
+
exc_value: BaseException | None,
|
|
1162
|
+
traceback: Any,
|
|
1163
|
+
) -> None:
|
|
1164
|
+
await self.close()
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
async def _expand_mcp_tool(tool_: ToolTypes) -> list[ToolTypes] | None:
|
|
1168
|
+
if not isinstance(tool_, MCPTool):
|
|
1169
|
+
return None
|
|
1170
|
+
if not tool_.is_connected:
|
|
1171
|
+
await tool_.connect()
|
|
1172
|
+
from chrys.service.mcp.cache import clone_mcp_function_tool
|
|
1173
|
+
|
|
1174
|
+
return normalize_tools(
|
|
1175
|
+
clone_mcp_function_tool(function, result_cap=DEFAULT_MCP_TOOL_RESULT_MAX_TOKENS) for function in tool_.functions
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
_set_tool_expander(_expand_mcp_tool)
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
class MCPStdioTool(MCPTool):
|
|
1183
|
+
"""MCP tool for stdio-based servers."""
|
|
1184
|
+
|
|
1185
|
+
def __init__(
|
|
1186
|
+
self,
|
|
1187
|
+
name: str,
|
|
1188
|
+
command: str,
|
|
1189
|
+
*,
|
|
1190
|
+
tool_name_prefix: str | None = None,
|
|
1191
|
+
load_tools: bool = True,
|
|
1192
|
+
parse_tool_results: Callable[[types.CallToolResult], str | list[Content]] | None = None,
|
|
1193
|
+
load_prompts: bool = True,
|
|
1194
|
+
parse_prompt_results: Callable[[types.GetPromptResult], str] | None = None,
|
|
1195
|
+
request_timeout: int | None = None,
|
|
1196
|
+
session: ClientSession | None = None,
|
|
1197
|
+
description: str | None = None,
|
|
1198
|
+
allowed_tools: Collection[str] | None = None,
|
|
1199
|
+
args: list[str] | None = None,
|
|
1200
|
+
env: dict[str, str] | None = None,
|
|
1201
|
+
encoding: str | None = None,
|
|
1202
|
+
additional_properties: dict[str, Any] | None = None,
|
|
1203
|
+
additional_tool_argument_names: Sequence[str] | Mapping[str, str | Sequence[str]] | None = None,
|
|
1204
|
+
**kwargs: Any,
|
|
1205
|
+
) -> None:
|
|
1206
|
+
super().__init__(
|
|
1207
|
+
name=name,
|
|
1208
|
+
description=description,
|
|
1209
|
+
allowed_tools=allowed_tools,
|
|
1210
|
+
tool_name_prefix=tool_name_prefix,
|
|
1211
|
+
additional_properties=additional_properties,
|
|
1212
|
+
session=session,
|
|
1213
|
+
load_tools=load_tools,
|
|
1214
|
+
parse_tool_results=parse_tool_results,
|
|
1215
|
+
load_prompts=load_prompts,
|
|
1216
|
+
parse_prompt_results=parse_prompt_results,
|
|
1217
|
+
request_timeout=request_timeout,
|
|
1218
|
+
additional_tool_argument_names=additional_tool_argument_names,
|
|
1219
|
+
)
|
|
1220
|
+
self.command = command
|
|
1221
|
+
self.args = args or []
|
|
1222
|
+
self.env = env
|
|
1223
|
+
self.encoding = encoding
|
|
1224
|
+
self._client_kwargs = kwargs
|
|
1225
|
+
|
|
1226
|
+
def _mcp_base_span_attributes(self) -> dict[str, Any]:
|
|
1227
|
+
attrs = super()._mcp_base_span_attributes()
|
|
1228
|
+
attrs[OtelAttr.NETWORK_TRANSPORT] = "pipe"
|
|
1229
|
+
return attrs
|
|
1230
|
+
|
|
1231
|
+
def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
|
|
1232
|
+
args: dict[str, Any] = {"command": self.command, "args": self.args, "env": self.env}
|
|
1233
|
+
if self.encoding:
|
|
1234
|
+
args["encoding"] = self.encoding
|
|
1235
|
+
if self._client_kwargs:
|
|
1236
|
+
args.update(self._client_kwargs)
|
|
1237
|
+
try:
|
|
1238
|
+
from mcp.client.stdio import StdioServerParameters, stdio_client
|
|
1239
|
+
except ModuleNotFoundError as ex:
|
|
1240
|
+
raise ModuleNotFoundError("`mcp` is required to use `MCPStdioTool`. Please install `mcp`.") from ex
|
|
1241
|
+
return stdio_client(server=StdioServerParameters(**args))
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
class MCPStreamableHTTPTool(MCPTool):
|
|
1245
|
+
"""MCP tool for streamable HTTP/SSE servers."""
|
|
1246
|
+
|
|
1247
|
+
def __init__(
|
|
1248
|
+
self,
|
|
1249
|
+
name: str,
|
|
1250
|
+
url: str,
|
|
1251
|
+
*,
|
|
1252
|
+
tool_name_prefix: str | None = None,
|
|
1253
|
+
load_tools: bool = True,
|
|
1254
|
+
parse_tool_results: Callable[[types.CallToolResult], str | list[Content]] | None = None,
|
|
1255
|
+
load_prompts: bool = True,
|
|
1256
|
+
parse_prompt_results: Callable[[types.GetPromptResult], str] | None = None,
|
|
1257
|
+
request_timeout: int | None = None,
|
|
1258
|
+
session: ClientSession | None = None,
|
|
1259
|
+
description: str | None = None,
|
|
1260
|
+
allowed_tools: Collection[str] | None = None,
|
|
1261
|
+
terminate_on_close: bool | None = None,
|
|
1262
|
+
additional_properties: dict[str, Any] | None = None,
|
|
1263
|
+
http_client: AsyncClient | None = None,
|
|
1264
|
+
additional_tool_argument_names: Sequence[str] | Mapping[str, str | Sequence[str]] | None = None,
|
|
1265
|
+
**_kwargs: Any,
|
|
1266
|
+
) -> None:
|
|
1267
|
+
"""Initialize a streamable HTTP MCP tool.
|
|
1268
|
+
|
|
1269
|
+
Keyword Args:
|
|
1270
|
+
http_client: Optional caller-managed HTTP client. Sensitive credentials
|
|
1271
|
+
configured directly on this client, such as with
|
|
1272
|
+
``AsyncClient(headers=...)``, bypass Chrys's origin-scoped header
|
|
1273
|
+
hooks. If the client follows cross-origin redirects, those
|
|
1274
|
+
credentials can be sent to another origin. The caller is responsible
|
|
1275
|
+
for scoping them to the configured URL's origin.
|
|
1276
|
+
"""
|
|
1277
|
+
super().__init__(
|
|
1278
|
+
name=name,
|
|
1279
|
+
description=description,
|
|
1280
|
+
allowed_tools=allowed_tools,
|
|
1281
|
+
tool_name_prefix=tool_name_prefix,
|
|
1282
|
+
additional_properties=additional_properties,
|
|
1283
|
+
session=session,
|
|
1284
|
+
load_tools=load_tools,
|
|
1285
|
+
parse_tool_results=parse_tool_results,
|
|
1286
|
+
load_prompts=load_prompts,
|
|
1287
|
+
parse_prompt_results=parse_prompt_results,
|
|
1288
|
+
request_timeout=request_timeout,
|
|
1289
|
+
additional_tool_argument_names=additional_tool_argument_names,
|
|
1290
|
+
)
|
|
1291
|
+
self.url = url
|
|
1292
|
+
self.terminate_on_close = terminate_on_close
|
|
1293
|
+
self._httpx_client: AsyncClient | None = http_client
|
|
1294
|
+
|
|
1295
|
+
def _mcp_base_span_attributes(self) -> dict[str, Any]:
|
|
1296
|
+
attrs = super()._mcp_base_span_attributes()
|
|
1297
|
+
attrs[OtelAttr.NETWORK_TRANSPORT] = "tcp"
|
|
1298
|
+
attrs[OtelAttr.NETWORK_PROTOCOL_NAME] = "http"
|
|
1299
|
+
try:
|
|
1300
|
+
from httpx import URL
|
|
1301
|
+
|
|
1302
|
+
parsed = URL(self.url)
|
|
1303
|
+
if parsed.host:
|
|
1304
|
+
attrs[OtelAttr.ADDRESS] = parsed.host
|
|
1305
|
+
attrs[OtelAttr.PORT] = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
1306
|
+
except Exception:
|
|
1307
|
+
logger.debug("Failed to parse URL for MCP span transport attributes", exc_info=True)
|
|
1308
|
+
return attrs
|
|
1309
|
+
|
|
1310
|
+
def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
|
|
1311
|
+
return streamable_http_client(
|
|
1312
|
+
url=self.url,
|
|
1313
|
+
http_client=self._httpx_client,
|
|
1314
|
+
terminate_on_close=self.terminate_on_close if self.terminate_on_close is not None else True,
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def streamable_http_client(*args: Any, **kwargs: Any) -> _AsyncGeneratorContextManager[Any, None]:
|
|
1319
|
+
try:
|
|
1320
|
+
from mcp.client.streamable_http import streamable_http_client as _streamable_http_client
|
|
1321
|
+
except ModuleNotFoundError as ex:
|
|
1322
|
+
missing_name = ex.name or str(ex)
|
|
1323
|
+
if missing_name == "mcp" or missing_name.startswith("mcp.") or "mcp" in missing_name:
|
|
1324
|
+
raise ModuleNotFoundError("`MCPStreamableHTTPTool` requires `mcp`. Please install `mcp`.") from ex
|
|
1325
|
+
raise ModuleNotFoundError(
|
|
1326
|
+
"`MCPStreamableHTTPTool` requires streamable HTTP transport support. "
|
|
1327
|
+
f"The optional dependency `{missing_name}` is not installed. Please update your dependencies."
|
|
1328
|
+
) from ex
|
|
1329
|
+
|
|
1330
|
+
return _streamable_http_client(*args, **kwargs)
|