iCode-TUI 0.29.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chrys/__init__.py +16 -0
- chrys/app/__init__.py +3 -0
- chrys/app/acp/__init__.py +3 -0
- chrys/app/acp/bridge.py +664 -0
- chrys/app/acp/content.py +132 -0
- chrys/app/acp/doc/frontend-api.md +425 -0
- chrys/app/acp/history.py +585 -0
- chrys/app/acp/server.py +1796 -0
- chrys/app/acp/session_manager.py +1815 -0
- chrys/app/acp/tool_status.py +45 -0
- chrys/app/cli/__init__.py +3 -0
- chrys/app/cli/acp.py +178 -0
- chrys/app/cli/app.py +165 -0
- chrys/app/cli/approvals.py +101 -0
- chrys/app/cli/headless.py +223 -0
- chrys/app/cli/launch_cwd.py +42 -0
- chrys/app/cli/profiles.py +298 -0
- chrys/app/cli/progress.py +857 -0
- chrys/app/cli/run.py +425 -0
- chrys/app/cli/serve.py +1593 -0
- chrys/app/cli/tool_summary.py +225 -0
- chrys/app/cli/trajectory.py +201 -0
- chrys/app/cli/workflow.py +601 -0
- chrys/app/features/__init__.py +3 -0
- chrys/app/features/buddy/__init__.py +3 -0
- chrys/app/features/buddy/actions.py +68 -0
- chrys/app/features/buddy/animation.py +76 -0
- chrys/app/features/buddy/commands.py +196 -0
- chrys/app/features/buddy/docs/design.md +187 -0
- chrys/app/features/buddy/hatchery.py +80 -0
- chrys/app/features/buddy/lifecycle.py +41 -0
- chrys/app/features/buddy/model.py +265 -0
- chrys/app/features/buddy/pixel_renderer.py +113 -0
- chrys/app/features/buddy/pixel_sprites.py +279 -0
- chrys/app/features/buddy/portrait.py +76 -0
- chrys/app/features/buddy/progression.py +58 -0
- chrys/app/features/buddy/replies.py +191 -0
- chrys/app/features/buddy/sprites/alien.toml +70 -0
- chrys/app/features/buddy/sprites/axolotl.toml +71 -0
- chrys/app/features/buddy/sprites/bat.toml +69 -0
- chrys/app/features/buddy/sprites/bee.toml +69 -0
- chrys/app/features/buddy/sprites/cactus.toml +73 -0
- chrys/app/features/buddy/sprites/capybara.toml +70 -0
- chrys/app/features/buddy/sprites/cat.toml +69 -0
- chrys/app/features/buddy/sprites/crab.toml +70 -0
- chrys/app/features/buddy/sprites/dragon.toml +71 -0
- chrys/app/features/buddy/sprites/duck.toml +69 -0
- chrys/app/features/buddy/sprites/fox.toml +69 -0
- chrys/app/features/buddy/sprites/frog.toml +70 -0
- chrys/app/features/buddy/sprites/ghost.toml +70 -0
- chrys/app/features/buddy/sprites/goose.toml +69 -0
- chrys/app/features/buddy/sprites/jelly.toml +70 -0
- chrys/app/features/buddy/sprites/llama.toml +70 -0
- chrys/app/features/buddy/sprites/mushroom.toml +69 -0
- chrys/app/features/buddy/sprites/octopus.toml +70 -0
- chrys/app/features/buddy/sprites/owl.toml +70 -0
- chrys/app/features/buddy/sprites/panda.toml +71 -0
- chrys/app/features/buddy/sprites/penguin.toml +69 -0
- chrys/app/features/buddy/sprites/rabbit.toml +70 -0
- chrys/app/features/buddy/sprites/robot.toml +72 -0
- chrys/app/features/buddy/sprites/shark.toml +70 -0
- chrys/app/features/buddy/sprites/snail.toml +69 -0
- chrys/app/features/buddy/sprites/snake.toml +69 -0
- chrys/app/features/buddy/sprites/tabby.toml +69 -0
- chrys/app/features/buddy/sprites/turtle.toml +69 -0
- chrys/app/features/buddy/store.py +143 -0
- chrys/app/features/session_title/__init__.py +7 -0
- chrys/app/features/session_title/updater.py +582 -0
- chrys/app/installer.py +651 -0
- chrys/app/parsing.py +17 -0
- chrys/app/tui/__init__.py +3 -0
- chrys/app/tui/app.py +1287 -0
- chrys/app/tui/behaviors/__init__.py +3 -0
- chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
- chrys/app/tui/behaviors/insert_clipboard.py +73 -0
- chrys/app/tui/behaviors/right_click_copy.py +146 -0
- chrys/app/tui/binding_display.py +139 -0
- chrys/app/tui/buddy_messages.py +17 -0
- chrys/app/tui/buddy_reply.py +86 -0
- chrys/app/tui/chrys.tcss +549 -0
- chrys/app/tui/clipboard.py +109 -0
- chrys/app/tui/copy_messages.py +10 -0
- chrys/app/tui/i18n.py +230 -0
- chrys/app/tui/language.py +58 -0
- chrys/app/tui/notifications/__init__.py +12 -0
- chrys/app/tui/notifications/drivers.py +180 -0
- chrys/app/tui/notifications/service.py +126 -0
- chrys/app/tui/notifications/settings.py +141 -0
- chrys/app/tui/screens/__init__.py +3 -0
- chrys/app/tui/screens/agents/__init__.py +8 -0
- chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
- chrys/app/tui/screens/agents/config.py +2142 -0
- chrys/app/tui/screens/agents/config.tcss +169 -0
- chrys/app/tui/screens/agents/panels/__init__.py +3 -0
- chrys/app/tui/screens/agents/panels/acp.py +1176 -0
- chrys/app/tui/screens/agents/panels/basic.py +497 -0
- chrys/app/tui/screens/agents/panels/compaction.py +231 -0
- chrys/app/tui/screens/agents/panels/config_card.py +218 -0
- chrys/app/tui/screens/agents/panels/instructions.py +91 -0
- chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
- chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
- chrys/app/tui/screens/agents/panels/memory.py +684 -0
- chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
- chrys/app/tui/screens/agents/panels/skills.py +806 -0
- chrys/app/tui/screens/agents/panels/subagents.py +652 -0
- chrys/app/tui/screens/agents/panels/tools.py +205 -0
- chrys/app/tui/screens/agents/picker.py +103 -0
- chrys/app/tui/screens/agents/skill_paths.py +38 -0
- chrys/app/tui/screens/agents/validation_messages.py +452 -0
- chrys/app/tui/screens/dialogs/__init__.py +35 -0
- chrys/app/tui/screens/dialogs/agent_load.py +834 -0
- chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
- chrys/app/tui/screens/dialogs/app_mode.py +55 -0
- chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
- chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
- chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
- chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
- chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
- chrys/app/tui/screens/dialogs/approval/body.py +110 -0
- chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
- chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
- chrys/app/tui/screens/dialogs/ask_user.py +148 -0
- chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
- chrys/app/tui/screens/dialogs/base.py +84 -0
- chrys/app/tui/screens/dialogs/confirm.py +184 -0
- chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
- chrys/app/tui/screens/dialogs/connection_test.py +183 -0
- chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
- chrys/app/tui/screens/dialogs/diagram.py +102 -0
- chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
- chrys/app/tui/screens/dialogs/editor.py +378 -0
- chrys/app/tui/screens/dialogs/editor.tcss +132 -0
- chrys/app/tui/screens/dialogs/file_picker.py +582 -0
- chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
- chrys/app/tui/screens/dialogs/fork_session.py +219 -0
- chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
- chrys/app/tui/screens/dialogs/image_compression.py +102 -0
- chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
- chrys/app/tui/screens/dialogs/man_page.py +172 -0
- chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
- chrys/app/tui/screens/dialogs/new_folder.py +277 -0
- chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
- chrys/app/tui/screens/dialogs/notice.tcss +14 -0
- chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
- chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
- chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
- chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
- chrys/app/tui/screens/dialogs/session_title.py +110 -0
- chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
- chrys/app/tui/screens/dialogs/tool_view.py +207 -0
- chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
- chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
- chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
- chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
- chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
- chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
- chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
- chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
- chrys/app/tui/screens/diff/__init__.py +22 -0
- chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
- chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
- chrys/app/tui/screens/diff/screen.py +1532 -0
- chrys/app/tui/screens/diff/screen.tcss +104 -0
- chrys/app/tui/screens/guides/__init__.py +7 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
- chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/index.py +215 -0
- chrys/app/tui/screens/guides/reader.py +173 -0
- chrys/app/tui/screens/guides/screen.py +442 -0
- chrys/app/tui/screens/guides/screen.tcss +52 -0
- chrys/app/tui/screens/logs/__init__.py +7 -0
- chrys/app/tui/screens/logs/screen.py +504 -0
- chrys/app/tui/screens/logs/screen.tcss +45 -0
- chrys/app/tui/screens/main/__init__.py +7 -0
- chrys/app/tui/screens/main/action_availability.py +30 -0
- chrys/app/tui/screens/main/buddy_command.py +143 -0
- chrys/app/tui/screens/main/commands.py +1073 -0
- chrys/app/tui/screens/main/config_actions.py +464 -0
- chrys/app/tui/screens/main/copy_actions.py +117 -0
- chrys/app/tui/screens/main/dialog_controllers.py +826 -0
- chrys/app/tui/screens/main/dialog_gateway.py +166 -0
- chrys/app/tui/screens/main/diff_controller.py +478 -0
- chrys/app/tui/screens/main/engine_read_model.py +184 -0
- chrys/app/tui/screens/main/event_handlers.py +2014 -0
- chrys/app/tui/screens/main/input_flow.py +241 -0
- chrys/app/tui/screens/main/live_diff.py +244 -0
- chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
- chrys/app/tui/screens/main/model_indicator.py +217 -0
- chrys/app/tui/screens/main/navigation.py +343 -0
- chrys/app/tui/screens/main/ports.py +571 -0
- chrys/app/tui/screens/main/recent_dirs.py +123 -0
- chrys/app/tui/screens/main/rollback_controller.py +586 -0
- chrys/app/tui/screens/main/runtime_info.py +71 -0
- chrys/app/tui/screens/main/screen.py +2265 -0
- chrys/app/tui/screens/main/screen.tcss +21 -0
- chrys/app/tui/screens/main/session_handlers.py +1127 -0
- chrys/app/tui/screens/main/session_title.py +239 -0
- chrys/app/tui/screens/main/settings_coordinator.py +365 -0
- chrys/app/tui/screens/main/settings_persistence.py +184 -0
- chrys/app/tui/screens/main/shell_mode.py +112 -0
- chrys/app/tui/screens/main/state.py +289 -0
- chrys/app/tui/screens/main/submit_state.py +11 -0
- chrys/app/tui/screens/main/subscriptions.py +177 -0
- chrys/app/tui/screens/main/suggestions.py +982 -0
- chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
- chrys/app/tui/screens/main/view_adapter.py +1567 -0
- chrys/app/tui/screens/main/workflow_browser.py +747 -0
- chrys/app/tui/screens/main/workflow_content.py +248 -0
- chrys/app/tui/screens/main/workflow_controller.py +240 -0
- chrys/app/tui/screens/main/workflow_feedback.py +137 -0
- chrys/app/tui/screens/main/workflow_flow.py +49 -0
- chrys/app/tui/screens/main/workflow_run_control.py +456 -0
- chrys/app/tui/screens/main/workflow_session_view.py +316 -0
- chrys/app/tui/screens/main/workspace_actions.py +272 -0
- chrys/app/tui/screens/main/workspace_branch.py +194 -0
- chrys/app/tui/screens/menus/__init__.py +8 -0
- chrys/app/tui/screens/menus/languages.py +114 -0
- chrys/app/tui/screens/models/__init__.py +7 -0
- chrys/app/tui/screens/models/picker.py +147 -0
- chrys/app/tui/screens/models/screen.py +2043 -0
- chrys/app/tui/screens/models/screen.tcss +255 -0
- chrys/app/tui/screens/sessions/__init__.py +8 -0
- chrys/app/tui/screens/sessions/presenter.py +586 -0
- chrys/app/tui/screens/sessions/screen.py +779 -0
- chrys/app/tui/screens/sessions/screen.tcss +127 -0
- chrys/app/tui/screens/settings/__init__.py +29 -0
- chrys/app/tui/screens/settings/danger.py +26 -0
- chrys/app/tui/screens/settings/dialog.py +281 -0
- chrys/app/tui/screens/settings/layout.py +519 -0
- chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
- chrys/app/tui/screens/settings/panes/__init__.py +3 -0
- chrys/app/tui/screens/settings/panes/notifications.py +248 -0
- chrys/app/tui/screens/settings/panes/sessions.py +148 -0
- chrys/app/tui/screens/settings/ports.py +55 -0
- chrys/app/tui/screens/settings/provenance.py +102 -0
- chrys/app/tui/screens/settings/rows.py +546 -0
- chrys/app/tui/screens/settings/settings.tcss +406 -0
- chrys/app/tui/screens/themes/__init__.py +8 -0
- chrys/app/tui/screens/themes/dialogs.py +330 -0
- chrys/app/tui/screens/themes/editor.py +632 -0
- chrys/app/tui/screens/themes/editor.tcss +268 -0
- chrys/app/tui/screens/themes/messages.py +95 -0
- chrys/app/tui/screens/themes/palette.py +694 -0
- chrys/app/tui/screens/themes/panel.py +113 -0
- chrys/app/tui/screens/themes/picker.py +203 -0
- chrys/app/tui/screens/themes/save.py +191 -0
- chrys/app/tui/support/__init__.py +3 -0
- chrys/app/tui/support/file_manager.py +64 -0
- chrys/app/tui/support/gc_freeze.py +1114 -0
- chrys/app/tui/support/path_shortcuts.py +50 -0
- chrys/app/tui/support/prompt_history.py +298 -0
- chrys/app/tui/support/workspace_mru.py +476 -0
- chrys/app/tui/terminal/__init__.py +6 -0
- chrys/app/tui/terminal/_pty_backend.py +114 -0
- chrys/app/tui/terminal/emulator/__init__.py +24 -0
- chrys/app/tui/terminal/emulator/buffer.py +456 -0
- chrys/app/tui/terminal/emulator/charsets.py +113 -0
- chrys/app/tui/terminal/emulator/core.py +963 -0
- chrys/app/tui/terminal/emulator/input.py +342 -0
- chrys/app/tui/terminal/emulator/parser.py +227 -0
- chrys/app/tui/terminal/emulator/pen.py +158 -0
- chrys/app/tui/terminal/launcher.py +455 -0
- chrys/app/tui/terminal/leaked_reports.py +44 -0
- chrys/app/tui/terminal/panel.py +323 -0
- chrys/app/tui/terminal/pty_process.py +405 -0
- chrys/app/tui/terminal/rendering.py +151 -0
- chrys/app/tui/terminal/shell.py +273 -0
- chrys/app/tui/terminal/shell_integration.py +199 -0
- chrys/app/tui/terminal/title.py +224 -0
- chrys/app/tui/terminal/widget.py +654 -0
- chrys/app/tui/theme.py +422 -0
- chrys/app/tui/theme_loader.py +201 -0
- chrys/app/tui/theme_stylesheet.py +49 -0
- chrys/app/tui/themes/__init__.py +3 -0
- chrys/app/tui/themes/document.py +120 -0
- chrys/app/tui/themes/preview.py +170 -0
- chrys/app/tui/themes/store.py +176 -0
- chrys/app/tui/toasts.py +69 -0
- chrys/app/tui/util/__init__.py +3 -0
- chrys/app/tui/util/context_pressure.py +146 -0
- chrys/app/tui/util/debug_log.py +292 -0
- chrys/app/tui/util/diff_entries.py +64 -0
- chrys/app/tui/util/formatting.py +53 -0
- chrys/app/tui/util/git_branch.py +326 -0
- chrys/app/tui/util/invocation_progress.py +57 -0
- chrys/app/tui/util/logo.py +32 -0
- chrys/app/tui/util/message_gate.py +35 -0
- chrys/app/tui/util/mutation_sources.py +14 -0
- chrys/app/tui/util/picker_text.py +27 -0
- chrys/app/tui/util/removal.py +47 -0
- chrys/app/tui/util/rich_style.py +36 -0
- chrys/app/tui/util/source_text.py +12 -0
- chrys/app/tui/util/static_update.py +88 -0
- chrys/app/tui/util/todo_format.py +51 -0
- chrys/app/tui/util/visibility.py +214 -0
- chrys/app/tui/widgets/__init__.py +60 -0
- chrys/app/tui/widgets/ask_user_controls.py +652 -0
- chrys/app/tui/widgets/ask_user_markdown.py +17 -0
- chrys/app/tui/widgets/ask_user_prompt.py +606 -0
- chrys/app/tui/widgets/buttons.py +52 -0
- chrys/app/tui/widgets/chat/__init__.py +3 -0
- chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
- chrys/app/tui/widgets/chat/chrome.py +296 -0
- chrys/app/tui/widgets/chat/compaction_card.py +353 -0
- chrys/app/tui/widgets/chat/context_fold.py +77 -0
- chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
- chrys/app/tui/widgets/chat/image_preview.py +214 -0
- chrys/app/tui/widgets/chat/live_renderer.py +383 -0
- chrys/app/tui/widgets/chat/messages.py +1039 -0
- chrys/app/tui/widgets/chat/panel.py +1769 -0
- chrys/app/tui/widgets/chat/ports.py +245 -0
- chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
- chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
- chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
- chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
- chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
- chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
- chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
- chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
- chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
- chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
- chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
- chrys/app/tui/widgets/chat/renderers/search.py +348 -0
- chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
- chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
- chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
- chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
- chrys/app/tui/widgets/chat/renderers/web.py +69 -0
- chrys/app/tui/widgets/chat/replay.py +1605 -0
- chrys/app/tui/widgets/chat/replay_mount.py +322 -0
- chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
- chrys/app/tui/widgets/chat/selection_controller.py +435 -0
- chrys/app/tui/widgets/chat/session_json.py +508 -0
- chrys/app/tui/widgets/chat/toc_model.py +156 -0
- chrys/app/tui/widgets/chat/tool_call.py +2478 -0
- chrys/app/tui/widgets/chat/tool_registry.py +753 -0
- chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
- chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
- chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
- chrys/app/tui/widgets/checkbox.py +24 -0
- chrys/app/tui/widgets/chrome/__init__.py +3 -0
- chrys/app/tui/widgets/chrome/app_header.py +283 -0
- chrys/app/tui/widgets/chrome/commands.py +95 -0
- chrys/app/tui/widgets/chrome/file_index.py +68 -0
- chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
- chrys/app/tui/widgets/chrome/footer.py +284 -0
- chrys/app/tui/widgets/chrome/image_paste.py +351 -0
- chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
- chrys/app/tui/widgets/chrome/status_bar.py +911 -0
- chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
- chrys/app/tui/widgets/click_affordance.py +24 -0
- chrys/app/tui/widgets/color_picker/__init__.py +254 -0
- chrys/app/tui/widgets/color_picker/controls.py +171 -0
- chrys/app/tui/widgets/color_picker/model.py +89 -0
- chrys/app/tui/widgets/dialog_buttons.py +58 -0
- chrys/app/tui/widgets/dialog_scroll.py +24 -0
- chrys/app/tui/widgets/diff_view/__init__.py +20 -0
- chrys/app/tui/widgets/diff_view/cells.py +59 -0
- chrys/app/tui/widgets/diff_view/code.py +280 -0
- chrys/app/tui/widgets/diff_view/compute.py +286 -0
- chrys/app/tui/widgets/diff_view/gutter.py +161 -0
- chrys/app/tui/widgets/diff_view/inline.py +77 -0
- chrys/app/tui/widgets/diff_view/palette.py +87 -0
- chrys/app/tui/widgets/diff_view/protocols.py +24 -0
- chrys/app/tui/widgets/diff_view/rows.py +138 -0
- chrys/app/tui/widgets/diff_view/unified.py +267 -0
- chrys/app/tui/widgets/diff_view/widget.py +209 -0
- chrys/app/tui/widgets/editor/__init__.py +79 -0
- chrys/app/tui/widgets/editor/coordinates.py +30 -0
- chrys/app/tui/widgets/editor/highlighter.py +139 -0
- chrys/app/tui/widgets/editor/keymaps.py +357 -0
- chrys/app/tui/widgets/editor/types.py +216 -0
- chrys/app/tui/widgets/editor/vim.py +1195 -0
- chrys/app/tui/widgets/editor/widget.py +401 -0
- chrys/app/tui/widgets/file_preview.py +58 -0
- chrys/app/tui/widgets/hatch.py +138 -0
- chrys/app/tui/widgets/input.py +66 -0
- chrys/app/tui/widgets/loading.py +125 -0
- chrys/app/tui/widgets/markdown/__init__.py +25 -0
- chrys/app/tui/widgets/markdown/_utils.py +21 -0
- chrys/app/tui/widgets/markdown/anchors.py +71 -0
- chrys/app/tui/widgets/markdown/blocks.py +115 -0
- chrys/app/tui/widgets/markdown/copyable.py +163 -0
- chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
- chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
- chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
- chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
- chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
- chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
- chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
- chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
- chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
- chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
- chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
- chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
- chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
- chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
- chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
- chrys/app/tui/widgets/markdown/links.py +47 -0
- chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
- chrys/app/tui/widgets/markdown/math/layout.py +739 -0
- chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
- chrys/app/tui/widgets/markdown/math/parser.py +325 -0
- chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
- chrys/app/tui/widgets/markdown/navigator.py +80 -0
- chrys/app/tui/widgets/markdown/parser.py +1057 -0
- chrys/app/tui/widgets/markdown/stream.py +73 -0
- chrys/app/tui/widgets/markdown/toc.py +98 -0
- chrys/app/tui/widgets/markdown/viewer.py +146 -0
- chrys/app/tui/widgets/markdown/widget.py +2046 -0
- chrys/app/tui/widgets/marquee.py +101 -0
- chrys/app/tui/widgets/option_menu.py +82 -0
- chrys/app/tui/widgets/page_navigator.py +118 -0
- chrys/app/tui/widgets/select.py +196 -0
- chrys/app/tui/widgets/selection.py +59 -0
- chrys/app/tui/widgets/sidebar/__init__.py +3 -0
- chrys/app/tui/widgets/sidebar/buddy.py +367 -0
- chrys/app/tui/widgets/sidebar/context.py +482 -0
- chrys/app/tui/widgets/sidebar/debug.py +195 -0
- chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
- chrys/app/tui/widgets/sidebar/panel.py +198 -0
- chrys/app/tui/widgets/sidebar/tasks.py +198 -0
- chrys/app/tui/widgets/sidebar/toc.py +166 -0
- chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
- chrys/app/tui/widgets/syntax_theme.py +85 -0
- chrys/app/tui/widgets/text_area.py +258 -0
- chrys/app/tui/widgets/trajectory/__init__.py +8 -0
- chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
- chrys/app/tui/widgets/trajectory/insights.py +1266 -0
- chrys/app/tui/widgets/trajectory/overview.py +1101 -0
- chrys/app/tui/widgets/trajectory/panel.py +983 -0
- chrys/app/tui/widgets/trajectory/presentation.py +311 -0
- chrys/app/tui/widgets/trajectory/session_info.py +124 -0
- chrys/app/tui/widgets/trajectory/text_view.py +124 -0
- chrys/app/tui/widgets/trajectory/timeline.py +380 -0
- chrys/app/tui/widgets/welcome.py +126 -0
- chrys/app/tui/widgets/workflow/__init__.py +5 -0
- chrys/app/tui/widgets/workflow/edges.py +89 -0
- chrys/app/tui/widgets/workflow/graph.py +869 -0
- chrys/app/tui/widgets/workflow/info.py +310 -0
- chrys/app/tui/widgets/workflow/node_view.py +27 -0
- chrys/app/tui/widgets/workflow/output.py +225 -0
- chrys/app/tui/widgets/workflow/panel.py +658 -0
- chrys/app/tui/widgets/workflow/projector.py +324 -0
- chrys/app/tui/widgets/workflow/records.py +69 -0
- chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
- chrys/app/tui/widgets/workflow/selection.py +151 -0
- chrys/app/tui/widgets/workflow/source.py +65 -0
- chrys/app/tui/widgets/workflow/text.py +257 -0
- chrys/app/tui/widgets/workflow/values.py +317 -0
- chrys/foundation/__init__.py +0 -0
- chrys/foundation/branding.py +15 -0
- chrys/foundation/config/__init__.py +7 -0
- chrys/foundation/config/coercion.py +374 -0
- chrys/foundation/config/context.py +34 -0
- chrys/foundation/config/env_file.py +141 -0
- chrys/foundation/config/env_layers.py +370 -0
- chrys/foundation/config/migrations.py +521 -0
- chrys/foundation/config/notification_events.py +22 -0
- chrys/foundation/config/process_settings.py +332 -0
- chrys/foundation/config/runtime_pointer.py +133 -0
- chrys/foundation/config/settings.py +1952 -0
- chrys/foundation/config/settings_store.py +996 -0
- chrys/foundation/config/spec.py +396 -0
- chrys/foundation/config/user_settings.py +140 -0
- chrys/foundation/config/warnings.py +262 -0
- chrys/foundation/config/web_values.py +135 -0
- chrys/foundation/config/yaml_store.py +487 -0
- chrys/foundation/errors/__init__.py +68 -0
- chrys/foundation/errors/_legacy.py +117 -0
- chrys/foundation/errors/_text.py +13 -0
- chrys/foundation/errors/_walk.py +85 -0
- chrys/foundation/errors/classify.py +278 -0
- chrys/foundation/errors/display.py +274 -0
- chrys/foundation/errors/formatting.py +283 -0
- chrys/foundation/errors/kinds.py +67 -0
- chrys/foundation/errors/network.py +497 -0
- chrys/foundation/errors/provider.py +445 -0
- chrys/foundation/errors/route.py +85 -0
- chrys/foundation/events/__init__.py +8 -0
- chrys/foundation/events/bus.py +143 -0
- chrys/foundation/events/types.py +1629 -0
- chrys/foundation/events/workflow.py +38 -0
- chrys/foundation/hosted_tools.py +142 -0
- chrys/foundation/i18n/__init__.py +32 -0
- chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
- chrys/foundation/i18n/catalogs.py +42 -0
- chrys/foundation/i18n/formatting.py +202 -0
- chrys/foundation/i18n/locale.py +92 -0
- chrys/foundation/i18n/localizer.py +187 -0
- chrys/foundation/i18n/messages.py +268 -0
- chrys/foundation/io/__init__.py +3 -0
- chrys/foundation/io/result_content.py +237 -0
- chrys/foundation/models/__init__.py +3 -0
- chrys/foundation/models/approval_reuse.py +23 -0
- chrys/foundation/models/ask_user.py +468 -0
- chrys/foundation/models/execution.py +18 -0
- chrys/foundation/models/history_markers.py +115 -0
- chrys/foundation/models/invocations.py +63 -0
- chrys/foundation/models/mutation_scope.py +28 -0
- chrys/foundation/models/session_env.py +87 -0
- chrys/foundation/models/session_surface.py +29 -0
- chrys/foundation/models/todos.py +74 -0
- chrys/foundation/models/turns.py +125 -0
- chrys/foundation/models/workflow_session.py +89 -0
- chrys/foundation/models/workspace.py +59 -0
- chrys/foundation/net/__init__.py +3 -0
- chrys/foundation/net/address_class.py +57 -0
- chrys/foundation/net/route_probe.py +94 -0
- chrys/foundation/net/url.py +107 -0
- chrys/foundation/observability/__init__.py +3 -0
- chrys/foundation/observability/exporters.py +174 -0
- chrys/foundation/observability/gate.py +31 -0
- chrys/foundation/observability/setup.py +497 -0
- chrys/foundation/observability/sink.py +228 -0
- chrys/foundation/patches/__init__.py +19 -0
- chrys/foundation/patches/mdurl_cache.py +59 -0
- chrys/foundation/patches/patcher.py +287 -0
- chrys/foundation/patches/staged_members.py +205 -0
- chrys/foundation/patches/textual_block_border.py +105 -0
- chrys/foundation/patches/textual_button_ansi.py +65 -0
- chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
- chrys/foundation/patches/textual_callback_cache.py +248 -0
- chrys/foundation/patches/textual_callback_dispatch.py +109 -0
- chrys/foundation/patches/textual_compositor_cjk.py +566 -0
- chrys/foundation/patches/textual_dispatch_cache.py +150 -0
- chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
- chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
- chrys/foundation/patches/textual_lru_acyclic.py +241 -0
- chrys/foundation/patches/textual_message_pump.py +136 -0
- chrys/foundation/patches/textual_node_diet.py +875 -0
- chrys/foundation/patches/textual_one_shot_timer.py +48 -0
- chrys/foundation/patches/textual_option_list.py +90 -0
- chrys/foundation/patches/textual_precompose.py +89 -0
- chrys/foundation/patches/textual_pruned_tabs.py +69 -0
- chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
- chrys/foundation/patches/textual_removed_node_caches.py +86 -0
- chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
- chrys/foundation/patches/textual_selection_extract.py +48 -0
- chrys/foundation/patches/textual_strip_cycles.py +119 -0
- chrys/foundation/patches/textual_tab_selection.py +1611 -0
- chrys/foundation/patches/textual_timer_skip.py +70 -0
- chrys/foundation/patches/textual_utf8_decoder.py +106 -0
- chrys/foundation/patches/textual_win_sleep.py +167 -0
- chrys/foundation/patches/watchdog_fsevents.py +70 -0
- chrys/foundation/patches/watchdog_windows.py +52 -0
- chrys/foundation/platform/__init__.py +556 -0
- chrys/foundation/platform/c_api.py +32 -0
- chrys/foundation/platform/child_reap.py +107 -0
- chrys/foundation/platform/command_line.py +95 -0
- chrys/foundation/platform/files.py +1470 -0
- chrys/foundation/platform/output_capture.py +113 -0
- chrys/foundation/platform/paths.py +193 -0
- chrys/foundation/platform/process.py +1468 -0
- chrys/foundation/platform/pty_output.py +19 -0
- chrys/foundation/platform/runtime_paths.py +281 -0
- chrys/foundation/platform/win_console.py +129 -0
- chrys/foundation/reasoning_origin.py +67 -0
- chrys/foundation/recovery.py +16 -0
- chrys/foundation/retry.py +278 -0
- chrys/foundation/text/__init__.py +3 -0
- chrys/foundation/text/encoding.py +603 -0
- chrys/foundation/text/images.py +304 -0
- chrys/foundation/text/lines.py +24 -0
- chrys/foundation/text/mentions.py +90 -0
- chrys/foundation/text/model_json.py +25 -0
- chrys/foundation/text/reading_order.py +20 -0
- chrys/foundation/text/tokenizer.py +25 -0
- chrys/foundation/text/tool_output.py +287 -0
- chrys/foundation/text/yaml_io.py +62 -0
- chrys/foundation/tool_call_context.py +202 -0
- chrys/foundation/tool_execution_stamp.py +184 -0
- chrys/foundation/tool_invocation_order.py +48 -0
- chrys/foundation/tool_kinds.py +102 -0
- chrys/foundation/tool_result_metadata.py +287 -0
- chrys/foundation/trajectory/__init__.py +10 -0
- chrys/foundation/trajectory/context.py +481 -0
- chrys/foundation/trajectory/envelope.py +571 -0
- chrys/foundation/trajectory/event_types.py +294 -0
- chrys/foundation/trajectory/finalizers.py +46 -0
- chrys/foundation/trajectory/fingerprint.py +62 -0
- chrys/foundation/trajectory/ids.py +155 -0
- chrys/foundation/trajectory/keys.py +99 -0
- chrys/foundation/trajectory/lease.py +173 -0
- chrys/foundation/trajectory/metadata.py +72 -0
- chrys/foundation/trajectory/reader.py +271 -0
- chrys/foundation/trajectory/recovery.py +225 -0
- chrys/foundation/trajectory/revisions.py +152 -0
- chrys/foundation/trajectory/segments.py +225 -0
- chrys/foundation/trajectory/tools.py +117 -0
- chrys/foundation/trajectory/usage.py +151 -0
- chrys/foundation/trajectory/writer.py +972 -0
- chrys/foundation/trajectory_timing.py +97 -0
- chrys/foundation/util/__init__.py +3 -0
- chrys/foundation/util/async_tasks.py +29 -0
- chrys/foundation/util/chrys_headers.py +23 -0
- chrys/foundation/util/env_templates.py +104 -0
- chrys/foundation/util/filenames.py +96 -0
- chrys/foundation/util/header_charset.py +138 -0
- chrys/foundation/util/httpx_helpers.py +12 -0
- chrys/foundation/util/lock.py +236 -0
- chrys/foundation/util/once_close.py +46 -0
- chrys/foundation/util/session_ids.py +29 -0
- chrys/foundation/util/shell_tokens.py +88 -0
- chrys/foundation/util/sub_agent_context.py +38 -0
- chrys/foundation/util/time.py +35 -0
- chrys/foundation/util/unicode_scalars.py +36 -0
- chrys/foundation/vendor/__init__.py +43 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
- chrys/kernel/__init__.py +256 -0
- chrys/kernel/_content.py +1866 -0
- chrys/kernel/_loop_recorder.py +631 -0
- chrys/kernel/_null_overlay.py +1026 -0
- chrys/kernel/_result_ceiling.py +216 -0
- chrys/kernel/_serialization.py +431 -0
- chrys/kernel/_tool_arg_errors.py +1629 -0
- chrys/kernel/_tool_execution.py +1172 -0
- chrys/kernel/_tool_expansion.py +26 -0
- chrys/kernel/_types.py +2598 -0
- chrys/kernel/agent.py +994 -0
- chrys/kernel/client.py +1084 -0
- chrys/kernel/compaction.py +1315 -0
- chrys/kernel/exceptions.py +211 -0
- chrys/kernel/exchanges.py +683 -0
- chrys/kernel/identity.py +95 -0
- chrys/kernel/images.py +36 -0
- chrys/kernel/instrumentation.py +1618 -0
- chrys/kernel/loop.py +2308 -0
- chrys/kernel/middleware.py +616 -0
- chrys/kernel/sessions.py +604 -0
- chrys/kernel/tools.py +1061 -0
- chrys/kernel/types.py +126 -0
- chrys/orchestration/__init__.py +3 -0
- chrys/orchestration/engine/__init__.py +3 -0
- chrys/orchestration/engine/assembly.py +236 -0
- chrys/orchestration/engine/build/__init__.py +3 -0
- chrys/orchestration/engine/build/builder.py +1061 -0
- chrys/orchestration/engine/build/construction.py +408 -0
- chrys/orchestration/engine/build/loaded.py +139 -0
- chrys/orchestration/engine/engine.py +789 -0
- chrys/orchestration/engine/execution.py +771 -0
- chrys/orchestration/engine/loader.py +865 -0
- chrys/orchestration/engine/rollback.py +918 -0
- chrys/orchestration/engine/run/__init__.py +3 -0
- chrys/orchestration/engine/run/active_injection.py +419 -0
- chrys/orchestration/engine/run/attachments.py +519 -0
- chrys/orchestration/engine/run/bindings.py +1095 -0
- chrys/orchestration/engine/run/coordinator.py +1062 -0
- chrys/orchestration/engine/run/finalizer.py +417 -0
- chrys/orchestration/engine/run/input_refs.py +60 -0
- chrys/orchestration/engine/run/prompt_content.py +457 -0
- chrys/orchestration/engine/run/resume.py +299 -0
- chrys/orchestration/engine/run/retry.py +948 -0
- chrys/orchestration/engine/run/runner.py +625 -0
- chrys/orchestration/engine/run/runtime_skills.py +146 -0
- chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
- chrys/orchestration/engine/run/turn_hooks.py +258 -0
- chrys/orchestration/engine/run/turn_state.py +149 -0
- chrys/orchestration/engine/run/working_dir.py +50 -0
- chrys/orchestration/engine/session_lifecycle.py +2013 -0
- chrys/orchestration/engine/state/__init__.py +3 -0
- chrys/orchestration/engine/state/active_session.py +183 -0
- chrys/orchestration/engine/state/controls.py +639 -0
- chrys/orchestration/engine/state/current_agent.py +26 -0
- chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
- chrys/orchestration/engine/state/machine.py +193 -0
- chrys/orchestration/engine/state/session_writer.py +434 -0
- chrys/orchestration/engine/trajectory.py +714 -0
- chrys/orchestration/engine/usage.py +118 -0
- chrys/orchestration/invoker/__init__.py +3 -0
- chrys/orchestration/invoker/acp.py +727 -0
- chrys/orchestration/invoker/acp_protocol.py +1115 -0
- chrys/orchestration/invoker/acp_spec.py +79 -0
- chrys/orchestration/invoker/attempts.py +994 -0
- chrys/orchestration/invoker/child_compaction.py +131 -0
- chrys/orchestration/invoker/child_history.py +99 -0
- chrys/orchestration/invoker/contracts.py +253 -0
- chrys/orchestration/invoker/evidence.py +94 -0
- chrys/orchestration/invoker/kernel.py +343 -0
- chrys/orchestration/invoker/origin.py +79 -0
- chrys/orchestration/invoker/resources.py +343 -0
- chrys/orchestration/invoker/runtime.py +299 -0
- chrys/orchestration/invoker/usage.py +80 -0
- chrys/orchestration/session_hooks.py +117 -0
- chrys/orchestration/session_host.py +1023 -0
- chrys/orchestration/session_resources.py +95 -0
- chrys/orchestration/session_usage.py +219 -0
- chrys/orchestration/startup.py +255 -0
- chrys/orchestration/sub_agents/__init__.py +3 -0
- chrys/orchestration/sub_agents/acp_policy.py +269 -0
- chrys/orchestration/sub_agents/kernel_policy.py +848 -0
- chrys/orchestration/sub_agents/registration.py +44 -0
- chrys/orchestration/sub_agents/shell.py +337 -0
- chrys/orchestration/sub_agents/tools.py +2228 -0
- chrys/orchestration/workflows/__init__.py +3 -0
- chrys/orchestration/workflows/agent_archive.py +156 -0
- chrys/orchestration/workflows/agent_node.py +894 -0
- chrys/orchestration/workflows/agent_node_build.py +687 -0
- chrys/orchestration/workflows/catalog.py +332 -0
- chrys/orchestration/workflows/coordinator.py +771 -0
- chrys/orchestration/workflows/hooks.py +176 -0
- chrys/orchestration/workflows/preview.py +322 -0
- chrys/orchestration/workflows/rollback.py +151 -0
- chrys/orchestration/workflows/runner.py +986 -0
- chrys/orchestration/workflows/session.py +390 -0
- chrys/orchestration/workflows/settings.py +97 -0
- chrys/orchestration/workflows/validation.py +674 -0
- chrys/orchestration/workflows/worker_client.py +1002 -0
- chrys/service/__init__.py +3 -0
- chrys/service/acp_client/__init__.py +65 -0
- chrys/service/acp_client/_transport.py +922 -0
- chrys/service/acp_client/client.py +1311 -0
- chrys/service/acp_client/errors.py +224 -0
- chrys/service/acp_client/protocol.py +220 -0
- chrys/service/acp_client/spawn.py +239 -0
- chrys/service/acp_client/spec.py +100 -0
- chrys/service/agent_middleware/__init__.py +60 -0
- chrys/service/agent_middleware/_metadata_keys.py +52 -0
- chrys/service/agent_middleware/control/__init__.py +3 -0
- chrys/service/agent_middleware/control/approval.py +769 -0
- chrys/service/agent_middleware/control/approval_hooks.py +62 -0
- chrys/service/agent_middleware/control/ask_user.py +207 -0
- chrys/service/agent_middleware/control/interrupt.py +50 -0
- chrys/service/agent_middleware/control/sleep.py +118 -0
- chrys/service/agent_middleware/control/todo.py +117 -0
- chrys/service/agent_middleware/events/__init__.py +3 -0
- chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
- chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
- chrys/service/agent_middleware/events/intermediate_text.py +128 -0
- chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
- chrys/service/agent_middleware/events/result_persistence.py +98 -0
- chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
- chrys/service/agent_middleware/events/tool_error_text.py +22 -0
- chrys/service/agent_middleware/events/tool_events.py +779 -0
- chrys/service/agent_middleware/injection.py +510 -0
- chrys/service/agent_middleware/reminders/__init__.py +91 -0
- chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
- chrys/service/agent_middleware/reminders/base.py +25 -0
- chrys/service/agent_middleware/reminders/context_usage.py +74 -0
- chrys/service/agent_middleware/reminders/file_change.py +63 -0
- chrys/service/agent_middleware/reminders/mcp.py +39 -0
- chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
- chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
- chrys/service/agent_middleware/reminders/skills.py +44 -0
- chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
- chrys/service/agent_middleware/reminders/todo.py +35 -0
- chrys/service/agent_middleware/reminders/turn_line.py +48 -0
- chrys/service/agent_middleware/response_validation.py +1380 -0
- chrys/service/agent_middleware/system_reminder.py +1107 -0
- chrys/service/agent_middleware/validators.py +396 -0
- chrys/service/analytics/__init__.py +124 -0
- chrys/service/analytics/_actions.py +896 -0
- chrys/service/analytics/_context_evidence.py +357 -0
- chrys/service/analytics/_critical_path.py +179 -0
- chrys/service/analytics/_facts.py +919 -0
- chrys/service/analytics/_insights.py +769 -0
- chrys/service/analytics/_metric_ops.py +83 -0
- chrys/service/analytics/_session_projection.py +460 -0
- chrys/service/analytics/_timeline.py +434 -0
- chrys/service/analytics/_turn_graph.py +1237 -0
- chrys/service/analytics/_turns.py +1377 -0
- chrys/service/analytics/_workflows.py +171 -0
- chrys/service/analytics/aggregation.py +1474 -0
- chrys/service/analytics/classification.py +128 -0
- chrys/service/analytics/export.py +803 -0
- chrys/service/analytics/findings.py +282 -0
- chrys/service/analytics/math.py +79 -0
- chrys/service/analytics/model.py +886 -0
- chrys/service/analytics/reader.py +321 -0
- chrys/service/approval/__init__.py +3 -0
- chrys/service/approval/arbitration.py +102 -0
- chrys/service/approval/command_identity.py +109 -0
- chrys/service/approval/correlation.py +57 -0
- chrys/service/approval/grant_store.py +108 -0
- chrys/service/approval/judge.py +589 -0
- chrys/service/approval/level.py +15 -0
- chrys/service/approval/policy.py +209 -0
- chrys/service/approval/reuse.py +223 -0
- chrys/service/approval/reuse_binding.py +133 -0
- chrys/service/approval/safety_classifier.py +536 -0
- chrys/service/approval/turn_context.py +49 -0
- chrys/service/context/__init__.py +3 -0
- chrys/service/context/compaction/__init__.py +38 -0
- chrys/service/context/compaction/breaker.py +156 -0
- chrys/service/context/compaction/budgets.py +93 -0
- chrys/service/context/compaction/calibration.py +77 -0
- chrys/service/context/compaction/compression.py +690 -0
- chrys/service/context/compaction/current_turn_drop.py +458 -0
- chrys/service/context/compaction/events.py +201 -0
- chrys/service/context/compaction/exclusions.py +261 -0
- chrys/service/context/compaction/groups.py +361 -0
- chrys/service/context/compaction/last_words.py +1996 -0
- chrys/service/context/compaction/last_words_state.py +752 -0
- chrys/service/context/compaction/scoped.py +472 -0
- chrys/service/context/compaction/spill.py +1947 -0
- chrys/service/context/compaction/strategy.py +1116 -0
- chrys/service/context/compaction/summaries.py +319 -0
- chrys/service/context/compaction/turns.py +375 -0
- chrys/service/context/manager.py +174 -0
- chrys/service/context/memory_loader.py +792 -0
- chrys/service/context/middleware/__init__.py +3 -0
- chrys/service/context/middleware/usage.py +332 -0
- chrys/service/context/providers/__init__.py +3 -0
- chrys/service/context/providers/context_management.py +432 -0
- chrys/service/context/providers/history.py +647 -0
- chrys/service/context/providers/memory.py +50 -0
- chrys/service/context/tool_content.py +22 -0
- chrys/service/hooks/__init__.py +37 -0
- chrys/service/hooks/detached_worker.py +118 -0
- chrys/service/hooks/docs/README.md +14 -0
- chrys/service/hooks/docs/authoring.md +684 -0
- chrys/service/hooks/docs/configuration.md +499 -0
- chrys/service/hooks/docs/design.md +359 -0
- chrys/service/hooks/docs/project-hooks.md +348 -0
- chrys/service/hooks/events.py +157 -0
- chrys/service/hooks/loader.py +665 -0
- chrys/service/hooks/manager.py +807 -0
- chrys/service/hooks/matcher.py +111 -0
- chrys/service/hooks/outbox.py +294 -0
- chrys/service/hooks/runner.py +572 -0
- chrys/service/hooks/schema.py +407 -0
- chrys/service/llm/__init__.py +7 -0
- chrys/service/llm/_structured_outputs.py +192 -0
- chrys/service/llm/anthropic_messages/__init__.py +9 -0
- chrys/service/llm/anthropic_messages/client.py +254 -0
- chrys/service/llm/anthropic_messages/decode.py +331 -0
- chrys/service/llm/anthropic_messages/history.py +279 -0
- chrys/service/llm/anthropic_messages/request.py +323 -0
- chrys/service/llm/anthropic_messages/server_tools.py +319 -0
- chrys/service/llm/anthropic_messages/stream.py +297 -0
- chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
- chrys/service/llm/chat_completions/__init__.py +9 -0
- chrys/service/llm/chat_completions/client.py +369 -0
- chrys/service/llm/chat_completions/decode.py +338 -0
- chrys/service/llm/chat_completions/history.py +657 -0
- chrys/service/llm/chat_completions/reasoning.py +244 -0
- chrys/service/llm/chat_completions/request.py +192 -0
- chrys/service/llm/chat_completions/stream.py +546 -0
- chrys/service/llm/chat_completions/validation.py +308 -0
- chrys/service/llm/clients.py +613 -0
- chrys/service/llm/images.py +65 -0
- chrys/service/llm/mock.py +413 -0
- chrys/service/llm/observer.py +446 -0
- chrys/service/llm/one_shot.py +106 -0
- chrys/service/llm/openai_exceptions.py +93 -0
- chrys/service/llm/openai_responses/__init__.py +10 -0
- chrys/service/llm/openai_responses/client.py +323 -0
- chrys/service/llm/openai_responses/decode.py +432 -0
- chrys/service/llm/openai_responses/history.py +519 -0
- chrys/service/llm/openai_responses/hosted.py +625 -0
- chrys/service/llm/openai_responses/replay.py +629 -0
- chrys/service/llm/openai_responses/request.py +342 -0
- chrys/service/llm/openai_responses/stream.py +847 -0
- chrys/service/llm/openai_timestamps.py +57 -0
- chrys/service/llm/providers.py +107 -0
- chrys/service/llm/proxy_route.py +54 -0
- chrys/service/llm/raw_http_log.py +309 -0
- chrys/service/llm/route_facts.py +63 -0
- chrys/service/llm/route_sessions.py +49 -0
- chrys/service/llm/sdk_anthropic.py +18 -0
- chrys/service/llm/sdk_headers.py +47 -0
- chrys/service/llm/sdk_openai.py +18 -0
- chrys/service/llm/sdk_retry.py +59 -0
- chrys/service/llm/wire_client.py +246 -0
- chrys/service/mcp/__init__.py +3 -0
- chrys/service/mcp/_connection.py +235 -0
- chrys/service/mcp/_http_transport.py +350 -0
- chrys/service/mcp/_progressive.py +459 -0
- chrys/service/mcp/_stdio_transport.py +614 -0
- chrys/service/mcp/_tool_mixins.py +96 -0
- chrys/service/mcp/adapter.py +1031 -0
- chrys/service/mcp/cache.py +476 -0
- chrys/service/mcp/content.py +32 -0
- chrys/service/mcp/errors.py +169 -0
- chrys/service/mcp/owned.py +1330 -0
- chrys/service/mcp/result_limits.py +168 -0
- chrys/service/mcp/thinking_warning.py +95 -0
- chrys/service/mcp/validation.py +74 -0
- chrys/service/mutations/__init__.py +84 -0
- chrys/service/mutations/coordination.py +894 -0
- chrys/service/mutations/detector.py +458 -0
- chrys/service/mutations/git_calibrator.py +397 -0
- chrys/service/mutations/git_state.py +647 -0
- chrys/service/mutations/pipeline.py +779 -0
- chrys/service/mutations/scanner.py +466 -0
- chrys/service/mutations/snapshot_files.py +233 -0
- chrys/service/mutations/store.py +625 -0
- chrys/service/mutations/tool_names.py +15 -0
- chrys/service/mutations/trace.py +246 -0
- chrys/service/mutations/tracker.py +1289 -0
- chrys/service/mutations/types.py +559 -0
- chrys/service/mutations/workspace_changes.py +1583 -0
- chrys/service/profiles/__init__.py +2 -0
- chrys/service/profiles/agents/__init__.py +3 -0
- chrys/service/profiles/agents/builtins/Code.yaml +199 -0
- chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
- chrys/service/profiles/agents/builtins/General.yaml +114 -0
- chrys/service/profiles/agents/builtins/QA.yaml +128 -0
- chrys/service/profiles/agents/loader.py +808 -0
- chrys/service/profiles/agents/registry.py +727 -0
- chrys/service/profiles/agents/schema.py +371 -0
- chrys/service/profiles/agents/serializer.py +196 -0
- chrys/service/profiles/coercion.py +22 -0
- chrys/service/profiles/models/__init__.py +3 -0
- chrys/service/profiles/models/env_bridge.py +157 -0
- chrys/service/profiles/models/loader.py +282 -0
- chrys/service/profiles/models/options.py +438 -0
- chrys/service/profiles/models/registry.py +104 -0
- chrys/service/profiles/models/resolver.py +286 -0
- chrys/service/profiles/models/schema.py +91 -0
- chrys/service/profiles/models/serializer.py +76 -0
- chrys/service/session/__init__.py +3 -0
- chrys/service/session/checkpoint.py +156 -0
- chrys/service/session/history.py +1616 -0
- chrys/service/session/message_metadata.py +151 -0
- chrys/service/session/persistence.py +435 -0
- chrys/service/session/runtime_metadata.py +210 -0
- chrys/service/session/sub_agent_logs.py +1069 -0
- chrys/service/session/sub_agent_transcript.py +654 -0
- chrys/service/skills/__init__.py +3 -0
- chrys/service/skills/adapter.py +408 -0
- chrys/service/skills/constants.py +8 -0
- chrys/service/skills/loader.py +575 -0
- chrys/service/skills/model.py +130 -0
- chrys/service/skills/provider.py +841 -0
- chrys/service/skills/runner.py +390 -0
- chrys/service/state/__init__.py +3 -0
- chrys/service/state/_fork.py +710 -0
- chrys/service/state/_session_files.py +175 -0
- chrys/service/state/_session_meta.py +552 -0
- chrys/service/state/locks.py +154 -0
- chrys/service/state/serializers.py +156 -0
- chrys/service/state/session_catalog.py +361 -0
- chrys/service/state/session_listing.py +84 -0
- chrys/service/state/session_migration.py +446 -0
- chrys/service/state/session_mru.py +315 -0
- chrys/service/state/store.py +2132 -0
- chrys/service/state/workflow.py +158 -0
- chrys/service/text_blocks.py +47 -0
- chrys/service/todos/__init__.py +3 -0
- chrys/service/todos/reminder.py +31 -0
- chrys/service/todos/tracker.py +58 -0
- chrys/service/tools/__init__.py +3 -0
- chrys/service/tools/approval_targets.py +92 -0
- chrys/service/tools/builtins/__init__.py +3 -0
- chrys/service/tools/builtins/ask_user.py +326 -0
- chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
- chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
- chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
- chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
- chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
- chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
- chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
- chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
- chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
- chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
- chrys/service/tools/builtins/doc_converter/registry.py +55 -0
- chrys/service/tools/builtins/filesystem.py +1071 -0
- chrys/service/tools/builtins/search.py +1265 -0
- chrys/service/tools/builtins/shell.py +967 -0
- chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
- chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
- chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
- chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
- chrys/service/tools/builtins/shell_filter.py +24 -0
- chrys/service/tools/builtins/sleep.py +91 -0
- chrys/service/tools/builtins/todo.py +64 -0
- chrys/service/tools/builtins/web/__init__.py +6 -0
- chrys/service/tools/builtins/web/build.py +268 -0
- chrys/service/tools/builtins/web/config.py +338 -0
- chrys/service/tools/builtins/web/credentials.py +37 -0
- chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
- chrys/service/tools/builtins/web/fetch/cache.py +68 -0
- chrys/service/tools/builtins/web/fetch/convert.py +56 -0
- chrys/service/tools/builtins/web/fetch/tool.py +223 -0
- chrys/service/tools/builtins/web/hosted.py +61 -0
- chrys/service/tools/builtins/web/http.py +330 -0
- chrys/service/tools/builtins/web/progress.py +18 -0
- chrys/service/tools/builtins/web/search/__init__.py +3 -0
- chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
- chrys/service/tools/builtins/web/search/html.py +152 -0
- chrys/service/tools/builtins/web/search/json_api.py +227 -0
- chrys/service/tools/builtins/web/search/providers.py +77 -0
- chrys/service/tools/builtins/web/search/tool.py +218 -0
- chrys/service/tools/builtins/web/search/types.py +28 -0
- chrys/service/tools/builtins/web/tls_identity.py +66 -0
- chrys/service/tools/kinds.py +115 -0
- chrys/service/tools/names.py +47 -0
- chrys/service/tools/registry.py +242 -0
- chrys/service/tools/result_metadata.py +103 -0
- chrys/service/tools/session_artifacts.py +196 -0
- chrys/service/tools/spill.py +218 -0
- chrys/service/tools/workspace_paths.py +81 -0
- chrys/service/trajectory/__init__.py +11 -0
- chrys/service/trajectory/approvals.py +201 -0
- chrys/service/trajectory/compaction.py +420 -0
- chrys/service/trajectory/fork.py +75 -0
- chrys/service/trajectory/hooks.py +217 -0
- chrys/service/trajectory/items.py +41 -0
- chrys/service/trajectory/preparation.py +374 -0
- chrys/service/trajectory/retries.py +95 -0
- chrys/service/trajectory/revisions.py +129 -0
- chrys/service/trajectory/segmented.py +163 -0
- chrys/service/trajectory/session.py +762 -0
- chrys/service/trajectory/state.py +83 -0
- chrys/service/trajectory/sub_agents.py +169 -0
- chrys/service/trajectory/tombstone.py +262 -0
- chrys/service/trajectory/tools.py +323 -0
- chrys/service/trajectory/validation.py +130 -0
- chrys/service/trajectory/waits.py +161 -0
- chrys/service/trajectory/workflow.py +148 -0
- chrys/service/vision.py +128 -0
- chrys/service/workflows/__init__.py +3 -0
- chrys/service/workflows/admission.py +151 -0
- chrys/service/workflows/artifacts.py +109 -0
- chrys/service/workflows/asks.py +130 -0
- chrys/service/workflows/builtins/__init__.py +12 -0
- chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
- chrys/service/workflows/builtins/demo-workflow.py +756 -0
- chrys/service/workflows/discovery.py +554 -0
- chrys/service/workflows/environment.py +284 -0
- chrys/service/workflows/graph.py +430 -0
- chrys/service/workflows/history.py +82 -0
- chrys/service/workflows/interpreter.py +107 -0
- chrys/service/workflows/journal.py +334 -0
- chrys/service/workflows/layout.py +56 -0
- chrys/service/workflows/ledger.py +149 -0
- chrys/service/workflows/model_selection.py +18 -0
- chrys/service/workflows/orphans.py +209 -0
- chrys/service/workflows/outcomes.py +46 -0
- chrys/service/workflows/protocol.py +210 -0
- chrys/service/workflows/records.py +142 -0
- chrys/service/workflows/scheduler.py +770 -0
- chrys/service/workflows/sdk/__init__.py +51 -0
- chrys/service/workflows/sdk/_ask.py +167 -0
- chrys/service/workflows/sdk/_builder.py +781 -0
- chrys/service/workflows/sdk/_values.py +92 -0
- chrys/service/workflows/sdk_artifact.py +74 -0
- chrys/service/workflows/store.py +494 -0
- chrys/service/workflows/transcript.py +102 -0
- chrys/service/workflows/values.py +73 -0
- chrys/service/workflows/worker_host.py +1820 -0
- chrys/workflows.py +38 -0
- icode_tui-0.29.1.dist-info/METADATA +104 -0
- icode_tui-0.29.1.dist-info/RECORD +1138 -0
- icode_tui-0.29.1.dist-info/WHEEL +4 -0
- icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
- icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
- icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
chrys/app/acp/server.py
ADDED
|
@@ -0,0 +1,1796 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""ACP server adapter for Chrys."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import contextlib
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from math import isfinite
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from acp import PROTOCOL_VERSION, RequestError
|
|
14
|
+
from acp import schema as acp_schema
|
|
15
|
+
from acp.interfaces import Client
|
|
16
|
+
|
|
17
|
+
from chrys import __version__
|
|
18
|
+
from chrys.app.acp.bridge import (
|
|
19
|
+
AcpEventBridge,
|
|
20
|
+
acp_tool_kind,
|
|
21
|
+
plan_update_for_todos,
|
|
22
|
+
session_title_info_update,
|
|
23
|
+
tool_call_title,
|
|
24
|
+
)
|
|
25
|
+
from chrys.app.acp.content import AcpContentError, ContentBlock, convert_prompt_blocks
|
|
26
|
+
from chrys.app.acp.history import replay_session_history
|
|
27
|
+
from chrys.app.acp.session_manager import AcpSessionError, AcpSessionManager, ManagedSession, jsonable_dataclass
|
|
28
|
+
from chrys.foundation.branding import APP_DISPLAY_NAME
|
|
29
|
+
from chrys.foundation.events.types import (
|
|
30
|
+
AgentLoadFailed,
|
|
31
|
+
AgentLoadFinished,
|
|
32
|
+
AgentLoadProgress,
|
|
33
|
+
AgentLoadStarted,
|
|
34
|
+
AgentRuntimeUpdated,
|
|
35
|
+
ApprovalCancelled,
|
|
36
|
+
ApprovalModeUpdated,
|
|
37
|
+
ApprovalRequest,
|
|
38
|
+
ApprovalResponse,
|
|
39
|
+
ApprovalReviewed,
|
|
40
|
+
AskUserResponse,
|
|
41
|
+
AskUserTimedOut,
|
|
42
|
+
CompactionFinished,
|
|
43
|
+
CompactionStarted,
|
|
44
|
+
ContextCompressed,
|
|
45
|
+
Error,
|
|
46
|
+
Event,
|
|
47
|
+
InvocationAborted,
|
|
48
|
+
InvocationCascadeAborted,
|
|
49
|
+
InvocationCompactionCommitted,
|
|
50
|
+
InvocationCompactionFinished,
|
|
51
|
+
InvocationCompactionStarted,
|
|
52
|
+
InvocationContextPressure,
|
|
53
|
+
InvocationEvent,
|
|
54
|
+
InvocationPaused,
|
|
55
|
+
InvocationProgress,
|
|
56
|
+
InvocationResumed,
|
|
57
|
+
InvocationRetryAttempt,
|
|
58
|
+
InvocationStarted,
|
|
59
|
+
InvocationToolCallResult,
|
|
60
|
+
InvocationToolCallStart,
|
|
61
|
+
ProfileSwitched,
|
|
62
|
+
QuestionToUser,
|
|
63
|
+
RollbackResult,
|
|
64
|
+
SessionReady,
|
|
65
|
+
SessionRestored,
|
|
66
|
+
SessionSaved,
|
|
67
|
+
SessionTitleUpdated,
|
|
68
|
+
ToolCompacted,
|
|
69
|
+
UsageUpdate,
|
|
70
|
+
UserInjectResult,
|
|
71
|
+
Warning,
|
|
72
|
+
WorkspaceUpdated,
|
|
73
|
+
)
|
|
74
|
+
from chrys.foundation.models.ask_user import AskUserQuestion, validate_request_input_response
|
|
75
|
+
from chrys.foundation.text.encoding import EncodingDetector, decode_bytes
|
|
76
|
+
from chrys.orchestration.session_host import Cancelled, EndTurn, Errored
|
|
77
|
+
|
|
78
|
+
if TYPE_CHECKING:
|
|
79
|
+
from chrys.service.mutations.types import FileHashDiff
|
|
80
|
+
|
|
81
|
+
_ALLOW_OPTION_ID = "allow"
|
|
82
|
+
_REJECT_OPTION_ID = "reject"
|
|
83
|
+
_PERMISSION_REQUEST_TIMEOUT_SECONDS = 600.0
|
|
84
|
+
|
|
85
|
+
# Approval modes surfaced as standard ACP session modes (id, name, description).
|
|
86
|
+
_APPROVAL_MODES: tuple[tuple[str, str, str], ...] = (
|
|
87
|
+
("manual", "Manual", "Every tool call that requires approval shows a prompt."),
|
|
88
|
+
("auto", "Auto", "An LLM judge evaluates calls; only flagged ones prompt."),
|
|
89
|
+
("bypass", "Bypass", "All tool calls are auto-approved."),
|
|
90
|
+
)
|
|
91
|
+
_APPROVAL_MODE_IDS = frozenset(mode_id for mode_id, _, _ in _APPROVAL_MODES)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _request_input_question_payload(question: AskUserQuestion) -> dict[str, object]:
|
|
95
|
+
return {
|
|
96
|
+
"question": question.question,
|
|
97
|
+
"header": question.header,
|
|
98
|
+
"options": [{"label": option.label, "description": option.description} for option in question.options],
|
|
99
|
+
"multiSelect": question.multi_select,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ChrysAcpServer:
|
|
104
|
+
"""ACP request handlers backed by ``ChrysSessionHost``."""
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
manager: AcpSessionManager,
|
|
109
|
+
*,
|
|
110
|
+
initial_vision: bool,
|
|
111
|
+
permission_timeout_seconds: float = _PERMISSION_REQUEST_TIMEOUT_SECONDS,
|
|
112
|
+
) -> None:
|
|
113
|
+
if not isfinite(permission_timeout_seconds) or permission_timeout_seconds <= 0:
|
|
114
|
+
raise ValueError("permission_timeout_seconds must be finite and greater than zero.")
|
|
115
|
+
self._manager = manager
|
|
116
|
+
self._initial_vision = initial_vision
|
|
117
|
+
self._permission_timeout_seconds = permission_timeout_seconds
|
|
118
|
+
self._client: Client | None = None
|
|
119
|
+
self._pending_input_cancels: dict[tuple[str, str], asyncio.Future[None]] = {}
|
|
120
|
+
self._pending_permission_cancels: dict[tuple[str, str], asyncio.Future[None]] = {}
|
|
121
|
+
self._pending_permission_tasks: dict[tuple[str, str], asyncio.Task[acp_schema.RequestPermissionResponse]] = {}
|
|
122
|
+
self._input_cancel_tombstones: set[tuple[str, str]] = set()
|
|
123
|
+
self._permission_cancel_tombstones: set[tuple[str, str]] = set()
|
|
124
|
+
|
|
125
|
+
def on_connect(self, conn: Client) -> None:
|
|
126
|
+
"""Capture the ACP client connection used for server-to-client requests."""
|
|
127
|
+
self._client = conn
|
|
128
|
+
|
|
129
|
+
async def initialize(
|
|
130
|
+
self,
|
|
131
|
+
protocol_version: int,
|
|
132
|
+
client_capabilities: acp_schema.ClientCapabilities | None = None,
|
|
133
|
+
client_info: acp_schema.Implementation | None = None,
|
|
134
|
+
**kwargs: Any,
|
|
135
|
+
) -> acp_schema.InitializeResponse:
|
|
136
|
+
"""Return Chrys ACP capabilities."""
|
|
137
|
+
_ = client_capabilities, client_info, kwargs
|
|
138
|
+
return acp_schema.InitializeResponse(
|
|
139
|
+
protocolVersion=min(protocol_version, PROTOCOL_VERSION),
|
|
140
|
+
agentInfo=acp_schema.Implementation(name=APP_DISPLAY_NAME, version=__version__),
|
|
141
|
+
agentCapabilities=acp_schema.AgentCapabilities(
|
|
142
|
+
# Keep this explicit until the ACP SDK replaces its invalid
|
|
143
|
+
# bare-dict auth default; the TUI normalizes omitted remote
|
|
144
|
+
# values in panels/acp.py::_handshake_payload.
|
|
145
|
+
auth=None,
|
|
146
|
+
loadSession=True,
|
|
147
|
+
promptCapabilities=acp_schema.PromptCapabilities(
|
|
148
|
+
embeddedContext=True,
|
|
149
|
+
image=self._initial_vision,
|
|
150
|
+
audio=False,
|
|
151
|
+
),
|
|
152
|
+
mcpCapabilities=acp_schema.McpCapabilities(http=True, sse=False),
|
|
153
|
+
sessionCapabilities=acp_schema.SessionCapabilities(
|
|
154
|
+
close=acp_schema.SessionCloseCapabilities(),
|
|
155
|
+
list=acp_schema.SessionListCapabilities(),
|
|
156
|
+
additionalDirectories=acp_schema.SessionAdditionalDirectoriesCapabilities(),
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def _watch_session_titles(self, session: ManagedSession) -> None:
|
|
162
|
+
"""Forward ``SessionTitleUpdated`` as ``session_info_update`` for a session's lifetime.
|
|
163
|
+
|
|
164
|
+
Title generation is a post-turn side call, so its event lands after
|
|
165
|
+
the prompt's event stream has already closed — only a long-lived
|
|
166
|
+
subscription on the session's bus can deliver it live. The
|
|
167
|
+
subscription dies with the host bus when the session closes.
|
|
168
|
+
"""
|
|
169
|
+
session_id = session.session_id
|
|
170
|
+
|
|
171
|
+
async def _on_title_updated(event: SessionTitleUpdated) -> None:
|
|
172
|
+
if event.session_id != session_id or self._client is None:
|
|
173
|
+
return
|
|
174
|
+
with contextlib.suppress(Exception):
|
|
175
|
+
await self._client.session_update(
|
|
176
|
+
session_id=session_id,
|
|
177
|
+
update=session_title_info_update(event),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
await session.host.event_bus.subscribe(SessionTitleUpdated, _on_title_updated)
|
|
181
|
+
|
|
182
|
+
async def _watch_nested_wait_cancellations(self, session: ManagedSession) -> None:
|
|
183
|
+
"""Subscribe out of band so serialized prompt delivery cannot hide cancellation."""
|
|
184
|
+
session_id = session.session_id
|
|
185
|
+
|
|
186
|
+
async def _on_approval_cancelled(event: ApprovalCancelled) -> None:
|
|
187
|
+
if event.session_id != session_id:
|
|
188
|
+
return
|
|
189
|
+
self._deliver_or_record_cancellation(
|
|
190
|
+
self._pending_permission_cancels,
|
|
191
|
+
self._permission_cancel_tombstones,
|
|
192
|
+
(session_id, event.request_id),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
async def _on_ask_user_cleared(event: AskUserTimedOut) -> None:
|
|
196
|
+
if event.session_id != session_id:
|
|
197
|
+
return
|
|
198
|
+
self._deliver_or_record_cancellation(
|
|
199
|
+
self._pending_input_cancels,
|
|
200
|
+
self._input_cancel_tombstones,
|
|
201
|
+
(session_id, event.request_id),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
await session.host.event_bus.subscribe(ApprovalCancelled, _on_approval_cancelled)
|
|
205
|
+
await session.host.event_bus.subscribe(AskUserTimedOut, _on_ask_user_cleared)
|
|
206
|
+
|
|
207
|
+
async def new_session(
|
|
208
|
+
self,
|
|
209
|
+
cwd: str | None = None,
|
|
210
|
+
additional_directories: list[str] | None = None,
|
|
211
|
+
mcp_servers: list[acp_schema.HttpMcpServer | acp_schema.SseMcpServer | acp_schema.McpServerStdio] | None = None,
|
|
212
|
+
**kwargs: Any,
|
|
213
|
+
) -> acp_schema.NewSessionResponse:
|
|
214
|
+
"""Create a new Chrys session."""
|
|
215
|
+
_ = kwargs
|
|
216
|
+
# Session creation runs outside any prompt turn, so the settings
|
|
217
|
+
# warnings its load produced — a denied project key, say — have no
|
|
218
|
+
# forwarder; collect and send them once the session exists.
|
|
219
|
+
warnings: list[Warning] = []
|
|
220
|
+
try:
|
|
221
|
+
session = await self._manager.new_session(
|
|
222
|
+
cwd=cwd,
|
|
223
|
+
additional_directories=additional_directories,
|
|
224
|
+
mcp_servers=mcp_servers,
|
|
225
|
+
warnings=warnings,
|
|
226
|
+
)
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
raise _request_error(exc) from exc
|
|
229
|
+
# The session is in the active map from here on, but its id has not
|
|
230
|
+
# reached the caller yet: any failure below — the sends losing the
|
|
231
|
+
# client, or this task being cancelled — would strand a session
|
|
232
|
+
# nobody can ever address, so the just-created session is closed
|
|
233
|
+
# before the failure propagates.
|
|
234
|
+
try:
|
|
235
|
+
await self._watch_session_titles(session)
|
|
236
|
+
await self._watch_nested_wait_cancellations(session)
|
|
237
|
+
for warning in warnings:
|
|
238
|
+
await self._send_warning(session.session_id, warning)
|
|
239
|
+
await self._send_runtime_update(session.session_id)
|
|
240
|
+
await self._send_plan_update(session.session_id)
|
|
241
|
+
except BaseException:
|
|
242
|
+
with contextlib.suppress(Exception):
|
|
243
|
+
await self._manager.close(session.session_id)
|
|
244
|
+
raise
|
|
245
|
+
return acp_schema.NewSessionResponse(
|
|
246
|
+
sessionId=session.session_id,
|
|
247
|
+
modes=self._session_mode_state(session.session_id),
|
|
248
|
+
models=self._session_model_state(session.session_id),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
async def load_session(
|
|
252
|
+
self,
|
|
253
|
+
session_id: str,
|
|
254
|
+
cwd: str | None = None,
|
|
255
|
+
additional_directories: list[str] | None = None,
|
|
256
|
+
mcp_servers: list[acp_schema.HttpMcpServer | acp_schema.SseMcpServer | acp_schema.McpServerStdio] | None = None,
|
|
257
|
+
**kwargs: Any,
|
|
258
|
+
) -> acp_schema.LoadSessionResponse:
|
|
259
|
+
"""Load a saved Chrys session."""
|
|
260
|
+
_ = kwargs
|
|
261
|
+
# Same gap as ``new_session``: the load happens outside any prompt
|
|
262
|
+
# turn, so its settings warnings are only delivered if collected here.
|
|
263
|
+
warnings: list[Warning] = []
|
|
264
|
+
try:
|
|
265
|
+
result = await self._manager.load_session(
|
|
266
|
+
cwd=cwd,
|
|
267
|
+
session_id=session_id,
|
|
268
|
+
additional_directories=additional_directories,
|
|
269
|
+
mcp_servers=mcp_servers,
|
|
270
|
+
warnings=warnings,
|
|
271
|
+
)
|
|
272
|
+
session = result.session
|
|
273
|
+
# One cleanup scope for everything after the load: a session this
|
|
274
|
+
# request created must not stay in the active map when any of the
|
|
275
|
+
# follow-up sends — warnings included — loses the client, nor when
|
|
276
|
+
# the request task is cancelled mid-establishment.
|
|
277
|
+
try:
|
|
278
|
+
if not result.reused_existing:
|
|
279
|
+
# A reused active session already has a title watcher from
|
|
280
|
+
# the request that created it.
|
|
281
|
+
await self._watch_session_titles(session)
|
|
282
|
+
await self._watch_nested_wait_cancellations(session)
|
|
283
|
+
if self._client is not None:
|
|
284
|
+
for warning in warnings:
|
|
285
|
+
await self._send_warning(session.session_id, warning)
|
|
286
|
+
await self._send_session_restored(session.session_id)
|
|
287
|
+
await replay_session_history(
|
|
288
|
+
self._client,
|
|
289
|
+
self._manager.state_store,
|
|
290
|
+
session.session_id,
|
|
291
|
+
prefer_recovery=result.recovered_from_sidecar,
|
|
292
|
+
tool_kind_resolver=self._manager.tool_kind_resolver(session.session_id),
|
|
293
|
+
)
|
|
294
|
+
await self._send_runtime_update(session.session_id)
|
|
295
|
+
await self._send_plan_update(session.session_id)
|
|
296
|
+
except BaseException:
|
|
297
|
+
if not result.reused_existing:
|
|
298
|
+
with contextlib.suppress(Exception):
|
|
299
|
+
await self._manager.close(session.session_id)
|
|
300
|
+
raise
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
raise _request_error(exc) from exc
|
|
303
|
+
return acp_schema.LoadSessionResponse(
|
|
304
|
+
modes=self._session_mode_state(session.session_id),
|
|
305
|
+
models=self._session_model_state(session.session_id),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
async def list_sessions(
|
|
309
|
+
self,
|
|
310
|
+
additional_directories: list[str] | None = None,
|
|
311
|
+
cursor: str | None = None,
|
|
312
|
+
cwd: str | None = None,
|
|
313
|
+
**kwargs: Any,
|
|
314
|
+
) -> acp_schema.ListSessionsResponse:
|
|
315
|
+
"""List saved sessions for this workspace cwd."""
|
|
316
|
+
_ = kwargs
|
|
317
|
+
try:
|
|
318
|
+
sessions, next_cursor = await self._manager.list_sessions(
|
|
319
|
+
cwd=cwd, cursor=cursor, additional_directories=additional_directories
|
|
320
|
+
)
|
|
321
|
+
except Exception as exc:
|
|
322
|
+
raise _request_error(exc) from exc
|
|
323
|
+
return acp_schema.ListSessionsResponse(sessions=sessions, nextCursor=next_cursor)
|
|
324
|
+
|
|
325
|
+
async def prompt(
|
|
326
|
+
self,
|
|
327
|
+
prompt: list[ContentBlock],
|
|
328
|
+
session_id: str,
|
|
329
|
+
message_id: str | None = None,
|
|
330
|
+
**kwargs: Any,
|
|
331
|
+
) -> acp_schema.PromptResponse:
|
|
332
|
+
"""Run one ACP prompt turn."""
|
|
333
|
+
_ = kwargs
|
|
334
|
+
if self._client is None:
|
|
335
|
+
raise RequestError.internal_error({"details": "ACP client connection is not ready."})
|
|
336
|
+
bridge = AcpEventBridge()
|
|
337
|
+
pending_approvals: dict[str, ApprovalRequest] = {}
|
|
338
|
+
outcome: EndTurn | Cancelled | Errored | None = None
|
|
339
|
+
usage_payload: dict[str, int] | None = None
|
|
340
|
+
execution_started = False
|
|
341
|
+
try:
|
|
342
|
+
session = self._manager.get(session_id)
|
|
343
|
+
async with session.prompt_lock:
|
|
344
|
+
try:
|
|
345
|
+
# A close/delete may have started while this prompt waited for
|
|
346
|
+
# the per-session lock. Re-resolve before touching the host so
|
|
347
|
+
# a queued prompt cannot run against a closing session.
|
|
348
|
+
if self._manager.get(session_id) is not session:
|
|
349
|
+
raise AcpSessionError(f"ACP session is not active: {session_id}")
|
|
350
|
+
converted = convert_prompt_blocks(prompt, vision_enabled=session.host.vision_enabled)
|
|
351
|
+
execution_started = True
|
|
352
|
+
async for event in session.host.iter_turn_events(converted.message):
|
|
353
|
+
await self._handle_event(session_id, event, bridge, pending_approvals)
|
|
354
|
+
outcome = session.host.last_turn_outcome
|
|
355
|
+
finally:
|
|
356
|
+
# This snapshot stays under prompt_lock: a queued turn must
|
|
357
|
+
# not mutate cumulative session usage before this response
|
|
358
|
+
# (or error) captures the completed turn's total.
|
|
359
|
+
if execution_started:
|
|
360
|
+
usage_payload = _prompt_usage_payload(
|
|
361
|
+
session.host.engine.usage_publisher.make_usage_event(session_id=session_id)
|
|
362
|
+
)
|
|
363
|
+
# A cancellation tombstone only bridges to a request event
|
|
364
|
+
# still queued in THIS turn's per-turn stream. Judge-aborted
|
|
365
|
+
# approvals and requests dropped by cancellation never send
|
|
366
|
+
# that event, so whatever survives the stream is dead —
|
|
367
|
+
# sweep it or a long-lived session accumulates tombstones.
|
|
368
|
+
self._drop_session_tombstones(session_id)
|
|
369
|
+
except AcpContentError as exc:
|
|
370
|
+
raise RequestError.invalid_params({"details": str(exc)}) from exc
|
|
371
|
+
except Exception as exc:
|
|
372
|
+
raise _request_error(exc, usage=usage_payload) from exc
|
|
373
|
+
|
|
374
|
+
if isinstance(outcome, EndTurn):
|
|
375
|
+
return acp_schema.PromptResponse(
|
|
376
|
+
stopReason="end_turn",
|
|
377
|
+
userMessageId=message_id,
|
|
378
|
+
usage=acp_schema.Usage.model_validate(usage_payload),
|
|
379
|
+
)
|
|
380
|
+
if isinstance(outcome, Cancelled):
|
|
381
|
+
return acp_schema.PromptResponse(
|
|
382
|
+
stopReason="cancelled",
|
|
383
|
+
userMessageId=message_id,
|
|
384
|
+
usage=acp_schema.Usage.model_validate(usage_payload),
|
|
385
|
+
)
|
|
386
|
+
if isinstance(outcome, Errored):
|
|
387
|
+
error = outcome.error
|
|
388
|
+
raise RequestError.internal_error(
|
|
389
|
+
{
|
|
390
|
+
"code": error.code,
|
|
391
|
+
"message": error.message,
|
|
392
|
+
"usage": usage_payload,
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
raise RequestError.internal_error(
|
|
396
|
+
{
|
|
397
|
+
"details": f"{APP_DISPLAY_NAME} turn ended without an outcome.",
|
|
398
|
+
"usage": usage_payload,
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
async def cancel(self, session_id: str, **kwargs: Any) -> None:
|
|
403
|
+
"""Cancel the active prompt for a session."""
|
|
404
|
+
_ = kwargs
|
|
405
|
+
try:
|
|
406
|
+
await self._manager.cancel(session_id)
|
|
407
|
+
except AcpSessionError:
|
|
408
|
+
pass
|
|
409
|
+
finally:
|
|
410
|
+
# Interrupt the engine first. A rejected approval is not turn
|
|
411
|
+
# terminating, so releasing the bridge wait before interruption
|
|
412
|
+
# could feed one extra doomed model call.
|
|
413
|
+
self._cancel_pending_waits(session_id)
|
|
414
|
+
|
|
415
|
+
async def close_session(self, session_id: str, **kwargs: Any) -> acp_schema.CloseSessionResponse:
|
|
416
|
+
"""Close an active ACP session."""
|
|
417
|
+
_ = kwargs
|
|
418
|
+
# Stop prompt admission before releasing waits: once the pending
|
|
419
|
+
# ask-user/permission callbacks resolve, a prompt queued on the
|
|
420
|
+
# session's prompt lock would otherwise pass its re-check and start a
|
|
421
|
+
# fresh turn that manager.close then wedges behind.
|
|
422
|
+
await self._manager.begin_close(session_id)
|
|
423
|
+
# The wait release is inside the try: cancelling this RPC mid-release
|
|
424
|
+
# must still drop the session's cancellation state in the finally.
|
|
425
|
+
try:
|
|
426
|
+
await self._interrupt_and_cancel_pending_waits(session_id)
|
|
427
|
+
await self._manager.close(session_id)
|
|
428
|
+
except Exception as exc:
|
|
429
|
+
raise _request_error(exc) from exc
|
|
430
|
+
finally:
|
|
431
|
+
self._clear_cancellation_state(session_id)
|
|
432
|
+
return acp_schema.CloseSessionResponse()
|
|
433
|
+
|
|
434
|
+
async def set_session_mode(self, mode_id: str, session_id: str, **kwargs: Any) -> acp_schema.SetSessionModeResponse:
|
|
435
|
+
"""Set the session approval mode (standard ACP session mode)."""
|
|
436
|
+
_ = kwargs
|
|
437
|
+
if mode_id not in _APPROVAL_MODE_IDS:
|
|
438
|
+
raise RequestError.invalid_params({"details": "mode_id must be manual, auto, or bypass."})
|
|
439
|
+
try:
|
|
440
|
+
result = await self._manager.set_approval_mode(session_id, mode_id)
|
|
441
|
+
except Exception as exc:
|
|
442
|
+
raise _request_error(exc) from exc
|
|
443
|
+
if self._client is not None:
|
|
444
|
+
with contextlib.suppress(Exception):
|
|
445
|
+
await self._client.session_update(
|
|
446
|
+
session_id=session_id,
|
|
447
|
+
update=acp_schema.CurrentModeUpdate(
|
|
448
|
+
sessionUpdate="current_mode_update", currentModeId=result.mode or mode_id
|
|
449
|
+
),
|
|
450
|
+
)
|
|
451
|
+
return acp_schema.SetSessionModeResponse()
|
|
452
|
+
|
|
453
|
+
async def set_session_model(
|
|
454
|
+
self, model_id: str, session_id: str, **kwargs: Any
|
|
455
|
+
) -> acp_schema.SetSessionModelResponse:
|
|
456
|
+
"""Switch the session model (standard ACP) — per-session, no global .env write."""
|
|
457
|
+
_ = kwargs
|
|
458
|
+
try:
|
|
459
|
+
await self._manager.set_model_profile(session_id, model_id)
|
|
460
|
+
except Exception as exc:
|
|
461
|
+
raise _request_error(exc) from exc
|
|
462
|
+
await self._send_runtime_update(session_id)
|
|
463
|
+
return acp_schema.SetSessionModelResponse()
|
|
464
|
+
|
|
465
|
+
async def delete_session(
|
|
466
|
+
self,
|
|
467
|
+
session_id: str,
|
|
468
|
+
cwd: str | None = None,
|
|
469
|
+
additional_directories: list[str] | None = None,
|
|
470
|
+
**kwargs: Any,
|
|
471
|
+
) -> dict[str, Any]:
|
|
472
|
+
"""Delete a saved Chrys session scoped to the requested workspace."""
|
|
473
|
+
_ = kwargs
|
|
474
|
+
self._reject_additional_directories(additional_directories)
|
|
475
|
+
try:
|
|
476
|
+
canonical_id = await self._manager.begin_delete_session(cwd=cwd, session_id=session_id)
|
|
477
|
+
except Exception as exc:
|
|
478
|
+
raise _request_error(exc) from exc
|
|
479
|
+
# Deleting an active session tears it down like close: once the
|
|
480
|
+
# request validates, release pending client waits under the canonical
|
|
481
|
+
# id so the manager's prompt-lock-protected teardown cannot wedge
|
|
482
|
+
# behind an unresolved ask-user/permission callback. The raw id may be
|
|
483
|
+
# a short id that would miss the engine and the wait maps, and a
|
|
484
|
+
# request rejected for workspace scoping must not interrupt the turn.
|
|
485
|
+
# begin_delete_session also marks the session closing, so a prompt
|
|
486
|
+
# queued behind the released waits is rejected instead of being
|
|
487
|
+
# admitted into a fresh turn the teardown would wedge behind.
|
|
488
|
+
try:
|
|
489
|
+
await self._interrupt_and_cancel_pending_waits(canonical_id)
|
|
490
|
+
await self._manager.finish_delete_session(canonical_id)
|
|
491
|
+
except Exception as exc:
|
|
492
|
+
raise _request_error(exc) from exc
|
|
493
|
+
finally:
|
|
494
|
+
self._clear_cancellation_state(canonical_id)
|
|
495
|
+
return {}
|
|
496
|
+
|
|
497
|
+
async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
498
|
+
"""Handle Chrys ACP extension requests from clients."""
|
|
499
|
+
if method == "session/delete":
|
|
500
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
501
|
+
cwd = _optional_string_param(params, "cwd")
|
|
502
|
+
additional_directories = _string_list_param(params, "additionalDirectories")
|
|
503
|
+
await self.delete_session(
|
|
504
|
+
session_id=session_id,
|
|
505
|
+
cwd=cwd,
|
|
506
|
+
additional_directories=additional_directories,
|
|
507
|
+
)
|
|
508
|
+
return {}
|
|
509
|
+
if method == "chrys/session_runtime":
|
|
510
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
511
|
+
return self._runtime_payload(session_id)
|
|
512
|
+
if method == "session/inject":
|
|
513
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
514
|
+
text = _string_param(params, "text")
|
|
515
|
+
if not text:
|
|
516
|
+
raise RequestError.invalid_params({"details": "text is required."})
|
|
517
|
+
try:
|
|
518
|
+
await self._manager.inject(session_id, text)
|
|
519
|
+
except Exception as exc:
|
|
520
|
+
raise _request_error(exc) from exc
|
|
521
|
+
return {}
|
|
522
|
+
if method == "session/mutations":
|
|
523
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
524
|
+
await self._refresh_mutation_attribution(session_id)
|
|
525
|
+
return self._mutations_payload(session_id)
|
|
526
|
+
if method == "session/diff":
|
|
527
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
528
|
+
path = _optional_string_param(params, "path")
|
|
529
|
+
turn = _optional_int_param(params, "turn")
|
|
530
|
+
await self._refresh_mutation_attribution(session_id)
|
|
531
|
+
return self._diff_payload(session_id, path=path, turn=turn)
|
|
532
|
+
if method == "session/rollback":
|
|
533
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
534
|
+
target_turn = _int_param(params, "targetTurn")
|
|
535
|
+
revert_changes = _bool_param(params, "revertChanges", default=False)
|
|
536
|
+
selected_paths = _string_list_param(params, "selectedPaths")
|
|
537
|
+
try:
|
|
538
|
+
result = await self._manager.rollback(
|
|
539
|
+
session_id,
|
|
540
|
+
target_turn=target_turn,
|
|
541
|
+
revert_changes=revert_changes,
|
|
542
|
+
selected_paths=selected_paths,
|
|
543
|
+
)
|
|
544
|
+
except Exception as exc:
|
|
545
|
+
raise _request_error(exc) from exc
|
|
546
|
+
payload = _rollback_result_payload(result)
|
|
547
|
+
await self._client_or_error().ext_notification("chrys/rollback_result", payload)
|
|
548
|
+
if target_turn > 0:
|
|
549
|
+
await replay_session_history(
|
|
550
|
+
self._client_or_error(),
|
|
551
|
+
self._manager.state_store,
|
|
552
|
+
session_id,
|
|
553
|
+
tool_kind_resolver=self._manager.tool_kind_resolver(session_id),
|
|
554
|
+
)
|
|
555
|
+
await self._send_runtime_update(session_id)
|
|
556
|
+
# Both branches: >0 re-seeds the rolled-back list, ==0 (reset
|
|
557
|
+
# to welcome) clears the client plan.
|
|
558
|
+
await self._send_plan_update(session_id)
|
|
559
|
+
return payload
|
|
560
|
+
if method == "sub_agent/retry":
|
|
561
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
562
|
+
invocation_id = _string_param(params, "invocationId") or _string_param(params, "invocation_id")
|
|
563
|
+
if not invocation_id:
|
|
564
|
+
raise RequestError.invalid_params({"details": "invocationId is required."})
|
|
565
|
+
try:
|
|
566
|
+
await self._manager.retry_sub_agent(session_id, invocation_id)
|
|
567
|
+
except Exception as exc:
|
|
568
|
+
raise _request_error(exc) from exc
|
|
569
|
+
return {}
|
|
570
|
+
if method == "sub_agent/abort":
|
|
571
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
572
|
+
invocation_id = _string_param(params, "invocationId") or _string_param(params, "invocation_id")
|
|
573
|
+
if not invocation_id:
|
|
574
|
+
raise RequestError.invalid_params({"details": "invocationId is required."})
|
|
575
|
+
try:
|
|
576
|
+
await self._manager.abort_sub_agent(session_id, invocation_id)
|
|
577
|
+
except Exception as exc:
|
|
578
|
+
raise _request_error(exc) from exc
|
|
579
|
+
return {}
|
|
580
|
+
if method == "session/skip_sleep":
|
|
581
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
582
|
+
call_id = _string_param(params, "callId") or _string_param(params, "call_id")
|
|
583
|
+
if not call_id:
|
|
584
|
+
raise RequestError.invalid_params({"details": "callId is required."})
|
|
585
|
+
try:
|
|
586
|
+
await self._manager.skip_sleep(session_id, call_id)
|
|
587
|
+
except Exception as exc:
|
|
588
|
+
raise _request_error(exc) from exc
|
|
589
|
+
return {}
|
|
590
|
+
if method == "session/switch_agent":
|
|
591
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
592
|
+
profile_name = _string_param(params, "agentProfile") or _string_param(params, "profileName")
|
|
593
|
+
if not profile_name:
|
|
594
|
+
raise RequestError.invalid_params({"details": "agentProfile is required."})
|
|
595
|
+
try:
|
|
596
|
+
result = await self._manager.switch_agent(session_id, profile_name)
|
|
597
|
+
except Exception as exc:
|
|
598
|
+
raise _request_error(exc) from exc
|
|
599
|
+
return _profile_switched_payload(result, session_id=session_id)
|
|
600
|
+
if method == "settings/reload":
|
|
601
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
602
|
+
# A reload happens outside any prompt turn, and ``_handle_event`` only
|
|
603
|
+
# runs inside one — so the warnings saying which of the client's
|
|
604
|
+
# values were dropped had nowhere to go and were simply lost.
|
|
605
|
+
warnings: list[Warning] = []
|
|
606
|
+
try:
|
|
607
|
+
await self._manager.reload_settings(session_id, warnings=warnings)
|
|
608
|
+
except Exception as exc:
|
|
609
|
+
raise _request_error(exc) from exc
|
|
610
|
+
for warning in warnings:
|
|
611
|
+
await self._send_warning(session_id, warning)
|
|
612
|
+
await self._send_runtime_update(session_id)
|
|
613
|
+
return {}
|
|
614
|
+
if method == "settings/options":
|
|
615
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
616
|
+
try:
|
|
617
|
+
# Reads the settings document under its file lock, so it waits
|
|
618
|
+
# on whatever holds it — inline, that wait is the whole ACP
|
|
619
|
+
# loop's, and every other session stalls behind a read.
|
|
620
|
+
return await asyncio.to_thread(self._manager.get_config_options, session_id or None)
|
|
621
|
+
except Exception as exc:
|
|
622
|
+
raise _request_error(exc) from exc
|
|
623
|
+
if method == "session/set_config_option":
|
|
624
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
625
|
+
key = _string_param(params, "key")
|
|
626
|
+
# Same gap as ``settings/reload``, and worse: this route writes the
|
|
627
|
+
# value to disk before reloading it, so a clamped or rejected input
|
|
628
|
+
# is persisted while the client is told only that it succeeded.
|
|
629
|
+
warnings = []
|
|
630
|
+
try:
|
|
631
|
+
result = await self._manager.apply_config_option(
|
|
632
|
+
session_id, key, params.get("value", ""), warnings=warnings
|
|
633
|
+
)
|
|
634
|
+
except Exception as exc:
|
|
635
|
+
raise _request_error(exc) from exc
|
|
636
|
+
for warning in warnings:
|
|
637
|
+
await self._send_warning(session_id, warning)
|
|
638
|
+
await self._send_runtime_update(session_id)
|
|
639
|
+
return {"sessionId": session_id, **result}
|
|
640
|
+
if method == "session/set_workspace":
|
|
641
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
642
|
+
primary_cwd = _string_param(params, "primaryCwd") or _string_param(params, "primary_cwd")
|
|
643
|
+
if not primary_cwd:
|
|
644
|
+
raise RequestError.invalid_params({"details": "primaryCwd is required."})
|
|
645
|
+
try:
|
|
646
|
+
result = await self._manager.set_workspace(session_id, primary_cwd)
|
|
647
|
+
except Exception as exc:
|
|
648
|
+
raise _request_error(exc) from exc
|
|
649
|
+
# A workspace change soft-restarts the agent, which can alter skills,
|
|
650
|
+
# MCP/runtime details, memory, and workspace-loaded context — push the
|
|
651
|
+
# refreshed runtime so the client doesn't keep stale state.
|
|
652
|
+
await self._send_runtime_update(session_id)
|
|
653
|
+
return {
|
|
654
|
+
"sessionId": result.session_id or session_id,
|
|
655
|
+
"primaryCwd": result.primary_cwd,
|
|
656
|
+
"workingDirs": result.working_dirs,
|
|
657
|
+
"referenceFiles": result.reference_files,
|
|
658
|
+
}
|
|
659
|
+
if method == "session/history":
|
|
660
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
661
|
+
cwd = _optional_string_param(params, "cwd")
|
|
662
|
+
try:
|
|
663
|
+
canonical_id, messages = await self._manager.session_history(cwd=cwd, session_id=session_id)
|
|
664
|
+
except Exception as exc:
|
|
665
|
+
raise _request_error(exc) from exc
|
|
666
|
+
return {"sessionId": canonical_id, "messages": messages}
|
|
667
|
+
if method == "profiles/agents/list":
|
|
668
|
+
return {"agents": self._manager.list_agent_profiles()}
|
|
669
|
+
if method == "profiles/agents/read":
|
|
670
|
+
name = _string_param(params, "name")
|
|
671
|
+
return {"profile": self._manager.read_agent_profile(name)}
|
|
672
|
+
if method == "profiles/agents/write":
|
|
673
|
+
profile = _dict_param(params, "profile")
|
|
674
|
+
try:
|
|
675
|
+
return self._manager.write_agent_profile(profile)
|
|
676
|
+
except Exception as exc:
|
|
677
|
+
raise _request_error(exc) from exc
|
|
678
|
+
if method == "profiles/agents/delete":
|
|
679
|
+
name = _string_param(params, "name")
|
|
680
|
+
try:
|
|
681
|
+
return self._manager.delete_agent_profile(name)
|
|
682
|
+
except Exception as exc:
|
|
683
|
+
raise _request_error(exc) from exc
|
|
684
|
+
if method == "profiles/agents/reset":
|
|
685
|
+
name = _string_param(params, "name")
|
|
686
|
+
try:
|
|
687
|
+
return self._manager.reset_agent_profile(name)
|
|
688
|
+
except Exception as exc:
|
|
689
|
+
raise _request_error(exc) from exc
|
|
690
|
+
if method == "profiles/models/list":
|
|
691
|
+
return {"models": self._manager.list_model_profiles()}
|
|
692
|
+
if method == "profiles/models/read":
|
|
693
|
+
profile_id = _string_param(params, "id") or _string_param(params, "profileId")
|
|
694
|
+
return {"profile": self._manager.read_model_profile(profile_id)}
|
|
695
|
+
if method == "profiles/models/write":
|
|
696
|
+
profile = _dict_param(params, "profile")
|
|
697
|
+
try:
|
|
698
|
+
return self._manager.write_model_profile(profile)
|
|
699
|
+
except Exception as exc:
|
|
700
|
+
raise _request_error(exc) from exc
|
|
701
|
+
if method == "profiles/models/delete":
|
|
702
|
+
profile_id = _string_param(params, "id") or _string_param(params, "profileId")
|
|
703
|
+
try:
|
|
704
|
+
return self._manager.delete_model_profile(profile_id)
|
|
705
|
+
except Exception as exc:
|
|
706
|
+
raise _request_error(exc) from exc
|
|
707
|
+
if method == "mcp/test":
|
|
708
|
+
config = _dict_param(params, "server")
|
|
709
|
+
try:
|
|
710
|
+
return await self._manager.test_mcp_server(config)
|
|
711
|
+
except Exception as exc:
|
|
712
|
+
raise _request_error(exc) from exc
|
|
713
|
+
if method == "mcp/list":
|
|
714
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
715
|
+
runtime = self._runtime_payload(session_id)
|
|
716
|
+
details = runtime.get("runtimeDetails", {})
|
|
717
|
+
return {
|
|
718
|
+
"sessionId": session_id,
|
|
719
|
+
"mcpTools": details.get("mcp_tools", {}),
|
|
720
|
+
"mcpFailures": details.get("mcp_failures", {}),
|
|
721
|
+
}
|
|
722
|
+
if method == "skills/list":
|
|
723
|
+
session_id = _string_param(params, "sessionId") or _string_param(params, "session_id")
|
|
724
|
+
runtime = self._runtime_payload(session_id)
|
|
725
|
+
details = runtime.get("runtimeDetails", {})
|
|
726
|
+
return {
|
|
727
|
+
"sessionId": session_id,
|
|
728
|
+
"skillSources": details.get("skill_sources", {}),
|
|
729
|
+
"skillDetails": details.get("skill_details", []),
|
|
730
|
+
}
|
|
731
|
+
raise RequestError.method_not_found(method)
|
|
732
|
+
|
|
733
|
+
async def _handle_event(
|
|
734
|
+
self,
|
|
735
|
+
session_id: str,
|
|
736
|
+
event: Event,
|
|
737
|
+
bridge: AcpEventBridge,
|
|
738
|
+
pending_approvals: dict[str, ApprovalRequest],
|
|
739
|
+
) -> None:
|
|
740
|
+
if isinstance(event, ApprovalRequest):
|
|
741
|
+
if event.judging:
|
|
742
|
+
pending_approvals[event.request_id] = event
|
|
743
|
+
else:
|
|
744
|
+
await self._request_permission(session_id, event)
|
|
745
|
+
return
|
|
746
|
+
if isinstance(event, ApprovalReviewed):
|
|
747
|
+
await self._client_or_error().ext_notification(
|
|
748
|
+
"chrys/approval_reviewed",
|
|
749
|
+
{
|
|
750
|
+
"sessionId": session_id,
|
|
751
|
+
"requestId": event.request_id,
|
|
752
|
+
"approved": event.approved,
|
|
753
|
+
"reason": event.reason,
|
|
754
|
+
},
|
|
755
|
+
)
|
|
756
|
+
request = pending_approvals.pop(event.request_id, None)
|
|
757
|
+
if request is not None and not event.approved:
|
|
758
|
+
await self._request_permission(session_id, request, reason=event.reason)
|
|
759
|
+
return
|
|
760
|
+
if isinstance(event, QuestionToUser):
|
|
761
|
+
await self._request_input(session_id, event)
|
|
762
|
+
return
|
|
763
|
+
if await self._handle_chrys_extension_event(session_id, event):
|
|
764
|
+
return
|
|
765
|
+
for update in bridge.updates_for_event(event):
|
|
766
|
+
await self._client_or_error().session_update(session_id=session_id, update=update)
|
|
767
|
+
|
|
768
|
+
async def _handle_chrys_extension_event(self, session_id: str, event: Event) -> bool:
|
|
769
|
+
if isinstance(event, InvocationEvent) and event.origin.root.kind == "workflow_node":
|
|
770
|
+
return False
|
|
771
|
+
client = self._client_or_error()
|
|
772
|
+
event_session_id = _event_session_id(event, session_id)
|
|
773
|
+
if isinstance(event, AgentLoadStarted):
|
|
774
|
+
await client.ext_notification(
|
|
775
|
+
"chrys/agent_load_started",
|
|
776
|
+
{
|
|
777
|
+
"sessionId": event_session_id,
|
|
778
|
+
"operation": event.operation,
|
|
779
|
+
"fromProfile": event.from_profile,
|
|
780
|
+
"toProfile": event.to_profile,
|
|
781
|
+
"fromDisplayName": event.from_display_name,
|
|
782
|
+
"toDisplayName": event.to_display_name,
|
|
783
|
+
},
|
|
784
|
+
)
|
|
785
|
+
return True
|
|
786
|
+
if isinstance(event, AgentLoadProgress):
|
|
787
|
+
await client.ext_notification(
|
|
788
|
+
"chrys/agent_load_progress",
|
|
789
|
+
{
|
|
790
|
+
"sessionId": event_session_id,
|
|
791
|
+
"phase": event.phase,
|
|
792
|
+
"message": event.message,
|
|
793
|
+
"serverName": event.server_name,
|
|
794
|
+
"current": event.current,
|
|
795
|
+
"total": event.total,
|
|
796
|
+
"failed": event.failed,
|
|
797
|
+
},
|
|
798
|
+
)
|
|
799
|
+
return True
|
|
800
|
+
if isinstance(event, AgentLoadFinished):
|
|
801
|
+
await client.ext_notification(
|
|
802
|
+
"chrys/agent_load_finished",
|
|
803
|
+
{
|
|
804
|
+
"sessionId": event_session_id,
|
|
805
|
+
"operation": event.operation,
|
|
806
|
+
"agentProfile": event.agent_profile,
|
|
807
|
+
"displayName": event.display_name,
|
|
808
|
+
},
|
|
809
|
+
)
|
|
810
|
+
return True
|
|
811
|
+
if isinstance(event, AgentLoadFailed):
|
|
812
|
+
await client.ext_notification(
|
|
813
|
+
"chrys/agent_load_failed",
|
|
814
|
+
{
|
|
815
|
+
"sessionId": event_session_id,
|
|
816
|
+
"operation": event.operation,
|
|
817
|
+
"agentProfile": event.agent_profile,
|
|
818
|
+
"displayName": event.display_name,
|
|
819
|
+
"message": event.message,
|
|
820
|
+
},
|
|
821
|
+
)
|
|
822
|
+
return True
|
|
823
|
+
if isinstance(event, SessionReady):
|
|
824
|
+
await client.ext_notification(
|
|
825
|
+
"chrys/runtime_update",
|
|
826
|
+
{
|
|
827
|
+
"sessionId": event_session_id,
|
|
828
|
+
"runtime": {
|
|
829
|
+
"sessionId": event_session_id,
|
|
830
|
+
"agentProfile": event.agent_profile,
|
|
831
|
+
"displayName": event.display_name,
|
|
832
|
+
"modelProfileId": event.model_profile_id,
|
|
833
|
+
"maxContextTokens": event.max_context_tokens,
|
|
834
|
+
"toolNames": event.tool_names,
|
|
835
|
+
"toolKinds": event.tool_kinds,
|
|
836
|
+
"skillNames": event.skill_names,
|
|
837
|
+
"subAgentToolNames": event.sub_agent_tool_names,
|
|
838
|
+
"memoryFiles": event.memory_files,
|
|
839
|
+
"runtimeDetails": jsonable_dataclass(event.runtime_details),
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
)
|
|
843
|
+
return True
|
|
844
|
+
if isinstance(event, AgentRuntimeUpdated):
|
|
845
|
+
await client.ext_notification(
|
|
846
|
+
"chrys/runtime_update",
|
|
847
|
+
{
|
|
848
|
+
"sessionId": session_id,
|
|
849
|
+
"runtime": _runtime_update_payload(event),
|
|
850
|
+
},
|
|
851
|
+
)
|
|
852
|
+
return True
|
|
853
|
+
if isinstance(event, ProfileSwitched):
|
|
854
|
+
await client.ext_notification(
|
|
855
|
+
"chrys/profile_switched", _profile_switched_payload(event, session_id=session_id)
|
|
856
|
+
)
|
|
857
|
+
return True
|
|
858
|
+
if isinstance(event, WorkspaceUpdated):
|
|
859
|
+
await client.ext_notification(
|
|
860
|
+
"chrys/workspace_updated",
|
|
861
|
+
{
|
|
862
|
+
"sessionId": event_session_id,
|
|
863
|
+
"primaryCwd": event.primary_cwd,
|
|
864
|
+
"workingDirs": event.working_dirs,
|
|
865
|
+
"referenceFiles": event.reference_files,
|
|
866
|
+
},
|
|
867
|
+
)
|
|
868
|
+
return True
|
|
869
|
+
if isinstance(event, UsageUpdate):
|
|
870
|
+
await client.ext_notification(
|
|
871
|
+
"chrys/usage_update",
|
|
872
|
+
{
|
|
873
|
+
"sessionId": event_session_id,
|
|
874
|
+
"agentProfile": event.agent_profile,
|
|
875
|
+
"usageSourceId": event.usage_source_id,
|
|
876
|
+
"inputTokens": event.input_tokens,
|
|
877
|
+
"outputTokens": event.output_tokens,
|
|
878
|
+
"totalTokens": event.total_tokens,
|
|
879
|
+
"pct": event.pct,
|
|
880
|
+
"maxContextTokens": event.max_context_tokens,
|
|
881
|
+
"totalSessionTokens": event.total_session_tokens,
|
|
882
|
+
"totalSessionInputTokens": event.total_session_input_tokens,
|
|
883
|
+
"totalSessionOutputTokens": event.total_session_output_tokens,
|
|
884
|
+
"cacheHitTokens": event.cache_hit_tokens,
|
|
885
|
+
"totalSessionCacheHitTokens": event.total_session_cache_hit_tokens,
|
|
886
|
+
"localTokens": event.local_tokens,
|
|
887
|
+
"calibrationRatio": event.calibration_ratio,
|
|
888
|
+
"systemOverheadTokens": event.system_overhead_tokens,
|
|
889
|
+
},
|
|
890
|
+
)
|
|
891
|
+
return False
|
|
892
|
+
if isinstance(event, SessionRestored):
|
|
893
|
+
await client.ext_notification(
|
|
894
|
+
"chrys/session_restored",
|
|
895
|
+
{
|
|
896
|
+
"sessionId": event_session_id,
|
|
897
|
+
"agentProfile": event.agent_profile,
|
|
898
|
+
"displayName": event.display_name,
|
|
899
|
+
"messageCount": event.message_count,
|
|
900
|
+
"cwdWarning": event.cwd_warning,
|
|
901
|
+
"primaryCwd": event.primary_cwd,
|
|
902
|
+
},
|
|
903
|
+
)
|
|
904
|
+
return True
|
|
905
|
+
if isinstance(event, Error):
|
|
906
|
+
await client.ext_notification(
|
|
907
|
+
"chrys/error",
|
|
908
|
+
{
|
|
909
|
+
"sessionId": event_session_id,
|
|
910
|
+
"code": event.code,
|
|
911
|
+
"message": event.message,
|
|
912
|
+
"recoverable": event.recoverable,
|
|
913
|
+
},
|
|
914
|
+
)
|
|
915
|
+
return True
|
|
916
|
+
if isinstance(event, Warning):
|
|
917
|
+
await self._send_warning(event_session_id, event)
|
|
918
|
+
return True
|
|
919
|
+
if isinstance(event, ApprovalModeUpdated):
|
|
920
|
+
await client.session_update(
|
|
921
|
+
session_id=event_session_id,
|
|
922
|
+
update=acp_schema.CurrentModeUpdate(sessionUpdate="current_mode_update", currentModeId=event.mode),
|
|
923
|
+
)
|
|
924
|
+
return True
|
|
925
|
+
if isinstance(event, ContextCompressed):
|
|
926
|
+
await client.ext_notification(
|
|
927
|
+
"chrys/context_compressed",
|
|
928
|
+
{
|
|
929
|
+
"sessionId": event_session_id,
|
|
930
|
+
"compressedContextId": event.compressed_context_id,
|
|
931
|
+
"summary": event.summary,
|
|
932
|
+
"freedMessages": event.freed_messages,
|
|
933
|
+
"turnRange": list(event.turn_range),
|
|
934
|
+
"source": event.source,
|
|
935
|
+
},
|
|
936
|
+
)
|
|
937
|
+
return True
|
|
938
|
+
if isinstance(event, InvocationContextPressure):
|
|
939
|
+
await client.ext_notification(
|
|
940
|
+
"chrys/context_pressure",
|
|
941
|
+
{
|
|
942
|
+
"sessionId": event_session_id,
|
|
943
|
+
"reason": event.reason,
|
|
944
|
+
"attempts": event.attempts,
|
|
945
|
+
"sideCallTokens": event.side_call_tokens,
|
|
946
|
+
"sideCallTokenBudget": event.side_call_token_budget,
|
|
947
|
+
"source": event.source,
|
|
948
|
+
"invocationId": event.origin.invocation_id if event.origin.kind == "sub_agent" else "",
|
|
949
|
+
},
|
|
950
|
+
)
|
|
951
|
+
return True
|
|
952
|
+
if isinstance(event, ToolCompacted):
|
|
953
|
+
await client.ext_notification(
|
|
954
|
+
"chrys/tool_compacted",
|
|
955
|
+
{
|
|
956
|
+
"sessionId": event_session_id,
|
|
957
|
+
"compactedGroups": event.compacted_groups,
|
|
958
|
+
"phase": event.phase,
|
|
959
|
+
"turnNumbers": event.turn_numbers,
|
|
960
|
+
"compactedToolNames": event.compacted_tool_names,
|
|
961
|
+
"tokensBefore": event.tokens_before,
|
|
962
|
+
"tokensAfter": event.tokens_after,
|
|
963
|
+
"lastWordsGenerated": event.last_words_generated,
|
|
964
|
+
},
|
|
965
|
+
)
|
|
966
|
+
return True
|
|
967
|
+
if isinstance(event, CompactionStarted):
|
|
968
|
+
await client.ext_notification(
|
|
969
|
+
"chrys/compaction_started",
|
|
970
|
+
{
|
|
971
|
+
"sessionId": event_session_id,
|
|
972
|
+
"compactionId": event.compaction_id,
|
|
973
|
+
"phase": event.phase,
|
|
974
|
+
},
|
|
975
|
+
)
|
|
976
|
+
return True
|
|
977
|
+
if isinstance(event, CompactionFinished):
|
|
978
|
+
await client.ext_notification(
|
|
979
|
+
"chrys/compaction_finished",
|
|
980
|
+
{
|
|
981
|
+
"sessionId": event_session_id,
|
|
982
|
+
"compactionId": event.compaction_id,
|
|
983
|
+
"outcome": event.outcome,
|
|
984
|
+
"durationMs": event.duration_ms,
|
|
985
|
+
"lastWords": event.last_words,
|
|
986
|
+
"formatViolation": event.format_violation,
|
|
987
|
+
"failureReason": event.failure_reason,
|
|
988
|
+
},
|
|
989
|
+
)
|
|
990
|
+
return True
|
|
991
|
+
if isinstance(event, UserInjectResult):
|
|
992
|
+
await client.ext_notification(
|
|
993
|
+
"chrys/user_inject_result",
|
|
994
|
+
{
|
|
995
|
+
"sessionId": event_session_id,
|
|
996
|
+
"text": event.text,
|
|
997
|
+
"consumed": event.consumed,
|
|
998
|
+
"createdAt": event.created_at.isoformat()
|
|
999
|
+
if isinstance(event.created_at, datetime)
|
|
1000
|
+
else event.created_at,
|
|
1001
|
+
"injectionId": event.injection_id,
|
|
1002
|
+
},
|
|
1003
|
+
)
|
|
1004
|
+
return True
|
|
1005
|
+
if isinstance(event, InvocationEvent) and event.origin.kind != "sub_agent":
|
|
1006
|
+
return False
|
|
1007
|
+
# Sub-agent events are dual-path (like UsageUpdate): the Chrys extension
|
|
1008
|
+
# notification carries the rich nested detail, and returning False lets
|
|
1009
|
+
# AcpEventBridge also project standard `session/update` tool-call progress
|
|
1010
|
+
# onto the parent tool call — so standard-only clients aren't regressed.
|
|
1011
|
+
if isinstance(event, InvocationStarted):
|
|
1012
|
+
await client.ext_notification(
|
|
1013
|
+
"chrys/sub_agent_invocation_start",
|
|
1014
|
+
{
|
|
1015
|
+
"sessionId": event_session_id,
|
|
1016
|
+
"agentName": event.agent_name,
|
|
1017
|
+
"invocationId": event.origin.invocation_id,
|
|
1018
|
+
"toolName": event.tool_name,
|
|
1019
|
+
"parentCallId": event.parent_call_id,
|
|
1020
|
+
},
|
|
1021
|
+
)
|
|
1022
|
+
return False
|
|
1023
|
+
if isinstance(event, InvocationToolCallStart):
|
|
1024
|
+
await client.ext_notification(
|
|
1025
|
+
"chrys/sub_agent_tool_call_start",
|
|
1026
|
+
{
|
|
1027
|
+
"sessionId": event_session_id,
|
|
1028
|
+
"agentName": event.agent_name,
|
|
1029
|
+
"invocationId": event.origin.invocation_id,
|
|
1030
|
+
"toolName": event.tool_name,
|
|
1031
|
+
"args": event.args,
|
|
1032
|
+
"callId": event.call_id,
|
|
1033
|
+
},
|
|
1034
|
+
)
|
|
1035
|
+
return False
|
|
1036
|
+
if isinstance(event, InvocationToolCallResult):
|
|
1037
|
+
await client.ext_notification(
|
|
1038
|
+
"chrys/sub_agent_tool_call_result",
|
|
1039
|
+
{
|
|
1040
|
+
"sessionId": event_session_id,
|
|
1041
|
+
"agentName": event.agent_name,
|
|
1042
|
+
"invocationId": event.origin.invocation_id,
|
|
1043
|
+
"toolName": event.tool_name,
|
|
1044
|
+
"callId": event.call_id,
|
|
1045
|
+
"result": event.result,
|
|
1046
|
+
"durationMs": event.duration_ms,
|
|
1047
|
+
"metadata": event.metadata,
|
|
1048
|
+
},
|
|
1049
|
+
)
|
|
1050
|
+
return False
|
|
1051
|
+
if isinstance(event, InvocationProgress):
|
|
1052
|
+
await client.ext_notification(
|
|
1053
|
+
"chrys/sub_agent_progress",
|
|
1054
|
+
{
|
|
1055
|
+
"sessionId": event_session_id,
|
|
1056
|
+
"agentName": event.agent_name,
|
|
1057
|
+
"invocationId": event.origin.invocation_id,
|
|
1058
|
+
"toolCallCount": event.tool_call_count,
|
|
1059
|
+
"totalTokens": event.total_tokens,
|
|
1060
|
+
"totalUsageTokens": event.total_usage_tokens,
|
|
1061
|
+
"usageUnreportedAttempts": event.usage_unreported_attempts,
|
|
1062
|
+
},
|
|
1063
|
+
)
|
|
1064
|
+
return False
|
|
1065
|
+
if isinstance(event, InvocationRetryAttempt):
|
|
1066
|
+
await client.ext_notification(
|
|
1067
|
+
"chrys/sub_agent_retry_attempt",
|
|
1068
|
+
{
|
|
1069
|
+
"sessionId": event_session_id,
|
|
1070
|
+
"agentName": event.agent_name,
|
|
1071
|
+
"invocationId": event.origin.invocation_id,
|
|
1072
|
+
"message": event.message,
|
|
1073
|
+
"attempt": event.attempt,
|
|
1074
|
+
"maxAttempts": event.max_attempts,
|
|
1075
|
+
"delaySeconds": event.delay_seconds,
|
|
1076
|
+
},
|
|
1077
|
+
)
|
|
1078
|
+
return False
|
|
1079
|
+
if isinstance(event, InvocationCompactionStarted):
|
|
1080
|
+
await client.ext_notification(
|
|
1081
|
+
"chrys/sub_agent_compaction_started",
|
|
1082
|
+
{
|
|
1083
|
+
"sessionId": event_session_id,
|
|
1084
|
+
"agentName": event.agent_name,
|
|
1085
|
+
"invocationId": event.origin.invocation_id,
|
|
1086
|
+
"compactionId": event.compaction_id,
|
|
1087
|
+
"phase": event.phase,
|
|
1088
|
+
},
|
|
1089
|
+
)
|
|
1090
|
+
return False
|
|
1091
|
+
if isinstance(event, InvocationCompactionFinished):
|
|
1092
|
+
await client.ext_notification(
|
|
1093
|
+
"chrys/sub_agent_compaction_finished",
|
|
1094
|
+
{
|
|
1095
|
+
"sessionId": event_session_id,
|
|
1096
|
+
"agentName": event.agent_name,
|
|
1097
|
+
"invocationId": event.origin.invocation_id,
|
|
1098
|
+
"compactionId": event.compaction_id,
|
|
1099
|
+
"outcome": event.outcome,
|
|
1100
|
+
"durationMs": event.duration_ms,
|
|
1101
|
+
"formatViolation": event.format_violation,
|
|
1102
|
+
"failureReason": event.failure_reason,
|
|
1103
|
+
},
|
|
1104
|
+
)
|
|
1105
|
+
return False
|
|
1106
|
+
if isinstance(event, InvocationCompactionCommitted):
|
|
1107
|
+
# Post-commit confirmation trailing finished(ok) — a finished-ok
|
|
1108
|
+
# round without this notification was abandoned (spill failure).
|
|
1109
|
+
await client.ext_notification(
|
|
1110
|
+
"chrys/sub_agent_compaction_committed",
|
|
1111
|
+
{
|
|
1112
|
+
"sessionId": event_session_id,
|
|
1113
|
+
"agentName": event.agent_name,
|
|
1114
|
+
"invocationId": event.origin.invocation_id,
|
|
1115
|
+
"compactionId": event.compaction_id,
|
|
1116
|
+
"phase": event.phase,
|
|
1117
|
+
},
|
|
1118
|
+
)
|
|
1119
|
+
return False
|
|
1120
|
+
if isinstance(event, InvocationPaused):
|
|
1121
|
+
await client.ext_notification(
|
|
1122
|
+
"chrys/sub_agent_paused",
|
|
1123
|
+
{
|
|
1124
|
+
"sessionId": event_session_id,
|
|
1125
|
+
"agentName": event.agent_name,
|
|
1126
|
+
"invocationId": event.origin.invocation_id,
|
|
1127
|
+
"toolName": event.tool_name,
|
|
1128
|
+
"reason": event.reason,
|
|
1129
|
+
"lastError": event.last_error,
|
|
1130
|
+
"retryAttempts": event.retry_attempts,
|
|
1131
|
+
},
|
|
1132
|
+
)
|
|
1133
|
+
return False
|
|
1134
|
+
if isinstance(event, InvocationResumed):
|
|
1135
|
+
await client.ext_notification(
|
|
1136
|
+
"chrys/sub_agent_resumed",
|
|
1137
|
+
{
|
|
1138
|
+
"sessionId": event_session_id,
|
|
1139
|
+
"agentName": event.agent_name,
|
|
1140
|
+
"invocationId": event.origin.invocation_id,
|
|
1141
|
+
},
|
|
1142
|
+
)
|
|
1143
|
+
return False
|
|
1144
|
+
if isinstance(event, InvocationAborted):
|
|
1145
|
+
await client.ext_notification(
|
|
1146
|
+
"chrys/sub_agent_aborted",
|
|
1147
|
+
{
|
|
1148
|
+
"sessionId": event_session_id,
|
|
1149
|
+
"agentName": event.agent_name,
|
|
1150
|
+
"invocationId": event.origin.invocation_id,
|
|
1151
|
+
"lastError": event.last_error,
|
|
1152
|
+
},
|
|
1153
|
+
)
|
|
1154
|
+
return False
|
|
1155
|
+
if isinstance(event, InvocationCascadeAborted):
|
|
1156
|
+
await client.ext_notification(
|
|
1157
|
+
"chrys/sub_agent_cascade_aborted",
|
|
1158
|
+
{
|
|
1159
|
+
"sessionId": event_session_id,
|
|
1160
|
+
"agentName": event.agent_name,
|
|
1161
|
+
"invocationId": event.origin.invocation_id,
|
|
1162
|
+
},
|
|
1163
|
+
)
|
|
1164
|
+
return False
|
|
1165
|
+
if isinstance(event, SessionSaved):
|
|
1166
|
+
return False
|
|
1167
|
+
return False
|
|
1168
|
+
|
|
1169
|
+
def _session_mode_state(self, session_id: str) -> acp_schema.SessionModeState:
|
|
1170
|
+
current = self._manager.get(session_id).host.engine.approval_mode.value
|
|
1171
|
+
return acp_schema.SessionModeState(
|
|
1172
|
+
availableModes=[
|
|
1173
|
+
acp_schema.SessionMode(id=mode_id, name=name, description=desc)
|
|
1174
|
+
for mode_id, name, desc in _APPROVAL_MODES
|
|
1175
|
+
],
|
|
1176
|
+
currentModeId=current,
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
def _session_model_state(self, session_id: str) -> acp_schema.SessionModelState:
|
|
1180
|
+
current = self._manager.get(session_id).host.engine.runtime_details.model.profile_id
|
|
1181
|
+
return acp_schema.SessionModelState(
|
|
1182
|
+
availableModels=[
|
|
1183
|
+
acp_schema.ModelInfo(
|
|
1184
|
+
modelId=str(profile["id"]),
|
|
1185
|
+
name=str(profile.get("name") or profile["id"]),
|
|
1186
|
+
description=_model_info_description(profile),
|
|
1187
|
+
)
|
|
1188
|
+
for profile in self._manager.list_model_profiles()
|
|
1189
|
+
],
|
|
1190
|
+
currentModelId=current,
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
async def _send_session_restored(self, session_id: str) -> None:
|
|
1194
|
+
payload = {"sessionId": session_id}
|
|
1195
|
+
await self._client_or_error().ext_notification("chrys/session_restored", payload)
|
|
1196
|
+
|
|
1197
|
+
async def _send_plan_update(self, session_id: str) -> None:
|
|
1198
|
+
# Seed/refresh the client's plan at session-establishment points
|
|
1199
|
+
# (new_session, load_session, rollback) where no TodoListUpdated
|
|
1200
|
+
# event fires. Sent unconditionally: empty entries clear a stale
|
|
1201
|
+
# plan left over from a previous session in the same client pane.
|
|
1202
|
+
tracker = self._manager.get(session_id).host.engine.todo_tracker
|
|
1203
|
+
items = tracker.snapshot() if tracker is not None else ()
|
|
1204
|
+
await self._client_or_error().session_update(
|
|
1205
|
+
session_id=session_id,
|
|
1206
|
+
update=plan_update_for_todos(items),
|
|
1207
|
+
)
|
|
1208
|
+
|
|
1209
|
+
async def _send_warning(self, session_id: str, warning: Warning) -> None:
|
|
1210
|
+
"""Send one ``chrys/warning``, whether it came from a turn or an RPC."""
|
|
1211
|
+
await self._client_or_error().ext_notification(
|
|
1212
|
+
"chrys/warning",
|
|
1213
|
+
{
|
|
1214
|
+
"sessionId": session_id,
|
|
1215
|
+
"code": warning.code,
|
|
1216
|
+
"message": warning.message,
|
|
1217
|
+
},
|
|
1218
|
+
)
|
|
1219
|
+
|
|
1220
|
+
async def _send_runtime_update(self, session_id: str) -> None:
|
|
1221
|
+
# Same envelope as the event-bridged senders (SessionReady /
|
|
1222
|
+
# AgentRuntimeUpdated): `chrys/runtime_update` is always
|
|
1223
|
+
# {sessionId, runtime: {...}} so a client handler reads one shape.
|
|
1224
|
+
await self._client_or_error().ext_notification(
|
|
1225
|
+
"chrys/runtime_update",
|
|
1226
|
+
{"sessionId": session_id, "runtime": self._runtime_payload(session_id)},
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
def _runtime_payload(self, session_id: str) -> dict[str, Any]:
|
|
1230
|
+
session = self._manager.get(session_id)
|
|
1231
|
+
engine = session.host.engine
|
|
1232
|
+
details = engine.runtime_details
|
|
1233
|
+
model = details.model
|
|
1234
|
+
usage = engine.usage_publisher.make_usage_event(session_id=session_id)
|
|
1235
|
+
return {
|
|
1236
|
+
"sessionId": session_id,
|
|
1237
|
+
"agentProfile": session.profile_name,
|
|
1238
|
+
"displayName": session.profile_name,
|
|
1239
|
+
"modelProfileId": model.profile_id,
|
|
1240
|
+
"maxContextTokens": model.max_context_tokens,
|
|
1241
|
+
"inputTokens": usage.input_tokens,
|
|
1242
|
+
"outputTokens": usage.output_tokens,
|
|
1243
|
+
"totalTokens": usage.total_tokens,
|
|
1244
|
+
"pct": usage.pct,
|
|
1245
|
+
"totalSessionTokens": usage.total_session_tokens,
|
|
1246
|
+
"totalSessionInputTokens": usage.total_session_input_tokens,
|
|
1247
|
+
"totalSessionOutputTokens": usage.total_session_output_tokens,
|
|
1248
|
+
"cacheHitTokens": usage.cache_hit_tokens,
|
|
1249
|
+
"totalSessionCacheHitTokens": usage.total_session_cache_hit_tokens,
|
|
1250
|
+
"localTokens": usage.local_tokens,
|
|
1251
|
+
"calibrationRatio": usage.calibration_ratio,
|
|
1252
|
+
"systemOverheadTokens": usage.system_overhead_tokens,
|
|
1253
|
+
"runtimeDetails": jsonable_dataclass(details),
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
async def _refresh_mutation_attribution(self, session_id: str) -> None:
|
|
1257
|
+
"""Fold newly published peer claims in before a net-summary read.
|
|
1258
|
+
|
|
1259
|
+
Best-effort: failures (including unknown session ids) fall
|
|
1260
|
+
through — the payload builder raises the canonical error.
|
|
1261
|
+
"""
|
|
1262
|
+
with contextlib.suppress(Exception):
|
|
1263
|
+
engine = self._manager.get(session_id).host.engine
|
|
1264
|
+
await engine.refresh_mutation_attribution()
|
|
1265
|
+
|
|
1266
|
+
def _mutations_payload(self, session_id: str) -> dict[str, Any]:
|
|
1267
|
+
session = self._manager.get(session_id)
|
|
1268
|
+
engine = session.host.engine
|
|
1269
|
+
tracker = engine.mutation_tracker
|
|
1270
|
+
if tracker is None:
|
|
1271
|
+
return {
|
|
1272
|
+
"sessionId": session_id,
|
|
1273
|
+
"currentTurn": engine.current_turn_number,
|
|
1274
|
+
"availableRollbackTurns": [],
|
|
1275
|
+
"turns": [],
|
|
1276
|
+
"files": [],
|
|
1277
|
+
}
|
|
1278
|
+
file_summary = tracker.get_session_file_summary()
|
|
1279
|
+
return {
|
|
1280
|
+
"sessionId": session_id,
|
|
1281
|
+
"currentTurn": engine.current_turn_number,
|
|
1282
|
+
"availableRollbackTurns": engine.available_rollback_turns(),
|
|
1283
|
+
"turns": [
|
|
1284
|
+
{
|
|
1285
|
+
"turnId": turn.turn_id,
|
|
1286
|
+
"mutationCount": len(turn.mutations),
|
|
1287
|
+
"mutations": [_mutation_payload(mutation) for mutation in turn.mutations],
|
|
1288
|
+
}
|
|
1289
|
+
for turn in tracker.get_all_turns()
|
|
1290
|
+
],
|
|
1291
|
+
"files": [
|
|
1292
|
+
{
|
|
1293
|
+
"path": path,
|
|
1294
|
+
"beforeHash": diff.before,
|
|
1295
|
+
"afterHash": diff.after,
|
|
1296
|
+
"operation": _operation_from_hashes(diff.before, diff.after),
|
|
1297
|
+
# Folded provenance badges: a peer session also wrote
|
|
1298
|
+
# this path / the net change includes window-diff
|
|
1299
|
+
# inference. Foreign-only paths are excluded from
|
|
1300
|
+
# this net-level list entirely (raw turn payloads
|
|
1301
|
+
# keep them).
|
|
1302
|
+
"contested": diff.contested,
|
|
1303
|
+
"inferred": diff.inferred,
|
|
1304
|
+
}
|
|
1305
|
+
for path, diff in sorted(file_summary.items())
|
|
1306
|
+
# Content withheld by SnapshotPolicy (too large / binary):
|
|
1307
|
+
# a None hash would mis-read as create/delete.
|
|
1308
|
+
if not diff.content_unavailable
|
|
1309
|
+
],
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
def _diff_payload(self, session_id: str, *, path: str | None, turn: int | None) -> dict[str, Any]:
|
|
1313
|
+
session = self._manager.get(session_id)
|
|
1314
|
+
tracker = session.host.engine.mutation_tracker
|
|
1315
|
+
if tracker is None:
|
|
1316
|
+
return {"sessionId": session_id, "entries": []}
|
|
1317
|
+
summary = tracker.get_turn_file_summary(turn) if turn is not None else tracker.get_session_file_summary()
|
|
1318
|
+
entries = []
|
|
1319
|
+
for file_path, diff in sorted(summary.items()):
|
|
1320
|
+
if path is not None and file_path != path:
|
|
1321
|
+
continue
|
|
1322
|
+
if diff.content_unavailable:
|
|
1323
|
+
continue
|
|
1324
|
+
entries.append(_diff_entry_payload(tracker, file_path, diff))
|
|
1325
|
+
return {
|
|
1326
|
+
"sessionId": session_id,
|
|
1327
|
+
"turn": turn,
|
|
1328
|
+
"entries": entries,
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
async def _request_input(self, session_id: str, event: QuestionToUser) -> None:
|
|
1332
|
+
client = self._client_or_error()
|
|
1333
|
+
event_bus = self._manager.get(session_id).host.event_bus
|
|
1334
|
+
loop = asyncio.get_running_loop()
|
|
1335
|
+
cancel_future: asyncio.Future[None] = loop.create_future()
|
|
1336
|
+
cancel_key = (session_id, event.request_id)
|
|
1337
|
+
already_cancelled = self._install_pending_cancel(
|
|
1338
|
+
self._pending_input_cancels,
|
|
1339
|
+
self._input_cancel_tombstones,
|
|
1340
|
+
cancel_key,
|
|
1341
|
+
cancel_future,
|
|
1342
|
+
)
|
|
1343
|
+
if already_cancelled:
|
|
1344
|
+
# The wait died before we could ask: sending the request anyway
|
|
1345
|
+
# would render a stale dialog on the client (nothing revokes it).
|
|
1346
|
+
await event_bus.publish(
|
|
1347
|
+
AskUserResponse(
|
|
1348
|
+
request_id=event.request_id,
|
|
1349
|
+
cancelled=True,
|
|
1350
|
+
session_id=event.session_id or session_id,
|
|
1351
|
+
)
|
|
1352
|
+
)
|
|
1353
|
+
return
|
|
1354
|
+
|
|
1355
|
+
async def _request(params: dict[str, Any]) -> object | None:
|
|
1356
|
+
if cancel_future.done():
|
|
1357
|
+
return None
|
|
1358
|
+
request_task = asyncio.create_task(client.ext_method("chrys/request_input", params))
|
|
1359
|
+
try:
|
|
1360
|
+
done, _pending = await asyncio.wait(
|
|
1361
|
+
{request_task, cancel_future},
|
|
1362
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
1363
|
+
)
|
|
1364
|
+
if cancel_future in done:
|
|
1365
|
+
return None
|
|
1366
|
+
try:
|
|
1367
|
+
return request_task.result()
|
|
1368
|
+
except asyncio.CancelledError:
|
|
1369
|
+
# Only the client callback died; the prompt itself keeps
|
|
1370
|
+
# running and reads this as "no answer". Cancellation of
|
|
1371
|
+
# this coroutine propagates out of the wait above so an
|
|
1372
|
+
# owner tearing the prompt down never sees a synthetic
|
|
1373
|
+
# response.
|
|
1374
|
+
return None
|
|
1375
|
+
except Exception:
|
|
1376
|
+
return None
|
|
1377
|
+
finally:
|
|
1378
|
+
if request_task.done():
|
|
1379
|
+
_observe_task_exception(request_task)
|
|
1380
|
+
else:
|
|
1381
|
+
request_task.cancel()
|
|
1382
|
+
request_task.add_done_callback(_observe_task_exception)
|
|
1383
|
+
|
|
1384
|
+
try:
|
|
1385
|
+
response = await _request(
|
|
1386
|
+
{
|
|
1387
|
+
"sessionId": session_id,
|
|
1388
|
+
"requestId": event.request_id,
|
|
1389
|
+
"questions": [_request_input_question_payload(item) for item in event.questions],
|
|
1390
|
+
"callerName": event.caller_name,
|
|
1391
|
+
}
|
|
1392
|
+
)
|
|
1393
|
+
finally:
|
|
1394
|
+
self._pending_input_cancels.pop(cancel_key, None)
|
|
1395
|
+
outcome = validate_request_input_response(response, questions=event.questions)
|
|
1396
|
+
if cancel_future.done() or not isinstance(outcome, tuple):
|
|
1397
|
+
response_event = AskUserResponse(request_id=event.request_id, cancelled=True)
|
|
1398
|
+
else:
|
|
1399
|
+
response_event = AskUserResponse(request_id=event.request_id, answers=outcome)
|
|
1400
|
+
response_event.session_id = event.session_id or session_id
|
|
1401
|
+
await event_bus.publish(response_event)
|
|
1402
|
+
|
|
1403
|
+
async def _request_permission(self, session_id: str, event: ApprovalRequest, *, reason: str = "") -> None:
|
|
1404
|
+
client = self._client_or_error()
|
|
1405
|
+
event_bus = self._manager.get(session_id).host.event_bus
|
|
1406
|
+
tool_call = acp_schema.ToolCallUpdate(
|
|
1407
|
+
toolCallId=event.request_id,
|
|
1408
|
+
title=tool_call_title(event.tool_name, event.tool_kind, event.args, intent_summary=event.intent_summary),
|
|
1409
|
+
kind=acp_tool_kind(event.tool_kind),
|
|
1410
|
+
status="pending",
|
|
1411
|
+
rawInput=event.args,
|
|
1412
|
+
rawOutput=reason or None,
|
|
1413
|
+
field_meta={
|
|
1414
|
+
"chrys": {
|
|
1415
|
+
"tool_name": event.tool_name,
|
|
1416
|
+
"tool_kind": event.tool_kind,
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
)
|
|
1420
|
+
loop = asyncio.get_running_loop()
|
|
1421
|
+
cancel_future: asyncio.Future[None] = loop.create_future()
|
|
1422
|
+
cancel_key = (session_id, event.request_id)
|
|
1423
|
+
already_cancelled = self._install_pending_cancel(
|
|
1424
|
+
self._pending_permission_cancels,
|
|
1425
|
+
self._permission_cancel_tombstones,
|
|
1426
|
+
cancel_key,
|
|
1427
|
+
cancel_future,
|
|
1428
|
+
)
|
|
1429
|
+
if already_cancelled:
|
|
1430
|
+
# The wait died before we could ask: sending the request anyway
|
|
1431
|
+
# would render a stale dialog on the client (nothing revokes it).
|
|
1432
|
+
await event_bus.publish(
|
|
1433
|
+
ApprovalResponse(
|
|
1434
|
+
request_id=event.request_id,
|
|
1435
|
+
approved=False,
|
|
1436
|
+
reason="ACP permission request was cancelled.",
|
|
1437
|
+
session_id=event.session_id or session_id,
|
|
1438
|
+
)
|
|
1439
|
+
)
|
|
1440
|
+
return
|
|
1441
|
+
request_task = asyncio.create_task(
|
|
1442
|
+
client.request_permission(
|
|
1443
|
+
session_id=session_id,
|
|
1444
|
+
tool_call=tool_call,
|
|
1445
|
+
options=[
|
|
1446
|
+
acp_schema.PermissionOption(
|
|
1447
|
+
optionId=_ALLOW_OPTION_ID,
|
|
1448
|
+
name="Allow once",
|
|
1449
|
+
kind="allow_once",
|
|
1450
|
+
),
|
|
1451
|
+
acp_schema.PermissionOption(
|
|
1452
|
+
optionId=_REJECT_OPTION_ID,
|
|
1453
|
+
name="Reject once",
|
|
1454
|
+
kind="reject_once",
|
|
1455
|
+
),
|
|
1456
|
+
],
|
|
1457
|
+
)
|
|
1458
|
+
)
|
|
1459
|
+
self._pending_permission_tasks[cancel_key] = request_task
|
|
1460
|
+
try:
|
|
1461
|
+
done, _pending = await asyncio.wait(
|
|
1462
|
+
{request_task, cancel_future},
|
|
1463
|
+
timeout=self._permission_timeout_seconds,
|
|
1464
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
1465
|
+
)
|
|
1466
|
+
if cancel_future in done:
|
|
1467
|
+
approved = False
|
|
1468
|
+
reason_text = "ACP permission request was cancelled."
|
|
1469
|
+
elif request_task in done:
|
|
1470
|
+
try:
|
|
1471
|
+
response = request_task.result()
|
|
1472
|
+
except asyncio.CancelledError:
|
|
1473
|
+
approved = False
|
|
1474
|
+
reason_text = "ACP permission request failed or was cancelled."
|
|
1475
|
+
else:
|
|
1476
|
+
approved = (
|
|
1477
|
+
isinstance(response.outcome, acp_schema.AllowedOutcome)
|
|
1478
|
+
and response.outcome.option_id == _ALLOW_OPTION_ID
|
|
1479
|
+
)
|
|
1480
|
+
if approved:
|
|
1481
|
+
reason_text = ""
|
|
1482
|
+
elif isinstance(response.outcome, acp_schema.DeniedOutcome):
|
|
1483
|
+
reason_text = "ACP permission request was cancelled."
|
|
1484
|
+
else:
|
|
1485
|
+
reason_text = "Rejected by ACP client."
|
|
1486
|
+
else:
|
|
1487
|
+
approved = False
|
|
1488
|
+
reason_text = "ACP permission request timed out."
|
|
1489
|
+
except asyncio.CancelledError:
|
|
1490
|
+
raise
|
|
1491
|
+
except Exception:
|
|
1492
|
+
approved = False
|
|
1493
|
+
reason_text = "ACP permission request failed or was cancelled."
|
|
1494
|
+
finally:
|
|
1495
|
+
if self._pending_permission_cancels.get(cancel_key) is cancel_future:
|
|
1496
|
+
self._pending_permission_cancels.pop(cancel_key, None)
|
|
1497
|
+
if self._pending_permission_tasks.get(cancel_key) is request_task:
|
|
1498
|
+
self._pending_permission_tasks.pop(cancel_key, None)
|
|
1499
|
+
if request_task.done():
|
|
1500
|
+
_observe_task_exception(request_task)
|
|
1501
|
+
else:
|
|
1502
|
+
request_task.cancel()
|
|
1503
|
+
request_task.add_done_callback(_observe_task_exception)
|
|
1504
|
+
await event_bus.publish(
|
|
1505
|
+
ApprovalResponse(
|
|
1506
|
+
request_id=event.request_id,
|
|
1507
|
+
approved=approved,
|
|
1508
|
+
reason=reason_text,
|
|
1509
|
+
session_id=event.session_id or session_id,
|
|
1510
|
+
)
|
|
1511
|
+
)
|
|
1512
|
+
|
|
1513
|
+
def _client_or_error(self) -> Client:
|
|
1514
|
+
if self._client is None:
|
|
1515
|
+
raise RequestError.internal_error({"details": "ACP client connection is not ready."})
|
|
1516
|
+
return self._client
|
|
1517
|
+
|
|
1518
|
+
async def _interrupt_and_cancel_pending_waits(self, session_id: str) -> None:
|
|
1519
|
+
try:
|
|
1520
|
+
await self._manager.cancel(session_id)
|
|
1521
|
+
except AcpSessionError:
|
|
1522
|
+
pass
|
|
1523
|
+
finally:
|
|
1524
|
+
self._cancel_pending_waits(session_id)
|
|
1525
|
+
|
|
1526
|
+
def _cancel_pending_waits(self, session_id: str) -> None:
|
|
1527
|
+
self._resolve_pending_cancels(self._pending_input_cancels, session_id)
|
|
1528
|
+
self._resolve_pending_cancels(self._pending_permission_cancels, session_id)
|
|
1529
|
+
|
|
1530
|
+
@staticmethod
|
|
1531
|
+
def _deliver_or_record_cancellation(
|
|
1532
|
+
pending_cancels: dict[tuple[str, str], asyncio.Future[None]],
|
|
1533
|
+
tombstones: set[tuple[str, str]],
|
|
1534
|
+
key: tuple[str, str],
|
|
1535
|
+
) -> None:
|
|
1536
|
+
future = pending_cancels.get(key)
|
|
1537
|
+
if future is None:
|
|
1538
|
+
tombstones.add(key)
|
|
1539
|
+
elif not future.done():
|
|
1540
|
+
future.set_result(None)
|
|
1541
|
+
|
|
1542
|
+
@staticmethod
|
|
1543
|
+
def _install_pending_cancel(
|
|
1544
|
+
pending_cancels: dict[tuple[str, str], asyncio.Future[None]],
|
|
1545
|
+
tombstones: set[tuple[str, str]],
|
|
1546
|
+
key: tuple[str, str],
|
|
1547
|
+
future: asyncio.Future[None],
|
|
1548
|
+
) -> bool:
|
|
1549
|
+
"""Install a waiter and consume an early cancellation without an await gap.
|
|
1550
|
+
|
|
1551
|
+
Returns True when an early-cancellation tombstone was consumed: the
|
|
1552
|
+
wait is already dead, so the caller must not send the client a
|
|
1553
|
+
request nobody is waiting on.
|
|
1554
|
+
"""
|
|
1555
|
+
if key in tombstones:
|
|
1556
|
+
tombstones.remove(key)
|
|
1557
|
+
future.set_result(None)
|
|
1558
|
+
return True
|
|
1559
|
+
pending_cancels[key] = future
|
|
1560
|
+
return False
|
|
1561
|
+
|
|
1562
|
+
def _clear_cancellation_state(self, session_id: str) -> None:
|
|
1563
|
+
self._cancel_pending_waits(session_id)
|
|
1564
|
+
self._drop_session_tombstones(session_id)
|
|
1565
|
+
|
|
1566
|
+
def _drop_session_tombstones(self, session_id: str) -> None:
|
|
1567
|
+
self._input_cancel_tombstones = {key for key in self._input_cancel_tombstones if key[0] != session_id}
|
|
1568
|
+
self._permission_cancel_tombstones = {key for key in self._permission_cancel_tombstones if key[0] != session_id}
|
|
1569
|
+
|
|
1570
|
+
@staticmethod
|
|
1571
|
+
def _resolve_pending_cancels(
|
|
1572
|
+
pending_cancels: dict[tuple[str, str], asyncio.Future[None]],
|
|
1573
|
+
session_id: str,
|
|
1574
|
+
) -> None:
|
|
1575
|
+
for (pending_session_id, _request_id), future in list(pending_cancels.items()):
|
|
1576
|
+
if pending_session_id == session_id and not future.done():
|
|
1577
|
+
future.set_result(None)
|
|
1578
|
+
|
|
1579
|
+
@staticmethod
|
|
1580
|
+
def _reject_additional_directories(additional_directories: list[str] | None) -> None:
|
|
1581
|
+
if additional_directories:
|
|
1582
|
+
raise RequestError.invalid_params(
|
|
1583
|
+
{"details": f"additionalDirectories are not supported by {APP_DISPLAY_NAME} ACP phase 1."}
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
def _request_error(exc: Exception, *, usage: dict[str, int] | None = None) -> RequestError:
|
|
1588
|
+
if isinstance(exc, RequestError):
|
|
1589
|
+
if usage is None:
|
|
1590
|
+
return exc
|
|
1591
|
+
data = dict(exc.data) if isinstance(exc.data, dict) else {}
|
|
1592
|
+
data["usage"] = usage
|
|
1593
|
+
return RequestError(exc.code, str(exc), data)
|
|
1594
|
+
if isinstance(exc, AcpSessionError):
|
|
1595
|
+
data: dict[str, object] = {"details": str(exc)}
|
|
1596
|
+
if usage is not None:
|
|
1597
|
+
data["usage"] = usage
|
|
1598
|
+
return RequestError.invalid_params(data)
|
|
1599
|
+
details = str(exc)
|
|
1600
|
+
details = f"{type(exc).__name__}: {details}" if details else type(exc).__name__
|
|
1601
|
+
data: dict[str, object] = {"details": details}
|
|
1602
|
+
if usage is not None:
|
|
1603
|
+
data["usage"] = usage
|
|
1604
|
+
return RequestError.internal_error(data)
|
|
1605
|
+
|
|
1606
|
+
|
|
1607
|
+
def _prompt_usage_payload(event: UsageUpdate) -> dict[str, int]:
|
|
1608
|
+
"""Return strict cumulative ACP spend aliases from one locked engine snapshot."""
|
|
1609
|
+
payload = {
|
|
1610
|
+
"inputTokens": max(0, event.total_session_input_tokens),
|
|
1611
|
+
"outputTokens": max(0, event.total_session_output_tokens),
|
|
1612
|
+
"totalTokens": max(0, event.total_session_tokens),
|
|
1613
|
+
}
|
|
1614
|
+
if event.total_session_cache_hit_tokens is not None:
|
|
1615
|
+
payload["cachedReadTokens"] = max(0, event.total_session_cache_hit_tokens)
|
|
1616
|
+
return payload
|
|
1617
|
+
|
|
1618
|
+
|
|
1619
|
+
def _observe_task_exception(task: asyncio.Task[Any]) -> None:
|
|
1620
|
+
if task.cancelled():
|
|
1621
|
+
return
|
|
1622
|
+
with contextlib.suppress(Exception):
|
|
1623
|
+
task.exception()
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
def _runtime_update_payload(event: AgentRuntimeUpdated) -> dict[str, Any]:
|
|
1627
|
+
return {
|
|
1628
|
+
"modelProfileId": event.model_profile_id,
|
|
1629
|
+
"maxContextTokens": event.max_context_tokens,
|
|
1630
|
+
"toolNames": event.tool_names,
|
|
1631
|
+
"skillNames": event.skill_names,
|
|
1632
|
+
"memoryFiles": event.memory_files,
|
|
1633
|
+
"runtimeDetails": jsonable_dataclass(event.runtime_details),
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
|
|
1637
|
+
def _rollback_result_payload(event: RollbackResult) -> dict[str, Any]:
|
|
1638
|
+
return {
|
|
1639
|
+
"sessionId": event.session_id,
|
|
1640
|
+
"targetTurn": event.target_turn,
|
|
1641
|
+
"rolledBackUserText": event.rolled_back_user_text,
|
|
1642
|
+
"filesReverted": event.files_reverted,
|
|
1643
|
+
"restoreResults": [
|
|
1644
|
+
{
|
|
1645
|
+
"path": result.path,
|
|
1646
|
+
"outcome": result.outcome.value,
|
|
1647
|
+
"reason": result.reason,
|
|
1648
|
+
"changed": result.changed,
|
|
1649
|
+
"ok": result.ok,
|
|
1650
|
+
}
|
|
1651
|
+
for result in event.restore_results
|
|
1652
|
+
],
|
|
1653
|
+
# Files the rollback plan dropped, with a RollbackExclusionReason
|
|
1654
|
+
# value ("unrestorable" / "move_poisoned" / ...) — never silent.
|
|
1655
|
+
"exclusions": [{"path": path, "reason": reason} for path, reason in event.exclusions],
|
|
1656
|
+
# Advisory repo-level notices (plan warnings), plain strings.
|
|
1657
|
+
"warnings": list(event.warnings),
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
def _profile_switched_payload(event: ProfileSwitched, *, session_id: str) -> dict[str, Any]:
|
|
1662
|
+
return {
|
|
1663
|
+
"sessionId": session_id,
|
|
1664
|
+
"fromProfile": event.from_profile,
|
|
1665
|
+
"toProfile": event.to_profile,
|
|
1666
|
+
"fromDisplayName": event.from_display_name,
|
|
1667
|
+
"toDisplayName": event.to_display_name,
|
|
1668
|
+
"messageCount": event.message_count,
|
|
1669
|
+
"modelProfileId": event.model_profile_id,
|
|
1670
|
+
"maxContextTokens": event.max_context_tokens,
|
|
1671
|
+
"toolNames": event.tool_names,
|
|
1672
|
+
"skillNames": event.skill_names,
|
|
1673
|
+
"subAgentToolNames": event.sub_agent_tool_names,
|
|
1674
|
+
"memoryFiles": event.memory_files,
|
|
1675
|
+
"runtimeDetails": jsonable_dataclass(event.runtime_details),
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
def _mutation_payload(mutation: Any) -> dict[str, Any]:
|
|
1680
|
+
return {
|
|
1681
|
+
"path": mutation.path,
|
|
1682
|
+
"operation": mutation.operation.value,
|
|
1683
|
+
"source": mutation.source.value,
|
|
1684
|
+
"toolCallId": mutation.tool_call_id,
|
|
1685
|
+
"timestamp": mutation.timestamp,
|
|
1686
|
+
"oldPath": mutation.old_path,
|
|
1687
|
+
"beforeHash": mutation.before_hash,
|
|
1688
|
+
"afterHash": mutation.after_hash,
|
|
1689
|
+
# Why a side's content backup was withheld ("too_large" /
|
|
1690
|
+
# "binary"), or None when the hash is real / the file was absent.
|
|
1691
|
+
"beforeSkip": mutation.before_skip.value if mutation.before_skip else None,
|
|
1692
|
+
"afterSkip": mutation.after_skip.value if mutation.after_skip else None,
|
|
1693
|
+
# Attribution confidence ("proven" / "assumed" / "foreign") and
|
|
1694
|
+
# peer-conflict flag. Raw turn payloads stay unfiltered —
|
|
1695
|
+
# foreign rows remain present so clients can filter or badge
|
|
1696
|
+
# themselves (net-level lists exclude them instead).
|
|
1697
|
+
"provenance": mutation.provenance.value if mutation.provenance else None,
|
|
1698
|
+
"contested": mutation.contested,
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
def _diff_entry_payload(tracker: Any, path: str, diff: FileHashDiff) -> dict[str, Any]:
|
|
1703
|
+
before_bytes = tracker.store.read_blob(diff.before) if diff.before else None
|
|
1704
|
+
after_bytes = tracker.store.read_blob(diff.after) if diff.after else None
|
|
1705
|
+
is_binary = bool(
|
|
1706
|
+
(before_bytes and EncodingDetector.looks_binary(before_bytes))
|
|
1707
|
+
or (after_bytes and EncodingDetector.looks_binary(after_bytes))
|
|
1708
|
+
)
|
|
1709
|
+
before_text = "" if before_bytes is None or is_binary else decode_bytes(before_bytes)
|
|
1710
|
+
after_text = "" if after_bytes is None or is_binary else decode_bytes(after_bytes)
|
|
1711
|
+
return {
|
|
1712
|
+
"path": path,
|
|
1713
|
+
"operation": _operation_from_hashes(diff.before, diff.after),
|
|
1714
|
+
"beforeHash": diff.before,
|
|
1715
|
+
"afterHash": diff.after,
|
|
1716
|
+
"beforeText": before_text,
|
|
1717
|
+
"afterText": after_text,
|
|
1718
|
+
"isBinary": is_binary,
|
|
1719
|
+
"bytesChanged": diff.before != diff.after,
|
|
1720
|
+
# Folded provenance badges — same semantics as the ``files``
|
|
1721
|
+
# list in session/mutations, so diff clients can badge entries
|
|
1722
|
+
# without joining against that endpoint.
|
|
1723
|
+
"contested": diff.contested,
|
|
1724
|
+
"inferred": diff.inferred,
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
def _operation_from_hashes(before_hash: str | None, after_hash: str | None) -> str:
|
|
1729
|
+
if before_hash is None and after_hash is not None:
|
|
1730
|
+
return "create"
|
|
1731
|
+
if before_hash is not None and after_hash is None:
|
|
1732
|
+
return "delete"
|
|
1733
|
+
return "modify"
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
def _event_session_id(event: Event, fallback: str) -> str:
|
|
1737
|
+
value = event.session_id
|
|
1738
|
+
return value if isinstance(value, str) and value else fallback
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
def _model_info_description(profile: dict[str, Any]) -> str | None:
|
|
1742
|
+
parts = [str(profile[key]) for key in ("provider", "modelId") if profile.get(key)]
|
|
1743
|
+
return " · ".join(parts) or None
|
|
1744
|
+
|
|
1745
|
+
|
|
1746
|
+
def _string_param(params: dict[str, Any], key: str) -> str:
|
|
1747
|
+
value = params.get(key, "")
|
|
1748
|
+
return value if isinstance(value, str) else ""
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
def _optional_string_param(params: dict[str, Any], key: str) -> str | None:
|
|
1752
|
+
value = params.get(key)
|
|
1753
|
+
if value is None:
|
|
1754
|
+
return None
|
|
1755
|
+
if not isinstance(value, str):
|
|
1756
|
+
raise RequestError.invalid_params({"details": f"{key} must be a string."})
|
|
1757
|
+
return value
|
|
1758
|
+
|
|
1759
|
+
|
|
1760
|
+
def _int_param(params: dict[str, Any], key: str) -> int:
|
|
1761
|
+
value = params.get(key)
|
|
1762
|
+
if type(value) is not int:
|
|
1763
|
+
raise RequestError.invalid_params({"details": f"{key} must be an integer."})
|
|
1764
|
+
return value
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
def _optional_int_param(params: dict[str, Any], key: str) -> int | None:
|
|
1768
|
+
value = params.get(key)
|
|
1769
|
+
if value is None:
|
|
1770
|
+
return None
|
|
1771
|
+
if type(value) is not int:
|
|
1772
|
+
raise RequestError.invalid_params({"details": f"{key} must be an integer."})
|
|
1773
|
+
return value
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
def _bool_param(params: dict[str, Any], key: str, *, default: bool) -> bool:
|
|
1777
|
+
value = params.get(key, default)
|
|
1778
|
+
if not isinstance(value, bool):
|
|
1779
|
+
raise RequestError.invalid_params({"details": f"{key} must be a boolean."})
|
|
1780
|
+
return value
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
def _dict_param(params: dict[str, Any], key: str) -> dict[str, Any]:
|
|
1784
|
+
value = params.get(key)
|
|
1785
|
+
if isinstance(value, dict):
|
|
1786
|
+
return value
|
|
1787
|
+
raise RequestError.invalid_params({"details": f"{key} must be an object."})
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
def _string_list_param(params: dict[str, Any], key: str) -> list[str] | None:
|
|
1791
|
+
value = params.get(key)
|
|
1792
|
+
if value is None:
|
|
1793
|
+
return None
|
|
1794
|
+
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
1795
|
+
raise RequestError.invalid_params({"details": f"{key} must be a list of strings."})
|
|
1796
|
+
return value
|