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,1311 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Hardened, owned ACP client connection for one external sub-agent attempt."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import contextlib
|
|
9
|
+
import logging
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from typing import Any, Protocol, cast
|
|
12
|
+
|
|
13
|
+
from acp import PROTOCOL_VERSION, Client
|
|
14
|
+
from acp.client.connection import ClientSideConnection
|
|
15
|
+
from acp.exceptions import RequestError
|
|
16
|
+
from acp.schema import (
|
|
17
|
+
AgentCapabilities,
|
|
18
|
+
AllowedOutcome,
|
|
19
|
+
ClientCapabilities,
|
|
20
|
+
CreateTerminalResponse,
|
|
21
|
+
DeniedOutcome,
|
|
22
|
+
EnvVariable,
|
|
23
|
+
FileSystemCapabilities,
|
|
24
|
+
KillTerminalResponse,
|
|
25
|
+
NewSessionResponse,
|
|
26
|
+
PermissionOption,
|
|
27
|
+
PromptResponse,
|
|
28
|
+
ReadTextFileResponse,
|
|
29
|
+
ReleaseTerminalResponse,
|
|
30
|
+
RequestPermissionResponse,
|
|
31
|
+
SessionConfigOptionBoolean,
|
|
32
|
+
SessionConfigOptionSelect,
|
|
33
|
+
SessionNotification,
|
|
34
|
+
TerminalOutputResponse,
|
|
35
|
+
TextContentBlock,
|
|
36
|
+
ToolCallUpdate,
|
|
37
|
+
WaitForTerminalExitResponse,
|
|
38
|
+
WriteTextFileResponse,
|
|
39
|
+
)
|
|
40
|
+
from acp.task import (
|
|
41
|
+
InMemoryMessageQueue,
|
|
42
|
+
MessageQueue,
|
|
43
|
+
MessageStateStore,
|
|
44
|
+
NotificationRunner,
|
|
45
|
+
RequestRunner,
|
|
46
|
+
TaskSupervisor,
|
|
47
|
+
)
|
|
48
|
+
from pydantic import ValidationError
|
|
49
|
+
|
|
50
|
+
from chrys.foundation.models.ask_user import validate_request_input_params
|
|
51
|
+
|
|
52
|
+
from . import _transport, protocol
|
|
53
|
+
from ._transport import (
|
|
54
|
+
_CURRENT_INBOUND_ID,
|
|
55
|
+
_AcpDisconnectError,
|
|
56
|
+
_AcpProtocolLimitError,
|
|
57
|
+
_BackpressureDispatcher,
|
|
58
|
+
_consume_future_exception,
|
|
59
|
+
_FrameObserver,
|
|
60
|
+
_FramingPump,
|
|
61
|
+
_ObserverBuffer,
|
|
62
|
+
_PauseTransport,
|
|
63
|
+
_PromptBarrier,
|
|
64
|
+
_reap_task,
|
|
65
|
+
_RetainedSender,
|
|
66
|
+
_TrackingStateStore,
|
|
67
|
+
_valid_usage_int,
|
|
68
|
+
)
|
|
69
|
+
from .errors import (
|
|
70
|
+
AcpAuthRequiredError,
|
|
71
|
+
AcpClientError,
|
|
72
|
+
AcpConfigError,
|
|
73
|
+
AcpConnectError,
|
|
74
|
+
AcpIdleTimeoutError,
|
|
75
|
+
AcpOperation,
|
|
76
|
+
AcpSpawnError,
|
|
77
|
+
AcpTransportError,
|
|
78
|
+
classify_acp_error,
|
|
79
|
+
is_best_effort_option_rejection,
|
|
80
|
+
)
|
|
81
|
+
from .protocol import _validate_payload_caps
|
|
82
|
+
from .protocol import encode_protocol_json as encode_protocol_json
|
|
83
|
+
from .protocol import parse_protocol_json as parse_protocol_json
|
|
84
|
+
from .protocol import validate_json_rpc_envelope as validate_json_rpc_envelope
|
|
85
|
+
from .protocol import validate_json_scalar_tree as validate_json_scalar_tree
|
|
86
|
+
from .spawn import ACP_STDIO_LIMIT_BYTES, AcpSpawnResult, spawn_acp_process, validate_agent_spec
|
|
87
|
+
from .spec import AcpAgentSpec, AcpHandshakeInfo, AcpPromptOutcome, PermissionDecision
|
|
88
|
+
|
|
89
|
+
logger = logging.getLogger(__name__)
|
|
90
|
+
|
|
91
|
+
_MAX_PERMISSION_OPTIONS = 128
|
|
92
|
+
_CLOSE_RPC_TIMEOUT_SECONDS = 2.0
|
|
93
|
+
_FINAL_DRAIN_TIMEOUT_SECONDS = 30.0
|
|
94
|
+
|
|
95
|
+
_INITIALIZE_METHOD = "initialize"
|
|
96
|
+
_NEW_SESSION_METHOD = "session/new"
|
|
97
|
+
# The selected outcome carries only an option id, so the id's kind is the
|
|
98
|
+
# sole proof that the wire outcome matches the callback's decision.
|
|
99
|
+
_DECISION_OPTION_KINDS: dict[str, frozenset[str]] = {
|
|
100
|
+
"allow": frozenset({"allow_once", "allow_always"}),
|
|
101
|
+
"deny": frozenset({"reject_once", "reject_always"}),
|
|
102
|
+
}
|
|
103
|
+
_META_COLLISION_KEYS = frozenset(
|
|
104
|
+
{
|
|
105
|
+
"options",
|
|
106
|
+
"sessionId",
|
|
107
|
+
"session_id",
|
|
108
|
+
"toolCall",
|
|
109
|
+
"tool_call",
|
|
110
|
+
"kwargs",
|
|
111
|
+
"method",
|
|
112
|
+
"params",
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class AcpClientCallbacks(Protocol):
|
|
118
|
+
"""Orchestration-owned callbacks; the service layer publishes no events."""
|
|
119
|
+
|
|
120
|
+
async def on_update(self, seq: int, notification: SessionNotification) -> None: ...
|
|
121
|
+
|
|
122
|
+
async def on_permission_request(
|
|
123
|
+
self,
|
|
124
|
+
tool_call: ToolCallUpdate,
|
|
125
|
+
options: Sequence[PermissionOption],
|
|
126
|
+
) -> PermissionDecision: ...
|
|
127
|
+
|
|
128
|
+
async def on_ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]: ...
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class AcpUpdateSink(Protocol):
|
|
132
|
+
"""Small observer seam consumed by the PR-2 translator."""
|
|
133
|
+
|
|
134
|
+
async def put(self, seq: int, notification: SessionNotification) -> None: ...
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class AcpClientWaitController(Protocol):
|
|
138
|
+
"""PR-2 broker seam for resolving human waits during transport teardown."""
|
|
139
|
+
|
|
140
|
+
async def cancel_pending_waits(self) -> None: ...
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class _CallbackUpdateSink:
|
|
144
|
+
def __init__(self, callbacks: AcpClientCallbacks) -> None:
|
|
145
|
+
self._callbacks = callbacks
|
|
146
|
+
|
|
147
|
+
async def put(self, seq: int, notification: SessionNotification) -> None:
|
|
148
|
+
await self._callbacks.on_update(seq, notification)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class AcpAgentClient:
|
|
152
|
+
"""One-process ACP client with strict protocol and lifecycle ownership."""
|
|
153
|
+
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
spec: AcpAgentSpec,
|
|
157
|
+
callbacks: AcpClientCallbacks,
|
|
158
|
+
*,
|
|
159
|
+
update_sink: AcpUpdateSink | None = None,
|
|
160
|
+
wait_controller: AcpClientWaitController | None = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
self._spec = spec
|
|
163
|
+
self._callbacks = callbacks
|
|
164
|
+
self._update_sink = update_sink or _CallbackUpdateSink(callbacks)
|
|
165
|
+
self._wait_controller = wait_controller
|
|
166
|
+
self._spawn: AcpSpawnResult | None = None
|
|
167
|
+
self._conn: ClientSideConnection | None = None
|
|
168
|
+
self._store: _TrackingStateStore | None = None
|
|
169
|
+
self._sender: _RetainedSender | None = None
|
|
170
|
+
self._dispatcher: _BackpressureDispatcher | None = None
|
|
171
|
+
self._supervisor: TaskSupervisor | None = None
|
|
172
|
+
self._observer: _FrameObserver | None = None
|
|
173
|
+
self._updates: _ObserverBuffer | None = None
|
|
174
|
+
self._consumer_task: asyncio.Task[None] | None = None
|
|
175
|
+
self._pump: _FramingPump | None = None
|
|
176
|
+
self._terminal_failure: asyncio.Future[BaseException] | None = None
|
|
177
|
+
self._completion_future: asyncio.Future[None] | None = None
|
|
178
|
+
self._handshake_deadline: float | None = None
|
|
179
|
+
self._stateful = False
|
|
180
|
+
self._closing = False
|
|
181
|
+
self._session_id: str | None = None
|
|
182
|
+
self._announced_session_id: str | None = None
|
|
183
|
+
self._session_response_claimed = False
|
|
184
|
+
self._agent_info: Any | None = None
|
|
185
|
+
self._auth_methods: tuple[Any, ...] = ()
|
|
186
|
+
self._capabilities = AgentCapabilities()
|
|
187
|
+
self._connect_started = False
|
|
188
|
+
self._prompt_started = False
|
|
189
|
+
self._cancel_requested = False
|
|
190
|
+
self._cancel_task: asyncio.Task[None] | None = None
|
|
191
|
+
self._force_close_task: asyncio.Task[None] | None = None
|
|
192
|
+
self._failure_close_task: asyncio.Task[None] | None = None
|
|
193
|
+
self._activity_event: asyncio.Event | None = None
|
|
194
|
+
self._last_activity = 0.0
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def stderr_tail(self) -> str:
|
|
198
|
+
return self._spawn.stderr_tail if self._spawn is not None else ""
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def stateful_phase_started(self) -> bool:
|
|
202
|
+
return self._stateful
|
|
203
|
+
|
|
204
|
+
async def connect(self) -> None:
|
|
205
|
+
"""Spawn and initialize; roll back every acquired resource on failure."""
|
|
206
|
+
if self._conn is not None:
|
|
207
|
+
raise RuntimeError("ACP client is already connected.")
|
|
208
|
+
if self._closing:
|
|
209
|
+
raise RuntimeError("ACP client is already closed.")
|
|
210
|
+
# Claimed synchronously before the first await: _conn is only
|
|
211
|
+
# installed after spawning, so two concurrent calls would otherwise
|
|
212
|
+
# both spawn and the second installation would strand the first
|
|
213
|
+
# child beyond force_close's reach.
|
|
214
|
+
if self._connect_started:
|
|
215
|
+
raise RuntimeError("An ACP connection attempt has already been started.")
|
|
216
|
+
self._connect_started = True
|
|
217
|
+
validate_agent_spec(self._spec)
|
|
218
|
+
loop = asyncio.get_running_loop()
|
|
219
|
+
self._handshake_deadline = loop.time() + self._spec.handshake_timeout_seconds
|
|
220
|
+
self._terminal_failure = loop.create_future()
|
|
221
|
+
self._activity_event = asyncio.Event()
|
|
222
|
+
self._last_activity = loop.time()
|
|
223
|
+
try:
|
|
224
|
+
async with asyncio.timeout_at(self._handshake_deadline):
|
|
225
|
+
spawn = await spawn_acp_process(self._spec)
|
|
226
|
+
if self._closing:
|
|
227
|
+
# A force_close that completed during the spawn await saw
|
|
228
|
+
# nothing to release, and its cached task never runs
|
|
229
|
+
# again; this late acquisition must be disposed here.
|
|
230
|
+
await _dispose_spawn(spawn)
|
|
231
|
+
raise AcpConnectError("The ACP client was closed.")
|
|
232
|
+
# No suspension between the check above and installation, so a
|
|
233
|
+
# force_close starting any later sees the full component set.
|
|
234
|
+
self._spawn = spawn
|
|
235
|
+
self._install_connection()
|
|
236
|
+
initialize = await self._require_conn().initialize(
|
|
237
|
+
protocol_version=PROTOCOL_VERSION,
|
|
238
|
+
client_capabilities=ClientCapabilities(
|
|
239
|
+
fs=FileSystemCapabilities(read_text_file=False, write_text_file=False),
|
|
240
|
+
terminal=False,
|
|
241
|
+
),
|
|
242
|
+
client_info=self._spec.client_info,
|
|
243
|
+
)
|
|
244
|
+
raw_result = self._require_store().latest_raw_result(_INITIALIZE_METHOD)
|
|
245
|
+
raw_version = raw_result.get("protocolVersion") if raw_result is not None else None
|
|
246
|
+
if type(raw_version) is not int or raw_version != PROTOCOL_VERSION:
|
|
247
|
+
raise AcpConfigError("The ACP agent returned an unsupported protocol version.")
|
|
248
|
+
self._agent_info = initialize.agent_info
|
|
249
|
+
self._auth_methods = tuple(initialize.auth_methods or ())
|
|
250
|
+
self._capabilities = initialize.agent_capabilities or AgentCapabilities()
|
|
251
|
+
terminal = self._require_terminal_failure()
|
|
252
|
+
if terminal.done():
|
|
253
|
+
raise terminal.result()
|
|
254
|
+
except asyncio.CancelledError:
|
|
255
|
+
await self.force_close()
|
|
256
|
+
raise
|
|
257
|
+
except BaseException as exc:
|
|
258
|
+
classified, failure = self._classified_connect_failure(exc)
|
|
259
|
+
await self.force_close()
|
|
260
|
+
raise classified from failure
|
|
261
|
+
|
|
262
|
+
def _classified_connect_failure(self, exc: BaseException) -> tuple[AcpClientError, BaseException]:
|
|
263
|
+
"""Pick the classified failure and root cause for a failed connect.
|
|
264
|
+
|
|
265
|
+
The terminal future usually holds the most specific failure (the pump
|
|
266
|
+
sees protocol violations the SDK surfaces only as generic closes), but
|
|
267
|
+
a deterministic local classification must not be displaced by it — an
|
|
268
|
+
agent closing stdout right after a bad initialize response would
|
|
269
|
+
otherwise flip from config-rejected to retryable.
|
|
270
|
+
"""
|
|
271
|
+
local = classify_acp_error(exc, operation=AcpOperation.INITIALIZE, stateful=False)
|
|
272
|
+
# AcpSpawnError is in the preserved set for the close-vs-spawn race: a
|
|
273
|
+
# force_close landing while process creation is pending settles the
|
|
274
|
+
# terminal with the retryable close classification, which must not
|
|
275
|
+
# displace a deterministic launch failure such as ENOENT.
|
|
276
|
+
if isinstance(local, AcpConfigError | AcpAuthRequiredError | AcpSpawnError):
|
|
277
|
+
return local, exc
|
|
278
|
+
terminal = self._terminal_failure
|
|
279
|
+
if terminal is not None and terminal.done():
|
|
280
|
+
root = terminal.result()
|
|
281
|
+
return classify_acp_error(root, operation=AcpOperation.INITIALIZE, stateful=False), root
|
|
282
|
+
return local, exc
|
|
283
|
+
|
|
284
|
+
async def open_session(self) -> AcpHandshakeInfo:
|
|
285
|
+
"""Create exactly one fresh remote session and apply configured options."""
|
|
286
|
+
# Single-use: a second session/new would race the pump announcement and
|
|
287
|
+
# every session-id comparison site against the first session's id.
|
|
288
|
+
if self._stateful:
|
|
289
|
+
raise RuntimeError("An ACP session has already been opened for this client.")
|
|
290
|
+
# A background failure that settled after connect returned must fail
|
|
291
|
+
# here: the child may still be running with the connection already
|
|
292
|
+
# known dead, and enqueueing session/new would create an orphaned
|
|
293
|
+
# remote session no later frame can reach. Nothing stateful has been
|
|
294
|
+
# sent yet, so the connect-phase classification (retryable) applies.
|
|
295
|
+
terminal = self._require_terminal_failure()
|
|
296
|
+
if terminal.done():
|
|
297
|
+
root = terminal.result()
|
|
298
|
+
raise classify_acp_error(root, operation=AcpOperation.NEW_SESSION, stateful=False) from root
|
|
299
|
+
self._stateful = True
|
|
300
|
+
conn = self._require_conn()
|
|
301
|
+
try:
|
|
302
|
+
async with asyncio.timeout_at(self._require_handshake_deadline()):
|
|
303
|
+
additional = list(dict.fromkeys(self._spec.additional_directories))
|
|
304
|
+
supported = (
|
|
305
|
+
self._capabilities.session_capabilities is not None
|
|
306
|
+
and self._capabilities.session_capabilities.additional_directories is not None
|
|
307
|
+
)
|
|
308
|
+
if additional and not supported:
|
|
309
|
+
logger.warning("ACP agent does not advertise additionalDirectories; sending cwd only")
|
|
310
|
+
additional = []
|
|
311
|
+
response = await conn.new_session(
|
|
312
|
+
cwd=self._spec.cwd,
|
|
313
|
+
additional_directories=additional or None,
|
|
314
|
+
mcp_servers=[],
|
|
315
|
+
)
|
|
316
|
+
# The SDK schema accepts any str; an empty id would send ""
|
|
317
|
+
# outbound while _active_session_id() reads it as no-session,
|
|
318
|
+
# rejecting every callback on a "successfully" opened client.
|
|
319
|
+
# The id is retained and echoed on every session frame, so it
|
|
320
|
+
# rides the same cap as retained JSON-RPC ids.
|
|
321
|
+
session_id = response.session_id
|
|
322
|
+
if type(session_id) is not str or not 0 < len(session_id) <= protocol._MAX_REQUEST_ID_CHARS:
|
|
323
|
+
raise AcpConfigError("The ACP agent returned an invalid session id.")
|
|
324
|
+
self._session_id = session_id
|
|
325
|
+
final_options = await self._apply_session_options(response)
|
|
326
|
+
terminal = self._require_terminal_failure()
|
|
327
|
+
if terminal.done():
|
|
328
|
+
raise terminal.result()
|
|
329
|
+
except asyncio.CancelledError:
|
|
330
|
+
await self.force_close()
|
|
331
|
+
raise
|
|
332
|
+
except BaseException as exc:
|
|
333
|
+
raise classify_acp_error(exc, operation=AcpOperation.NEW_SESSION, stateful=True) from exc
|
|
334
|
+
|
|
335
|
+
if self._cancel_requested:
|
|
336
|
+
await self.cancel()
|
|
337
|
+
return AcpHandshakeInfo(
|
|
338
|
+
session_id=self._session_id,
|
|
339
|
+
agent_info=self._agent_info,
|
|
340
|
+
auth_methods=self._auth_methods,
|
|
341
|
+
capabilities=self._capabilities,
|
|
342
|
+
modes=response.modes,
|
|
343
|
+
models=response.models,
|
|
344
|
+
config_options=final_options,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
async def _apply_session_options(
|
|
348
|
+
self, response: Any
|
|
349
|
+
) -> tuple[SessionConfigOptionSelect | SessionConfigOptionBoolean, ...]:
|
|
350
|
+
conn = self._require_conn()
|
|
351
|
+
session_id = self._require_session_id()
|
|
352
|
+
if self._spec.session_mode:
|
|
353
|
+
advertised = response.modes is not None and self._spec.session_mode in {
|
|
354
|
+
mode.id for mode in response.modes.available_modes
|
|
355
|
+
}
|
|
356
|
+
if not advertised:
|
|
357
|
+
if self._spec.best_effort_options:
|
|
358
|
+
logger.warning("Configured ACP session mode is not advertised; continuing best-effort")
|
|
359
|
+
else:
|
|
360
|
+
raise AcpConfigError("The configured ACP session mode is not advertised.")
|
|
361
|
+
else:
|
|
362
|
+
try:
|
|
363
|
+
await conn.set_session_mode(mode_id=self._spec.session_mode, session_id=session_id)
|
|
364
|
+
except RequestError as exc:
|
|
365
|
+
if self._spec.best_effort_options and is_best_effort_option_rejection(exc):
|
|
366
|
+
logger.warning("ACP agent deterministically rejected session mode; continuing best-effort")
|
|
367
|
+
else:
|
|
368
|
+
raise classify_acp_error(exc, operation=AcpOperation.SET_MODE, stateful=True) from exc
|
|
369
|
+
except Exception as exc:
|
|
370
|
+
raise classify_acp_error(exc, operation=AcpOperation.SET_MODE, stateful=True) from exc
|
|
371
|
+
else:
|
|
372
|
+
# The session/new snapshot predates this accepted switch;
|
|
373
|
+
# patch it so the handshake reports the applied state
|
|
374
|
+
# (config options already get this via the returned sets).
|
|
375
|
+
response.modes.current_mode_id = self._spec.session_mode
|
|
376
|
+
|
|
377
|
+
if self._spec.model_id:
|
|
378
|
+
advertised = response.models is not None and self._spec.model_id in {
|
|
379
|
+
model.model_id for model in response.models.available_models
|
|
380
|
+
}
|
|
381
|
+
if not advertised:
|
|
382
|
+
logger.warning("Configured ACP model is not advertised; continuing with the agent default")
|
|
383
|
+
else:
|
|
384
|
+
try:
|
|
385
|
+
await conn.set_session_model(model_id=self._spec.model_id, session_id=session_id)
|
|
386
|
+
except RequestError as exc:
|
|
387
|
+
if is_best_effort_option_rejection(exc):
|
|
388
|
+
logger.warning("ACP agent rejected the configured model; continuing with the agent default")
|
|
389
|
+
else:
|
|
390
|
+
raise classify_acp_error(exc, operation=AcpOperation.SET_MODEL, stateful=True) from exc
|
|
391
|
+
except Exception as exc:
|
|
392
|
+
raise classify_acp_error(exc, operation=AcpOperation.SET_MODEL, stateful=True) from exc
|
|
393
|
+
else:
|
|
394
|
+
# Same snapshot-staleness rule as the mode above.
|
|
395
|
+
response.models.current_model_id = self._spec.model_id
|
|
396
|
+
|
|
397
|
+
# Config options may depend on one another, and every set response
|
|
398
|
+
# carries the complete replacement set, so each option validates
|
|
399
|
+
# against the latest returned state rather than the session/new list.
|
|
400
|
+
current_options: tuple[SessionConfigOptionSelect | SessionConfigOptionBoolean, ...] = tuple(
|
|
401
|
+
response.config_options or ()
|
|
402
|
+
)
|
|
403
|
+
for option_id, value in self._spec.config_options.items():
|
|
404
|
+
if option_id not in {option.id for option in current_options}:
|
|
405
|
+
if self._spec.best_effort_options:
|
|
406
|
+
logger.warning("Configured ACP option %r is not advertised; continuing best-effort", option_id)
|
|
407
|
+
continue
|
|
408
|
+
raise AcpConfigError("A configured ACP session option is not advertised.")
|
|
409
|
+
try:
|
|
410
|
+
result = await conn.set_config_option(
|
|
411
|
+
config_id=option_id,
|
|
412
|
+
session_id=session_id,
|
|
413
|
+
value=value,
|
|
414
|
+
)
|
|
415
|
+
except RequestError as exc:
|
|
416
|
+
if self._spec.best_effort_options and is_best_effort_option_rejection(exc):
|
|
417
|
+
logger.warning("ACP agent rejected option %r; continuing best-effort", option_id)
|
|
418
|
+
continue
|
|
419
|
+
raise classify_acp_error(exc, operation=AcpOperation.SET_CONFIG_OPTION, stateful=True) from exc
|
|
420
|
+
except Exception as exc:
|
|
421
|
+
raise classify_acp_error(exc, operation=AcpOperation.SET_CONFIG_OPTION, stateful=True) from exc
|
|
422
|
+
if result is not None and result.config_options is not None:
|
|
423
|
+
current_options = tuple(result.config_options)
|
|
424
|
+
return current_options
|
|
425
|
+
|
|
426
|
+
async def prompt(self, text: str) -> AcpPromptOutcome:
|
|
427
|
+
"""Prompt once and return only after the exact response barrier drains."""
|
|
428
|
+
if type(text) is not str:
|
|
429
|
+
raise AcpConfigError("ACP prompt text must be a string.")
|
|
430
|
+
try:
|
|
431
|
+
validate_json_scalar_tree(text)
|
|
432
|
+
except ValueError as exc:
|
|
433
|
+
raise AcpConfigError("ACP prompt text is not valid Unicode.", cause=exc) from exc
|
|
434
|
+
# One prompt per session, claimed synchronously: a second call would
|
|
435
|
+
# replace the shared completion future and reset the store's prompt
|
|
436
|
+
# registration (cross-wiring barriers and ids under concurrency), and
|
|
437
|
+
# ACP usage is session-cumulative, so a sequential reuse would report
|
|
438
|
+
# cumulative totals as per-attempt usage.
|
|
439
|
+
if self._prompt_started:
|
|
440
|
+
raise RuntimeError("An ACP prompt has already been started for this client.")
|
|
441
|
+
self._prompt_started = True
|
|
442
|
+
conn = self._require_conn()
|
|
443
|
+
store = self._require_store()
|
|
444
|
+
terminal = self._require_terminal_failure()
|
|
445
|
+
loop = asyncio.get_running_loop()
|
|
446
|
+
self._completion_future = loop.create_future()
|
|
447
|
+
self._completion_future.add_done_callback(_consume_future_exception)
|
|
448
|
+
store.reset_prompt()
|
|
449
|
+
self._touch_activity()
|
|
450
|
+
# An already-settled terminal failure must fail before create_task:
|
|
451
|
+
# conn.prompt registers and enqueues session/prompt at its first
|
|
452
|
+
# yield, and cancelling the caller never removes the queued sender
|
|
453
|
+
# item — the remote could execute a prompt after the client failed.
|
|
454
|
+
if terminal.done():
|
|
455
|
+
await self._fail_prompt_without_response(None, terminal.result())
|
|
456
|
+
sdk_prompt_task = asyncio.create_task(
|
|
457
|
+
conn.prompt(
|
|
458
|
+
prompt=[TextContentBlock(type="text", text=text)],
|
|
459
|
+
session_id=self._require_session_id(),
|
|
460
|
+
),
|
|
461
|
+
name="chrys.acp.prompt",
|
|
462
|
+
)
|
|
463
|
+
registration_task = asyncio.create_task(store.wait_for_prompt_id(), name="chrys.acp.prompt-registration")
|
|
464
|
+
watchdog = self._start_idle_watchdog()
|
|
465
|
+
try:
|
|
466
|
+
registered, _ = await asyncio.wait(
|
|
467
|
+
{registration_task, terminal},
|
|
468
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
469
|
+
)
|
|
470
|
+
if terminal in registered and registration_task not in registered:
|
|
471
|
+
registration_task.cancel()
|
|
472
|
+
await _reap_task(registration_task)
|
|
473
|
+
await self._fail_prompt_without_response(sdk_prompt_task, terminal.result())
|
|
474
|
+
prompt_id = await registration_task
|
|
475
|
+
|
|
476
|
+
done, _ = await asyncio.wait(
|
|
477
|
+
{sdk_prompt_task, terminal},
|
|
478
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
479
|
+
)
|
|
480
|
+
record = store.record_for(prompt_id)
|
|
481
|
+
response_observed = record is not None and record.response_observed
|
|
482
|
+
if terminal in done and not response_observed:
|
|
483
|
+
await self._fail_prompt_without_response(sdk_prompt_task, terminal.result())
|
|
484
|
+
|
|
485
|
+
usage = record.usage if record is not None else None
|
|
486
|
+
response = None
|
|
487
|
+
response_error: BaseException | None = None
|
|
488
|
+
try:
|
|
489
|
+
response = await sdk_prompt_task
|
|
490
|
+
except asyncio.CancelledError as exc:
|
|
491
|
+
# This await suspends when the response was observed on the
|
|
492
|
+
# wire while the SDK task is still pending (e.g. a settled
|
|
493
|
+
# terminal woke the wait above). A caller cancellation lands
|
|
494
|
+
# here and must reach the outer handler, which owns the
|
|
495
|
+
# force-close-and-reraise; only an independently cancelled
|
|
496
|
+
# SDK task stays a classified transport failure.
|
|
497
|
+
current = asyncio.current_task()
|
|
498
|
+
if current is not None and current.cancelling():
|
|
499
|
+
raise
|
|
500
|
+
response_error = exc
|
|
501
|
+
except BaseException as exc:
|
|
502
|
+
response_error = exc
|
|
503
|
+
|
|
504
|
+
# The final drain must stay bounded even with the idle watchdog
|
|
505
|
+
# disabled (idle_timeout_seconds=0): a stuck update sink would
|
|
506
|
+
# otherwise hold prompt() open forever after the response arrived.
|
|
507
|
+
drain_error: BaseException | None = None
|
|
508
|
+
completion = self._require_completion_future()
|
|
509
|
+
drained, _ = await asyncio.wait({completion}, timeout=_FINAL_DRAIN_TIMEOUT_SECONDS)
|
|
510
|
+
if completion in drained:
|
|
511
|
+
try:
|
|
512
|
+
completion.result()
|
|
513
|
+
except BaseException as exc:
|
|
514
|
+
drain_error = exc
|
|
515
|
+
else:
|
|
516
|
+
drain_error = TimeoutError("ACP update processing did not drain after the prompt response.")
|
|
517
|
+
|
|
518
|
+
if drain_error is not None:
|
|
519
|
+
error = AcpTransportError(
|
|
520
|
+
"ACP updates failed before prompt completion.", cause=drain_error, usage=usage
|
|
521
|
+
)
|
|
522
|
+
await self.force_close()
|
|
523
|
+
raise error from drain_error
|
|
524
|
+
# The response sealed the attempt — success AND error frames both
|
|
525
|
+
# end the turn: outstanding inbound callbacks must not outlive it.
|
|
526
|
+
await self._finish_sealed_prompt()
|
|
527
|
+
if terminal.done():
|
|
528
|
+
root = terminal.result()
|
|
529
|
+
if not isinstance(root, _AcpDisconnectError):
|
|
530
|
+
# Response-wins is reserved for EOF after the exact
|
|
531
|
+
# response; any other settled terminal failure (malformed
|
|
532
|
+
# frame, oversized notification, idle expiry that raced
|
|
533
|
+
# the response) is a protocol failure the caller must
|
|
534
|
+
# see — it beats even an in-band error response — with
|
|
535
|
+
# the observed usage preserved.
|
|
536
|
+
classified = classify_acp_error(root, operation=AcpOperation.PROMPT, stateful=True)
|
|
537
|
+
if isinstance(classified, AcpTransportError):
|
|
538
|
+
classified.usage = usage
|
|
539
|
+
await self.force_close()
|
|
540
|
+
raise classified from root
|
|
541
|
+
if response_error is not None:
|
|
542
|
+
classified = classify_acp_error(
|
|
543
|
+
response_error,
|
|
544
|
+
operation=AcpOperation.PROMPT,
|
|
545
|
+
stateful=True,
|
|
546
|
+
)
|
|
547
|
+
if isinstance(classified, AcpTransportError):
|
|
548
|
+
classified.usage = usage
|
|
549
|
+
await self.force_close()
|
|
550
|
+
elif terminal.done():
|
|
551
|
+
# Only the deferred EOF-after-response settles here (the
|
|
552
|
+
# arbitration above raised every other root). The exact
|
|
553
|
+
# error response still wins the classification, but the
|
|
554
|
+
# known-dead transport must be closed before raising:
|
|
555
|
+
# the deferred disconnect spawned no failure-close.
|
|
556
|
+
await self.force_close()
|
|
557
|
+
raise classified from response_error
|
|
558
|
+
if terminal.done():
|
|
559
|
+
# Only the deferred EOF-after-response reaches here: the
|
|
560
|
+
# response wins, and this close releases the dead transport.
|
|
561
|
+
await self.force_close()
|
|
562
|
+
# A missing response is possible only on the error path raised immediately above.
|
|
563
|
+
prompt_response = cast(PromptResponse, response)
|
|
564
|
+
return AcpPromptOutcome(
|
|
565
|
+
stop_reason=prompt_response.stop_reason,
|
|
566
|
+
usage=usage,
|
|
567
|
+
user_message_id=prompt_response.user_message_id,
|
|
568
|
+
)
|
|
569
|
+
except asyncio.CancelledError:
|
|
570
|
+
store.reject_all_outgoing(ConnectionError("ACP prompt was cancelled."))
|
|
571
|
+
sdk_prompt_task.cancel()
|
|
572
|
+
await _reap_task(sdk_prompt_task)
|
|
573
|
+
completion = self._completion_future
|
|
574
|
+
if completion is not None and not completion.done():
|
|
575
|
+
completion.cancel()
|
|
576
|
+
await self.force_close()
|
|
577
|
+
raise
|
|
578
|
+
finally:
|
|
579
|
+
if watchdog is not None:
|
|
580
|
+
watchdog.cancel()
|
|
581
|
+
await _reap_task(watchdog)
|
|
582
|
+
if not registration_task.done():
|
|
583
|
+
registration_task.cancel()
|
|
584
|
+
await _reap_task(registration_task)
|
|
585
|
+
|
|
586
|
+
async def _finish_sealed_prompt(self) -> None:
|
|
587
|
+
"""Release inbound waits once the exact prompt response is observed.
|
|
588
|
+
|
|
589
|
+
In-flight human callbacks are cancelled and PR-2 waits torn down here
|
|
590
|
+
on the success path; every failure path reaches the same releases via
|
|
591
|
+
the force-close ladder instead.
|
|
592
|
+
"""
|
|
593
|
+
if self._wait_controller is not None:
|
|
594
|
+
# Run the PR-2 seam as a task: an inline await could swallow an
|
|
595
|
+
# outer cancellation inside the seam and stall the caller. Unlike
|
|
596
|
+
# the close ladder's _reap_task, this bounded wait must PRESERVE a
|
|
597
|
+
# caller cancellation: prompt()'s CancelledError handler owns the
|
|
598
|
+
# force-close-and-reraise, and suppressing here would hand a
|
|
599
|
+
# successful outcome to a cancelled caller.
|
|
600
|
+
waits_task = asyncio.create_task(
|
|
601
|
+
self._wait_controller.cancel_pending_waits(),
|
|
602
|
+
name="chrys.acp.cancel-waits",
|
|
603
|
+
)
|
|
604
|
+
waits_task.add_done_callback(_consume_future_exception)
|
|
605
|
+
try:
|
|
606
|
+
_done, pending = await asyncio.wait({waits_task}, timeout=_transport._REQUEST_DRAIN_TIMEOUT_SECONDS)
|
|
607
|
+
except asyncio.CancelledError:
|
|
608
|
+
waits_task.cancel()
|
|
609
|
+
raise
|
|
610
|
+
for stuck in pending:
|
|
611
|
+
stuck.cancel()
|
|
612
|
+
if self._dispatcher is not None:
|
|
613
|
+
await self._dispatcher.cancel_runners()
|
|
614
|
+
|
|
615
|
+
async def _fail_prompt_without_response(
|
|
616
|
+
self,
|
|
617
|
+
sdk_prompt_task: asyncio.Task[Any] | None,
|
|
618
|
+
failure: BaseException,
|
|
619
|
+
) -> None:
|
|
620
|
+
self._require_store().reject_all_outgoing(ConnectionError("ACP prompt failed."))
|
|
621
|
+
if sdk_prompt_task is not None:
|
|
622
|
+
sdk_prompt_task.cancel()
|
|
623
|
+
await _reap_task(sdk_prompt_task)
|
|
624
|
+
completion = self._completion_future
|
|
625
|
+
if completion is not None:
|
|
626
|
+
if not completion.done():
|
|
627
|
+
completion.set_exception(failure)
|
|
628
|
+
with contextlib.suppress(BaseException):
|
|
629
|
+
await completion
|
|
630
|
+
await self.force_close()
|
|
631
|
+
if isinstance(failure, _AcpDisconnectError):
|
|
632
|
+
raise AcpTransportError("The ACP process closed before returning a prompt response.", cause=failure)
|
|
633
|
+
if isinstance(failure, AcpClientError):
|
|
634
|
+
raise failure
|
|
635
|
+
raise AcpTransportError("The ACP transport failed before a prompt response.", cause=failure) from failure
|
|
636
|
+
|
|
637
|
+
async def cancel(self) -> None:
|
|
638
|
+
"""Fire a session-cancel notification and return without waiting for the peer."""
|
|
639
|
+
self._cancel_requested = True
|
|
640
|
+
if self._conn is None or self._session_id is None or self._closing:
|
|
641
|
+
return
|
|
642
|
+
if self._cancel_task is not None and not self._cancel_task.done():
|
|
643
|
+
return
|
|
644
|
+
|
|
645
|
+
async def send_cancel() -> None:
|
|
646
|
+
with contextlib.suppress(Exception):
|
|
647
|
+
await self._require_conn().cancel(session_id=self._require_session_id())
|
|
648
|
+
|
|
649
|
+
self._cancel_task = asyncio.create_task(send_cancel(), name="chrys.acp.cancel")
|
|
650
|
+
|
|
651
|
+
async def aclose(self) -> None:
|
|
652
|
+
"""Attempt a bounded session close, then always execute the force-close ladder."""
|
|
653
|
+
try:
|
|
654
|
+
if self._conn is not None and self._session_id is not None and not self._closing:
|
|
655
|
+
try:
|
|
656
|
+
async with asyncio.timeout(_CLOSE_RPC_TIMEOUT_SECONDS):
|
|
657
|
+
await self._conn.close_session(session_id=self._session_id)
|
|
658
|
+
except RequestError as exc:
|
|
659
|
+
if not (type(exc.code) is int and exc.code == -32601):
|
|
660
|
+
logger.debug("ACP session/close failed", exc_info=True)
|
|
661
|
+
except Exception:
|
|
662
|
+
logger.debug("ACP session/close failed", exc_info=True)
|
|
663
|
+
finally:
|
|
664
|
+
await self.force_close()
|
|
665
|
+
|
|
666
|
+
async def force_close(self) -> None:
|
|
667
|
+
"""Idempotently close SDK tasks and terminate the retained process tree."""
|
|
668
|
+
if self._force_close_task is None:
|
|
669
|
+
self._force_close_task = asyncio.create_task(self._force_close_impl(), name="chrys.acp.force-close")
|
|
670
|
+
task = self._force_close_task
|
|
671
|
+
try:
|
|
672
|
+
await asyncio.shield(task)
|
|
673
|
+
except asyncio.CancelledError:
|
|
674
|
+
await asyncio.shield(task)
|
|
675
|
+
raise
|
|
676
|
+
|
|
677
|
+
async def _force_close_impl(self) -> None:
|
|
678
|
+
self._closing = True
|
|
679
|
+
close_failure: AcpClientError = (
|
|
680
|
+
AcpTransportError("The ACP client was closed.")
|
|
681
|
+
if self._stateful
|
|
682
|
+
else AcpConnectError("The ACP client was closed.")
|
|
683
|
+
)
|
|
684
|
+
terminal = self._terminal_failure
|
|
685
|
+
if terminal is not None and not terminal.done():
|
|
686
|
+
terminal.set_result(close_failure)
|
|
687
|
+
completion = self._completion_future
|
|
688
|
+
if completion is not None and not completion.done():
|
|
689
|
+
completion.set_exception(close_failure)
|
|
690
|
+
pump = self._pump
|
|
691
|
+
if pump is not None:
|
|
692
|
+
await pump.stop()
|
|
693
|
+
dispatcher = self._dispatcher
|
|
694
|
+
sender = self._sender
|
|
695
|
+
if dispatcher is not None:
|
|
696
|
+
dispatcher.close_admission()
|
|
697
|
+
if self._observer is not None:
|
|
698
|
+
self._observer.clear()
|
|
699
|
+
if self._wait_controller is not None:
|
|
700
|
+
# Run the PR-2 seam as a task: an inline await could swallow the
|
|
701
|
+
# timeout's single cancellation and stall the ladder forever.
|
|
702
|
+
waits_task = asyncio.create_task(
|
|
703
|
+
self._wait_controller.cancel_pending_waits(),
|
|
704
|
+
name="chrys.acp.cancel-waits",
|
|
705
|
+
)
|
|
706
|
+
await _reap_task(waits_task, timeout_seconds=_transport._REQUEST_DRAIN_TIMEOUT_SECONDS)
|
|
707
|
+
if dispatcher is not None:
|
|
708
|
+
await dispatcher.drain_runners()
|
|
709
|
+
if self._cancel_task is not None:
|
|
710
|
+
self._cancel_task.cancel()
|
|
711
|
+
await _reap_task(self._cancel_task)
|
|
712
|
+
if sender is not None:
|
|
713
|
+
sender.begin_close()
|
|
714
|
+
|
|
715
|
+
close_failed = False
|
|
716
|
+
if self._conn is not None:
|
|
717
|
+
close_task = asyncio.create_task(self._conn.close(), name="chrys.acp.sdk-close")
|
|
718
|
+
# asyncio.wait, never `asyncio.timeout: await close_task`: the
|
|
719
|
+
# timeout's single cancellation would forward into close_task,
|
|
720
|
+
# where conn.close()'s own suppress(CancelledError) awaits (our
|
|
721
|
+
# dispatcher.stop, the SDK supervisor.shutdown) can consume it
|
|
722
|
+
# while a wedged sender keeps close_task pending forever.
|
|
723
|
+
close_failed = True
|
|
724
|
+
with contextlib.suppress(asyncio.CancelledError, Exception):
|
|
725
|
+
done, _ = await asyncio.wait({close_task}, timeout=_transport._CLOSE_STAGE_TIMEOUT_SECONDS)
|
|
726
|
+
if close_task in done:
|
|
727
|
+
close_failed = close_task.cancelled() or close_task.exception() is not None
|
|
728
|
+
if close_failed:
|
|
729
|
+
close_task.cancel()
|
|
730
|
+
await _reap_task(close_task)
|
|
731
|
+
if close_failed:
|
|
732
|
+
if self._store is not None:
|
|
733
|
+
self._store.reject_all_outgoing(ConnectionError("ACP connection close failed."))
|
|
734
|
+
if sender is not None:
|
|
735
|
+
await sender.emergency_stop()
|
|
736
|
+
if dispatcher is not None:
|
|
737
|
+
await dispatcher.emergency_stop()
|
|
738
|
+
# The SDK only shuts its supervisor down after its sender close
|
|
739
|
+
# succeeds; a wedged close would otherwise leak the receive task.
|
|
740
|
+
# Run shutdown as a task: awaited inline, the SDK's own
|
|
741
|
+
# suppress(CancelledError) would swallow the timeout's single
|
|
742
|
+
# cancellation and the next stuck runner would hang the ladder.
|
|
743
|
+
supervisor = self._supervisor
|
|
744
|
+
if supervisor is not None:
|
|
745
|
+
shutdown_task = asyncio.create_task(
|
|
746
|
+
supervisor.shutdown(),
|
|
747
|
+
name="chrys.acp.supervisor-shutdown",
|
|
748
|
+
)
|
|
749
|
+
await _reap_task(shutdown_task)
|
|
750
|
+
|
|
751
|
+
spawn = self._spawn
|
|
752
|
+
try:
|
|
753
|
+
if spawn is not None:
|
|
754
|
+
exited = False
|
|
755
|
+
try:
|
|
756
|
+
spawn.process.close_stdin()
|
|
757
|
+
exited = await _wait_process_bounded(spawn, _transport._CLOSE_STAGE_TIMEOUT_SECONDS)
|
|
758
|
+
if not exited:
|
|
759
|
+
spawn.process.terminate_tree()
|
|
760
|
+
exited = await _wait_process_bounded(spawn, _transport._CLOSE_STAGE_TIMEOUT_SECONDS)
|
|
761
|
+
except OSError:
|
|
762
|
+
# A failed OS-level wait must not abort the ladder: this
|
|
763
|
+
# close task is cached, so an escape here would leave every
|
|
764
|
+
# later force_close re-raising with resources still held.
|
|
765
|
+
pass
|
|
766
|
+
finally:
|
|
767
|
+
# Descendants may survive stdin EOF or SIGTERM even when
|
|
768
|
+
# the direct child exits; the owned process group/Job stays
|
|
769
|
+
# the authority, so always finish with a kill sweep.
|
|
770
|
+
spawn.process.kill_tree()
|
|
771
|
+
if not exited:
|
|
772
|
+
with contextlib.suppress(OSError):
|
|
773
|
+
await _wait_process_bounded(spawn, _transport._CLOSE_STAGE_TIMEOUT_SECONDS)
|
|
774
|
+
finally:
|
|
775
|
+
if self._updates is not None:
|
|
776
|
+
self._updates.close()
|
|
777
|
+
if self._consumer_task is not None:
|
|
778
|
+
self._consumer_task.cancel()
|
|
779
|
+
await _reap_task(self._consumer_task)
|
|
780
|
+
if spawn is not None:
|
|
781
|
+
await spawn.stderr.stop()
|
|
782
|
+
spawn.process.close_transports()
|
|
783
|
+
|
|
784
|
+
def _install_connection(self) -> None:
|
|
785
|
+
spawn = self._spawn
|
|
786
|
+
if spawn is None:
|
|
787
|
+
raise RuntimeError("ACP process has not been spawned.")
|
|
788
|
+
sdk_reader = asyncio.StreamReader(limit=ACP_STDIO_LIMIT_BYTES)
|
|
789
|
+
pause_transport = _PauseTransport()
|
|
790
|
+
# StreamReader uses only pause/resume hooks here, which _PauseTransport implements.
|
|
791
|
+
sdk_reader.set_transport(cast(asyncio.BaseTransport, pause_transport))
|
|
792
|
+
self._store = _TrackingStateStore()
|
|
793
|
+
self._updates = _ObserverBuffer()
|
|
794
|
+
self._observer = _FrameObserver(
|
|
795
|
+
store=self._store,
|
|
796
|
+
updates=self._updates,
|
|
797
|
+
active_session=self._active_session_id,
|
|
798
|
+
fail=self._fail,
|
|
799
|
+
activity=self._touch_activity,
|
|
800
|
+
)
|
|
801
|
+
self._pump = _FramingPump(
|
|
802
|
+
spawn.process.stdout,
|
|
803
|
+
sdk_reader,
|
|
804
|
+
pause_transport,
|
|
805
|
+
stateful=lambda: self._stateful,
|
|
806
|
+
closing=lambda: self._closing,
|
|
807
|
+
preflight=self._preflight_request,
|
|
808
|
+
on_response=self._observe_response_at_pump,
|
|
809
|
+
active_session=self._active_session_id,
|
|
810
|
+
fail=self._fail,
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
def sender_factory(writer: asyncio.StreamWriter, supervisor: TaskSupervisor) -> _RetainedSender:
|
|
814
|
+
self._supervisor = supervisor
|
|
815
|
+
self._sender = _RetainedSender(
|
|
816
|
+
writer,
|
|
817
|
+
supervisor,
|
|
818
|
+
store=self._require_store(),
|
|
819
|
+
before_response_send=self._require_observer().before_response_send,
|
|
820
|
+
on_failure=self._fail,
|
|
821
|
+
)
|
|
822
|
+
return self._sender
|
|
823
|
+
|
|
824
|
+
def dispatcher_factory(
|
|
825
|
+
queue: MessageQueue,
|
|
826
|
+
supervisor: TaskSupervisor,
|
|
827
|
+
state: MessageStateStore,
|
|
828
|
+
request_runner: RequestRunner,
|
|
829
|
+
notification_runner: NotificationRunner,
|
|
830
|
+
) -> _BackpressureDispatcher:
|
|
831
|
+
self._dispatcher = _BackpressureDispatcher(
|
|
832
|
+
queue=queue,
|
|
833
|
+
supervisor=supervisor,
|
|
834
|
+
store=state,
|
|
835
|
+
request_runner=request_runner,
|
|
836
|
+
notification_runner=notification_runner,
|
|
837
|
+
on_failure=self._fail,
|
|
838
|
+
)
|
|
839
|
+
return self._dispatcher
|
|
840
|
+
|
|
841
|
+
# AcpAgentClient implements the SDK Client protocol; decorators obscure structural matching from ty.
|
|
842
|
+
self._conn = ClientSideConnection(
|
|
843
|
+
cast(Client, self),
|
|
844
|
+
spawn.process.stdin,
|
|
845
|
+
sdk_reader,
|
|
846
|
+
queue=InMemoryMessageQueue(maxsize=256),
|
|
847
|
+
state_store=self._store,
|
|
848
|
+
sender_factory=sender_factory,
|
|
849
|
+
dispatcher_factory=dispatcher_factory,
|
|
850
|
+
observers=[self._observer],
|
|
851
|
+
receive_timeout=None,
|
|
852
|
+
)
|
|
853
|
+
self._pump.start()
|
|
854
|
+
self._consumer_task = asyncio.create_task(self._consume_updates(), name="chrys.acp.updates")
|
|
855
|
+
|
|
856
|
+
async def _consume_updates(self) -> None:
|
|
857
|
+
sequence = 0
|
|
858
|
+
try:
|
|
859
|
+
while True:
|
|
860
|
+
item = await self._require_updates().get()
|
|
861
|
+
if isinstance(item, _PromptBarrier):
|
|
862
|
+
completion = self._completion_future
|
|
863
|
+
if (
|
|
864
|
+
completion is not None
|
|
865
|
+
and not completion.done()
|
|
866
|
+
and self._store is not None
|
|
867
|
+
and self._store.prompt_id == item.request_id
|
|
868
|
+
):
|
|
869
|
+
completion.set_result(None)
|
|
870
|
+
continue
|
|
871
|
+
# Residual guard behind the observer's intake filter — a raw
|
|
872
|
+
# peek, so a foreign frame is dropped before its shape can
|
|
873
|
+
# fail validation and terminate the attempt.
|
|
874
|
+
if item.get("sessionId") != self._active_session_id():
|
|
875
|
+
logger.warning("Dropping ACP session/update for a foreign session")
|
|
876
|
+
continue
|
|
877
|
+
if not _valid_raw_usage_update(item):
|
|
878
|
+
continue
|
|
879
|
+
notification = SessionNotification.model_validate(item)
|
|
880
|
+
sequence += 1
|
|
881
|
+
await self._update_sink.put(sequence, notification)
|
|
882
|
+
except asyncio.CancelledError:
|
|
883
|
+
raise
|
|
884
|
+
except EOFError:
|
|
885
|
+
return
|
|
886
|
+
except BaseException as exc:
|
|
887
|
+
failure = AcpTransportError("ACP update validation or callback failed.", cause=exc)
|
|
888
|
+
completion = self._completion_future
|
|
889
|
+
if completion is not None and not completion.done():
|
|
890
|
+
completion.set_exception(failure)
|
|
891
|
+
self._fail(failure)
|
|
892
|
+
|
|
893
|
+
def _fail(self, failure: BaseException) -> None:
|
|
894
|
+
if not self._stateful and isinstance(failure, AcpTransportError):
|
|
895
|
+
if isinstance(failure, _AcpProtocolLimitError):
|
|
896
|
+
# Cap and budget violations replay identically on every
|
|
897
|
+
# respawn (same executable, same greeting), so the connect
|
|
898
|
+
# window must surface them as deterministic incompatibility,
|
|
899
|
+
# not as a retryable connect failure.
|
|
900
|
+
failure = AcpConfigError(failure.detail, cause=failure.cause or failure)
|
|
901
|
+
else:
|
|
902
|
+
failure = AcpConnectError(failure.detail, cause=failure.cause or failure)
|
|
903
|
+
terminal = self._terminal_failure
|
|
904
|
+
if terminal is not None and not terminal.done():
|
|
905
|
+
terminal.set_result(failure)
|
|
906
|
+
completion = self._completion_future
|
|
907
|
+
response_observed = False
|
|
908
|
+
if self._store is not None and self._store.prompt_id is not None:
|
|
909
|
+
record = self._store.record_for(self._store.prompt_id)
|
|
910
|
+
response_observed = record is not None and record.response_observed
|
|
911
|
+
defer_disconnect = isinstance(failure, _AcpDisconnectError) and response_observed
|
|
912
|
+
if completion is not None and not completion.done() and not defer_disconnect:
|
|
913
|
+
completion.set_exception(failure)
|
|
914
|
+
if isinstance(failure, AcpTransportError | AcpConnectError) and not self._closing and not defer_disconnect:
|
|
915
|
+
self._failure_close_task = asyncio.create_task(self.force_close(), name="chrys.acp.failure-close")
|
|
916
|
+
|
|
917
|
+
def _active_session_id(self) -> str | None:
|
|
918
|
+
"""The authoritative session id, or the pump-announced one before assignment."""
|
|
919
|
+
return self._session_id or self._announced_session_id
|
|
920
|
+
|
|
921
|
+
def _observe_response_at_pump(self, message: dict[str, Any]) -> None:
|
|
922
|
+
"""Track a matching response synchronously, in pump frame order.
|
|
923
|
+
|
|
924
|
+
A response coalesced with EOF in one transport read is processed by
|
|
925
|
+
the pump without yielding, so the SDK observer would mark
|
|
926
|
+
``response_observed`` only after the EOF classification already ran —
|
|
927
|
+
misclassifying a completed prompt, or a deterministic error response,
|
|
928
|
+
as a transport failure. Observation therefore happens here; sealing
|
|
929
|
+
and the update barrier stay in the SDK observer, which is the only
|
|
930
|
+
point ordered against the queued updates.
|
|
931
|
+
"""
|
|
932
|
+
store = self._store
|
|
933
|
+
request_id = message.get("id")
|
|
934
|
+
if store is not None and type(request_id) is int:
|
|
935
|
+
try:
|
|
936
|
+
store.observe_response(request_id, message)
|
|
937
|
+
except AcpTransportError:
|
|
938
|
+
raise
|
|
939
|
+
except BaseException as exc:
|
|
940
|
+
raise AcpTransportError(
|
|
941
|
+
"The ACP frame observer failed.",
|
|
942
|
+
cause=exc,
|
|
943
|
+
) from exc
|
|
944
|
+
self._sniff_session_response(message)
|
|
945
|
+
|
|
946
|
+
def _sniff_session_response(self, message: dict[str, Any]) -> None:
|
|
947
|
+
"""Capture the announced session id at the pump, ahead of SDK routing.
|
|
948
|
+
|
|
949
|
+
``_session_id`` is assigned only when ``open_session`` resumes after
|
|
950
|
+
the SDK resolves ``session/new``; an agent may write a session-bound
|
|
951
|
+
request back-to-back with that response, and the pump would preflight
|
|
952
|
+
it before the assignment lands. The pump orders frames serially, so
|
|
953
|
+
sniffing the response stream here closes the race for every session
|
|
954
|
+
check downstream (preflight, observer, callbacks, update consumer).
|
|
955
|
+
"""
|
|
956
|
+
if self._session_id is not None or self._announced_session_id is not None:
|
|
957
|
+
return
|
|
958
|
+
store = self._store
|
|
959
|
+
request_id = message.get("id")
|
|
960
|
+
if store is None or type(request_id) is not int:
|
|
961
|
+
return
|
|
962
|
+
record = store.record_for(request_id)
|
|
963
|
+
if record is None or record.method != _NEW_SESSION_METHOD or not record.write_committed:
|
|
964
|
+
return
|
|
965
|
+
# First-response claim: the SDK ignores duplicate response ids, but the
|
|
966
|
+
# pump sees every frame before routing — after a failed session/new, a
|
|
967
|
+
# duplicate success frame with the same id must not announce.
|
|
968
|
+
if self._session_response_claimed:
|
|
969
|
+
return
|
|
970
|
+
self._session_response_claimed = True
|
|
971
|
+
result = message.get("result")
|
|
972
|
+
if type(result) is not dict:
|
|
973
|
+
return
|
|
974
|
+
# The announced id is retained and compared on every subsequent frame
|
|
975
|
+
# BEFORE the store's caps run SDK-side, so an uncapped result must
|
|
976
|
+
# never announce; downstream validation still fails the open attempt.
|
|
977
|
+
try:
|
|
978
|
+
_validate_payload_caps(result)
|
|
979
|
+
except ValueError:
|
|
980
|
+
return
|
|
981
|
+
# Caps bound only size and shape; open_session accepts this result
|
|
982
|
+
# solely when it validates as NewSessionResponse, so announcement
|
|
983
|
+
# must clear the same schema gate before exposing the id.
|
|
984
|
+
try:
|
|
985
|
+
NewSessionResponse.model_validate(result)
|
|
986
|
+
except ValidationError:
|
|
987
|
+
return
|
|
988
|
+
session_id = result.get("sessionId")
|
|
989
|
+
if type(session_id) is str and 0 < len(session_id) <= protocol._MAX_REQUEST_ID_CHARS:
|
|
990
|
+
self._announced_session_id = session_id
|
|
991
|
+
|
|
992
|
+
def _touch_activity(self) -> None:
|
|
993
|
+
with contextlib.suppress(RuntimeError):
|
|
994
|
+
self._last_activity = asyncio.get_running_loop().time()
|
|
995
|
+
if self._activity_event is not None:
|
|
996
|
+
self._activity_event.set()
|
|
997
|
+
|
|
998
|
+
def _start_idle_watchdog(self) -> asyncio.Task[None] | None:
|
|
999
|
+
if self._spec.idle_timeout_seconds == 0:
|
|
1000
|
+
return None
|
|
1001
|
+
return asyncio.create_task(self._idle_watchdog(), name="chrys.acp.idle-watchdog")
|
|
1002
|
+
|
|
1003
|
+
async def _idle_watchdog(self) -> None:
|
|
1004
|
+
timeout_seconds = self._spec.idle_timeout_seconds
|
|
1005
|
+
event = self._activity_event
|
|
1006
|
+
if event is None:
|
|
1007
|
+
return
|
|
1008
|
+
while True:
|
|
1009
|
+
event.clear()
|
|
1010
|
+
# The exact prompt response ends the agent's idle responsibility;
|
|
1011
|
+
# draining already-received updates through a slow local sink is
|
|
1012
|
+
# governed by the final-drain bound, never blamed on the agent.
|
|
1013
|
+
if self._prompt_response_sealed():
|
|
1014
|
+
return
|
|
1015
|
+
observer = self._observer
|
|
1016
|
+
if observer is not None and observer.human_wait_count:
|
|
1017
|
+
await event.wait()
|
|
1018
|
+
continue
|
|
1019
|
+
remaining = timeout_seconds - (asyncio.get_running_loop().time() - self._last_activity)
|
|
1020
|
+
if remaining <= 0:
|
|
1021
|
+
self._fail(AcpIdleTimeoutError("The ACP agent stopped producing protocol activity."))
|
|
1022
|
+
return
|
|
1023
|
+
try:
|
|
1024
|
+
async with asyncio.timeout(remaining):
|
|
1025
|
+
await event.wait()
|
|
1026
|
+
except TimeoutError:
|
|
1027
|
+
# The seal and the expiry timer can land on the same wake;
|
|
1028
|
+
# a sealed attempt must never fail as agent-idle.
|
|
1029
|
+
if self._prompt_response_sealed():
|
|
1030
|
+
return
|
|
1031
|
+
self._fail(AcpIdleTimeoutError("The ACP agent stopped producing protocol activity."))
|
|
1032
|
+
return
|
|
1033
|
+
|
|
1034
|
+
def _prompt_response_sealed(self) -> bool:
|
|
1035
|
+
observer = self._observer
|
|
1036
|
+
if observer is not None and observer.prompt_sealed:
|
|
1037
|
+
return True
|
|
1038
|
+
# The pump marks the prompt record strictly before the SDK observer
|
|
1039
|
+
# runs the seal; a response already observed on the wire must never
|
|
1040
|
+
# lose to idle expiry inside that scheduling window.
|
|
1041
|
+
store = self._store
|
|
1042
|
+
if store is None or store.prompt_id is None:
|
|
1043
|
+
return False
|
|
1044
|
+
record = store.record_for(store.prompt_id)
|
|
1045
|
+
return record is not None and record.response_observed
|
|
1046
|
+
|
|
1047
|
+
def _preflight_request(self, message: dict[str, Any]) -> dict[str, Any]:
|
|
1048
|
+
try:
|
|
1049
|
+
params = message.get("params")
|
|
1050
|
+
if type(params) is not dict:
|
|
1051
|
+
raise ValueError("ACP request params must be an object.")
|
|
1052
|
+
_validate_payload_caps(params)
|
|
1053
|
+
active = self._active_session_id()
|
|
1054
|
+
if active is None or type(params.get("sessionId")) is not str or params["sessionId"] != active:
|
|
1055
|
+
raise ValueError("ACP request is not bound to the active session.")
|
|
1056
|
+
meta = params.get("_meta")
|
|
1057
|
+
if meta is not None and (type(meta) is not dict or _META_COLLISION_KEYS.intersection(meta)):
|
|
1058
|
+
raise ValueError("ACP request metadata collides with callback fields.")
|
|
1059
|
+
method = message["method"]
|
|
1060
|
+
if method == protocol._PERMISSION_METHOD:
|
|
1061
|
+
_preflight_permission(params)
|
|
1062
|
+
elif method == protocol._ASK_USER_METHOD:
|
|
1063
|
+
_preflight_ask_user(params)
|
|
1064
|
+
except ValueError:
|
|
1065
|
+
replacement = dict(message)
|
|
1066
|
+
replacement["params"] = {}
|
|
1067
|
+
return replacement
|
|
1068
|
+
return message
|
|
1069
|
+
|
|
1070
|
+
async def request_permission(
|
|
1071
|
+
self,
|
|
1072
|
+
options: list[PermissionOption],
|
|
1073
|
+
session_id: str,
|
|
1074
|
+
tool_call: ToolCallUpdate,
|
|
1075
|
+
**kwargs: Any,
|
|
1076
|
+
) -> RequestPermissionResponse:
|
|
1077
|
+
if session_id != self._active_session_id():
|
|
1078
|
+
raise RequestError.invalid_params({"details": "Foreign ACP session."})
|
|
1079
|
+
request_id = _CURRENT_INBOUND_ID.get()
|
|
1080
|
+
observer = self._require_observer()
|
|
1081
|
+
if not observer.human_wait_admitted(request_id):
|
|
1082
|
+
return RequestPermissionResponse(outcome=DeniedOutcome(outcome="cancelled"))
|
|
1083
|
+
options_by_id: dict[str, PermissionOption] = {}
|
|
1084
|
+
for option in options:
|
|
1085
|
+
if option.option_id in options_by_id:
|
|
1086
|
+
# The wire outcome names only an option id, so duplicate ids
|
|
1087
|
+
# hand the agent the choice of what a selection meant — no
|
|
1088
|
+
# response can be transmitted faithfully.
|
|
1089
|
+
observer.clear_human_wait(request_id)
|
|
1090
|
+
raise RequestError.invalid_params({"details": "Duplicate ACP permission option ids."})
|
|
1091
|
+
options_by_id[option.option_id] = option
|
|
1092
|
+
try:
|
|
1093
|
+
try:
|
|
1094
|
+
decision = await self._callbacks.on_permission_request(tool_call, tuple(options))
|
|
1095
|
+
except asyncio.CancelledError:
|
|
1096
|
+
raise
|
|
1097
|
+
except Exception as exc:
|
|
1098
|
+
self._fail(AcpTransportError("The ACP permission callback failed.", cause=exc))
|
|
1099
|
+
raise
|
|
1100
|
+
finally:
|
|
1101
|
+
observer.clear_human_wait(request_id)
|
|
1102
|
+
if decision.action == "cancelled" or decision.option_id is None:
|
|
1103
|
+
return RequestPermissionResponse(outcome=DeniedOutcome(outcome="cancelled"))
|
|
1104
|
+
selected = options_by_id.get(decision.option_id)
|
|
1105
|
+
if selected is None:
|
|
1106
|
+
return RequestPermissionResponse(outcome=DeniedOutcome(outcome="cancelled"))
|
|
1107
|
+
if selected.kind not in _DECISION_OPTION_KINDS.get(decision.action, frozenset()):
|
|
1108
|
+
# An id of the wrong kind must never transmute the decision —
|
|
1109
|
+
# deny stays deny even when the callback names an allow option.
|
|
1110
|
+
logger.warning("An ACP permission decision does not match its selected option kind; cancelling")
|
|
1111
|
+
return RequestPermissionResponse(outcome=DeniedOutcome(outcome="cancelled"))
|
|
1112
|
+
return RequestPermissionResponse(
|
|
1113
|
+
outcome=AllowedOutcome(outcome="selected", option_id=decision.option_id),
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
async def session_update(self, **kwargs: Any) -> None:
|
|
1117
|
+
return
|
|
1118
|
+
|
|
1119
|
+
async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
1120
|
+
request_id = _CURRENT_INBOUND_ID.get()
|
|
1121
|
+
try:
|
|
1122
|
+
if method != "chrys/request_input":
|
|
1123
|
+
raise RequestError.method_not_found(method)
|
|
1124
|
+
active = self._active_session_id()
|
|
1125
|
+
if active is None:
|
|
1126
|
+
raise RequestError.invalid_params({"details": "Foreign ACP session."})
|
|
1127
|
+
_validate_ext_params(method, params, active)
|
|
1128
|
+
if not self._require_observer().human_wait_admitted(request_id):
|
|
1129
|
+
return {"cancelled": True}
|
|
1130
|
+
try:
|
|
1131
|
+
return await self._callbacks.on_ext_method(method, params)
|
|
1132
|
+
except asyncio.CancelledError:
|
|
1133
|
+
raise
|
|
1134
|
+
except Exception as exc:
|
|
1135
|
+
self._fail(AcpTransportError("The ACP request-input callback failed.", cause=exc))
|
|
1136
|
+
raise
|
|
1137
|
+
finally:
|
|
1138
|
+
if method == "chrys/request_input":
|
|
1139
|
+
self._require_observer().clear_human_wait(request_id)
|
|
1140
|
+
|
|
1141
|
+
async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
|
|
1142
|
+
return
|
|
1143
|
+
|
|
1144
|
+
async def write_text_file(
|
|
1145
|
+
self,
|
|
1146
|
+
content: str,
|
|
1147
|
+
path: str,
|
|
1148
|
+
session_id: str,
|
|
1149
|
+
**kwargs: Any,
|
|
1150
|
+
) -> WriteTextFileResponse | None:
|
|
1151
|
+
raise RequestError.method_not_found("fs/write_text_file")
|
|
1152
|
+
|
|
1153
|
+
async def read_text_file(
|
|
1154
|
+
self,
|
|
1155
|
+
path: str,
|
|
1156
|
+
session_id: str,
|
|
1157
|
+
limit: int | None = None,
|
|
1158
|
+
line: int | None = None,
|
|
1159
|
+
**kwargs: Any,
|
|
1160
|
+
) -> ReadTextFileResponse:
|
|
1161
|
+
raise RequestError.method_not_found("fs/read_text_file")
|
|
1162
|
+
|
|
1163
|
+
async def create_terminal(
|
|
1164
|
+
self,
|
|
1165
|
+
command: str,
|
|
1166
|
+
session_id: str,
|
|
1167
|
+
args: list[str] | None = None,
|
|
1168
|
+
cwd: str | None = None,
|
|
1169
|
+
env: list[EnvVariable] | None = None,
|
|
1170
|
+
output_byte_limit: int | None = None,
|
|
1171
|
+
**kwargs: Any,
|
|
1172
|
+
) -> CreateTerminalResponse:
|
|
1173
|
+
raise RequestError.method_not_found("terminal/create")
|
|
1174
|
+
|
|
1175
|
+
async def terminal_output(
|
|
1176
|
+
self,
|
|
1177
|
+
session_id: str,
|
|
1178
|
+
terminal_id: str,
|
|
1179
|
+
**kwargs: Any,
|
|
1180
|
+
) -> TerminalOutputResponse:
|
|
1181
|
+
raise RequestError.method_not_found("terminal/output")
|
|
1182
|
+
|
|
1183
|
+
async def release_terminal(
|
|
1184
|
+
self,
|
|
1185
|
+
session_id: str,
|
|
1186
|
+
terminal_id: str,
|
|
1187
|
+
**kwargs: Any,
|
|
1188
|
+
) -> ReleaseTerminalResponse | None:
|
|
1189
|
+
raise RequestError.method_not_found("terminal/release")
|
|
1190
|
+
|
|
1191
|
+
async def wait_for_terminal_exit(
|
|
1192
|
+
self,
|
|
1193
|
+
session_id: str,
|
|
1194
|
+
terminal_id: str,
|
|
1195
|
+
**kwargs: Any,
|
|
1196
|
+
) -> WaitForTerminalExitResponse:
|
|
1197
|
+
raise RequestError.method_not_found("terminal/wait_for_exit")
|
|
1198
|
+
|
|
1199
|
+
async def kill_terminal(
|
|
1200
|
+
self,
|
|
1201
|
+
session_id: str,
|
|
1202
|
+
terminal_id: str,
|
|
1203
|
+
**kwargs: Any,
|
|
1204
|
+
) -> KillTerminalResponse | None:
|
|
1205
|
+
raise RequestError.method_not_found("terminal/kill")
|
|
1206
|
+
|
|
1207
|
+
def on_connect(self, conn: Any) -> None:
|
|
1208
|
+
return
|
|
1209
|
+
|
|
1210
|
+
def _require_conn(self) -> ClientSideConnection:
|
|
1211
|
+
if self._conn is None:
|
|
1212
|
+
raise RuntimeError("ACP client is not connected.")
|
|
1213
|
+
return self._conn
|
|
1214
|
+
|
|
1215
|
+
def _require_store(self) -> _TrackingStateStore:
|
|
1216
|
+
if self._store is None:
|
|
1217
|
+
raise RuntimeError("ACP state store is not installed.")
|
|
1218
|
+
return self._store
|
|
1219
|
+
|
|
1220
|
+
def _require_observer(self) -> _FrameObserver:
|
|
1221
|
+
if self._observer is None:
|
|
1222
|
+
raise RuntimeError("ACP frame observer is not installed.")
|
|
1223
|
+
return self._observer
|
|
1224
|
+
|
|
1225
|
+
def _require_updates(self) -> _ObserverBuffer:
|
|
1226
|
+
if self._updates is None:
|
|
1227
|
+
raise RuntimeError("ACP update buffer is not installed.")
|
|
1228
|
+
return self._updates
|
|
1229
|
+
|
|
1230
|
+
def _require_terminal_failure(self) -> asyncio.Future[BaseException]:
|
|
1231
|
+
if self._terminal_failure is None:
|
|
1232
|
+
raise RuntimeError("ACP terminal failure future is not installed.")
|
|
1233
|
+
return self._terminal_failure
|
|
1234
|
+
|
|
1235
|
+
def _require_completion_future(self) -> asyncio.Future[None]:
|
|
1236
|
+
if self._completion_future is None:
|
|
1237
|
+
raise RuntimeError("ACP completion future is not installed.")
|
|
1238
|
+
return self._completion_future
|
|
1239
|
+
|
|
1240
|
+
def _require_session_id(self) -> str:
|
|
1241
|
+
if self._session_id is None:
|
|
1242
|
+
raise RuntimeError("ACP session is not open.")
|
|
1243
|
+
return self._session_id
|
|
1244
|
+
|
|
1245
|
+
def _require_handshake_deadline(self) -> float:
|
|
1246
|
+
if self._handshake_deadline is None:
|
|
1247
|
+
raise RuntimeError("ACP handshake has not started.")
|
|
1248
|
+
return self._handshake_deadline
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def _preflight_permission(params: dict[str, Any]) -> None:
|
|
1252
|
+
options = params.get("options")
|
|
1253
|
+
tool_call = params.get("toolCall")
|
|
1254
|
+
if type(options) is not list or len(options) > _MAX_PERMISSION_OPTIONS or type(tool_call) is not dict:
|
|
1255
|
+
raise ValueError("Malformed ACP permission request.")
|
|
1256
|
+
for option in options:
|
|
1257
|
+
if type(option) is not dict:
|
|
1258
|
+
raise ValueError("Malformed ACP permission option.")
|
|
1259
|
+
if type(option.get("optionId")) is not str or type(option.get("name")) is not str:
|
|
1260
|
+
raise ValueError("Malformed ACP permission option.")
|
|
1261
|
+
for field in ("toolCallId",):
|
|
1262
|
+
if type(tool_call.get(field)) is not str:
|
|
1263
|
+
raise ValueError("Malformed ACP permission tool call.")
|
|
1264
|
+
if "title" in tool_call and tool_call["title"] is not None and type(tool_call["title"]) is not str:
|
|
1265
|
+
raise ValueError("Malformed ACP permission title.")
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def _preflight_ask_user(params: dict[str, Any]) -> None:
|
|
1269
|
+
validate_request_input_params(params)
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def _validate_ext_params(method: str, params: dict[str, Any], session_id: str) -> None:
|
|
1273
|
+
if type(method) is not str or type(params) is not dict:
|
|
1274
|
+
raise RequestError.invalid_params({"details": "Malformed ACP extension request."})
|
|
1275
|
+
try:
|
|
1276
|
+
validate_json_scalar_tree(params)
|
|
1277
|
+
_validate_payload_caps(params)
|
|
1278
|
+
if type(params.get("sessionId")) is not str or params["sessionId"] != session_id:
|
|
1279
|
+
raise ValueError
|
|
1280
|
+
if method == "chrys/request_input":
|
|
1281
|
+
_preflight_ask_user(params)
|
|
1282
|
+
except ValueError as exc:
|
|
1283
|
+
raise RequestError.invalid_params({"details": "Malformed ACP extension request."}) from exc
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def _valid_raw_usage_update(params: dict[str, Any]) -> bool:
|
|
1287
|
+
update = params.get("update")
|
|
1288
|
+
if type(update) is not dict or update.get("sessionUpdate") != "usage_update":
|
|
1289
|
+
return True
|
|
1290
|
+
return _valid_usage_int(update.get("used")) and _valid_usage_int(update.get("size"))
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
async def _dispose_spawn(spawn: AcpSpawnResult) -> None:
|
|
1294
|
+
"""Release a spawn acquired after an already-completed force_close ran."""
|
|
1295
|
+
spawn.process.kill_tree()
|
|
1296
|
+
try:
|
|
1297
|
+
await _wait_process_bounded(spawn, _transport._CLOSE_STAGE_TIMEOUT_SECONDS)
|
|
1298
|
+
finally:
|
|
1299
|
+
await spawn.stderr.stop()
|
|
1300
|
+
spawn.process.close_transports()
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
async def _wait_process_bounded(spawn: AcpSpawnResult, timeout_seconds: float) -> bool:
|
|
1304
|
+
if spawn.process.returncode is not None:
|
|
1305
|
+
return True
|
|
1306
|
+
try:
|
|
1307
|
+
async with asyncio.timeout(timeout_seconds):
|
|
1308
|
+
await spawn.process.wait()
|
|
1309
|
+
except TimeoutError:
|
|
1310
|
+
return False
|
|
1311
|
+
return True
|