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,2228 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""SubAgentTools — manages sub-agent instances and concurrency control.
|
|
4
|
+
|
|
5
|
+
Creates FunctionTool wrappers around sub-agents so the parent agent can
|
|
6
|
+
delegate tasks via tool calls. Enforces two levels of concurrency limits:
|
|
7
|
+
|
|
8
|
+
- **Global**: ``max_total_concurrency`` caps total active sub-agent calls.
|
|
9
|
+
- **Per-agent**: ``SubAgentRef.max_concurrency`` caps calls to a single sub-agent.
|
|
10
|
+
|
|
11
|
+
When either limit is exceeded, the tool returns an error string immediately
|
|
12
|
+
rather than blocking.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import contextlib
|
|
19
|
+
import logging
|
|
20
|
+
import tempfile
|
|
21
|
+
from contextvars import Token
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
24
|
+
from uuid import uuid4
|
|
25
|
+
|
|
26
|
+
from chrys.foundation.config.settings import DEFAULT_ASK_USER_TIMEOUT_SECONDS
|
|
27
|
+
from chrys.foundation.events.types import (
|
|
28
|
+
InvocationCompactionCommitted,
|
|
29
|
+
InvocationCompactionFinished,
|
|
30
|
+
InvocationCompactionStarted,
|
|
31
|
+
InvocationContextPressure,
|
|
32
|
+
InvocationMessage,
|
|
33
|
+
InvocationRetryAttempt,
|
|
34
|
+
InvocationStarted,
|
|
35
|
+
Warning,
|
|
36
|
+
)
|
|
37
|
+
from chrys.foundation.models.invocations import InvocationOrigin
|
|
38
|
+
from chrys.foundation.platform.files import (
|
|
39
|
+
secure_unlink_owner_verified,
|
|
40
|
+
)
|
|
41
|
+
from chrys.foundation.retry import RetryAttemptInfo
|
|
42
|
+
from chrys.foundation.tool_kinds import (
|
|
43
|
+
KIND_ASK_USER,
|
|
44
|
+
KIND_FILESYSTEM_READ,
|
|
45
|
+
KIND_SHELL,
|
|
46
|
+
KIND_SUB_AGENT,
|
|
47
|
+
get_tool_kind,
|
|
48
|
+
set_tool_kind,
|
|
49
|
+
)
|
|
50
|
+
from chrys.foundation.tool_result_metadata import tool_result_metadata_failure_state
|
|
51
|
+
from chrys.foundation.trajectory.context import TRAJECTORY_CONTEXT_KWARG, trajectory_scope
|
|
52
|
+
from chrys.foundation.trajectory.event_types import TraceCoverage
|
|
53
|
+
from chrys.foundation.util.filenames import safe_fs_name
|
|
54
|
+
from chrys.foundation.util.sub_agent_context import (
|
|
55
|
+
SubAgentParentResultMetadata,
|
|
56
|
+
sub_agent_parent_call_id,
|
|
57
|
+
sub_agent_parent_result_metadata,
|
|
58
|
+
)
|
|
59
|
+
from chrys.kernel import (
|
|
60
|
+
Agent,
|
|
61
|
+
FunctionTool,
|
|
62
|
+
LoopRecorder,
|
|
63
|
+
Message,
|
|
64
|
+
StallExhaustedAction,
|
|
65
|
+
)
|
|
66
|
+
from chrys.orchestration.invoker.acp_protocol import AcpPermissionBroker
|
|
67
|
+
from chrys.orchestration.invoker.acp_spec import resolve_acp_spec
|
|
68
|
+
from chrys.orchestration.invoker.attempts import AgentRunKwargs, WireRecipe
|
|
69
|
+
from chrys.orchestration.invoker.child_history import service_storage_side
|
|
70
|
+
from chrys.orchestration.invoker.contracts import SubAgentFailureReason, SubAgentStatus, Unbind
|
|
71
|
+
from chrys.orchestration.invoker.origin import BoundEmitter, current_invocation_origin, invocation_routing_key
|
|
72
|
+
from chrys.orchestration.invoker.resources import Conversation, OperationLifetime, PreparedAgent, rollback_resources
|
|
73
|
+
from chrys.orchestration.invoker.runtime import (
|
|
74
|
+
ApprovalInputs,
|
|
75
|
+
AskUserInputs,
|
|
76
|
+
ChildRecipe,
|
|
77
|
+
ContextInputs,
|
|
78
|
+
KernelRuntime,
|
|
79
|
+
LastWordsInputs,
|
|
80
|
+
ReminderInputs,
|
|
81
|
+
SharedRuntime,
|
|
82
|
+
create_approval,
|
|
83
|
+
create_ask_user,
|
|
84
|
+
create_runtime,
|
|
85
|
+
create_validation,
|
|
86
|
+
)
|
|
87
|
+
from chrys.orchestration.sub_agents.acp_policy import AcpSubAgentPolicy
|
|
88
|
+
from chrys.orchestration.sub_agents.kernel_policy import KernelSubAgentPolicy
|
|
89
|
+
from chrys.service.acp_client import AcpConfigError
|
|
90
|
+
from chrys.service.agent_middleware import (
|
|
91
|
+
ApprovalMiddleware,
|
|
92
|
+
IntermediateTextBuffer,
|
|
93
|
+
SleepMiddleware,
|
|
94
|
+
SubAgentEventMiddleware,
|
|
95
|
+
SubAgentStatsMiddleware,
|
|
96
|
+
)
|
|
97
|
+
from chrys.service.approval.policy import ApprovalMode, ApprovalPolicy
|
|
98
|
+
from chrys.service.approval.turn_context import TurnContextHolder
|
|
99
|
+
from chrys.service.context.compaction.last_words_state import DropRoundBreakerState
|
|
100
|
+
from chrys.service.context.compaction.spill import COMPACTIONS_DIR_NAME, sub_agent_dropped_turn_relative_path
|
|
101
|
+
from chrys.service.context.manager import ContextManager
|
|
102
|
+
from chrys.service.context.middleware.usage import UsageTrackingMiddleware
|
|
103
|
+
from chrys.service.hooks.events import HookEvent
|
|
104
|
+
from chrys.service.llm.clients import create_client, effective_model_base_url
|
|
105
|
+
from chrys.service.llm.route_sessions import derive_llm_route_session_id, llm_parent_session_id, llm_route_session_id
|
|
106
|
+
from chrys.service.mcp.adapter import MCPAdapter
|
|
107
|
+
from chrys.service.mcp.thinking_warning import warn_if_tool_loading_unbinds_thinking
|
|
108
|
+
from chrys.service.profiles.agents.schema import DEFAULT_SUB_AGENT_CONCURRENCY
|
|
109
|
+
from chrys.service.profiles.models.options import effective_chat_options, uses_responses_compact_continuation
|
|
110
|
+
from chrys.service.profiles.models.schema import API_STYLE_RESPONSES
|
|
111
|
+
from chrys.service.session.message_metadata import TOOL_RESULT_METADATA_KEY
|
|
112
|
+
from chrys.service.session.sub_agent_logs import (
|
|
113
|
+
SubAgentLogStats,
|
|
114
|
+
SubAgentModelProvenance,
|
|
115
|
+
SubAgentSessionLogWriter,
|
|
116
|
+
pending_dir,
|
|
117
|
+
sanitize_base_url_origin,
|
|
118
|
+
sessions_dir,
|
|
119
|
+
)
|
|
120
|
+
from chrys.service.state.serializers import serialized_message_payload
|
|
121
|
+
from chrys.service.tools.builtins.web.build import assemble_web_tools
|
|
122
|
+
from chrys.service.tools.names import chrys_reserved_tool_names
|
|
123
|
+
from chrys.service.tools.registry import ToolRegistry
|
|
124
|
+
from chrys.service.tools.result_metadata import tool_error, tool_result_metadata
|
|
125
|
+
from chrys.service.trajectory.sub_agents import SubAgentStatus as TrajectorySubAgentStatus
|
|
126
|
+
from chrys.service.trajectory.sub_agents import SubAgentTrace
|
|
127
|
+
from chrys.service.vision import filter_image_tools
|
|
128
|
+
|
|
129
|
+
if TYPE_CHECKING:
|
|
130
|
+
from collections.abc import Awaitable, Callable, Coroutine, Mapping
|
|
131
|
+
|
|
132
|
+
from chrys.foundation.config.settings import Settings
|
|
133
|
+
from chrys.foundation.events.bus import EventBus
|
|
134
|
+
from chrys.foundation.models.session_env import SessionEnvironment
|
|
135
|
+
from chrys.service.approval.judge import ApprovalJudge
|
|
136
|
+
from chrys.service.context.compaction import PreCompactInfo
|
|
137
|
+
from chrys.service.context.compaction.last_words import CompactionStatus
|
|
138
|
+
from chrys.service.context.compaction.spill import SpillQuota
|
|
139
|
+
from chrys.service.hooks.manager import HookManager
|
|
140
|
+
from chrys.service.mcp.cache import MCPConnectionCache
|
|
141
|
+
from chrys.service.mutations.coordination import MutationCoordinator
|
|
142
|
+
from chrys.service.mutations.tracker import MutationTracker
|
|
143
|
+
from chrys.service.profiles.agents.schema import AgentProfile, ApprovalConfig, SubAgentRef
|
|
144
|
+
from chrys.service.profiles.models.registry import ModelProfileRegistry
|
|
145
|
+
from chrys.service.profiles.models.schema import ModelProfile
|
|
146
|
+
from chrys.service.skills.provider import ChrysSkillsProvider
|
|
147
|
+
|
|
148
|
+
from .shell import SubAgentToolShell
|
|
149
|
+
|
|
150
|
+
logger = logging.getLogger(__name__)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# Item cap for the cross-attempt ACP audit-update trail — matches the
|
|
154
|
+
# write-time envelope ring (``normalize_acp_state``'s ``_ACP_RING_ITEMS``),
|
|
155
|
+
# which also applies the authoritative byte bound on every write.
|
|
156
|
+
_ACP_AUDIT_TRAIL_ITEMS = 500
|
|
157
|
+
# Mirrors the write-time attempt ring in ``normalize_acp_state``
|
|
158
|
+
# (``_ACP_ATTEMPT_ITEMS``): manual retries are unlimited, so the in-memory
|
|
159
|
+
# attempt history must be bounded the same newest-wins way.
|
|
160
|
+
_ACP_AUDIT_ATTEMPT_ITEMS = 50
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _execution_note(per_agent_max: int, max_total: int) -> str:
|
|
164
|
+
"""Describe execution and concurrency limits for either sub-agent backend."""
|
|
165
|
+
return (
|
|
166
|
+
f"\n(Concurrency limits: max {per_agent_max} concurrent calls to this tool, "
|
|
167
|
+
f"max {max_total} concurrent sub-agent calls total. "
|
|
168
|
+
"Exceeding either limit returns an error.)\n"
|
|
169
|
+
"This call waits for the sub-agent's result. Calls in the same response are dispatched "
|
|
170
|
+
"together with no guaranteed execution order. The calling agent resumes only after the entire "
|
|
171
|
+
"batch returns. Calls that depend on this result or could conflict through shared state must go "
|
|
172
|
+
"in a later response. No asynchronous task handle is provided."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _close_sub_agent_trace(
|
|
177
|
+
trace: SubAgentTrace | None,
|
|
178
|
+
*,
|
|
179
|
+
hook_status: str,
|
|
180
|
+
failure_code: str | None,
|
|
181
|
+
controller: SubAgentToolShell | None = None,
|
|
182
|
+
failure_reason: SubAgentFailureReason | None = None,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Close the boundary from the invocation's terminal status (queued, never awaited: cleanup path).
|
|
185
|
+
|
|
186
|
+
``hook_status`` is read off the result text, so a child that reports its
|
|
187
|
+
failure in prose records as a success, and one whose answer merely opens
|
|
188
|
+
like an error records as a failure. The controller knows which it was;
|
|
189
|
+
where it has run, its verdict decides — with the structured result
|
|
190
|
+
metadata deciding what its terminal status leaves open.
|
|
191
|
+
"""
|
|
192
|
+
if trace is None:
|
|
193
|
+
return
|
|
194
|
+
if hook_status == "cancelled":
|
|
195
|
+
trace.finished_soon(status=TrajectorySubAgentStatus.CANCELLED)
|
|
196
|
+
return
|
|
197
|
+
succeeded = hook_status == "ok"
|
|
198
|
+
reason_code = failure_code
|
|
199
|
+
if controller is not None:
|
|
200
|
+
if controller.status == SubAgentStatus.COMPLETED:
|
|
201
|
+
# COMPLETED says the invocation reached its end, not that the work
|
|
202
|
+
# succeeded: a controller that gives up (spawn, auth, refusal)
|
|
203
|
+
# still completes, and records the failure structurally on its way
|
|
204
|
+
# out. That verdict decides where it exists; the prose never does.
|
|
205
|
+
succeeded = tool_result_metadata_failure_state(tool_result_metadata.get(None)) is not True
|
|
206
|
+
elif controller.status in (SubAgentStatus.ABORTED, SubAgentStatus.CASCADE_ABORTED):
|
|
207
|
+
succeeded = False
|
|
208
|
+
reason_code = failure_code or controller.status.value
|
|
209
|
+
if failure_reason is not None:
|
|
210
|
+
reason_code = failure_reason.value
|
|
211
|
+
if succeeded:
|
|
212
|
+
trace.finished_soon(status=TrajectorySubAgentStatus.OK)
|
|
213
|
+
return
|
|
214
|
+
trace.finished_soon(
|
|
215
|
+
status=TrajectorySubAgentStatus.SETUP_FAILED
|
|
216
|
+
if failure_code == "setup_failed"
|
|
217
|
+
else TrajectorySubAgentStatus.FAILED,
|
|
218
|
+
failure_reason_code=reason_code,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _parent_result_replay_metadata(metadata: SubAgentParentResultMetadata) -> dict[str, Any]:
|
|
223
|
+
result_metadata: dict[str, Any] = {}
|
|
224
|
+
if metadata.sub_agent_invocation_id:
|
|
225
|
+
result_metadata["sub_agent_invocation_id"] = metadata.sub_agent_invocation_id
|
|
226
|
+
if metadata.sub_agent_log_file:
|
|
227
|
+
result_metadata["sub_agent_log_file"] = metadata.sub_agent_log_file
|
|
228
|
+
if metadata.sub_agent_audit_complete:
|
|
229
|
+
result_metadata["sub_agent_audit_complete"] = True
|
|
230
|
+
return result_metadata
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _commit_interrupted_parent_result(metadata: SubAgentParentResultMetadata | None) -> None:
|
|
234
|
+
if metadata is None or metadata.commit_interrupted_result is None or metadata.interrupted_result_committed:
|
|
235
|
+
return
|
|
236
|
+
metadata.commit_interrupted_result(_parent_result_replay_metadata(metadata))
|
|
237
|
+
metadata.interrupted_result_committed = True
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _upgrade_interrupted_parent_result(metadata: SubAgentParentResultMetadata | None) -> None:
|
|
241
|
+
"""Merge late audit references into an already committed interrupted result."""
|
|
242
|
+
if metadata is None or metadata.commit_interrupted_result is None or not metadata.interrupted_result_committed:
|
|
243
|
+
return
|
|
244
|
+
metadata.commit_interrupted_result(_parent_result_replay_metadata(metadata))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def _finish_cancellation_cleanup(awaitable: Coroutine[Any, Any, None]) -> None:
|
|
248
|
+
task = asyncio.create_task(awaitable)
|
|
249
|
+
while not task.done():
|
|
250
|
+
try:
|
|
251
|
+
await asyncio.shield(task)
|
|
252
|
+
except asyncio.CancelledError:
|
|
253
|
+
continue
|
|
254
|
+
try:
|
|
255
|
+
task.result()
|
|
256
|
+
except Exception:
|
|
257
|
+
logger.debug("sub-agent cancellation finalization failed", exc_info=True)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _acp_stderr_path(
|
|
261
|
+
parent_session_dir: Path | None,
|
|
262
|
+
writer: SubAgentSessionLogWriter | None,
|
|
263
|
+
invocation_id: str,
|
|
264
|
+
) -> Path:
|
|
265
|
+
if parent_session_dir is not None and writer is not None:
|
|
266
|
+
return sessions_dir(parent_session_dir) / f"{writer.log_file_name}.stderr.log"
|
|
267
|
+
return Path(tempfile.gettempdir()) / f"chrys-acp-{invocation_id}.stderr.log"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class SubAgentTools:
|
|
271
|
+
"""Manages sub-agent lifecycle and exposes them as FunctionTools.
|
|
272
|
+
|
|
273
|
+
Each sub-agent runs with an independent session (``propagate_session=False``).
|
|
274
|
+
Concurrency is enforced at two levels:
|
|
275
|
+
|
|
276
|
+
- ``_total_active``: shared counter across all sub-agents.
|
|
277
|
+
- ``_agent_active[name]``: per-agent counter.
|
|
278
|
+
|
|
279
|
+
Both are checked before execution; exceeding either returns an error
|
|
280
|
+
string to the model.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
def __init__(
|
|
284
|
+
self,
|
|
285
|
+
max_total_concurrency: int = DEFAULT_SUB_AGENT_CONCURRENCY,
|
|
286
|
+
event_bus: EventBus | None = None,
|
|
287
|
+
session_id: str | None = None,
|
|
288
|
+
session_dir: Path | None = None,
|
|
289
|
+
on_sub_agent_usage: Callable[..., None] | None = None,
|
|
290
|
+
on_side_call_usage: Callable[[Mapping[str, Any]], None] | None = None,
|
|
291
|
+
drain_parent_usage_publishes: Callable[[], Awaitable[None]] | None = None,
|
|
292
|
+
parent_approval: ApprovalConfig | None = None,
|
|
293
|
+
mutation_tracker: MutationTracker | None = None,
|
|
294
|
+
mutation_coordinator: MutationCoordinator | None = None,
|
|
295
|
+
approval_mode: ApprovalMode = ApprovalMode.MANUAL,
|
|
296
|
+
approval_judge: ApprovalJudge | None = None,
|
|
297
|
+
workspace_roots: list[str] | None = None,
|
|
298
|
+
approval_log_dir: Path | None = None,
|
|
299
|
+
mcp_cache: MCPConnectionCache | None = None,
|
|
300
|
+
hook_manager: HookManager | None = None,
|
|
301
|
+
workspace_cwd: str = "",
|
|
302
|
+
serialize_implicit_windows: bool = False,
|
|
303
|
+
spill_quota: SpillQuota | None = None,
|
|
304
|
+
ask_user_timeout_seconds: int | None = DEFAULT_ASK_USER_TIMEOUT_SECONDS,
|
|
305
|
+
turn_context: TurnContextHolder | None = None,
|
|
306
|
+
max_transient_retries: int | None = None,
|
|
307
|
+
tool_result_ceiling_tokens: int | None = None,
|
|
308
|
+
allow_user_interaction: bool = True,
|
|
309
|
+
human_failure_decisions: bool = True,
|
|
310
|
+
) -> None:
|
|
311
|
+
self._max_total = max_total_concurrency
|
|
312
|
+
self._bus = event_bus
|
|
313
|
+
self._session_id = session_id
|
|
314
|
+
self._session_dir = session_dir
|
|
315
|
+
self._on_sub_agent_usage = on_sub_agent_usage
|
|
316
|
+
self._on_side_call_usage = on_side_call_usage
|
|
317
|
+
# Awaited before ``_invoke`` returns so the sub-agent's final
|
|
318
|
+
# ``UsageUpdate`` — queued on the parent engine's
|
|
319
|
+
# ``_usage_publish_tail`` via ``_track_usage_publish`` — is delivered
|
|
320
|
+
# before the parent's ``InvocationToolCallResult``. Without this, a slow
|
|
321
|
+
# subscriber can let the parent tool result overtake the trailing
|
|
322
|
+
# sub-agent usage event.
|
|
323
|
+
self._drain_parent_usage_publishes = drain_parent_usage_publishes
|
|
324
|
+
self._parent_approval = parent_approval
|
|
325
|
+
self._mutation_tracker = mutation_tracker
|
|
326
|
+
self._mutation_coordinator = mutation_coordinator
|
|
327
|
+
self._approval_mode = approval_mode
|
|
328
|
+
self._approval_judge = approval_judge
|
|
329
|
+
self._workspace_roots = list(workspace_roots or [])
|
|
330
|
+
self._approval_log_dir = approval_log_dir
|
|
331
|
+
self._mcp_cache = mcp_cache
|
|
332
|
+
self._hook_manager = hook_manager
|
|
333
|
+
self._workspace_cwd = workspace_cwd
|
|
334
|
+
self._serialize_implicit_windows = serialize_implicit_windows
|
|
335
|
+
self._spill_quota = spill_quota
|
|
336
|
+
# Per-tool profile name for hook payloads. Filled in
|
|
337
|
+
# :meth:`register`; reads on missing keys fall back to the bare
|
|
338
|
+
# tool name in :meth:`_make_tool._invoke`.
|
|
339
|
+
self._hook_profile_names: dict[str, str] = {}
|
|
340
|
+
self._total_active = 0
|
|
341
|
+
self._runtime: SessionEnvironment | None = None
|
|
342
|
+
self._ask_user_timeout_seconds = ask_user_timeout_seconds
|
|
343
|
+
self._max_transient_retries = max_transient_retries
|
|
344
|
+
self._tool_result_ceiling_tokens = tool_result_ceiling_tokens
|
|
345
|
+
self._allow_user_interaction = allow_user_interaction
|
|
346
|
+
# Whether a failed child pauses for the user's Retry/Abort on its card; a caller whose
|
|
347
|
+
# surface shows no such card ends the child instead, its error becoming the tool result.
|
|
348
|
+
self._human_failure_decisions = human_failure_decisions
|
|
349
|
+
self._turn_context = turn_context or TurnContextHolder()
|
|
350
|
+
self._agent_active: dict[str, int] = {} # tool_name → active count
|
|
351
|
+
self._agent_max: dict[str, int] = {} # tool_name → per-agent limit
|
|
352
|
+
self._prepared_by_tool: dict[str, PreparedAgent] = {}
|
|
353
|
+
self._agents: dict[str, Agent] = {} # tool_name → Agent
|
|
354
|
+
self._context_managers: dict[str, ContextManager] = {} # tool_name → ContextManager
|
|
355
|
+
self._mcp_adapters: dict[str, MCPAdapter] = {} # tool_name → MCPAdapter
|
|
356
|
+
self._chat_options_by_tool: dict[str, dict[str, Any] | None] = {} # tool_name → agent.run(options=...)
|
|
357
|
+
self._model_provenance_by_tool: dict[str, SubAgentModelProvenance] = {}
|
|
358
|
+
self._approval_policies: dict[str, ApprovalPolicy] = {} # tool_name → ApprovalPolicy
|
|
359
|
+
self._tool_kinds: dict[str, dict[str, str]] = {} # tool_name → {fn_name → kind}
|
|
360
|
+
self._display_names: dict[str, str] = {} # tool_name → human-readable agent name
|
|
361
|
+
self._acp_profiles: dict[str, AgentProfile] = {}
|
|
362
|
+
self._tools: list[FunctionTool] = []
|
|
363
|
+
# Live ApprovalMiddleware instances for in-flight sub-agent runs.
|
|
364
|
+
# Mid-run mode switches must propagate into these so subsequent tool
|
|
365
|
+
# calls within the same run pick up the new mode.
|
|
366
|
+
self._live_approvals: list[ApprovalMiddleware] = []
|
|
367
|
+
# Live controllers keyed by invocation_id. The engine routes
|
|
368
|
+
# InvocationRetryRequested / InvocationAbortRequested events through
|
|
369
|
+
# this registry so the right sub-agent receives the decision.
|
|
370
|
+
self._controllers: dict[str, SubAgentToolShell] = {}
|
|
371
|
+
self._pending_cleanup_paths: set[Path] = set()
|
|
372
|
+
self._intermediate_buffers: dict[str, IntermediateTextBuffer] = {}
|
|
373
|
+
self._invocation_emitters: dict[str, BoundEmitter] = {}
|
|
374
|
+
|
|
375
|
+
def _emitter_for(self, invocation_id: str) -> BoundEmitter:
|
|
376
|
+
"""Resolve a registered callback key; missing keys never borrow identity."""
|
|
377
|
+
try:
|
|
378
|
+
return self._invocation_emitters[invocation_id]
|
|
379
|
+
except KeyError as exc:
|
|
380
|
+
raise ValueError("Cannot route an unbound invocation callback") from exc
|
|
381
|
+
|
|
382
|
+
async def register(
|
|
383
|
+
self,
|
|
384
|
+
ref: SubAgentRef,
|
|
385
|
+
profile: AgentProfile,
|
|
386
|
+
runtime: SessionEnvironment,
|
|
387
|
+
*,
|
|
388
|
+
settings: Settings,
|
|
389
|
+
fallback_profile: ModelProfile,
|
|
390
|
+
model_registry: ModelProfileRegistry | None = None,
|
|
391
|
+
) -> None:
|
|
392
|
+
"""Build a sub-agent from a profile and register it as a tool.
|
|
393
|
+
|
|
394
|
+
``fallback_profile`` is the parent agent's resolved
|
|
395
|
+
``ModelProfile`` — used when this sub-agent's
|
|
396
|
+
``profile.model.profile_id`` is empty or references a deleted
|
|
397
|
+
profile. Sub-agents with their own model binding use that profile;
|
|
398
|
+
unbound sub-agents inherit the parent's resolved profile. The
|
|
399
|
+
command-line ``--model`` flag changes only the active fallback model,
|
|
400
|
+
so it does not replace sub-agent model bindings.
|
|
401
|
+
"""
|
|
402
|
+
prepared = PreparedAgent()
|
|
403
|
+
|
|
404
|
+
async def _acquire(conversation: Conversation) -> None:
|
|
405
|
+
from chrys.service.profiles.models.resolver import resolve_for_agent
|
|
406
|
+
|
|
407
|
+
sub_active_profile = resolve_for_agent(model_registry, settings, profile, fallback=fallback_profile)
|
|
408
|
+
|
|
409
|
+
self._runtime = runtime
|
|
410
|
+
self._ask_user_timeout_seconds = settings.ask_user_timeout_seconds
|
|
411
|
+
runtime_cwd = runtime.cwd if isinstance(runtime.cwd, str) else None
|
|
412
|
+
if not self._workspace_cwd and runtime_cwd is not None:
|
|
413
|
+
self._workspace_cwd = runtime_cwd
|
|
414
|
+
chat_options = effective_chat_options(sub_active_profile)
|
|
415
|
+
capture_service_loop_messages = uses_responses_compact_continuation(sub_active_profile, chat_options)
|
|
416
|
+
tool_name = ref.tool_name or profile.name
|
|
417
|
+
sub_agent_session_id = derive_llm_route_session_id(
|
|
418
|
+
self._session_id,
|
|
419
|
+
route_kind="sub-agent",
|
|
420
|
+
route_parts=(tool_name, profile.name),
|
|
421
|
+
model_profile=sub_active_profile,
|
|
422
|
+
)
|
|
423
|
+
last_words_session_id = derive_llm_route_session_id(
|
|
424
|
+
self._session_id,
|
|
425
|
+
route_kind="last-words",
|
|
426
|
+
route_parts=(tool_name, profile.name),
|
|
427
|
+
model_profile=sub_active_profile,
|
|
428
|
+
)
|
|
429
|
+
agent_display_name = profile.display_name or profile.name
|
|
430
|
+
|
|
431
|
+
async def _on_intermediate_async(text: str) -> None:
|
|
432
|
+
invocation_id = invocation_routing_key.get()
|
|
433
|
+
buffer = self._intermediate_buffers.get(invocation_id)
|
|
434
|
+
if buffer is None:
|
|
435
|
+
return
|
|
436
|
+
emitter = self._emitter_for(invocation_id)
|
|
437
|
+
buffer.new_batch()
|
|
438
|
+
if self._bus is not None:
|
|
439
|
+
await emitter.publish(
|
|
440
|
+
InvocationMessage(
|
|
441
|
+
is_final=False,
|
|
442
|
+
is_intermediate=True,
|
|
443
|
+
origin=emitter.origin,
|
|
444
|
+
agent_name=agent_display_name,
|
|
445
|
+
text=text,
|
|
446
|
+
session_id=self._session_id,
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def _on_intermediate_sync(text: str) -> None:
|
|
451
|
+
invocation_id = invocation_routing_key.get()
|
|
452
|
+
buffer = self._intermediate_buffers.get(invocation_id)
|
|
453
|
+
if buffer is not None:
|
|
454
|
+
buffer.store(text)
|
|
455
|
+
|
|
456
|
+
client = await create_client(
|
|
457
|
+
sub_active_profile,
|
|
458
|
+
on_intermediate_text_async=_on_intermediate_async,
|
|
459
|
+
on_intermediate_text_sync=_on_intermediate_sync,
|
|
460
|
+
session_id=sub_agent_session_id,
|
|
461
|
+
parent_session_id=self._session_id,
|
|
462
|
+
use_route_session_context=True,
|
|
463
|
+
session_dir=self._session_dir,
|
|
464
|
+
tool_result_ceiling_tokens=self._tool_result_ceiling_tokens,
|
|
465
|
+
)
|
|
466
|
+
await prepared.own_or_release(client.aclose)
|
|
467
|
+
model_provenance = SubAgentModelProvenance(
|
|
468
|
+
provider=sub_active_profile.provider,
|
|
469
|
+
api_style=sub_active_profile.api_style,
|
|
470
|
+
model_id=sub_active_profile.model_id,
|
|
471
|
+
base_url_origin=sanitize_base_url_origin(effective_model_base_url(sub_active_profile)),
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# Build sub-agent's own tools from its profile
|
|
475
|
+
tool_registry = ToolRegistry(vision_enabled=sub_active_profile.vision)
|
|
476
|
+
builtin_categories = profile.tools.builtins or []
|
|
477
|
+
web_tools = assemble_web_tools(
|
|
478
|
+
builtin_categories,
|
|
479
|
+
settings,
|
|
480
|
+
profile.tools.web_search,
|
|
481
|
+
profile.tools.web_fetch,
|
|
482
|
+
chat_options=chat_options,
|
|
483
|
+
agent=profile.name,
|
|
484
|
+
model_profile_id=sub_active_profile.id,
|
|
485
|
+
session_id=self._session_id,
|
|
486
|
+
)
|
|
487
|
+
tool_registry.load_builtins(
|
|
488
|
+
builtin_categories,
|
|
489
|
+
runtime=runtime,
|
|
490
|
+
settings=settings,
|
|
491
|
+
shell_filter_config=profile.tools.shell_filter,
|
|
492
|
+
session_id=self._session_id,
|
|
493
|
+
session_dir=self._session_dir,
|
|
494
|
+
web=web_tools,
|
|
495
|
+
)
|
|
496
|
+
if self._bus is not None:
|
|
497
|
+
for warning in web_tools.warnings:
|
|
498
|
+
await self._bus.publish(warning)
|
|
499
|
+
|
|
500
|
+
tools = filter_image_tools(tool_registry.get_all(), vision_enabled=sub_active_profile.vision)
|
|
501
|
+
if not self._allow_user_interaction:
|
|
502
|
+
tools = [tool for tool in tools if get_tool_kind(tool) != KIND_ASK_USER]
|
|
503
|
+
|
|
504
|
+
# Load MCP tools from the sub-agent's profile
|
|
505
|
+
mcp_adapter: MCPAdapter | None = None
|
|
506
|
+
if profile.tools.mcp:
|
|
507
|
+
reserved_tool_names = chrys_reserved_tool_names()
|
|
508
|
+
reserved_tool_names.update(tool.name for tool in tools)
|
|
509
|
+
# A provider-hosted web tool owns its name too, so a clashing MCP tool fails
|
|
510
|
+
# the build with the guidance to rename or exclude it.
|
|
511
|
+
reserved_tool_names.update(web_tools.hosted)
|
|
512
|
+
mcp_adapter = MCPAdapter(
|
|
513
|
+
cache=self._mcp_cache,
|
|
514
|
+
stdio_cwd=runtime_cwd,
|
|
515
|
+
session_dir=self._session_dir,
|
|
516
|
+
reserved_tool_names=reserved_tool_names,
|
|
517
|
+
)
|
|
518
|
+
prepared.own(mcp_adapter.disconnect_all)
|
|
519
|
+
mcp_tools = await mcp_adapter.connect_all(profile.tools.mcp)
|
|
520
|
+
self._mcp_adapters[tool_name] = mcp_adapter
|
|
521
|
+
tools.extend(mcp_tools)
|
|
522
|
+
warn_if_tool_loading_unbinds_thinking(
|
|
523
|
+
profile, sub_active_profile, chat_options, mcp_adapter.tool_names_by_server
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# Wire compaction + usage tracking for sub-agents. Compression
|
|
527
|
+
# tools (compress_context, recall_context, list_compressed_contexts)
|
|
528
|
+
# are disabled — sub-agents are single-turn and don't need cross-turn
|
|
529
|
+
# context management.
|
|
530
|
+
async def _on_pre_compact(info: PreCompactInfo) -> None:
|
|
531
|
+
await self._fire_pre_compact_hook(tool_name, info)
|
|
532
|
+
|
|
533
|
+
def _publish_context_pressure(
|
|
534
|
+
reason: str,
|
|
535
|
+
breaker: DropRoundBreakerState,
|
|
536
|
+
budget: int,
|
|
537
|
+
) -> Awaitable[None] | None:
|
|
538
|
+
invocation_id = invocation_routing_key.get()
|
|
539
|
+
if self._bus is None:
|
|
540
|
+
return None
|
|
541
|
+
emitter = self._emitter_for(invocation_id)
|
|
542
|
+
return emitter.publish(
|
|
543
|
+
InvocationContextPressure(
|
|
544
|
+
origin=emitter.origin,
|
|
545
|
+
reason=reason,
|
|
546
|
+
attempts=breaker.attempts,
|
|
547
|
+
side_call_tokens=breaker.side_call_tokens,
|
|
548
|
+
side_call_token_budget=budget,
|
|
549
|
+
source="sub_agent",
|
|
550
|
+
session_id=self._session_id,
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
sub_log_dir: Path | None = None
|
|
555
|
+
if self._session_dir:
|
|
556
|
+
sub_log_dir = self._session_dir / "compactions" / "debug" / "sub_agents" / safe_fs_name(tool_name)
|
|
557
|
+
|
|
558
|
+
context_inputs = ContextInputs(
|
|
559
|
+
profile=sub_active_profile,
|
|
560
|
+
compaction_config=profile.compaction,
|
|
561
|
+
on_pre_compact=_on_pre_compact,
|
|
562
|
+
on_context_pressure=_publish_context_pressure,
|
|
563
|
+
spill_quota=self._spill_quota,
|
|
564
|
+
spill_record_dir=sub_agent_dropped_turn_relative_path(tool_name, "registered", 0).parent,
|
|
565
|
+
debug_log_dir=sub_log_dir,
|
|
566
|
+
include_context_management=False,
|
|
567
|
+
parent_session_id=self._session_id,
|
|
568
|
+
session_dir=self._session_dir,
|
|
569
|
+
skip_local_history_for_service_context=(
|
|
570
|
+
sub_active_profile.provider == "openai" and sub_active_profile.api_style == API_STYLE_RESPONSES
|
|
571
|
+
),
|
|
572
|
+
service_context_default_options=chat_options,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Load skills from the sub-agent's profile
|
|
576
|
+
shared_providers = []
|
|
577
|
+
progressive_resume_provider = mcp_adapter.create_resume_provider() if mcp_adapter is not None else None
|
|
578
|
+
from chrys.service.skills.adapter import create_skills_provider
|
|
579
|
+
|
|
580
|
+
skills_provider, skill_warnings = await create_skills_provider(
|
|
581
|
+
profile.skills,
|
|
582
|
+
runtime=runtime,
|
|
583
|
+
session_dir=self._session_dir,
|
|
584
|
+
project_skills_enabled=settings.project_skills_enabled,
|
|
585
|
+
)
|
|
586
|
+
if skills_provider is not None:
|
|
587
|
+
shared_providers.append(skills_provider)
|
|
588
|
+
if progressive_resume_provider is not None:
|
|
589
|
+
shared_providers.append(progressive_resume_provider)
|
|
590
|
+
if self._bus is not None:
|
|
591
|
+
for warn in skill_warnings:
|
|
592
|
+
await self._bus.publish(Warning(code=warn.code, message=warn.message))
|
|
593
|
+
|
|
594
|
+
# Sub-agents get their own SystemReminderMiddleware for runtime
|
|
595
|
+
# context injection (no usage/switch — single-turn agents).
|
|
596
|
+
shell_tool_enabled = any(get_tool_kind(t) == KIND_SHELL for t in tools)
|
|
597
|
+
reminder_inputs = ReminderInputs(
|
|
598
|
+
runtime=runtime,
|
|
599
|
+
shell_tool_enabled=shell_tool_enabled,
|
|
600
|
+
session_root=self._session_dir,
|
|
601
|
+
file_read_available=any(get_tool_kind(tool) == KIND_FILESYSTEM_READ for tool in tools),
|
|
602
|
+
spill_quota=self._spill_quota,
|
|
603
|
+
catalog_pointer_enabled=False,
|
|
604
|
+
skill_catalog_provider=skills_provider.render_catalog_reminder if skills_provider else None,
|
|
605
|
+
mcp_instructions_provider=mcp_adapter.render_instructions_reminder if mcp_adapter is not None else None,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# Wire Phase 4 LAST_WORDS for the sub-agent. Sub-agents run long
|
|
609
|
+
# tool loops (e.g. Explore searching many files) that absolutely can
|
|
610
|
+
# trigger Phase 4. Without this wiring the compaction strategy's
|
|
611
|
+
# `have_note` guard is always False and Phase 4 silently no-ops —
|
|
612
|
+
# leaving the sub-agent to blow past the API context limit.
|
|
613
|
+
#
|
|
614
|
+
# NOTE: LAST_WORDS generation retries transient failures internally
|
|
615
|
+
# against the same compaction input. If that local retry budget is
|
|
616
|
+
# exhausted, the controller pauses and the user decides (Retry / Abort)
|
|
617
|
+
# without first replaying the whole sub-agent attempt.
|
|
618
|
+
async def _publish_last_words_retry(info: RetryAttemptInfo, *, _tool_name=tool_name) -> None:
|
|
619
|
+
invocation_id = invocation_routing_key.get()
|
|
620
|
+
if self._bus is None or not invocation_id:
|
|
621
|
+
return
|
|
622
|
+
emitter = self._emitter_for(invocation_id)
|
|
623
|
+
await emitter.publish(
|
|
624
|
+
InvocationRetryAttempt(
|
|
625
|
+
scope="compaction",
|
|
626
|
+
origin=emitter.origin,
|
|
627
|
+
agent_name=self._display_names.get(_tool_name, _tool_name),
|
|
628
|
+
message=f"LAST_WORDS compaction: {info.reason}",
|
|
629
|
+
attempt=info.attempt,
|
|
630
|
+
max_attempts=info.max_attempts,
|
|
631
|
+
delay_seconds=int(info.delay_seconds),
|
|
632
|
+
session_id=self._session_id,
|
|
633
|
+
)
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
async def _publish_compaction_status(status: CompactionStatus, *, _tool_name=tool_name) -> None:
|
|
637
|
+
invocation_id = invocation_routing_key.get()
|
|
638
|
+
if self._bus is None or not invocation_id:
|
|
639
|
+
return
|
|
640
|
+
await self._publish_sub_agent_compaction_status(status, _tool_name, self._emitter_for(invocation_id))
|
|
641
|
+
|
|
642
|
+
last_words_inputs = LastWordsInputs(
|
|
643
|
+
profile=sub_active_profile,
|
|
644
|
+
template=profile.compaction.last_words_template,
|
|
645
|
+
max_output_tokens=profile.compaction.last_words_max_output_tokens,
|
|
646
|
+
log_dir=sub_log_dir,
|
|
647
|
+
session_id=last_words_session_id,
|
|
648
|
+
parent_session_id=self._session_id,
|
|
649
|
+
session_dir=self._session_dir,
|
|
650
|
+
max_transient_retries=self._max_transient_retries,
|
|
651
|
+
publish_retry=_publish_last_words_retry,
|
|
652
|
+
publish_status=_publish_compaction_status,
|
|
653
|
+
report_usage=self._on_side_call_usage,
|
|
654
|
+
)
|
|
655
|
+
registered = create_runtime(
|
|
656
|
+
conversation,
|
|
657
|
+
SharedRuntime(
|
|
658
|
+
client=client,
|
|
659
|
+
tools=tools,
|
|
660
|
+
providers=shared_providers,
|
|
661
|
+
name=profile.display_name or profile.name,
|
|
662
|
+
instructions=profile.instructions,
|
|
663
|
+
vision=sub_active_profile.vision,
|
|
664
|
+
),
|
|
665
|
+
ChildRecipe(
|
|
666
|
+
context=context_inputs, reminder=reminder_inputs, last_words=last_words_inputs, registration=True
|
|
667
|
+
),
|
|
668
|
+
)
|
|
669
|
+
ctx = registered.context
|
|
670
|
+
agent = registered.agent
|
|
671
|
+
await agent.__aenter__()
|
|
672
|
+
await prepared.own_or_release(lambda: agent.__aexit__(None, None, None))
|
|
673
|
+
|
|
674
|
+
per_agent_max = ref.max_concurrency
|
|
675
|
+
description = (
|
|
676
|
+
ref.tool_description
|
|
677
|
+
or profile.description
|
|
678
|
+
or f"Delegate task to {profile.display_name or profile.name}"
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
full_description = description + _execution_note(per_agent_max, self._max_total)
|
|
682
|
+
|
|
683
|
+
self._agents[tool_name] = agent
|
|
684
|
+
self._context_managers[tool_name] = ctx
|
|
685
|
+
self._chat_options_by_tool[tool_name] = chat_options
|
|
686
|
+
self._model_provenance_by_tool[tool_name] = model_provenance
|
|
687
|
+
# Use the parent's approval config so sub-agents inherit the same
|
|
688
|
+
# approval rules (e.g. shell: require) as the main agent.
|
|
689
|
+
approval_config = self._parent_approval if self._parent_approval is not None else profile.approval
|
|
690
|
+
self._approval_policies[tool_name] = ApprovalPolicy(approval_config, tools=tools)
|
|
691
|
+
self._tool_kinds[tool_name] = {t.name: k for t in tools if (k := get_tool_kind(t))}
|
|
692
|
+
self._display_names[tool_name] = profile.display_name or profile.name
|
|
693
|
+
self._hook_profile_names[tool_name] = profile.name
|
|
694
|
+
self._agent_active[tool_name] = 0
|
|
695
|
+
self._agent_max[tool_name] = per_agent_max
|
|
696
|
+
|
|
697
|
+
def _make_invocation_runtime(
|
|
698
|
+
owner: Conversation,
|
|
699
|
+
invocation_id: str,
|
|
700
|
+
) -> KernelRuntime:
|
|
701
|
+
"""Create per-invocation runtime state for registered sub-agents.
|
|
702
|
+
|
|
703
|
+
``Agent`` instances are cheap wrappers around the shared client and
|
|
704
|
+
tools. Building a fresh context manager/reminder per invocation
|
|
705
|
+
keeps same-tool concurrent runs from sharing compaction calibration,
|
|
706
|
+
LAST_WORDS, or history-provider state.
|
|
707
|
+
"""
|
|
708
|
+
emitter = self._emitter_for(invocation_id)
|
|
709
|
+
invocation_last_words_session_id = derive_llm_route_session_id(
|
|
710
|
+
self._session_id,
|
|
711
|
+
route_kind="last-words",
|
|
712
|
+
route_parts=(tool_name, profile.name, invocation_id),
|
|
713
|
+
model_profile=sub_active_profile,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
async def _on_pre_compact(info: PreCompactInfo, *, _tool_name: str = tool_name) -> None:
|
|
717
|
+
await self._fire_pre_compact_hook(_tool_name, info)
|
|
718
|
+
|
|
719
|
+
def _publish_invocation_context_pressure(
|
|
720
|
+
reason: str,
|
|
721
|
+
breaker: DropRoundBreakerState,
|
|
722
|
+
budget: int,
|
|
723
|
+
) -> Awaitable[None] | None:
|
|
724
|
+
if self._bus is None:
|
|
725
|
+
return None
|
|
726
|
+
return emitter.publish(
|
|
727
|
+
InvocationContextPressure(
|
|
728
|
+
origin=emitter.origin,
|
|
729
|
+
reason=reason,
|
|
730
|
+
attempts=breaker.attempts,
|
|
731
|
+
side_call_tokens=breaker.side_call_tokens,
|
|
732
|
+
side_call_token_budget=budget,
|
|
733
|
+
source="sub_agent",
|
|
734
|
+
session_id=self._session_id,
|
|
735
|
+
)
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
context_inputs = ContextInputs(
|
|
739
|
+
profile=sub_active_profile,
|
|
740
|
+
compaction_config=profile.compaction,
|
|
741
|
+
on_pre_compact=_on_pre_compact,
|
|
742
|
+
on_context_pressure=_publish_invocation_context_pressure,
|
|
743
|
+
spill_quota=self._spill_quota,
|
|
744
|
+
spill_record_dir=sub_agent_dropped_turn_relative_path(tool_name, invocation_id, 0).parent,
|
|
745
|
+
debug_log_dir=sub_log_dir,
|
|
746
|
+
include_context_management=False,
|
|
747
|
+
parent_session_id=self._session_id,
|
|
748
|
+
session_dir=self._session_dir,
|
|
749
|
+
skip_local_history_for_service_context=(
|
|
750
|
+
sub_active_profile.provider == "openai" and sub_active_profile.api_style == API_STYLE_RESPONSES
|
|
751
|
+
),
|
|
752
|
+
service_context_default_options=chat_options,
|
|
753
|
+
)
|
|
754
|
+
reminder_inputs = ReminderInputs(
|
|
755
|
+
runtime=runtime,
|
|
756
|
+
shell_tool_enabled=shell_tool_enabled,
|
|
757
|
+
session_root=self._session_dir,
|
|
758
|
+
file_read_available=any(get_tool_kind(tool) == KIND_FILESYSTEM_READ for tool in tools),
|
|
759
|
+
spill_quota=self._spill_quota,
|
|
760
|
+
catalog_pointer_enabled=False,
|
|
761
|
+
skill_catalog_provider=skills_provider.render_catalog_reminder if skills_provider else None,
|
|
762
|
+
mcp_instructions_provider=sub_mcp.render_instructions_reminder
|
|
763
|
+
if (sub_mcp := self._mcp_adapters.get(tool_name)) is not None
|
|
764
|
+
else None,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
async def _publish_last_words_retry(
|
|
768
|
+
info: RetryAttemptInfo,
|
|
769
|
+
*,
|
|
770
|
+
_tool_name: str = tool_name,
|
|
771
|
+
_invocation_id: str = invocation_id,
|
|
772
|
+
) -> None:
|
|
773
|
+
if self._bus is None:
|
|
774
|
+
return
|
|
775
|
+
await emitter.publish(
|
|
776
|
+
InvocationRetryAttempt(
|
|
777
|
+
scope="compaction",
|
|
778
|
+
origin=emitter.origin,
|
|
779
|
+
agent_name=self._display_names.get(_tool_name, _tool_name),
|
|
780
|
+
message=f"LAST_WORDS compaction: {info.reason}",
|
|
781
|
+
attempt=info.attempt,
|
|
782
|
+
max_attempts=info.max_attempts,
|
|
783
|
+
delay_seconds=int(info.delay_seconds),
|
|
784
|
+
session_id=self._session_id,
|
|
785
|
+
)
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
async def _publish_compaction_status(
|
|
789
|
+
status: CompactionStatus,
|
|
790
|
+
*,
|
|
791
|
+
_tool_name: str = tool_name,
|
|
792
|
+
_invocation_id: str = invocation_id,
|
|
793
|
+
) -> None:
|
|
794
|
+
if self._bus is None:
|
|
795
|
+
return
|
|
796
|
+
await self._publish_sub_agent_compaction_status(status, _tool_name, emitter)
|
|
797
|
+
|
|
798
|
+
last_words_inputs = LastWordsInputs(
|
|
799
|
+
profile=sub_active_profile,
|
|
800
|
+
template=profile.compaction.last_words_template,
|
|
801
|
+
max_output_tokens=profile.compaction.last_words_max_output_tokens,
|
|
802
|
+
log_dir=sub_log_dir,
|
|
803
|
+
session_id=invocation_last_words_session_id,
|
|
804
|
+
parent_session_id=self._session_id,
|
|
805
|
+
session_dir=self._session_dir,
|
|
806
|
+
max_transient_retries=self._max_transient_retries,
|
|
807
|
+
publish_retry=_publish_last_words_retry,
|
|
808
|
+
publish_status=_publish_compaction_status,
|
|
809
|
+
report_usage=self._on_side_call_usage,
|
|
810
|
+
)
|
|
811
|
+
return create_runtime(
|
|
812
|
+
owner,
|
|
813
|
+
SharedRuntime(
|
|
814
|
+
client=client,
|
|
815
|
+
tools=tools,
|
|
816
|
+
providers=shared_providers,
|
|
817
|
+
name=profile.display_name or profile.name,
|
|
818
|
+
instructions=profile.instructions,
|
|
819
|
+
vision=sub_active_profile.vision,
|
|
820
|
+
),
|
|
821
|
+
ChildRecipe(
|
|
822
|
+
context=context_inputs,
|
|
823
|
+
reminder=reminder_inputs,
|
|
824
|
+
last_words=last_words_inputs,
|
|
825
|
+
registration=False,
|
|
826
|
+
),
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
web_tools.check_names(tools)
|
|
830
|
+
|
|
831
|
+
fn_tool = self._make_tool(
|
|
832
|
+
tool_name,
|
|
833
|
+
full_description,
|
|
834
|
+
prepared=prepared,
|
|
835
|
+
runtime_factory=_make_invocation_runtime,
|
|
836
|
+
begin_web_pass=web_tools.begin_pass,
|
|
837
|
+
stream=sub_active_profile.stream,
|
|
838
|
+
stream_attempt_timeout=sub_active_profile.http_read_timeout,
|
|
839
|
+
skills_provider=skills_provider,
|
|
840
|
+
capture_service_loop_messages=capture_service_loop_messages,
|
|
841
|
+
sub_agent_session_id=sub_agent_session_id,
|
|
842
|
+
profile=profile,
|
|
843
|
+
active_profile=sub_active_profile,
|
|
844
|
+
model_provenance=model_provenance,
|
|
845
|
+
)
|
|
846
|
+
self._tools.append(fn_tool)
|
|
847
|
+
|
|
848
|
+
logger.debug("Registered sub-agent tool: %s (profile=%s, max=%d)", tool_name, profile.name, per_agent_max)
|
|
849
|
+
|
|
850
|
+
try:
|
|
851
|
+
await prepared.open(_acquire)
|
|
852
|
+
except BaseException:
|
|
853
|
+
await rollback_resources(prepared)
|
|
854
|
+
raise
|
|
855
|
+
self._prepared_by_tool[ref.tool_name or profile.name] = prepared
|
|
856
|
+
|
|
857
|
+
async def _refresh_skill_catalog(self, provider: ChrysSkillsProvider, tool_name: str) -> None:
|
|
858
|
+
"""Refresh a sub-agent's runtime skills and publish non-fatal warnings."""
|
|
859
|
+
try:
|
|
860
|
+
skill_warnings = await provider.refresh_context()
|
|
861
|
+
except Exception:
|
|
862
|
+
logger.exception("Failed to refresh runtime skills for sub-agent '%s'", tool_name)
|
|
863
|
+
return
|
|
864
|
+
if self._bus is None:
|
|
865
|
+
return
|
|
866
|
+
for warn in skill_warnings:
|
|
867
|
+
await self._bus.publish(Warning(code=warn.code, message=warn.message, session_id=self._session_id))
|
|
868
|
+
|
|
869
|
+
async def register_acp(
|
|
870
|
+
self,
|
|
871
|
+
ref: SubAgentRef,
|
|
872
|
+
profile: AgentProfile,
|
|
873
|
+
runtime: SessionEnvironment,
|
|
874
|
+
) -> None:
|
|
875
|
+
"""Register an external ACP profile without constructing kernel machinery."""
|
|
876
|
+
if profile.acp is None:
|
|
877
|
+
raise ValueError("register_acp requires an ACP agent profile")
|
|
878
|
+
self._runtime = runtime
|
|
879
|
+
if not self._workspace_cwd:
|
|
880
|
+
self._workspace_cwd = runtime.cwd
|
|
881
|
+
tool_name = ref.tool_name or profile.name
|
|
882
|
+
per_agent_max = ref.max_concurrency
|
|
883
|
+
description = (
|
|
884
|
+
ref.tool_description or profile.description or f"Delegate task to {profile.display_name or profile.name}"
|
|
885
|
+
)
|
|
886
|
+
description += _execution_note(per_agent_max, self._max_total)
|
|
887
|
+
self._acp_profiles[tool_name] = profile
|
|
888
|
+
self._display_names[tool_name] = profile.display_name or profile.name
|
|
889
|
+
self._hook_profile_names[tool_name] = profile.name
|
|
890
|
+
self._agent_active[tool_name] = 0
|
|
891
|
+
self._agent_max[tool_name] = per_agent_max
|
|
892
|
+
self._prepared_by_tool[tool_name] = PreparedAgent()
|
|
893
|
+
self._tools.append(self._make_acp_tool(tool_name, description, profile, runtime))
|
|
894
|
+
logger.debug("Registered ACP sub-agent tool: %s (profile=%s, max=%d)", tool_name, profile.name, per_agent_max)
|
|
895
|
+
|
|
896
|
+
def _make_acp_tool(
|
|
897
|
+
self,
|
|
898
|
+
tool_name: str,
|
|
899
|
+
description: str,
|
|
900
|
+
profile: AgentProfile,
|
|
901
|
+
runtime: SessionEnvironment,
|
|
902
|
+
) -> FunctionTool:
|
|
903
|
+
async def _invoke_acp(prompt: str) -> str:
|
|
904
|
+
if self._total_active >= self._max_total:
|
|
905
|
+
return tool_error(
|
|
906
|
+
"sub_agent_concurrency_limit",
|
|
907
|
+
(
|
|
908
|
+
f"total sub-agent concurrency limit reached ({self._total_active}/{self._max_total} active). "
|
|
909
|
+
"Wait for a running sub-agent to complete before starting another."
|
|
910
|
+
),
|
|
911
|
+
retryable=True,
|
|
912
|
+
details={"active": self._total_active, "limit": self._max_total},
|
|
913
|
+
)
|
|
914
|
+
per_max = self._agent_max.get(tool_name, DEFAULT_SUB_AGENT_CONCURRENCY)
|
|
915
|
+
per_active = self._agent_active.get(tool_name, 0)
|
|
916
|
+
if per_active >= per_max:
|
|
917
|
+
return tool_error(
|
|
918
|
+
"sub_agent_concurrency_limit",
|
|
919
|
+
(
|
|
920
|
+
f"concurrency limit for '{tool_name}' reached ({per_active}/{per_max} active). "
|
|
921
|
+
"Wait for a running call to complete before starting another."
|
|
922
|
+
),
|
|
923
|
+
retryable=True,
|
|
924
|
+
details={"tool_name": tool_name, "active": per_active, "limit": per_max},
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
invocation_id = uuid4().hex[:12]
|
|
928
|
+
origin = InvocationOrigin(
|
|
929
|
+
"sub_agent", self._session_id or "", invocation_id, current_invocation_origin.get()
|
|
930
|
+
)
|
|
931
|
+
emitter = BoundEmitter(self._bus, origin)
|
|
932
|
+
self._invocation_emitters[invocation_id] = emitter
|
|
933
|
+
invocation_token = invocation_routing_key.set(invocation_id)
|
|
934
|
+
parent_metadata = sub_agent_parent_result_metadata.get()
|
|
935
|
+
parent_event_call_id = ""
|
|
936
|
+
parent_provider_call_id = ""
|
|
937
|
+
if parent_metadata is not None:
|
|
938
|
+
parent_event_call_id = parent_metadata.parent_event_call_id
|
|
939
|
+
parent_provider_call_id = parent_metadata.parent_provider_call_id
|
|
940
|
+
parent_metadata.sub_agent_invocation_id = invocation_id
|
|
941
|
+
else:
|
|
942
|
+
parent_event_call_id = sub_agent_parent_call_id.get()
|
|
943
|
+
hook_status = "failed"
|
|
944
|
+
hook_summary = ""
|
|
945
|
+
# ``boundary_only`` excludes the external ACP process's own model
|
|
946
|
+
# runs and tools. Parent-recorded approvals, input waits, and
|
|
947
|
+
# connection retries still appear under the child actor.
|
|
948
|
+
sub_agent_trace = SubAgentTrace.open(
|
|
949
|
+
invocation_id=invocation_id, trace_coverage=TraceCoverage.BOUNDARY_ONLY
|
|
950
|
+
)
|
|
951
|
+
sub_agent_failure_code: str | None = None
|
|
952
|
+
hook_started = False
|
|
953
|
+
controller: SubAgentToolShell | None = None
|
|
954
|
+
policy: AcpSubAgentPolicy | None = None
|
|
955
|
+
broker: AcpPermissionBroker | None = None
|
|
956
|
+
log_stats = SubAgentLogStats()
|
|
957
|
+
log_writer: SubAgentSessionLogWriter | None = None
|
|
958
|
+
log_finalized = False
|
|
959
|
+
attempts: list[dict[str, Any]] = []
|
|
960
|
+
# Bounded audit-update trail instead of retaining every attempt's
|
|
961
|
+
# translator: each translator holds up to ~5 MiB of assembled content
|
|
962
|
+
# and thousands of call records, and manual retries are unlimited.
|
|
963
|
+
# Only the newest entries survive the write-time envelope ring
|
|
964
|
+
# (``normalize_acp_state``), so folding finished attempts down to
|
|
965
|
+
# their ring-bounded updates loses nothing the audit would keep.
|
|
966
|
+
audit_updates: list[Any] = []
|
|
967
|
+
active_translator: Any = None
|
|
968
|
+
last_agent_info: Any = None
|
|
969
|
+
stderr_path: Path | None = None
|
|
970
|
+
operation = OperationLifetime()
|
|
971
|
+
conversation: Conversation | None = None
|
|
972
|
+
unbind: Unbind | None = None
|
|
973
|
+
controller = SubAgentToolShell(
|
|
974
|
+
origin=origin,
|
|
975
|
+
tool_name=tool_name,
|
|
976
|
+
agent_name=self._display_names[tool_name],
|
|
977
|
+
event_bus=self._bus,
|
|
978
|
+
operation=operation,
|
|
979
|
+
human_failure_decisions=self._human_failure_decisions,
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
def _audit_update_trail() -> list[Any]:
|
|
983
|
+
trail = list(audit_updates)
|
|
984
|
+
if active_translator is not None:
|
|
985
|
+
trail.extend(active_translator.translated_updates)
|
|
986
|
+
# Same item cap as the write-time envelope ring; the byte
|
|
987
|
+
# bound is applied by ``normalize_acp_state`` on write.
|
|
988
|
+
return trail[-_ACP_AUDIT_TRAIL_ITEMS:]
|
|
989
|
+
|
|
990
|
+
def _record_terminal_audit(written: bool) -> None:
|
|
991
|
+
nonlocal log_finalized
|
|
992
|
+
log_finalized = written
|
|
993
|
+
if written and parent_metadata is not None and log_writer is not None:
|
|
994
|
+
parent_metadata.sub_agent_log_file = log_writer.log_file_name
|
|
995
|
+
parent_metadata.sub_agent_audit_complete = True
|
|
996
|
+
_upgrade_interrupted_parent_result(parent_metadata)
|
|
997
|
+
|
|
998
|
+
try:
|
|
999
|
+
# Inside the block whose ``finally`` releases them: the started
|
|
1000
|
+
# record awaits a write ack, and an interrupt there must not
|
|
1001
|
+
# leave an unfinished boundary or a counter nobody gives back.
|
|
1002
|
+
self._total_active += 1
|
|
1003
|
+
self._agent_active[tool_name] = per_active + 1
|
|
1004
|
+
|
|
1005
|
+
async def _open_acp(owner: Conversation) -> Conversation:
|
|
1006
|
+
return owner
|
|
1007
|
+
|
|
1008
|
+
conversation = await self._prepared_by_tool[tool_name].open(_open_acp)
|
|
1009
|
+
unbind = conversation.bind_operation(controller)
|
|
1010
|
+
if sub_agent_trace is not None:
|
|
1011
|
+
await sub_agent_trace.started(tool_name=tool_name, agent_profile=profile.name)
|
|
1012
|
+
config = profile.acp
|
|
1013
|
+
if config is None:
|
|
1014
|
+
raise RuntimeError("An ACP sub-agent requires ACP configuration.")
|
|
1015
|
+
if self._session_dir is not None:
|
|
1016
|
+
log_writer = SubAgentSessionLogWriter(
|
|
1017
|
+
parent_session_dir=self._session_dir,
|
|
1018
|
+
parent_session_id=self._session_id,
|
|
1019
|
+
invocation_id=invocation_id,
|
|
1020
|
+
tool_name=tool_name,
|
|
1021
|
+
agent_profile=profile.name,
|
|
1022
|
+
agent_display_name=self._display_names[tool_name],
|
|
1023
|
+
prompt=prompt,
|
|
1024
|
+
parent_provider_call_id=parent_provider_call_id,
|
|
1025
|
+
parent_event_call_id=parent_event_call_id,
|
|
1026
|
+
primary_cwd=self._workspace_cwd,
|
|
1027
|
+
working_dirs=list(
|
|
1028
|
+
self._workspace_roots or ([self._workspace_cwd] if self._workspace_cwd else [])
|
|
1029
|
+
),
|
|
1030
|
+
stats=log_stats,
|
|
1031
|
+
runner="acp",
|
|
1032
|
+
acp_command=config.command,
|
|
1033
|
+
acp_args=config.args,
|
|
1034
|
+
)
|
|
1035
|
+
stderr_path = _acp_stderr_path(self._session_dir, log_writer, invocation_id)
|
|
1036
|
+
try:
|
|
1037
|
+
initial_spec = resolve_acp_spec(
|
|
1038
|
+
config, runtime, stderr_path, workspace_roots=self._workspace_roots
|
|
1039
|
+
)
|
|
1040
|
+
except AcpConfigError:
|
|
1041
|
+
pass
|
|
1042
|
+
else:
|
|
1043
|
+
log_writer.set_acp_launch(initial_spec.command, list(initial_spec.args))
|
|
1044
|
+
initial_log_written = await log_writer.write(
|
|
1045
|
+
status="running",
|
|
1046
|
+
acp_state={
|
|
1047
|
+
"agent_info": None,
|
|
1048
|
+
"attempts": [],
|
|
1049
|
+
"stop_reason": "",
|
|
1050
|
+
"translated_updates": [],
|
|
1051
|
+
},
|
|
1052
|
+
)
|
|
1053
|
+
if initial_log_written and parent_metadata is not None:
|
|
1054
|
+
parent_metadata.sub_agent_log_file = log_writer.log_file_name
|
|
1055
|
+
if stderr_path is None:
|
|
1056
|
+
stderr_path = _acp_stderr_path(self._session_dir, log_writer, invocation_id)
|
|
1057
|
+
if self._bus is not None:
|
|
1058
|
+
await emitter.publish(
|
|
1059
|
+
InvocationStarted(
|
|
1060
|
+
origin=emitter.origin,
|
|
1061
|
+
agent_name=self._display_names[tool_name],
|
|
1062
|
+
tool_name=tool_name,
|
|
1063
|
+
parent_call_id=parent_event_call_id,
|
|
1064
|
+
sub_agent_log_file=log_writer.log_file_name if log_writer is not None else "",
|
|
1065
|
+
session_id=self._session_id,
|
|
1066
|
+
)
|
|
1067
|
+
)
|
|
1068
|
+
await self._fire_sub_agent_hook(
|
|
1069
|
+
HookEvent.SUB_AGENT_START,
|
|
1070
|
+
tool_name=tool_name,
|
|
1071
|
+
invocation_id=invocation_id,
|
|
1072
|
+
parent_call_id=parent_event_call_id,
|
|
1073
|
+
parent_provider_call_id=parent_provider_call_id,
|
|
1074
|
+
target_operation_id=sub_agent_trace.operation_id if sub_agent_trace is not None else None,
|
|
1075
|
+
)
|
|
1076
|
+
hook_started = True
|
|
1077
|
+
if self._bus is None:
|
|
1078
|
+
raise RuntimeError("ACP sub-agents require an EventBus")
|
|
1079
|
+
broker = AcpPermissionBroker(
|
|
1080
|
+
event_bus=self._bus,
|
|
1081
|
+
session_id=self._session_id,
|
|
1082
|
+
caller_name=self._display_names[tool_name],
|
|
1083
|
+
mode_getter=lambda: self._approval_mode,
|
|
1084
|
+
turn_context=self._turn_context,
|
|
1085
|
+
workspace_roots=self._workspace_roots,
|
|
1086
|
+
workspace_cwd=self._workspace_cwd,
|
|
1087
|
+
approval_judge=self._approval_judge,
|
|
1088
|
+
ask_user_timeout_seconds=self._ask_user_timeout_seconds,
|
|
1089
|
+
allow_user_interaction=self._allow_user_interaction,
|
|
1090
|
+
trajectory_context=sub_agent_trace.child_context if sub_agent_trace is not None else None,
|
|
1091
|
+
trajectory_boundary_operation_id=(
|
|
1092
|
+
sub_agent_trace.operation_id if sub_agent_trace is not None else None
|
|
1093
|
+
),
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
async def _adopt_translator(translator: Any) -> None:
|
|
1097
|
+
# Fires from the controller BEFORE the attempt's transport
|
|
1098
|
+
# exists: a permission decision recorded during
|
|
1099
|
+
# open_session (ACP allows requests the moment session/new
|
|
1100
|
+
# is announced) must already ride _audit_update_trail()
|
|
1101
|
+
# when a pre-handshake failure writes the pause audit.
|
|
1102
|
+
nonlocal active_translator
|
|
1103
|
+
if active_translator is not None and active_translator is not translator:
|
|
1104
|
+
audit_updates.extend(active_translator.translated_updates)
|
|
1105
|
+
del audit_updates[:-_ACP_AUDIT_TRAIL_ITEMS]
|
|
1106
|
+
active_translator = translator
|
|
1107
|
+
|
|
1108
|
+
async def _record_attempt(attempt: int, handshake: Any, translator: Any) -> None:
|
|
1109
|
+
nonlocal last_agent_info
|
|
1110
|
+
attempts.append({"attempt": attempt, "acp_session_id": handshake.session_id})
|
|
1111
|
+
del attempts[:-_ACP_AUDIT_ATTEMPT_ITEMS]
|
|
1112
|
+
await _adopt_translator(translator)
|
|
1113
|
+
if log_writer is not None:
|
|
1114
|
+
if policy is not None and policy.backend.last_spec is not None:
|
|
1115
|
+
log_writer.set_acp_launch(
|
|
1116
|
+
policy.backend.last_spec.command,
|
|
1117
|
+
list(policy.backend.last_spec.args),
|
|
1118
|
+
)
|
|
1119
|
+
last_agent_info = (
|
|
1120
|
+
handshake.agent_info.model_dump(mode="json", by_alias=True)
|
|
1121
|
+
if handshake.agent_info is not None
|
|
1122
|
+
else None
|
|
1123
|
+
)
|
|
1124
|
+
await log_writer.write(
|
|
1125
|
+
status="running",
|
|
1126
|
+
acp_state={
|
|
1127
|
+
"agent_info": last_agent_info,
|
|
1128
|
+
"attempts": attempts,
|
|
1129
|
+
"stop_reason": "",
|
|
1130
|
+
"translated_updates": _audit_update_trail(),
|
|
1131
|
+
"stderr_log_path": str(stderr_path),
|
|
1132
|
+
},
|
|
1133
|
+
)
|
|
1134
|
+
|
|
1135
|
+
async def _record_pause() -> None:
|
|
1136
|
+
if log_writer is None or not log_writer.active:
|
|
1137
|
+
return
|
|
1138
|
+
ctrl = policy
|
|
1139
|
+
# The pause fires mid-run(): sync spend/tool counters from
|
|
1140
|
+
# the live controller exactly like the terminal writes do —
|
|
1141
|
+
# a crash while paused must not durably record zero
|
|
1142
|
+
# counters for attempts the parent was already charged for.
|
|
1143
|
+
if ctrl is not None:
|
|
1144
|
+
log_stats.tool_call_count = ctrl.backend.completed_tool_calls
|
|
1145
|
+
log_stats.total_tokens = ctrl.backend.latest_context_tokens
|
|
1146
|
+
log_stats.total_usage_tokens = ctrl.backend.total_usage_tokens
|
|
1147
|
+
log_stats.usage_unreported_attempts = ctrl.backend.usage_unreported_attempts
|
|
1148
|
+
if ctrl.backend.last_spec is not None:
|
|
1149
|
+
log_writer.set_acp_launch(ctrl.backend.last_spec.command, list(ctrl.backend.last_spec.args))
|
|
1150
|
+
await log_writer.write(
|
|
1151
|
+
status="paused",
|
|
1152
|
+
failure_reason=SubAgentFailureReason.ACP_TRANSPORT.value,
|
|
1153
|
+
last_error=ctrl.backend.last_error if ctrl is not None else "",
|
|
1154
|
+
acp_state={
|
|
1155
|
+
"agent_info": last_agent_info,
|
|
1156
|
+
"attempts": attempts,
|
|
1157
|
+
"stop_reason": ctrl.backend.stop_reason if ctrl is not None else "",
|
|
1158
|
+
"translated_updates": _audit_update_trail(),
|
|
1159
|
+
"stderr_log_path": str(stderr_path),
|
|
1160
|
+
"usage_unreported_attempts": log_stats.usage_unreported_attempts,
|
|
1161
|
+
},
|
|
1162
|
+
)
|
|
1163
|
+
|
|
1164
|
+
def _commit_parent_interruption() -> None:
|
|
1165
|
+
_commit_interrupted_parent_result(parent_metadata)
|
|
1166
|
+
|
|
1167
|
+
async def _finalize_cancellation() -> None:
|
|
1168
|
+
nonlocal log_finalized
|
|
1169
|
+
ctrl = policy
|
|
1170
|
+
if ctrl is not None:
|
|
1171
|
+
log_stats.tool_call_count = ctrl.backend.completed_tool_calls
|
|
1172
|
+
log_stats.total_tokens = ctrl.backend.latest_context_tokens
|
|
1173
|
+
log_stats.total_usage_tokens = ctrl.backend.total_usage_tokens
|
|
1174
|
+
log_stats.usage_unreported_attempts = ctrl.backend.usage_unreported_attempts
|
|
1175
|
+
if log_writer is None:
|
|
1176
|
+
return
|
|
1177
|
+
if not log_finalized:
|
|
1178
|
+
terminal_written = await log_writer.write(
|
|
1179
|
+
status="cancelled",
|
|
1180
|
+
result="cancelled",
|
|
1181
|
+
last_error="cancelled",
|
|
1182
|
+
ended=True,
|
|
1183
|
+
acp_state={
|
|
1184
|
+
"agent_info": last_agent_info,
|
|
1185
|
+
"attempts": attempts,
|
|
1186
|
+
"stop_reason": ctrl.backend.stop_reason if ctrl is not None else "",
|
|
1187
|
+
"translated_updates": _audit_update_trail(),
|
|
1188
|
+
"stderr_log_path": str(stderr_path),
|
|
1189
|
+
"usage_unreported_attempts": log_stats.usage_unreported_attempts,
|
|
1190
|
+
},
|
|
1191
|
+
)
|
|
1192
|
+
_record_terminal_audit(terminal_written)
|
|
1193
|
+
|
|
1194
|
+
if controller is None:
|
|
1195
|
+
raise RuntimeError("The sub-agent invocation has no controller.")
|
|
1196
|
+
controller.bind_parent_interrupt_commit(_commit_parent_interruption)
|
|
1197
|
+
policy = AcpSubAgentPolicy(
|
|
1198
|
+
shell=controller,
|
|
1199
|
+
prompt=prompt,
|
|
1200
|
+
spec_factory=lambda _attempt: resolve_acp_spec(
|
|
1201
|
+
config, runtime, stderr_path, workspace_roots=self._workspace_roots
|
|
1202
|
+
),
|
|
1203
|
+
broker=broker,
|
|
1204
|
+
result_mode=config.result_mode,
|
|
1205
|
+
usage_callback=self._on_sub_agent_usage,
|
|
1206
|
+
attempt_callback=_record_attempt,
|
|
1207
|
+
translator_callback=_adopt_translator,
|
|
1208
|
+
pause_callback=_record_pause,
|
|
1209
|
+
persist_dir=self._pending_dir(),
|
|
1210
|
+
parent_provider_call_id=parent_provider_call_id,
|
|
1211
|
+
parent_event_call_id=parent_event_call_id,
|
|
1212
|
+
sub_agent_log_file=(
|
|
1213
|
+
log_writer.log_file_name if log_writer is not None and log_writer.active else ""
|
|
1214
|
+
),
|
|
1215
|
+
pending_record_finalizer=self.queue_pending_cleanup,
|
|
1216
|
+
cancellation_finalizer=_finalize_cancellation,
|
|
1217
|
+
trajectory_context=sub_agent_trace.child_context if sub_agent_trace is not None else None,
|
|
1218
|
+
trajectory_boundary_operation_id=(
|
|
1219
|
+
sub_agent_trace.operation_id if sub_agent_trace is not None else None
|
|
1220
|
+
),
|
|
1221
|
+
)
|
|
1222
|
+
controller.attach_policy(policy)
|
|
1223
|
+
self._controllers[invocation_id] = controller
|
|
1224
|
+
result = await controller.run()
|
|
1225
|
+
if parent_metadata is not None and policy.backend.transcript_final_text is not None:
|
|
1226
|
+
parent_metadata.transcript_final_text = policy.backend.transcript_final_text
|
|
1227
|
+
log_stats.tool_call_count = policy.backend.completed_tool_calls
|
|
1228
|
+
log_stats.total_tokens = policy.backend.latest_context_tokens
|
|
1229
|
+
log_stats.total_usage_tokens = policy.backend.total_usage_tokens
|
|
1230
|
+
log_stats.usage_unreported_attempts = policy.backend.usage_unreported_attempts
|
|
1231
|
+
hook_status = "failed" if result.startswith("Error:") else "ok"
|
|
1232
|
+
hook_summary = result[:500]
|
|
1233
|
+
if hook_status == "failed":
|
|
1234
|
+
sub_agent_failure_code = "tool_error"
|
|
1235
|
+
if log_writer is not None:
|
|
1236
|
+
if policy.backend.last_spec is not None:
|
|
1237
|
+
log_writer.set_acp_launch(
|
|
1238
|
+
policy.backend.last_spec.command,
|
|
1239
|
+
list(policy.backend.last_spec.args),
|
|
1240
|
+
)
|
|
1241
|
+
terminal_written = await log_writer.write(
|
|
1242
|
+
status="completed" if hook_status == "ok" else "failed",
|
|
1243
|
+
result=result,
|
|
1244
|
+
ended=True,
|
|
1245
|
+
acp_state={
|
|
1246
|
+
"agent_info": last_agent_info,
|
|
1247
|
+
"attempts": attempts,
|
|
1248
|
+
"successful_attempt": policy.backend.transport_ordinal if hook_status == "ok" else None,
|
|
1249
|
+
"stop_reason": policy.backend.stop_reason,
|
|
1250
|
+
"translated_updates": _audit_update_trail(),
|
|
1251
|
+
"stderr_log_path": str(stderr_path),
|
|
1252
|
+
"usage_unreported_attempts": log_stats.usage_unreported_attempts,
|
|
1253
|
+
},
|
|
1254
|
+
)
|
|
1255
|
+
_record_terminal_audit(terminal_written)
|
|
1256
|
+
return result
|
|
1257
|
+
except asyncio.CancelledError as exc:
|
|
1258
|
+
hook_status = "cancelled"
|
|
1259
|
+
hook_summary = "cancelled"
|
|
1260
|
+
_commit_interrupted_parent_result(parent_metadata)
|
|
1261
|
+
if policy is not None:
|
|
1262
|
+
if controller is None:
|
|
1263
|
+
raise RuntimeError("The sub-agent invocation has no controller.") from exc
|
|
1264
|
+
await _finish_cancellation_cleanup(controller.finalize_cancellation())
|
|
1265
|
+
raise
|
|
1266
|
+
except BaseException as exc:
|
|
1267
|
+
hook_status = "failed"
|
|
1268
|
+
hook_summary = str(exc)[:500]
|
|
1269
|
+
sub_agent_failure_code = "setup_failed" if policy is None else "exception"
|
|
1270
|
+
raise
|
|
1271
|
+
finally:
|
|
1272
|
+
operation.begin_cleanup()
|
|
1273
|
+
cleanup_cancelled = False
|
|
1274
|
+
cleanup_error: Exception | None = None
|
|
1275
|
+
_close_sub_agent_trace(
|
|
1276
|
+
sub_agent_trace,
|
|
1277
|
+
hook_status=hook_status,
|
|
1278
|
+
failure_code=sub_agent_failure_code,
|
|
1279
|
+
controller=controller if policy is not None else None,
|
|
1280
|
+
)
|
|
1281
|
+
if broker is not None:
|
|
1282
|
+
try:
|
|
1283
|
+
if policy is not None:
|
|
1284
|
+
await policy.backend.aclose()
|
|
1285
|
+
else:
|
|
1286
|
+
await broker.close()
|
|
1287
|
+
except asyncio.CancelledError:
|
|
1288
|
+
cleanup_cancelled = True
|
|
1289
|
+
except Exception as exc:
|
|
1290
|
+
cleanup_error = exc
|
|
1291
|
+
if policy is not None:
|
|
1292
|
+
log_stats.tool_call_count = policy.backend.completed_tool_calls
|
|
1293
|
+
log_stats.total_tokens = policy.backend.latest_context_tokens
|
|
1294
|
+
log_stats.total_usage_tokens = policy.backend.total_usage_tokens
|
|
1295
|
+
log_stats.usage_unreported_attempts = policy.backend.usage_unreported_attempts
|
|
1296
|
+
if log_writer is not None and not log_finalized:
|
|
1297
|
+
try:
|
|
1298
|
+
if policy is not None and policy.backend.last_spec is not None:
|
|
1299
|
+
log_writer.set_acp_launch(
|
|
1300
|
+
policy.backend.last_spec.command,
|
|
1301
|
+
list(policy.backend.last_spec.args),
|
|
1302
|
+
)
|
|
1303
|
+
terminal_written = await log_writer.write(
|
|
1304
|
+
status="cancelled" if hook_status == "cancelled" else "failed",
|
|
1305
|
+
result=hook_summary,
|
|
1306
|
+
ended=True,
|
|
1307
|
+
acp_state={
|
|
1308
|
+
"agent_info": last_agent_info,
|
|
1309
|
+
"attempts": attempts,
|
|
1310
|
+
"stop_reason": policy.backend.stop_reason if policy is not None else "",
|
|
1311
|
+
"translated_updates": _audit_update_trail(),
|
|
1312
|
+
"stderr_log_path": str(stderr_path) if stderr_path is not None else "",
|
|
1313
|
+
"usage_unreported_attempts": log_stats.usage_unreported_attempts,
|
|
1314
|
+
},
|
|
1315
|
+
)
|
|
1316
|
+
_record_terminal_audit(terminal_written)
|
|
1317
|
+
except asyncio.CancelledError:
|
|
1318
|
+
cleanup_cancelled = True
|
|
1319
|
+
except Exception as exc:
|
|
1320
|
+
cleanup_error = cleanup_error or exc
|
|
1321
|
+
if self._drain_parent_usage_publishes is not None:
|
|
1322
|
+
try:
|
|
1323
|
+
await self._drain_parent_usage_publishes()
|
|
1324
|
+
except asyncio.CancelledError:
|
|
1325
|
+
cleanup_cancelled = True
|
|
1326
|
+
except Exception as exc:
|
|
1327
|
+
cleanup_error = cleanup_error or exc
|
|
1328
|
+
if hook_started:
|
|
1329
|
+
try:
|
|
1330
|
+
await self._fire_sub_agent_hook(
|
|
1331
|
+
HookEvent.SUB_AGENT_END,
|
|
1332
|
+
tool_name=tool_name,
|
|
1333
|
+
invocation_id=invocation_id,
|
|
1334
|
+
parent_call_id=parent_event_call_id,
|
|
1335
|
+
parent_provider_call_id=parent_provider_call_id,
|
|
1336
|
+
status=hook_status,
|
|
1337
|
+
result_summary=hook_summary,
|
|
1338
|
+
target_operation_id=(sub_agent_trace.operation_id if sub_agent_trace is not None else None),
|
|
1339
|
+
)
|
|
1340
|
+
except asyncio.CancelledError:
|
|
1341
|
+
cleanup_cancelled = True
|
|
1342
|
+
except Exception:
|
|
1343
|
+
logger.exception("Failed to fire ACP sub-agent end hook")
|
|
1344
|
+
self._controllers.pop(invocation_id, None)
|
|
1345
|
+
self._invocation_emitters.pop(invocation_id, None)
|
|
1346
|
+
self._total_active -= 1
|
|
1347
|
+
self._agent_active[tool_name] -= 1
|
|
1348
|
+
invocation_routing_key.reset(invocation_token)
|
|
1349
|
+
if unbind is not None:
|
|
1350
|
+
unbind()
|
|
1351
|
+
operation.finish()
|
|
1352
|
+
if conversation is not None:
|
|
1353
|
+
try:
|
|
1354
|
+
await conversation.aclose()
|
|
1355
|
+
except asyncio.CancelledError:
|
|
1356
|
+
cleanup_cancelled = True
|
|
1357
|
+
if cleanup_cancelled:
|
|
1358
|
+
raise asyncio.CancelledError
|
|
1359
|
+
if cleanup_error is not None:
|
|
1360
|
+
raise cleanup_error
|
|
1361
|
+
|
|
1362
|
+
tool = FunctionTool(func=_invoke_acp, name=tool_name, description=description)
|
|
1363
|
+
set_tool_kind(tool, KIND_SUB_AGENT)
|
|
1364
|
+
return tool
|
|
1365
|
+
|
|
1366
|
+
def _make_tool(
|
|
1367
|
+
self,
|
|
1368
|
+
tool_name: str,
|
|
1369
|
+
description: str,
|
|
1370
|
+
*,
|
|
1371
|
+
prepared: PreparedAgent,
|
|
1372
|
+
runtime_factory: Callable[[Conversation, str], KernelRuntime],
|
|
1373
|
+
stream: bool,
|
|
1374
|
+
stream_attempt_timeout: float,
|
|
1375
|
+
skills_provider: ChrysSkillsProvider | None = None,
|
|
1376
|
+
capture_service_loop_messages: bool = False,
|
|
1377
|
+
profile: AgentProfile | None = None,
|
|
1378
|
+
active_profile: ModelProfile | None = None,
|
|
1379
|
+
sub_agent_session_id: str | None = None,
|
|
1380
|
+
model_provenance: SubAgentModelProvenance | None = None,
|
|
1381
|
+
begin_web_pass: Callable[[], None] | None = None,
|
|
1382
|
+
) -> FunctionTool:
|
|
1383
|
+
"""Create a FunctionTool that invokes the sub-agent with concurrency control."""
|
|
1384
|
+
|
|
1385
|
+
def _route_profile_name() -> str:
|
|
1386
|
+
return profile.name if profile is not None else self._hook_profile_names.get(tool_name, tool_name)
|
|
1387
|
+
|
|
1388
|
+
def _derive_invocation_session_id(invocation_id: str) -> str | None:
|
|
1389
|
+
if active_profile is None:
|
|
1390
|
+
return sub_agent_session_id
|
|
1391
|
+
return derive_llm_route_session_id(
|
|
1392
|
+
self._session_id,
|
|
1393
|
+
route_kind="sub-agent",
|
|
1394
|
+
route_parts=(tool_name, _route_profile_name(), invocation_id),
|
|
1395
|
+
model_profile=active_profile,
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
async def _invoke(prompt: str) -> str:
|
|
1399
|
+
# Global concurrency guard
|
|
1400
|
+
if self._total_active >= self._max_total:
|
|
1401
|
+
return tool_error(
|
|
1402
|
+
"sub_agent_concurrency_limit",
|
|
1403
|
+
(
|
|
1404
|
+
f"total sub-agent concurrency limit reached ({self._total_active}/{self._max_total} active). "
|
|
1405
|
+
"Wait for a running sub-agent to complete before starting another."
|
|
1406
|
+
),
|
|
1407
|
+
retryable=True,
|
|
1408
|
+
details={"active": self._total_active, "limit": self._max_total},
|
|
1409
|
+
)
|
|
1410
|
+
|
|
1411
|
+
# Per-agent concurrency guard
|
|
1412
|
+
per_max = self._agent_max.get(tool_name, DEFAULT_SUB_AGENT_CONCURRENCY)
|
|
1413
|
+
per_active = self._agent_active.get(tool_name, 0)
|
|
1414
|
+
if per_active >= per_max:
|
|
1415
|
+
return tool_error(
|
|
1416
|
+
"sub_agent_concurrency_limit",
|
|
1417
|
+
(
|
|
1418
|
+
f"concurrency limit for '{tool_name}' reached ({per_active}/{per_max} active). "
|
|
1419
|
+
"Wait for a running call to complete before starting another."
|
|
1420
|
+
),
|
|
1421
|
+
retryable=True,
|
|
1422
|
+
details={"tool_name": tool_name, "active": per_active, "limit": per_max},
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
invocation_id = ""
|
|
1426
|
+
invocation_token: Token[str] | None = None
|
|
1427
|
+
route_session_token: Token[str] | None = None
|
|
1428
|
+
parent_session_token: Token[str] | None = None
|
|
1429
|
+
total_counter_incremented = False
|
|
1430
|
+
agent_counter_incremented = False
|
|
1431
|
+
parent_event_call_id = ""
|
|
1432
|
+
parent_provider_call_id = ""
|
|
1433
|
+
parent_call_id = ""
|
|
1434
|
+
hook_status = "failed"
|
|
1435
|
+
hook_result_summary = ""
|
|
1436
|
+
sub_agent_hook_started = False
|
|
1437
|
+
approval_mw: ApprovalMiddleware | None = None
|
|
1438
|
+
controller: SubAgentToolShell | None = None
|
|
1439
|
+
policy: KernelSubAgentPolicy | None = None
|
|
1440
|
+
sub_event_mw: SubAgentEventMiddleware | None = None
|
|
1441
|
+
sleep_mw: SleepMiddleware | None = None
|
|
1442
|
+
log_stats = SubAgentLogStats()
|
|
1443
|
+
log_writer: SubAgentSessionLogWriter | None = None
|
|
1444
|
+
parent_result_metadata = None
|
|
1445
|
+
sub_agent_trace: SubAgentTrace | None = None
|
|
1446
|
+
sub_agent_failure_code: str | None = None
|
|
1447
|
+
conversation: Conversation | None = None
|
|
1448
|
+
operation = OperationLifetime()
|
|
1449
|
+
unbind: Unbind | None = None
|
|
1450
|
+
try:
|
|
1451
|
+
self._total_active += 1
|
|
1452
|
+
total_counter_incremented = True
|
|
1453
|
+
self._agent_active[tool_name] = per_active + 1
|
|
1454
|
+
agent_counter_incremented = True
|
|
1455
|
+
invocation_id = uuid4().hex[:12]
|
|
1456
|
+
origin = InvocationOrigin(
|
|
1457
|
+
"sub_agent", self._session_id or "", invocation_id, current_invocation_origin.get()
|
|
1458
|
+
)
|
|
1459
|
+
emitter = BoundEmitter(self._bus, origin)
|
|
1460
|
+
self._invocation_emitters[invocation_id] = emitter
|
|
1461
|
+
intermediate_buffer = IntermediateTextBuffer()
|
|
1462
|
+
self._intermediate_buffers[invocation_id] = intermediate_buffer
|
|
1463
|
+
invocation_session_id = _derive_invocation_session_id(invocation_id)
|
|
1464
|
+
invocation_token = invocation_routing_key.set(invocation_id)
|
|
1465
|
+
# In-process: the child's events inline into this writer under
|
|
1466
|
+
# the child's actor, hung off this boundary.
|
|
1467
|
+
sub_agent_trace = SubAgentTrace.open(invocation_id=invocation_id, trace_coverage=TraceCoverage.FULL)
|
|
1468
|
+
|
|
1469
|
+
async def _open_operation(owner: Conversation) -> Conversation:
|
|
1470
|
+
return owner
|
|
1471
|
+
|
|
1472
|
+
controller = SubAgentToolShell(
|
|
1473
|
+
origin=origin,
|
|
1474
|
+
tool_name=tool_name,
|
|
1475
|
+
agent_name=self._display_names.get(tool_name, tool_name),
|
|
1476
|
+
event_bus=self._bus,
|
|
1477
|
+
operation=operation,
|
|
1478
|
+
human_failure_decisions=self._human_failure_decisions,
|
|
1479
|
+
)
|
|
1480
|
+
conversation = await prepared.open(_open_operation)
|
|
1481
|
+
unbind = conversation.bind_operation(controller)
|
|
1482
|
+
if sub_agent_trace is not None:
|
|
1483
|
+
await sub_agent_trace.started(
|
|
1484
|
+
tool_name=tool_name, agent_profile=self._hook_profile_names.get(tool_name, tool_name)
|
|
1485
|
+
)
|
|
1486
|
+
route_session_token = (
|
|
1487
|
+
llm_route_session_id.set(invocation_session_id) if invocation_session_id is not None else None
|
|
1488
|
+
)
|
|
1489
|
+
parent_session_token = llm_parent_session_id.set(self._session_id or "")
|
|
1490
|
+
parent_result_metadata = sub_agent_parent_result_metadata.get()
|
|
1491
|
+
if parent_result_metadata is not None:
|
|
1492
|
+
parent_event_call_id = parent_result_metadata.parent_event_call_id
|
|
1493
|
+
parent_provider_call_id = parent_result_metadata.parent_provider_call_id
|
|
1494
|
+
parent_result_metadata.sub_agent_invocation_id = invocation_id
|
|
1495
|
+
else:
|
|
1496
|
+
parent_event_call_id = sub_agent_parent_call_id.get()
|
|
1497
|
+
parent_call_id = parent_event_call_id
|
|
1498
|
+
if self._session_dir is not None:
|
|
1499
|
+
log_writer = SubAgentSessionLogWriter(
|
|
1500
|
+
parent_session_dir=self._session_dir,
|
|
1501
|
+
parent_session_id=self._session_id,
|
|
1502
|
+
invocation_id=invocation_id,
|
|
1503
|
+
tool_name=tool_name,
|
|
1504
|
+
agent_profile=self._hook_profile_names.get(tool_name, tool_name),
|
|
1505
|
+
agent_display_name=self._display_names.get(tool_name, tool_name),
|
|
1506
|
+
prompt=prompt,
|
|
1507
|
+
parent_provider_call_id=parent_provider_call_id,
|
|
1508
|
+
parent_event_call_id=parent_event_call_id,
|
|
1509
|
+
model=model_provenance,
|
|
1510
|
+
primary_cwd=self._workspace_cwd,
|
|
1511
|
+
working_dirs=list(
|
|
1512
|
+
self._workspace_roots or ([self._workspace_cwd] if self._workspace_cwd else [])
|
|
1513
|
+
),
|
|
1514
|
+
stats=log_stats,
|
|
1515
|
+
)
|
|
1516
|
+
if log_writer is not None:
|
|
1517
|
+
initial_log_written = await log_writer.write(status="running")
|
|
1518
|
+
if initial_log_written and parent_result_metadata is not None:
|
|
1519
|
+
parent_result_metadata.sub_agent_log_file = log_writer.log_file_name
|
|
1520
|
+
# Sub-agents are single-turn and do not carry parent usage
|
|
1521
|
+
# or profile-switch context; this snapshots runtime only.
|
|
1522
|
+
if skills_provider is not None:
|
|
1523
|
+
await self._refresh_skill_catalog(skills_provider, tool_name)
|
|
1524
|
+
|
|
1525
|
+
invocation_runtime = runtime_factory(conversation, invocation_id)
|
|
1526
|
+
invocation_agent = invocation_runtime.agent
|
|
1527
|
+
invocation_ctx = invocation_runtime.context
|
|
1528
|
+
invocation_reminder = invocation_runtime.reminder
|
|
1529
|
+
invocation_reminder.prepare_turn()
|
|
1530
|
+
# Publish the invocation-start event BEFORE any LLM call so
|
|
1531
|
+
# the TUI links this invocation_id to the parent tool call's
|
|
1532
|
+
# widget deterministically — even if the first LLM call
|
|
1533
|
+
# fails immediately (e.g. 529 overloaded) and no inner tool
|
|
1534
|
+
# call arrives for a while. See
|
|
1535
|
+
# :class:`InvocationStarted` docstring.
|
|
1536
|
+
if self._bus is not None:
|
|
1537
|
+
await emitter.publish(
|
|
1538
|
+
InvocationStarted(
|
|
1539
|
+
origin=emitter.origin,
|
|
1540
|
+
agent_name=self._display_names.get(tool_name, tool_name),
|
|
1541
|
+
tool_name=tool_name,
|
|
1542
|
+
parent_call_id=parent_call_id,
|
|
1543
|
+
sub_agent_log_file=log_writer.log_file_name if log_writer is not None else "",
|
|
1544
|
+
session_id=self._session_id,
|
|
1545
|
+
)
|
|
1546
|
+
)
|
|
1547
|
+
await self._fire_sub_agent_hook(
|
|
1548
|
+
HookEvent.SUB_AGENT_START,
|
|
1549
|
+
tool_name=tool_name,
|
|
1550
|
+
invocation_id=invocation_id,
|
|
1551
|
+
parent_call_id=parent_call_id,
|
|
1552
|
+
parent_provider_call_id=parent_provider_call_id,
|
|
1553
|
+
target_operation_id=sub_agent_trace.operation_id if sub_agent_trace is not None else None,
|
|
1554
|
+
)
|
|
1555
|
+
sub_agent_hook_started = True
|
|
1556
|
+
|
|
1557
|
+
middleware = []
|
|
1558
|
+
loop_recorder = LoopRecorder(
|
|
1559
|
+
capture_service_loop_messages=capture_service_loop_messages,
|
|
1560
|
+
message_hasher=serialized_message_payload,
|
|
1561
|
+
)
|
|
1562
|
+
if self._bus is not None:
|
|
1563
|
+
sub_event_mw = SubAgentEventMiddleware(
|
|
1564
|
+
self._bus,
|
|
1565
|
+
self._display_names.get(tool_name, tool_name),
|
|
1566
|
+
invocation_id,
|
|
1567
|
+
origin=origin,
|
|
1568
|
+
mutation_tracker=self._mutation_tracker,
|
|
1569
|
+
mutation_coordinator=self._mutation_coordinator,
|
|
1570
|
+
hook_manager=self._hook_manager,
|
|
1571
|
+
profile_name=self._hook_profile_names.get(tool_name, tool_name),
|
|
1572
|
+
session_id=self._session_id,
|
|
1573
|
+
workspace_cwd=self._workspace_cwd,
|
|
1574
|
+
serialize_implicit_windows=self._serialize_implicit_windows,
|
|
1575
|
+
stats=log_stats,
|
|
1576
|
+
tool_result_ceiling_tokens=self._tool_result_ceiling_tokens,
|
|
1577
|
+
intermediate_buffer=intermediate_buffer,
|
|
1578
|
+
)
|
|
1579
|
+
middleware.append(sub_event_mw)
|
|
1580
|
+
# AskUser middleware — sub-agent ask_user calls show the
|
|
1581
|
+
# same dialog as the main agent via the shared EventBus.
|
|
1582
|
+
middleware.append(
|
|
1583
|
+
create_ask_user(
|
|
1584
|
+
conversation,
|
|
1585
|
+
AskUserInputs(
|
|
1586
|
+
event_bus=self._bus,
|
|
1587
|
+
session_id=self._session_id,
|
|
1588
|
+
caller_name=self._display_names.get(tool_name, tool_name),
|
|
1589
|
+
timeout_seconds=self._ask_user_timeout_seconds,
|
|
1590
|
+
),
|
|
1591
|
+
)
|
|
1592
|
+
)
|
|
1593
|
+
# Approval middleware — gates sub-agent tool calls with
|
|
1594
|
+
# user approval via the same EventBus flow as the main agent.
|
|
1595
|
+
# Reads current approval_mode/judge off self so mid-session
|
|
1596
|
+
# mode switches (MANUAL ↔ AUTO ↔ BYPASS) take effect on the
|
|
1597
|
+
# next sub-agent invocation.
|
|
1598
|
+
approval_policy = self._approval_policies.get(tool_name)
|
|
1599
|
+
if approval_policy is not None:
|
|
1600
|
+
approval_mw = await create_approval(
|
|
1601
|
+
conversation,
|
|
1602
|
+
ApprovalInputs(
|
|
1603
|
+
approval_policy=approval_policy,
|
|
1604
|
+
event_bus=self._bus,
|
|
1605
|
+
session_id=self._session_id,
|
|
1606
|
+
tool_kinds=self._tool_kinds.get(tool_name),
|
|
1607
|
+
caller_name=self._display_names.get(tool_name, tool_name),
|
|
1608
|
+
workspace_roots=self._workspace_roots,
|
|
1609
|
+
approval_mode=self._approval_mode,
|
|
1610
|
+
approval_judge=self._approval_judge,
|
|
1611
|
+
approval_log_dir=self._approval_log_dir,
|
|
1612
|
+
workspace_cwd=self._workspace_cwd,
|
|
1613
|
+
# Developer-mode handoff review is scoped to main-agent → sub-agent delegation.
|
|
1614
|
+
dev_mode=False,
|
|
1615
|
+
hook_manager=self._hook_manager,
|
|
1616
|
+
profile_name=self._hook_profile_names.get(tool_name, tool_name),
|
|
1617
|
+
session_archive_read_roots=(
|
|
1618
|
+
[self._session_dir / COMPACTIONS_DIR_NAME] if self._session_dir else None
|
|
1619
|
+
),
|
|
1620
|
+
turn_context=self._turn_context,
|
|
1621
|
+
),
|
|
1622
|
+
)
|
|
1623
|
+
approval_mw.bind_publisher(emitter)
|
|
1624
|
+
middleware.append(approval_mw)
|
|
1625
|
+
self._live_approvals.append(approval_mw)
|
|
1626
|
+
sleep_mw = SleepMiddleware(self._bus, session_id=self._session_id)
|
|
1627
|
+
middleware.append(sleep_mw)
|
|
1628
|
+
if sub_event_mw is None:
|
|
1629
|
+
middleware.append(SubAgentStatsMiddleware(log_stats))
|
|
1630
|
+
# Wire usage callback so token counts flow to TUI via
|
|
1631
|
+
# InvocationProgress. Per-invocation UsageTrackingMiddleware
|
|
1632
|
+
# — adding it to run-scoped middleware gives each concurrent
|
|
1633
|
+
# invocation its own callback state, so parallel sub-agent
|
|
1634
|
+
# runs no longer clobber each other's totals through the
|
|
1635
|
+
# registered tool's shared ``ctx.usage_middleware``. For
|
|
1636
|
+
# registered sub-agents, ``ctx`` is also fresh per invocation,
|
|
1637
|
+
# so calibration/overhead/LAST_WORDS state cannot leak between
|
|
1638
|
+
# same-tool concurrent runs.
|
|
1639
|
+
ctx = invocation_ctx
|
|
1640
|
+
if ctx:
|
|
1641
|
+
parent_cb = self._on_sub_agent_usage
|
|
1642
|
+
total_usage_tokens = 0
|
|
1643
|
+
max_context_tokens = ctx.usage_middleware.max_context_tokens
|
|
1644
|
+
agent_profile_name = self._hook_profile_names.get(tool_name, tool_name)
|
|
1645
|
+
|
|
1646
|
+
# Positional args mirror ``UsageTrackingMiddleware._fire_callback``;
|
|
1647
|
+
# if that callback grows a new arg, add it here too or the
|
|
1648
|
+
# parent accumulator won't see it (``*_rest`` will swallow it).
|
|
1649
|
+
def _on_usage(
|
|
1650
|
+
total,
|
|
1651
|
+
inp=0,
|
|
1652
|
+
out=0,
|
|
1653
|
+
local=0,
|
|
1654
|
+
ratio=1.0,
|
|
1655
|
+
overhead=0,
|
|
1656
|
+
cache_hit=None,
|
|
1657
|
+
calibration_initialized=False,
|
|
1658
|
+
use_local_context_estimate=False,
|
|
1659
|
+
*_rest,
|
|
1660
|
+
_mw=sub_event_mw,
|
|
1661
|
+
_cb=parent_cb,
|
|
1662
|
+
_max_context_tokens=max_context_tokens,
|
|
1663
|
+
_agent_profile=agent_profile_name,
|
|
1664
|
+
_usage_source_id=invocation_id,
|
|
1665
|
+
):
|
|
1666
|
+
# ``normalize_stream_usage`` collapses provider deltas
|
|
1667
|
+
# into a single per-call snapshot; ``total`` is the
|
|
1668
|
+
# per-call cost, so we
|
|
1669
|
+
# can accumulate it directly. An ``(inp + out) or total``
|
|
1670
|
+
# fallback would double-count on any future provider
|
|
1671
|
+
# that reports ``total`` without an input/output split.
|
|
1672
|
+
nonlocal total_usage_tokens
|
|
1673
|
+
total_usage_tokens += total
|
|
1674
|
+
log_stats.record_usage(total, total_usage_tokens)
|
|
1675
|
+
if _mw is not None:
|
|
1676
|
+
_mw.record_usage(total, total_usage_tokens)
|
|
1677
|
+
if _cb is not None:
|
|
1678
|
+
_cb(
|
|
1679
|
+
total,
|
|
1680
|
+
inp,
|
|
1681
|
+
out,
|
|
1682
|
+
local,
|
|
1683
|
+
ratio,
|
|
1684
|
+
overhead,
|
|
1685
|
+
cache_hit,
|
|
1686
|
+
_max_context_tokens,
|
|
1687
|
+
_agent_profile,
|
|
1688
|
+
_usage_source_id,
|
|
1689
|
+
None,
|
|
1690
|
+
use_local_context_estimate,
|
|
1691
|
+
)
|
|
1692
|
+
|
|
1693
|
+
per_invocation_usage = UsageTrackingMiddleware(
|
|
1694
|
+
max_context_tokens=max_context_tokens,
|
|
1695
|
+
warn_threshold_pct=ctx.usage_middleware.warn_threshold_pct,
|
|
1696
|
+
on_usage=_on_usage,
|
|
1697
|
+
compaction_strategy=ctx.compaction_strategy,
|
|
1698
|
+
use_local_context_estimate_for_hosted_usage=(ctx.use_local_context_estimate_for_hosted_usage),
|
|
1699
|
+
)
|
|
1700
|
+
middleware.append(per_invocation_usage)
|
|
1701
|
+
|
|
1702
|
+
# Response-validation middleware — appended AFTER all other
|
|
1703
|
+
# per-invocation middleware so it ends up innermost in the
|
|
1704
|
+
# chat pipeline (closest to ``_inner_get_response``). Run-
|
|
1705
|
+
# scoped middleware is concatenated AFTER the agent's own
|
|
1706
|
+
# chat middleware, so listing it last here means it sits
|
|
1707
|
+
# last overall. Retries are therefore invisible to outer
|
|
1708
|
+
# instrumentation — the per-invocation usage middleware
|
|
1709
|
+
# above, and the agent-level system_reminder / ctx.middleware
|
|
1710
|
+
# — which each fire exactly once per turn.
|
|
1711
|
+
# ``publish_retry`` surfaces client-mode validation retries
|
|
1712
|
+
# as ``InvocationRetryAttempt`` keyed to this invocation;
|
|
1713
|
+
# service-side failures raise to the whole-run retry loop,
|
|
1714
|
+
# which publishes its own retry events.
|
|
1715
|
+
if self._bus is not None:
|
|
1716
|
+
|
|
1717
|
+
async def _publish_validation_retry(info: RetryAttemptInfo) -> None:
|
|
1718
|
+
await emitter.publish(
|
|
1719
|
+
InvocationRetryAttempt(
|
|
1720
|
+
scope="wire",
|
|
1721
|
+
origin=emitter.origin,
|
|
1722
|
+
agent_name=self._display_names.get(tool_name, tool_name),
|
|
1723
|
+
message=f"Invalid response: {info.reason}",
|
|
1724
|
+
attempt=info.attempt,
|
|
1725
|
+
max_attempts=info.max_attempts,
|
|
1726
|
+
delay_seconds=int(info.delay_seconds),
|
|
1727
|
+
session_id=self._session_id,
|
|
1728
|
+
)
|
|
1729
|
+
)
|
|
1730
|
+
|
|
1731
|
+
validation_middleware = create_validation(conversation, publish_retry=_publish_validation_retry)
|
|
1732
|
+
else:
|
|
1733
|
+
validation_middleware = create_validation(conversation, publish_retry=None)
|
|
1734
|
+
middleware.append(validation_middleware)
|
|
1735
|
+
|
|
1736
|
+
run_kwargs: AgentRunKwargs = {}
|
|
1737
|
+
sub_session = invocation_agent.create_session()
|
|
1738
|
+
run_kwargs["session"] = sub_session
|
|
1739
|
+
# Per-invocation channel for the chrys ToolLoopLayer — parallel
|
|
1740
|
+
# sub-agents share one client stack, so the recorder must ride
|
|
1741
|
+
# the run, never the client.
|
|
1742
|
+
run_client_kwargs: dict[str, Any] = {"loop_recorder": loop_recorder}
|
|
1743
|
+
if sub_agent_trace is not None:
|
|
1744
|
+
run_client_kwargs[TRAJECTORY_CONTEXT_KWARG] = sub_agent_trace.child_context
|
|
1745
|
+
run_kwargs["client_kwargs"] = run_client_kwargs
|
|
1746
|
+
if middleware:
|
|
1747
|
+
run_kwargs["middleware"] = middleware
|
|
1748
|
+
invocation_chat_options = self._chat_options_by_tool.get(tool_name)
|
|
1749
|
+
if invocation_chat_options:
|
|
1750
|
+
options_copy = dict(invocation_chat_options)
|
|
1751
|
+
if isinstance(extra_body := options_copy.get("extra_body"), dict):
|
|
1752
|
+
options_copy["extra_body"] = dict(extra_body)
|
|
1753
|
+
run_kwargs["options"] = options_copy
|
|
1754
|
+
if ctx is not None:
|
|
1755
|
+
run_kwargs["compaction_strategy"] = ctx.compaction_strategy
|
|
1756
|
+
# Same wiring as TurnBindings._build_run_kwargs: the raw client
|
|
1757
|
+
# estimates live tool definitions with the strategy's own
|
|
1758
|
+
# tokenizer.
|
|
1759
|
+
run_kwargs["tokenizer"] = ctx.compaction_strategy.tokenizer
|
|
1760
|
+
|
|
1761
|
+
# Controller owns the run → retry → pause → decide loop.
|
|
1762
|
+
# Registered before run() so an early cascade_abort (e.g.
|
|
1763
|
+
# global interrupt fired during concurrent startup) can
|
|
1764
|
+
# reach it even before it enters RUNNING.
|
|
1765
|
+
def _commit_parent_interruption() -> None:
|
|
1766
|
+
_commit_interrupted_parent_result(parent_result_metadata)
|
|
1767
|
+
|
|
1768
|
+
def _record_terminal_audit() -> None:
|
|
1769
|
+
if parent_result_metadata is not None and log_writer is not None:
|
|
1770
|
+
parent_result_metadata.sub_agent_log_file = log_writer.log_file_name
|
|
1771
|
+
parent_result_metadata.sub_agent_audit_complete = True
|
|
1772
|
+
_upgrade_interrupted_parent_result(parent_result_metadata)
|
|
1773
|
+
|
|
1774
|
+
def _begin_hosted_pass() -> None:
|
|
1775
|
+
if sub_event_mw is None:
|
|
1776
|
+
validation_middleware.set_observation_hook(None)
|
|
1777
|
+
return
|
|
1778
|
+
validation_middleware.set_observation_hook(sub_event_mw.begin_hosted_pass())
|
|
1779
|
+
|
|
1780
|
+
retry_kwargs: dict[str, Any] = {}
|
|
1781
|
+
if self._max_transient_retries is not None:
|
|
1782
|
+
retry_kwargs["max_retries"] = self._max_transient_retries
|
|
1783
|
+
if controller is None:
|
|
1784
|
+
raise RuntimeError("The sub-agent invocation has no controller.")
|
|
1785
|
+
controller.bind_parent_interrupt_commit(_commit_parent_interruption)
|
|
1786
|
+
policy = KernelSubAgentPolicy(
|
|
1787
|
+
shell=controller,
|
|
1788
|
+
conversation=invocation_runtime.owner,
|
|
1789
|
+
agent=invocation_agent,
|
|
1790
|
+
session=sub_session,
|
|
1791
|
+
loop_recorder=loop_recorder,
|
|
1792
|
+
prompt=prompt,
|
|
1793
|
+
run_kwargs=run_kwargs,
|
|
1794
|
+
# Persist the parent assistant function_call's
|
|
1795
|
+
# call_id so session-reload recovery can re-pair a
|
|
1796
|
+
# paused record to its dangling function_call by id
|
|
1797
|
+
# instead of by tool_name + appearance order.
|
|
1798
|
+
parent_call_id=parent_provider_call_id,
|
|
1799
|
+
parent_event_call_id=parent_event_call_id,
|
|
1800
|
+
sub_agent_log_file=log_writer.log_file_name if log_writer is not None and log_writer.active else "",
|
|
1801
|
+
persist_dir=self._pending_dir(),
|
|
1802
|
+
log_writer=log_writer,
|
|
1803
|
+
log_stats=log_stats,
|
|
1804
|
+
terminal_audit_callback=_record_terminal_audit,
|
|
1805
|
+
pending_record_finalizer=self.queue_pending_cleanup,
|
|
1806
|
+
tool_event_middleware=sub_event_mw,
|
|
1807
|
+
stream=stream,
|
|
1808
|
+
stream_attempt_timeout=stream_attempt_timeout,
|
|
1809
|
+
sleep_middleware=sleep_mw,
|
|
1810
|
+
pass_start_hooks=(
|
|
1811
|
+
*((begin_web_pass,) if begin_web_pass is not None else ()),
|
|
1812
|
+
_begin_hosted_pass,
|
|
1813
|
+
validation_middleware.reset_service_retry_state,
|
|
1814
|
+
),
|
|
1815
|
+
hosted_commits_probe=validation_middleware.hosted_commits_observed,
|
|
1816
|
+
begin_hosted_baseline=validation_middleware.begin_pass_hosted_baseline,
|
|
1817
|
+
trajectory_context=sub_agent_trace.child_context if sub_agent_trace is not None else None,
|
|
1818
|
+
trajectory_boundary_operation_id=(
|
|
1819
|
+
sub_agent_trace.operation_id if sub_agent_trace is not None else None
|
|
1820
|
+
),
|
|
1821
|
+
**retry_kwargs,
|
|
1822
|
+
)
|
|
1823
|
+
controller.attach_policy(policy)
|
|
1824
|
+
if not service_storage_side(invocation_agent.client, run_kwargs.get("options")):
|
|
1825
|
+
# Look up callbacks at retry time, not while constructing
|
|
1826
|
+
# the adapter.
|
|
1827
|
+
def _wire_sleep(seconds: int) -> Awaitable[bool]:
|
|
1828
|
+
return policy.sleep_for_wire_retry(seconds)
|
|
1829
|
+
|
|
1830
|
+
def _wire_notice(
|
|
1831
|
+
message: str, attempt: int, maximum: int, delay: int, exc: BaseException
|
|
1832
|
+
) -> Awaitable[None]:
|
|
1833
|
+
return policy.publish_wire_retry_attempt(message, attempt, maximum, delay, exc)
|
|
1834
|
+
|
|
1835
|
+
run_client_kwargs["wire_retry_policy"] = WireRecipe(
|
|
1836
|
+
max_retries=lambda: policy.max_retries,
|
|
1837
|
+
stall_timeout_seconds=stream_attempt_timeout,
|
|
1838
|
+
stall_max_retries=lambda: policy.max_retries,
|
|
1839
|
+
stall_exhausted_action=StallExhaustedAction.RAISE,
|
|
1840
|
+
backoff_schedule=lambda: policy.backoff_schedule,
|
|
1841
|
+
interrupted=lambda: controller.cascade_requested,
|
|
1842
|
+
interruptible_sleep=_wire_sleep,
|
|
1843
|
+
publish_retry=_wire_notice,
|
|
1844
|
+
prepare_retry=None,
|
|
1845
|
+
hosted_commits_in_flight=validation_middleware.hosted_commits_in_flight,
|
|
1846
|
+
).build()
|
|
1847
|
+
run_client_kwargs["continuation_token_observer"] = policy.observe_continuation_token
|
|
1848
|
+
self._controllers[invocation_id] = controller
|
|
1849
|
+
with (
|
|
1850
|
+
trajectory_scope(sub_agent_trace.child_context)
|
|
1851
|
+
if sub_agent_trace is not None
|
|
1852
|
+
else contextlib.nullcontext()
|
|
1853
|
+
):
|
|
1854
|
+
result = await controller.run()
|
|
1855
|
+
segment = policy.final_segment
|
|
1856
|
+
if parent_result_metadata is not None and segment is not None and segment.transcript != segment.result:
|
|
1857
|
+
parent_result_metadata.transcript_final_text = segment.transcript
|
|
1858
|
+
hook_status = "failed" if result.startswith("Error:") else "ok"
|
|
1859
|
+
hook_result_summary = result[:500]
|
|
1860
|
+
if hook_status == "failed":
|
|
1861
|
+
sub_agent_failure_code = "tool_error"
|
|
1862
|
+
return result
|
|
1863
|
+
except asyncio.CancelledError as exc:
|
|
1864
|
+
hook_status = "cancelled"
|
|
1865
|
+
hook_result_summary = "cancelled"
|
|
1866
|
+
_commit_interrupted_parent_result(parent_result_metadata)
|
|
1867
|
+
if policy is not None:
|
|
1868
|
+
if controller is None:
|
|
1869
|
+
raise RuntimeError("The sub-agent invocation has no controller.") from exc
|
|
1870
|
+
await _finish_cancellation_cleanup(controller.finalize_cancellation())
|
|
1871
|
+
elif log_writer is not None:
|
|
1872
|
+
with contextlib.suppress(Exception):
|
|
1873
|
+
terminal_written = await log_writer.write(
|
|
1874
|
+
status="cancelled", result="cancelled", last_error="cancelled", ended=True
|
|
1875
|
+
)
|
|
1876
|
+
if terminal_written and parent_result_metadata is not None:
|
|
1877
|
+
parent_result_metadata.sub_agent_log_file = log_writer.log_file_name
|
|
1878
|
+
parent_result_metadata.sub_agent_audit_complete = True
|
|
1879
|
+
_upgrade_interrupted_parent_result(parent_result_metadata)
|
|
1880
|
+
raise
|
|
1881
|
+
except BaseException as exc:
|
|
1882
|
+
hook_status = "failed"
|
|
1883
|
+
hook_result_summary = str(exc)[:500]
|
|
1884
|
+
sub_agent_failure_code = "setup_failed" if policy is None else "exception"
|
|
1885
|
+
if policy is None and log_writer is not None:
|
|
1886
|
+
with contextlib.suppress(Exception):
|
|
1887
|
+
terminal_written = await log_writer.write(
|
|
1888
|
+
status="setup_failed",
|
|
1889
|
+
result=hook_result_summary,
|
|
1890
|
+
failure_reason="setup_failed",
|
|
1891
|
+
last_error=hook_result_summary,
|
|
1892
|
+
ended=True,
|
|
1893
|
+
)
|
|
1894
|
+
if terminal_written and parent_result_metadata is not None:
|
|
1895
|
+
parent_result_metadata.sub_agent_log_file = log_writer.log_file_name
|
|
1896
|
+
parent_result_metadata.sub_agent_audit_complete = True
|
|
1897
|
+
_upgrade_interrupted_parent_result(parent_result_metadata)
|
|
1898
|
+
raise
|
|
1899
|
+
finally:
|
|
1900
|
+
operation.begin_cleanup()
|
|
1901
|
+
cleanup_cancelled = False
|
|
1902
|
+
_close_sub_agent_trace(
|
|
1903
|
+
sub_agent_trace,
|
|
1904
|
+
hook_status=hook_status,
|
|
1905
|
+
failure_code=sub_agent_failure_code,
|
|
1906
|
+
controller=controller if policy is not None else None,
|
|
1907
|
+
failure_reason=policy.failure_reason if policy is not None else None,
|
|
1908
|
+
)
|
|
1909
|
+
if sub_event_mw is not None:
|
|
1910
|
+
try:
|
|
1911
|
+
await sub_event_mw.flush_progress()
|
|
1912
|
+
except asyncio.CancelledError:
|
|
1913
|
+
cleanup_cancelled = True
|
|
1914
|
+
except Exception:
|
|
1915
|
+
logger.debug("sub-agent progress flush failed for invocation %s", invocation_id, exc_info=True)
|
|
1916
|
+
if self._drain_parent_usage_publishes is not None:
|
|
1917
|
+
try:
|
|
1918
|
+
await self._drain_parent_usage_publishes()
|
|
1919
|
+
except asyncio.CancelledError:
|
|
1920
|
+
cleanup_cancelled = True
|
|
1921
|
+
except Exception:
|
|
1922
|
+
logger.debug(
|
|
1923
|
+
"parent usage publish drain failed for invocation %s", invocation_id, exc_info=True
|
|
1924
|
+
)
|
|
1925
|
+
if sub_agent_hook_started:
|
|
1926
|
+
try:
|
|
1927
|
+
await self._fire_sub_agent_hook(
|
|
1928
|
+
HookEvent.SUB_AGENT_END,
|
|
1929
|
+
tool_name=tool_name,
|
|
1930
|
+
invocation_id=invocation_id,
|
|
1931
|
+
parent_call_id=parent_call_id,
|
|
1932
|
+
parent_provider_call_id=parent_provider_call_id,
|
|
1933
|
+
status=hook_status,
|
|
1934
|
+
result_summary=hook_result_summary,
|
|
1935
|
+
target_operation_id=(sub_agent_trace.operation_id if sub_agent_trace is not None else None),
|
|
1936
|
+
)
|
|
1937
|
+
except asyncio.CancelledError:
|
|
1938
|
+
cleanup_cancelled = True
|
|
1939
|
+
logger.debug("sub-agent end hook cancelled for invocation %s", invocation_id)
|
|
1940
|
+
except Exception:
|
|
1941
|
+
logger.warning("sub-agent end hook failed for invocation %s", invocation_id, exc_info=True)
|
|
1942
|
+
if invocation_id:
|
|
1943
|
+
self._controllers.pop(invocation_id, None)
|
|
1944
|
+
self._intermediate_buffers.pop(invocation_id, None)
|
|
1945
|
+
self._invocation_emitters.pop(invocation_id, None)
|
|
1946
|
+
if total_counter_incremented:
|
|
1947
|
+
self._total_active -= 1
|
|
1948
|
+
if agent_counter_incremented:
|
|
1949
|
+
# ``register()`` seeds ``_agent_active[tool_name] = 0`` and
|
|
1950
|
+
# the enclosing ``try`` already bumped it by 1, so direct
|
|
1951
|
+
# subscript access is correct here. A KeyError would be a
|
|
1952
|
+
# genuine bug (tool invoked without registration) — surface
|
|
1953
|
+
# it rather than silently masking.
|
|
1954
|
+
try:
|
|
1955
|
+
self._agent_active[tool_name] -= 1
|
|
1956
|
+
except KeyError:
|
|
1957
|
+
logger.error(
|
|
1958
|
+
"sub-agent counter underflow: tool %r not in _agent_active (invocation %s)",
|
|
1959
|
+
tool_name,
|
|
1960
|
+
invocation_id,
|
|
1961
|
+
)
|
|
1962
|
+
if route_session_token is not None:
|
|
1963
|
+
llm_route_session_id.reset(route_session_token)
|
|
1964
|
+
if parent_session_token is not None:
|
|
1965
|
+
llm_parent_session_id.reset(parent_session_token)
|
|
1966
|
+
if invocation_token is not None:
|
|
1967
|
+
invocation_routing_key.reset(invocation_token)
|
|
1968
|
+
if unbind is not None:
|
|
1969
|
+
unbind()
|
|
1970
|
+
if approval_mw is not None:
|
|
1971
|
+
try:
|
|
1972
|
+
if conversation is None:
|
|
1973
|
+
raise RuntimeError("Closing sub-agent approvals requires their owning conversation.")
|
|
1974
|
+
await conversation.release(approval_mw.close)
|
|
1975
|
+
except asyncio.CancelledError:
|
|
1976
|
+
cleanup_cancelled = True
|
|
1977
|
+
except Exception:
|
|
1978
|
+
logger.debug("approval middleware close failed for invocation %s", invocation_id, exc_info=True)
|
|
1979
|
+
with contextlib.suppress(ValueError):
|
|
1980
|
+
self._live_approvals.remove(approval_mw)
|
|
1981
|
+
operation.finish()
|
|
1982
|
+
if conversation is not None:
|
|
1983
|
+
try:
|
|
1984
|
+
await conversation.aclose()
|
|
1985
|
+
except asyncio.CancelledError:
|
|
1986
|
+
cleanup_cancelled = True
|
|
1987
|
+
if cleanup_cancelled:
|
|
1988
|
+
raise asyncio.CancelledError
|
|
1989
|
+
|
|
1990
|
+
sub_agent_tool = FunctionTool(
|
|
1991
|
+
func=_invoke,
|
|
1992
|
+
name=tool_name,
|
|
1993
|
+
description=description,
|
|
1994
|
+
)
|
|
1995
|
+
set_tool_kind(sub_agent_tool, KIND_SUB_AGENT)
|
|
1996
|
+
return sub_agent_tool
|
|
1997
|
+
|
|
1998
|
+
async def _fire_sub_agent_hook(
|
|
1999
|
+
self,
|
|
2000
|
+
event: HookEvent,
|
|
2001
|
+
*,
|
|
2002
|
+
tool_name: str,
|
|
2003
|
+
invocation_id: str,
|
|
2004
|
+
parent_call_id: str,
|
|
2005
|
+
parent_provider_call_id: str = "",
|
|
2006
|
+
status: str = "",
|
|
2007
|
+
result_summary: str = "",
|
|
2008
|
+
target_operation_id: str | None = None,
|
|
2009
|
+
) -> None:
|
|
2010
|
+
if self._hook_manager is None or not self._hook_manager.has_hooks_for(event):
|
|
2011
|
+
return
|
|
2012
|
+
payload: dict[str, Any] = {
|
|
2013
|
+
"session_id": self._session_id,
|
|
2014
|
+
"profile": self._hook_profile_names.get(tool_name, tool_name),
|
|
2015
|
+
"cwd": self._workspace_cwd,
|
|
2016
|
+
"sub_agent": {
|
|
2017
|
+
"name": self._display_names.get(tool_name, tool_name),
|
|
2018
|
+
"tool_name": tool_name,
|
|
2019
|
+
"invocation_id": invocation_id,
|
|
2020
|
+
"parent_call_id": parent_call_id,
|
|
2021
|
+
"parent_provider_call_id": parent_provider_call_id,
|
|
2022
|
+
},
|
|
2023
|
+
}
|
|
2024
|
+
if event == HookEvent.SUB_AGENT_END:
|
|
2025
|
+
payload["status"] = status
|
|
2026
|
+
payload["result_summary"] = result_summary
|
|
2027
|
+
await self._hook_manager.fire(event, payload, target_operation_id=target_operation_id)
|
|
2028
|
+
|
|
2029
|
+
async def _fire_pre_compact_hook(self, tool_name: str, info: PreCompactInfo) -> None:
|
|
2030
|
+
if self._hook_manager is None:
|
|
2031
|
+
return
|
|
2032
|
+
if not self._hook_manager.has_hooks_for(HookEvent.PRE_COMPACT):
|
|
2033
|
+
return
|
|
2034
|
+
await self._hook_manager.fire(
|
|
2035
|
+
HookEvent.PRE_COMPACT,
|
|
2036
|
+
{
|
|
2037
|
+
"session_id": self._session_id,
|
|
2038
|
+
"profile": self._hook_profile_names.get(tool_name, tool_name),
|
|
2039
|
+
"cwd": self._workspace_cwd,
|
|
2040
|
+
"trigger": info.trigger,
|
|
2041
|
+
"usage_pct": info.usage_pct,
|
|
2042
|
+
"tokens_before": info.tokens_before,
|
|
2043
|
+
"sub_agent": {
|
|
2044
|
+
"name": self._display_names.get(tool_name, tool_name),
|
|
2045
|
+
"tool_name": tool_name,
|
|
2046
|
+
},
|
|
2047
|
+
},
|
|
2048
|
+
target_operation_id=info.trajectory_operation_id,
|
|
2049
|
+
)
|
|
2050
|
+
|
|
2051
|
+
async def _publish_sub_agent_compaction_status(
|
|
2052
|
+
self,
|
|
2053
|
+
status: CompactionStatus,
|
|
2054
|
+
tool_name: str,
|
|
2055
|
+
emitter: BoundEmitter,
|
|
2056
|
+
) -> None:
|
|
2057
|
+
"""Publish a Phase-4 compaction UX signal scoped to one invocation.
|
|
2058
|
+
|
|
2059
|
+
Callers guard bus/invocation-id availability; this only translates
|
|
2060
|
+
the generator's :class:`CompactionStatus` into the sub-agent
|
|
2061
|
+
started/finished bus events (note text deliberately not forwarded —
|
|
2062
|
+
sub-agent cards only flip a status line).
|
|
2063
|
+
"""
|
|
2064
|
+
bus = self._bus
|
|
2065
|
+
if bus is None:
|
|
2066
|
+
return
|
|
2067
|
+
agent_name = self._display_names.get(tool_name, tool_name)
|
|
2068
|
+
if status.stage == "started":
|
|
2069
|
+
await emitter.publish(
|
|
2070
|
+
InvocationCompactionStarted(
|
|
2071
|
+
origin=emitter.origin,
|
|
2072
|
+
agent_name=agent_name,
|
|
2073
|
+
compaction_id=status.compaction_id,
|
|
2074
|
+
session_id=self._session_id,
|
|
2075
|
+
)
|
|
2076
|
+
)
|
|
2077
|
+
return
|
|
2078
|
+
if status.stage == "committed":
|
|
2079
|
+
# Post-commit confirmation — a distinct event, NOT a second
|
|
2080
|
+
# finished signal: the card's compaction line already flipped on
|
|
2081
|
+
# finished-ok, this only drives the "Compactions: N" counter.
|
|
2082
|
+
await emitter.publish(
|
|
2083
|
+
InvocationCompactionCommitted(
|
|
2084
|
+
origin=emitter.origin,
|
|
2085
|
+
agent_name=agent_name,
|
|
2086
|
+
compaction_id=status.compaction_id,
|
|
2087
|
+
session_id=self._session_id,
|
|
2088
|
+
)
|
|
2089
|
+
)
|
|
2090
|
+
return
|
|
2091
|
+
await emitter.publish(
|
|
2092
|
+
InvocationCompactionFinished(
|
|
2093
|
+
origin=emitter.origin,
|
|
2094
|
+
agent_name=agent_name,
|
|
2095
|
+
compaction_id=status.compaction_id,
|
|
2096
|
+
outcome=status.outcome,
|
|
2097
|
+
duration_ms=status.duration_ms,
|
|
2098
|
+
format_violation=status.format_violation,
|
|
2099
|
+
failure_reason=status.failure_reason,
|
|
2100
|
+
session_id=self._session_id,
|
|
2101
|
+
)
|
|
2102
|
+
)
|
|
2103
|
+
|
|
2104
|
+
def get_tools(self) -> list[FunctionTool]:
|
|
2105
|
+
"""Return all registered sub-agent tools."""
|
|
2106
|
+
return list(self._tools)
|
|
2107
|
+
|
|
2108
|
+
def tool_names(self) -> list[str]:
|
|
2109
|
+
"""Return the tool names of all registered sub-agents."""
|
|
2110
|
+
return [tool.name for tool in self._tools]
|
|
2111
|
+
|
|
2112
|
+
# -- Per-invocation dispatch (used by engine event handlers) ------
|
|
2113
|
+
|
|
2114
|
+
def _pending_dir(self) -> Path | None:
|
|
2115
|
+
if not self._session_dir:
|
|
2116
|
+
return None
|
|
2117
|
+
return pending_dir(self._session_dir)
|
|
2118
|
+
|
|
2119
|
+
def request_retry(self, invocation_id: str) -> bool:
|
|
2120
|
+
"""Deliver a user-triggered retry to the controller for ``invocation_id``.
|
|
2121
|
+
|
|
2122
|
+
Returns ``True`` if the controller existed and was paused;
|
|
2123
|
+
``False`` if there's no such controller or it wasn't paused
|
|
2124
|
+
(e.g. finished or cascade-aborted between the click and delivery).
|
|
2125
|
+
"""
|
|
2126
|
+
controller = self._controllers.get(invocation_id)
|
|
2127
|
+
if controller is None:
|
|
2128
|
+
logger.info("Sub-agent retry: no controller for invocation_id=%s", invocation_id)
|
|
2129
|
+
return False
|
|
2130
|
+
return controller.request_retry()
|
|
2131
|
+
|
|
2132
|
+
def request_abort(self, invocation_id: str) -> bool:
|
|
2133
|
+
"""Deliver a user-triggered abort to the controller for ``invocation_id``."""
|
|
2134
|
+
controller = self._controllers.get(invocation_id)
|
|
2135
|
+
if controller is None:
|
|
2136
|
+
logger.info("Sub-agent abort: no controller for invocation_id=%s", invocation_id)
|
|
2137
|
+
return False
|
|
2138
|
+
return controller.request_abort()
|
|
2139
|
+
|
|
2140
|
+
def queue_pending_cleanup(self, path: Path | None) -> None:
|
|
2141
|
+
"""Queue a pending control record for post-save cleanup."""
|
|
2142
|
+
if path is not None:
|
|
2143
|
+
self._pending_cleanup_paths.add(path)
|
|
2144
|
+
|
|
2145
|
+
def finalize_pending_cleanups(self) -> None:
|
|
2146
|
+
"""Delete queued pending records after a successful parent session save."""
|
|
2147
|
+
remaining: set[Path] = set()
|
|
2148
|
+
for path in self._pending_cleanup_paths:
|
|
2149
|
+
if secure_unlink_owner_verified(path):
|
|
2150
|
+
continue
|
|
2151
|
+
try:
|
|
2152
|
+
path.lstat()
|
|
2153
|
+
except FileNotFoundError:
|
|
2154
|
+
continue
|
|
2155
|
+
except OSError:
|
|
2156
|
+
pass
|
|
2157
|
+
logger.warning("Failed to delete finalized sub-agent pending record %s", path)
|
|
2158
|
+
remaining.add(path)
|
|
2159
|
+
self._pending_cleanup_paths = remaining
|
|
2160
|
+
|
|
2161
|
+
@staticmethod
|
|
2162
|
+
def _sub_agent_backlinks(parent_messages: list[Message]) -> tuple[set[str], set[str]]:
|
|
2163
|
+
invocation_ids: set[str] = set()
|
|
2164
|
+
log_files: set[str] = set()
|
|
2165
|
+
for message in parent_messages:
|
|
2166
|
+
for content in message.contents:
|
|
2167
|
+
metadata = content.additional_properties.get(TOOL_RESULT_METADATA_KEY)
|
|
2168
|
+
if not isinstance(metadata, dict):
|
|
2169
|
+
continue
|
|
2170
|
+
invocation_id = metadata.get("sub_agent_invocation_id")
|
|
2171
|
+
if isinstance(invocation_id, str) and invocation_id:
|
|
2172
|
+
invocation_ids.add(invocation_id)
|
|
2173
|
+
log_file = metadata.get("sub_agent_log_file")
|
|
2174
|
+
if isinstance(log_file, str) and log_file:
|
|
2175
|
+
log_files.add(log_file)
|
|
2176
|
+
return invocation_ids, log_files
|
|
2177
|
+
|
|
2178
|
+
async def cascade_abort_all(self) -> None:
|
|
2179
|
+
"""Tear down every live sub-agent — used by global :class:`UserInterrupt`.
|
|
2180
|
+
|
|
2181
|
+
Paused controllers resolve via the cascade decision path; running
|
|
2182
|
+
controllers cancel their active attempt directly so long-running
|
|
2183
|
+
inner tool calls stop before the parent task is cancelled.
|
|
2184
|
+
"""
|
|
2185
|
+
controllers = list(self._controllers.values())
|
|
2186
|
+
results = await asyncio.gather(
|
|
2187
|
+
*(controller.cascade_abort() for controller in controllers),
|
|
2188
|
+
return_exceptions=True,
|
|
2189
|
+
)
|
|
2190
|
+
for result in results:
|
|
2191
|
+
if isinstance(result, BaseException):
|
|
2192
|
+
logger.debug("sub-agent cascade abort failed: %s", result)
|
|
2193
|
+
|
|
2194
|
+
@property
|
|
2195
|
+
def approval_mode(self) -> ApprovalMode:
|
|
2196
|
+
"""The mode a fresh invocation constructs its approval middleware with."""
|
|
2197
|
+
return self._approval_mode
|
|
2198
|
+
|
|
2199
|
+
def set_approval_mode(self, mode: ApprovalMode) -> None:
|
|
2200
|
+
"""Update the active approval mode for sub-agent invocations.
|
|
2201
|
+
|
|
2202
|
+
Propagates to **live** ``ApprovalMiddleware`` instances so in-flight
|
|
2203
|
+
sub-agent runs pick up the new mode on their next tool call. New
|
|
2204
|
+
invocations read the stored value at construction time.
|
|
2205
|
+
"""
|
|
2206
|
+
self._approval_mode = mode
|
|
2207
|
+
for mw in list(self._live_approvals):
|
|
2208
|
+
mw.set_approval_mode(mode)
|
|
2209
|
+
|
|
2210
|
+
async def cleanup(self) -> None:
|
|
2211
|
+
"""Shut down all sub-agent instances and disconnect MCP servers."""
|
|
2212
|
+
cancelled: asyncio.CancelledError | None = None
|
|
2213
|
+
for prepared in reversed(tuple(self._prepared_by_tool.values())):
|
|
2214
|
+
try:
|
|
2215
|
+
await prepared.aclose()
|
|
2216
|
+
except asyncio.CancelledError as exc:
|
|
2217
|
+
cancelled = exc
|
|
2218
|
+
self._prepared_by_tool.clear()
|
|
2219
|
+
self._mcp_adapters.clear()
|
|
2220
|
+
self._agents.clear()
|
|
2221
|
+
self._acp_profiles.clear()
|
|
2222
|
+
self._chat_options_by_tool.clear()
|
|
2223
|
+
self._tools.clear()
|
|
2224
|
+
self._agent_active.clear()
|
|
2225
|
+
self._agent_max.clear()
|
|
2226
|
+
self._total_active = 0
|
|
2227
|
+
if cancelled is not None:
|
|
2228
|
+
raise cancelled
|