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,1629 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Compact, model-actionable diagnostics for tool argument errors."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from difflib import get_close_matches
|
|
11
|
+
from heapq import heappop, heappush
|
|
12
|
+
from itertools import islice
|
|
13
|
+
from typing import Any, cast
|
|
14
|
+
|
|
15
|
+
from pydantic import AliasChoices, AliasPath, BaseModel, ValidationError
|
|
16
|
+
from pydantic_core import ErrorDetails
|
|
17
|
+
|
|
18
|
+
_MAX_ARGUMENT_ISSUES = 3
|
|
19
|
+
_MAX_SCHEMA_FIELDS = 10
|
|
20
|
+
_MAX_SCHEMA_DEPTH = 4
|
|
21
|
+
_MAX_SCHEMA_TEXT_CHARS = 700
|
|
22
|
+
_MAX_MESSAGE_CHARS = 1200
|
|
23
|
+
_MAX_ERROR_DETAIL_CHARS = 160
|
|
24
|
+
_MAX_IDENTIFIER_CHARS = 80
|
|
25
|
+
# Magnitude bound (~77 digits) for rendering exact-int mapping keys. Must be
|
|
26
|
+
# independent of the interpreter's int→str digit limit, which
|
|
27
|
+
# sys.set_int_max_str_digits(0) disables entirely — a multi-megabyte repr must
|
|
28
|
+
# not be built even transiently.
|
|
29
|
+
_MAX_INT_KEY_BITS = 256
|
|
30
|
+
# Bounds on formatting-path work over argument values: a hostile container can
|
|
31
|
+
# iterate indefinitely, shared references can amplify a small graph into
|
|
32
|
+
# exponential traversal work, and comparison operators on non-exact types
|
|
33
|
+
# dispatch to user code. Enum/const checks therefore only compare all-exact
|
|
34
|
+
# trees within a TOTAL node-visit budget, and the payload scan spends at most
|
|
35
|
+
# a fixed work budget (large strings charge by size).
|
|
36
|
+
_MAX_COMPARE_ITEMS = 256
|
|
37
|
+
_MAX_PAYLOAD_SCAN_ITEMS = 10_000
|
|
38
|
+
# Enum rendering cap: the narrowest rendered value is one character plus a
|
|
39
|
+
# three-character separator, so 192 values always reach the 700-character
|
|
40
|
+
# schema-text bound (1 + 191*4 = 765) — the capped join truncates identically
|
|
41
|
+
# to the uncapped one while never serializing a pathologically wide enum.
|
|
42
|
+
_MAX_ENUM_RENDER_VALUES = 192
|
|
43
|
+
# Caps on per-issue work for degenerate calls: only the first
|
|
44
|
+
# ``_MAX_ARGUMENT_ISSUES`` issues are displayed, so sorting/annotating vast
|
|
45
|
+
# unexpected-name sets or materializing every error dict of a huge
|
|
46
|
+
# ValidationError is wasted work — beyond these bounds the trailing
|
|
47
|
+
# issue COUNT degrades (label-only) or issues fall back to schema/kind
|
|
48
|
+
# comparison, but the formatter stays under the adversarial time ceiling.
|
|
49
|
+
_MAX_UNEXPECTED_NAMES = 4096
|
|
50
|
+
_MAX_MATERIALIZED_ERRORS = 50_000
|
|
51
|
+
# Entry cap for the display-safe arguments copy: every per-key pass over the
|
|
52
|
+
# mapping (exactness check, normalization, kind comparison) must stay bounded
|
|
53
|
+
# for degenerate multi-million-key calls. Entries beyond the cap are dropped
|
|
54
|
+
# from the DISPLAY view only (validation itself already ran on the full
|
|
55
|
+
# mapping); the copy is then marked incomplete so the echo guard suppresses
|
|
56
|
+
# detail rather than vouching for strings it never saw.
|
|
57
|
+
_MAX_SAFE_ARGUMENT_ENTRIES = 100_000
|
|
58
|
+
|
|
59
|
+
# Pydantic-core error types whose built-in ``msg`` is derived only from the
|
|
60
|
+
# schema or the parser, never from the argument payload. Any other type —
|
|
61
|
+
# including ``value_error`` / ``assertion_error`` and ``PydanticCustomError``
|
|
62
|
+
# codes — carries arbitrary validator text that may embed input values, so its
|
|
63
|
+
# ``msg`` must not reach the model-visible message. ``PydanticCustomError`` can
|
|
64
|
+
# spoof an allowlisted code, so allowlisted details additionally pass a
|
|
65
|
+
# best-effort payload-echo guard (exact string containment). A validator that
|
|
66
|
+
# deliberately transforms the payload to evade it is out of scope: validators
|
|
67
|
+
# are in-process tool-author code that can already emit arbitrary tool results.
|
|
68
|
+
_DETAIL_SAFE_ERROR_TYPES = frozenset(
|
|
69
|
+
{
|
|
70
|
+
"enum",
|
|
71
|
+
"string_too_short",
|
|
72
|
+
"string_too_long",
|
|
73
|
+
"too_short",
|
|
74
|
+
"too_long",
|
|
75
|
+
"greater_than",
|
|
76
|
+
"greater_than_equal",
|
|
77
|
+
"less_than",
|
|
78
|
+
"less_than_equal",
|
|
79
|
+
"multiple_of",
|
|
80
|
+
"finite_number",
|
|
81
|
+
"string_pattern_mismatch",
|
|
82
|
+
"int_parsing",
|
|
83
|
+
"int_from_float",
|
|
84
|
+
"float_parsing",
|
|
85
|
+
"bool_parsing",
|
|
86
|
+
"decimal_parsing",
|
|
87
|
+
"json_invalid",
|
|
88
|
+
"date_parsing",
|
|
89
|
+
"datetime_parsing",
|
|
90
|
+
"time_parsing",
|
|
91
|
+
"timedelta_parsing",
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_exact_type(value: Any, *expected: type) -> bool:
|
|
97
|
+
"""Identity-compare ``type(value)`` against ``expected``.
|
|
98
|
+
|
|
99
|
+
``type(value) in (int, float)`` compares by EQUALITY, which dispatches to
|
|
100
|
+
a caller-controlled metaclass ``__eq__`` — user code that can lie about
|
|
101
|
+
exactness, raise, or block mid-formatting. ``is`` never dispatches.
|
|
102
|
+
"""
|
|
103
|
+
value_type = type(value)
|
|
104
|
+
return any(value_type is candidate for candidate in expected)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _bounded_text(text: str, limit: int) -> str:
|
|
108
|
+
if len(text) <= limit:
|
|
109
|
+
return text
|
|
110
|
+
return text[: limit - 1].rstrip() + "…"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _sanitize_message_text(text: str) -> str:
|
|
114
|
+
"""Escape unprintable and lone-surrogate characters.
|
|
115
|
+
|
|
116
|
+
``json.dumps(..., ensure_ascii=False)`` passes lone surrogates and C1
|
|
117
|
+
controls through raw; either would make the next request body
|
|
118
|
+
non-UTF-8-encodable or inject terminal escapes, so the finished message
|
|
119
|
+
gets one defensive pass.
|
|
120
|
+
"""
|
|
121
|
+
if all(char == " " or char.isprintable() for char in text):
|
|
122
|
+
return text
|
|
123
|
+
return "".join(char if char == " " or char.isprintable() else ascii(char)[1:-1] for char in text)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _bounded_schema_text(text: str) -> str:
|
|
127
|
+
return _bounded_text(text, _MAX_SCHEMA_TEXT_CHARS)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _schema_field_name(value: Any) -> str:
|
|
131
|
+
if isinstance(value, str) and value.isidentifier():
|
|
132
|
+
return value
|
|
133
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), default=str)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _resolve_local_schema_ref(schema: Mapping[str, Any], root_schema: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
137
|
+
"""Resolve a local JSON-schema ref without following external resources."""
|
|
138
|
+
current = schema
|
|
139
|
+
visited: set[str] = set()
|
|
140
|
+
while isinstance(ref := current.get("$ref"), str) and ref.startswith("#/") and ref not in visited:
|
|
141
|
+
visited.add(ref)
|
|
142
|
+
target: Any = root_schema
|
|
143
|
+
for raw_part in ref[2:].split("/"):
|
|
144
|
+
part = raw_part.replace("~1", "/").replace("~0", "~")
|
|
145
|
+
if not isinstance(target, Mapping) or part not in target:
|
|
146
|
+
return current
|
|
147
|
+
target = target[part]
|
|
148
|
+
if not isinstance(target, Mapping):
|
|
149
|
+
return current
|
|
150
|
+
current = target
|
|
151
|
+
return current
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _schema_variants(schema: Mapping[str, Any], root_schema: Mapping[str, Any]) -> list[Mapping[str, Any]]:
|
|
155
|
+
resolved = _resolve_local_schema_ref(schema, root_schema)
|
|
156
|
+
for key in ("anyOf", "oneOf", "allOf"):
|
|
157
|
+
variants = resolved.get(key)
|
|
158
|
+
if isinstance(variants, list):
|
|
159
|
+
return [_resolve_local_schema_ref(item, root_schema) for item in variants if isinstance(item, Mapping)]
|
|
160
|
+
return [resolved]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _schema_type_text(
|
|
164
|
+
schema: Mapping[str, Any],
|
|
165
|
+
root_schema: Mapping[str, Any],
|
|
166
|
+
*,
|
|
167
|
+
depth: int = 0,
|
|
168
|
+
) -> str:
|
|
169
|
+
"""Render a compact, model-readable JSON-schema type expression."""
|
|
170
|
+
if depth >= _MAX_SCHEMA_DEPTH:
|
|
171
|
+
return "object" if schema.get("type") == "object" else "value"
|
|
172
|
+
|
|
173
|
+
resolved = _resolve_local_schema_ref(schema, root_schema)
|
|
174
|
+
for key in ("anyOf", "oneOf", "allOf"):
|
|
175
|
+
variants = resolved.get(key)
|
|
176
|
+
if isinstance(variants, list):
|
|
177
|
+
rendered = [
|
|
178
|
+
_schema_type_text(item, root_schema, depth=depth + 1) for item in variants if isinstance(item, Mapping)
|
|
179
|
+
]
|
|
180
|
+
separator = " & " if key == "allOf" else " | "
|
|
181
|
+
return _bounded_schema_text(separator.join(dict.fromkeys(rendered))) or "value"
|
|
182
|
+
|
|
183
|
+
enum_values = resolved.get("enum")
|
|
184
|
+
if isinstance(enum_values, list) and enum_values:
|
|
185
|
+
return _bounded_schema_text(
|
|
186
|
+
" | ".join(
|
|
187
|
+
json.dumps(value, ensure_ascii=False, separators=(",", ":"), default=str)
|
|
188
|
+
for value in islice(enum_values, _MAX_ENUM_RENDER_VALUES)
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
if "const" in resolved:
|
|
192
|
+
return _bounded_schema_text(
|
|
193
|
+
json.dumps(resolved["const"], ensure_ascii=False, separators=(",", ":"), default=str)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
schema_type = resolved.get("type")
|
|
197
|
+
if isinstance(schema_type, list):
|
|
198
|
+
return _bounded_schema_text(" | ".join(str(item) for item in schema_type))
|
|
199
|
+
if schema_type == "array":
|
|
200
|
+
prefix_items = resolved.get("prefixItems")
|
|
201
|
+
if isinstance(prefix_items, list):
|
|
202
|
+
rendered_items = [
|
|
203
|
+
_schema_type_text(item, root_schema, depth=depth + 1)
|
|
204
|
+
for item in prefix_items
|
|
205
|
+
if isinstance(item, Mapping)
|
|
206
|
+
]
|
|
207
|
+
return _bounded_schema_text("[" + ", ".join(rendered_items) + "]")
|
|
208
|
+
items = resolved.get("items")
|
|
209
|
+
item_text = _schema_type_text(items, root_schema, depth=depth + 1) if isinstance(items, Mapping) else "value"
|
|
210
|
+
return _bounded_schema_text(f"[{item_text}]")
|
|
211
|
+
if schema_type == "object" or isinstance(resolved.get("properties"), Mapping):
|
|
212
|
+
properties = resolved.get("properties")
|
|
213
|
+
if not isinstance(properties, Mapping) or not properties:
|
|
214
|
+
additional = resolved.get("additionalProperties")
|
|
215
|
+
if isinstance(additional, Mapping):
|
|
216
|
+
return _bounded_schema_text(
|
|
217
|
+
f"{{string: {_schema_type_text(additional, root_schema, depth=depth + 1)}}}"
|
|
218
|
+
)
|
|
219
|
+
return "object"
|
|
220
|
+
typed_properties = cast("Mapping[str, Any]", properties)
|
|
221
|
+
required = {name for name in resolved.get("required", []) if isinstance(name, str)}
|
|
222
|
+
fields: list[str] = []
|
|
223
|
+
for index, (name, field_schema) in enumerate(typed_properties.items()):
|
|
224
|
+
if index >= _MAX_SCHEMA_FIELDS:
|
|
225
|
+
fields.append(f"… +{len(typed_properties) - _MAX_SCHEMA_FIELDS} fields")
|
|
226
|
+
break
|
|
227
|
+
field_type = (
|
|
228
|
+
_schema_type_text(field_schema, root_schema, depth=depth + 1)
|
|
229
|
+
if isinstance(field_schema, Mapping)
|
|
230
|
+
else "value"
|
|
231
|
+
)
|
|
232
|
+
optional = "" if name in required else "?"
|
|
233
|
+
fields.append(f"{_schema_field_name(name)}{optional}: {field_type}")
|
|
234
|
+
if sum(len(field) for field in fields) > _MAX_SCHEMA_TEXT_CHARS:
|
|
235
|
+
fields.append("…")
|
|
236
|
+
break
|
|
237
|
+
return _bounded_schema_text("{" + ", ".join(fields) + "}")
|
|
238
|
+
if isinstance(schema_type, str):
|
|
239
|
+
return schema_type
|
|
240
|
+
return "value"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True, slots=True)
|
|
244
|
+
class _UnionVariantMatch:
|
|
245
|
+
"""A union variant identified from a Pydantic branch marker."""
|
|
246
|
+
|
|
247
|
+
variant: Mapping[str, Any]
|
|
248
|
+
"""The selected variant schema."""
|
|
249
|
+
|
|
250
|
+
via_discriminator: bool
|
|
251
|
+
"""True for a ``discriminator.mapping`` tag match — authoritative, since
|
|
252
|
+
Pydantic always writes the tag into ``loc`` for tagged-union sub-errors.
|
|
253
|
+
False for a schema ``title`` match, which is only a heuristic."""
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _select_union_variant(
|
|
257
|
+
union_schema: Mapping[str, Any],
|
|
258
|
+
marker: Any,
|
|
259
|
+
root_schema: Mapping[str, Any],
|
|
260
|
+
) -> _UnionVariantMatch | None:
|
|
261
|
+
"""Pick the union variant a Pydantic branch marker names, if identifiable.
|
|
262
|
+
|
|
263
|
+
Primitive-type markers (``'str'``, ``'list[str]'``) match neither the
|
|
264
|
+
discriminator mapping nor a title and return ``None`` — for those the
|
|
265
|
+
caller's first-variant-with-member search is unambiguous because
|
|
266
|
+
primitive variants have no members to collide on.
|
|
267
|
+
"""
|
|
268
|
+
if isinstance(marker, str):
|
|
269
|
+
mapping_keys = [marker]
|
|
270
|
+
elif isinstance(marker, bool):
|
|
271
|
+
mapping_keys = [str(marker), json.dumps(marker)]
|
|
272
|
+
elif isinstance(marker, int | float):
|
|
273
|
+
# Numeric discriminator values appear as numbers in ``loc`` but as
|
|
274
|
+
# strings in the JSON-schema ``discriminator.mapping``. Boolean tags
|
|
275
|
+
# surface as ``1``/``0`` in ``loc`` while the mapping uses
|
|
276
|
+
# ``"True"``/``"False"``.
|
|
277
|
+
mapping_keys = [str(marker)]
|
|
278
|
+
if marker in (0, 1):
|
|
279
|
+
mapping_keys.append(str(bool(marker)))
|
|
280
|
+
else:
|
|
281
|
+
return None
|
|
282
|
+
discriminator = union_schema.get("discriminator")
|
|
283
|
+
if isinstance(discriminator, Mapping):
|
|
284
|
+
mapping = discriminator.get("mapping")
|
|
285
|
+
if isinstance(mapping, Mapping):
|
|
286
|
+
for key in mapping_keys:
|
|
287
|
+
ref = mapping.get(key)
|
|
288
|
+
if isinstance(ref, str):
|
|
289
|
+
resolved = _resolve_local_schema_ref({"$ref": ref}, root_schema)
|
|
290
|
+
if "$ref" not in resolved:
|
|
291
|
+
return _UnionVariantMatch(resolved, via_discriminator=True)
|
|
292
|
+
if isinstance(marker, str):
|
|
293
|
+
for variant in _schema_variants(union_schema, root_schema):
|
|
294
|
+
if variant.get("title") == marker:
|
|
295
|
+
return _UnionVariantMatch(variant, via_discriminator=False)
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# Names Pydantic uses as union branch markers for untitled variants; used to
|
|
300
|
+
# tell a marker-shaped loc part from a plausible mapping key.
|
|
301
|
+
_MARKER_TYPE_NAMES = frozenset(
|
|
302
|
+
{
|
|
303
|
+
"str",
|
|
304
|
+
"int",
|
|
305
|
+
"float",
|
|
306
|
+
"bool",
|
|
307
|
+
"none",
|
|
308
|
+
"list",
|
|
309
|
+
"dict",
|
|
310
|
+
"set",
|
|
311
|
+
"frozenset",
|
|
312
|
+
"tuple",
|
|
313
|
+
"bytes",
|
|
314
|
+
"bytearray",
|
|
315
|
+
"date",
|
|
316
|
+
"datetime",
|
|
317
|
+
"time",
|
|
318
|
+
"timedelta",
|
|
319
|
+
"decimal",
|
|
320
|
+
"complex",
|
|
321
|
+
"enum",
|
|
322
|
+
"literal",
|
|
323
|
+
"json",
|
|
324
|
+
"url",
|
|
325
|
+
"uuid",
|
|
326
|
+
"callable",
|
|
327
|
+
"model",
|
|
328
|
+
"typed-dict",
|
|
329
|
+
"dataclass",
|
|
330
|
+
"is-instance",
|
|
331
|
+
"is-subclass",
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
_MAX_LOCATION_STATES = 500
|
|
335
|
+
# Adversarial schemas can make one bad payload fan out into thousands of
|
|
336
|
+
# per-variant validation errors; the message is bounded to ~1200 chars anyway,
|
|
337
|
+
# so resolving locations past this point is pure wasted work.
|
|
338
|
+
_MAX_VALIDATION_ERRORS = 50
|
|
339
|
+
|
|
340
|
+
_NAV_DISCRIMINATOR_SELECT_WEIGHT = 4.0
|
|
341
|
+
_NAV_EXACT_WEIGHT = 3.0
|
|
342
|
+
_NAV_TITLE_SELECT_WEIGHT = 2.0
|
|
343
|
+
_NAV_MARKER_SKIP_WEIGHT = 1.5
|
|
344
|
+
_NAV_ADDITIONAL_WEIGHT = 1.0
|
|
345
|
+
_NAV_DESPERATE_SKIP_WEIGHT = 0.5
|
|
346
|
+
# Selection after a blind skip must lose to ANY full reading that avoids it —
|
|
347
|
+
# additive weights can't express that locally (a later exact move would
|
|
348
|
+
# subsidize the selection past a cheap additionalProperties chain), so the
|
|
349
|
+
# move carries a penalty larger than any full path can earn. Full consumption
|
|
350
|
+
# still ranks above every partial reading regardless, so the move remains the
|
|
351
|
+
# resolver of last resort it is meant to be.
|
|
352
|
+
_NAV_BLIND_SELECT_WEIGHT = -1000.0
|
|
353
|
+
|
|
354
|
+
# Per-node skip state carried through the location search. A generic skip
|
|
355
|
+
# spends the union level's one marker; whether variant selection remains
|
|
356
|
+
# plausible afterwards depends on what the skipped marker's text says it
|
|
357
|
+
# wrapped.
|
|
358
|
+
_SKIP_NONE = 0
|
|
359
|
+
_SKIP_OPAQUE = 1
|
|
360
|
+
_SKIP_UNION_MARKER = 2
|
|
361
|
+
_SKIP_BLIND = 3
|
|
362
|
+
|
|
363
|
+
# Wrapper reprs Pydantic writes WITHOUT the wrapped schema — the marker
|
|
364
|
+
# carries no evidence for or against a union underneath.
|
|
365
|
+
_SCHEMALESS_WRAPPER_PREFIXES = ("function-wrap[", "function-plain[")
|
|
366
|
+
|
|
367
|
+
# Corroboration cost bounds. Scanning is errors x names x marker-length work,
|
|
368
|
+
# so an adversarial 30k-char ``Tag`` marker over a wide union costs real time.
|
|
369
|
+
# Genuine union reprs list every member, so one of the first names collected
|
|
370
|
+
# matches immediately; parts longer than any genuine repr classify opaque
|
|
371
|
+
# (fail-safe: real-key readings win).
|
|
372
|
+
_MAX_MARKER_SCAN_CHARS = 4096
|
|
373
|
+
_MAX_CORROBORATION_NAMES = 40
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _is_union_node(resolved: Mapping[str, Any]) -> bool:
|
|
377
|
+
return any(isinstance(resolved.get(key), list) for key in ("anyOf", "oneOf"))
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _is_identifier_char(char: str) -> bool:
|
|
381
|
+
return char.isalnum() or char == "_"
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _name_corroborated(name: str, part: str) -> bool:
|
|
385
|
+
"""Whether ``name`` appears in ``part`` delimited like a type reference.
|
|
386
|
+
|
|
387
|
+
Substring containment alone is spoofable — a variant titled ``B`` must
|
|
388
|
+
not be corroborated by ``union[Business]`` — so the occurrence has to be
|
|
389
|
+
bounded by non-identifier characters (``union[B,C]``, ``union[B]``).
|
|
390
|
+
"""
|
|
391
|
+
start = 0
|
|
392
|
+
while (index := part.find(name, start)) != -1:
|
|
393
|
+
before = part[index - 1] if index else ""
|
|
394
|
+
end = index + len(name)
|
|
395
|
+
after = part[end] if end < len(part) else ""
|
|
396
|
+
if not (before and _is_identifier_char(before)) and not (after and _is_identifier_char(after)):
|
|
397
|
+
return True
|
|
398
|
+
start = index + 1
|
|
399
|
+
return False
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _variant_reference_names(node: Mapping[str, Any], root_schema: Mapping[str, Any]) -> list[str]:
|
|
403
|
+
"""Names the marker of a union at ``node`` could reference, depth <= 1.
|
|
404
|
+
|
|
405
|
+
Pydantic writes CORE names into markers — the class name, which is also
|
|
406
|
+
the ``$defs`` key — while the advertised JSON schema may carry customized
|
|
407
|
+
``title`` values, so both the ``$ref`` basenames and the resolved titles
|
|
408
|
+
of every variant are candidates, plus (one nested-union level deep) the
|
|
409
|
+
members and discriminator-mapping entries of a variant that is itself a
|
|
410
|
+
union node.
|
|
411
|
+
"""
|
|
412
|
+
names: list[str] = []
|
|
413
|
+
|
|
414
|
+
def collect(item: Mapping[str, Any], depth: int) -> None:
|
|
415
|
+
reference = item.get("$ref")
|
|
416
|
+
if isinstance(reference, str) and reference:
|
|
417
|
+
names.append(reference.rsplit("/", 1)[-1].replace("~1", "/").replace("~0", "~"))
|
|
418
|
+
resolved = _resolve_local_schema_ref(item, root_schema)
|
|
419
|
+
title = resolved.get("title")
|
|
420
|
+
if isinstance(title, str) and title:
|
|
421
|
+
names.append(title)
|
|
422
|
+
if depth > 0:
|
|
423
|
+
return
|
|
424
|
+
discriminator = resolved.get("discriminator")
|
|
425
|
+
if isinstance(discriminator, Mapping):
|
|
426
|
+
mapping = discriminator.get("mapping")
|
|
427
|
+
if isinstance(mapping, Mapping):
|
|
428
|
+
for tag, target in mapping.items():
|
|
429
|
+
if isinstance(tag, str) and tag:
|
|
430
|
+
names.append(tag)
|
|
431
|
+
if isinstance(target, str) and target:
|
|
432
|
+
names.append(target.rsplit("/", 1)[-1].replace("~1", "/").replace("~0", "~"))
|
|
433
|
+
for key in ("anyOf", "oneOf"):
|
|
434
|
+
members = resolved.get(key)
|
|
435
|
+
if not isinstance(members, list):
|
|
436
|
+
continue
|
|
437
|
+
for member in members:
|
|
438
|
+
if isinstance(member, Mapping):
|
|
439
|
+
collect(member, depth + 1)
|
|
440
|
+
|
|
441
|
+
for key in ("anyOf", "oneOf"):
|
|
442
|
+
items = node.get(key)
|
|
443
|
+
if not isinstance(items, list):
|
|
444
|
+
continue
|
|
445
|
+
for item in items:
|
|
446
|
+
if isinstance(item, Mapping):
|
|
447
|
+
collect(item, 0)
|
|
448
|
+
return names
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _skip_state_for_marker(
|
|
452
|
+
part: Any,
|
|
453
|
+
node: Mapping[str, Any],
|
|
454
|
+
root_schema: Mapping[str, Any],
|
|
455
|
+
) -> int:
|
|
456
|
+
"""Classify what selection rights survive skipping this marker.
|
|
457
|
+
|
|
458
|
+
Schema generation can flatten a validator-wrapped union
|
|
459
|
+
(``Annotated[Cat | Dog, AfterValidator(...)]``) into the containing
|
|
460
|
+
``anyOf``, in which case the flattened level's own marker follows the
|
|
461
|
+
wrapper marker at the SAME schema node. The wrapper marker's text is the
|
|
462
|
+
only evidence: ``_SKIP_UNION_MARKER`` requires it to both name a core
|
|
463
|
+
union (``union[``) and mention, delimiter-bounded, a variant name
|
|
464
|
+
reachable here (titles, ``$ref``/discriminator-mapping names, depth
|
|
465
|
+
``<=`` one nested union) — corroboration that defeats poisoned
|
|
466
|
+
``Tag("union[...")`` text naming no real variant. Wrapper reprs written
|
|
467
|
+
without the wrapped schema (``function-wrap[fn()]``) prove nothing
|
|
468
|
+
either way, so ``_SKIP_BLIND`` keeps selection possible at a penalty
|
|
469
|
+
below every real-key reading. Everything else named a non-union, so no
|
|
470
|
+
flattened level can follow: ``_SKIP_OPAQUE`` turns selection off.
|
|
471
|
+
"""
|
|
472
|
+
# Length gate FIRST: the containment scan is O(len(part)), and a loc part
|
|
473
|
+
# can be a caller-supplied mapping key of arbitrary size repeated across
|
|
474
|
+
# every error of a wide union.
|
|
475
|
+
if isinstance(part, str) and len(part) <= _MAX_MARKER_SCAN_CHARS and "union[" in part:
|
|
476
|
+
for name in _variant_reference_names(node, root_schema)[:_MAX_CORROBORATION_NAMES]:
|
|
477
|
+
if _name_corroborated(name, part):
|
|
478
|
+
return _SKIP_UNION_MARKER
|
|
479
|
+
if isinstance(part, str) and part.startswith(_SCHEMALESS_WRAPPER_PREFIXES):
|
|
480
|
+
return _SKIP_BLIND
|
|
481
|
+
return _SKIP_OPAQUE
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _marker_like(part: Any, variants: Sequence[Mapping[str, Any]]) -> bool:
|
|
485
|
+
"""Whether a loc part is shaped like a Pydantic union branch marker."""
|
|
486
|
+
if not isinstance(part, str):
|
|
487
|
+
# Numeric/boolean markers are discriminator tags; JSON object keys
|
|
488
|
+
# are always strings.
|
|
489
|
+
return True
|
|
490
|
+
if len(part) > _MAX_MARKER_SCAN_CHARS:
|
|
491
|
+
# No genuine Pydantic marker repr is this long — this is a real
|
|
492
|
+
# (giant) input key, and the containment scans below are O(len).
|
|
493
|
+
return False
|
|
494
|
+
if "[" in part or "-" in part:
|
|
495
|
+
# Parameterized or wrapped type expressions: ``dict[str,int]``,
|
|
496
|
+
# ``function-after[...]``, ``lax-or-strict``.
|
|
497
|
+
return True
|
|
498
|
+
if part in _MARKER_TYPE_NAMES:
|
|
499
|
+
return True
|
|
500
|
+
return any(variant.get("title") == part for variant in variants)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _resolve_error_location(
|
|
504
|
+
root_schema: Mapping[str, Any],
|
|
505
|
+
location: Sequence[Any],
|
|
506
|
+
alias_to_property: Mapping[str, str] | None = None,
|
|
507
|
+
location_display: Mapping[str, str] | None = None,
|
|
508
|
+
loc_uses_field_names: bool = False,
|
|
509
|
+
) -> tuple[str, Mapping[str, Any] | None]:
|
|
510
|
+
"""Map a Pydantic error location to a display path and schema node.
|
|
511
|
+
|
|
512
|
+
Pydantic inserts the union variant's tag or type name into ``loc`` for
|
|
513
|
+
union errors (e.g. ``('pet', 'dog', 'barks')``) — but omits it in some
|
|
514
|
+
shapes (nullable single-variant unions) — so a loc part at a union node is
|
|
515
|
+
genuinely ambiguous between a branch marker and a real input key. The
|
|
516
|
+
resolver runs a small best-first search over the readings and keeps the
|
|
517
|
+
best-scoring one: full consumption of the loc beats falling off the
|
|
518
|
+
schema, then summed move weights decide — authoritative variant selection
|
|
519
|
+
(4: a discriminator-tag match, or a title match at a union with two or
|
|
520
|
+
more non-null variants, where Pydantic always writes the marker; includes
|
|
521
|
+
one nested-union level, e.g. an optional discriminated union) beats exact
|
|
522
|
+
property/alias navigation (3), which beats title selection at a nullable
|
|
523
|
+
single-variant union (2 — no marker is written there, so a title match is
|
|
524
|
+
only a heuristic), which beats a generic marker skip (1.5 for
|
|
525
|
+
marker-shaped parts, 0.5 as a last resort), which beats
|
|
526
|
+
``additionalProperties`` navigation (1 — it matches any string and must
|
|
527
|
+
not swallow markers). The generic skip likewise requires at least two
|
|
528
|
+
non-null variants and runs at most once per union node — except that a
|
|
529
|
+
blind skip may be followed by one more skip of a part carrying its own
|
|
530
|
+
marker evidence, because a schema-less wrapper can hide a second wrapper
|
|
531
|
+
level. After a skip, Pydantic's one-marker-per-union-level rule says
|
|
532
|
+
this node's marker is spent; what may still select here is classified by
|
|
533
|
+
``_skip_state_for_marker`` from the skipped marker's text. A marker that
|
|
534
|
+
names a core union AND mentions, delimiter-bounded, a reachable variant
|
|
535
|
+
name (corroboration — ``tagged-union[Cat,Dog]``,
|
|
536
|
+
``function-after[..., union[Cat,Dog]]``, matched against titles and
|
|
537
|
+
``$ref``/discriminator names since JSON titles may be customized) keeps
|
|
538
|
+
selection available: direct selection demoted below exact property
|
|
539
|
+
navigation (a variant's real key named like a sibling title wins),
|
|
540
|
+
nested selection into an inner union node at full weight (its tag may
|
|
541
|
+
follow the wrapper marker). A schema-less wrapper repr
|
|
542
|
+
(``function-wrap[fn()]``) proves nothing, so selection survives only as
|
|
543
|
+
a last resort: its penalty weight loses to every full reading that
|
|
544
|
+
avoids it, while full consumption still beats any partial reading. Any
|
|
545
|
+
other marker named a non-union, so no flattened level can follow and
|
|
546
|
+
selection — nested included — is off; the following part must be a real
|
|
547
|
+
key. The search runs a greedy
|
|
548
|
+
first-choice dive to secure a full-path incumbent, then a best-first
|
|
549
|
+
refinement ordered by an admissible optimistic bound: the result is never
|
|
550
|
+
worse than the greedy reading, and provably optimal whenever the
|
|
551
|
+
refinement completes within the state cap (a safety valve for
|
|
552
|
+
adversarially wide schemas). Loc parts that used an accepted alias
|
|
553
|
+
spelling navigate via ``alias_to_property`` but keep the spelling the
|
|
554
|
+
model actually sent in the display path; top-level parts Pydantic spelled
|
|
555
|
+
with a name validation does not accept are re-spelled via
|
|
556
|
+
``location_display``, and with ``loc_uses_field_names``
|
|
557
|
+
(``loc_by_alias=False``) the field-name map takes precedence over a
|
|
558
|
+
same-named sibling property. Both maps cover top-level fields only and
|
|
559
|
+
are consulted only for the top-level part, so nested alias locs degrade
|
|
560
|
+
to the containing object node.
|
|
561
|
+
"""
|
|
562
|
+
best_key: tuple[int, float] = (-1, -1.0)
|
|
563
|
+
best_display: list[Any] = list(location)
|
|
564
|
+
best_node: Mapping[str, Any] | None = root_schema
|
|
565
|
+
|
|
566
|
+
def offer(key: tuple[int, float], display: list[Any], node: Mapping[str, Any]) -> None:
|
|
567
|
+
nonlocal best_key, best_display, best_node
|
|
568
|
+
if key > best_key:
|
|
569
|
+
best_key, best_display, best_node = key, display, node
|
|
570
|
+
|
|
571
|
+
def expand(
|
|
572
|
+
index: int,
|
|
573
|
+
node: Mapping[str, Any],
|
|
574
|
+
skip_state: int,
|
|
575
|
+
display: list[Any],
|
|
576
|
+
) -> list[tuple[float, Mapping[str, Any], int, list[Any]]]:
|
|
577
|
+
"""All readings of ``location[index]`` as (weight, node, skip_state, display)."""
|
|
578
|
+
part = location[index]
|
|
579
|
+
resolved = _resolve_local_schema_ref(node, root_schema)
|
|
580
|
+
variants = _schema_variants(resolved, root_schema)
|
|
581
|
+
union = _is_union_node(resolved)
|
|
582
|
+
non_null_variants = [variant for variant in variants if variant.get("type") != "null"]
|
|
583
|
+
# Pydantic emits branch markers only when there is a real branch
|
|
584
|
+
# choice; a nullable single-variant union gets none, so there a
|
|
585
|
+
# marker-shaped part is a real key and a title match is a guess.
|
|
586
|
+
markers_expected = len(non_null_variants) >= 2
|
|
587
|
+
moves: list[tuple[float, Mapping[str, Any], int, list[Any]]] = []
|
|
588
|
+
selected: _UnionVariantMatch | None = None
|
|
589
|
+
if union and skip_state != _SKIP_OPAQUE:
|
|
590
|
+
selected = _select_union_variant(resolved, part, root_schema)
|
|
591
|
+
if selected is not None:
|
|
592
|
+
if skip_state == _SKIP_UNION_MARKER:
|
|
593
|
+
# The skip spent this level's one marker, but its text
|
|
594
|
+
# named a flattened core union whose own marker may
|
|
595
|
+
# legitimately follow at this node. Demote the move below
|
|
596
|
+
# exact property navigation so a variant's real key named
|
|
597
|
+
# like a sibling title still wins.
|
|
598
|
+
select_weight = _NAV_TITLE_SELECT_WEIGHT
|
|
599
|
+
elif skip_state == _SKIP_BLIND:
|
|
600
|
+
# The skipped wrapper proved nothing about what it
|
|
601
|
+
# wrapped; keep selection reachable but below EVERY
|
|
602
|
+
# real-key reading, additionalProperties included.
|
|
603
|
+
select_weight = _NAV_BLIND_SELECT_WEIGHT
|
|
604
|
+
else:
|
|
605
|
+
select_weight = (
|
|
606
|
+
_NAV_DISCRIMINATOR_SELECT_WEIGHT
|
|
607
|
+
if selected.via_discriminator or markers_expected
|
|
608
|
+
else _NAV_TITLE_SELECT_WEIGHT
|
|
609
|
+
)
|
|
610
|
+
moves.append((select_weight, selected.variant, _SKIP_NONE, display))
|
|
611
|
+
for variant in variants:
|
|
612
|
+
if not _is_union_node(variant):
|
|
613
|
+
continue
|
|
614
|
+
nested = _select_union_variant(variant, part, root_schema)
|
|
615
|
+
if nested is None or (selected is not None and nested.variant is selected.variant):
|
|
616
|
+
continue
|
|
617
|
+
if skip_state == _SKIP_BLIND:
|
|
618
|
+
select_weight = _NAV_BLIND_SELECT_WEIGHT
|
|
619
|
+
else:
|
|
620
|
+
# _SKIP_UNION_MARKER corroborated the nested union's own
|
|
621
|
+
# member titles, so the inner tag keeps full weight there
|
|
622
|
+
# just as with no skip at all.
|
|
623
|
+
nested_variants = _schema_variants(variant, root_schema)
|
|
624
|
+
nested_markers_expected = sum(1 for item in nested_variants if item.get("type") != "null") >= 2
|
|
625
|
+
select_weight = (
|
|
626
|
+
_NAV_DISCRIMINATOR_SELECT_WEIGHT
|
|
627
|
+
if nested.via_discriminator or nested_markers_expected
|
|
628
|
+
else _NAV_TITLE_SELECT_WEIGHT
|
|
629
|
+
)
|
|
630
|
+
moves.append((select_weight, nested.variant, _SKIP_NONE, display))
|
|
631
|
+
seen_children: list[Mapping[str, Any]] = []
|
|
632
|
+
for variant in variants:
|
|
633
|
+
candidates: list[tuple[Any, float]] = []
|
|
634
|
+
if isinstance(part, str):
|
|
635
|
+
properties = variant.get("properties")
|
|
636
|
+
exact: Any = None
|
|
637
|
+
if isinstance(properties, Mapping):
|
|
638
|
+
# The alias map covers top-level fields only; consulting
|
|
639
|
+
# it deeper could resolve to an unrelated sibling.
|
|
640
|
+
mapped = alias_to_property.get(part) if alias_to_property and index == 0 else None
|
|
641
|
+
if loc_uses_field_names and mapped is not None:
|
|
642
|
+
# ``loc_by_alias=False`` locs are field names; the map
|
|
643
|
+
# is authoritative over a same-named sibling property.
|
|
644
|
+
exact = properties.get(mapped)
|
|
645
|
+
if not isinstance(exact, Mapping):
|
|
646
|
+
exact = properties.get(part)
|
|
647
|
+
if not isinstance(exact, Mapping) and mapped is not None:
|
|
648
|
+
exact = properties.get(mapped)
|
|
649
|
+
candidates.append((exact, _NAV_EXACT_WEIGHT))
|
|
650
|
+
candidates.append((variant.get("additionalProperties"), _NAV_ADDITIONAL_WEIGHT))
|
|
651
|
+
elif isinstance(part, int):
|
|
652
|
+
prefix_items = variant.get("prefixItems")
|
|
653
|
+
if isinstance(prefix_items, list) and part < len(prefix_items):
|
|
654
|
+
candidates.append((prefix_items[part], _NAV_EXACT_WEIGHT))
|
|
655
|
+
else:
|
|
656
|
+
candidates.append((variant.get("items"), _NAV_EXACT_WEIGHT))
|
|
657
|
+
for child, move_weight in candidates:
|
|
658
|
+
if not isinstance(child, Mapping) or any(child is other for other in seen_children):
|
|
659
|
+
continue
|
|
660
|
+
seen_children.append(child)
|
|
661
|
+
shown = part
|
|
662
|
+
if isinstance(part, str) and index == 0 and location_display:
|
|
663
|
+
shown = location_display.get(part, part)
|
|
664
|
+
moves.append((move_weight, child, _SKIP_NONE, [*display, shown]))
|
|
665
|
+
if union and skip_state in (_SKIP_NONE, _SKIP_BLIND) and markers_expected:
|
|
666
|
+
next_skip_state = _skip_state_for_marker(part, resolved, root_schema)
|
|
667
|
+
# From a blind skip a SECOND same-node skip is allowed only for a
|
|
668
|
+
# part carrying its own marker evidence: the schema-less wrapper
|
|
669
|
+
# may have hidden another wrapper level (``function-wrap[...]``
|
|
670
|
+
# around ``Annotated[Cat | Dog, AfterValidator(...)]`` emits two
|
|
671
|
+
# consecutive same-node wrapper markers). An evidence-free part
|
|
672
|
+
# after a blind skip stays a selection/real-key question instead.
|
|
673
|
+
if skip_state == _SKIP_NONE or next_skip_state != _SKIP_OPAQUE:
|
|
674
|
+
skip_weight = _NAV_MARKER_SKIP_WEIGHT if _marker_like(part, variants) else _NAV_DESPERATE_SKIP_WEIGHT
|
|
675
|
+
moves.append((skip_weight, resolved, next_skip_state, display))
|
|
676
|
+
return moves
|
|
677
|
+
|
|
678
|
+
# Phase 1: greedy first-choice dive. Costs at most ``len(location)``
|
|
679
|
+
# expansions and secures a full-path incumbent whenever best-local moves
|
|
680
|
+
# reach one, so adversarially wide schemas can't starve the result down
|
|
681
|
+
# to a raw-marker path.
|
|
682
|
+
index, node, skip_state, weight = 0, root_schema, _SKIP_NONE, 0.0
|
|
683
|
+
display: list[Any] = []
|
|
684
|
+
while True:
|
|
685
|
+
if index == len(location):
|
|
686
|
+
offer((1, weight), display, node)
|
|
687
|
+
break
|
|
688
|
+
moves = expand(index, node, skip_state, display)
|
|
689
|
+
if not moves:
|
|
690
|
+
offer((0, weight), [*display, *location[index:]], node)
|
|
691
|
+
break
|
|
692
|
+
move_weight, node, skip_state, display = max(moves, key=lambda move: move[0])
|
|
693
|
+
weight += move_weight
|
|
694
|
+
index += 1
|
|
695
|
+
|
|
696
|
+
# Phase 2: best-first refinement. Heap entries: (-optimistic_bound,
|
|
697
|
+
# insertion_order, index, node, skip_state, weight, display). The bound
|
|
698
|
+
# is admissible (no move exceeds the authoritative-selection weight), so
|
|
699
|
+
# the first fully-consumed state popped is the global optimum; the greedy
|
|
700
|
+
# incumbent prunes everything it already dominates.
|
|
701
|
+
counter = 0
|
|
702
|
+
heap: list[tuple[float, int, int, Mapping[str, Any], int, float, list[Any]]] = [
|
|
703
|
+
(-(len(location) * _NAV_DISCRIMINATOR_SELECT_WEIGHT), 0, 0, root_schema, _SKIP_NONE, 0.0, [])
|
|
704
|
+
]
|
|
705
|
+
states = 0
|
|
706
|
+
while heap:
|
|
707
|
+
states += 1
|
|
708
|
+
if states > _MAX_LOCATION_STATES:
|
|
709
|
+
break
|
|
710
|
+
negated_bound, _, index, node, skip_state, weight, display = heappop(heap)
|
|
711
|
+
if best_key >= (1, -negated_bound):
|
|
712
|
+
continue
|
|
713
|
+
if index == len(location):
|
|
714
|
+
offer((1, weight), display, node)
|
|
715
|
+
break
|
|
716
|
+
moves = expand(index, node, skip_state, display)
|
|
717
|
+
if not moves:
|
|
718
|
+
offer((0, weight), [*display, *location[index:]], node)
|
|
719
|
+
continue
|
|
720
|
+
for move_weight, next_node, skip_state_after, display_after in moves:
|
|
721
|
+
counter += 1
|
|
722
|
+
new_weight = weight + move_weight
|
|
723
|
+
bound = new_weight + (len(location) - index - 1) * _NAV_DISCRIMINATOR_SELECT_WEIGHT
|
|
724
|
+
heappush(heap, (-bound, counter, index + 1, next_node, skip_state_after, new_weight, display_after))
|
|
725
|
+
return _format_argument_location(best_display), best_node
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _format_argument_location(location: Sequence[Any]) -> str:
|
|
729
|
+
parts: list[str] = []
|
|
730
|
+
for part in location:
|
|
731
|
+
if isinstance(part, str) and len(part) > _MAX_IDENTIFIER_CHARS + 1:
|
|
732
|
+
# The rendered path is bounded to ``_MAX_IDENTIFIER_CHARS`` below,
|
|
733
|
+
# but the identifier check, JSON quoting, and join are all O(len)
|
|
734
|
+
# — clamp a giant part (a caller-supplied mapping key, repeated in
|
|
735
|
+
# every error's loc) before any of them run.
|
|
736
|
+
part = part[: _MAX_IDENTIFIER_CHARS + 1]
|
|
737
|
+
if isinstance(part, int):
|
|
738
|
+
if parts:
|
|
739
|
+
parts[-1] += f"[{part}]"
|
|
740
|
+
else:
|
|
741
|
+
parts.append(f"[{part}]")
|
|
742
|
+
elif isinstance(part, str) and part.isidentifier():
|
|
743
|
+
parts.append(part)
|
|
744
|
+
else:
|
|
745
|
+
parts.append(json.dumps(part, ensure_ascii=False, separators=(",", ":"), default=str))
|
|
746
|
+
rendered = ".".join(parts) or "<arguments>"
|
|
747
|
+
return _bounded_text(rendered, _MAX_IDENTIFIER_CHARS)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _short_identifier(value: str) -> str:
|
|
751
|
+
# A str-subclass input (e.g. a subclassed tool name) must not dispatch
|
|
752
|
+
# ``split`` to user code mid-formatting.
|
|
753
|
+
value = value if type(value) is str else str.__str__(value)
|
|
754
|
+
return _bounded_text(" ".join(value.split()), _MAX_IDENTIFIER_CHARS)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _quoted_identifier(value: str) -> str:
|
|
758
|
+
shortened = _short_identifier(value)
|
|
759
|
+
if shortened.isidentifier():
|
|
760
|
+
return f"'{shortened}'"
|
|
761
|
+
return json.dumps(shortened, ensure_ascii=False, separators=(",", ":"))
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _safe_type_name(value: object) -> str:
|
|
765
|
+
"""Class name of ``value`` without running user code.
|
|
766
|
+
|
|
767
|
+
``type(value).__name__`` dispatches through a custom metaclass's
|
|
768
|
+
``__getattribute__``, which can raise mid-formatting, and a dynamically
|
|
769
|
+
created class can smuggle arbitrary payload text as its name. Read the
|
|
770
|
+
slot through ``type``'s own descriptor and trust only identifier-shaped
|
|
771
|
+
names.
|
|
772
|
+
"""
|
|
773
|
+
try:
|
|
774
|
+
name = type.__dict__["__name__"].__get__(type(value))
|
|
775
|
+
except Exception:
|
|
776
|
+
return "object"
|
|
777
|
+
if not isinstance(name, str):
|
|
778
|
+
return "object"
|
|
779
|
+
if type(name) is not str:
|
|
780
|
+
# A str-subclass name would re-enter user code via its methods.
|
|
781
|
+
name = str.__str__(name)
|
|
782
|
+
if name.isidentifier():
|
|
783
|
+
return _bounded_text(name, _MAX_IDENTIFIER_CHARS)
|
|
784
|
+
return "object"
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _json_value_kind(value: Any) -> str:
|
|
788
|
+
if value is None:
|
|
789
|
+
return "null"
|
|
790
|
+
if isinstance(value, bool):
|
|
791
|
+
return "boolean"
|
|
792
|
+
if isinstance(value, str):
|
|
793
|
+
return "string"
|
|
794
|
+
if isinstance(value, int):
|
|
795
|
+
return "integer"
|
|
796
|
+
if isinstance(value, float):
|
|
797
|
+
return "number"
|
|
798
|
+
if isinstance(value, list | tuple):
|
|
799
|
+
return "array"
|
|
800
|
+
if isinstance(value, Mapping):
|
|
801
|
+
return "object"
|
|
802
|
+
return _safe_type_name(value)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _bounded_error_detail(value: str) -> str:
|
|
806
|
+
value = value if type(value) is str else str.__str__(value)
|
|
807
|
+
normalized = " ".join(value.split()).removeprefix("Input should be ")
|
|
808
|
+
return _bounded_text(normalized, _MAX_ERROR_DETAIL_CHARS)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _collect_payload_strings(
|
|
812
|
+
value: Any,
|
|
813
|
+
out: set[str],
|
|
814
|
+
depth: int = 0,
|
|
815
|
+
_budget: list[int] | None = None,
|
|
816
|
+
_seen: set[int] | None = None,
|
|
817
|
+
) -> bool:
|
|
818
|
+
"""Collect string values from the argument payload for the echo guard.
|
|
819
|
+
|
|
820
|
+
Returns False whenever any part of the payload went unverified — budget
|
|
821
|
+
truncation, an unexpanded subtree past the depth cap, or any non-exact
|
|
822
|
+
value. Only EXACT JSON-adjacent types are traversed (including dict KEYS
|
|
823
|
+
and set members): a subclass or foreign object can smuggle strings where
|
|
824
|
+
the scan cannot safely look (attributes, hooks), and its iteration /
|
|
825
|
+
``len`` / ``hash`` dispatch to user code that can raise or block — so it
|
|
826
|
+
marks the scan incomplete instead of being touched at all; the scan
|
|
827
|
+
itself never runs user code. Work is charged by size (hashing large
|
|
828
|
+
strings), and a repeated reference to one string object is processed
|
|
829
|
+
once (identity memo), so shared references cannot amplify the cost.
|
|
830
|
+
"""
|
|
831
|
+
if _budget is None:
|
|
832
|
+
_budget = [_MAX_PAYLOAD_SCAN_ITEMS]
|
|
833
|
+
if _seen is None:
|
|
834
|
+
_seen = set()
|
|
835
|
+
if type(value) is str:
|
|
836
|
+
if id(value) in _seen:
|
|
837
|
+
return True
|
|
838
|
+
_seen.add(id(value))
|
|
839
|
+
cost = 1 + len(value) // 1024
|
|
840
|
+
if _budget[0] < cost:
|
|
841
|
+
return False
|
|
842
|
+
_budget[0] -= cost
|
|
843
|
+
if len(value) >= 3:
|
|
844
|
+
out.add(value)
|
|
845
|
+
return True
|
|
846
|
+
if _is_exact_type(value, int, float, bool) or value is None:
|
|
847
|
+
return True
|
|
848
|
+
if type(value) is dict:
|
|
849
|
+
items: Any = (element for pair in value.items() for element in pair)
|
|
850
|
+
elif _is_exact_type(value, list, tuple, set, frozenset):
|
|
851
|
+
items = value
|
|
852
|
+
else:
|
|
853
|
+
return False
|
|
854
|
+
if depth >= 6:
|
|
855
|
+
# An unexpanded subtree may hold payload strings.
|
|
856
|
+
return False
|
|
857
|
+
for item in items:
|
|
858
|
+
if _budget[0] <= 0:
|
|
859
|
+
return False
|
|
860
|
+
_budget[0] -= 1
|
|
861
|
+
if not _collect_payload_strings(item, out, depth + 1, _budget, _seen):
|
|
862
|
+
return False
|
|
863
|
+
return True
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _matches_json_type(value: Any, schema_type: str) -> bool:
|
|
867
|
+
match schema_type:
|
|
868
|
+
case "string":
|
|
869
|
+
return isinstance(value, str)
|
|
870
|
+
case "integer":
|
|
871
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
872
|
+
case "number":
|
|
873
|
+
return isinstance(value, int | float) and not isinstance(value, bool)
|
|
874
|
+
case "boolean":
|
|
875
|
+
return isinstance(value, bool)
|
|
876
|
+
case "array":
|
|
877
|
+
return isinstance(value, list)
|
|
878
|
+
case "object":
|
|
879
|
+
return isinstance(value, Mapping)
|
|
880
|
+
case "null":
|
|
881
|
+
return value is None
|
|
882
|
+
case _:
|
|
883
|
+
return True
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _all_exact_json_tree(value: Any, depth: int = 0, _budget: list[int] | None = None) -> bool:
|
|
887
|
+
"""Whether ``value`` is built solely from exact JSON types (bounded walk).
|
|
888
|
+
|
|
889
|
+
Equality over such a tree never leaves CPython's C slots; any subclass or
|
|
890
|
+
foreign object anywhere in it could dispatch a comparison to user code,
|
|
891
|
+
so it must not be equality-compared in the formatting path. The walk
|
|
892
|
+
spends at most ``_MAX_COMPARE_ITEMS`` node visits TOTAL — shared
|
|
893
|
+
references amplify a small graph into exponentially many visits, so a
|
|
894
|
+
per-level size cap is not enough.
|
|
895
|
+
"""
|
|
896
|
+
if _budget is None:
|
|
897
|
+
_budget = [_MAX_COMPARE_ITEMS]
|
|
898
|
+
if _is_exact_type(value, str, int, float, bool) or value is None:
|
|
899
|
+
return True
|
|
900
|
+
if depth >= _MAX_SCHEMA_DEPTH:
|
|
901
|
+
return False
|
|
902
|
+
if _is_exact_type(value, list, tuple):
|
|
903
|
+
items: Any = value
|
|
904
|
+
elif type(value) is dict:
|
|
905
|
+
items = value.values()
|
|
906
|
+
else:
|
|
907
|
+
return False
|
|
908
|
+
if len(value) > _budget[0]:
|
|
909
|
+
# Also bounds the key scan below on repeated visits of a shared node.
|
|
910
|
+
return False
|
|
911
|
+
if type(value) is dict and not all(type(key) is str for key in value):
|
|
912
|
+
return False
|
|
913
|
+
for item in items:
|
|
914
|
+
_budget[0] -= 1
|
|
915
|
+
if _budget[0] < 0:
|
|
916
|
+
return False
|
|
917
|
+
if not _all_exact_json_tree(item, depth + 1, _budget):
|
|
918
|
+
return False
|
|
919
|
+
return True
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def _comparison_safe_value(value: Any) -> tuple[bool, Any]:
|
|
923
|
+
"""``(comparable, safe_value)`` for equality against schema constants.
|
|
924
|
+
|
|
925
|
+
Exact JSON scalars compare via C slots; subclass scalars are copied
|
|
926
|
+
through their base slots (never running overridden methods); exact
|
|
927
|
+
containers qualify only when every element does. Anything else is not
|
|
928
|
+
comparable here — the caller accepts it (fail-safe: no issue beats a
|
|
929
|
+
crash or a mislabel).
|
|
930
|
+
"""
|
|
931
|
+
if _is_exact_type(value, str, int, float, bool) or value is None:
|
|
932
|
+
return True, value
|
|
933
|
+
if isinstance(value, str):
|
|
934
|
+
return True, str.__str__(value)
|
|
935
|
+
if isinstance(value, int):
|
|
936
|
+
# bool is final, so an int instance here is a plain-int subclass.
|
|
937
|
+
return True, int.__index__(value)
|
|
938
|
+
if isinstance(value, float):
|
|
939
|
+
return True, float.__float__(value)
|
|
940
|
+
if _all_exact_json_tree(value):
|
|
941
|
+
return True, value
|
|
942
|
+
return False, None
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _schema_accepts_value(
|
|
946
|
+
value: Any,
|
|
947
|
+
schema: Mapping[str, Any],
|
|
948
|
+
root_schema: Mapping[str, Any],
|
|
949
|
+
*,
|
|
950
|
+
depth: int = 0,
|
|
951
|
+
) -> bool:
|
|
952
|
+
if depth >= _MAX_SCHEMA_DEPTH:
|
|
953
|
+
return True
|
|
954
|
+
resolved = _resolve_local_schema_ref(schema, root_schema)
|
|
955
|
+
enum_values = resolved.get("enum")
|
|
956
|
+
has_const = "const" in resolved
|
|
957
|
+
if isinstance(enum_values, list) or has_const:
|
|
958
|
+
comparable, safe_value = _comparison_safe_value(value)
|
|
959
|
+
if comparable:
|
|
960
|
+
if isinstance(enum_values, list) and safe_value not in enum_values:
|
|
961
|
+
return False
|
|
962
|
+
if has_const and safe_value != resolved["const"]:
|
|
963
|
+
return False
|
|
964
|
+
for key in ("anyOf", "oneOf", "allOf"):
|
|
965
|
+
variants = resolved.get(key)
|
|
966
|
+
if not isinstance(variants, list):
|
|
967
|
+
continue
|
|
968
|
+
matches = [
|
|
969
|
+
_schema_accepts_value(value, item, root_schema, depth=depth + 1)
|
|
970
|
+
for item in variants
|
|
971
|
+
if isinstance(item, Mapping)
|
|
972
|
+
]
|
|
973
|
+
if key == "allOf":
|
|
974
|
+
return all(matches)
|
|
975
|
+
if key == "oneOf":
|
|
976
|
+
return sum(matches) == 1
|
|
977
|
+
return any(matches)
|
|
978
|
+
schema_type = resolved.get("type")
|
|
979
|
+
if isinstance(schema_type, str):
|
|
980
|
+
return _matches_json_type(value, schema_type)
|
|
981
|
+
if isinstance(schema_type, list):
|
|
982
|
+
return any(isinstance(item, str) and _matches_json_type(value, item) for item in schema_type)
|
|
983
|
+
return True
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def _schema_value_issues(
|
|
987
|
+
arguments: Mapping[str, Any],
|
|
988
|
+
schema: Mapping[str, Any],
|
|
989
|
+
) -> list[str]:
|
|
990
|
+
properties = schema.get("properties")
|
|
991
|
+
if not isinstance(properties, Mapping):
|
|
992
|
+
return []
|
|
993
|
+
issues: list[str] = []
|
|
994
|
+
# Only the first ``_MAX_ARGUMENT_ISSUES`` issues render with text (the
|
|
995
|
+
# rest fold into a trailing count), so scanning a bounded prefix of a
|
|
996
|
+
# degenerate huge mapping merely degrades that count, never the shown
|
|
997
|
+
# issues.
|
|
998
|
+
for name, value in islice(arguments.items(), _MAX_UNEXPECTED_NAMES):
|
|
999
|
+
field_schema = properties.get(name)
|
|
1000
|
+
if not isinstance(field_schema, Mapping) or _schema_accepts_value(value, field_schema, schema):
|
|
1001
|
+
continue
|
|
1002
|
+
issues.append(
|
|
1003
|
+
f"{_format_argument_location((name,))}: expected {_schema_type_text(field_schema, schema)}, "
|
|
1004
|
+
f"got {_json_value_kind(value)}"
|
|
1005
|
+
)
|
|
1006
|
+
return issues
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _rejects_unexpected_arguments(schema: Mapping[str, Any], *, typed_model_dump: bool) -> bool:
|
|
1010
|
+
"""Return whether this tool schema rejects unadvertised top-level keys."""
|
|
1011
|
+
additional = schema.get("additionalProperties")
|
|
1012
|
+
if additional is True or isinstance(additional, Mapping):
|
|
1013
|
+
return False
|
|
1014
|
+
if additional is False:
|
|
1015
|
+
return True
|
|
1016
|
+
# A composed root (e.g. ``RootModel[dict[str, int] | None]`` emitting
|
|
1017
|
+
# ``anyOf``) accepts keys its top-level ``properties`` can't enumerate.
|
|
1018
|
+
if any(key in schema for key in ("anyOf", "oneOf", "allOf", "$ref")):
|
|
1019
|
+
return False
|
|
1020
|
+
return typed_model_dump and isinstance(schema.get("properties"), Mapping)
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def _field_spellings(input_model: type[BaseModel] | None) -> dict[str, set[str]]:
|
|
1024
|
+
"""Map each typed-model field to every top-level spelling validation accepts.
|
|
1025
|
+
|
|
1026
|
+
Mirrors Pydantic's acceptance rules: with an alias, the field name only
|
|
1027
|
+
counts under ``validate_by_name``/``populate_by_name``, and aliases only
|
|
1028
|
+
count under ``validate_by_alias`` (default on). A spelling Pydantic would
|
|
1029
|
+
silently ignore (``extra="ignore"`` + default) must NOT be accepted here —
|
|
1030
|
+
the pre-check error is the only signal the model gets that its key was
|
|
1031
|
+
dropped.
|
|
1032
|
+
"""
|
|
1033
|
+
if input_model is None:
|
|
1034
|
+
return {}
|
|
1035
|
+
config = input_model.model_config
|
|
1036
|
+
# An explicit ``validate_by_name`` wins over the deprecated
|
|
1037
|
+
# ``populate_by_name`` (Pydantic ignores the latter when both are set).
|
|
1038
|
+
validate_by_name_setting = config.get("validate_by_name")
|
|
1039
|
+
if validate_by_name_setting is None:
|
|
1040
|
+
validate_by_name_setting = config.get("populate_by_name")
|
|
1041
|
+
validate_by_name = bool(validate_by_name_setting)
|
|
1042
|
+
validate_by_alias = config.get("validate_by_alias", True) is not False
|
|
1043
|
+
spellings: dict[str, set[str]] = {}
|
|
1044
|
+
for field_name, field_info in input_model.model_fields.items():
|
|
1045
|
+
alias_source = field_info.validation_alias if field_info.validation_alias is not None else field_info.alias
|
|
1046
|
+
names: set[str] = set()
|
|
1047
|
+
if validate_by_alias and alias_source is not None:
|
|
1048
|
+
if isinstance(alias_source, str):
|
|
1049
|
+
names.add(alias_source)
|
|
1050
|
+
elif isinstance(alias_source, AliasPath):
|
|
1051
|
+
if alias_source.path and isinstance(alias_source.path[0], str):
|
|
1052
|
+
names.add(alias_source.path[0])
|
|
1053
|
+
elif isinstance(alias_source, AliasChoices):
|
|
1054
|
+
for choice in alias_source.choices:
|
|
1055
|
+
if isinstance(choice, str):
|
|
1056
|
+
names.add(choice)
|
|
1057
|
+
elif isinstance(choice, AliasPath) and choice.path and isinstance(choice.path[0], str):
|
|
1058
|
+
names.add(choice.path[0])
|
|
1059
|
+
if alias_source is None or validate_by_name:
|
|
1060
|
+
names.add(field_name)
|
|
1061
|
+
spellings[field_name] = names or {field_name}
|
|
1062
|
+
return spellings
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def _accepted_argument_names(
|
|
1066
|
+
schema: Mapping[str, Any],
|
|
1067
|
+
input_model: type[BaseModel] | None,
|
|
1068
|
+
) -> set[str]:
|
|
1069
|
+
"""Top-level names the tool accepts.
|
|
1070
|
+
|
|
1071
|
+
For typed models the spellings map is authoritative — schema property
|
|
1072
|
+
names are rendered by-alias even under ``validate_by_alias=False``, where
|
|
1073
|
+
Pydantic silently ignores the alias key, so adding them back would
|
|
1074
|
+
reintroduce the silent-default trap.
|
|
1075
|
+
"""
|
|
1076
|
+
if input_model is None:
|
|
1077
|
+
properties = schema.get("properties")
|
|
1078
|
+
if not isinstance(properties, Mapping):
|
|
1079
|
+
return set()
|
|
1080
|
+
return set(cast("Mapping[str, Any]", properties))
|
|
1081
|
+
names: set[str] = set()
|
|
1082
|
+
for field_names in _field_spellings(input_model).values():
|
|
1083
|
+
names |= field_names
|
|
1084
|
+
return names
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
_CORE_SCHEMA_WRAPPER_TYPES = frozenset({"function-before", "function-wrap", "function-plain"})
|
|
1088
|
+
_CORE_SCHEMA_STATIC_TERMINAL_TYPES = frozenset({"model-fields", "typed-dict", "dataclass-args"})
|
|
1089
|
+
_CORE_SCHEMA_CHILD_KEYS: dict[str, tuple[str, ...]] = {
|
|
1090
|
+
"function-after": ("schema",),
|
|
1091
|
+
"default": ("schema",),
|
|
1092
|
+
"nullable": ("schema",),
|
|
1093
|
+
"custom-error": ("schema",),
|
|
1094
|
+
"json-or-python": ("json_schema", "python_schema"),
|
|
1095
|
+
"lax-or-strict": ("lax_schema", "strict_schema"),
|
|
1096
|
+
}
|
|
1097
|
+
_MAX_CORE_SCHEMA_DEPTH = 8
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
def _core_schema_may_rewrite_keys(
|
|
1101
|
+
node: object,
|
|
1102
|
+
definitions: Mapping[str, Mapping[str, Any]] | None = None,
|
|
1103
|
+
depth: int = 0,
|
|
1104
|
+
) -> bool:
|
|
1105
|
+
"""True unless the chain provably terminates in static field validation.
|
|
1106
|
+
|
|
1107
|
+
Only the model's top-level chain is walked — field schemas under the
|
|
1108
|
+
terminal fields node are never entered, so nested models and field-level
|
|
1109
|
+
validators can't trigger a stand-down. Anything unrecognized (including
|
|
1110
|
+
branch containers like ``json-or-python``) counts as rewriting unless
|
|
1111
|
+
every branch is proven static: standing down too eagerly merely skips the
|
|
1112
|
+
name pre-check while Pydantic stays authoritative, whereas missing a
|
|
1113
|
+
rewriting wrapper falsely rejects valid calls.
|
|
1114
|
+
"""
|
|
1115
|
+
if depth > _MAX_CORE_SCHEMA_DEPTH or not isinstance(node, Mapping):
|
|
1116
|
+
return True
|
|
1117
|
+
schema_node = cast("Mapping[str, Any]", node)
|
|
1118
|
+
node_type = schema_node.get("type")
|
|
1119
|
+
if node_type in _CORE_SCHEMA_STATIC_TERMINAL_TYPES:
|
|
1120
|
+
return False
|
|
1121
|
+
if node_type in _CORE_SCHEMA_WRAPPER_TYPES:
|
|
1122
|
+
return True
|
|
1123
|
+
if node_type == "model":
|
|
1124
|
+
# A custom ``__init__`` participates in validation and can rewrite
|
|
1125
|
+
# argument keys before field validation sees them.
|
|
1126
|
+
if schema_node.get("custom_init"):
|
|
1127
|
+
return True
|
|
1128
|
+
return _core_schema_may_rewrite_keys(schema_node.get("schema"), definitions, depth + 1)
|
|
1129
|
+
if node_type == "definitions":
|
|
1130
|
+
collected = dict(definitions or {})
|
|
1131
|
+
definition_nodes = schema_node.get("definitions", ())
|
|
1132
|
+
for definition in cast("Sequence[object]", definition_nodes):
|
|
1133
|
+
if isinstance(definition, Mapping):
|
|
1134
|
+
definition_map = cast("Mapping[str, Any]", definition)
|
|
1135
|
+
ref = definition_map.get("ref")
|
|
1136
|
+
if isinstance(ref, str):
|
|
1137
|
+
collected[ref] = definition_map
|
|
1138
|
+
return _core_schema_may_rewrite_keys(schema_node.get("schema"), collected, depth + 1)
|
|
1139
|
+
if node_type == "definition-ref":
|
|
1140
|
+
target = (definitions or {}).get(schema_node.get("schema_ref"))
|
|
1141
|
+
return _core_schema_may_rewrite_keys(target, definitions, depth + 1)
|
|
1142
|
+
child_keys = _CORE_SCHEMA_CHILD_KEYS.get(node_type)
|
|
1143
|
+
if child_keys is None:
|
|
1144
|
+
return True
|
|
1145
|
+
return any(_core_schema_may_rewrite_keys(schema_node.get(key), definitions, depth + 1) for key in child_keys)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def _accepts_arbitrary_argument_names(input_model: type[BaseModel] | None) -> bool:
|
|
1149
|
+
"""True when top-level validation may rewrite argument keys.
|
|
1150
|
+
|
|
1151
|
+
Such logic can accept and transform names that appear in neither the field
|
|
1152
|
+
spellings nor the schema, so name membership cannot be judged statically
|
|
1153
|
+
and the pre-check must stand down. A class-level ``model_validate``
|
|
1154
|
+
override is checked first — the loop validates through that method, and
|
|
1155
|
+
an override rewrites keys invisibly to every schema-level signal. Beyond
|
|
1156
|
+
that, detected from the core schema — the model's top-level chain must
|
|
1157
|
+
provably end in static field validation, which covers
|
|
1158
|
+
``model_validator(mode="before"/"wrap")``, the legacy
|
|
1159
|
+
``root_validator(pre=True)``, and ``__get_pydantic_core_schema__``
|
|
1160
|
+
overrides alike — with the decorator registries as a fallback for deferred
|
|
1161
|
+
core schemas. Field-level validators can't rename top-level keys and don't
|
|
1162
|
+
count.
|
|
1163
|
+
"""
|
|
1164
|
+
if input_model is None:
|
|
1165
|
+
return False
|
|
1166
|
+
base_validate = getattr(BaseModel.model_validate, "__func__", BaseModel.model_validate)
|
|
1167
|
+
model_validate = getattr(input_model.model_validate, "__func__", None)
|
|
1168
|
+
if model_validate is not base_validate:
|
|
1169
|
+
# The loop validates through ``model_validate``; a class-level
|
|
1170
|
+
# override can rewrite argument keys before Pydantic ever sees them,
|
|
1171
|
+
# invisible to both the core schema and the decorator registries.
|
|
1172
|
+
return True
|
|
1173
|
+
try:
|
|
1174
|
+
core_schema: Any = input_model.__pydantic_core_schema__
|
|
1175
|
+
except Exception:
|
|
1176
|
+
core_schema = None
|
|
1177
|
+
if core_schema is not None and _core_schema_may_rewrite_keys(core_schema):
|
|
1178
|
+
return True
|
|
1179
|
+
decorators = getattr(input_model, "__pydantic_decorators__", None)
|
|
1180
|
+
for attribute in ("model_validators", "root_validators"):
|
|
1181
|
+
validators = getattr(decorators, attribute, None) or {}
|
|
1182
|
+
if any(
|
|
1183
|
+
getattr(getattr(validator, "info", None), "mode", None) in ("before", "wrap")
|
|
1184
|
+
for validator in validators.values()
|
|
1185
|
+
):
|
|
1186
|
+
return True
|
|
1187
|
+
return False
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
@dataclass(frozen=True, slots=True)
|
|
1191
|
+
class _TypedModelNaming:
|
|
1192
|
+
"""Name bookkeeping for a typed model whose spellings diverge from the schema."""
|
|
1193
|
+
|
|
1194
|
+
spellings: dict[str, set[str]]
|
|
1195
|
+
"""Per-field accepted argument spellings."""
|
|
1196
|
+
|
|
1197
|
+
schema_names: dict[str, str]
|
|
1198
|
+
"""Each field's schema property name."""
|
|
1199
|
+
|
|
1200
|
+
alias_to_property: dict[str, str]
|
|
1201
|
+
"""Loc spelling → schema property, for location navigation and missing dedup."""
|
|
1202
|
+
|
|
1203
|
+
property_display: dict[str, str]
|
|
1204
|
+
"""Schema property → accepted spelling, when the schema advertises a name
|
|
1205
|
+
validation does not accept (e.g. ``validate_by_alias=False``)."""
|
|
1206
|
+
|
|
1207
|
+
location_display: dict[str, str]
|
|
1208
|
+
"""Loc spelling → accepted spelling, for loc parts Pydantic reports with a
|
|
1209
|
+
name validation does not accept (bare field names under
|
|
1210
|
+
``loc_by_alias=False``)."""
|
|
1211
|
+
|
|
1212
|
+
loc_uses_field_names: bool
|
|
1213
|
+
"""``loc_by_alias=False``: error locations use bare field names, making the
|
|
1214
|
+
loc → property map authoritative over same-named sibling properties."""
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def _typed_model_naming(
|
|
1218
|
+
input_model: type[BaseModel] | None,
|
|
1219
|
+
property_names: Sequence[str],
|
|
1220
|
+
) -> _TypedModelNaming:
|
|
1221
|
+
"""Field-name bookkeeping for typed models whose spellings diverge from the schema.
|
|
1222
|
+
|
|
1223
|
+
``AliasPath`` fields are left out of the display map — their flat schema
|
|
1224
|
+
name can't be replaced by the nested alias root without misstating the
|
|
1225
|
+
shape.
|
|
1226
|
+
"""
|
|
1227
|
+
spellings = _field_spellings(input_model)
|
|
1228
|
+
schema_names: dict[str, str] = {}
|
|
1229
|
+
alias_to_property: dict[str, str] = {}
|
|
1230
|
+
property_display: dict[str, str] = {}
|
|
1231
|
+
location_display: dict[str, str] = {}
|
|
1232
|
+
loc_uses_field_names = input_model is not None and input_model.model_config.get("loc_by_alias", True) is False
|
|
1233
|
+
naming = _TypedModelNaming(
|
|
1234
|
+
spellings, schema_names, alias_to_property, property_display, location_display, loc_uses_field_names
|
|
1235
|
+
)
|
|
1236
|
+
if input_model is None:
|
|
1237
|
+
return naming
|
|
1238
|
+
serialization_schema = input_model.model_config.get("json_schema_mode_override") == "serialization"
|
|
1239
|
+
for field_name, field_info in input_model.model_fields.items():
|
|
1240
|
+
alias_source = field_info.validation_alias if field_info.validation_alias is not None else field_info.alias
|
|
1241
|
+
has_alias_path = isinstance(alias_source, AliasPath)
|
|
1242
|
+
validation_candidates: list[str] = []
|
|
1243
|
+
if isinstance(alias_source, str):
|
|
1244
|
+
validation_candidates.append(alias_source)
|
|
1245
|
+
elif isinstance(alias_source, AliasChoices):
|
|
1246
|
+
for choice in alias_source.choices:
|
|
1247
|
+
if isinstance(choice, str):
|
|
1248
|
+
validation_candidates.append(choice)
|
|
1249
|
+
else:
|
|
1250
|
+
has_alias_path = has_alias_path or isinstance(choice, AliasPath)
|
|
1251
|
+
# Candidates must follow the schema's generation mode: a
|
|
1252
|
+
# serialization-mode schema (``json_schema_mode_override``) renders
|
|
1253
|
+
# serialization aliases and never validation aliases, and vice versa.
|
|
1254
|
+
# Trying both per-field would let one field's validation alias claim
|
|
1255
|
+
# another field's serialization-alias property. Serialization aliases
|
|
1256
|
+
# are schema-name candidates only — never accepted spellings.
|
|
1257
|
+
if serialization_schema:
|
|
1258
|
+
mode_candidates = [field_info.serialization_alias, field_info.alias]
|
|
1259
|
+
else:
|
|
1260
|
+
mode_candidates = [*validation_candidates, field_info.alias]
|
|
1261
|
+
candidates: list[str] = []
|
|
1262
|
+
for candidate in mode_candidates:
|
|
1263
|
+
if isinstance(candidate, str) and candidate not in candidates:
|
|
1264
|
+
candidates.append(candidate)
|
|
1265
|
+
candidates.append(field_name)
|
|
1266
|
+
schema_name = next((name for name in candidates if name in property_names), field_name)
|
|
1267
|
+
schema_names[field_name] = schema_name
|
|
1268
|
+
field_spellings = spellings.get(field_name, {field_name})
|
|
1269
|
+
if not loc_uses_field_names:
|
|
1270
|
+
for spelling in field_spellings:
|
|
1271
|
+
if spelling != schema_name:
|
|
1272
|
+
alias_to_property.setdefault(spelling, schema_name)
|
|
1273
|
+
if schema_name not in field_spellings and not has_alias_path:
|
|
1274
|
+
property_display[schema_name] = field_name if field_name in field_spellings else min(field_spellings)
|
|
1275
|
+
if loc_uses_field_names:
|
|
1276
|
+
# ``loc_by_alias=False``: every error location uses the bare field
|
|
1277
|
+
# name, so field-name entries are authoritative — alias spellings
|
|
1278
|
+
# never appear in locs and would only create collisions.
|
|
1279
|
+
for field_name in input_model.model_fields:
|
|
1280
|
+
schema_name = schema_names[field_name]
|
|
1281
|
+
if field_name != schema_name:
|
|
1282
|
+
alias_to_property[field_name] = schema_name
|
|
1283
|
+
display = property_display.get(schema_name, schema_name)
|
|
1284
|
+
if display != field_name:
|
|
1285
|
+
location_display[field_name] = display
|
|
1286
|
+
else:
|
|
1287
|
+
# ``loc_by_alias=True`` locs use accepted spellings, but bare field
|
|
1288
|
+
# names can still surface for fields matched by name; map those too
|
|
1289
|
+
# when unambiguous. Applied last with ``setdefault`` so accepted
|
|
1290
|
+
# spellings win any collision with another field's spelling entries.
|
|
1291
|
+
accepted_spellings = {spelling for names in spellings.values() for spelling in names}
|
|
1292
|
+
for field_name in input_model.model_fields:
|
|
1293
|
+
if field_name in accepted_spellings:
|
|
1294
|
+
continue
|
|
1295
|
+
schema_name = schema_names[field_name]
|
|
1296
|
+
alias_to_property.setdefault(field_name, schema_name)
|
|
1297
|
+
display = property_display.get(schema_name, schema_name)
|
|
1298
|
+
if display != field_name:
|
|
1299
|
+
location_display.setdefault(field_name, display)
|
|
1300
|
+
return naming
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
def _missing_required_names(
|
|
1304
|
+
required: Sequence[str],
|
|
1305
|
+
arguments: Mapping[str, Any],
|
|
1306
|
+
spellings: Mapping[str, set[str]],
|
|
1307
|
+
schema_names: Mapping[str, str],
|
|
1308
|
+
) -> list[str]:
|
|
1309
|
+
"""Required schema names with no accepted spelling present in the arguments.
|
|
1310
|
+
|
|
1311
|
+
A required name is matched to its field by schema name, field name, or
|
|
1312
|
+
spelling — the schema key falls back to the field name for non-string
|
|
1313
|
+
validation aliases (e.g. ``AliasPath``), whose accepted spelling is the
|
|
1314
|
+
alias root instead.
|
|
1315
|
+
"""
|
|
1316
|
+
missing: list[str] = []
|
|
1317
|
+
for name in required:
|
|
1318
|
+
if name in arguments:
|
|
1319
|
+
continue
|
|
1320
|
+
satisfied = any(
|
|
1321
|
+
(name == field_name or name == schema_names.get(field_name) or name in field_names)
|
|
1322
|
+
and any(spelling in arguments for spelling in field_names)
|
|
1323
|
+
for field_name, field_names in spellings.items()
|
|
1324
|
+
)
|
|
1325
|
+
if not satisfied:
|
|
1326
|
+
missing.append(name)
|
|
1327
|
+
return missing
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def _argument_key_display(name: object) -> str:
|
|
1331
|
+
"""Safe display text for a non-string mapping key.
|
|
1332
|
+
|
|
1333
|
+
Never calls an arbitrary ``__repr__``: a custom key type's repr is user
|
|
1334
|
+
code that may raise (escaping the loop's ``TypeError | ValidationError``
|
|
1335
|
+
handler and aborting the turn), run unboundedly, or embed value content
|
|
1336
|
+
(a tuple key) the no-echo policy would otherwise never show. Only exact
|
|
1337
|
+
JSON-adjacent scalar types render their (safe, bounded) repr; everything
|
|
1338
|
+
else degrades to a type placeholder.
|
|
1339
|
+
"""
|
|
1340
|
+
if type(name) is int and name.bit_length() > _MAX_INT_KEY_BITS:
|
|
1341
|
+
return "<int key>"
|
|
1342
|
+
if _is_exact_type(name, bool, int, float) or name is None:
|
|
1343
|
+
try:
|
|
1344
|
+
return repr(name)
|
|
1345
|
+
except ValueError:
|
|
1346
|
+
# Defensive backstop behind the magnitude bound above.
|
|
1347
|
+
pass
|
|
1348
|
+
return f"<{_safe_type_name(name)} key>"
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def _display_safe_arguments(arguments: Mapping[str, Any]) -> tuple[Mapping[str, Any], bool]:
|
|
1352
|
+
"""``(safe, complete)``: a copy of ``arguments`` whose keys are exact ``str``.
|
|
1353
|
+
|
|
1354
|
+
Mapping-form calls from custom clients are not runtime-checked, so keys
|
|
1355
|
+
can be str subclasses or arbitrary objects whose ``__hash__`` /
|
|
1356
|
+
``__eq__`` / ``__lt__`` / ``split`` are user code; left in place they
|
|
1357
|
+
would run inside membership probes, ``sorted``, and the close-match
|
|
1358
|
+
suggester and could raise past the loop's ``TypeError | ValidationError``
|
|
1359
|
+
handler. Non-string keys take their placeholder display form (they are
|
|
1360
|
+
always unexpected); colliding normalized keys keep the first value.
|
|
1361
|
+
Copying stops at ``_MAX_SAFE_ARGUMENT_ENTRIES`` so degenerate
|
|
1362
|
+
multi-million-key calls stay bounded; ``complete`` is False when any
|
|
1363
|
+
entry was dropped — past the cap OR because normalization collapsed two
|
|
1364
|
+
distinct caller keys — since strings in a dropped entry are unverified
|
|
1365
|
+
and the payload scan must not vouch for detail text it never saw.
|
|
1366
|
+
"""
|
|
1367
|
+
if (
|
|
1368
|
+
type(arguments) is dict
|
|
1369
|
+
and len(arguments) <= _MAX_SAFE_ARGUMENT_ENTRIES
|
|
1370
|
+
and all(type(name) is str for name in arguments)
|
|
1371
|
+
):
|
|
1372
|
+
return arguments, True
|
|
1373
|
+
safe: dict[str, Any] = {}
|
|
1374
|
+
complete = True
|
|
1375
|
+
for count, (name, value) in enumerate(arguments.items()):
|
|
1376
|
+
if count >= _MAX_SAFE_ARGUMENT_ENTRIES:
|
|
1377
|
+
return safe, False
|
|
1378
|
+
if type(name) is str:
|
|
1379
|
+
key = name
|
|
1380
|
+
elif isinstance(name, str):
|
|
1381
|
+
# The base slot copies without running subclass methods.
|
|
1382
|
+
key = str.__str__(name)
|
|
1383
|
+
else:
|
|
1384
|
+
key = _argument_key_display(name)
|
|
1385
|
+
if key in safe:
|
|
1386
|
+
complete = False
|
|
1387
|
+
else:
|
|
1388
|
+
safe[key] = value
|
|
1389
|
+
return safe, complete
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
def _unexpected_argument_names(
|
|
1393
|
+
arguments: Mapping[str, Any],
|
|
1394
|
+
schema: Mapping[str, Any],
|
|
1395
|
+
*,
|
|
1396
|
+
reject_unexpected: bool,
|
|
1397
|
+
input_model: type[BaseModel] | None = None,
|
|
1398
|
+
) -> list[str]:
|
|
1399
|
+
"""List top-level argument keys that the tool does not accept.
|
|
1400
|
+
|
|
1401
|
+
Always returns exact strings: JSON-decoded keys are strings, but
|
|
1402
|
+
mapping-form calls from custom clients are not runtime-checked, and a
|
|
1403
|
+
hostile key must surface as an argument error rather than crash
|
|
1404
|
+
``sorted`` or the close-match suggester downstream.
|
|
1405
|
+
"""
|
|
1406
|
+
if not reject_unexpected:
|
|
1407
|
+
return []
|
|
1408
|
+
accepted = _accepted_argument_names(schema, input_model)
|
|
1409
|
+
names: list[str] = []
|
|
1410
|
+
for visited, name in enumerate(arguments):
|
|
1411
|
+
if len(names) >= _MAX_UNEXPECTED_NAMES:
|
|
1412
|
+
# Stop collecting, not just displaying: exact-str dict keys are
|
|
1413
|
+
# unique, so at most ``len(accepted)`` of them skip this list and
|
|
1414
|
+
# the walk stays bounded no matter how many keys the caller sends.
|
|
1415
|
+
break
|
|
1416
|
+
if visited >= _MAX_SAFE_ARGUMENT_ENTRIES:
|
|
1417
|
+
# Only a flood of distinct str-SUBCLASS keys whose base text all
|
|
1418
|
+
# normalizes to an accepted name can reach this many visits
|
|
1419
|
+
# without filling ``names`` (a legitimate exact-str mapping breaks
|
|
1420
|
+
# above within cap + len(accepted) visits). Stand down: the
|
|
1421
|
+
# pre-check is an enhancement over Pydantic's own validation,
|
|
1422
|
+
# which still runs on the full mapping, so truncating the scan
|
|
1423
|
+
# never accepts anything Pydantic itself would reject.
|
|
1424
|
+
break
|
|
1425
|
+
if type(name) is str:
|
|
1426
|
+
if name not in accepted:
|
|
1427
|
+
names.append(name)
|
|
1428
|
+
elif isinstance(name, str):
|
|
1429
|
+
text = str.__str__(name)
|
|
1430
|
+
if text not in accepted:
|
|
1431
|
+
names.append(text)
|
|
1432
|
+
else:
|
|
1433
|
+
names.append(_argument_key_display(name))
|
|
1434
|
+
return sorted(names)
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
def _capped_validation_errors(exception: ValidationError) -> list[ErrorDetails]:
|
|
1438
|
+
"""At most ``_MAX_VALIDATION_ERRORS`` errors, breadth-first across top-level fields.
|
|
1439
|
+
|
|
1440
|
+
A single wide union can fan one bad value out into thousands of
|
|
1441
|
+
per-variant errors that Pydantic lists contiguously, so a plain prefix
|
|
1442
|
+
slice would spend every slot on duplicates of one field and drop another
|
|
1443
|
+
field's only error. Round-robin over the errors grouped by their loc's
|
|
1444
|
+
first part keeps at least one error per argument (up to the cap) while
|
|
1445
|
+
preserving Pydantic's order for fields and within each field.
|
|
1446
|
+
"""
|
|
1447
|
+
errors = exception.errors(include_url=False, include_context=False)
|
|
1448
|
+
for error in errors:
|
|
1449
|
+
location = error.get("loc")
|
|
1450
|
+
# Clamp giant string parts BEFORE anything hashes them: pydantic
|
|
1451
|
+
# materializes a DISTINCT copy of a mapping key per error, so
|
|
1452
|
+
# per-object hash caching never amortizes — every schema-map lookup
|
|
1453
|
+
# in the resolver would pay O(len) per error. Truncation only
|
|
1454
|
+
# degrades resolution for parts no genuine marker or sane property
|
|
1455
|
+
# name can reach (the marker scans and display already gate there).
|
|
1456
|
+
# Clamp to one char PAST the marker gate: a clamped part must stay in
|
|
1457
|
+
# the oversized class, or truncation would launder an oversized
|
|
1458
|
+
# marker into one eligible for corroboration (pinned opaque).
|
|
1459
|
+
if isinstance(location, tuple) and any(
|
|
1460
|
+
isinstance(part, str) and len(part) > _MAX_MARKER_SCAN_CHARS + 1 for part in location
|
|
1461
|
+
):
|
|
1462
|
+
error["loc"] = tuple(
|
|
1463
|
+
part[: _MAX_MARKER_SCAN_CHARS + 1]
|
|
1464
|
+
if isinstance(part, str) and len(part) > _MAX_MARKER_SCAN_CHARS + 1
|
|
1465
|
+
else part
|
|
1466
|
+
for part in location
|
|
1467
|
+
)
|
|
1468
|
+
if len(errors) <= _MAX_VALIDATION_ERRORS:
|
|
1469
|
+
return errors
|
|
1470
|
+
groups: dict[object, list[ErrorDetails]] = {}
|
|
1471
|
+
for error in errors:
|
|
1472
|
+
location = error.get("loc")
|
|
1473
|
+
head = location[0] if isinstance(location, tuple) and location else None
|
|
1474
|
+
groups.setdefault(head, []).append(error)
|
|
1475
|
+
capped: list[ErrorDetails] = []
|
|
1476
|
+
depth = 0
|
|
1477
|
+
while len(capped) < _MAX_VALIDATION_ERRORS:
|
|
1478
|
+
progressed = False
|
|
1479
|
+
for bucket in groups.values():
|
|
1480
|
+
if depth >= len(bucket):
|
|
1481
|
+
continue
|
|
1482
|
+
capped.append(bucket[depth])
|
|
1483
|
+
progressed = True
|
|
1484
|
+
if len(capped) == _MAX_VALIDATION_ERRORS:
|
|
1485
|
+
break
|
|
1486
|
+
if not progressed:
|
|
1487
|
+
break
|
|
1488
|
+
depth += 1
|
|
1489
|
+
return capped
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
def _argument_validation_message(
|
|
1493
|
+
*,
|
|
1494
|
+
tool_name: str,
|
|
1495
|
+
arguments: Mapping[str, Any],
|
|
1496
|
+
arguments_unparseable: bool,
|
|
1497
|
+
schema: Mapping[str, Any],
|
|
1498
|
+
exception: TypeError | ValidationError,
|
|
1499
|
+
reject_unexpected: bool,
|
|
1500
|
+
input_model: type[BaseModel] | None = None,
|
|
1501
|
+
) -> str:
|
|
1502
|
+
"""Build a concise validation error that lets the model repair its next call."""
|
|
1503
|
+
arguments, arguments_complete = _display_safe_arguments(arguments)
|
|
1504
|
+
issues: list[str] = []
|
|
1505
|
+
properties = schema.get("properties")
|
|
1506
|
+
property_names = [name for name in properties if isinstance(name, str)] if isinstance(properties, Mapping) else []
|
|
1507
|
+
naming = _typed_model_naming(input_model, property_names)
|
|
1508
|
+
|
|
1509
|
+
if arguments_unparseable:
|
|
1510
|
+
issues.append("arguments must be a valid JSON object")
|
|
1511
|
+
else:
|
|
1512
|
+
unexpected = _unexpected_argument_names(
|
|
1513
|
+
arguments,
|
|
1514
|
+
schema,
|
|
1515
|
+
reject_unexpected=reject_unexpected,
|
|
1516
|
+
input_model=input_model,
|
|
1517
|
+
)
|
|
1518
|
+
suggestion_names = sorted(_accepted_argument_names(schema, input_model)) if input_model else property_names
|
|
1519
|
+
for index, name in enumerate(unexpected):
|
|
1520
|
+
# Close matches are only ever displayed for the first
|
|
1521
|
+
# ``_MAX_ARGUMENT_ISSUES`` issues; later entries just add to the
|
|
1522
|
+
# trailing count, so skipping their suggestions changes nothing
|
|
1523
|
+
# visible while bounding difflib work on vast unknown sets.
|
|
1524
|
+
suggestions = (
|
|
1525
|
+
get_close_matches(name, suggestion_names, n=1, cutoff=0.6) if index < _MAX_ARGUMENT_ISSUES else []
|
|
1526
|
+
)
|
|
1527
|
+
suggestion = f" (did you mean {_quoted_identifier(suggestions[0])}?)" if suggestions else ""
|
|
1528
|
+
issues.append(f"unknown {_quoted_identifier(name)}{suggestion}")
|
|
1529
|
+
|
|
1530
|
+
required = [name for name in schema.get("required", []) if isinstance(name, str)]
|
|
1531
|
+
# A before/wrap validator may populate required fields from rewritten
|
|
1532
|
+
# keys, so static missing detection would report fields the validator
|
|
1533
|
+
# filled; Pydantic's own ``missing`` errors are authoritative there.
|
|
1534
|
+
missing_top_level = (
|
|
1535
|
+
[]
|
|
1536
|
+
if _accepts_arbitrary_argument_names(input_model)
|
|
1537
|
+
else _missing_required_names(required, arguments, naming.spellings, naming.schema_names)
|
|
1538
|
+
)
|
|
1539
|
+
missing_top_level_set = set(missing_top_level)
|
|
1540
|
+
issues.extend(
|
|
1541
|
+
f"missing {_quoted_identifier(naming.property_display.get(name, name))}" for name in missing_top_level
|
|
1542
|
+
)
|
|
1543
|
+
|
|
1544
|
+
if isinstance(exception, ValidationError) and exception.error_count() <= _MAX_MATERIALIZED_ERRORS:
|
|
1545
|
+
payload_strings: set[str] = set()
|
|
1546
|
+
try:
|
|
1547
|
+
# An entry-capped display copy never saw the dropped entries'
|
|
1548
|
+
# strings, so its scan can only ever be incomplete.
|
|
1549
|
+
payload_scan_complete = arguments_complete and _collect_payload_strings(arguments, payload_strings)
|
|
1550
|
+
except Exception:
|
|
1551
|
+
# A hostile container's iteration hooks can raise mid-scan; an
|
|
1552
|
+
# unverified payload set must suppress detail text rather than
|
|
1553
|
+
# risk echoing it.
|
|
1554
|
+
payload_scan_complete = False
|
|
1555
|
+
for error in _capped_validation_errors(exception):
|
|
1556
|
+
error_type = error.get("type")
|
|
1557
|
+
location = error.get("loc")
|
|
1558
|
+
if not isinstance(location, tuple):
|
|
1559
|
+
continue
|
|
1560
|
+
location_text, expected_schema = _resolve_error_location(
|
|
1561
|
+
schema,
|
|
1562
|
+
location,
|
|
1563
|
+
naming.alias_to_property,
|
|
1564
|
+
naming.location_display,
|
|
1565
|
+
naming.loc_uses_field_names,
|
|
1566
|
+
)
|
|
1567
|
+
if error_type == "missing":
|
|
1568
|
+
# Compare through the spelling → schema-name map so a
|
|
1569
|
+
# Pydantic loc using the accepted spelling dedups against
|
|
1570
|
+
# the schema-derived missing entry.
|
|
1571
|
+
top_level = location[0] if len(location) == 1 else None
|
|
1572
|
+
if isinstance(top_level, str):
|
|
1573
|
+
top_level = naming.alias_to_property.get(top_level, top_level)
|
|
1574
|
+
if top_level not in missing_top_level_set:
|
|
1575
|
+
issues.append(f"missing '{location_text}'")
|
|
1576
|
+
continue
|
|
1577
|
+
if error_type == "extra_forbidden":
|
|
1578
|
+
issues.append(f"unknown '{location_text}'")
|
|
1579
|
+
continue
|
|
1580
|
+
expected = (
|
|
1581
|
+
_schema_type_text(expected_schema, schema)
|
|
1582
|
+
if isinstance(expected_schema, Mapping)
|
|
1583
|
+
else "valid value"
|
|
1584
|
+
)
|
|
1585
|
+
if error_type == "literal_error":
|
|
1586
|
+
issues.append(f"{location_text}: expected {expected}")
|
|
1587
|
+
elif isinstance(error_type, str) and error_type.endswith("_type"):
|
|
1588
|
+
issues.append(f"{location_text}: expected {expected}, got {_json_value_kind(error.get('input'))}")
|
|
1589
|
+
else:
|
|
1590
|
+
detail = (
|
|
1591
|
+
error.get("msg") if payload_scan_complete and error_type in _DETAIL_SAFE_ERROR_TYPES else None
|
|
1592
|
+
)
|
|
1593
|
+
if isinstance(detail, str) and any(value in detail for value in payload_strings):
|
|
1594
|
+
# Best-effort guard: a spoofed allowlisted code must not
|
|
1595
|
+
# echo payload text verbatim.
|
|
1596
|
+
detail = None
|
|
1597
|
+
suffix = f" ({_bounded_error_detail(detail)})" if isinstance(detail, str) and detail else ""
|
|
1598
|
+
issues.append(f"{location_text}: expected {expected}{suffix}")
|
|
1599
|
+
else:
|
|
1600
|
+
issues.extend(_schema_value_issues(arguments, schema))
|
|
1601
|
+
|
|
1602
|
+
# Union errors repeat per variant; after branch-marker filtering those
|
|
1603
|
+
# collapse to identical issue lines.
|
|
1604
|
+
issues = list(dict.fromkeys(issues))
|
|
1605
|
+
if not issues:
|
|
1606
|
+
issues.append("arguments did not match the tool schema")
|
|
1607
|
+
|
|
1608
|
+
issue_text = "; ".join(issues[:_MAX_ARGUMENT_ISSUES])
|
|
1609
|
+
if len(issues) > _MAX_ARGUMENT_ISSUES:
|
|
1610
|
+
issue_text += f"; … and {len(issues) - _MAX_ARGUMENT_ISSUES} more issue(s)"
|
|
1611
|
+
|
|
1612
|
+
display_schema: Mapping[str, Any] = schema
|
|
1613
|
+
if naming.property_display and isinstance(properties, Mapping):
|
|
1614
|
+
# Advertise the spellings validation actually accepts, not schema
|
|
1615
|
+
# names the pre-check would reject right back.
|
|
1616
|
+
display_schema = {
|
|
1617
|
+
**schema,
|
|
1618
|
+
"properties": {naming.property_display.get(name, name): value for name, value in properties.items()},
|
|
1619
|
+
}
|
|
1620
|
+
required_names = schema.get("required")
|
|
1621
|
+
if isinstance(required_names, list):
|
|
1622
|
+
display_schema["required"] = [
|
|
1623
|
+
naming.property_display.get(name, name) if isinstance(name, str) else name for name in required_names
|
|
1624
|
+
]
|
|
1625
|
+
message = (
|
|
1626
|
+
f"Invalid arguments for {_quoted_identifier(tool_name)}: {issue_text}. "
|
|
1627
|
+
f"Expected: {_schema_type_text(display_schema, schema)}."
|
|
1628
|
+
)
|
|
1629
|
+
return _bounded_text(_sanitize_message_text(message), _MAX_MESSAGE_CHARS)
|