iCode-TUI 0.29.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chrys/__init__.py +16 -0
- chrys/app/__init__.py +3 -0
- chrys/app/acp/__init__.py +3 -0
- chrys/app/acp/bridge.py +664 -0
- chrys/app/acp/content.py +132 -0
- chrys/app/acp/doc/frontend-api.md +425 -0
- chrys/app/acp/history.py +585 -0
- chrys/app/acp/server.py +1796 -0
- chrys/app/acp/session_manager.py +1815 -0
- chrys/app/acp/tool_status.py +45 -0
- chrys/app/cli/__init__.py +3 -0
- chrys/app/cli/acp.py +178 -0
- chrys/app/cli/app.py +165 -0
- chrys/app/cli/approvals.py +101 -0
- chrys/app/cli/headless.py +223 -0
- chrys/app/cli/launch_cwd.py +42 -0
- chrys/app/cli/profiles.py +298 -0
- chrys/app/cli/progress.py +857 -0
- chrys/app/cli/run.py +425 -0
- chrys/app/cli/serve.py +1593 -0
- chrys/app/cli/tool_summary.py +225 -0
- chrys/app/cli/trajectory.py +201 -0
- chrys/app/cli/workflow.py +601 -0
- chrys/app/features/__init__.py +3 -0
- chrys/app/features/buddy/__init__.py +3 -0
- chrys/app/features/buddy/actions.py +68 -0
- chrys/app/features/buddy/animation.py +76 -0
- chrys/app/features/buddy/commands.py +196 -0
- chrys/app/features/buddy/docs/design.md +187 -0
- chrys/app/features/buddy/hatchery.py +80 -0
- chrys/app/features/buddy/lifecycle.py +41 -0
- chrys/app/features/buddy/model.py +265 -0
- chrys/app/features/buddy/pixel_renderer.py +113 -0
- chrys/app/features/buddy/pixel_sprites.py +279 -0
- chrys/app/features/buddy/portrait.py +76 -0
- chrys/app/features/buddy/progression.py +58 -0
- chrys/app/features/buddy/replies.py +191 -0
- chrys/app/features/buddy/sprites/alien.toml +70 -0
- chrys/app/features/buddy/sprites/axolotl.toml +71 -0
- chrys/app/features/buddy/sprites/bat.toml +69 -0
- chrys/app/features/buddy/sprites/bee.toml +69 -0
- chrys/app/features/buddy/sprites/cactus.toml +73 -0
- chrys/app/features/buddy/sprites/capybara.toml +70 -0
- chrys/app/features/buddy/sprites/cat.toml +69 -0
- chrys/app/features/buddy/sprites/crab.toml +70 -0
- chrys/app/features/buddy/sprites/dragon.toml +71 -0
- chrys/app/features/buddy/sprites/duck.toml +69 -0
- chrys/app/features/buddy/sprites/fox.toml +69 -0
- chrys/app/features/buddy/sprites/frog.toml +70 -0
- chrys/app/features/buddy/sprites/ghost.toml +70 -0
- chrys/app/features/buddy/sprites/goose.toml +69 -0
- chrys/app/features/buddy/sprites/jelly.toml +70 -0
- chrys/app/features/buddy/sprites/llama.toml +70 -0
- chrys/app/features/buddy/sprites/mushroom.toml +69 -0
- chrys/app/features/buddy/sprites/octopus.toml +70 -0
- chrys/app/features/buddy/sprites/owl.toml +70 -0
- chrys/app/features/buddy/sprites/panda.toml +71 -0
- chrys/app/features/buddy/sprites/penguin.toml +69 -0
- chrys/app/features/buddy/sprites/rabbit.toml +70 -0
- chrys/app/features/buddy/sprites/robot.toml +72 -0
- chrys/app/features/buddy/sprites/shark.toml +70 -0
- chrys/app/features/buddy/sprites/snail.toml +69 -0
- chrys/app/features/buddy/sprites/snake.toml +69 -0
- chrys/app/features/buddy/sprites/tabby.toml +69 -0
- chrys/app/features/buddy/sprites/turtle.toml +69 -0
- chrys/app/features/buddy/store.py +143 -0
- chrys/app/features/session_title/__init__.py +7 -0
- chrys/app/features/session_title/updater.py +582 -0
- chrys/app/installer.py +651 -0
- chrys/app/parsing.py +17 -0
- chrys/app/tui/__init__.py +3 -0
- chrys/app/tui/app.py +1287 -0
- chrys/app/tui/behaviors/__init__.py +3 -0
- chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
- chrys/app/tui/behaviors/insert_clipboard.py +73 -0
- chrys/app/tui/behaviors/right_click_copy.py +146 -0
- chrys/app/tui/binding_display.py +139 -0
- chrys/app/tui/buddy_messages.py +17 -0
- chrys/app/tui/buddy_reply.py +86 -0
- chrys/app/tui/chrys.tcss +549 -0
- chrys/app/tui/clipboard.py +109 -0
- chrys/app/tui/copy_messages.py +10 -0
- chrys/app/tui/i18n.py +230 -0
- chrys/app/tui/language.py +58 -0
- chrys/app/tui/notifications/__init__.py +12 -0
- chrys/app/tui/notifications/drivers.py +180 -0
- chrys/app/tui/notifications/service.py +126 -0
- chrys/app/tui/notifications/settings.py +141 -0
- chrys/app/tui/screens/__init__.py +3 -0
- chrys/app/tui/screens/agents/__init__.py +8 -0
- chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
- chrys/app/tui/screens/agents/config.py +2142 -0
- chrys/app/tui/screens/agents/config.tcss +169 -0
- chrys/app/tui/screens/agents/panels/__init__.py +3 -0
- chrys/app/tui/screens/agents/panels/acp.py +1176 -0
- chrys/app/tui/screens/agents/panels/basic.py +497 -0
- chrys/app/tui/screens/agents/panels/compaction.py +231 -0
- chrys/app/tui/screens/agents/panels/config_card.py +218 -0
- chrys/app/tui/screens/agents/panels/instructions.py +91 -0
- chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
- chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
- chrys/app/tui/screens/agents/panels/memory.py +684 -0
- chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
- chrys/app/tui/screens/agents/panels/skills.py +806 -0
- chrys/app/tui/screens/agents/panels/subagents.py +652 -0
- chrys/app/tui/screens/agents/panels/tools.py +205 -0
- chrys/app/tui/screens/agents/picker.py +103 -0
- chrys/app/tui/screens/agents/skill_paths.py +38 -0
- chrys/app/tui/screens/agents/validation_messages.py +452 -0
- chrys/app/tui/screens/dialogs/__init__.py +35 -0
- chrys/app/tui/screens/dialogs/agent_load.py +834 -0
- chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
- chrys/app/tui/screens/dialogs/app_mode.py +55 -0
- chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
- chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
- chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
- chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
- chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
- chrys/app/tui/screens/dialogs/approval/body.py +110 -0
- chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
- chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
- chrys/app/tui/screens/dialogs/ask_user.py +148 -0
- chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
- chrys/app/tui/screens/dialogs/base.py +84 -0
- chrys/app/tui/screens/dialogs/confirm.py +184 -0
- chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
- chrys/app/tui/screens/dialogs/connection_test.py +183 -0
- chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
- chrys/app/tui/screens/dialogs/diagram.py +102 -0
- chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
- chrys/app/tui/screens/dialogs/editor.py +378 -0
- chrys/app/tui/screens/dialogs/editor.tcss +132 -0
- chrys/app/tui/screens/dialogs/file_picker.py +582 -0
- chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
- chrys/app/tui/screens/dialogs/fork_session.py +219 -0
- chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
- chrys/app/tui/screens/dialogs/image_compression.py +102 -0
- chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
- chrys/app/tui/screens/dialogs/man_page.py +172 -0
- chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
- chrys/app/tui/screens/dialogs/new_folder.py +277 -0
- chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
- chrys/app/tui/screens/dialogs/notice.tcss +14 -0
- chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
- chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
- chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
- chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
- chrys/app/tui/screens/dialogs/session_title.py +110 -0
- chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
- chrys/app/tui/screens/dialogs/tool_view.py +207 -0
- chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
- chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
- chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
- chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
- chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
- chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
- chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
- chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
- chrys/app/tui/screens/diff/__init__.py +22 -0
- chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
- chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
- chrys/app/tui/screens/diff/screen.py +1532 -0
- chrys/app/tui/screens/diff/screen.tcss +104 -0
- chrys/app/tui/screens/guides/__init__.py +7 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
- chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/index.py +215 -0
- chrys/app/tui/screens/guides/reader.py +173 -0
- chrys/app/tui/screens/guides/screen.py +442 -0
- chrys/app/tui/screens/guides/screen.tcss +52 -0
- chrys/app/tui/screens/logs/__init__.py +7 -0
- chrys/app/tui/screens/logs/screen.py +504 -0
- chrys/app/tui/screens/logs/screen.tcss +45 -0
- chrys/app/tui/screens/main/__init__.py +7 -0
- chrys/app/tui/screens/main/action_availability.py +30 -0
- chrys/app/tui/screens/main/buddy_command.py +143 -0
- chrys/app/tui/screens/main/commands.py +1073 -0
- chrys/app/tui/screens/main/config_actions.py +464 -0
- chrys/app/tui/screens/main/copy_actions.py +117 -0
- chrys/app/tui/screens/main/dialog_controllers.py +826 -0
- chrys/app/tui/screens/main/dialog_gateway.py +166 -0
- chrys/app/tui/screens/main/diff_controller.py +478 -0
- chrys/app/tui/screens/main/engine_read_model.py +184 -0
- chrys/app/tui/screens/main/event_handlers.py +2014 -0
- chrys/app/tui/screens/main/input_flow.py +241 -0
- chrys/app/tui/screens/main/live_diff.py +244 -0
- chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
- chrys/app/tui/screens/main/model_indicator.py +217 -0
- chrys/app/tui/screens/main/navigation.py +343 -0
- chrys/app/tui/screens/main/ports.py +571 -0
- chrys/app/tui/screens/main/recent_dirs.py +123 -0
- chrys/app/tui/screens/main/rollback_controller.py +586 -0
- chrys/app/tui/screens/main/runtime_info.py +71 -0
- chrys/app/tui/screens/main/screen.py +2265 -0
- chrys/app/tui/screens/main/screen.tcss +21 -0
- chrys/app/tui/screens/main/session_handlers.py +1127 -0
- chrys/app/tui/screens/main/session_title.py +239 -0
- chrys/app/tui/screens/main/settings_coordinator.py +365 -0
- chrys/app/tui/screens/main/settings_persistence.py +184 -0
- chrys/app/tui/screens/main/shell_mode.py +112 -0
- chrys/app/tui/screens/main/state.py +289 -0
- chrys/app/tui/screens/main/submit_state.py +11 -0
- chrys/app/tui/screens/main/subscriptions.py +177 -0
- chrys/app/tui/screens/main/suggestions.py +982 -0
- chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
- chrys/app/tui/screens/main/view_adapter.py +1567 -0
- chrys/app/tui/screens/main/workflow_browser.py +747 -0
- chrys/app/tui/screens/main/workflow_content.py +248 -0
- chrys/app/tui/screens/main/workflow_controller.py +240 -0
- chrys/app/tui/screens/main/workflow_feedback.py +137 -0
- chrys/app/tui/screens/main/workflow_flow.py +49 -0
- chrys/app/tui/screens/main/workflow_run_control.py +456 -0
- chrys/app/tui/screens/main/workflow_session_view.py +316 -0
- chrys/app/tui/screens/main/workspace_actions.py +272 -0
- chrys/app/tui/screens/main/workspace_branch.py +194 -0
- chrys/app/tui/screens/menus/__init__.py +8 -0
- chrys/app/tui/screens/menus/languages.py +114 -0
- chrys/app/tui/screens/models/__init__.py +7 -0
- chrys/app/tui/screens/models/picker.py +147 -0
- chrys/app/tui/screens/models/screen.py +2043 -0
- chrys/app/tui/screens/models/screen.tcss +255 -0
- chrys/app/tui/screens/sessions/__init__.py +8 -0
- chrys/app/tui/screens/sessions/presenter.py +586 -0
- chrys/app/tui/screens/sessions/screen.py +779 -0
- chrys/app/tui/screens/sessions/screen.tcss +127 -0
- chrys/app/tui/screens/settings/__init__.py +29 -0
- chrys/app/tui/screens/settings/danger.py +26 -0
- chrys/app/tui/screens/settings/dialog.py +281 -0
- chrys/app/tui/screens/settings/layout.py +519 -0
- chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
- chrys/app/tui/screens/settings/panes/__init__.py +3 -0
- chrys/app/tui/screens/settings/panes/notifications.py +248 -0
- chrys/app/tui/screens/settings/panes/sessions.py +148 -0
- chrys/app/tui/screens/settings/ports.py +55 -0
- chrys/app/tui/screens/settings/provenance.py +102 -0
- chrys/app/tui/screens/settings/rows.py +546 -0
- chrys/app/tui/screens/settings/settings.tcss +406 -0
- chrys/app/tui/screens/themes/__init__.py +8 -0
- chrys/app/tui/screens/themes/dialogs.py +330 -0
- chrys/app/tui/screens/themes/editor.py +632 -0
- chrys/app/tui/screens/themes/editor.tcss +268 -0
- chrys/app/tui/screens/themes/messages.py +95 -0
- chrys/app/tui/screens/themes/palette.py +694 -0
- chrys/app/tui/screens/themes/panel.py +113 -0
- chrys/app/tui/screens/themes/picker.py +203 -0
- chrys/app/tui/screens/themes/save.py +191 -0
- chrys/app/tui/support/__init__.py +3 -0
- chrys/app/tui/support/file_manager.py +64 -0
- chrys/app/tui/support/gc_freeze.py +1114 -0
- chrys/app/tui/support/path_shortcuts.py +50 -0
- chrys/app/tui/support/prompt_history.py +298 -0
- chrys/app/tui/support/workspace_mru.py +476 -0
- chrys/app/tui/terminal/__init__.py +6 -0
- chrys/app/tui/terminal/_pty_backend.py +114 -0
- chrys/app/tui/terminal/emulator/__init__.py +24 -0
- chrys/app/tui/terminal/emulator/buffer.py +456 -0
- chrys/app/tui/terminal/emulator/charsets.py +113 -0
- chrys/app/tui/terminal/emulator/core.py +963 -0
- chrys/app/tui/terminal/emulator/input.py +342 -0
- chrys/app/tui/terminal/emulator/parser.py +227 -0
- chrys/app/tui/terminal/emulator/pen.py +158 -0
- chrys/app/tui/terminal/launcher.py +455 -0
- chrys/app/tui/terminal/leaked_reports.py +44 -0
- chrys/app/tui/terminal/panel.py +323 -0
- chrys/app/tui/terminal/pty_process.py +405 -0
- chrys/app/tui/terminal/rendering.py +151 -0
- chrys/app/tui/terminal/shell.py +273 -0
- chrys/app/tui/terminal/shell_integration.py +199 -0
- chrys/app/tui/terminal/title.py +224 -0
- chrys/app/tui/terminal/widget.py +654 -0
- chrys/app/tui/theme.py +422 -0
- chrys/app/tui/theme_loader.py +201 -0
- chrys/app/tui/theme_stylesheet.py +49 -0
- chrys/app/tui/themes/__init__.py +3 -0
- chrys/app/tui/themes/document.py +120 -0
- chrys/app/tui/themes/preview.py +170 -0
- chrys/app/tui/themes/store.py +176 -0
- chrys/app/tui/toasts.py +69 -0
- chrys/app/tui/util/__init__.py +3 -0
- chrys/app/tui/util/context_pressure.py +146 -0
- chrys/app/tui/util/debug_log.py +292 -0
- chrys/app/tui/util/diff_entries.py +64 -0
- chrys/app/tui/util/formatting.py +53 -0
- chrys/app/tui/util/git_branch.py +326 -0
- chrys/app/tui/util/invocation_progress.py +57 -0
- chrys/app/tui/util/logo.py +32 -0
- chrys/app/tui/util/message_gate.py +35 -0
- chrys/app/tui/util/mutation_sources.py +14 -0
- chrys/app/tui/util/picker_text.py +27 -0
- chrys/app/tui/util/removal.py +47 -0
- chrys/app/tui/util/rich_style.py +36 -0
- chrys/app/tui/util/source_text.py +12 -0
- chrys/app/tui/util/static_update.py +88 -0
- chrys/app/tui/util/todo_format.py +51 -0
- chrys/app/tui/util/visibility.py +214 -0
- chrys/app/tui/widgets/__init__.py +60 -0
- chrys/app/tui/widgets/ask_user_controls.py +652 -0
- chrys/app/tui/widgets/ask_user_markdown.py +17 -0
- chrys/app/tui/widgets/ask_user_prompt.py +606 -0
- chrys/app/tui/widgets/buttons.py +52 -0
- chrys/app/tui/widgets/chat/__init__.py +3 -0
- chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
- chrys/app/tui/widgets/chat/chrome.py +296 -0
- chrys/app/tui/widgets/chat/compaction_card.py +353 -0
- chrys/app/tui/widgets/chat/context_fold.py +77 -0
- chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
- chrys/app/tui/widgets/chat/image_preview.py +214 -0
- chrys/app/tui/widgets/chat/live_renderer.py +383 -0
- chrys/app/tui/widgets/chat/messages.py +1039 -0
- chrys/app/tui/widgets/chat/panel.py +1769 -0
- chrys/app/tui/widgets/chat/ports.py +245 -0
- chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
- chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
- chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
- chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
- chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
- chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
- chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
- chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
- chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
- chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
- chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
- chrys/app/tui/widgets/chat/renderers/search.py +348 -0
- chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
- chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
- chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
- chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
- chrys/app/tui/widgets/chat/renderers/web.py +69 -0
- chrys/app/tui/widgets/chat/replay.py +1605 -0
- chrys/app/tui/widgets/chat/replay_mount.py +322 -0
- chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
- chrys/app/tui/widgets/chat/selection_controller.py +435 -0
- chrys/app/tui/widgets/chat/session_json.py +508 -0
- chrys/app/tui/widgets/chat/toc_model.py +156 -0
- chrys/app/tui/widgets/chat/tool_call.py +2478 -0
- chrys/app/tui/widgets/chat/tool_registry.py +753 -0
- chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
- chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
- chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
- chrys/app/tui/widgets/checkbox.py +24 -0
- chrys/app/tui/widgets/chrome/__init__.py +3 -0
- chrys/app/tui/widgets/chrome/app_header.py +283 -0
- chrys/app/tui/widgets/chrome/commands.py +95 -0
- chrys/app/tui/widgets/chrome/file_index.py +68 -0
- chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
- chrys/app/tui/widgets/chrome/footer.py +284 -0
- chrys/app/tui/widgets/chrome/image_paste.py +351 -0
- chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
- chrys/app/tui/widgets/chrome/status_bar.py +911 -0
- chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
- chrys/app/tui/widgets/click_affordance.py +24 -0
- chrys/app/tui/widgets/color_picker/__init__.py +254 -0
- chrys/app/tui/widgets/color_picker/controls.py +171 -0
- chrys/app/tui/widgets/color_picker/model.py +89 -0
- chrys/app/tui/widgets/dialog_buttons.py +58 -0
- chrys/app/tui/widgets/dialog_scroll.py +24 -0
- chrys/app/tui/widgets/diff_view/__init__.py +20 -0
- chrys/app/tui/widgets/diff_view/cells.py +59 -0
- chrys/app/tui/widgets/diff_view/code.py +280 -0
- chrys/app/tui/widgets/diff_view/compute.py +286 -0
- chrys/app/tui/widgets/diff_view/gutter.py +161 -0
- chrys/app/tui/widgets/diff_view/inline.py +77 -0
- chrys/app/tui/widgets/diff_view/palette.py +87 -0
- chrys/app/tui/widgets/diff_view/protocols.py +24 -0
- chrys/app/tui/widgets/diff_view/rows.py +138 -0
- chrys/app/tui/widgets/diff_view/unified.py +267 -0
- chrys/app/tui/widgets/diff_view/widget.py +209 -0
- chrys/app/tui/widgets/editor/__init__.py +79 -0
- chrys/app/tui/widgets/editor/coordinates.py +30 -0
- chrys/app/tui/widgets/editor/highlighter.py +139 -0
- chrys/app/tui/widgets/editor/keymaps.py +357 -0
- chrys/app/tui/widgets/editor/types.py +216 -0
- chrys/app/tui/widgets/editor/vim.py +1195 -0
- chrys/app/tui/widgets/editor/widget.py +401 -0
- chrys/app/tui/widgets/file_preview.py +58 -0
- chrys/app/tui/widgets/hatch.py +138 -0
- chrys/app/tui/widgets/input.py +66 -0
- chrys/app/tui/widgets/loading.py +125 -0
- chrys/app/tui/widgets/markdown/__init__.py +25 -0
- chrys/app/tui/widgets/markdown/_utils.py +21 -0
- chrys/app/tui/widgets/markdown/anchors.py +71 -0
- chrys/app/tui/widgets/markdown/blocks.py +115 -0
- chrys/app/tui/widgets/markdown/copyable.py +163 -0
- chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
- chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
- chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
- chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
- chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
- chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
- chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
- chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
- chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
- chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
- chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
- chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
- chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
- chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
- chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
- chrys/app/tui/widgets/markdown/links.py +47 -0
- chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
- chrys/app/tui/widgets/markdown/math/layout.py +739 -0
- chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
- chrys/app/tui/widgets/markdown/math/parser.py +325 -0
- chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
- chrys/app/tui/widgets/markdown/navigator.py +80 -0
- chrys/app/tui/widgets/markdown/parser.py +1057 -0
- chrys/app/tui/widgets/markdown/stream.py +73 -0
- chrys/app/tui/widgets/markdown/toc.py +98 -0
- chrys/app/tui/widgets/markdown/viewer.py +146 -0
- chrys/app/tui/widgets/markdown/widget.py +2046 -0
- chrys/app/tui/widgets/marquee.py +101 -0
- chrys/app/tui/widgets/option_menu.py +82 -0
- chrys/app/tui/widgets/page_navigator.py +118 -0
- chrys/app/tui/widgets/select.py +196 -0
- chrys/app/tui/widgets/selection.py +59 -0
- chrys/app/tui/widgets/sidebar/__init__.py +3 -0
- chrys/app/tui/widgets/sidebar/buddy.py +367 -0
- chrys/app/tui/widgets/sidebar/context.py +482 -0
- chrys/app/tui/widgets/sidebar/debug.py +195 -0
- chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
- chrys/app/tui/widgets/sidebar/panel.py +198 -0
- chrys/app/tui/widgets/sidebar/tasks.py +198 -0
- chrys/app/tui/widgets/sidebar/toc.py +166 -0
- chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
- chrys/app/tui/widgets/syntax_theme.py +85 -0
- chrys/app/tui/widgets/text_area.py +258 -0
- chrys/app/tui/widgets/trajectory/__init__.py +8 -0
- chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
- chrys/app/tui/widgets/trajectory/insights.py +1266 -0
- chrys/app/tui/widgets/trajectory/overview.py +1101 -0
- chrys/app/tui/widgets/trajectory/panel.py +983 -0
- chrys/app/tui/widgets/trajectory/presentation.py +311 -0
- chrys/app/tui/widgets/trajectory/session_info.py +124 -0
- chrys/app/tui/widgets/trajectory/text_view.py +124 -0
- chrys/app/tui/widgets/trajectory/timeline.py +380 -0
- chrys/app/tui/widgets/welcome.py +126 -0
- chrys/app/tui/widgets/workflow/__init__.py +5 -0
- chrys/app/tui/widgets/workflow/edges.py +89 -0
- chrys/app/tui/widgets/workflow/graph.py +869 -0
- chrys/app/tui/widgets/workflow/info.py +310 -0
- chrys/app/tui/widgets/workflow/node_view.py +27 -0
- chrys/app/tui/widgets/workflow/output.py +225 -0
- chrys/app/tui/widgets/workflow/panel.py +658 -0
- chrys/app/tui/widgets/workflow/projector.py +324 -0
- chrys/app/tui/widgets/workflow/records.py +69 -0
- chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
- chrys/app/tui/widgets/workflow/selection.py +151 -0
- chrys/app/tui/widgets/workflow/source.py +65 -0
- chrys/app/tui/widgets/workflow/text.py +257 -0
- chrys/app/tui/widgets/workflow/values.py +317 -0
- chrys/foundation/__init__.py +0 -0
- chrys/foundation/branding.py +15 -0
- chrys/foundation/config/__init__.py +7 -0
- chrys/foundation/config/coercion.py +374 -0
- chrys/foundation/config/context.py +34 -0
- chrys/foundation/config/env_file.py +141 -0
- chrys/foundation/config/env_layers.py +370 -0
- chrys/foundation/config/migrations.py +521 -0
- chrys/foundation/config/notification_events.py +22 -0
- chrys/foundation/config/process_settings.py +332 -0
- chrys/foundation/config/runtime_pointer.py +133 -0
- chrys/foundation/config/settings.py +1952 -0
- chrys/foundation/config/settings_store.py +996 -0
- chrys/foundation/config/spec.py +396 -0
- chrys/foundation/config/user_settings.py +140 -0
- chrys/foundation/config/warnings.py +262 -0
- chrys/foundation/config/web_values.py +135 -0
- chrys/foundation/config/yaml_store.py +487 -0
- chrys/foundation/errors/__init__.py +68 -0
- chrys/foundation/errors/_legacy.py +117 -0
- chrys/foundation/errors/_text.py +13 -0
- chrys/foundation/errors/_walk.py +85 -0
- chrys/foundation/errors/classify.py +278 -0
- chrys/foundation/errors/display.py +274 -0
- chrys/foundation/errors/formatting.py +283 -0
- chrys/foundation/errors/kinds.py +67 -0
- chrys/foundation/errors/network.py +497 -0
- chrys/foundation/errors/provider.py +445 -0
- chrys/foundation/errors/route.py +85 -0
- chrys/foundation/events/__init__.py +8 -0
- chrys/foundation/events/bus.py +143 -0
- chrys/foundation/events/types.py +1629 -0
- chrys/foundation/events/workflow.py +38 -0
- chrys/foundation/hosted_tools.py +142 -0
- chrys/foundation/i18n/__init__.py +32 -0
- chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
- chrys/foundation/i18n/catalogs.py +42 -0
- chrys/foundation/i18n/formatting.py +202 -0
- chrys/foundation/i18n/locale.py +92 -0
- chrys/foundation/i18n/localizer.py +187 -0
- chrys/foundation/i18n/messages.py +268 -0
- chrys/foundation/io/__init__.py +3 -0
- chrys/foundation/io/result_content.py +237 -0
- chrys/foundation/models/__init__.py +3 -0
- chrys/foundation/models/approval_reuse.py +23 -0
- chrys/foundation/models/ask_user.py +468 -0
- chrys/foundation/models/execution.py +18 -0
- chrys/foundation/models/history_markers.py +115 -0
- chrys/foundation/models/invocations.py +63 -0
- chrys/foundation/models/mutation_scope.py +28 -0
- chrys/foundation/models/session_env.py +87 -0
- chrys/foundation/models/session_surface.py +29 -0
- chrys/foundation/models/todos.py +74 -0
- chrys/foundation/models/turns.py +125 -0
- chrys/foundation/models/workflow_session.py +89 -0
- chrys/foundation/models/workspace.py +59 -0
- chrys/foundation/net/__init__.py +3 -0
- chrys/foundation/net/address_class.py +57 -0
- chrys/foundation/net/route_probe.py +94 -0
- chrys/foundation/net/url.py +107 -0
- chrys/foundation/observability/__init__.py +3 -0
- chrys/foundation/observability/exporters.py +174 -0
- chrys/foundation/observability/gate.py +31 -0
- chrys/foundation/observability/setup.py +497 -0
- chrys/foundation/observability/sink.py +228 -0
- chrys/foundation/patches/__init__.py +19 -0
- chrys/foundation/patches/mdurl_cache.py +59 -0
- chrys/foundation/patches/patcher.py +287 -0
- chrys/foundation/patches/staged_members.py +205 -0
- chrys/foundation/patches/textual_block_border.py +105 -0
- chrys/foundation/patches/textual_button_ansi.py +65 -0
- chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
- chrys/foundation/patches/textual_callback_cache.py +248 -0
- chrys/foundation/patches/textual_callback_dispatch.py +109 -0
- chrys/foundation/patches/textual_compositor_cjk.py +566 -0
- chrys/foundation/patches/textual_dispatch_cache.py +150 -0
- chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
- chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
- chrys/foundation/patches/textual_lru_acyclic.py +241 -0
- chrys/foundation/patches/textual_message_pump.py +136 -0
- chrys/foundation/patches/textual_node_diet.py +875 -0
- chrys/foundation/patches/textual_one_shot_timer.py +48 -0
- chrys/foundation/patches/textual_option_list.py +90 -0
- chrys/foundation/patches/textual_precompose.py +89 -0
- chrys/foundation/patches/textual_pruned_tabs.py +69 -0
- chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
- chrys/foundation/patches/textual_removed_node_caches.py +86 -0
- chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
- chrys/foundation/patches/textual_selection_extract.py +48 -0
- chrys/foundation/patches/textual_strip_cycles.py +119 -0
- chrys/foundation/patches/textual_tab_selection.py +1611 -0
- chrys/foundation/patches/textual_timer_skip.py +70 -0
- chrys/foundation/patches/textual_utf8_decoder.py +106 -0
- chrys/foundation/patches/textual_win_sleep.py +167 -0
- chrys/foundation/patches/watchdog_fsevents.py +70 -0
- chrys/foundation/patches/watchdog_windows.py +52 -0
- chrys/foundation/platform/__init__.py +556 -0
- chrys/foundation/platform/c_api.py +32 -0
- chrys/foundation/platform/child_reap.py +107 -0
- chrys/foundation/platform/command_line.py +95 -0
- chrys/foundation/platform/files.py +1470 -0
- chrys/foundation/platform/output_capture.py +113 -0
- chrys/foundation/platform/paths.py +193 -0
- chrys/foundation/platform/process.py +1468 -0
- chrys/foundation/platform/pty_output.py +19 -0
- chrys/foundation/platform/runtime_paths.py +281 -0
- chrys/foundation/platform/win_console.py +129 -0
- chrys/foundation/reasoning_origin.py +67 -0
- chrys/foundation/recovery.py +16 -0
- chrys/foundation/retry.py +278 -0
- chrys/foundation/text/__init__.py +3 -0
- chrys/foundation/text/encoding.py +603 -0
- chrys/foundation/text/images.py +304 -0
- chrys/foundation/text/lines.py +24 -0
- chrys/foundation/text/mentions.py +90 -0
- chrys/foundation/text/model_json.py +25 -0
- chrys/foundation/text/reading_order.py +20 -0
- chrys/foundation/text/tokenizer.py +25 -0
- chrys/foundation/text/tool_output.py +287 -0
- chrys/foundation/text/yaml_io.py +62 -0
- chrys/foundation/tool_call_context.py +202 -0
- chrys/foundation/tool_execution_stamp.py +184 -0
- chrys/foundation/tool_invocation_order.py +48 -0
- chrys/foundation/tool_kinds.py +102 -0
- chrys/foundation/tool_result_metadata.py +287 -0
- chrys/foundation/trajectory/__init__.py +10 -0
- chrys/foundation/trajectory/context.py +481 -0
- chrys/foundation/trajectory/envelope.py +571 -0
- chrys/foundation/trajectory/event_types.py +294 -0
- chrys/foundation/trajectory/finalizers.py +46 -0
- chrys/foundation/trajectory/fingerprint.py +62 -0
- chrys/foundation/trajectory/ids.py +155 -0
- chrys/foundation/trajectory/keys.py +99 -0
- chrys/foundation/trajectory/lease.py +173 -0
- chrys/foundation/trajectory/metadata.py +72 -0
- chrys/foundation/trajectory/reader.py +271 -0
- chrys/foundation/trajectory/recovery.py +225 -0
- chrys/foundation/trajectory/revisions.py +152 -0
- chrys/foundation/trajectory/segments.py +225 -0
- chrys/foundation/trajectory/tools.py +117 -0
- chrys/foundation/trajectory/usage.py +151 -0
- chrys/foundation/trajectory/writer.py +972 -0
- chrys/foundation/trajectory_timing.py +97 -0
- chrys/foundation/util/__init__.py +3 -0
- chrys/foundation/util/async_tasks.py +29 -0
- chrys/foundation/util/chrys_headers.py +23 -0
- chrys/foundation/util/env_templates.py +104 -0
- chrys/foundation/util/filenames.py +96 -0
- chrys/foundation/util/header_charset.py +138 -0
- chrys/foundation/util/httpx_helpers.py +12 -0
- chrys/foundation/util/lock.py +236 -0
- chrys/foundation/util/once_close.py +46 -0
- chrys/foundation/util/session_ids.py +29 -0
- chrys/foundation/util/shell_tokens.py +88 -0
- chrys/foundation/util/sub_agent_context.py +38 -0
- chrys/foundation/util/time.py +35 -0
- chrys/foundation/util/unicode_scalars.py +36 -0
- chrys/foundation/vendor/__init__.py +43 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
- chrys/kernel/__init__.py +256 -0
- chrys/kernel/_content.py +1866 -0
- chrys/kernel/_loop_recorder.py +631 -0
- chrys/kernel/_null_overlay.py +1026 -0
- chrys/kernel/_result_ceiling.py +216 -0
- chrys/kernel/_serialization.py +431 -0
- chrys/kernel/_tool_arg_errors.py +1629 -0
- chrys/kernel/_tool_execution.py +1172 -0
- chrys/kernel/_tool_expansion.py +26 -0
- chrys/kernel/_types.py +2598 -0
- chrys/kernel/agent.py +994 -0
- chrys/kernel/client.py +1084 -0
- chrys/kernel/compaction.py +1315 -0
- chrys/kernel/exceptions.py +211 -0
- chrys/kernel/exchanges.py +683 -0
- chrys/kernel/identity.py +95 -0
- chrys/kernel/images.py +36 -0
- chrys/kernel/instrumentation.py +1618 -0
- chrys/kernel/loop.py +2308 -0
- chrys/kernel/middleware.py +616 -0
- chrys/kernel/sessions.py +604 -0
- chrys/kernel/tools.py +1061 -0
- chrys/kernel/types.py +126 -0
- chrys/orchestration/__init__.py +3 -0
- chrys/orchestration/engine/__init__.py +3 -0
- chrys/orchestration/engine/assembly.py +236 -0
- chrys/orchestration/engine/build/__init__.py +3 -0
- chrys/orchestration/engine/build/builder.py +1061 -0
- chrys/orchestration/engine/build/construction.py +408 -0
- chrys/orchestration/engine/build/loaded.py +139 -0
- chrys/orchestration/engine/engine.py +789 -0
- chrys/orchestration/engine/execution.py +771 -0
- chrys/orchestration/engine/loader.py +865 -0
- chrys/orchestration/engine/rollback.py +918 -0
- chrys/orchestration/engine/run/__init__.py +3 -0
- chrys/orchestration/engine/run/active_injection.py +419 -0
- chrys/orchestration/engine/run/attachments.py +519 -0
- chrys/orchestration/engine/run/bindings.py +1095 -0
- chrys/orchestration/engine/run/coordinator.py +1062 -0
- chrys/orchestration/engine/run/finalizer.py +417 -0
- chrys/orchestration/engine/run/input_refs.py +60 -0
- chrys/orchestration/engine/run/prompt_content.py +457 -0
- chrys/orchestration/engine/run/resume.py +299 -0
- chrys/orchestration/engine/run/retry.py +948 -0
- chrys/orchestration/engine/run/runner.py +625 -0
- chrys/orchestration/engine/run/runtime_skills.py +146 -0
- chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
- chrys/orchestration/engine/run/turn_hooks.py +258 -0
- chrys/orchestration/engine/run/turn_state.py +149 -0
- chrys/orchestration/engine/run/working_dir.py +50 -0
- chrys/orchestration/engine/session_lifecycle.py +2013 -0
- chrys/orchestration/engine/state/__init__.py +3 -0
- chrys/orchestration/engine/state/active_session.py +183 -0
- chrys/orchestration/engine/state/controls.py +639 -0
- chrys/orchestration/engine/state/current_agent.py +26 -0
- chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
- chrys/orchestration/engine/state/machine.py +193 -0
- chrys/orchestration/engine/state/session_writer.py +434 -0
- chrys/orchestration/engine/trajectory.py +714 -0
- chrys/orchestration/engine/usage.py +118 -0
- chrys/orchestration/invoker/__init__.py +3 -0
- chrys/orchestration/invoker/acp.py +727 -0
- chrys/orchestration/invoker/acp_protocol.py +1115 -0
- chrys/orchestration/invoker/acp_spec.py +79 -0
- chrys/orchestration/invoker/attempts.py +994 -0
- chrys/orchestration/invoker/child_compaction.py +131 -0
- chrys/orchestration/invoker/child_history.py +99 -0
- chrys/orchestration/invoker/contracts.py +253 -0
- chrys/orchestration/invoker/evidence.py +94 -0
- chrys/orchestration/invoker/kernel.py +343 -0
- chrys/orchestration/invoker/origin.py +79 -0
- chrys/orchestration/invoker/resources.py +343 -0
- chrys/orchestration/invoker/runtime.py +299 -0
- chrys/orchestration/invoker/usage.py +80 -0
- chrys/orchestration/session_hooks.py +117 -0
- chrys/orchestration/session_host.py +1023 -0
- chrys/orchestration/session_resources.py +95 -0
- chrys/orchestration/session_usage.py +219 -0
- chrys/orchestration/startup.py +255 -0
- chrys/orchestration/sub_agents/__init__.py +3 -0
- chrys/orchestration/sub_agents/acp_policy.py +269 -0
- chrys/orchestration/sub_agents/kernel_policy.py +848 -0
- chrys/orchestration/sub_agents/registration.py +44 -0
- chrys/orchestration/sub_agents/shell.py +337 -0
- chrys/orchestration/sub_agents/tools.py +2228 -0
- chrys/orchestration/workflows/__init__.py +3 -0
- chrys/orchestration/workflows/agent_archive.py +156 -0
- chrys/orchestration/workflows/agent_node.py +894 -0
- chrys/orchestration/workflows/agent_node_build.py +687 -0
- chrys/orchestration/workflows/catalog.py +332 -0
- chrys/orchestration/workflows/coordinator.py +771 -0
- chrys/orchestration/workflows/hooks.py +176 -0
- chrys/orchestration/workflows/preview.py +322 -0
- chrys/orchestration/workflows/rollback.py +151 -0
- chrys/orchestration/workflows/runner.py +986 -0
- chrys/orchestration/workflows/session.py +390 -0
- chrys/orchestration/workflows/settings.py +97 -0
- chrys/orchestration/workflows/validation.py +674 -0
- chrys/orchestration/workflows/worker_client.py +1002 -0
- chrys/service/__init__.py +3 -0
- chrys/service/acp_client/__init__.py +65 -0
- chrys/service/acp_client/_transport.py +922 -0
- chrys/service/acp_client/client.py +1311 -0
- chrys/service/acp_client/errors.py +224 -0
- chrys/service/acp_client/protocol.py +220 -0
- chrys/service/acp_client/spawn.py +239 -0
- chrys/service/acp_client/spec.py +100 -0
- chrys/service/agent_middleware/__init__.py +60 -0
- chrys/service/agent_middleware/_metadata_keys.py +52 -0
- chrys/service/agent_middleware/control/__init__.py +3 -0
- chrys/service/agent_middleware/control/approval.py +769 -0
- chrys/service/agent_middleware/control/approval_hooks.py +62 -0
- chrys/service/agent_middleware/control/ask_user.py +207 -0
- chrys/service/agent_middleware/control/interrupt.py +50 -0
- chrys/service/agent_middleware/control/sleep.py +118 -0
- chrys/service/agent_middleware/control/todo.py +117 -0
- chrys/service/agent_middleware/events/__init__.py +3 -0
- chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
- chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
- chrys/service/agent_middleware/events/intermediate_text.py +128 -0
- chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
- chrys/service/agent_middleware/events/result_persistence.py +98 -0
- chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
- chrys/service/agent_middleware/events/tool_error_text.py +22 -0
- chrys/service/agent_middleware/events/tool_events.py +779 -0
- chrys/service/agent_middleware/injection.py +510 -0
- chrys/service/agent_middleware/reminders/__init__.py +91 -0
- chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
- chrys/service/agent_middleware/reminders/base.py +25 -0
- chrys/service/agent_middleware/reminders/context_usage.py +74 -0
- chrys/service/agent_middleware/reminders/file_change.py +63 -0
- chrys/service/agent_middleware/reminders/mcp.py +39 -0
- chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
- chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
- chrys/service/agent_middleware/reminders/skills.py +44 -0
- chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
- chrys/service/agent_middleware/reminders/todo.py +35 -0
- chrys/service/agent_middleware/reminders/turn_line.py +48 -0
- chrys/service/agent_middleware/response_validation.py +1380 -0
- chrys/service/agent_middleware/system_reminder.py +1107 -0
- chrys/service/agent_middleware/validators.py +396 -0
- chrys/service/analytics/__init__.py +124 -0
- chrys/service/analytics/_actions.py +896 -0
- chrys/service/analytics/_context_evidence.py +357 -0
- chrys/service/analytics/_critical_path.py +179 -0
- chrys/service/analytics/_facts.py +919 -0
- chrys/service/analytics/_insights.py +769 -0
- chrys/service/analytics/_metric_ops.py +83 -0
- chrys/service/analytics/_session_projection.py +460 -0
- chrys/service/analytics/_timeline.py +434 -0
- chrys/service/analytics/_turn_graph.py +1237 -0
- chrys/service/analytics/_turns.py +1377 -0
- chrys/service/analytics/_workflows.py +171 -0
- chrys/service/analytics/aggregation.py +1474 -0
- chrys/service/analytics/classification.py +128 -0
- chrys/service/analytics/export.py +803 -0
- chrys/service/analytics/findings.py +282 -0
- chrys/service/analytics/math.py +79 -0
- chrys/service/analytics/model.py +886 -0
- chrys/service/analytics/reader.py +321 -0
- chrys/service/approval/__init__.py +3 -0
- chrys/service/approval/arbitration.py +102 -0
- chrys/service/approval/command_identity.py +109 -0
- chrys/service/approval/correlation.py +57 -0
- chrys/service/approval/grant_store.py +108 -0
- chrys/service/approval/judge.py +589 -0
- chrys/service/approval/level.py +15 -0
- chrys/service/approval/policy.py +209 -0
- chrys/service/approval/reuse.py +223 -0
- chrys/service/approval/reuse_binding.py +133 -0
- chrys/service/approval/safety_classifier.py +536 -0
- chrys/service/approval/turn_context.py +49 -0
- chrys/service/context/__init__.py +3 -0
- chrys/service/context/compaction/__init__.py +38 -0
- chrys/service/context/compaction/breaker.py +156 -0
- chrys/service/context/compaction/budgets.py +93 -0
- chrys/service/context/compaction/calibration.py +77 -0
- chrys/service/context/compaction/compression.py +690 -0
- chrys/service/context/compaction/current_turn_drop.py +458 -0
- chrys/service/context/compaction/events.py +201 -0
- chrys/service/context/compaction/exclusions.py +261 -0
- chrys/service/context/compaction/groups.py +361 -0
- chrys/service/context/compaction/last_words.py +1996 -0
- chrys/service/context/compaction/last_words_state.py +752 -0
- chrys/service/context/compaction/scoped.py +472 -0
- chrys/service/context/compaction/spill.py +1947 -0
- chrys/service/context/compaction/strategy.py +1116 -0
- chrys/service/context/compaction/summaries.py +319 -0
- chrys/service/context/compaction/turns.py +375 -0
- chrys/service/context/manager.py +174 -0
- chrys/service/context/memory_loader.py +792 -0
- chrys/service/context/middleware/__init__.py +3 -0
- chrys/service/context/middleware/usage.py +332 -0
- chrys/service/context/providers/__init__.py +3 -0
- chrys/service/context/providers/context_management.py +432 -0
- chrys/service/context/providers/history.py +647 -0
- chrys/service/context/providers/memory.py +50 -0
- chrys/service/context/tool_content.py +22 -0
- chrys/service/hooks/__init__.py +37 -0
- chrys/service/hooks/detached_worker.py +118 -0
- chrys/service/hooks/docs/README.md +14 -0
- chrys/service/hooks/docs/authoring.md +684 -0
- chrys/service/hooks/docs/configuration.md +499 -0
- chrys/service/hooks/docs/design.md +359 -0
- chrys/service/hooks/docs/project-hooks.md +348 -0
- chrys/service/hooks/events.py +157 -0
- chrys/service/hooks/loader.py +665 -0
- chrys/service/hooks/manager.py +807 -0
- chrys/service/hooks/matcher.py +111 -0
- chrys/service/hooks/outbox.py +294 -0
- chrys/service/hooks/runner.py +572 -0
- chrys/service/hooks/schema.py +407 -0
- chrys/service/llm/__init__.py +7 -0
- chrys/service/llm/_structured_outputs.py +192 -0
- chrys/service/llm/anthropic_messages/__init__.py +9 -0
- chrys/service/llm/anthropic_messages/client.py +254 -0
- chrys/service/llm/anthropic_messages/decode.py +331 -0
- chrys/service/llm/anthropic_messages/history.py +279 -0
- chrys/service/llm/anthropic_messages/request.py +323 -0
- chrys/service/llm/anthropic_messages/server_tools.py +319 -0
- chrys/service/llm/anthropic_messages/stream.py +297 -0
- chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
- chrys/service/llm/chat_completions/__init__.py +9 -0
- chrys/service/llm/chat_completions/client.py +369 -0
- chrys/service/llm/chat_completions/decode.py +338 -0
- chrys/service/llm/chat_completions/history.py +657 -0
- chrys/service/llm/chat_completions/reasoning.py +244 -0
- chrys/service/llm/chat_completions/request.py +192 -0
- chrys/service/llm/chat_completions/stream.py +546 -0
- chrys/service/llm/chat_completions/validation.py +308 -0
- chrys/service/llm/clients.py +613 -0
- chrys/service/llm/images.py +65 -0
- chrys/service/llm/mock.py +413 -0
- chrys/service/llm/observer.py +446 -0
- chrys/service/llm/one_shot.py +106 -0
- chrys/service/llm/openai_exceptions.py +93 -0
- chrys/service/llm/openai_responses/__init__.py +10 -0
- chrys/service/llm/openai_responses/client.py +323 -0
- chrys/service/llm/openai_responses/decode.py +432 -0
- chrys/service/llm/openai_responses/history.py +519 -0
- chrys/service/llm/openai_responses/hosted.py +625 -0
- chrys/service/llm/openai_responses/replay.py +629 -0
- chrys/service/llm/openai_responses/request.py +342 -0
- chrys/service/llm/openai_responses/stream.py +847 -0
- chrys/service/llm/openai_timestamps.py +57 -0
- chrys/service/llm/providers.py +107 -0
- chrys/service/llm/proxy_route.py +54 -0
- chrys/service/llm/raw_http_log.py +309 -0
- chrys/service/llm/route_facts.py +63 -0
- chrys/service/llm/route_sessions.py +49 -0
- chrys/service/llm/sdk_anthropic.py +18 -0
- chrys/service/llm/sdk_headers.py +47 -0
- chrys/service/llm/sdk_openai.py +18 -0
- chrys/service/llm/sdk_retry.py +59 -0
- chrys/service/llm/wire_client.py +246 -0
- chrys/service/mcp/__init__.py +3 -0
- chrys/service/mcp/_connection.py +235 -0
- chrys/service/mcp/_http_transport.py +350 -0
- chrys/service/mcp/_progressive.py +459 -0
- chrys/service/mcp/_stdio_transport.py +614 -0
- chrys/service/mcp/_tool_mixins.py +96 -0
- chrys/service/mcp/adapter.py +1031 -0
- chrys/service/mcp/cache.py +476 -0
- chrys/service/mcp/content.py +32 -0
- chrys/service/mcp/errors.py +169 -0
- chrys/service/mcp/owned.py +1330 -0
- chrys/service/mcp/result_limits.py +168 -0
- chrys/service/mcp/thinking_warning.py +95 -0
- chrys/service/mcp/validation.py +74 -0
- chrys/service/mutations/__init__.py +84 -0
- chrys/service/mutations/coordination.py +894 -0
- chrys/service/mutations/detector.py +458 -0
- chrys/service/mutations/git_calibrator.py +397 -0
- chrys/service/mutations/git_state.py +647 -0
- chrys/service/mutations/pipeline.py +779 -0
- chrys/service/mutations/scanner.py +466 -0
- chrys/service/mutations/snapshot_files.py +233 -0
- chrys/service/mutations/store.py +625 -0
- chrys/service/mutations/tool_names.py +15 -0
- chrys/service/mutations/trace.py +246 -0
- chrys/service/mutations/tracker.py +1289 -0
- chrys/service/mutations/types.py +559 -0
- chrys/service/mutations/workspace_changes.py +1583 -0
- chrys/service/profiles/__init__.py +2 -0
- chrys/service/profiles/agents/__init__.py +3 -0
- chrys/service/profiles/agents/builtins/Code.yaml +199 -0
- chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
- chrys/service/profiles/agents/builtins/General.yaml +114 -0
- chrys/service/profiles/agents/builtins/QA.yaml +128 -0
- chrys/service/profiles/agents/loader.py +808 -0
- chrys/service/profiles/agents/registry.py +727 -0
- chrys/service/profiles/agents/schema.py +371 -0
- chrys/service/profiles/agents/serializer.py +196 -0
- chrys/service/profiles/coercion.py +22 -0
- chrys/service/profiles/models/__init__.py +3 -0
- chrys/service/profiles/models/env_bridge.py +157 -0
- chrys/service/profiles/models/loader.py +282 -0
- chrys/service/profiles/models/options.py +438 -0
- chrys/service/profiles/models/registry.py +104 -0
- chrys/service/profiles/models/resolver.py +286 -0
- chrys/service/profiles/models/schema.py +91 -0
- chrys/service/profiles/models/serializer.py +76 -0
- chrys/service/session/__init__.py +3 -0
- chrys/service/session/checkpoint.py +156 -0
- chrys/service/session/history.py +1616 -0
- chrys/service/session/message_metadata.py +151 -0
- chrys/service/session/persistence.py +435 -0
- chrys/service/session/runtime_metadata.py +210 -0
- chrys/service/session/sub_agent_logs.py +1069 -0
- chrys/service/session/sub_agent_transcript.py +654 -0
- chrys/service/skills/__init__.py +3 -0
- chrys/service/skills/adapter.py +408 -0
- chrys/service/skills/constants.py +8 -0
- chrys/service/skills/loader.py +575 -0
- chrys/service/skills/model.py +130 -0
- chrys/service/skills/provider.py +841 -0
- chrys/service/skills/runner.py +390 -0
- chrys/service/state/__init__.py +3 -0
- chrys/service/state/_fork.py +710 -0
- chrys/service/state/_session_files.py +175 -0
- chrys/service/state/_session_meta.py +552 -0
- chrys/service/state/locks.py +154 -0
- chrys/service/state/serializers.py +156 -0
- chrys/service/state/session_catalog.py +361 -0
- chrys/service/state/session_listing.py +84 -0
- chrys/service/state/session_migration.py +446 -0
- chrys/service/state/session_mru.py +315 -0
- chrys/service/state/store.py +2132 -0
- chrys/service/state/workflow.py +158 -0
- chrys/service/text_blocks.py +47 -0
- chrys/service/todos/__init__.py +3 -0
- chrys/service/todos/reminder.py +31 -0
- chrys/service/todos/tracker.py +58 -0
- chrys/service/tools/__init__.py +3 -0
- chrys/service/tools/approval_targets.py +92 -0
- chrys/service/tools/builtins/__init__.py +3 -0
- chrys/service/tools/builtins/ask_user.py +326 -0
- chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
- chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
- chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
- chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
- chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
- chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
- chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
- chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
- chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
- chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
- chrys/service/tools/builtins/doc_converter/registry.py +55 -0
- chrys/service/tools/builtins/filesystem.py +1071 -0
- chrys/service/tools/builtins/search.py +1265 -0
- chrys/service/tools/builtins/shell.py +967 -0
- chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
- chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
- chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
- chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
- chrys/service/tools/builtins/shell_filter.py +24 -0
- chrys/service/tools/builtins/sleep.py +91 -0
- chrys/service/tools/builtins/todo.py +64 -0
- chrys/service/tools/builtins/web/__init__.py +6 -0
- chrys/service/tools/builtins/web/build.py +268 -0
- chrys/service/tools/builtins/web/config.py +338 -0
- chrys/service/tools/builtins/web/credentials.py +37 -0
- chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
- chrys/service/tools/builtins/web/fetch/cache.py +68 -0
- chrys/service/tools/builtins/web/fetch/convert.py +56 -0
- chrys/service/tools/builtins/web/fetch/tool.py +223 -0
- chrys/service/tools/builtins/web/hosted.py +61 -0
- chrys/service/tools/builtins/web/http.py +330 -0
- chrys/service/tools/builtins/web/progress.py +18 -0
- chrys/service/tools/builtins/web/search/__init__.py +3 -0
- chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
- chrys/service/tools/builtins/web/search/html.py +152 -0
- chrys/service/tools/builtins/web/search/json_api.py +227 -0
- chrys/service/tools/builtins/web/search/providers.py +77 -0
- chrys/service/tools/builtins/web/search/tool.py +218 -0
- chrys/service/tools/builtins/web/search/types.py +28 -0
- chrys/service/tools/builtins/web/tls_identity.py +66 -0
- chrys/service/tools/kinds.py +115 -0
- chrys/service/tools/names.py +47 -0
- chrys/service/tools/registry.py +242 -0
- chrys/service/tools/result_metadata.py +103 -0
- chrys/service/tools/session_artifacts.py +196 -0
- chrys/service/tools/spill.py +218 -0
- chrys/service/tools/workspace_paths.py +81 -0
- chrys/service/trajectory/__init__.py +11 -0
- chrys/service/trajectory/approvals.py +201 -0
- chrys/service/trajectory/compaction.py +420 -0
- chrys/service/trajectory/fork.py +75 -0
- chrys/service/trajectory/hooks.py +217 -0
- chrys/service/trajectory/items.py +41 -0
- chrys/service/trajectory/preparation.py +374 -0
- chrys/service/trajectory/retries.py +95 -0
- chrys/service/trajectory/revisions.py +129 -0
- chrys/service/trajectory/segmented.py +163 -0
- chrys/service/trajectory/session.py +762 -0
- chrys/service/trajectory/state.py +83 -0
- chrys/service/trajectory/sub_agents.py +169 -0
- chrys/service/trajectory/tombstone.py +262 -0
- chrys/service/trajectory/tools.py +323 -0
- chrys/service/trajectory/validation.py +130 -0
- chrys/service/trajectory/waits.py +161 -0
- chrys/service/trajectory/workflow.py +148 -0
- chrys/service/vision.py +128 -0
- chrys/service/workflows/__init__.py +3 -0
- chrys/service/workflows/admission.py +151 -0
- chrys/service/workflows/artifacts.py +109 -0
- chrys/service/workflows/asks.py +130 -0
- chrys/service/workflows/builtins/__init__.py +12 -0
- chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
- chrys/service/workflows/builtins/demo-workflow.py +756 -0
- chrys/service/workflows/discovery.py +554 -0
- chrys/service/workflows/environment.py +284 -0
- chrys/service/workflows/graph.py +430 -0
- chrys/service/workflows/history.py +82 -0
- chrys/service/workflows/interpreter.py +107 -0
- chrys/service/workflows/journal.py +334 -0
- chrys/service/workflows/layout.py +56 -0
- chrys/service/workflows/ledger.py +149 -0
- chrys/service/workflows/model_selection.py +18 -0
- chrys/service/workflows/orphans.py +209 -0
- chrys/service/workflows/outcomes.py +46 -0
- chrys/service/workflows/protocol.py +210 -0
- chrys/service/workflows/records.py +142 -0
- chrys/service/workflows/scheduler.py +770 -0
- chrys/service/workflows/sdk/__init__.py +51 -0
- chrys/service/workflows/sdk/_ask.py +167 -0
- chrys/service/workflows/sdk/_builder.py +781 -0
- chrys/service/workflows/sdk/_values.py +92 -0
- chrys/service/workflows/sdk_artifact.py +74 -0
- chrys/service/workflows/store.py +494 -0
- chrys/service/workflows/transcript.py +102 -0
- chrys/service/workflows/values.py +73 -0
- chrys/service/workflows/worker_host.py +1820 -0
- chrys/workflows.py +38 -0
- icode_tui-0.29.1.dist-info/METADATA +104 -0
- icode_tui-0.29.1.dist-info/RECORD +1138 -0
- icode_tui-0.29.1.dist-info/WHEEL +4 -0
- icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
- icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
- icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Owner of one workflow worker process and the RPC client that talks to it.
|
|
4
|
+
|
|
5
|
+
One reader task demultiplexes every frame the worker writes: responses go to
|
|
6
|
+
the future registered under their id, ``emit`` notifications go through an
|
|
7
|
+
ordered dispatcher, ``ask`` requests each get their own task so a human answer
|
|
8
|
+
never blocks the channel. Worker EOF, a malformed frame or the worker's exit
|
|
9
|
+
(noticed on its own, since a forked child of the worker can keep its pipes
|
|
10
|
+
open) rejects every pending future with :class:`WorkerLostError`, cancels the
|
|
11
|
+
helpers and terminates the process tree, in that order and only once.
|
|
12
|
+
|
|
13
|
+
Two contracts the runner relies on live here:
|
|
14
|
+
|
|
15
|
+
* **attempt fence** — once a ``run_python`` attempt has its terminal envelope,
|
|
16
|
+
a later ``emit`` for it is dropped and a later ``ask`` is answered
|
|
17
|
+
``attempt_terminated``; a ``cancel`` names one exact attempt.
|
|
18
|
+
* **projection barrier** — ``run_python`` returns only after the emit
|
|
19
|
+
dispatcher has processed every ordinal up to the envelope's
|
|
20
|
+
``last_emit_ordinal``; the cancel and worker-lost paths are exempt.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import contextlib
|
|
27
|
+
import hashlib
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
from collections import defaultdict
|
|
31
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Final, cast
|
|
35
|
+
|
|
36
|
+
from chrys.foundation.models.ask_user import AskUserAnswer, AskUserQuestion
|
|
37
|
+
from chrys.foundation.platform import get_platform
|
|
38
|
+
from chrys.foundation.platform.process import (
|
|
39
|
+
ManagedStdioProcess,
|
|
40
|
+
MissingWorkingDirectoryError,
|
|
41
|
+
spawn_managed_stdio_process,
|
|
42
|
+
)
|
|
43
|
+
from chrys.foundation.trajectory.keys import ensure_owner_only_directory
|
|
44
|
+
from chrys.service.workflows import protocol
|
|
45
|
+
from chrys.service.workflows.asks import answers_to_wire, questions_from_wire
|
|
46
|
+
from chrys.service.workflows.environment import PreparedEnvironment
|
|
47
|
+
from chrys.service.workflows.graph import ManifestError, manifest_warnings
|
|
48
|
+
from chrys.service.workflows.protocol import (
|
|
49
|
+
LIMITS,
|
|
50
|
+
PROTOCOL_VERSION,
|
|
51
|
+
ErrorCode,
|
|
52
|
+
Method,
|
|
53
|
+
ProtocolError,
|
|
54
|
+
decode_frame,
|
|
55
|
+
encode_frame,
|
|
56
|
+
ref_from_wire,
|
|
57
|
+
ref_key,
|
|
58
|
+
ref_to_wire,
|
|
59
|
+
)
|
|
60
|
+
from chrys.service.workflows.scheduler import AttemptRef
|
|
61
|
+
from chrys.service.workflows.sdk import SourceValue, WorkflowValue
|
|
62
|
+
from chrys.service.workflows.sdk_artifact import SdkArtifact
|
|
63
|
+
from chrys.service.workflows.values import canonical_json, source_to_wire, value_from_wire, value_to_wire
|
|
64
|
+
|
|
65
|
+
logger = logging.getLogger(__name__)
|
|
66
|
+
|
|
67
|
+
HOST_PATH = Path(protocol.__file__).resolve().parent / "worker_host.py"
|
|
68
|
+
BYTECODE_CACHE_ENV = "PYTHONPYCACHEPREFIX"
|
|
69
|
+
STDERR_TAIL_BYTES = 64 * 1024
|
|
70
|
+
MAX_IN_FLIGHT_REQUESTS = 64
|
|
71
|
+
_STDIO_LIMIT = LIMITS.max_frame_bytes + 2
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _bounded_diagnostic(text: str) -> str:
|
|
75
|
+
"""Cap diagnostic text so its error frame always fits the wire limit; a dropped error reply hangs the worker."""
|
|
76
|
+
limit = LIMITS.captured_output_bytes
|
|
77
|
+
if len(text) <= limit:
|
|
78
|
+
return text
|
|
79
|
+
return text[:limit] + "... [" + str(len(text) - limit) + " chars dropped]"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_CONTROL_METHODS = frozenset({Method.CANCEL, Method.SHUTDOWN})
|
|
83
|
+
# Requests that run one attempt each; their terminal envelope carries the attempt's leak verdict.
|
|
84
|
+
_ATTEMPT_METHODS = frozenset({Method.RUN_PYTHON, Method.EVAL_OUTGOING, Method.EVAL_LOOP_UNTIL, Method.COMBINE})
|
|
85
|
+
_EVAL_METHODS = frozenset({Method.EVAL_OUTGOING, Method.EVAL_LOOP_UNTIL, Method.COMBINE})
|
|
86
|
+
|
|
87
|
+
AskHandler = Callable[[AttemptRef, tuple[AskUserQuestion, ...]], Awaitable[tuple[AskUserAnswer, ...]]]
|
|
88
|
+
EmitHandler = Callable[[AttemptRef, int, str], Awaitable[None]]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class WorkerError(Exception):
|
|
92
|
+
"""Base of every failure the client reports."""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class WorkerStartError(WorkerError):
|
|
96
|
+
"""The worker never became usable (probe, spawn, hello, SDK origin)."""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class WorkerLostError(WorkerError):
|
|
100
|
+
"""The worker exited, broke the protocol, or was killed; the run is over."""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class WorkerRpcError(WorkerError):
|
|
104
|
+
"""An ``error`` response; ``code`` is one of :class:`protocol.ErrorCode`."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, code: str, message: str, data: Mapping[str, Any] | None = None) -> None:
|
|
107
|
+
super().__init__(f"{code}: {message}")
|
|
108
|
+
self.code = code
|
|
109
|
+
self.message = message
|
|
110
|
+
self.data: dict[str, Any] = dict(data or {})
|
|
111
|
+
self.stdout = _captured(self.data.get("stdout"))
|
|
112
|
+
self.traceback = _string(self.data.get("traceback", ""), "traceback")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AttemptTimeout(WorkerError):
|
|
116
|
+
"""A deadline passed; the attempt was cancelled (``leaked_thread`` says whether a thread stayed behind)."""
|
|
117
|
+
|
|
118
|
+
def __init__(self, ref: AttemptRef, *, leaked_thread: bool, data: Mapping[str, Any]) -> None:
|
|
119
|
+
super().__init__(f"{ref.activation_id} attempt {ref.attempt} timed out")
|
|
120
|
+
self.ref = ref
|
|
121
|
+
self.leaked_thread = leaked_thread
|
|
122
|
+
self.stdout = _captured(data.get("stdout"))
|
|
123
|
+
self.traceback = _string(data.get("traceback", ""), "traceback")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class AskUnavailable(Exception):
|
|
127
|
+
"""Raised by an ask handler when nobody can answer (headless)."""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True, slots=True)
|
|
131
|
+
class CapturedOutput:
|
|
132
|
+
text: str
|
|
133
|
+
truncated: bool
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True, slots=True)
|
|
137
|
+
class Hello:
|
|
138
|
+
protocol_version: int
|
|
139
|
+
python_version: str
|
|
140
|
+
implementation: str
|
|
141
|
+
platform: str
|
|
142
|
+
sdk_origin_ok: bool
|
|
143
|
+
sdk_origin_error: str | None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass(frozen=True, slots=True)
|
|
147
|
+
class LoadResult:
|
|
148
|
+
entry_digest: str
|
|
149
|
+
manifest_digest: str
|
|
150
|
+
manifest: dict[str, Any]
|
|
151
|
+
stdout: CapturedOutput
|
|
152
|
+
sites: dict[str, tuple[str | None, int | None]] = field(default_factory=dict)
|
|
153
|
+
"""Where each node was declared, as ``(file, line)``; only a ``diagnose`` load reports them."""
|
|
154
|
+
sites_truncated: bool = False
|
|
155
|
+
"""The sites were left out to keep the response within the frame limit."""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
LOAD_TIMED_OUT: Final = "timeout"
|
|
159
|
+
"""``WorkerRpcError.data["reason"]`` of a load that did not finish within ``LIMITS.load_timeout``."""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True, slots=True)
|
|
163
|
+
class LoadNote:
|
|
164
|
+
message: str
|
|
165
|
+
file: str | None
|
|
166
|
+
line: int | None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(frozen=True, slots=True)
|
|
170
|
+
class LoadDiagnostic:
|
|
171
|
+
"""A load failure as the worker placed it; lines and character columns count from 1.
|
|
172
|
+
|
|
173
|
+
``end_column`` is the column just past the faulty range; ``node`` is the
|
|
174
|
+
node or edge an SDK validation error names.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
code: str
|
|
178
|
+
message: str
|
|
179
|
+
file: str | None
|
|
180
|
+
line: int | None
|
|
181
|
+
column: int | None
|
|
182
|
+
end_line: int | None
|
|
183
|
+
end_column: int | None
|
|
184
|
+
node: str | None
|
|
185
|
+
source_line: str | None
|
|
186
|
+
notes: tuple[LoadNote, ...]
|
|
187
|
+
hint: str | None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def load_diagnostics(data: Mapping[str, Any]) -> tuple[tuple[LoadDiagnostic, ...], bool]:
|
|
191
|
+
"""The diagnostics of a ``diagnose`` load's ``load_failed`` data, and whether any were left out."""
|
|
192
|
+
raw = data.get("diagnostics", [])
|
|
193
|
+
if not isinstance(raw, list) or not all(isinstance(item, dict) for item in raw):
|
|
194
|
+
raise ProtocolError("diagnostics must be a list of objects.")
|
|
195
|
+
diagnostics = tuple(
|
|
196
|
+
LoadDiagnostic(
|
|
197
|
+
code=_string(item.get("code"), "diagnostic code"),
|
|
198
|
+
message=_string(item.get("message"), "diagnostic message"),
|
|
199
|
+
file=_optional_string(item.get("file"), "diagnostic file"),
|
|
200
|
+
line=_optional_count(item.get("line"), "diagnostic line"),
|
|
201
|
+
column=_optional_count(item.get("column"), "diagnostic column"),
|
|
202
|
+
end_line=_optional_count(item.get("end_line"), "diagnostic end_line"),
|
|
203
|
+
end_column=_optional_count(item.get("end_column"), "diagnostic end_column"),
|
|
204
|
+
node=_optional_string(item.get("node"), "diagnostic node"),
|
|
205
|
+
source_line=_optional_string(item.get("source_line"), "diagnostic source_line"),
|
|
206
|
+
notes=_load_notes(item.get("notes", [])),
|
|
207
|
+
hint=_optional_string(item.get("hint"), "diagnostic hint"),
|
|
208
|
+
)
|
|
209
|
+
for item in raw
|
|
210
|
+
)
|
|
211
|
+
return diagnostics, _boolean(data.get("diagnostics_truncated", False), "diagnostics_truncated")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _load_notes(raw: Any) -> tuple[LoadNote, ...]:
|
|
215
|
+
if not isinstance(raw, list) or not all(isinstance(item, dict) for item in raw):
|
|
216
|
+
raise ProtocolError("diagnostic notes must be a list of objects.")
|
|
217
|
+
return tuple(
|
|
218
|
+
LoadNote(
|
|
219
|
+
message=_string(item.get("message"), "note message"),
|
|
220
|
+
file=_optional_string(item.get("file"), "note file"),
|
|
221
|
+
line=_optional_count(item.get("line"), "note line"),
|
|
222
|
+
)
|
|
223
|
+
for item in raw
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _load_sites(raw: Any) -> dict[str, tuple[str | None, int | None]]:
|
|
228
|
+
"""``{"files": [file, ...], "nodes": {node_id: [file_index, line]}}`` as ``{node_id: (file, line)}``."""
|
|
229
|
+
if raw is None:
|
|
230
|
+
return {}
|
|
231
|
+
files, nodes = (raw.get("files"), raw.get("nodes")) if isinstance(raw, dict) else (None, None)
|
|
232
|
+
if not isinstance(files, list) or not isinstance(nodes, dict):
|
|
233
|
+
raise ProtocolError("sites must hold a files list and a nodes object.")
|
|
234
|
+
paths = [_optional_string(file, "site file") for file in files]
|
|
235
|
+
sites: dict[str, tuple[str | None, int | None]] = {}
|
|
236
|
+
for node_id, site in nodes.items():
|
|
237
|
+
if not (isinstance(site, list) and len(site) == 2 and type(site[0]) is int and 0 <= site[0] < len(paths)):
|
|
238
|
+
raise ProtocolError("a node site must be [file_index, line].")
|
|
239
|
+
sites[node_id] = (paths[site[0]], _optional_count(site[1], "site line"))
|
|
240
|
+
return sites
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True, slots=True)
|
|
244
|
+
class PythonResult:
|
|
245
|
+
value: WorkflowValue
|
|
246
|
+
stdout: CapturedOutput
|
|
247
|
+
last_emit_ordinal: int
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@dataclass(frozen=True, slots=True)
|
|
251
|
+
class CancelResult:
|
|
252
|
+
cancelled: bool
|
|
253
|
+
leaked_threads_by_pool: dict[str, int]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass(frozen=True, slots=True)
|
|
257
|
+
class NativeOutput:
|
|
258
|
+
text: str
|
|
259
|
+
dropped_bytes: int
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _encode(frame: dict[str, Any]) -> bytes:
|
|
263
|
+
try:
|
|
264
|
+
return encode_frame(frame)
|
|
265
|
+
except ProtocolError as exc:
|
|
266
|
+
raise WorkerRpcError(ErrorCode.VALUE_TOO_LARGE, str(exc)) from exc
|
|
267
|
+
except ValueError as exc:
|
|
268
|
+
raise WorkerRpcError(ErrorCode.VALUE_NOT_SERIALIZABLE, str(exc)) from exc
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _string(value: Any, field: str) -> str:
|
|
272
|
+
if not isinstance(value, str):
|
|
273
|
+
raise ProtocolError(f"{field} must be a string.")
|
|
274
|
+
return value
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _boolean(value: Any, field: str) -> bool:
|
|
278
|
+
if not isinstance(value, bool):
|
|
279
|
+
raise ProtocolError(f"{field} must be a boolean.")
|
|
280
|
+
return value
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _optional_string(value: Any, field: str) -> str | None:
|
|
284
|
+
return None if value is None else _string(value, field)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _optional_count(value: Any, field: str) -> int | None:
|
|
288
|
+
"""A 1-based line or column, or ``None``."""
|
|
289
|
+
if value is not None and (type(value) is not int or value < 1):
|
|
290
|
+
raise ProtocolError(f"{field} must be a positive integer or null.")
|
|
291
|
+
return value
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _ordinal(value: Any) -> int:
|
|
295
|
+
if type(value) is not int or value < 0:
|
|
296
|
+
raise ProtocolError("last_emit_ordinal must be a non-negative integer.")
|
|
297
|
+
return value
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _leaks(payload: Mapping[str, Any]) -> dict[str, int]:
|
|
301
|
+
pools = payload.get("leaked_threads_by_pool")
|
|
302
|
+
if (
|
|
303
|
+
not isinstance(pools, dict)
|
|
304
|
+
or set(pools) != {"body", "eval"}
|
|
305
|
+
or any(type(count) is not int or count < 0 for count in pools.values())
|
|
306
|
+
):
|
|
307
|
+
raise ProtocolError("invalid per-pool thread leak verdict.")
|
|
308
|
+
return pools
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _captured(payload: Any) -> CapturedOutput:
|
|
312
|
+
if payload is None:
|
|
313
|
+
return CapturedOutput(text="", truncated=False)
|
|
314
|
+
if not isinstance(payload, dict):
|
|
315
|
+
raise ProtocolError("stdout must be an object.")
|
|
316
|
+
return CapturedOutput(
|
|
317
|
+
_string(payload.get("text"), "stdout.text"), _boolean(payload.get("truncated"), "stdout.truncated")
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@dataclass(frozen=True, slots=True)
|
|
322
|
+
class EvaluationResult[T]:
|
|
323
|
+
value: T
|
|
324
|
+
stdout: CapturedOutput
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@dataclass(slots=True)
|
|
328
|
+
class _Pending:
|
|
329
|
+
method: str
|
|
330
|
+
key: str | None
|
|
331
|
+
future: asyncio.Future[dict[str, Any]]
|
|
332
|
+
lanes: tuple[str, ...]
|
|
333
|
+
sent: bool = False
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class WorkflowWorkerClient:
|
|
337
|
+
"""Async RPC client bound to one worker process; see the module docstring."""
|
|
338
|
+
|
|
339
|
+
def __init__(
|
|
340
|
+
self,
|
|
341
|
+
process: ManagedStdioProcess,
|
|
342
|
+
*,
|
|
343
|
+
bytecode_cache: Path,
|
|
344
|
+
ask_handler: AskHandler | None,
|
|
345
|
+
emit_handler: EmitHandler | None,
|
|
346
|
+
) -> None:
|
|
347
|
+
self._process = process
|
|
348
|
+
self._bytecode_cache = bytecode_cache
|
|
349
|
+
self._ask_handler = ask_handler
|
|
350
|
+
self._emit_handler = emit_handler
|
|
351
|
+
self._loop = asyncio.get_running_loop()
|
|
352
|
+
self._next_id = 1
|
|
353
|
+
self._pending: dict[int, _Pending] = {}
|
|
354
|
+
self._in_flight = {"body": 0, "eval": 0, "sync": 0}
|
|
355
|
+
self._capacity = {
|
|
356
|
+
"body": MAX_IN_FLIGHT_REQUESTS,
|
|
357
|
+
"eval": LIMITS.eval_thread_pool_size,
|
|
358
|
+
"sync": LIMITS.worker_thread_pool_size,
|
|
359
|
+
}
|
|
360
|
+
self._capacity_changed = asyncio.Event()
|
|
361
|
+
self._hello: asyncio.Future[Hello] = self._loop.create_future()
|
|
362
|
+
self._terminal: set[str] = set()
|
|
363
|
+
self._projected: dict[str, int] = {}
|
|
364
|
+
self._projection = asyncio.Condition()
|
|
365
|
+
self._emits: asyncio.Queue[tuple[AttemptRef, int, str] | None] = asyncio.Queue()
|
|
366
|
+
self._ask_tasks: dict[str, set[asyncio.Task[None]]] = defaultdict(set)
|
|
367
|
+
self._asks: set[asyncio.Task[None]] = set() # every live ask task, whichever attempt it served
|
|
368
|
+
self._lost: WorkerLostError | None = None
|
|
369
|
+
self._lost_event = asyncio.Event()
|
|
370
|
+
self._closing: asyncio.Task[None] | None = None
|
|
371
|
+
self._frames = 0
|
|
372
|
+
self._bytes = 0
|
|
373
|
+
self._stderr_tail = bytearray()
|
|
374
|
+
self._leaked_threads_by_pool = {"body": 0, "eval": 0}
|
|
375
|
+
self._reader = asyncio.create_task(self._read_loop(), name="chrys.workflow.worker.reader")
|
|
376
|
+
self._tasks = [
|
|
377
|
+
self._reader,
|
|
378
|
+
asyncio.create_task(self._dispatch_emits(), name="chrys.workflow.worker.emits"),
|
|
379
|
+
asyncio.create_task(self._read_stderr(), name="chrys.workflow.worker.stderr"),
|
|
380
|
+
asyncio.create_task(self._watch_exit(), name="chrys.workflow.worker.exit"),
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
# ----------------------------------------------------------------- lifecycle
|
|
384
|
+
|
|
385
|
+
@classmethod
|
|
386
|
+
async def launch(
|
|
387
|
+
cls,
|
|
388
|
+
*,
|
|
389
|
+
environment: PreparedEnvironment,
|
|
390
|
+
sdk: SdkArtifact,
|
|
391
|
+
workspace: Path,
|
|
392
|
+
bytecode_cache: Path,
|
|
393
|
+
env: Mapping[str, str] | None = None,
|
|
394
|
+
ask_handler: AskHandler | None = None,
|
|
395
|
+
emit_handler: EmitHandler | None = None,
|
|
396
|
+
host_path: Path = HOST_PATH,
|
|
397
|
+
hello_timeout: float = LIMITS.hello_timeout,
|
|
398
|
+
) -> WorkflowWorkerClient:
|
|
399
|
+
"""Spawn the host on the prepared interpreter, inject *sdk*, and wait for a hello that matches both.
|
|
400
|
+
|
|
401
|
+
The interpreter and every Python process it starts keep compiled
|
|
402
|
+
bytecode only under the private *bytecode_cache*, never next to a
|
|
403
|
+
source file, so a cache planted or left beside a workflow can't run in
|
|
404
|
+
place of the source that was confirmed.
|
|
405
|
+
"""
|
|
406
|
+
if environment.sdk_digest != sdk.digest:
|
|
407
|
+
raise WorkerStartError("The environment was prepared against a different SDK build than the one injected.")
|
|
408
|
+
try:
|
|
409
|
+
await asyncio.to_thread(ensure_owner_only_directory, bytecode_cache)
|
|
410
|
+
except OSError as exc:
|
|
411
|
+
raise WorkerStartError(
|
|
412
|
+
f"Cannot prepare the workflow bytecode cache {str(bytecode_cache)!r}: {exc}"
|
|
413
|
+
) from exc
|
|
414
|
+
child_env = dict(os.environ)
|
|
415
|
+
child_env.update(env or {})
|
|
416
|
+
child_env[BYTECODE_CACHE_ENV] = str(bytecode_cache)
|
|
417
|
+
try:
|
|
418
|
+
process = await spawn_managed_stdio_process(
|
|
419
|
+
environment.executable,
|
|
420
|
+
str(host_path),
|
|
421
|
+
str(sdk.path),
|
|
422
|
+
cwd=str(workspace),
|
|
423
|
+
env=child_env,
|
|
424
|
+
limit=_STDIO_LIMIT,
|
|
425
|
+
parent_env=dict(os.environ),
|
|
426
|
+
)
|
|
427
|
+
except MissingWorkingDirectoryError as exc:
|
|
428
|
+
raise WorkerStartError(f"Cannot start the workflow worker: {exc}") from exc
|
|
429
|
+
except OSError as exc:
|
|
430
|
+
raise WorkerStartError(f"Cannot start the workflow worker with {environment.executable!r}: {exc}") from exc
|
|
431
|
+
client = cls(process, bytecode_cache=bytecode_cache, ask_handler=ask_handler, emit_handler=emit_handler)
|
|
432
|
+
try:
|
|
433
|
+
await client._handshake(hello_timeout, environment)
|
|
434
|
+
except BaseException:
|
|
435
|
+
# Nobody else holds the client yet: a failed or cancelled launch owns its cleanup.
|
|
436
|
+
await client.close()
|
|
437
|
+
raise
|
|
438
|
+
return client
|
|
439
|
+
|
|
440
|
+
async def _handshake(self, hello_timeout: float, environment: PreparedEnvironment) -> None:
|
|
441
|
+
try:
|
|
442
|
+
hello = await asyncio.wait_for(asyncio.shield(self._hello), timeout=hello_timeout)
|
|
443
|
+
except TimeoutError:
|
|
444
|
+
raise WorkerStartError(f"The workflow worker sent no hello within {hello_timeout:g}s.") from None
|
|
445
|
+
except WorkerLostError as exc:
|
|
446
|
+
raise WorkerStartError(f"The workflow worker exited before hello: {exc}{self._stderr_suffix()}") from exc
|
|
447
|
+
if hello.protocol_version != PROTOCOL_VERSION:
|
|
448
|
+
raise WorkerStartError(f"Worker speaks protocol {hello.protocol_version}, expected {PROTOCOL_VERSION}.")
|
|
449
|
+
if not hello.sdk_origin_ok:
|
|
450
|
+
raise WorkerStartError(f"Workflow SDK injection failed: {hello.sdk_origin_error}")
|
|
451
|
+
facts = (hello.implementation, hello.python_version, hello.platform)
|
|
452
|
+
expected = (environment.implementation, environment.python_version, environment.platform)
|
|
453
|
+
if facts != expected:
|
|
454
|
+
raise WorkerStartError(
|
|
455
|
+
f"The worker is {' '.join(facts)}, but the environment was prepared as {' '.join(expected)}."
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def lost(self) -> WorkerLostError | None:
|
|
460
|
+
return self._lost
|
|
461
|
+
|
|
462
|
+
async def wait_lost(self) -> WorkerLostError:
|
|
463
|
+
"""Return once the worker is lost, for whatever reason (a close counts): the runner's watch."""
|
|
464
|
+
await self._lost_event.wait()
|
|
465
|
+
return self._lost_error()
|
|
466
|
+
|
|
467
|
+
@property
|
|
468
|
+
def stderr_tail(self) -> str:
|
|
469
|
+
return bytes(self._stderr_tail).decode("utf-8", errors="replace")
|
|
470
|
+
|
|
471
|
+
async def close(self, *, grace: float = LIMITS.shutdown_grace) -> None:
|
|
472
|
+
"""Shut the worker down: shutdown RPC, EOF, terminate, kill; concurrent callers share one close."""
|
|
473
|
+
if self._closing is None:
|
|
474
|
+
self._closing = asyncio.create_task(self._close(grace), name="chrys.workflow.worker.close")
|
|
475
|
+
await asyncio.shield(self._closing) # a cancelled waiter must not take the close down with it
|
|
476
|
+
|
|
477
|
+
async def _close(self, grace: float) -> None:
|
|
478
|
+
if self._lost is None and self._process.returncode is None:
|
|
479
|
+
with contextlib.suppress(WorkerError, TimeoutError, asyncio.CancelledError):
|
|
480
|
+
await asyncio.wait_for(self._request(Method.SHUTDOWN, {}, key=None), timeout=grace)
|
|
481
|
+
self._process.close_stdin()
|
|
482
|
+
if not await self._wait_exit(grace):
|
|
483
|
+
self._process.terminate_tree()
|
|
484
|
+
if not await self._wait_exit(grace):
|
|
485
|
+
self._process.kill_tree()
|
|
486
|
+
await self._wait_exit(grace)
|
|
487
|
+
self._mark_lost("worker closed")
|
|
488
|
+
for task in self._tasks:
|
|
489
|
+
task.cancel()
|
|
490
|
+
# Ask tasks were cancelled by settlement or by _mark_lost; their handlers may still be cleaning up.
|
|
491
|
+
for task in [*self._tasks, *self._asks]:
|
|
492
|
+
with contextlib.suppress(asyncio.CancelledError, Exception):
|
|
493
|
+
await task
|
|
494
|
+
self._process.close_transports()
|
|
495
|
+
|
|
496
|
+
async def _wait_exit(self, timeout: float) -> bool:
|
|
497
|
+
"""True once the worker and everything left in its process group are gone: the tree is what we own."""
|
|
498
|
+
deadline = self._loop.time() + timeout
|
|
499
|
+
try:
|
|
500
|
+
await asyncio.wait_for(self._process.wait(), timeout=timeout)
|
|
501
|
+
except TimeoutError:
|
|
502
|
+
return False
|
|
503
|
+
while self._group_alive():
|
|
504
|
+
if self._loop.time() >= deadline:
|
|
505
|
+
return False
|
|
506
|
+
await asyncio.sleep(0.02)
|
|
507
|
+
return True
|
|
508
|
+
|
|
509
|
+
def _group_alive(self) -> bool:
|
|
510
|
+
"""A body's child that ignores SIGTERM outlives the worker; it still sits in the worker's process group."""
|
|
511
|
+
group = self._process.process_group_id
|
|
512
|
+
if get_platform().is_windows or group is None: # the Windows job object takes the tree down on close
|
|
513
|
+
return False
|
|
514
|
+
try:
|
|
515
|
+
cast(Any, os).killpg(group, 0)
|
|
516
|
+
except ProcessLookupError:
|
|
517
|
+
return False
|
|
518
|
+
except PermissionError: # macOS: a member that is exiting but not yet reaped answers EPERM
|
|
519
|
+
pass
|
|
520
|
+
return True
|
|
521
|
+
|
|
522
|
+
# ----------------------------------------------------------------- RPC surface
|
|
523
|
+
|
|
524
|
+
async def load(
|
|
525
|
+
self,
|
|
526
|
+
source: bytes,
|
|
527
|
+
*,
|
|
528
|
+
filename: str,
|
|
529
|
+
workspace: Path,
|
|
530
|
+
package_dir: str | None = None,
|
|
531
|
+
diagnose: bool = False,
|
|
532
|
+
precompile: Sequence[str] = (),
|
|
533
|
+
) -> LoadResult:
|
|
534
|
+
"""Execute the workflow file from *source* bytes; ``filename`` is its original canonical path.
|
|
535
|
+
|
|
536
|
+
*package_dir* names a workflow folder: its modules' cached bytecode is
|
|
537
|
+
cleared first, so every file in it compiles from its current source.
|
|
538
|
+
With *diagnose*, the worker first compiles the *precompile* files (the
|
|
539
|
+
folder's other Python files) without running them, places a failure
|
|
540
|
+
in ``load_diagnostics(error.data)`` and reports where each node was
|
|
541
|
+
declared; a load that times out carries ``data["reason"] == LOAD_TIMED_OUT``.
|
|
542
|
+
"""
|
|
543
|
+
try:
|
|
544
|
+
text = source.decode("utf-8")
|
|
545
|
+
except UnicodeDecodeError as exc:
|
|
546
|
+
raise WorkerRpcError(ErrorCode.LOAD_FAILED, f"workflow file is not UTF-8: {exc}") from exc
|
|
547
|
+
params: dict[str, Any] = {
|
|
548
|
+
"source": text,
|
|
549
|
+
"filename": filename,
|
|
550
|
+
"workspace": str(workspace),
|
|
551
|
+
"bytecode_cache": str(self._bytecode_cache),
|
|
552
|
+
}
|
|
553
|
+
if package_dir is not None:
|
|
554
|
+
params["package_dir"] = package_dir
|
|
555
|
+
if diagnose:
|
|
556
|
+
params["diagnose"] = True
|
|
557
|
+
params["precompile"] = list(precompile)
|
|
558
|
+
try:
|
|
559
|
+
result = await asyncio.wait_for(self._call(Method.LOAD, params, key=None), timeout=LIMITS.load_timeout)
|
|
560
|
+
except TimeoutError:
|
|
561
|
+
await self.close()
|
|
562
|
+
raise WorkerRpcError(
|
|
563
|
+
ErrorCode.LOAD_FAILED,
|
|
564
|
+
f"Workflow load did not finish within {LIMITS.load_timeout:g}s.",
|
|
565
|
+
{"reason": LOAD_TIMED_OUT},
|
|
566
|
+
) from None
|
|
567
|
+
manifest = result.get("manifest")
|
|
568
|
+
if not isinstance(manifest, dict):
|
|
569
|
+
raise ProtocolError("load result must carry a manifest object.")
|
|
570
|
+
if type(manifest.get("schema_version")) is not int:
|
|
571
|
+
raise ProtocolError("manifest schema_version must be an integer.")
|
|
572
|
+
try:
|
|
573
|
+
manifest_warnings(manifest)
|
|
574
|
+
except ManifestError as exc:
|
|
575
|
+
raise ProtocolError(str(exc)) from exc
|
|
576
|
+
return LoadResult(
|
|
577
|
+
entry_digest=hashlib.sha256(source).hexdigest(),
|
|
578
|
+
manifest_digest=hashlib.sha256(canonical_json(manifest).encode("utf-8", "surrogatepass")).hexdigest(),
|
|
579
|
+
manifest=manifest,
|
|
580
|
+
stdout=_captured(result.get("stdout")),
|
|
581
|
+
sites=_load_sites(result.get("sites")),
|
|
582
|
+
sites_truncated=_boolean(result.get("sites_truncated", False), "sites_truncated"),
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
async def run_python(
|
|
586
|
+
self, ref: AttemptRef, value: WorkflowValue, *, blocking: bool, timeout: float | None = None
|
|
587
|
+
) -> PythonResult:
|
|
588
|
+
"""Run one python-node attempt; raises :class:`WorkerRpcError`, :class:`AttemptTimeout`, or lost."""
|
|
589
|
+
params = {"ref": ref_to_wire(ref), "value": value_to_wire(value)}
|
|
590
|
+
frame = await self._request(
|
|
591
|
+
Method.RUN_PYTHON, params, key=ref_key(ref), ref=ref, timeout=timeout, blocking=blocking
|
|
592
|
+
)
|
|
593
|
+
error = frame.get("error")
|
|
594
|
+
if error is not None:
|
|
595
|
+
data = error.get("data", {})
|
|
596
|
+
if error["code"] != ErrorCode.ATTEMPT_TERMINATED:
|
|
597
|
+
await self.wait_projected(ref, _ordinal(data.get("last_emit_ordinal", 0)))
|
|
598
|
+
raise WorkerRpcError(error["code"], error["message"], data)
|
|
599
|
+
result = frame["result"]
|
|
600
|
+
last_emit_ordinal = _ordinal(result.get("last_emit_ordinal", 0))
|
|
601
|
+
await self.wait_projected(ref, last_emit_ordinal)
|
|
602
|
+
return PythonResult(
|
|
603
|
+
value=value_from_wire(result.get("value"), where="python result"),
|
|
604
|
+
stdout=_captured(result.get("stdout")),
|
|
605
|
+
last_emit_ordinal=last_emit_ordinal,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
async def eval_outgoing(
|
|
609
|
+
self, ref: AttemptRef, value: WorkflowValue, edge_ids: tuple[str, ...], *, timeout: float | None = None
|
|
610
|
+
) -> EvaluationResult[dict[str, bool]]:
|
|
611
|
+
params = {"ref": ref_to_wire(ref), "value": value_to_wire(value), "edge_ids": list(edge_ids)}
|
|
612
|
+
result = await self._evaluate(Method.EVAL_OUTGOING, params, ref, timeout)
|
|
613
|
+
decisions = result.get("decisions")
|
|
614
|
+
if not isinstance(decisions, dict) or set(decisions) != set(edge_ids):
|
|
615
|
+
raise ProtocolError("eval_outgoing decisions do not cover the requested edges.")
|
|
616
|
+
return EvaluationResult(
|
|
617
|
+
{edge_id: _boolean(decisions[edge_id], "edge decision") for edge_id in edge_ids},
|
|
618
|
+
_captured(result.get("stdout")),
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
async def eval_loop_until(
|
|
622
|
+
self, ref: AttemptRef, iteration: int, value: WorkflowValue, *, timeout: float | None = None
|
|
623
|
+
) -> EvaluationResult[bool]:
|
|
624
|
+
params = {"ref": ref_to_wire(ref), "iteration": iteration, "value": value_to_wire(value)}
|
|
625
|
+
result = await self._evaluate(Method.EVAL_LOOP_UNTIL, params, ref, timeout)
|
|
626
|
+
return EvaluationResult(_boolean(result.get("verdict"), "until verdict"), _captured(result.get("stdout")))
|
|
627
|
+
|
|
628
|
+
async def combine(
|
|
629
|
+
self, ref: AttemptRef, sources: tuple[SourceValue, ...], *, timeout: float | None = None
|
|
630
|
+
) -> EvaluationResult[WorkflowValue]:
|
|
631
|
+
wire = [source_to_wire(source) for source in sources]
|
|
632
|
+
retained = len(canonical_json(wire).encode("utf-8", "surrogatepass"))
|
|
633
|
+
if retained > LIMITS.max_join_retained_bytes:
|
|
634
|
+
raise WorkerRpcError(ErrorCode.VALUE_TOO_LARGE, "join sources exceed max_join_retained_bytes.")
|
|
635
|
+
result = await self._evaluate(Method.COMBINE, {"ref": ref_to_wire(ref), "sources": wire}, ref, timeout)
|
|
636
|
+
return EvaluationResult(
|
|
637
|
+
value_from_wire(result.get("value"), where="combine result"), _captured(result.get("stdout"))
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
async def cancel(self, ref: AttemptRef) -> CancelResult:
|
|
641
|
+
"""Cancel exactly this attempt; the ack sums the leak verdicts its terminal envelopes just carried."""
|
|
642
|
+
result = await self._call(Method.CANCEL, {"ref": ref_to_wire(ref)}, key=None)
|
|
643
|
+
outcome = CancelResult(
|
|
644
|
+
cancelled=_boolean(result.get("cancelled"), "cancelled"), leaked_threads_by_pool=_leaks(result)
|
|
645
|
+
)
|
|
646
|
+
if self._lost is not None:
|
|
647
|
+
raise self._lost_error()
|
|
648
|
+
return outcome
|
|
649
|
+
|
|
650
|
+
async def native_output(self) -> NativeOutput:
|
|
651
|
+
result = await self._call(Method.NATIVE_OUTPUT, {}, key=None)
|
|
652
|
+
return NativeOutput(text=str(result["text"]), dropped_bytes=int(result["dropped_bytes"]))
|
|
653
|
+
|
|
654
|
+
async def wait_projected(self, ref: AttemptRef, ordinal: int) -> None:
|
|
655
|
+
"""Block until every emit of *ref* up to *ordinal* went through the emit handler."""
|
|
656
|
+
key = ref_key(ref)
|
|
657
|
+
async with self._projection:
|
|
658
|
+
await self._projection.wait_for(lambda: self._lost is not None or self._projected.get(key, 0) >= ordinal)
|
|
659
|
+
if self._projected.get(key, 0) < ordinal:
|
|
660
|
+
raise self._lost_error() # the emits can no longer be projected: converge to worker-lost
|
|
661
|
+
|
|
662
|
+
# ----------------------------------------------------------------- internals
|
|
663
|
+
|
|
664
|
+
async def _evaluate(
|
|
665
|
+
self, method: str, params: dict[str, Any], ref: AttemptRef, timeout: float | None
|
|
666
|
+
) -> dict[str, Any]:
|
|
667
|
+
deadline = LIMITS.eval_deadline if timeout is None else timeout
|
|
668
|
+
frame = await self._request(method, params, key=None, ref=ref, timeout=deadline)
|
|
669
|
+
error = frame.get("error")
|
|
670
|
+
if error is not None:
|
|
671
|
+
raise WorkerRpcError(error["code"], error["message"], error.get("data"))
|
|
672
|
+
return frame["result"]
|
|
673
|
+
|
|
674
|
+
async def _deadline(
|
|
675
|
+
self, request: Awaitable[dict[str, Any]], pending: _Pending, ref: AttemptRef, timeout: float | None
|
|
676
|
+
) -> dict[str, Any]:
|
|
677
|
+
if timeout is None:
|
|
678
|
+
return await request
|
|
679
|
+
task = asyncio.ensure_future(request)
|
|
680
|
+
try:
|
|
681
|
+
done, _ = await asyncio.wait({task}, timeout=timeout)
|
|
682
|
+
if done:
|
|
683
|
+
return task.result()
|
|
684
|
+
grace = LIMITS.shutdown_grace
|
|
685
|
+
try:
|
|
686
|
+
outcome = await asyncio.wait_for(self.cancel(ref), timeout=grace)
|
|
687
|
+
except TimeoutError:
|
|
688
|
+
self._mark_lost(f"cancel of {ref.activation_id} was not acknowledged within {grace:g}s")
|
|
689
|
+
raise self._lost_error() from None
|
|
690
|
+
# The worker sends the terminal envelope before acknowledging cancellation.
|
|
691
|
+
if not pending.future.done():
|
|
692
|
+
self._mark_lost(f"cancel of {ref.activation_id} was acknowledged without a terminal envelope")
|
|
693
|
+
frame = pending.future.result()
|
|
694
|
+
payload = frame["error"].get("data", {}) if "error" in frame else frame["result"]
|
|
695
|
+
raise AttemptTimeout(ref, leaked_thread=any(outcome.leaked_threads_by_pool.values()), data=payload)
|
|
696
|
+
finally:
|
|
697
|
+
if not task.done():
|
|
698
|
+
task.cancel()
|
|
699
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
700
|
+
|
|
701
|
+
async def _call(self, method: str, params: dict[str, Any], *, key: str | None) -> dict[str, Any]:
|
|
702
|
+
frame = await self._request(method, params, key=key)
|
|
703
|
+
error = frame.get("error")
|
|
704
|
+
if error is not None:
|
|
705
|
+
raise WorkerRpcError(error["code"], error["message"], error.get("data"))
|
|
706
|
+
return frame["result"]
|
|
707
|
+
|
|
708
|
+
async def _request(
|
|
709
|
+
self,
|
|
710
|
+
method: str,
|
|
711
|
+
params: dict[str, Any],
|
|
712
|
+
*,
|
|
713
|
+
key: str | None,
|
|
714
|
+
ref: AttemptRef | None = None,
|
|
715
|
+
timeout: float | None = None,
|
|
716
|
+
blocking: bool = False,
|
|
717
|
+
) -> dict[str, Any]:
|
|
718
|
+
# No deadline runs while waiting for capacity. The pending entry owns its slot, even when
|
|
719
|
+
# its caller stops waiting: only a terminal envelope, loss, or a never-sent frame releases it.
|
|
720
|
+
lanes = () if method in _CONTROL_METHODS else ("eval",) if method in _EVAL_METHODS else ("body",)
|
|
721
|
+
if blocking:
|
|
722
|
+
lanes += ("sync",)
|
|
723
|
+
while (
|
|
724
|
+
self._lost is None
|
|
725
|
+
and lanes
|
|
726
|
+
and (
|
|
727
|
+
any(self._in_flight[lane] >= self._capacity[lane] for lane in lanes)
|
|
728
|
+
or len(self._pending) >= LIMITS.max_pending_requests
|
|
729
|
+
)
|
|
730
|
+
):
|
|
731
|
+
self._capacity_changed.clear()
|
|
732
|
+
await self._capacity_changed.wait()
|
|
733
|
+
if self._lost is not None:
|
|
734
|
+
raise self._lost_error()
|
|
735
|
+
request_id = self._next_id
|
|
736
|
+
self._next_id += 2
|
|
737
|
+
data = _encode({"id": request_id, "method": method, "params": params})
|
|
738
|
+
future: asyncio.Future[dict[str, Any]] = self._loop.create_future()
|
|
739
|
+
pending = _Pending(method, key, future, lanes)
|
|
740
|
+
self._pending[request_id] = pending
|
|
741
|
+
for lane in lanes:
|
|
742
|
+
self._in_flight[lane] += 1
|
|
743
|
+
try:
|
|
744
|
+
request = self._exchange(data, pending)
|
|
745
|
+
return await request if ref is None else await self._deadline(request, pending, ref, timeout)
|
|
746
|
+
finally:
|
|
747
|
+
if not pending.sent:
|
|
748
|
+
self._pop_pending(request_id)
|
|
749
|
+
if not future.done():
|
|
750
|
+
future.cancel()
|
|
751
|
+
elif not future.cancelled():
|
|
752
|
+
future.exception() # loss during transmission may settle it before _exchange awaits it
|
|
753
|
+
|
|
754
|
+
async def _exchange(self, data: bytes, pending: _Pending) -> dict[str, Any]:
|
|
755
|
+
await self._transmit(data, pending=pending)
|
|
756
|
+
return await pending.future
|
|
757
|
+
|
|
758
|
+
def _pop_pending(self, request_id: int) -> _Pending | None:
|
|
759
|
+
pending = self._pending.pop(request_id, None)
|
|
760
|
+
if pending is not None:
|
|
761
|
+
for lane in pending.lanes:
|
|
762
|
+
self._in_flight[lane] -= 1
|
|
763
|
+
self._capacity_changed.set()
|
|
764
|
+
return pending
|
|
765
|
+
|
|
766
|
+
async def _send(self, frame: dict[str, Any]) -> None:
|
|
767
|
+
await self._transmit(_encode(frame))
|
|
768
|
+
|
|
769
|
+
async def _transmit(self, data: bytes, *, pending: _Pending | None = None) -> None:
|
|
770
|
+
self._account(len(data))
|
|
771
|
+
if self._lost is not None:
|
|
772
|
+
raise self._lost_error()
|
|
773
|
+
try:
|
|
774
|
+
self._process.stdin.write(data)
|
|
775
|
+
if pending is not None:
|
|
776
|
+
pending.sent = True
|
|
777
|
+
await self._process.stdin.drain()
|
|
778
|
+
except (OSError, RuntimeError, asyncio.CancelledError) as exc:
|
|
779
|
+
if isinstance(exc, asyncio.CancelledError):
|
|
780
|
+
raise
|
|
781
|
+
self._mark_lost(f"write failed: {exc}")
|
|
782
|
+
raise self._lost_error() from exc
|
|
783
|
+
|
|
784
|
+
def _account(self, size: int) -> None:
|
|
785
|
+
self._frames += 1
|
|
786
|
+
self._bytes += size
|
|
787
|
+
if self._frames > LIMITS.max_run_frames or self._bytes > LIMITS.max_run_bytes:
|
|
788
|
+
self._mark_lost("run frame budget exhausted")
|
|
789
|
+
|
|
790
|
+
def _charge(self, frame: dict[str, Any]) -> None:
|
|
791
|
+
"""Charge the threads an attempt left behind to the pool budget.
|
|
792
|
+
|
|
793
|
+
Called once per attempt, from its terminal envelope: each phase of a ref (body, then each
|
|
794
|
+
evaluation) is its own attempt with its own verdict, and a cancel ack only sums the verdicts the
|
|
795
|
+
envelopes it follows already carried.
|
|
796
|
+
"""
|
|
797
|
+
payload = frame["error"].get("data", {}) if "error" in frame else frame["result"]
|
|
798
|
+
# Invalid requests may fail before an attempt was registered, without a pool verdict.
|
|
799
|
+
if "error" in frame and "leaked_threads_by_pool" not in payload:
|
|
800
|
+
return
|
|
801
|
+
pools = _leaks(payload)
|
|
802
|
+
for pool, budget in (("body", LIMITS.worker_leak_budget), ("eval", LIMITS.eval_leak_budget)):
|
|
803
|
+
self._leaked_threads_by_pool[pool] += pools[pool]
|
|
804
|
+
if self._leaked_threads_by_pool[pool] >= budget:
|
|
805
|
+
self._mark_lost(f"{pool} thread leak budget exhausted")
|
|
806
|
+
# Do not reuse capacity charged as leaked: its abandoned work may never finish. Keep
|
|
807
|
+
# admission matched to the remaining threads, rather than queueing behind abandoned work.
|
|
808
|
+
self._capacity["eval"] = max(0, LIMITS.eval_thread_pool_size - self._leaked_threads_by_pool["eval"])
|
|
809
|
+
self._capacity["sync"] = max(0, LIMITS.worker_thread_pool_size - self._leaked_threads_by_pool["body"])
|
|
810
|
+
|
|
811
|
+
def _lost_error(self) -> WorkerLostError:
|
|
812
|
+
if self._lost is None:
|
|
813
|
+
raise RuntimeError("The workflow worker has not been marked lost.")
|
|
814
|
+
return self._lost
|
|
815
|
+
|
|
816
|
+
def _stderr_suffix(self) -> str:
|
|
817
|
+
tail = self.stderr_tail.strip()
|
|
818
|
+
return f" (stderr: {tail[-500:]})" if tail else ""
|
|
819
|
+
|
|
820
|
+
def _mark_lost(self, reason: str) -> None:
|
|
821
|
+
if self._lost is not None:
|
|
822
|
+
return
|
|
823
|
+
self._lost = WorkerLostError(reason)
|
|
824
|
+
self._lost_event.set()
|
|
825
|
+
for request_id in tuple(self._pending):
|
|
826
|
+
pending = self._pop_pending(request_id)
|
|
827
|
+
if pending is None:
|
|
828
|
+
raise RuntimeError("A registered worker request disappeared during failure handling.")
|
|
829
|
+
if not pending.future.done():
|
|
830
|
+
pending.future.set_exception(self._lost)
|
|
831
|
+
self._capacity_changed.set() # also wake callers that never registered a request
|
|
832
|
+
if not self._hello.done():
|
|
833
|
+
self._hello.set_exception(self._lost)
|
|
834
|
+
for tasks in self._ask_tasks.values():
|
|
835
|
+
for task in tasks:
|
|
836
|
+
task.cancel()
|
|
837
|
+
self._emits.put_nowait(None)
|
|
838
|
+
self._loop.create_task(self._wake_projection(), name="chrys.workflow.worker.wake")
|
|
839
|
+
with contextlib.suppress(Exception):
|
|
840
|
+
self._process.terminate_tree()
|
|
841
|
+
|
|
842
|
+
async def _wake_projection(self) -> None:
|
|
843
|
+
async with self._projection:
|
|
844
|
+
self._projection.notify_all()
|
|
845
|
+
|
|
846
|
+
async def _read_loop(self) -> None:
|
|
847
|
+
stdout = self._process.stdout
|
|
848
|
+
reason = "worker exited"
|
|
849
|
+
try:
|
|
850
|
+
while True:
|
|
851
|
+
line = await stdout.readline()
|
|
852
|
+
if not line:
|
|
853
|
+
break
|
|
854
|
+
self._account(len(line))
|
|
855
|
+
self._on_frame(decode_frame(line.rstrip(b"\r\n")))
|
|
856
|
+
except asyncio.CancelledError:
|
|
857
|
+
raise
|
|
858
|
+
except (ProtocolError, ValueError) as exc:
|
|
859
|
+
reason = f"protocol error: {exc}"
|
|
860
|
+
except Exception as exc:
|
|
861
|
+
reason = f"reader failed: {exc!r}"
|
|
862
|
+
self._mark_lost(f"{reason}{self._stderr_suffix()}")
|
|
863
|
+
|
|
864
|
+
async def _watch_exit(self) -> None:
|
|
865
|
+
"""The exit status settles a dead worker's debts even while a forked child of its holds the pipes open."""
|
|
866
|
+
code = await self._process.wait()
|
|
867
|
+
# Frames written before the exit are still in the pipe: the reader drains them and, at EOF, marks the loss.
|
|
868
|
+
await asyncio.wait({self._reader}, timeout=LIMITS.shutdown_grace)
|
|
869
|
+
self._mark_lost(f"worker exited with status {code}{self._stderr_suffix()}")
|
|
870
|
+
|
|
871
|
+
async def _read_stderr(self) -> None:
|
|
872
|
+
stderr = self._process.stderr
|
|
873
|
+
while chunk := await stderr.read(64 * 1024):
|
|
874
|
+
self._stderr_tail.extend(chunk)
|
|
875
|
+
if len(self._stderr_tail) > STDERR_TAIL_BYTES:
|
|
876
|
+
del self._stderr_tail[:-STDERR_TAIL_BYTES]
|
|
877
|
+
|
|
878
|
+
def _on_frame(self, frame: dict[str, Any]) -> None:
|
|
879
|
+
if self._lost is not None:
|
|
880
|
+
return # lost: its debts are settled already, and an ask would start a handler nobody cancels
|
|
881
|
+
if "method" not in frame:
|
|
882
|
+
pending = self._pop_pending(frame["id"])
|
|
883
|
+
if pending is None:
|
|
884
|
+
return
|
|
885
|
+
if pending.key is not None and pending.method == Method.RUN_PYTHON:
|
|
886
|
+
self._settle_attempt(pending.key)
|
|
887
|
+
if pending.method in _ATTEMPT_METHODS:
|
|
888
|
+
try:
|
|
889
|
+
self._charge(frame)
|
|
890
|
+
except (TypeError, ValueError) as exc:
|
|
891
|
+
self._mark_lost(str(exc))
|
|
892
|
+
if not pending.future.done():
|
|
893
|
+
pending.future.set_exception(self._lost_error())
|
|
894
|
+
return
|
|
895
|
+
if not pending.future.done():
|
|
896
|
+
pending.future.set_result(frame)
|
|
897
|
+
return
|
|
898
|
+
method, params = frame["method"], frame["params"]
|
|
899
|
+
if "id" in frame:
|
|
900
|
+
if method == Method.ASK:
|
|
901
|
+
self._on_ask(frame["id"], params)
|
|
902
|
+
else:
|
|
903
|
+
self._loop.create_task(
|
|
904
|
+
self._respond_error(frame["id"], ErrorCode.UNKNOWN_METHOD, f"unknown reverse request {method!r}.")
|
|
905
|
+
)
|
|
906
|
+
elif method == Method.HELLO:
|
|
907
|
+
if not self._hello.done():
|
|
908
|
+
self._hello.set_result(_hello_from_wire(params))
|
|
909
|
+
elif method == Method.EMIT:
|
|
910
|
+
self._on_emit(params)
|
|
911
|
+
|
|
912
|
+
def _settle_attempt(self, key: str) -> None:
|
|
913
|
+
self._terminal.add(key)
|
|
914
|
+
for task in self._ask_tasks.pop(key, ()):
|
|
915
|
+
task.cancel()
|
|
916
|
+
|
|
917
|
+
def _on_emit(self, params: dict[str, Any]) -> None:
|
|
918
|
+
ref = ref_from_wire(params.get("ref"))
|
|
919
|
+
ordinal, text = params.get("ordinal"), params.get("text")
|
|
920
|
+
if not isinstance(ordinal, int) or isinstance(ordinal, bool) or not isinstance(text, str):
|
|
921
|
+
raise ProtocolError("emit needs an int ordinal and a str text.")
|
|
922
|
+
if ref_key(ref) in self._terminal:
|
|
923
|
+
return
|
|
924
|
+
self._emits.put_nowait((ref, ordinal, text))
|
|
925
|
+
|
|
926
|
+
async def _dispatch_emits(self) -> None:
|
|
927
|
+
while (item := await self._emits.get()) is not None:
|
|
928
|
+
ref, ordinal, text = item
|
|
929
|
+
try:
|
|
930
|
+
if self._emit_handler is not None:
|
|
931
|
+
await self._emit_handler(ref, ordinal, text)
|
|
932
|
+
except asyncio.CancelledError:
|
|
933
|
+
raise
|
|
934
|
+
except Exception:
|
|
935
|
+
logger.warning("workflow emit handler failed for %s", ref.activation_id, exc_info=True)
|
|
936
|
+
finally:
|
|
937
|
+
async with self._projection:
|
|
938
|
+
key = ref_key(ref)
|
|
939
|
+
self._projected[key] = max(self._projected.get(key, 0), ordinal)
|
|
940
|
+
self._projection.notify_all()
|
|
941
|
+
|
|
942
|
+
def _on_ask(self, request_id: int, params: dict[str, Any]) -> None:
|
|
943
|
+
ref = ref_from_wire(params.get("ref"))
|
|
944
|
+
questions = questions_from_wire(params.get("questions"))
|
|
945
|
+
key = ref_key(ref)
|
|
946
|
+
if key in self._terminal:
|
|
947
|
+
self._loop.create_task(self._respond_error(request_id, ErrorCode.ATTEMPT_TERMINATED, "attempt is over."))
|
|
948
|
+
return
|
|
949
|
+
if self._ask_handler is None:
|
|
950
|
+
self._loop.create_task(
|
|
951
|
+
self._respond_error(request_id, ErrorCode.ASK_UNAVAILABLE, "nobody can answer in this run.")
|
|
952
|
+
)
|
|
953
|
+
return
|
|
954
|
+
task = self._loop.create_task(self._serve_ask(request_id, ref, questions), name="chrys.workflow.worker.ask")
|
|
955
|
+
self._ask_tasks[key].add(task)
|
|
956
|
+
self._asks.add(task)
|
|
957
|
+
task.add_done_callback(lambda done, key=key: self._forget_ask(key, done))
|
|
958
|
+
|
|
959
|
+
def _forget_ask(self, key: str, task: asyncio.Task[None]) -> None:
|
|
960
|
+
self._ask_tasks[key].discard(task)
|
|
961
|
+
self._asks.discard(task)
|
|
962
|
+
|
|
963
|
+
async def _serve_ask(self, request_id: int, ref: AttemptRef, questions: tuple[AskUserQuestion, ...]) -> None:
|
|
964
|
+
if self._ask_handler is None:
|
|
965
|
+
raise RuntimeError("Serving a workflow question requires an ask handler.")
|
|
966
|
+
try:
|
|
967
|
+
answers = answers_to_wire(questions, await self._ask_handler(ref, questions))
|
|
968
|
+
except asyncio.CancelledError:
|
|
969
|
+
with contextlib.suppress(WorkerError):
|
|
970
|
+
await self._respond_error(request_id, ErrorCode.ATTEMPT_TERMINATED, "attempt is over.")
|
|
971
|
+
raise
|
|
972
|
+
except AskUnavailable as exc:
|
|
973
|
+
await self._respond_error(request_id, ErrorCode.ASK_UNAVAILABLE, str(exc) or "nobody can answer.")
|
|
974
|
+
except Exception as exc:
|
|
975
|
+
logger.warning("workflow ask handler failed for %s", ref.activation_id, exc_info=True)
|
|
976
|
+
await self._respond_error(request_id, ErrorCode.ASK_UNAVAILABLE, f"ask handler failed: {exc!r}")
|
|
977
|
+
else:
|
|
978
|
+
try:
|
|
979
|
+
await self._send({"id": request_id, "result": {"answers": answers}})
|
|
980
|
+
except WorkerRpcError as exc: # the answer cannot be framed: the worker must not wait forever
|
|
981
|
+
await self._respond_error(request_id, ErrorCode.ASK_UNAVAILABLE, f"answer is not serializable: {exc}")
|
|
982
|
+
except WorkerLostError:
|
|
983
|
+
pass
|
|
984
|
+
|
|
985
|
+
async def _respond_error(self, request_id: int, code: str, message: str) -> None:
|
|
986
|
+
message = _bounded_diagnostic(message.encode("utf-8", "replace").decode("utf-8")) # must always frame
|
|
987
|
+
with contextlib.suppress(WorkerError):
|
|
988
|
+
await self._send({"id": request_id, "error": {"code": code, "message": message, "data": {}}})
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def _hello_from_wire(params: dict[str, Any]) -> Hello:
|
|
992
|
+
try:
|
|
993
|
+
return Hello(
|
|
994
|
+
protocol_version=int(params["protocol_version"]),
|
|
995
|
+
python_version=str(params["python_version"]),
|
|
996
|
+
implementation=str(params["implementation"]),
|
|
997
|
+
platform=str(params["platform"]),
|
|
998
|
+
sdk_origin_ok=bool(params["sdk_origin_ok"]),
|
|
999
|
+
sdk_origin_error=None if params.get("sdk_origin_error") is None else str(params["sdk_origin_error"]),
|
|
1000
|
+
)
|
|
1001
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
1002
|
+
raise ProtocolError(f"hello frame is malformed: {exc!r}") from exc
|